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