1use 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 person {
19 pub use crate::person::{Stream, TIMEOUT, open_terminal, run, sandboxed, shown, speccoder};
20}
21
22pub mod copy {
24 pub use crate::copy::block;
25 pub use crate::doctor::bug_report;
26}
27
28pub mod approvals {
30 pub use crate::approvals::{Answer, Approver, Presentation, Who, list};
31}
32
33pub mod progress {
35 pub use crate::progress::{Event, Progress, read_live};
36}
37
38pub mod hooks {
40 #[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
54pub mod status {
56 pub use crate::status::report;
57}
58
59pub mod shape {
62 pub use crate::shape::{FIELDS, findings, judge, shape};
63}
64
65pub 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
73pub mod explore {
75 pub use crate::explore::{list, session};
76}
77
78pub mod next {
80 pub use crate::next::attach;
81}
82
83pub mod review {
85 pub use crate::comments::list as comments;
87 pub use crate::review::{readiness, report};
88}
89
90pub mod planning {
93 pub use crate::planning_chain::{similar_command, similar_to, suggest};
94}
95
96pub 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
102pub mod welcome {
104 pub use crate::welcome::{
105 Recent, RecentView, default_parent, folders, forget, is_project, remember, tilde, views,
106 };
107}
108
109pub mod sample {
111 pub use crate::sample::{available, copy, home, missing_tool};
112}
113
114pub mod inbox {
116 pub use crate::inbox::inbox;
117}
118
119pub mod bug {
121 pub use crate::bug::show;
122}
123
124pub mod evidence {
126 pub use crate::evidence::{account_of, banner, rows, statements};
127}
128
129pub mod tour {
131 use std::path::Path;
132
133 use speccoder_core::{CommandData, CommandResult, CommandStatus};
134
135 #[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 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
181pub mod documents {
183 pub use crate::documents::{of_feature, render};
184}
185
186pub mod doctor {
188 pub use crate::doctor::{agents, run};
189}
190
191pub mod settings {
193 pub use crate::approvals::coverage::show as coverage;
195 pub use crate::settings::{apply, commit, show};
196}
197
198pub mod controls {
200 pub use crate::controls::{SkillCost, cost, skill_cost};
201}
202
203pub mod code {
205 pub use crate::code::{changes, file, tree, worktree_file, worktree_view};
206}
207
208pub mod init {
210 pub use crate::init::{commit_written, finish, prepare, preview, root_of};
211}
212
213pub mod current {
215 pub use crate::current::list;
216}
217
218pub mod handoff {
220 pub use crate::handoff::{PrOptions, pr};
221}
222
223pub mod worktree {
225 pub use crate::worktree::state_dir;
226}
227
228pub 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 pub mod auth {
237 pub use crate::github::auth::{Pending, client_id, start, wait};
238 }
239
240 pub mod http {
242 pub use crate::github::http::oauth;
243 }
244}
245
246#[derive(Debug, Clone, Copy)]
248pub struct FormatWords {
249 pub name: &'static str,
251 pub unit: &'static str,
253}
254
255#[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#[derive(Debug, Clone, serde::Serialize)]
269pub struct PlanningWords {
270 pub creates: Option<String>,
272 pub toward: Vec<String>,
274 pub checks: Option<String>,
276 pub revises: Option<String>,
278}
279
280#[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#[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#[must_use]
311pub fn commit_patch(root: &Path, commit: &str) -> String {
312 commit_patch_of(root, commit)
313}
314
315fn 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 let Ok(id) = git.line(&[
339 "rev-parse",
340 "--verify",
341 "-q",
342 &format!("{commit}^{{commit}}"),
343 ]) else {
344 return String::new();
345 };
346 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}