Skip to main content

codescout/cli/
artifact_augment.rs

1//! `codescout artifact-augment <id>` — attach or merge augmentation params/prompt.
2
3use crate::librarian::tools::Tool;
4use anyhow::{Context, Result};
5use clap::Args;
6use serde_json::Value;
7
8use crate::cli::{open_ctx, CommonOpts};
9
10#[derive(Debug, Args)]
11pub struct AugmentArgs {
12    /// Artifact id.
13    pub id: String,
14    /// Persistent prompt (or `@<file>` / `-`). Required unless `--merge` is passed.
15    #[arg(long)]
16    pub prompt: Option<String>,
17    /// Persistent prompt loaded from file path. Mutually exclusive with --prompt.
18    #[arg(long = "prompt-file")]
19    pub prompt_file: Option<std::path::PathBuf>,
20    /// Params JSON (`@<file>` / `-` / literal JSON).
21    #[arg(long)]
22    pub params: Option<String>,
23    /// Params JSON Schema (`@<file>` / `-` / literal JSON).
24    #[arg(long = "params-schema")]
25    pub params_schema: Option<String>,
26    /// MiniJinja render template (or `@<file>` / `-`).
27    #[arg(long = "render-template")]
28    pub render_template: Option<String>,
29    /// RFC 7396 merge-patch on params only. Requires prior augmentation.
30    #[arg(long)]
31    pub merge: bool,
32    /// Append-mode: prepend dated section to body instead of replacing.
33    #[arg(long = "append-mode")]
34    pub append_mode: bool,
35    /// Max number of dated ## YYYY-MM-DD sections to retain.
36    #[arg(long = "history-cap")]
37    pub history_cap: Option<usize>,
38    /// Optional project root override (defaults to cwd).
39    #[arg(long)]
40    pub project: Option<std::path::PathBuf>,
41    /// Emit JSON to stdout.
42    #[arg(long)]
43    pub json: bool,
44    /// Force no color (also implicit when stdout is not a TTY).
45    #[arg(long = "no-color")]
46    pub no_color: bool,
47}
48
49pub async fn run(args: AugmentArgs) -> Result<()> {
50    let common = CommonOpts {
51        project: args.project.clone(),
52        json: args.json,
53        no_color: args.no_color,
54    };
55    let output = common.output();
56    let ctx = open_ctx(&common).await?;
57
58    // Resolve prompt from --prompt OR --prompt-file (mutually exclusive).
59    let prompt = match (args.prompt.as_ref(), args.prompt_file.as_ref()) {
60        (Some(p), None) => Some(crate::cli::read_at_or_stdin(p)?),
61        (None, Some(path)) => Some(
62            std::fs::read_to_string(path).with_context(|| format!("reading {}", path.display()))?,
63        ),
64        (Some(_), Some(_)) => {
65            return Err(anyhow::anyhow!(
66                "pass at most one of --prompt or --prompt-file"
67            ));
68        }
69        (None, None) => None,
70    };
71
72    if !args.merge && prompt.is_none() {
73        return Err(anyhow::anyhow!(
74            "--prompt (or --prompt-file) is required unless --merge is passed"
75        ));
76    }
77
78    let mut tool_args = serde_json::Map::new();
79    tool_args.insert("id".into(), Value::String(args.id.clone()));
80    if let Some(p) = prompt {
81        tool_args.insert("prompt".into(), Value::String(p));
82    }
83    if let Some(params) = &args.params {
84        let raw = crate::cli::read_at_or_stdin(params)?;
85        let parsed: Value = serde_json::from_str(&raw).context("--params is not valid JSON")?;
86        tool_args.insert("params".into(), parsed);
87    }
88    if let Some(s) = &args.params_schema {
89        let raw = crate::cli::read_at_or_stdin(s)?;
90        let parsed: Value =
91            serde_json::from_str(&raw).context("--params-schema is not valid JSON")?;
92        tool_args.insert("params_schema".into(), parsed);
93    }
94    if let Some(t) = &args.render_template {
95        tool_args.insert(
96            "render_template".into(),
97            Value::String(crate::cli::read_at_or_stdin(t)?),
98        );
99    }
100    if args.merge {
101        tool_args.insert("merge".into(), Value::Bool(true));
102    }
103    if args.append_mode {
104        tool_args.insert("append_mode".into(), Value::Bool(true));
105    }
106    if let Some(cap) = args.history_cap {
107        tool_args.insert("history_cap".into(), Value::Number(cap.into()));
108    }
109
110    // `augment::call` is a Tool-trait method, not a free function — instantiate
111    // the zero-sized ArtifactAugment struct and dispatch via the trait.
112    let tool = crate::librarian::tools::augment::ArtifactAugment;
113    let v = tool.call(&ctx, Value::Object(tool_args)).await?;
114    crate::cli::format::print(&v, &output)?;
115    Ok(())
116}