1use 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 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 #[arg(long)]
120 pub agent: Option<String>,
121
122 #[arg(long)]
124 pub model: Option<String>,
125
126 #[arg(long, value_enum)]
128 harness: Option<HarnessArg>,
129
130 #[arg(long, value_enum)]
132 effort: Option<EffortArg>,
133
134 #[arg(long, value_enum)]
136 approval: Option<ApprovalArg>,
137
138 #[arg(long, value_enum)]
140 sandbox: Option<SandboxArg>,
141
142 #[arg(long = "skill", value_delimiter = ',', action = ArgAction::Append)]
144 pub extra_skills: Vec<String>,
145
146 #[arg(long, conflicts_with = "no_refresh_models")]
148 pub refresh_models: bool,
149
150 #[arg(long, conflicts_with = "refresh_models")]
152 pub no_refresh_models: bool,
153
154 #[arg(long)]
157 pub context: Option<String>,
158
159 #[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}