Skip to main content

speccoder_runtime/
service.rs

1//! The web host's one way into the runtime: every entry point it uses,
2//! re-exported or delegated without changing a result. A write the host
3//! serves goes through [`actions::perform`], like every interface's; a
4//! later feature adds read views here as delegates, never a second write
5//! path. The layering test holds the host to this module.
6
7use std::path::Path;
8
9pub use crate::RunOptions;
10
11/// The action list and the one dispatcher.
12pub mod actions {
13    pub use crate::actions::{Dispatch, Interface, Offer, available, list, offered, perform};
14}
15
16/// The person's own commands, run for them (a fix speccoder printed), and
17/// a terminal at the project.
18pub mod person {
19    pub use crate::person::{Stream, TIMEOUT, open_terminal, run, sandboxed, shown, speccoder};
20}
21
22/// What went wrong, as a block to copy; the setup as a bug report.
23pub mod copy {
24    pub use crate::copy::block;
25    pub use crate::doctor::bug_report;
26}
27
28/// Calls waiting for a person, and who answers them.
29pub mod approvals {
30    pub use crate::approvals::{Answer, Approver, Presentation, Who, list};
31}
32
33/// A run's progress, as it happens and as another process left it.
34pub mod progress {
35    pub use crate::progress::{Event, Progress, read_live};
36}
37
38/// Read views.
39pub mod status {
40    pub use crate::status::report;
41}
42
43/// The next steps a result names.
44pub mod next {
45    pub use crate::next::attach;
46}
47
48/// The person's review of the spec.
49pub mod review {
50    /// The comment tray and the last revision turn.
51    pub use crate::comments::list as comments;
52    pub use crate::review::{readiness, report};
53}
54
55/// Planning a new feature: the rung a description suggests, the
56/// requirements it is close to (read-only, no model).
57pub mod planning {
58    pub use crate::planning_chain::{similar_command, similar_to, suggest};
59}
60
61/// A new project: its plan (read-only), the idea offered once.
62pub mod new_project {
63    pub use crate::new_project::{Creation, creation, first_feature, plan, undo_offer};
64    pub use speccoder_core::new_project::{NewChoices, Stack, slug};
65}
66
67/// What a host outside a project shows.
68pub mod welcome {
69    pub use crate::welcome::{
70        Recent, RecentView, default_parent, folders, forget, is_project, remember, tilde, views,
71    };
72}
73
74/// The bundled sample.
75pub mod sample {
76    pub use crate::sample::{available, copy, home, missing_tool};
77}
78
79/// Needs You.
80pub mod inbox {
81    pub use crate::inbox::inbox;
82}
83
84/// A bug as speccoder keeps it (read-only; a bug's actions are kinds).
85pub mod bug {
86    pub use crate::bug::show;
87}
88
89/// A task's evidence.
90pub mod evidence {
91    pub use crate::evidence::{account_of, banner, rows, statements};
92}
93
94/// A task's evidence tour (`evidence_tour`), read as recorded.
95pub mod tour {
96    use std::path::Path;
97
98    use speccoder_core::{CommandData, CommandResult, CommandStatus};
99
100    /// The tour of `task` of `feature` (default: the selected feature) at
101    /// `HEAD`: [`crate::tour::command`] without trust, so it shows what the
102    /// statements and the commit say.
103    #[must_use]
104    pub fn of_task(root: &Path, feature: Option<&str>, task: &str) -> CommandResult {
105        let selected = || match crate::inspect(root, None).data() {
106            Some(CommandData::Inspect(snap)) => {
107                snap.feature.as_ref().map(|f| f.feature.name.clone())
108            }
109            _ => None,
110        };
111        let Some(feature) = feature.map(str::to_owned).or_else(selected) else {
112            return CommandResult::new(
113                "tour",
114                CommandStatus::SelectionNeeded,
115                "no feature selected",
116            );
117        };
118        crate::tour::command(
119            root,
120            &feature,
121            task,
122            &crate::attest::branch::Options::default(),
123            "tour",
124        )
125    }
126}
127
128/// The feature's documents.
129pub mod documents {
130    pub use crate::documents::{of_feature, render};
131}
132
133/// The machine and the project, checked.
134pub mod doctor {
135    pub use crate::doctor::{agents, run};
136}
137
138/// `speccoder.toml`.
139pub mod settings {
140    /// What a prefix entry would newly allow (`config coverage`).
141    pub use crate::approvals::coverage::show as coverage;
142    pub use crate::settings::{apply, commit, show};
143}
144
145/// Tokens and money.
146pub mod controls {
147    pub use crate::controls::{SkillCost, cost, skill_cost};
148}
149
150/// The code view: the branch's change, its files.
151pub mod code {
152    pub use crate::code::{changes, file, tree, worktree_file, worktree_view};
153}
154
155/// Setting up a project.
156pub mod init {
157    pub use crate::init::{commit_written, finish, prepare, preview, root_of};
158}
159
160/// The features.
161pub mod current {
162    pub use crate::current::list;
163}
164
165/// The pull request, previewed.
166pub mod handoff {
167    pub use crate::handoff::{PrOptions, pr};
168}
169
170/// Where speccoder keeps its state.
171pub mod worktree {
172    pub use crate::worktree::state_dir;
173}
174
175/// GitHub: sign-in, issues, the branch's pull request.
176pub mod github {
177    pub use crate::github::{
178        Ctx, RepoEntry, description, issue, issues, login_with_token, pr_for, reach, refused, repo,
179        repositories, started, status, valid_slug,
180    };
181
182    /// Signing in on a device.
183    pub mod auth {
184        pub use crate::github::auth::{Pending, client_id, start, wait};
185    }
186
187    /// GitHub's addresses.
188    pub mod http {
189        pub use crate::github::http::oauth;
190    }
191}
192
193/// How the project's format words things, for the page's header.
194#[derive(Debug, Clone, Copy)]
195pub struct FormatWords {
196    /// The format (`Spec Kit`).
197    pub name: &'static str,
198    /// What it calls a unit of work (`feature`, `change`).
199    pub unit: &'static str,
200}
201
202/// The words of the format of the project at `root`.
203#[must_use]
204pub fn format_words(root: &Path) -> FormatWords {
205    let format = crate::format::at(root);
206    FormatWords {
207        name: format.wording().name,
208        unit: format.messages().unit,
209    }
210}
211
212/// The format's planning chain, by installed skill name, for the New
213/// Feature sheet: what creates a unit, what leads to the task list, what
214/// checks it and what revises a written spec.
215#[derive(Debug, Clone, serde::Serialize)]
216pub struct PlanningWords {
217    /// The creating skill (`speckit-specify`).
218    pub creates: Option<String>,
219    /// The skills toward the task list after it (`speckit-plan`, `speckit-tasks`).
220    pub toward: Vec<String>,
221    /// The checking skill (`speckit-analyze`).
222    pub checks: Option<String>,
223    /// The revising skill (`speckit-clarify`).
224    pub revises: Option<String>,
225}
226
227/// [`PlanningWords`] of the project at `root`.
228#[must_use]
229pub fn planning_words(root: &Path) -> PlanningWords {
230    let p = crate::format::at(root).planning();
231    let full = |s: &str| format!("{}{s}", p.prefix);
232    let creates = p.creates.first().map(|c| full(c));
233    PlanningWords {
234        toward: (p.toward_tasks)(false)
235            .iter()
236            .map(|s| full(s))
237            .filter(|s| Some(s) != creates.as_ref())
238            .collect(),
239        creates,
240        checks: p.checks.map(full),
241        revises: p.revises.map(full),
242    }
243}
244
245/// The installed name of the format's skill that creates a unit, if it has one.
246#[must_use]
247pub fn create_skill(root: &Path) -> Option<String> {
248    let planning = crate::format::at(root).planning();
249    planning
250        .creates
251        .first()
252        .map(|c| format!("{}{c}", planning.prefix))
253}
254
255/// One commit's patch, if `commit` names a commit (a blob or a tree id
256/// from elsewhere in the repository is never shown); empty otherwise.
257#[must_use]
258pub fn commit_patch(root: &Path, commit: &str) -> String {
259    commit_patch_of(root, commit)
260}
261
262/// Whether `id` is reached from `HEAD`, a branch or a saved attempt.
263fn reachable(git: &crate::git::Git, id: &str) -> bool {
264    if git
265        .succeeds(&["merge-base", "--is-ancestor", id, "HEAD"])
266        .unwrap_or(false)
267    {
268        return true;
269    }
270    git.line(&[
271        "for-each-ref",
272        "--count=1",
273        "--format=%(refname)",
274        "--contains",
275        id,
276        "refs/heads",
277        "refs/speccoder/attempts",
278    ])
279    .is_ok_and(|l| !l.trim().is_empty())
280}
281
282fn commit_patch_of(root: &Path, commit: &str) -> String {
283    let git = crate::git::Git::at(root);
284    // `^{commit}` peels or fails.
285    let Ok(id) = git.line(&[
286        "rev-parse",
287        "--verify",
288        "-q",
289        &format!("{commit}^{{commit}}"),
290    ]) else {
291        return String::new();
292    };
293    // Only what a branch, `HEAD` or a saved attempt reaches: a commit the
294    // page could only name by its id (a sealed or unreachable object) shows
295    // nothing.
296    if !reachable(&git, &id) {
297        return String::new();
298    }
299    git.run(&["show", "--no-color", "--patch", "--format=%h %s%n%n%b", &id])
300        .map(|b| String::from_utf8_lossy(&b).into_owned())
301        .unwrap_or_default()
302}