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