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 shape {
46 pub use crate::shape::{FIELDS, findings, judge, shape};
47}
48
49pub mod ask {
52 pub use crate::ask::chips::{ChipRef, resolve as chip};
53 pub use crate::ask::proposal::revalidate as proposal;
54 pub use crate::ask::{Request, changed_since, conversations, new_id, stop, thread, turn};
55}
56
57pub mod explore {
59 pub use crate::explore::{list, session};
60}
61
62pub mod next {
64 pub use crate::next::attach;
65}
66
67pub mod review {
69 pub use crate::comments::list as comments;
71 pub use crate::review::{readiness, report};
72}
73
74pub mod planning {
77 pub use crate::planning_chain::{similar_command, similar_to, suggest};
78}
79
80pub mod new_project {
82 pub use crate::new_project::{Creation, creation, first_feature, plan, undo_offer};
83 pub use speccoder_core::new_project::{NewChoices, Stack, slug};
84}
85
86pub mod welcome {
88 pub use crate::welcome::{
89 Recent, RecentView, default_parent, folders, forget, is_project, remember, tilde, views,
90 };
91}
92
93pub mod sample {
95 pub use crate::sample::{available, copy, home, missing_tool};
96}
97
98pub mod inbox {
100 pub use crate::inbox::inbox;
101}
102
103pub mod bug {
105 pub use crate::bug::show;
106}
107
108pub mod evidence {
110 pub use crate::evidence::{account_of, banner, rows, statements};
111}
112
113pub mod tour {
115 use std::path::Path;
116
117 use speccoder_core::{CommandData, CommandResult, CommandStatus};
118
119 #[must_use]
123 pub fn of_task(root: &Path, feature: Option<&str>, task: &str) -> CommandResult {
124 let selected = || match crate::inspect(root, None).data() {
125 Some(CommandData::Inspect(snap)) => {
126 snap.feature.as_ref().map(|f| f.feature.name.clone())
127 }
128 _ => None,
129 };
130 let Some(feature) = feature.map(str::to_owned).or_else(selected) else {
131 return CommandResult::new(
132 "tour",
133 CommandStatus::SelectionNeeded,
134 "no feature selected",
135 );
136 };
137 let branch = format!("speccoder/{feature}");
139 let head = if crate::git::Git::at(root)
140 .line(&[
141 "rev-parse",
142 "--verify",
143 "--quiet",
144 &format!("refs/heads/{branch}"),
145 ])
146 .is_ok_and(|l| !l.trim().is_empty())
147 {
148 branch
149 } else {
150 "HEAD".to_owned()
151 };
152 crate::tour::command(
153 root,
154 &feature,
155 task,
156 &crate::attest::branch::Options {
157 head,
158 ..crate::attest::branch::Options::default()
159 },
160 "tour",
161 )
162 }
163}
164
165pub mod documents {
167 pub use crate::documents::{of_feature, render};
168}
169
170pub mod doctor {
172 pub use crate::doctor::{agents, run};
173}
174
175pub mod settings {
177 pub use crate::approvals::coverage::show as coverage;
179 pub use crate::settings::{apply, commit, show};
180}
181
182pub mod controls {
184 pub use crate::controls::{SkillCost, cost, skill_cost};
185}
186
187pub mod code {
189 pub use crate::code::{changes, file, tree, worktree_file, worktree_view};
190}
191
192pub mod init {
194 pub use crate::init::{commit_written, finish, prepare, preview, root_of};
195}
196
197pub mod current {
199 pub use crate::current::list;
200}
201
202pub mod handoff {
204 pub use crate::handoff::{PrOptions, pr};
205}
206
207pub mod worktree {
209 pub use crate::worktree::state_dir;
210}
211
212pub mod github {
214 pub use crate::github::{
215 Ctx, RepoEntry, description, issue, issues, login_with_token, pr_for, reach, refused, repo,
216 repositories, started, status, valid_slug,
217 };
218
219 pub mod auth {
221 pub use crate::github::auth::{Pending, client_id, start, wait};
222 }
223
224 pub mod http {
226 pub use crate::github::http::oauth;
227 }
228}
229
230#[derive(Debug, Clone, Copy)]
232pub struct FormatWords {
233 pub name: &'static str,
235 pub unit: &'static str,
237}
238
239#[must_use]
241pub fn format_words(root: &Path) -> FormatWords {
242 let format = crate::format::at(root);
243 FormatWords {
244 name: format.wording().name,
245 unit: format.messages().unit,
246 }
247}
248
249#[derive(Debug, Clone, serde::Serialize)]
253pub struct PlanningWords {
254 pub creates: Option<String>,
256 pub toward: Vec<String>,
258 pub checks: Option<String>,
260 pub revises: Option<String>,
262}
263
264#[must_use]
266pub fn planning_words(root: &Path) -> PlanningWords {
267 let p = crate::format::at(root).planning();
268 let full = |s: &str| format!("{}{s}", p.prefix);
269 let creates = p.creates.first().map(|c| full(c));
270 PlanningWords {
271 toward: (p.toward_tasks)(false)
272 .iter()
273 .map(|s| full(s))
274 .filter(|s| Some(s) != creates.as_ref())
275 .collect(),
276 creates,
277 checks: p.checks.map(full),
278 revises: p.revises.map(full),
279 }
280}
281
282#[must_use]
284pub fn create_skill(root: &Path) -> Option<String> {
285 let planning = crate::format::at(root).planning();
286 planning
287 .creates
288 .first()
289 .map(|c| format!("{}{c}", planning.prefix))
290}
291
292#[must_use]
295pub fn commit_patch(root: &Path, commit: &str) -> String {
296 commit_patch_of(root, commit)
297}
298
299fn reachable(git: &crate::git::Git, id: &str) -> bool {
301 if git
302 .succeeds(&["merge-base", "--is-ancestor", id, "HEAD"])
303 .unwrap_or(false)
304 {
305 return true;
306 }
307 git.line(&[
308 "for-each-ref",
309 "--count=1",
310 "--format=%(refname)",
311 "--contains",
312 id,
313 "refs/heads",
314 "refs/speccoder/attempts",
315 ])
316 .is_ok_and(|l| !l.trim().is_empty())
317}
318
319fn commit_patch_of(root: &Path, commit: &str) -> String {
320 let git = crate::git::Git::at(root);
321 let Ok(id) = git.line(&[
323 "rev-parse",
324 "--verify",
325 "-q",
326 &format!("{commit}^{{commit}}"),
327 ]) else {
328 return String::new();
329 };
330 if !reachable(&git, &id) {
334 return String::new();
335 }
336 git.run(&["show", "--no-color", "--patch", "--format=%h %s%n%n%b", &id])
337 .map(|b| String::from_utf8_lossy(&b).into_owned())
338 .unwrap_or_default()
339}