1use std::ffi::OsStr;
55use std::path::{Path, PathBuf};
56use std::process::ExitCode;
57
58use serde_json::json;
59
60pub const DEFAULT_BUDGET_CENTS: u64 = 1_000;
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67pub enum Platform {
68 ClaudeCode,
69 Codex,
70 Kimi,
71 Trae,
72 WorkBuddy,
73 DeepSeekHarness,
74 OpenClaw,
75 Hermes,
76}
77
78#[derive(Debug, Clone, PartialEq, Eq)]
80pub enum InitError {
81 UnknownPlatform(String),
83 McpBinaryNotFound { searched: Vec<PathBuf> },
85 McpBinaryInvalid(String),
87 WalPathInvalid(String),
89 TraeIncompatiblePath(String),
91}
92
93impl InitError {
94 pub fn message(&self) -> String {
96 match self {
97 InitError::UnknownPlatform(input) => format!(
98 "未知平台 '{input}'。--platform 支持矩阵:\n \
99 claude-code → 项目根 .mcp.json(type: stdio;W-19 实测)\n \
100 codex → config.toml [mcp_servers.wanning] 片段(无路径变量;W-35)\n \
101 kimi → .kimi-code/mcp.json(无 type 无变量;W-40 实测)\n \
102 trae → .trae/mcp.json(command 不能含空格;W-17)\n \
103 workbuddy → .workbuddy/mcp.json(无 type 无变量;W-37 直核)\n \
104 deepseek-harness → Cordis overlay patch(- insert: 列表;W-44)\n \
105 openclaw → `openclaw mcp set` 命令行(mcp.servers 段;W-45 实测)\n \
106 hermes → `hermes mcp add` 命令行(config.yaml mcp_servers;W-45 实测)\n\
107 未知值 fail-closed,绝不猜。"
108 ),
109 InitError::McpBinaryNotFound { searched } => {
110 let mut message = String::from(
111 "找不到 wanning-mcp 可执行文件(fail-closed,绝不猜一个命令)。先安装:\n \
112 cargo install wanning-cli wanning-mcp\n\
113 或在 Wanning 仓内 cargo build -p wanning-mcp 后,用 --bin 指到 \
114 target/debug/wanning-mcp(或把该目录加进 PATH)。",
115 );
116 if !searched.is_empty() {
117 message.push_str("\n已搜索的 PATH 目录:");
118 for dir in searched {
119 message.push_str(&format!("\n {}", dir.display()));
120 }
121 }
122 message
123 }
124 InitError::McpBinaryInvalid(message) => message.clone(),
125 InitError::WalPathInvalid(message) => message.clone(),
126 InitError::TraeIncompatiblePath(message) => message.clone(),
127 }
128 }
129}
130
131#[derive(Debug, Clone, Default, PartialEq, Eq)]
133pub struct GenerateOptions {
134 pub mcp_bin: Option<PathBuf>,
136 pub wal: Option<PathBuf>,
138}
139
140#[derive(Debug, Clone, PartialEq, Eq)]
142pub struct Resolved {
143 pub mcp_bin: PathBuf,
144 pub wal: PathBuf,
145}
146
147#[derive(Debug, Clone, PartialEq, Eq)]
149pub struct Artifact {
150 pub notes: Vec<String>,
151 pub content: String,
152}
153
154pub fn parse_platform(input: &str) -> Result<Platform, InitError> {
156 match input {
157 "claude-code" => Ok(Platform::ClaudeCode),
158 "codex" => Ok(Platform::Codex),
159 "kimi" => Ok(Platform::Kimi),
160 "trae" => Ok(Platform::Trae),
161 "workbuddy" => Ok(Platform::WorkBuddy),
162 "deepseek-harness" => Ok(Platform::DeepSeekHarness),
163 "openclaw" => Ok(Platform::OpenClaw),
164 "hermes" => Ok(Platform::Hermes),
165 other => Err(InitError::UnknownPlatform(other.to_string())),
166 }
167}
168
169pub fn resolve_bin(
173 explicit: Option<&Path>,
174 path_env: Option<&OsStr>,
175) -> Result<PathBuf, InitError> {
176 if let Some(bin) = explicit {
177 if bin.is_file() {
178 return Ok(bin.to_path_buf());
179 }
180 return Err(InitError::McpBinaryInvalid(format!(
181 "--bin 指向的路径不是文件:{bin:?}\n\
182 先安装:cargo install wanning-cli wanning-mcp\n\
183 或在 Wanning 仓内 cargo build -p wanning-mcp 后,把 --bin 指到 \
184 target/debug/wanning-mcp"
185 )));
186 }
187 let exe = format!("wanning-mcp{}", std::env::consts::EXE_SUFFIX);
188 let mut searched = Vec::new();
189 if let Some(path_env) = path_env {
190 for dir in std::env::split_paths(path_env) {
191 if dir.as_os_str().is_empty() {
192 continue;
193 }
194 searched.push(dir.clone());
195 let candidate = dir.join(&exe);
196 if candidate.is_file() {
197 return Ok(candidate);
198 }
199 }
200 }
201 Err(InitError::McpBinaryNotFound { searched })
202}
203
204pub fn resolve_wal(explicit: Option<&Path>) -> Result<PathBuf, InitError> {
207 let wal = match explicit {
208 Some(wal) => wal.to_path_buf(),
209 None => wanning_core::paths::default_wal_path().ok_or_else(|| {
210 InitError::WalPathInvalid(
211 "解析不出默认账本路径(WANNING_HOME / USERPROFILE / HOME 都没有)。\
212 用 --wal 显式给一个审计 WAL 路径"
213 .to_string(),
214 )
215 })?,
216 };
217 if wal.is_absolute() {
218 return Ok(wal);
219 }
220 let current = std::env::current_dir()
221 .map_err(|e| InitError::WalPathInvalid(format!("解析当前目录失败: {e}")))?;
222 Ok(current.join(wal))
223}
224
225pub fn resolve(options: &GenerateOptions) -> Result<Resolved, InitError> {
227 let path_env = std::env::var_os("PATH");
228 Ok(Resolved {
229 mcp_bin: resolve_bin(options.mcp_bin.as_deref(), path_env.as_deref())?,
230 wal: resolve_wal(options.wal.as_deref())?,
231 })
232}
233
234pub fn generate(platform: Platform, options: &GenerateOptions) -> Result<Artifact, InitError> {
237 generate_with(platform, &resolve(options)?)
238}
239
240pub fn generate_with(platform: Platform, resolved: &Resolved) -> Result<Artifact, InitError> {
242 if matches!(platform, Platform::Trae)
243 && resolved
244 .mcp_bin
245 .to_string_lossy()
246 .chars()
247 .any(char::is_whitespace)
248 {
249 return Err(InitError::TraeIncompatiblePath(format!(
250 "Trae 官方文档要求 command 不能含空格(W-17 直核),解析出的 wanning-mcp 路径含空格:{}\n\
251 把 wanning-mcp 装到无空格路径(cargo install 的默认 bin 目录即可),\
252 或用 --bin 指定无空格路径",
253 slash(&resolved.mcp_bin)
254 )));
255 }
256 let artifact = match platform {
257 Platform::ClaudeCode => claude_code(resolved),
258 Platform::Trae => trae(resolved),
259 Platform::Codex => codex(resolved),
260 Platform::Kimi => kimi(resolved),
261 Platform::WorkBuddy => workbuddy(resolved),
262 Platform::DeepSeekHarness => deepseek_harness(resolved),
263 Platform::OpenClaw => openclaw(resolved),
264 Platform::Hermes => hermes(resolved),
265 };
266 Ok(artifact)
267}
268
269fn single_writer_note() -> &'static str {
270 "多平台同挂一份 WAL 时,第二个写进程 fail-closed 拒启(W-18 单写者锁)是特性不是缺陷"
271}
272
273fn slash(path: &Path) -> String {
275 path.to_string_lossy().replace('\\', "/")
276}
277
278fn budget_arg() -> String {
279 DEFAULT_BUDGET_CENTS.to_string()
280}
281
282pub fn first_run_notes() -> Vec<String> {
284 vec![
285 "① 把生成的配置写进对应位置后,重启你的编码工具(配置只在启动时读取)。".into(),
286 "② 确认 Wanning 已挂载:工具现身名 mcp__wanning__wanning_gate_evaluate(闸评估)与 mcp__wanning__wanning_audit_tail(读审计尾)。".into(),
287 "③ 验证闸在工作:让 agent 试一笔超额消费(默认预算 1000 分 = ¥10),应被拒绝且 reason=over_budget;放行与拒绝都落审计账本,`wanning audit` 可对账。".into(),
288 ]
289}
290
291fn json_artifact(value: serde_json::Value, mut notes: Vec<String>) -> Artifact {
292 let mut content = serde_json::to_string_pretty(&value).expect("静态 JSON 序列化");
293 content.push('\n');
294 notes.push(first_run_note_line());
295 Artifact { notes, content }
296}
297
298fn first_run_note_line() -> String {
299 "装完三步:重启工具 → 认工具 mcp__wanning__wanning_gate_evaluate → 试一笔超额消费应被拒(over_budget)".to_string()
300}
301
302fn claude_code(resolved: &Resolved) -> Artifact {
303 let value = json!({
306 "mcpServers": {
307 "wanning": {
308 "type": "stdio",
309 "command": slash(&resolved.mcp_bin),
310 "args": ["--wal", slash(&resolved.wal), "--budget", budget_arg()]
311 }
312 }
313 });
314 json_artifact(
315 value,
316 vec![
317 "Wanning 支付闸 — Claude Code MCP 配置(W-36 生成;W-43a 起写实路径)".into(),
318 format!(
319 "写入位置:项目根 .mcp.json。闸:{},审计账本:{}(每个项目目录可以各挂一份,互不相干)",
320 slash(&resolved.mcp_bin),
321 slash(&resolved.wal)
322 ),
323 "字段面依据仓内 .mcp.json 现物(W-19 真插实测):claude-code 需要 type: stdio,别的平台多半不需要".into(),
324 single_writer_note().into(),
325 "严格 JSON 不支持注释 → 文件内无注释行;实测与语义见 docs/research/mcp-consumption.md".into(),
326 ],
327 )
328}
329
330fn trae(resolved: &Resolved) -> Artifact {
331 let value = json!({
332 "mcpServers": {
333 "wanning": {
334 "command": slash(&resolved.mcp_bin),
335 "args": ["--wal", slash(&resolved.wal), "--budget", budget_arg()]
336 }
337 }
338 });
339 json_artifact(
340 value,
341 vec![
342 "Wanning 支付闸 — Trae MCP 配置(W-36 生成;W-43a 起写实路径)".into(),
343 format!(
344 "写入位置:项目根 .trae/mcp.json。闸:{},审计账本:{}",
345 slash(&resolved.mcp_bin),
346 slash(&resolved.wal)
347 ),
348 "字段面依据仓内 .trae/mcp.json 现物(W-17 直核):无 type 字段,command 不能含空格(含空格的路径已拒绝生成)".into(),
349 single_writer_note().into(),
350 "严格 JSON 不支持注释 → 文件内无注释行".into(),
351 ],
352 )
353}
354
355fn codex(resolved: &Resolved) -> Artifact {
356 Artifact {
357 notes: vec![
358 "Wanning 支付闸 — Codex CLI MCP 配置片段(W-36 生成;W-43a 起写实路径,零占位符)".into(),
359 format!(
360 "追加到 ~/.codex/config.toml(全局)或 <repo>/.codex/config.toml(project-scoped,trust 机制待实测)。闸:{},审计账本:{}",
361 slash(&resolved.mcp_bin),
362 slash(&resolved.wal)
363 ),
364 single_writer_note().into(),
365 "会话级使用需 OpenAI 登录(doctor ✗ auth);配置面免登录已实测(W-35)".into(),
366 first_run_note_line(),
367 ],
368 content: format!(
369 concat!(
370 "# Wanning 支付闸 — Codex CLI MCP 配置片段(W-36 生成;W-43a 起写实路径;字段依据 W-35 调研 docs/research/codex-mcp.md)\n",
371 "# 用法:追加到 ~/.codex/config.toml(全局)或 <repo>/.codex/config.toml(project-scoped,trust 机制待实测)\n",
372 "# W-35 直核:codex 配置没有路径变量 → 本片段已是真实绝对路径,无需手改\n",
373 "# 并发语义:多平台同挂一份 WAL 时,第二个写进程 fail-closed 拒启(W-18 单写者锁)是特性\n",
374 "[mcp_servers.wanning]\n",
375 "command = '{bin}'\n",
376 "args = [\"--wal\", '{wal}', \"--budget\", \"{budget}\"]\n",
377 "# 可选加固(文档字段,待 OpenAI 登录后实测):required = true —— server 起不来就 fail 启动,与闸 fail-closed 同构\n",
378 "# cargo run 备选形态与 startup_timeout_sec 说明见 docs/plugins/codex.md\n",
379 ),
380 bin = slash(&resolved.mcp_bin),
381 wal = slash(&resolved.wal),
382 budget = budget_arg(),
383 ),
384 }
385}
386
387fn kimi(resolved: &Resolved) -> Artifact {
388 let value = json!({
397 "mcpServers": {
398 "wanning": {
399 "command": slash(&resolved.mcp_bin),
400 "args": ["--wal", slash(&resolved.wal), "--budget", budget_arg()]
401 }
402 }
403 });
404 json_artifact(
405 value,
406 vec![
407 "Wanning 支付闸 — Kimi Code CLI MCP 配置(W-36 生成;W-40 按本机实测修订;W-43a 起写实路径)".into(),
408 "写入位置:用户级 ~/.kimi-code/mcp.json(或 $KIMI_CODE_HOME/mcp.json,所有项目生效)或 <repo>/.kimi-code/mcp.json(单项目)".into(),
409 format!(
410 "kimi-code 无 ${{...}} 路径变量(W-40 官方文档直核)→ 本配置已是真实绝对路径:闸 {},审计账本 {}",
411 slash(&resolved.mcp_bin),
412 slash(&resolved.wal)
413 ),
414 "项目级 .kimi-code/mcp.json 在未信任目录会弹 workspace trust 提示(默认拒绝信任)——核对其中列出的命令后再确认;用户级挂法不经 trust 提示".into(),
415 "TUI 内交互管理:/mcp-config(增删改)、/mcp(看连接状态)".into(),
416 single_writer_note().into(),
417 "W-40 已实测:真 kimi 0.39.1 二进制拉起 wanning-mcp,工具注入 + 放行/重放拒/超额拒三判定落 WAL(模型侧为本地 mock,真实模型会话待所有者放行烧额度)".into(),
418 ],
419 )
420}
421
422fn workbuddy(resolved: &Resolved) -> Artifact {
423 let value = json!({
427 "mcpServers": {
428 "wanning": {
429 "command": slash(&resolved.mcp_bin),
430 "args": ["--wal", slash(&resolved.wal), "--budget", budget_arg()]
431 }
432 }
433 });
434 json_artifact(
435 value,
436 vec![
437 "Wanning 支付闸 — WorkBuddy MCP 配置(W-36 生成,字段依据 W-37 直核官方 MCP-Guide;W-43a 起写实路径)".into(),
438 "写入位置:用户级 ~/.workbuddy/mcp.json(所有项目生效)或 <项目目录>/.workbuddy/mcp.json(单项目)".into(),
439 format!(
440 "WorkBuddy 文档未提及路径变量 → 本配置已是真实绝对路径:闸 {},审计账本 {}",
441 slash(&resolved.mcp_bin),
442 slash(&resolved.wal)
443 ),
444 "官方示例字段面无 type(与 claude-code 现物带 type:stdio 是刻意差异);也可走 UI:侧边栏 插件 → MCP 服务器 → 配置 MCP".into(),
445 "传输形态按官方命令启动式示例推断 stdio,真插实测待所有者桌面端(待实测项)".into(),
446 single_writer_note().into(),
447 ],
448 )
449}
450
451fn deepseek_harness(resolved: &Resolved) -> Artifact {
452 let content = format!(
464 concat!(
465 "# Wanning 支付闸 — DeepSeek Harness (dsh) Cordis overlay patch(W-44 生成;W-43a 起写实路径)\n",
466 "# 启用二选一:\n",
467 "# 临时:dsh --profile <名> --patch <本文件>\n",
468 "# 持久:把下面 insert 块合并追加进 <profile>/cordis.patch.yml 或\n",
469 "# $DSH_HOME/cordis.patch.yml(合并追加,绝不整文件覆盖)\n",
470 "- insert:\n",
471 " - id: wanning-gate # 唯一 id\n",
472 " name: '@deepseek-ai/dsh-mcp-client'\n",
473 " config:\n",
474 " serverName: wanning # 工具将现身为 mcp__wanning__wanning_gate_evaluate\n",
475 " transport: stdio\n",
476 " command: {bin}\n",
477 " args: [\"--wal\", \"{wal}\", \"--budget\", \"{budget}\"]\n",
478 " env: {{}}\n",
479 " cwd: !!js process.cwd()\n",
480 ),
481 bin = slash(&resolved.mcp_bin),
482 wal = slash(&resolved.wal),
483 budget = budget_arg(),
484 );
485 Artifact {
486 notes: vec![
487 "Wanning 支付闸 — DeepSeek Harness (dsh) Cordis overlay patch(W-36 生成,W-44 按官方格式入矩阵;W-43a 起写实路径)".into(),
488 format!(
489 "dsh 用 Cordis overlay YAML patch 声明 MCP server(不是 mcp.json);本文件是 patch entry,落盘惯用名 *.cordis.yml(--out 显式给路径,已存在绝不覆盖)。闸 {},审计账本 {}",
490 slash(&resolved.mcp_bin),
491 slash(&resolved.wal)
492 ),
493 "启用二选一:临时 dsh --profile <名> --patch <本文件>;持久 = 把 insert 块合并追加进 <profile>/cordis.patch.yml 或 $DSH_HOME/cordis.patch.yml(合并追加,绝不整文件覆盖)".into(),
494 "工具现身名:mcp__wanning__wanning_gate_evaluate / mcp__wanning__wanning_audit_tail(serverName: wanning → mcp__<serverName>__<tool>,官方命名契约,与 Claude Code/Codex 同形)".into(),
495 "dsh stdio 桥启动子进程前丢弃 ambient credential-shaped 与全部 DSH_* 环境变量(scrubbedParentEnv),其余照常继承 → 将来接真实通道时密钥必须写进本 row 的 config.env,不能赌继承".into(),
496 single_writer_note().into(),
497 "可选加固:config.failOnStartupError: true(默认 false = 闸起不来插件仍激活但零工具,闸位形同虚设;置 true 则 dsh 拒绝激活,与闸 fail-closed 同构)".into(),
498 "dsh 0.1.0-rc.7 = developer preview,官方明示会有破坏性变更——升级后本配置可能要跟着改".into(),
499 "本机 dsh 0.1.0-rc.7 已实测:--dump-config --patch 接受本格式(W-44,隔离 DSH_HOME,零网络零会话);会话级端到端待所有者放行(dsh 会话 = 模型会话 + 网络,红线 2)".into(),
500 first_run_note_line(),
501 ],
502 content,
503 }
504}
505
506fn openclaw(resolved: &Resolved) -> Artifact {
507 let payload = serde_json::to_string(&json!({
518 "command": slash(&resolved.mcp_bin),
519 "args": ["--wal", slash(&resolved.wal), "--budget", budget_arg()]
520 }))
521 .expect("静态 JSON 序列化");
522 Artifact {
523 notes: vec![
524 "Wanning 支付闸 — OpenClaw MCP 配置(W-45 生成;字段依据本机 2026.5.22 隔离实测 + docs.openclaw.ai/mcp 直核)".into(),
525 "执行下面这条命令即完成写入(openclaw.json 由宿主管理,openclaw mcp set 只动 mcp.servers.wanning 一段,绝不整文件覆盖)".into(),
526 format!(
527 "配置落点:openclaw.json 的 mcp.servers.wanning = {{command, args}}。闸 {},审计账本 {}",
528 slash(&resolved.mcp_bin),
529 slash(&resolved.wal)
530 ),
531 "OpenClaw 2026.5.22 原生支持 MCP(mcp list/show/set/unset 子命令族);W-45 隔离 env(OPENCLAW_STATE_DIR/OPENCLAW_CONFIG_PATH)实测 set/list/show 全绿".into(),
532 "stdio 字段面(官方文档直核):command/args/env/cwd;env 有安全过滤,拦 NODE_OPTIONS/PYTHONSTARTUP/DYLD_*/LD_* 等键 → 将来接真实通道时密钥必须写进 env,不能赌继承".into(),
533 "诚实边界:W-45 实测到配置面 + models.providers 挂本地 mock 模型为止;工具现身与判定落 WAL 属 agent 回合,需 gateway/模型会话(烧额度,红线 2,所有者放行)".into(),
534 single_writer_note().into(),
535 first_run_note_line(),
536 ],
537 content: format!("openclaw mcp set wanning '{payload}'\n"),
538 }
539}
540
541fn hermes(resolved: &Resolved) -> Artifact {
542 let content = format!(
553 "hermes mcp add wanning --command {bin} --args --wal {wal} --budget {budget}\n",
554 bin = slash(&resolved.mcp_bin),
555 wal = slash(&resolved.wal),
556 budget = budget_arg(),
557 );
558 Artifact {
559 notes: vec![
560 "Wanning 支付闸 — Hermes Agent MCP 配置(W-45 生成;字段依据本机 hermes v0.19.1 隔离实测 + 包内 cli-config.yaml.example 直核)".into(),
561 format!(
562 "执行下面这条命令即完成挂载(mcp add 是 discovery-first:真连一次发现工具,挂载即验证)。配置落点:$HERMES_HOME/config.yaml 的 mcp_servers 段。闸 {},审计账本 {}",
563 slash(&resolved.mcp_bin),
564 slash(&resolved.wal)
565 ),
566 "终端里跑会问 Enable all 2 tools? [Y/n] 回车即全开;脚本/CI 无 TTY 场景用 echo y | 管道喂确认(W-45 实测)".into(),
567 "落盘形态(实测原文):mcp_servers.wanning = {command: <bin>, args: [--wal, <wal>, --budget, '1000'], enabled: true};管理用 hermes mcp list / mcp test wanning / mcp remove wanning".into(),
568 "工具现身名:mcp__wanning__wanning_gate_evaluate / mcp__wanning__wanning_audit_tail(与 Claude Code/Codex/dsh 同形);hermes 把 MCP 工具放进 deferred catalog,模型侧经 tool_call(name, arguments) 间接调用——直接调 mcp__ 名会报 does not exist(W-45 实测教训)".into(),
569 "one-shot 会话要显式带 toolset:hermes -z \"…\" -t wanning(默认 cli 工具集不含 MCP 工具,W-45 实测)".into(),
570 "W-45 已实测(真 hermes 二进制 + 本地 mock LLM,零外网零真实消费):allow 400 分落 WAL;二次会话同 nonce → replay 拒,完整性链连续;真实模型会话待所有者放行烧额度(红线 2)".into(),
571 single_writer_note().into(),
572 first_run_note_line(),
573 ],
574 content,
575 }
576}
577
578const USAGE: &str = "wanning-init:给编码工具吐 Wanning MCP 配置(零网络、零真实消费)
581
582用法: wanning-init --platform <名> [--bin <wanning-mcp 路径>] [--wal <审计账本路径>] [--out <文件>]
583
584 --platform <名> 目标平台(必填):claude-code / codex / kimi / trae / workbuddy /
585 deepseek-harness / openclaw / hermes
586 --bin <路径> wanning-mcp 可执行文件;缺省从 PATH 解析(找不到 = 拒,给安装指引)
587 --wal <路径> 审计 WAL 路径;缺省 = 产品默认 ~/.wanning/wal.jsonl(Windows %USERPROFILE%\\.wanning)
588 --out <文件> 落盘路径;缺省只打印 stdout。已存在的文件**绝不覆盖**(动别人工具的配置 = 危险动作)
589 -h / --help 打印本说明后退出
590";
591
592enum CliError {
594 Usage(String),
595 Failed(String),
596}
597
598pub fn run_cli(program: &str, args: &[String]) -> ExitCode {
602 match cli_run(program, args) {
603 Ok(()) => ExitCode::SUCCESS,
604 Err(CliError::Usage(message)) => {
605 eprintln!("{program}: {message}");
606 ExitCode::from(2)
607 }
608 Err(CliError::Failed(message)) => {
609 eprintln!("{program}: {message}");
610 ExitCode::FAILURE
611 }
612 }
613}
614
615fn cli_run(program: &str, args: &[String]) -> Result<(), CliError> {
616 let mut platform: Option<String> = None;
617 let mut mcp_bin: Option<PathBuf> = None;
618 let mut wal: Option<PathBuf> = None;
619 let mut out: Option<PathBuf> = None;
620 let mut index = 0;
621 while index < args.len() {
622 match args[index].as_str() {
623 "-h" | "--help" => {
624 print!("{USAGE}");
625 return Ok(());
626 }
627 "--platform" => platform = Some(next_value(args, &mut index, "--platform")?),
628 "--bin" => mcp_bin = Some(next_path(args, &mut index, "--bin")?),
629 "--wal" => wal = Some(next_path(args, &mut index, "--wal")?),
630 "--out" => out = Some(next_path(args, &mut index, "--out")?),
631 other => {
632 return Err(CliError::Usage(format!(
633 "未知参数: {other}(用 --help 看用法)"
634 )))
635 }
636 }
637 index += 1;
638 }
639 let Some(platform) = platform else {
640 return Err(CliError::Usage(format!(
641 "缺少 --platform <名>(支持矩阵:claude-code / codex / kimi / trae / workbuddy / \
642 deepseek-harness / openclaw / hermes;--help 看用法;{program} 是 Wanning 的配置生成器)"
643 )));
644 };
645 let platform = parse_platform(&platform).map_err(|e| CliError::Usage(e.message()))?;
646
647 let resolved =
648 resolve(&GenerateOptions { mcp_bin, wal }).map_err(|e| CliError::Failed(e.message()))?;
649 let artifact = generate_with(platform, &resolved).map_err(|e| CliError::Failed(e.message()))?;
650 println!("# Wanning 支付闸 — 配置生成完成");
651 for note in &artifact.notes {
652 println!("# {note}");
653 }
654 for note in first_run_notes() {
655 println!("# {note}");
656 }
657 println!();
658
659 match out {
660 Some(out) => {
661 let content = artifact.content;
662 std::fs::OpenOptions::new()
663 .write(true)
664 .create_new(true)
665 .open(&out)
666 .and_then(|mut file| std::io::Write::write_all(&mut file, content.as_bytes()))
667 .map_err(|e| {
668 CliError::Failed(if e.kind() == std::io::ErrorKind::AlreadyExists {
669 format!(
670 "拒绝覆盖:{} 已存在。动别人工具的配置 = 危险动作;请先人工确认,\
671 换个文件名,或把已有内容备份后删掉再生成",
672 out.display()
673 )
674 } else {
675 format!("写 {} 失败: {e}", out.display())
676 })
677 })?;
678 println!("已写入:{}(绝不覆盖已存在文件)", out.display());
679 }
680 None => print!("{content}", content = artifact.content),
681 }
682 Ok(())
683}
684
685fn next_value(args: &[String], index: &mut usize, flag: &str) -> Result<String, CliError> {
686 *index += 1;
687 args.get(*index)
688 .cloned()
689 .ok_or_else(|| CliError::Usage(format!("{flag} 缺少取值(用 --help 看用法)")))
690}
691
692fn next_path(args: &[String], index: &mut usize, flag: &str) -> Result<PathBuf, CliError> {
693 Ok(PathBuf::from(next_value(args, index, flag)?))
694}