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 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::{readiness, report};
41}
42
43pub mod planning {
46 pub use crate::planning_chain::{similar_command, similar_to, suggest};
47}
48
49pub mod new_project {
51 pub use crate::new_project::{Creation, creation, first_feature, plan, undo_offer};
52 pub use speccoder_core::new_project::{NewChoices, Stack, slug};
53}
54
55pub mod welcome {
57 pub use crate::welcome::{
58 Recent, RecentView, default_parent, folders, forget, is_project, remember, tilde, views,
59 };
60}
61
62pub mod sample {
64 pub use crate::sample::{available, copy, home, missing_tool};
65}
66
67pub mod inbox {
69 pub use crate::inbox::inbox;
70}
71
72pub mod bug {
74 pub use crate::bug::show;
75}
76
77pub mod evidence {
79 pub use crate::evidence::{account_of, banner, rows, statements};
80}
81
82pub mod tour {
84 use std::path::Path;
85
86 use speccoder_core::{CommandData, CommandResult, CommandStatus};
87
88 #[must_use]
92 pub fn of_task(root: &Path, feature: Option<&str>, task: &str) -> CommandResult {
93 let selected = || match crate::inspect(root, None).data() {
94 Some(CommandData::Inspect(snap)) => {
95 snap.feature.as_ref().map(|f| f.feature.name.clone())
96 }
97 _ => None,
98 };
99 let Some(feature) = feature.map(str::to_owned).or_else(selected) else {
100 return CommandResult::new(
101 "tour",
102 CommandStatus::SelectionNeeded,
103 "no feature selected",
104 );
105 };
106 crate::tour::command(
107 root,
108 &feature,
109 task,
110 &crate::attest::branch::Options::default(),
111 "tour",
112 )
113 }
114}
115
116pub mod documents {
118 pub use crate::documents::{of_feature, render};
119}
120
121pub mod doctor {
123 pub use crate::doctor::run;
124}
125
126pub mod settings {
128 pub use crate::approvals::coverage::show as coverage;
130 pub use crate::settings::{apply, commit, show};
131}
132
133pub mod controls {
135 pub use crate::controls::{SkillCost, cost, skill_cost};
136}
137
138pub mod code {
140 pub use crate::code::{changes, file, tree, worktree_file, worktree_view};
141}
142
143pub mod init {
145 pub use crate::init::{commit_written, finish, prepare, preview, root_of};
146}
147
148pub mod current {
150 pub use crate::current::list;
151}
152
153pub mod handoff {
155 pub use crate::handoff::{PrOptions, pr};
156}
157
158pub mod worktree {
160 pub use crate::worktree::state_dir;
161}
162
163pub mod github {
165 pub use crate::github::{
166 Ctx, RepoEntry, description, issue, issues, login_with_token, pr_for, reach, refused, repo,
167 repositories, started, status, valid_slug,
168 };
169
170 pub mod auth {
172 pub use crate::github::auth::{Pending, client_id, start, wait};
173 }
174
175 pub mod http {
177 pub use crate::github::http::oauth;
178 }
179}
180
181#[derive(Debug, Clone, Copy)]
183pub struct FormatWords {
184 pub name: &'static str,
186 pub unit: &'static str,
188}
189
190#[must_use]
192pub fn format_words(root: &Path) -> FormatWords {
193 let format = crate::format::at(root);
194 FormatWords {
195 name: format.wording().name,
196 unit: format.messages().unit,
197 }
198}
199
200#[derive(Debug, Clone, serde::Serialize)]
204pub struct PlanningWords {
205 pub creates: Option<String>,
207 pub toward: Vec<String>,
209 pub checks: Option<String>,
211 pub revises: Option<String>,
213}
214
215#[must_use]
217pub fn planning_words(root: &Path) -> PlanningWords {
218 let p = crate::format::at(root).planning();
219 let full = |s: &str| format!("{}{s}", p.prefix);
220 let creates = p.creates.first().map(|c| full(c));
221 PlanningWords {
222 toward: (p.toward_tasks)(false)
223 .iter()
224 .map(|s| full(s))
225 .filter(|s| Some(s) != creates.as_ref())
226 .collect(),
227 creates,
228 checks: p.checks.map(full),
229 revises: p.revises.map(full),
230 }
231}
232
233#[must_use]
235pub fn create_skill(root: &Path) -> Option<String> {
236 let planning = crate::format::at(root).planning();
237 planning
238 .creates
239 .first()
240 .map(|c| format!("{}{c}", planning.prefix))
241}
242
243#[must_use]
246pub fn commit_patch(root: &Path, commit: &str) -> String {
247 commit_patch_of(root, commit)
248}
249
250fn reachable(git: &crate::git::Git, id: &str) -> bool {
252 if git
253 .succeeds(&["merge-base", "--is-ancestor", id, "HEAD"])
254 .unwrap_or(false)
255 {
256 return true;
257 }
258 git.line(&[
259 "for-each-ref",
260 "--count=1",
261 "--format=%(refname)",
262 "--contains",
263 id,
264 "refs/heads",
265 "refs/speccoder/attempts",
266 ])
267 .is_ok_and(|l| !l.trim().is_empty())
268}
269
270fn commit_patch_of(root: &Path, commit: &str) -> String {
271 let git = crate::git::Git::at(root);
272 let Ok(id) = git.line(&[
274 "rev-parse",
275 "--verify",
276 "-q",
277 &format!("{commit}^{{commit}}"),
278 ]) else {
279 return String::new();
280 };
281 if !reachable(&git, &id) {
285 return String::new();
286 }
287 git.run(&["show", "--no-color", "--patch", "--format=%h %s%n%n%b", &id])
288 .map(|b| String::from_utf8_lossy(&b).into_owned())
289 .unwrap_or_default()
290}