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 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 #[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 #[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
71pub mod hooks {
73 #[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
87pub mod status {
89 pub use crate::status::report;
90}
91
92pub mod shape {
95 pub use crate::shape::{FIELDS, findings, judge, shape};
96}
97
98pub 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
106pub mod explore {
108 pub use crate::explore::{list, session};
109}
110
111pub mod next {
113 pub use crate::next::attach;
114}
115
116pub mod review {
118 pub use crate::comments::list as comments;
120 pub use crate::review::{readiness, report};
121}
122
123pub mod planning {
126 pub use crate::planning_chain::{similar_command, similar_to, suggest};
127}
128
129pub 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
135pub mod welcome {
137 pub use crate::welcome::{
138 Recent, RecentView, default_parent, folders, forget, is_project, remember, tilde, views,
139 };
140}
141
142pub mod sample {
144 pub use crate::sample::{available, copy, home, missing_tool};
145}
146
147pub mod inbox {
149 pub use crate::inbox::inbox;
150}
151
152pub mod bug {
154 pub use crate::bug::show;
155}
156
157pub mod evidence {
159 pub use crate::evidence::{account_of, banner, rows, statements};
160}
161
162pub mod tour {
164 use std::path::Path;
165
166 use speccoder_core::{CommandData, CommandResult, CommandStatus};
167
168 #[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 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
214pub mod documents {
216 pub use crate::documents::{of_feature, render};
217}
218
219pub mod doctor {
221 pub use crate::doctor::{agents, run};
222}
223
224pub mod settings {
226 pub use crate::approvals::coverage::show as coverage;
228 pub use crate::settings::{apply, commit, show};
229}
230
231pub mod controls {
233 pub use crate::controls::{SkillCost, cost, skill_cost};
234}
235
236pub mod code {
238 pub use crate::code::{changes, file, tree, worktree_file, worktree_view};
239}
240
241pub mod init {
243 pub use crate::init::{commit_written, finish, prepare, preview, root_of};
244}
245
246pub mod current {
248 pub use crate::current::list;
249}
250
251pub mod handoff {
253 pub use crate::handoff::{PrOptions, pr};
254}
255
256pub mod worktree {
258 pub use crate::worktree::state_dir;
259}
260
261pub 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 pub mod auth {
270 pub use crate::github::auth::{Pending, client_id, start, wait};
271 }
272
273 pub mod http {
275 pub use crate::github::http::oauth;
276 }
277}
278
279#[derive(Debug, Clone, Copy)]
281pub struct FormatWords {
282 pub name: &'static str,
284 pub unit: &'static str,
286}
287
288#[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#[derive(Debug, Clone, serde::Serialize)]
302pub struct PlanningWords {
303 pub creates: Option<String>,
305 pub toward: Vec<String>,
307 pub checks: Option<String>,
309 pub revises: Option<String>,
311}
312
313#[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#[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#[must_use]
344pub fn commit_patch(root: &Path, commit: &str) -> String {
345 commit_patch_of(root, commit)
346}
347
348fn 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 let Ok(id) = git.line(&[
372 "rev-parse",
373 "--verify",
374 "-q",
375 &format!("{commit}^{{commit}}"),
376 ]) else {
377 return String::new();
378 };
379 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}