speccoder_runtime/
service.rs1use std::path::Path;
8
9pub use crate::RunOptions;
10
11pub mod actions {
13 pub use crate::actions::{Dispatch, Interface, Offer, available, list, offered, perform};
14}
15
16pub mod approvals {
18 pub use crate::approvals::{Answer, Approver, Presentation, Who, list};
19}
20
21pub mod progress {
23 pub use crate::progress::{Event, Progress, read_live};
24}
25
26pub mod status {
28 pub use crate::status::report;
29}
30
31pub mod next {
33 pub use crate::next::attach;
34}
35
36pub mod review {
38 pub use crate::comments::list as comments;
40 pub use crate::review::report;
41}
42
43pub mod inbox {
45 pub use crate::inbox::inbox;
46}
47
48pub mod bug {
50 pub use crate::bug::show;
51}
52
53pub mod evidence {
55 pub use crate::evidence::{account_of, banner, rows, statements};
56}
57
58pub mod tour {
60 use std::path::Path;
61
62 use speccoder_core::{CommandData, CommandResult, CommandStatus};
63
64 #[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
92pub mod documents {
94 pub use crate::documents::{of_feature, render};
95}
96
97pub mod doctor {
99 pub use crate::doctor::run;
100}
101
102pub mod settings {
104 pub use crate::approvals::coverage::show as coverage;
106 pub use crate::settings::{apply, commit, show};
107}
108
109pub mod controls {
111 pub use crate::controls::cost;
112}
113
114pub mod code {
116 pub use crate::code::{changes, file, tree, worktree_file, worktree_view};
117}
118
119pub mod init {
121 pub use crate::init::{commit_written, finish, prepare, preview, root_of};
122}
123
124pub mod current {
126 pub use crate::current::list;
127}
128
129pub mod handoff {
131 pub use crate::handoff::{PrOptions, pr};
132}
133
134pub mod worktree {
136 pub use crate::worktree::state_dir;
137}
138
139pub 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 pub mod auth {
148 pub use crate::github::auth::{Pending, client_id, start, wait};
149 }
150
151 pub mod http {
153 pub use crate::github::http::oauth;
154 }
155}
156
157#[derive(Debug, Clone, Copy)]
159pub struct FormatWords {
160 pub name: &'static str,
162 pub unit: &'static str,
164}
165
166#[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#[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#[must_use]
189pub fn commit_patch(root: &Path, commit: &str) -> String {
190 commit_patch_of(root, commit)
191}
192
193fn 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 let Ok(id) = git.line(&[
217 "rev-parse",
218 "--verify",
219 "-q",
220 &format!("{commit}^{{commit}}"),
221 ]) else {
222 return String::new();
223 };
224 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}