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