Skip to main content

spec_driven_docs/commands/
stage.rs

1//! `stage` subcommand: runtime-shape.
2//!
3//! Resolves the target and the stage root, asks the stage service to render
4//! the candidate, and reports where it landed. Cleanup is its own verb, so
5//! removing a stage is always something somebody asked for.
6
7use camino::Utf8PathBuf;
8
9use crate::cli::stage::{StageArgs, StageCleanArgs, StageCommand};
10use crate::context::AppContext;
11use crate::domain::paths::UserEnv;
12use crate::error::AppError;
13use crate::output;
14use crate::stage::{self, Request};
15
16/// Render a stage, or remove one.
17///
18/// # Errors
19///
20/// [`AppError::Usage`] for a target or stage path the arguments cannot
21/// mean, [`AppError::Refused`] where a stage is already there or a cleanup
22/// target is not a stage, and I/O errors writing the stage.
23pub fn run(ctx: &AppContext, args: StageArgs) -> Result<(), AppError> {
24    if let Some(StageCommand::Clean(clean)) = args.command {
25        return remove(&clean);
26    }
27    let target = resolved(ctx, args.target)?;
28    let state_root = UserEnv::from_process()
29        .state_root()
30        .ok_or_else(|| AppError::Usage("no state root resolves".to_string()))?
31        .path;
32    let docs_scratch = args
33        .docs_scratch
34        .as_deref()
35        .map(crate::domain::manifest::parse_docs_scratch)
36        .transpose()
37        .map_err(|error| AppError::Usage(format!("--docs-scratch: {error}")))?;
38    let writing_style = args
39        .writing_style
40        .as_deref()
41        .map(crate::domain::instance_config::WritingStyle::parse_flag)
42        .transpose()
43        .map_err(|error| AppError::Usage(format!("--writing-style: {error}")))?;
44    let receipt = stage::create(
45        &Request {
46            target,
47            profile: args.profile,
48            output: args.output,
49            docs_scratch,
50            reserve: args.reserve,
51            writing_style,
52        },
53        &state_root,
54    )?;
55
56    if args.json {
57        return output::json(&receipt);
58    }
59    output::line(format!(
60        "staged spec-driven-docs {} ({}) for {}",
61        receipt.version, receipt.profile, receipt.target
62    ));
63    output::line(format!("stage: {}", receipt.root));
64    output::line(format!(
65        "{} artifacts under {}/, reference material under {}/",
66        receipt.artifacts.len(),
67        stage::ARTIFACTS_DIR,
68        stage::REFERENCE_DIR
69    ));
70    for note in &receipt.notes {
71        output::line(note.clone());
72    }
73    output::line(format!(
74        "the stage stays until 'sdd stage clean {}' removes it",
75        receipt.root
76    ));
77    Ok(())
78}
79
80/// Remove one stage this tool wrote.
81fn remove(args: &StageCleanArgs) -> Result<(), AppError> {
82    let lines = stage::clean(&args.path)?;
83    if args.json {
84        return output::json(&serde_json::json!({
85            "schema": "sdd.stage-clean/1",
86            "removed": args.path,
87        }));
88    }
89    for line in lines {
90        output::line(line);
91    }
92    Ok(())
93}
94
95/// The target, absolute or the working directory.
96fn resolved(ctx: &AppContext, target: Utf8PathBuf) -> Result<Utf8PathBuf, AppError> {
97    if target.is_absolute() {
98        Ok(target)
99    } else if target == "." {
100        Ok(ctx.cwd.clone())
101    } else {
102        Err(AppError::Usage("target must be absolute or .".to_string()))
103    }
104}