Skip to main content

mars_agents/cli/
build.rs

1//! `mars build` — build artifacts from static project state.
2
3use clap::{ArgAction, ValueEnum};
4
5use crate::build::bundle::RuntimeContext;
6use crate::build::{LaunchBundleRequest, build_launch_bundle};
7use crate::cli::MarsContext;
8use crate::error::MarsError;
9
10#[derive(Debug, clap::Args)]
11pub struct BuildArgs {
12    #[command(subcommand)]
13    pub command: BuildCommand,
14}
15
16#[derive(Debug, clap::Subcommand)]
17pub enum BuildCommand {
18    /// Build a harness-targeted launch scaffold/bundle for an agent or ad-hoc launch.
19    LaunchBundle(LaunchBundleArgs),
20}
21
22#[derive(Debug, Clone, ValueEnum)]
23enum HarnessArg {
24    Claude,
25    Codex,
26    Opencode,
27    Cursor,
28    Pi,
29}
30
31impl HarnessArg {
32    fn as_str(&self) -> &'static str {
33        match self {
34            Self::Claude => "claude",
35            Self::Codex => "codex",
36            Self::Opencode => "opencode",
37            Self::Cursor => "cursor",
38            Self::Pi => "pi",
39        }
40    }
41}
42
43#[derive(Debug, Clone, ValueEnum)]
44enum EffortArg {
45    Low,
46    Medium,
47    High,
48    Xhigh,
49}
50
51impl EffortArg {
52    fn as_str(&self) -> &'static str {
53        match self {
54            Self::Low => "low",
55            Self::Medium => "medium",
56            Self::High => "high",
57            Self::Xhigh => "xhigh",
58        }
59    }
60}
61
62#[derive(Debug, Clone, ValueEnum)]
63enum ApprovalArg {
64    Default,
65    Auto,
66    Confirm,
67    Never,
68}
69
70impl ApprovalArg {
71    fn as_str(&self) -> &'static str {
72        match self {
73            Self::Default => "default",
74            Self::Auto => "auto",
75            Self::Confirm => "confirm",
76            Self::Never => "never",
77        }
78    }
79}
80
81#[derive(Debug, Clone, ValueEnum)]
82enum SandboxArg {
83    Default,
84    ReadOnly,
85    WorkspaceWrite,
86    DangerFullAccess,
87}
88
89#[derive(Debug, Clone, ValueEnum)]
90enum TransportArg {
91    Subprocess,
92    Streaming,
93}
94
95impl TransportArg {
96    fn as_str(&self) -> &'static str {
97        match self {
98            Self::Subprocess => "subprocess",
99            Self::Streaming => "streaming",
100        }
101    }
102}
103
104impl SandboxArg {
105    fn as_str(&self) -> &'static str {
106        match self {
107            Self::Default => "default",
108            Self::ReadOnly => "read-only",
109            Self::WorkspaceWrite => "workspace-write",
110            Self::DangerFullAccess => "danger-full-access",
111        }
112    }
113}
114
115#[derive(Debug, clap::Args)]
116pub struct LaunchBundleArgs {
117    /// Agent name from `.mars/agents/<name>.md`.
118    /// Omit for ad-hoc mode; without `--model`, the resolved harness uses its default model.
119    #[arg(long)]
120    pub agent: Option<String>,
121
122    /// Override model token or canonical model id.
123    #[arg(long)]
124    pub model: Option<String>,
125
126    /// Override harness target.
127    #[arg(long, value_enum)]
128    harness: Option<HarnessArg>,
129
130    /// Override effort level.
131    #[arg(long, value_enum)]
132    effort: Option<EffortArg>,
133
134    /// Override approval mode.
135    #[arg(long, value_enum)]
136    approval: Option<ApprovalArg>,
137
138    /// Override sandbox mode.
139    #[arg(long, value_enum)]
140    sandbox: Option<SandboxArg>,
141
142    /// Add extra skills by name. Supports `--skill a --skill b` and `--skill a,b`.
143    #[arg(long = "skill", value_delimiter = ',', action = ArgAction::Append)]
144    pub extra_skills: Vec<String>,
145
146    /// Refresh models.dev catalog and harness probes synchronously before building (blocks until complete).
147    #[arg(long, conflicts_with = "no_refresh_models")]
148    pub refresh_models: bool,
149
150    /// Skip automatic models-cache refresh; use disk cache only (no probe background refresh).
151    #[arg(long, conflicts_with = "refresh_models")]
152    pub no_refresh_models: bool,
153
154    /// Runtime launch context JSON used to project concrete launch actions.
155    /// (EXPERIMENTAL; launch_actions are not consumed by meridian-cli and may be removed — see work item launch-bundle-projection)
156    #[arg(long)]
157    pub context: Option<String>,
158
159    /// Launch transport shape to project when --context is provided.
160    /// (EXPERIMENTAL; only used with --context)
161    #[arg(long, value_enum, default_value_t = TransportArg::Subprocess)]
162    transport: TransportArg,
163}
164
165pub fn run(args: &BuildArgs, ctx: &MarsContext, _json: bool) -> Result<i32, MarsError> {
166    match &args.command {
167        BuildCommand::LaunchBundle(subargs) => run_launch_bundle(subargs, ctx),
168    }
169}
170
171fn run_launch_bundle(args: &LaunchBundleArgs, ctx: &MarsContext) -> Result<i32, MarsError> {
172    let models_refresh =
173        crate::models::resolve_models_refresh_control(args.refresh_models, args.no_refresh_models)?;
174    let runtime_context = args
175        .context
176        .as_deref()
177        .map(|raw| {
178            serde_json::from_str::<RuntimeContext>(raw).map_err(|err| MarsError::InvalidRequest {
179                message: format!("invalid launch-bundle --context JSON: {err}"),
180            })
181        })
182        .transpose()?;
183    let transport = crate::build::project::Transport::parse(args.transport.as_str())?;
184    let bundle = build_launch_bundle(
185        ctx,
186        LaunchBundleRequest {
187            agent: args.agent.clone(),
188            model: args.model.clone(),
189            harness: args.harness.as_ref().map(|h| h.as_str().to_string()),
190            effort: args.effort.as_ref().map(|e| e.as_str().to_string()),
191            approval: args.approval.as_ref().map(|a| a.as_str().to_string()),
192            sandbox: args.sandbox.as_ref().map(|s| s.as_str().to_string()),
193            extra_skills: args.extra_skills.clone(),
194            models_refresh,
195            runtime_context,
196            transport,
197        },
198    )?;
199
200    if args.context.is_some() {
201        eprintln!(
202            "warning: --context launch_actions projection is EXPERIMENTAL and not consumed by meridian-cli; meridian builds argv/env in its own harness adapters (invariant I-2). May be removed. See work item launch-bundle-projection / PR #94."
203        );
204    }
205
206    println!(
207        "{}",
208        serde_json::to_string_pretty(&bundle).map_err(|err| MarsError::Internal(format!(
209            "failed to serialize launch bundle: {err}"
210        )))?
211    );
212
213    Ok(0)
214}