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