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/// Calls waiting for a person, and who answers them.
17pub mod approvals {
18    pub use crate::approvals::{Answer, Approver, Presentation, Who, list};
19}
20
21/// A run's progress, as it happens and as another process left it.
22pub mod progress {
23    pub use crate::progress::{Event, Progress, read_live};
24}
25
26/// Read views.
27pub mod status {
28    pub use crate::status::report;
29}
30
31/// The next steps a result names.
32pub mod next {
33    pub use crate::next::attach;
34}
35
36/// The person's review of the spec.
37pub mod review {
38    /// The comment tray and the last revision turn.
39    pub use crate::comments::list as comments;
40    pub use crate::review::report;
41}
42
43/// Needs You.
44pub mod inbox {
45    pub use crate::inbox::inbox;
46}
47
48/// A bug as speccoder keeps it (read-only; a bug's actions are kinds).
49pub mod bug {
50    pub use crate::bug::show;
51}
52
53/// A task's evidence.
54pub mod evidence {
55    pub use crate::evidence::{account_of, banner, rows, statements};
56}
57
58/// A task's evidence tour (`evidence_tour`), read as recorded.
59pub mod tour {
60    use std::path::Path;
61
62    use speccoder_core::{CommandData, CommandResult, CommandStatus};
63
64    /// The tour of `task` of `feature` (default: the selected feature) at
65    /// `HEAD`: [`crate::tour::command`] without trust, so it shows what the
66    /// statements and the commit say.
67    #[must_use]
68    pub fn of_task(root: &Path, feature: Option<&str>, task: &str) -> CommandResult {
69        let selected = || match crate::inspect(root, None).data() {
70            Some(CommandData::Inspect(snap)) => {
71                snap.feature.as_ref().map(|f| f.feature.name.clone())
72            }
73            _ => None,
74        };
75        let Some(feature) = feature.map(str::to_owned).or_else(selected) else {
76            return CommandResult::new(
77                "tour",
78                CommandStatus::SelectionNeeded,
79                "no feature selected",
80            );
81        };
82        crate::tour::command(
83            root,
84            &feature,
85            task,
86            &crate::attest::branch::Options::default(),
87            "tour",
88        )
89    }
90}
91
92/// The feature's documents.
93pub mod documents {
94    pub use crate::documents::{of_feature, render};
95}
96
97/// The machine and the project, checked.
98pub mod doctor {
99    pub use crate::doctor::run;
100}
101
102/// `speccoder.toml`.
103pub mod settings {
104    /// What a prefix entry would newly allow (`config coverage`).
105    pub use crate::approvals::coverage::show as coverage;
106    pub use crate::settings::{apply, commit, show};
107}
108
109/// Tokens and money.
110pub mod controls {
111    pub use crate::controls::cost;
112}
113
114/// The code view: the branch's change, its files.
115pub mod code {
116    pub use crate::code::{changes, file, tree, worktree_file, worktree_view};
117}
118
119/// Setting up a project.
120pub mod init {
121    pub use crate::init::{commit_written, finish, prepare, preview, root_of};
122}
123
124/// The features.
125pub mod current {
126    pub use crate::current::list;
127}
128
129/// The pull request, previewed.
130pub mod handoff {
131    pub use crate::handoff::{PrOptions, pr};
132}
133
134/// Where speccoder keeps its state.
135pub mod worktree {
136    pub use crate::worktree::state_dir;
137}
138
139/// GitHub: sign-in, issues, the branch's pull request.
140pub mod github {
141    pub use crate::github::{
142        Ctx, description, issue, issues, login_with_token, pr_for, reach, refused, repo, started,
143        status,
144    };
145
146    /// Signing in on a device.
147    pub mod auth {
148        pub use crate::github::auth::{Pending, client_id, start, wait};
149    }
150
151    /// GitHub's addresses.
152    pub mod http {
153        pub use crate::github::http::oauth;
154    }
155}
156
157/// How the project's format words things, for the page's header.
158#[derive(Debug, Clone, Copy)]
159pub struct FormatWords {
160    /// The format (`Spec Kit`).
161    pub name: &'static str,
162    /// What it calls a unit of work (`feature`, `change`).
163    pub unit: &'static str,
164}
165
166/// The words of the format of the project at `root`.
167#[must_use]
168pub fn format_words(root: &Path) -> FormatWords {
169    let format = crate::format::at(root);
170    FormatWords {
171        name: format.wording().name,
172        unit: format.messages().unit,
173    }
174}
175
176/// The installed name of the format's skill that creates a unit, if it has one.
177#[must_use]
178pub fn create_skill(root: &Path) -> Option<String> {
179    let planning = crate::format::at(root).planning();
180    planning
181        .creates
182        .first()
183        .map(|c| format!("{}{c}", planning.prefix))
184}
185
186/// One commit's patch, if `commit` names a commit (a blob or a tree id
187/// from elsewhere in the repository is never shown); empty otherwise.
188#[must_use]
189pub fn commit_patch(root: &Path, commit: &str) -> String {
190    commit_patch_of(root, commit)
191}
192
193/// Whether `id` is reached from `HEAD`, a branch or a saved attempt.
194fn reachable(git: &crate::git::Git, id: &str) -> bool {
195    if git
196        .succeeds(&["merge-base", "--is-ancestor", id, "HEAD"])
197        .unwrap_or(false)
198    {
199        return true;
200    }
201    git.line(&[
202        "for-each-ref",
203        "--count=1",
204        "--format=%(refname)",
205        "--contains",
206        id,
207        "refs/heads",
208        "refs/speccoder/attempts",
209    ])
210    .is_ok_and(|l| !l.trim().is_empty())
211}
212
213fn commit_patch_of(root: &Path, commit: &str) -> String {
214    let git = crate::git::Git::at(root);
215    // `^{commit}` peels or fails.
216    let Ok(id) = git.line(&[
217        "rev-parse",
218        "--verify",
219        "-q",
220        &format!("{commit}^{{commit}}"),
221    ]) else {
222        return String::new();
223    };
224    // Only what a branch, `HEAD` or a saved attempt reaches: a commit the
225    // page could only name by its id (a sealed or unreachable object) shows
226    // nothing.
227    if !reachable(&git, &id) {
228        return String::new();
229    }
230    git.run(&["show", "--no-color", "--patch", "--format=%h %s%n%n%b", &id])
231        .map(|b| String::from_utf8_lossy(&b).into_owned())
232        .unwrap_or_default()
233}