@x-otto/cli 0.0.1-alpha.16-debug → 0.0.1-alpha.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/README.md +65 -65
  2. package/dist/{auth-command-CKlvZtbJ.js → auth-command-kkJh-O5y.js} +2 -2
  3. package/dist/auth-command-kkJh-O5y.js.map +1 -0
  4. package/dist/build-id-B2hTdEbZ.js +3 -0
  5. package/dist/build-id-B2hTdEbZ.js.map +1 -0
  6. package/dist/bundled-extensions/plugin-otto-wire-protocols/plugin-dist/meta.json +1 -1
  7. package/dist/bundled-extensions/plugin-otto-wire-protocols/plugin-dist/plugin.cjs +37144 -193
  8. package/dist/bundled-extensions/plugin-otto-wire-protocols/src/anthropic-messages.ts +2 -2
  9. package/dist/bundled-extensions-C3n8jg20.js +3 -0
  10. package/dist/bundled-extensions-C3n8jg20.js.map +1 -0
  11. package/dist/{command-dispatcher-DibEV53Q.js → command-dispatcher-CbFjx7bg.js} +6 -6
  12. package/dist/command-dispatcher-CbFjx7bg.js.map +1 -0
  13. package/dist/{config-migration-DatflIxs.js → config-migration-0zdXsEDB.js} +1 -1
  14. package/dist/{config-migration-DatflIxs.js.map → config-migration-0zdXsEDB.js.map} +1 -1
  15. package/dist/create-host-app-BXQ8bU-B.js +3 -0
  16. package/dist/create-host-app-BXQ8bU-B.js.map +1 -0
  17. package/dist/create-host-app-DXII2WTM.js +2 -0
  18. package/dist/{daemon-COESl-zU.js → daemon-BtiaXSxn.js} +1 -1
  19. package/dist/{daemon-COESl-zU.js.map → daemon-BtiaXSxn.js.map} +1 -1
  20. package/dist/{daemon-base-YOWgYYN7.js → daemon-base-DKeB2Nyv.js} +1 -1
  21. package/dist/{daemon-base-YOWgYYN7.js.map → daemon-base-DKeB2Nyv.js.map} +1 -1
  22. package/dist/{debug-CiosFwme.js → debug-DfgcrVCI.js} +2 -2
  23. package/dist/{debug-CiosFwme.js.map → debug-DfgcrVCI.js.map} +1 -1
  24. package/dist/discover-with-bundled-B5d4q___.js +2 -0
  25. package/dist/discover-with-bundled-CF8qFP41.js +3 -0
  26. package/dist/discover-with-bundled-CF8qFP41.js.map +1 -0
  27. package/dist/doctor-DbQC8Z7p.js +36 -0
  28. package/dist/doctor-DbQC8Z7p.js.map +1 -0
  29. package/dist/{doctor-attach-_zeIJewx.js → doctor-attach-OJHn8L52.js} +2 -2
  30. package/dist/{doctor-attach-_zeIJewx.js.map → doctor-attach-OJHn8L52.js.map} +1 -1
  31. package/dist/{doctor-cdp-K_mIFRfA.js → doctor-cdp-D66ChCCN.js} +1 -1
  32. package/dist/{doctor-cdp-K_mIFRfA.js.map → doctor-cdp-D66ChCCN.js.map} +1 -1
  33. package/dist/{doctor-heap-Rr2nNvcw.js → doctor-heap-Brsro5QJ.js} +2 -2
  34. package/dist/{doctor-heap-Rr2nNvcw.js.map → doctor-heap-Brsro5QJ.js.map} +1 -1
  35. package/dist/{doctor-ps-OL3FGQYu.js → doctor-ps-B4Syy9qa.js} +2 -2
  36. package/dist/{doctor-ps-OL3FGQYu.js.map → doctor-ps-B4Syy9qa.js.map} +1 -1
  37. package/dist/{extension-DAex6RFK.js → extension-DLFCUBJr.js} +41 -33
  38. package/dist/extension-DLFCUBJr.js.map +1 -0
  39. package/dist/extension-I87MkZYp.js +2 -0
  40. package/dist/{extension-plugin-D7gnpllF.js → extension-plugin-DoEcnDu5.js} +4 -4
  41. package/dist/{extension-plugin-D7gnpllF.js.map → extension-plugin-DoEcnDu5.js.map} +1 -1
  42. package/dist/extension-plugin-VUdbaAN7.js +2 -0
  43. package/dist/feedback-DwQu2BmU.js +21 -0
  44. package/dist/feedback-DwQu2BmU.js.map +1 -0
  45. package/dist/feedback-j4tDn2uI.js +2 -0
  46. package/dist/{feishu-setup-DIcd3PMK.js → feishu-setup-C2UDdpIm.js} +2 -2
  47. package/dist/{feishu-setup-DIcd3PMK.js.map → feishu-setup-C2UDdpIm.js.map} +1 -1
  48. package/dist/ftue-doctor-recovery-DhiseXnP.js +3 -0
  49. package/dist/ftue-doctor-recovery-DhiseXnP.js.map +1 -0
  50. package/dist/index.d.ts.map +1 -1
  51. package/dist/index.js +10 -10
  52. package/dist/index.js.map +1 -1
  53. package/dist/install-DkJgbg2p.js +2 -0
  54. package/dist/{install-DlVJRHRT.js → install-hTDjeo61.js} +1 -1
  55. package/dist/{install-DlVJRHRT.js.map → install-hTDjeo61.js.map} +1 -1
  56. package/dist/{mcp-DbTYT7hB.js → mcp-BDfBf6gx.js} +1 -1
  57. package/dist/{mcp-DbTYT7hB.js.map → mcp-BDfBf6gx.js.map} +1 -1
  58. package/dist/{migrate-boDXhEyV.js → migrate-4QDwYLf6.js} +2 -2
  59. package/dist/{migrate-boDXhEyV.js.map → migrate-4QDwYLf6.js.map} +1 -1
  60. package/dist/{model-auth-Dih25xOB.js → model-auth-B6ufdM5K.js} +2 -2
  61. package/dist/{model-auth-Dih25xOB.js.map → model-auth-B6ufdM5K.js.map} +1 -1
  62. package/dist/model-auth-DMczbHA9.js +2 -0
  63. package/dist/{observe-CVg-MRM2.js → observe-C2jjDNDa.js} +2 -2
  64. package/dist/{observe-CVg-MRM2.js.map → observe-C2jjDNDa.js.map} +1 -1
  65. package/dist/{parse-cli-Dr-Fl0on.js → parse-cli-Cw5qyM5B.js} +2 -2
  66. package/dist/{parse-cli-Dr-Fl0on.js.map → parse-cli-Cw5qyM5B.js.map} +1 -1
  67. package/dist/{persistenced-CCwHu1Ww.js → persistenced-CcAhVOE7.js} +2 -2
  68. package/dist/{persistenced-CCwHu1Ww.js.map → persistenced-CcAhVOE7.js.map} +1 -1
  69. package/dist/plugin-assembly-check-CvuJ4dHf.js +3 -0
  70. package/dist/plugin-assembly-check-CvuJ4dHf.js.map +1 -0
  71. package/dist/{proxy-evg7ZvoD.js → proxy-CMQBvdZC.js} +1 -1
  72. package/dist/{proxy-evg7ZvoD.js.map → proxy-CMQBvdZC.js.map} +1 -1
  73. package/dist/proxy-TzOLqtkc.js +2 -0
  74. package/dist/{ps-DKrluEuy.js → ps-BWGntaR4.js} +1 -1
  75. package/dist/{ps-DKrluEuy.js.map → ps-BWGntaR4.js.map} +1 -1
  76. package/dist/{registry-D0NONjLv.js → registry-Cr5To_5r.js} +3 -3
  77. package/dist/registry-Cr5To_5r.js.map +1 -0
  78. package/dist/registry-DAJ7oZTe.js +2 -0
  79. package/dist/{remote-persistence-server-B_hONoVR.js → remote-persistence-server-CLI-y-zp.js} +2 -2
  80. package/dist/{remote-persistence-server-B_hONoVR.js.map → remote-persistence-server-CLI-y-zp.js.map} +1 -1
  81. package/dist/{run-attach-Ci5jYyCa.js → run-attach-DZmBqVjI.js} +2 -2
  82. package/dist/{run-attach-Ci5jYyCa.js.map → run-attach-DZmBqVjI.js.map} +1 -1
  83. package/dist/{serve-pVR6UIT9.js → serve-vWHGydiC.js} +3 -3
  84. package/dist/serve-vWHGydiC.js.map +1 -0
  85. package/dist/{service-client-BLNeugM9.js → service-client-Cr4sxDVM.js} +1 -1
  86. package/dist/{service-client-BLNeugM9.js.map → service-client-Cr4sxDVM.js.map} +1 -1
  87. package/dist/{time-travel-ZozuPPI-.js → time-travel-D1sg5dEb.js} +2 -2
  88. package/dist/{time-travel-ZozuPPI-.js.map → time-travel-D1sg5dEb.js.map} +1 -1
  89. package/dist/{version-Cd8HZ7ua.js → version-BaBgJOxV.js} +2 -2
  90. package/dist/{version-Cd8HZ7ua.js.map → version-BaBgJOxV.js.map} +1 -1
  91. package/package.json +3 -3
  92. package/dist/auth-command-CKlvZtbJ.js.map +0 -1
  93. package/dist/build-id-CIFJCmlj.js +0 -3
  94. package/dist/build-id-CIFJCmlj.js.map +0 -1
  95. package/dist/command-dispatcher-DibEV53Q.js.map +0 -1
  96. package/dist/discover-with-bundled-wSuXDvNc.js +0 -3
  97. package/dist/discover-with-bundled-wSuXDvNc.js.map +0 -1
  98. package/dist/doctor-C84-hhEr.js +0 -30
  99. package/dist/doctor-C84-hhEr.js.map +0 -1
  100. package/dist/extension-C3AP3KxC.js +0 -2
  101. package/dist/extension-DAex6RFK.js.map +0 -1
  102. package/dist/extension-plugin-fPDz7XlG.js +0 -2
  103. package/dist/feedback-BVl4sRah.js +0 -21
  104. package/dist/feedback-BVl4sRah.js.map +0 -1
  105. package/dist/feedback-DAY-1bJ0.js +0 -2
  106. package/dist/install-WCcQ9k9M.js +0 -2
  107. package/dist/model-auth-BxHBaDQx.js +0 -2
  108. package/dist/proxy-BvdBsB1O.js +0 -2
  109. package/dist/registry-B3nfN62q.js +0 -2
  110. package/dist/registry-D0NONjLv.js.map +0 -1
  111. package/dist/serve-pVR6UIT9.js.map +0 -1
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import{i as e}from"./install-hTDjeo61.js";export{e as handleInstall};
@@ -8,4 +8,4 @@ import{InstallManager as e,defaultPostInstallExecutor as t}from"@x-otto/coding";
8
8
  `)}),g(i,h);return}if(f.postinstall)if(f.postinstall.success)i.tui.info.showToast(o,`"${f.name}" installed (${_}). postinstall: ok`);else{let e=f.postinstall.timeout?`timed out (30s)`:f.postinstall.error??`exit=${f.postinstall.exitCode??`N/A`}`;i.tui.info.showToast(o,`"${f.name}" installed (${_}). postinstall: FAILED (${e})`);let t=[`Plugin "${f.name}" postinstall hook failed:`];t.push(` command: ${f.postinstall.command}`),f.postinstall.exitCode!=null&&t.push(` exit code: ${f.postinstall.exitCode}`),f.postinstall.stderr&&t.push(` stderr: ${f.postinstall.stderr}`),f.postinstall.truncated&&t.push(` (output truncated)`),i.tui.messages.pushMessage({role:`command_echo`,command:`/install`,status:`error`,summary:`"${f.name}" postinstall hook failed`,detail:t.join(`
9
9
  `)})}else i.tui.info.showToast(o,r.t(`cmd.installedTo`,{name:f.name??`extension`,scope:_,seg:f.targetPath?` (${f.targetPath})`:``}));f.warnings&&f.warnings.length>0&&i.tui.messages.pushMessage({role:`command_echo`,command:`/install`,status:`warn`,summary:`${f.warnings.length} 项警告`,detail:f.warnings.map(e=>`⚠ ${e}`).join(`
10
10
  `)}),g(i,h)}function g(e,t){if(t){e.tui.messages.pushMessage({role:`command_echo`,command:`/install`,status:`error`,summary:r.t(`cmd.installedRefreshErr`,{err:t})});return}e.tui.info.showToast(r.t(`cmd.titleInstall`),r.t(`cmd.capRefreshedInstall`))}export{o as a,a as i,p as n,l as r,c as t};
11
- //# sourceMappingURL=install-DlVJRHRT.js.map
11
+ //# sourceMappingURL=install-hTDjeo61.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"install-DlVJRHRT.js","names":[],"sources":["../src/commands/handlers/install.ts"],"sourcesContent":["/**\n * handlers/install.ts — /install 命令处理\n *\n * /install [--global] [--force] [--dry-run] [--as <type>] [--type <src>] <source>\n *\n * 安装 skill/plugin/mcp 包到项目 (.otto/) 或全局 (~/.otto/)。\n * 支持 file/url/git/npm 四种来源,自动检测内容类型。\n */\nimport { i18next } from '@x-otto/tui'\nimport { InstallManager, defaultPostInstallExecutor } from '@x-otto/coding'\nimport { formatBytes } from '@x-otto/shared'\nimport type { InstallOptions, InstallProgress, InstallResult } from '@x-otto/coding'\nimport type { McpServersConfig } from '@x-otto/setting'\nimport type { SelectOption } from '@x-otto/tui'\nimport type { CliContext } from '../../command-dispatcher'\n\nconst VALID_AS = new Set(['plugin', 'skill', 'mcp'])\n\nexport async function handleInstall(\n args: string[],\n ctx: CliContext,\n): Promise<void> {\n const parsed = parseArgs(args)\n if (!parsed.source) {\n ctx.tui.info.showToast(i18next.t('cmd.titleInstall'), i18next.t('cmd.installUsage'))\n return\n }\n\n const isGlobal = parsed.flags.has('--global')\n const isHere = parsed.flags.has('--here')\n\n if (isGlobal && isHere) {\n ctx.tui.info.showToast(i18next.t('cmd.titleInstall'), i18next.t('cmd.installMutuallyExclusive'))\n return\n }\n\n const options: InstallOptions = {\n // --here:锚到当前命令目录,绕过 ctx.app.workspaceDir 的向上工作区根查找。\n workspaceDir: isHere ? process.cwd() : ctx.app.workspaceDir,\n global: isGlobal,\n force: parsed.flags.has('--force'),\n dryRun: parsed.flags.has('--dry-run'),\n as: parsed.as,\n type: parsed.type,\n env: parsed.env,\n subPath: parsed.subPath,\n ref: parsed.ref,\n postInstallExecutor: defaultPostInstallExecutor,\n // RFC-131: 进度回调 — 仅非 dry-run 时注入\n onProgress: !parsed.dryRun ? (p) => {\n const prefix = p.total && p.current ? `[${p.current}/${p.total}] ` : ''\n ctx.tui.info.showToast('Installing', `${prefix}${p.message}`)\n } : undefined,\n }\n\n // 确认优先(P3 安全边界):始终先 dry-run 取 plan 展示——含将运行的 postinstall/setup\n // shell 命令——确认后才真装(项目级插件真装含 auto-trust + 立即跑 postinstall)。\n // --dry-run 仅预览,不提供安装按钮。\n await showConfirmPanel(parsed.source, options, ctx, !parsed.dryRun)\n}\n\n// ── 参数解析 ──\n\ninterface ParsedArgs {\n source: string | undefined\n flags: Set<string>\n as: 'plugin' | 'skill' | 'mcp' | undefined\n type: 'file' | 'url' | 'git' | 'npm' | undefined\n env: Record<string, string> | undefined\n /** RFC-201:monorepo 子目录(--sub-path,registry 条目 subPath 的 CLI 透传面)。 */\n subPath: string | undefined\n /**\n * RFC-221 D2/T221-3:git 来源的 tag/branch 名(`--ref`),透传到\n * `InstallOptions.ref` → `resolveGitSource(gitUrl, ref)`。仅对 git 来源有效,\n * 其余来源类型忽略此参数(不报错,与 subPath 对非 git/归档来源的静默忽略语义一致)。\n * 不支持 commit hash——见 `git-resolver.ts` 头部注释(主流托管方服务端默认拒绝)。\n */\n ref: string | undefined\n dryRun: boolean\n}\n\n/** 导出供单测覆盖 `--ref`/`--sub-path` 等标志解析行为(RFC-221 T221-3),非公开 API。 */\nexport function parseArgs(args: string[]): ParsedArgs {\n const flags = new Set<string>()\n let as: 'plugin' | 'skill' | 'mcp' | undefined\n let type: 'file' | 'url' | 'git' | 'npm' | undefined\n let env: Record<string, string> | undefined\n let subPath: string | undefined\n let ref: string | undefined\n let dryRun = false\n let i = 0\n\n while (i < args.length) {\n const a = args[i]!\n if (a === '--as') {\n i++\n const val = args[i]?.toLowerCase()\n if (VALID_AS.has(val ?? '')) as = val as typeof as\n } else if (a === '--type') {\n i++\n const val = args[i]?.toLowerCase()\n if (val === 'file' || val === 'url' || val === 'git' || val === 'npm') {\n type = val\n }\n } else if (a === '--sub-path') {\n // RFC-201 M0/M1:monorepo 子目录安装(registry 条目 subPath 的 CLI 透传面)\n i++\n subPath = args[i]\n } else if (a === '--ref') {\n // RFC-221 D2/T221-3:git 来源 tag/branch 指定安装\n i++\n ref = args[i]\n } else if (a === '--env') {\n i++\n const kv = args[i]\n if (kv) {\n const eq = kv.indexOf('=')\n if (eq > 0) {\n env = env ?? {}\n env[kv.slice(0, eq)] = kv.slice(eq + 1)\n }\n }\n } else if (a.startsWith('--')) {\n flags.add(a)\n } else {\n // positional (source)\n dryRun = flags.has('--dry-run')\n const rest = args.slice(i).filter((x) => !x.startsWith('--'))\n return { source: rest[0], flags, as, type, env, subPath, ref, dryRun }\n }\n i++\n }\n\n dryRun = flags.has('--dry-run')\n return { source: undefined, flags, as, type, env, subPath, ref, dryRun }\n}\n\n// ── 确认面板 ──\n\n/**\n * 先 dry-run 取安装 plan 并展示确认屏。\n *\n * RFC-226:命令发出到确认框出现之间新增 loading 反馈——dry-run 探测(git clone/npm pack/\n * url 下载 + 内容检测)本身耗时不短,此前全程静默。改造后:① 立即展示 loading 态弹窗\n * (options=[] 空选项态,D2);② dry-run 探测阶段(resolving/detecting)的进度通过局部\n * `onProgress`(不复用 `options.onProgress` toast 版本,D5b/R6)实时刷新 loading 文案;\n * ③ dry-run 完成后原地过渡为确认框或错误态(D3/D5a)。\n *\n * **修复**(dialog 栈堆积):此前每次状态转换(loading→进度更新→确认/错误/预览)都调用\n * `ctx.tui.modal.openModal({ kind: 'select' })`,而同步 select 路径每次 `router.navigate` 都是\n * push 新栈条目,导致对话框层层堆积、用户要按多次 Esc 才能退出。现改用框架层 `async` 字段:\n * loading 态由框架自动展示,dry-run 在 async promise 内完成,resolve 后框架用 `replace: true`\n * 原地过渡为真实内容。最终的确认/错误/预览弹窗由 `onSelect` 回调内驱动(此时 loading 弹窗\n * 已被框架 replace 为最终态,Esc 一次即可退出)。\n *\n * @param proceed true=确认后真装([安装, 取消]);false=纯预览(--dry-run,仅[关闭])。\n */\nasync function showConfirmPanel(\n source: string,\n options: InstallOptions,\n ctx: CliContext,\n proceed: boolean,\n): Promise<void> {\n const manager = new InstallManager()\n const loadingTitle = `${i18next.t('cmd.titleInstall')} — \"${source}\"`\n\n // dry-run 专用局部 onProgress——不复用 options.onProgress(toast 版本,只服务\n // executeInstall 真实安装路径)。探测阶段进度文案由 async promise 内部记录,\n // resolve 时作为 subtitle 回灌(框架 replace 一次性更新)。\n let progressMessage = i18next.t('cmd.installLoadingHint')\n const dryOptions: InstallOptions = {\n ...options,\n dryRun: true,\n onProgress: (p: InstallProgress) => {\n progressMessage = p.message\n },\n }\n\n ctx.tui.modal.openModal({\n kind: 'select',\n title: loadingTitle,\n subtitle: `${i18next.t('cmd.titleDryRun')}...\\n${progressMessage}`,\n layout: 'buttons',\n options: [],\n onCancel: () => {\n ctx.tui.info.showToast(i18next.t('cmd.titleInstall'), i18next.t('cmd.selectCancelled'))\n },\n // async:dry-run 探测在 promise 内完成,resolve 后框架用 replace:true 原地过渡。\n async: (async (): Promise<{ title?: string; subtitle?: string; options: SelectOption[] }> => {\n const result = await manager.install(\n source,\n dryOptions,\n () => ctx.app.settings.get('mcp_servers') as Record<string, unknown> | undefined,\n async (servers) => {\n await ctx.app.settings.persist({ mcp_servers: servers as McpServersConfig })\n },\n )\n\n if (!result.success) {\n return {\n title: loadingTitle,\n subtitle: i18next.t('cmd.dryRunDetectFailed', { err: result.error ?? i18next.t('cmd.unknownError') }),\n options: [{ label: i18next.t('cmd.commonClose'), value: 'close', color: 'gray' }],\n }\n }\n\n if (!result.plan || result.plan.length === 0) {\n return {\n title: loadingTitle,\n subtitle: i18next.t('cmd.dryRunPreviewOk', { name: result.name ?? i18next.t('cmd.unknownLabel') }),\n options: [{ label: i18next.t('cmd.commonClose'), value: 'close', color: 'gray' }],\n }\n }\n\n const plan = result.plan.filter((l) => !l.startsWith(i18next.t('cmd.planSourcePrefix')))\n const name = result.name ?? i18next.t('cmd.unknownLabel')\n const isMultiSkill = result.results !== undefined\n\n // RFC-130: 多 skill 仓库 → 聚合面板\n if (isMultiSkill) {\n const count = result.results!.length\n const title = `${i18next.t('cmd.titleInstall')} — \"${count} skills (${source})\"`\n\n if (!proceed) {\n return {\n title,\n subtitle: plan.join('\\n'),\n options: [{ label: i18next.t('cmd.commonClose'), value: 'close', color: 'gray' }],\n }\n }\n\n return {\n title,\n subtitle: plan.join('\\n'),\n options: [\n { label: i18next.t('cmd.installActionLabel'), value: 'install', color: 'accent' },\n { label: i18next.t('cmd.commonCancel'), value: 'cancel', color: 'gray' },\n ],\n }\n }\n\n // --dry-run:纯预览,不提供安装入口(修正旧语义——旧 dry-run 的\"安装\"按钮会真装)。\n if (!proceed) {\n return {\n title: `${i18next.t('cmd.titleDryRun')} — \"${name}\"`,\n subtitle: plan.join('\\n'),\n options: [{ label: i18next.t('cmd.commonClose'), value: 'close', color: 'gray' }],\n }\n }\n\n return {\n title: `${i18next.t('cmd.titleInstall')} — \"${name}\"`,\n subtitle: plan.join('\\n'),\n options: [\n { label: i18next.t('cmd.installActionLabel'), value: 'install', color: 'accent' },\n { label: i18next.t('cmd.commonCancel'), value: 'cancel', color: 'gray' },\n ],\n }\n })(),\n onSelect: async (v: string) => {\n if (v === 'cancel' || v === 'close') return\n if (v === 'install') {\n await executeInstall(source, { ...options, dryRun: false }, ctx)\n }\n },\n })\n}\n\n// ── 执行安装 ──\n\n/**\n * RFC-224 D2/D3:安装进度 fraction 计算——字节级进度(bytesDownloaded/bytesTotal)\n * 优先于多 skill 计数(current/total),两者互斥语义(见 InstallProgress JSDoc)。\n * 导出供单测覆盖(`install-progress-bytes.test.ts`),不作为公开 API。\n */\nexport function computeProgressFraction(p: {\n bytesDownloaded?: number\n bytesTotal?: number\n current?: number\n total?: number\n}): number {\n if (p.bytesTotal !== undefined && p.bytesDownloaded !== undefined) {\n return p.bytesDownloaded / p.bytesTotal\n }\n if (p.current && p.total) {\n return p.current / p.total\n }\n return 0\n}\n\n/**\n * RFC-224 D2/R4:安装进度 detail 文案格式化——bytesDownloaded 存在时渲染字节数\n * (`formatBytes`),Content-Length 缺失(bytesTotal 为 undefined)时降级为纯字节\n * 计数,不编造总数;否则保持原始 message 不变。导出供单测覆盖。\n */\nexport function formatProgressDetail(p: {\n message: string\n bytesDownloaded?: number\n bytesTotal?: number\n}): string {\n if (p.bytesDownloaded === undefined) return p.message\n if (p.bytesTotal !== undefined) {\n return `${p.message} ${formatBytes(p.bytesDownloaded)} / ${formatBytes(p.bytesTotal)}`\n }\n return `${p.message} ${formatBytes(p.bytesDownloaded)}`\n}\n\n/** `Refreshing…` 在四阶段清单中的下标(RFC-286 D4:该阶段由 CLI 持有)。 */\nconst REFRESHING_PHASE_INDEX = 3\n\n/** partial 面板最多直接列出的失败项条数,超出部分转由 command_echo 承载(RFC-286 D6)。 */\nconst PARTIAL_DETAIL_MAX_ITEMS = 3\n\n/**\n * RFC-286 D6:把失败子项渲染成面板可读的一行摘要。\n *\n * 面板是窄栏终端 UI,超过 `PARTIAL_DETAIL_MAX_ITEMS` 条时只列前几条并指向消息流\n * ——完整清单由 `executeInstall` 的 command_echo 承载(R5:不依赖 toast 单槽通道)。\n */\n/** 拼接多条子错误前去掉句末句号,避免出现「。;」这类双标点(RFC-290 T290-11 收尾)。 */\nfunction stripTrailingPeriod(text: string): string {\n return text.replace(/[。.]\\s*$/, '')\n}\n\nfunction describeFailures(\n subs: Array<{ success: boolean; name?: string; error?: string }> | undefined,\n unknownErrorLabel: string,\n): string {\n const failed = (subs ?? []).filter((r) => !r.success)\n if (failed.length === 0) return unknownErrorLabel\n const shown = failed.slice(0, PARTIAL_DETAIL_MAX_ITEMS)\n const details = shown\n .map((r) => `${r.name ?? '?'} —— ${stripTrailingPeriod(r.error ?? unknownErrorLabel)}`)\n .join(';')\n return failed.length > shown.length ? `${details}${i18next.t('install.failureOverflow')}` : details\n}\n\n/**\n * RFC-290 D2:冲突类失败 → `--force` 建议。\n *\n * **只读结构化的 `failureKind`,绝不匹配 `error` 文案**——错误文案是本地化的人读文本,\n * 子串嗅探(如判断是否含\"已存在\")会在换 locale 或改措辞时静默失效。\n * 单结果读顶层字段,多结果要求**全部**失败项同为 conflict(混合原因不给建议,R4)。\n */\nfunction resolveConflictNextStep(\n result: { failureKind?: 'conflict' },\n subs: Array<{ success: boolean; failureKind?: 'conflict' }> | undefined,\n): { nextStep: { kind: 'force'; label: string } } | undefined {\n const failed = (subs ?? []).filter((r) => !r.success)\n const isConflict = subs\n ? failed.length > 0 && failed.every((r) => r.failureKind === 'conflict')\n : result.failureKind === 'conflict'\n if (!isConflict) return undefined\n return { nextStep: { kind: 'force', label: i18next.t('install.nextStepForce') } }\n}\n\n/**\n * RFC-286 D2/D3/D6:安装终态裁决——把 `InstallResult` + reload 结果映射为进度对话框终态。\n *\n * 映射表(RFC-286 D2 唯一定义处):\n * - 全部成功且 reload 成功 → `success`(2s 自动关闭)\n * - 部分成功(ok>0 && fail>0)→ `partial`(不自动关闭)\n * - 全部失败 → `error`(不自动关闭)\n * - 产物已装、仅 reload 失败 → `partial`(degraded success,提示 /reload)\n *\n * 三种非 success 终态都显式下发 `percent` 与 `phaseStatuses`(R4),失败阶段标 `'error'`\n * ——该取值此前在全仓无任何生产者,`ProgressDialog` 的行内错误渲染是死代码,本函数激活它。\n *\n * 导出供单测覆盖(`install-terminal-state.test.ts`),不作为公开 API。\n */\nexport function computeTerminalProgress(\n result: {\n success: boolean\n error?: string\n failureKind?: 'conflict'\n results?: Array<{ success: boolean; name?: string; error?: string; failureKind?: 'conflict' }>\n },\n reloadError: string | undefined,\n unknownErrorLabel: string,\n): {\n status: 'success' | 'partial' | 'error'\n percent: number\n phaseStatuses: Array<'pending' | 'active' | 'done' | 'error'>\n errorMessage?: string\n nextStep?: { kind: 'force' | 'reload'; label: string }\n} {\n const subs = result.results\n const okCount = subs ? subs.filter((r) => r.success).length : result.success ? 1 : 0\n const failCount = subs ? subs.length - okCount : result.success ? 0 : 1\n\n // 全部失败 → error。失败阶段停在 `installing`(写盘阶段),而非 refreshing——\n // 安装根本没成功,不该显示\"刷新\"阶段被走到过。\n //\n // R3:错误文案按「顶层 error → 子项 error 还原 → 兜底文案」逐级降级。兜底只在\n // 上游连子项原因都没给时才允许出现,绝不用它掩盖上游缺口(本次事故的根因模式)。\n if (okCount === 0) {\n return {\n status: 'error',\n percent: 100,\n phaseStatuses: ['done', 'done', 'error', 'pending'],\n errorMessage: result.error ?? describeFailures(subs, unknownErrorLabel),\n ...(resolveConflictNextStep(result, subs) ?? {}),\n }\n }\n\n // 有成功项但也有失败项 → partial\n if (failCount > 0) {\n return {\n status: 'partial',\n percent: 100,\n phaseStatuses: ['done', 'done', 'error', 'done'],\n errorMessage:\n i18next.t('install.partialCounts', { ok: okCount, fail: failCount }) +\n describeFailures(subs, unknownErrorLabel),\n ...(resolveConflictNextStep(result, subs) ?? {}),\n }\n }\n\n // 全部装好但能力刷新失败 → degraded success(产物已落盘,不能报 error)\n if (reloadError) {\n return {\n status: 'partial',\n percent: 100,\n phaseStatuses: ['done', 'done', 'done', 'error'],\n errorMessage: i18next.t('cmd.installedRefreshErr', { err: reloadError }),\n nextStep: { kind: 'reload', label: i18next.t('install.nextStepReload') },\n }\n }\n\n return { status: 'success', percent: 100, phaseStatuses: ['done', 'done', 'done', 'done'] }\n}\n\nfunction applyTerminalProgress(\n ctx: CliContext,\n result: InstallResult,\n reloadError: string | undefined,\n): void {\n ctx.tui.modal.updateProgress(computeTerminalProgress(result, reloadError, i18next.t('cmd.unknownError')))\n}\n\nasync function executeInstall(\n source: string,\n options: InstallOptions,\n ctx: CliContext,\n): Promise<void> {\n const manager = new InstallManager()\n const label = i18next.t('cmd.titleInstall')\n\n // 打开进度条 dialog(聚焦阻断输入)\n // RFC-290 D5:阶段名与标题走 i18n——此前是硬编码英文字面量,中文环境下面板会突兀\n // 蹦出英文,与其余 20+ 弹窗不一致。\n const phases = [\n i18next.t('install.phaseResolving'),\n i18next.t('install.phaseDetecting'),\n i18next.t('install.phaseInstalling'),\n i18next.t('install.phaseRefreshing'),\n ]\n ctx.tui.modal.openModal({ kind: 'progress', title: i18next.t('install.title', { source }), phases })\n\n // 安装进度 → dialog 更新\n const phaseMap: Record<string, number> = { resolving: 0, detecting: 1, installing: 2, refreshing: 3 }\n const progressOptions: InstallOptions = {\n ...options,\n onProgress: (p) => {\n const idx = phaseMap[p.phase] ?? -1\n if (idx >= 0) {\n const statuses = phases.map((_, i) => {\n if (i < idx) return 'done' as const\n if (i === idx) return 'active' as const\n return 'pending' as const\n })\n const fraction = computeProgressFraction(p)\n const detail = formatProgressDetail(p)\n ctx.tui.modal.updateProgress({\n phaseIndex: idx,\n phaseStatuses: statuses,\n percent: Math.round(((idx + fraction) / phases.length) * 90),\n detail,\n subProgress: p.current !== undefined && p.total !== undefined ? { current: p.current, total: p.total } : undefined,\n })\n }\n },\n }\n\n const result = await manager.install(\n source,\n progressOptions,\n () => ctx.app.settings.get('mcp_servers') as Record<string, unknown> | undefined,\n async (servers) => {\n await ctx.app.settings.persist({ mcp_servers: servers as McpServersConfig })\n },\n )\n\n // ── 安装写盘结束 → 真实能力刷新(RFC-286 D4/D5) ──\n //\n // `refreshing` 阶段由本处 fire——`InstallManager` 不再自行 fire(它没有 App 句柄,\n // 其 refreshing 与真实 reload 无关,属伪进度)。谁执行谁上报:面板上的\n // \"Refreshing…\" 从此对应下方 `reloadCapabilities()` 的真实耗时。\n //\n // 时序变更:终态裁决移到 reload **之后**(此前在 reload 前就置 success 并开始\n // 2s 自动关闭倒计时)。这样 reload 失败才能降级为 partial 呈现给用户。\n const installOk = result.success || (result.results?.some((r) => r.success) ?? false)\n let reloadError: string | undefined\n if (installOk) {\n ctx.tui.modal.updateProgress({\n phaseIndex: REFRESHING_PHASE_INDEX,\n phaseStatuses: ['done', 'done', 'done', 'active'],\n percent: 90,\n detail: i18next.t('install.reloadingDetail'),\n })\n try {\n await ctx.app.reloadCapabilities()\n ctx.tui.plugins.setPluginRegistrations(ctx.app.getPluginRegistrations())\n } catch (err) {\n reloadError = err instanceof Error ? err.message : String(err)\n }\n }\n\n // 安装结束 → dialog 终态(RFC-286 D2/D3/R4:终态必须同时下发 percent 与\n // phaseStatuses,不得让面板继承中途快照——此前只传 status/errorMessage,\n // 导致失败面板冻结在 68% 且阶段行仍是 active,看起来像还在刷新)。\n applyTerminalProgress(ctx, result, reloadError)\n\n if (!result.success) {\n // 多 skill 部分失败时继续(D5:独立安装不回滚)\n // 全部失败时展示第一个错误\n if (!result.results || result.results.every((r) => !r.success)) {\n ctx.tui.info.showToast(label, i18next.t('cmd.installFailed', { err: result.error ?? i18next.t('cmd.unknownError') }))\n if (!result.results) return\n // 多 skill 全部失败但仍然有 results→继续执行 toast 汇总\n }\n }\n\n const scopeLabel = options.global ? i18next.t('cmd.scopeGlobal') : i18next.t('cmd.scopeProject')\n\n // RFC-130: 多 skill → 逐条 toast + 汇总\n if (result.results) {\n let ok = 0\n let fail = 0\n for (const r of result.results) {\n if (r.success) {\n ok++\n ctx.tui.info.showToast(\n label,\n i18next.t('cmd.installedTo', { name: r.name ?? 'extension', scope: scopeLabel, seg: r.targetPath ? ` (${r.targetPath})` : '' }),\n )\n } else {\n fail++\n ctx.tui.info.showToast(label, i18next.t('cmd.installFailed', { err: r.error ?? i18next.t('cmd.unknownError') }))\n }\n }\n const summary = `${ok} installed${fail > 0 ? `, ${fail} failed` : ''}`\n ctx.tui.info.showToast(label, summary)\n\n // RFC-286 D6/R5:失败清单必须落到消息流。上面的逐条 toast 是单槽通道\n // (`main.ts` 的 showToast 每次都覆盖上一条),多个失败项之间会互相顶掉,\n // 面板终态又只列前 3 条——没有这条 command_echo,超出部分将彻底不可见。\n if (fail > 0) {\n const failedLines = result.results\n .filter((r) => !r.success)\n .map((r) => `✗ ${r.name ?? '?'} —— ${r.error ?? i18next.t('cmd.unknownError')}`)\n ctx.tui.messages.pushMessage({\n role: 'command_echo',\n command: '/install',\n status: 'error',\n summary,\n detail: failedLines.join('\\n'),\n })\n }\n\n // 聚合 warnings\n if (result.warnings && result.warnings.length > 0) {\n ctx.tui.messages.pushMessage({\n role: 'command_echo',\n command: '/install',\n status: 'warn',\n summary: `${result.warnings.length} 项警告`,\n detail: result.warnings.map((w) => `⚠ ${w}`).join('\\n'),\n })\n }\n\n // 能力刷新已在终态裁决前统一执行(RFC-286 D4,见上方 `refreshing` 阶段),\n // 失败已降级为 partial 面板终态;此处只补一条 command_echo 供回溯(R5:\n // 关键失败信息不依赖 toast 单槽通道)。\n reportReloadOutcome(ctx, reloadError)\n return\n }\n\n // RFC-080:postinstall 钩子结果反馈\n if (result.postinstall) {\n if (result.postinstall.success) {\n ctx.tui.info.showToast(label, `\"${result.name}\" installed (${scopeLabel}). postinstall: ok`)\n } else {\n const reason = result.postinstall.timeout\n ? 'timed out (30s)'\n : result.postinstall.error ?? `exit=${result.postinstall.exitCode ?? 'N/A'}`\n ctx.tui.info.showToast(label, `\"${result.name}\" installed (${scopeLabel}). postinstall: FAILED (${reason})`)\n\n const lines = [`Plugin \"${result.name}\" postinstall hook failed:`]\n lines.push(` command: ${result.postinstall.command}`)\n if (result.postinstall.exitCode != null) lines.push(` exit code: ${result.postinstall.exitCode}`)\n if (result.postinstall.stderr) lines.push(` stderr: ${result.postinstall.stderr}`)\n if (result.postinstall.truncated) lines.push(' (output truncated)')\n ctx.tui.messages.pushMessage({\n role: 'command_echo',\n command: '/install',\n status: 'error',\n summary: `\"${result.name}\" postinstall hook failed`,\n detail: lines.join('\\n'),\n })\n }\n } else {\n ctx.tui.info.showToast(\n label,\n i18next.t('cmd.installedTo', { name: result.name ?? 'extension', scope: scopeLabel, seg: result.targetPath ? ` (${result.targetPath})` : '' }),\n )\n }\n\n // warnings(RFC-080:信任持久化失败等非致命警告)\n if (result.warnings && result.warnings.length > 0) {\n ctx.tui.messages.pushMessage({\n role: 'command_echo',\n command: '/install',\n status: 'warn',\n summary: `${result.warnings.length} 项警告`,\n detail: result.warnings.map((w) => `⚠ ${w}`).join('\\n'),\n })\n }\n\n // 能力刷新已在终态裁决前统一执行(RFC-286 D4)。\n reportReloadOutcome(ctx, reloadError)\n}\n\n/**\n * RFC-286 D4/R5:能力刷新结果的消息通道反馈。\n *\n * 刷新本身已在终态裁决前执行完毕(面板 `Refreshing…` 阶段对应其真实耗时),失败时面板\n * 已降级为 `partial`。此处只负责消息流留痕:成功走 toast(非关键信息,可被覆盖);\n * 失败走 command_echo(关键信息,不依赖 toast 单槽通道)。\n */\nfunction reportReloadOutcome(ctx: CliContext, reloadError: string | undefined): void {\n if (reloadError) {\n ctx.tui.messages.pushMessage({\n role: 'command_echo',\n command: '/install',\n status: 'error',\n summary: i18next.t('cmd.installedRefreshErr', { err: reloadError }),\n })\n return\n }\n ctx.tui.info.showToast(i18next.t('cmd.titleInstall'), i18next.t('cmd.capRefreshedInstall'))\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"install-hTDjeo61.js","names":[],"sources":["../src/commands/handlers/install.ts"],"sourcesContent":["/**\n * handlers/install.ts — /install 命令处理\n *\n * /install [--global] [--force] [--dry-run] [--as <type>] [--type <src>] <source>\n *\n * 安装 skill/plugin/mcp 包到项目 (.otto/) 或全局 (~/.otto/)。\n * 支持 file/url/git/npm 四种来源,自动检测内容类型。\n */\nimport { i18next } from '@x-otto/tui'\nimport { InstallManager, defaultPostInstallExecutor } from '@x-otto/coding'\nimport { formatBytes } from '@x-otto/shared'\nimport type { InstallOptions, InstallProgress, InstallResult } from '@x-otto/coding'\nimport type { McpServersConfig } from '@x-otto/setting'\nimport type { SelectOption } from '@x-otto/tui'\nimport type { CliContext } from '../../command-dispatcher'\n\nconst VALID_AS = new Set(['plugin', 'skill', 'mcp'])\n\nexport async function handleInstall(\n args: string[],\n ctx: CliContext,\n): Promise<void> {\n const parsed = parseArgs(args)\n if (!parsed.source) {\n ctx.tui.info.showToast(i18next.t('cmd.titleInstall'), i18next.t('cmd.installUsage'))\n return\n }\n\n const isGlobal = parsed.flags.has('--global')\n const isHere = parsed.flags.has('--here')\n\n if (isGlobal && isHere) {\n ctx.tui.info.showToast(i18next.t('cmd.titleInstall'), i18next.t('cmd.installMutuallyExclusive'))\n return\n }\n\n const options: InstallOptions = {\n // --here:锚到当前命令目录,绕过 ctx.app.workspaceDir 的向上工作区根查找。\n workspaceDir: isHere ? process.cwd() : ctx.app.workspaceDir,\n global: isGlobal,\n force: parsed.flags.has('--force'),\n dryRun: parsed.flags.has('--dry-run'),\n as: parsed.as,\n type: parsed.type,\n env: parsed.env,\n subPath: parsed.subPath,\n ref: parsed.ref,\n postInstallExecutor: defaultPostInstallExecutor,\n // RFC-131: 进度回调 — 仅非 dry-run 时注入\n onProgress: !parsed.dryRun ? (p) => {\n const prefix = p.total && p.current ? `[${p.current}/${p.total}] ` : ''\n ctx.tui.info.showToast('Installing', `${prefix}${p.message}`)\n } : undefined,\n }\n\n // 确认优先(P3 安全边界):始终先 dry-run 取 plan 展示——含将运行的 postinstall/setup\n // shell 命令——确认后才真装(项目级插件真装含 auto-trust + 立即跑 postinstall)。\n // --dry-run 仅预览,不提供安装按钮。\n await showConfirmPanel(parsed.source, options, ctx, !parsed.dryRun)\n}\n\n// ── 参数解析 ──\n\ninterface ParsedArgs {\n source: string | undefined\n flags: Set<string>\n as: 'plugin' | 'skill' | 'mcp' | undefined\n type: 'file' | 'url' | 'git' | 'npm' | undefined\n env: Record<string, string> | undefined\n /** RFC-201:monorepo 子目录(--sub-path,registry 条目 subPath 的 CLI 透传面)。 */\n subPath: string | undefined\n /**\n * RFC-221 D2/T221-3:git 来源的 tag/branch 名(`--ref`),透传到\n * `InstallOptions.ref` → `resolveGitSource(gitUrl, ref)`。仅对 git 来源有效,\n * 其余来源类型忽略此参数(不报错,与 subPath 对非 git/归档来源的静默忽略语义一致)。\n * 不支持 commit hash——见 `git-resolver.ts` 头部注释(主流托管方服务端默认拒绝)。\n */\n ref: string | undefined\n dryRun: boolean\n}\n\n/** 导出供单测覆盖 `--ref`/`--sub-path` 等标志解析行为(RFC-221 T221-3),非公开 API。 */\nexport function parseArgs(args: string[]): ParsedArgs {\n const flags = new Set<string>()\n let as: 'plugin' | 'skill' | 'mcp' | undefined\n let type: 'file' | 'url' | 'git' | 'npm' | undefined\n let env: Record<string, string> | undefined\n let subPath: string | undefined\n let ref: string | undefined\n let dryRun = false\n let i = 0\n\n while (i < args.length) {\n const a = args[i]!\n if (a === '--as') {\n i++\n const val = args[i]?.toLowerCase()\n if (VALID_AS.has(val ?? '')) as = val as typeof as\n } else if (a === '--type') {\n i++\n const val = args[i]?.toLowerCase()\n if (val === 'file' || val === 'url' || val === 'git' || val === 'npm') {\n type = val\n }\n } else if (a === '--sub-path') {\n // RFC-201 M0/M1:monorepo 子目录安装(registry 条目 subPath 的 CLI 透传面)\n i++\n subPath = args[i]\n } else if (a === '--ref') {\n // RFC-221 D2/T221-3:git 来源 tag/branch 指定安装\n i++\n ref = args[i]\n } else if (a === '--env') {\n i++\n const kv = args[i]\n if (kv) {\n const eq = kv.indexOf('=')\n if (eq > 0) {\n env = env ?? {}\n env[kv.slice(0, eq)] = kv.slice(eq + 1)\n }\n }\n } else if (a.startsWith('--')) {\n flags.add(a)\n } else {\n // positional (source)\n dryRun = flags.has('--dry-run')\n const rest = args.slice(i).filter((x) => !x.startsWith('--'))\n return { source: rest[0], flags, as, type, env, subPath, ref, dryRun }\n }\n i++\n }\n\n dryRun = flags.has('--dry-run')\n return { source: undefined, flags, as, type, env, subPath, ref, dryRun }\n}\n\n// ── 确认面板 ──\n\n/**\n * 先 dry-run 取安装 plan 并展示确认屏。\n *\n * RFC-226:命令发出到确认框出现之间新增 loading 反馈——dry-run 探测(git clone/npm pack/\n * url 下载 + 内容检测)本身耗时不短,此前全程静默。改造后:① 立即展示 loading 态弹窗\n * (options=[] 空选项态,D2);② dry-run 探测阶段(resolving/detecting)的进度通过局部\n * `onProgress`(不复用 `options.onProgress` toast 版本,D5b/R6)实时刷新 loading 文案;\n * ③ dry-run 完成后原地过渡为确认框或错误态(D3/D5a)。\n *\n * **修复**(dialog 栈堆积):此前每次状态转换(loading→进度更新→确认/错误/预览)都调用\n * `ctx.tui.modal.openModal({ kind: 'select' })`,而同步 select 路径每次 `router.navigate` 都是\n * push 新栈条目,导致对话框层层堆积、用户要按多次 Esc 才能退出。现改用框架层 `async` 字段:\n * loading 态由框架自动展示,dry-run 在 async promise 内完成,resolve 后框架用 `replace: true`\n * 原地过渡为真实内容。最终的确认/错误/预览弹窗由 `onSelect` 回调内驱动(此时 loading 弹窗\n * 已被框架 replace 为最终态,Esc 一次即可退出)。\n *\n * @param proceed true=确认后真装([安装, 取消]);false=纯预览(--dry-run,仅[关闭])。\n */\nasync function showConfirmPanel(\n source: string,\n options: InstallOptions,\n ctx: CliContext,\n proceed: boolean,\n): Promise<void> {\n const manager = new InstallManager()\n const loadingTitle = `${i18next.t('cmd.titleInstall')} — \"${source}\"`\n\n // dry-run 专用局部 onProgress——不复用 options.onProgress(toast 版本,只服务\n // executeInstall 真实安装路径)。探测阶段进度文案由 async promise 内部记录,\n // resolve 时作为 subtitle 回灌(框架 replace 一次性更新)。\n let progressMessage = i18next.t('cmd.installLoadingHint')\n const dryOptions: InstallOptions = {\n ...options,\n dryRun: true,\n onProgress: (p: InstallProgress) => {\n progressMessage = p.message\n },\n }\n\n ctx.tui.modal.openModal({\n kind: 'select',\n title: loadingTitle,\n subtitle: `${i18next.t('cmd.titleDryRun')}...\\n${progressMessage}`,\n layout: 'buttons',\n options: [],\n onCancel: () => {\n ctx.tui.info.showToast(i18next.t('cmd.titleInstall'), i18next.t('cmd.selectCancelled'))\n },\n // async:dry-run 探测在 promise 内完成,resolve 后框架用 replace:true 原地过渡。\n async: (async (): Promise<{ title?: string; subtitle?: string; options: SelectOption[] }> => {\n const result = await manager.install(\n source,\n dryOptions,\n () => ctx.app.settings.get('mcp_servers') as Record<string, unknown> | undefined,\n async (servers) => {\n await ctx.app.settings.persist({ mcp_servers: servers as McpServersConfig })\n },\n )\n\n if (!result.success) {\n return {\n title: loadingTitle,\n subtitle: i18next.t('cmd.dryRunDetectFailed', { err: result.error ?? i18next.t('cmd.unknownError') }),\n options: [{ label: i18next.t('cmd.commonClose'), value: 'close', color: 'gray' }],\n }\n }\n\n if (!result.plan || result.plan.length === 0) {\n return {\n title: loadingTitle,\n subtitle: i18next.t('cmd.dryRunPreviewOk', { name: result.name ?? i18next.t('cmd.unknownLabel') }),\n options: [{ label: i18next.t('cmd.commonClose'), value: 'close', color: 'gray' }],\n }\n }\n\n const plan = result.plan.filter((l) => !l.startsWith(i18next.t('cmd.planSourcePrefix')))\n const name = result.name ?? i18next.t('cmd.unknownLabel')\n const isMultiSkill = result.results !== undefined\n\n // RFC-130: 多 skill 仓库 → 聚合面板\n if (isMultiSkill) {\n const count = result.results!.length\n const title = `${i18next.t('cmd.titleInstall')} — \"${count} skills (${source})\"`\n\n if (!proceed) {\n return {\n title,\n subtitle: plan.join('\\n'),\n options: [{ label: i18next.t('cmd.commonClose'), value: 'close', color: 'gray' }],\n }\n }\n\n return {\n title,\n subtitle: plan.join('\\n'),\n options: [\n { label: i18next.t('cmd.installActionLabel'), value: 'install', color: 'accent' },\n { label: i18next.t('cmd.commonCancel'), value: 'cancel', color: 'gray' },\n ],\n }\n }\n\n // --dry-run:纯预览,不提供安装入口(修正旧语义——旧 dry-run 的\"安装\"按钮会真装)。\n if (!proceed) {\n return {\n title: `${i18next.t('cmd.titleDryRun')} — \"${name}\"`,\n subtitle: plan.join('\\n'),\n options: [{ label: i18next.t('cmd.commonClose'), value: 'close', color: 'gray' }],\n }\n }\n\n return {\n title: `${i18next.t('cmd.titleInstall')} — \"${name}\"`,\n subtitle: plan.join('\\n'),\n options: [\n { label: i18next.t('cmd.installActionLabel'), value: 'install', color: 'accent' },\n { label: i18next.t('cmd.commonCancel'), value: 'cancel', color: 'gray' },\n ],\n }\n })(),\n onSelect: async (v: string) => {\n if (v === 'cancel' || v === 'close') return\n if (v === 'install') {\n await executeInstall(source, { ...options, dryRun: false }, ctx)\n }\n },\n })\n}\n\n// ── 执行安装 ──\n\n/**\n * RFC-224 D2/D3:安装进度 fraction 计算——字节级进度(bytesDownloaded/bytesTotal)\n * 优先于多 skill 计数(current/total),两者互斥语义(见 InstallProgress JSDoc)。\n * 导出供单测覆盖(`install-progress-bytes.test.ts`),不作为公开 API。\n */\nexport function computeProgressFraction(p: {\n bytesDownloaded?: number\n bytesTotal?: number\n current?: number\n total?: number\n}): number {\n if (p.bytesTotal !== undefined && p.bytesDownloaded !== undefined) {\n return p.bytesDownloaded / p.bytesTotal\n }\n if (p.current && p.total) {\n return p.current / p.total\n }\n return 0\n}\n\n/**\n * RFC-224 D2/R4:安装进度 detail 文案格式化——bytesDownloaded 存在时渲染字节数\n * (`formatBytes`),Content-Length 缺失(bytesTotal 为 undefined)时降级为纯字节\n * 计数,不编造总数;否则保持原始 message 不变。导出供单测覆盖。\n */\nexport function formatProgressDetail(p: {\n message: string\n bytesDownloaded?: number\n bytesTotal?: number\n}): string {\n if (p.bytesDownloaded === undefined) return p.message\n if (p.bytesTotal !== undefined) {\n return `${p.message} ${formatBytes(p.bytesDownloaded)} / ${formatBytes(p.bytesTotal)}`\n }\n return `${p.message} ${formatBytes(p.bytesDownloaded)}`\n}\n\n/** `Refreshing…` 在四阶段清单中的下标(RFC-286 D4:该阶段由 CLI 持有)。 */\nconst REFRESHING_PHASE_INDEX = 3\n\n/** partial 面板最多直接列出的失败项条数,超出部分转由 command_echo 承载(RFC-286 D6)。 */\nconst PARTIAL_DETAIL_MAX_ITEMS = 3\n\n/**\n * RFC-286 D6:把失败子项渲染成面板可读的一行摘要。\n *\n * 面板是窄栏终端 UI,超过 `PARTIAL_DETAIL_MAX_ITEMS` 条时只列前几条并指向消息流\n * ——完整清单由 `executeInstall` 的 command_echo 承载(R5:不依赖 toast 单槽通道)。\n */\n/** 拼接多条子错误前去掉句末句号,避免出现「。;」这类双标点(RFC-290 T290-11 收尾)。 */\nfunction stripTrailingPeriod(text: string): string {\n return text.replace(/[。.]\\s*$/, '')\n}\n\nfunction describeFailures(\n subs: Array<{ success: boolean; name?: string; error?: string }> | undefined,\n unknownErrorLabel: string,\n): string {\n const failed = (subs ?? []).filter((r) => !r.success)\n if (failed.length === 0) return unknownErrorLabel\n const shown = failed.slice(0, PARTIAL_DETAIL_MAX_ITEMS)\n const details = shown\n .map((r) => `${r.name ?? '?'} —— ${stripTrailingPeriod(r.error ?? unknownErrorLabel)}`)\n .join(';')\n return failed.length > shown.length ? `${details}${i18next.t('install.failureOverflow')}` : details\n}\n\n/**\n * RFC-290 D2:冲突类失败 → `--force` 建议。\n *\n * **只读结构化的 `failureKind`,绝不匹配 `error` 文案**——错误文案是本地化的人读文本,\n * 子串嗅探(如判断是否含\"已存在\")会在换 locale 或改措辞时静默失效。\n * 单结果读顶层字段,多结果要求**全部**失败项同为 conflict(混合原因不给建议,R4)。\n */\nfunction resolveConflictNextStep(\n result: { failureKind?: 'conflict' },\n subs: Array<{ success: boolean; failureKind?: 'conflict' }> | undefined,\n): { nextStep: { kind: 'force'; label: string } } | undefined {\n const failed = (subs ?? []).filter((r) => !r.success)\n const isConflict = subs\n ? failed.length > 0 && failed.every((r) => r.failureKind === 'conflict')\n : result.failureKind === 'conflict'\n if (!isConflict) return undefined\n return { nextStep: { kind: 'force', label: i18next.t('install.nextStepForce') } }\n}\n\n/**\n * RFC-286 D2/D3/D6:安装终态裁决——把 `InstallResult` + reload 结果映射为进度对话框终态。\n *\n * 映射表(RFC-286 D2 唯一定义处):\n * - 全部成功且 reload 成功 → `success`(2s 自动关闭)\n * - 部分成功(ok>0 && fail>0)→ `partial`(不自动关闭)\n * - 全部失败 → `error`(不自动关闭)\n * - 产物已装、仅 reload 失败 → `partial`(degraded success,提示 /reload)\n *\n * 三种非 success 终态都显式下发 `percent` 与 `phaseStatuses`(R4),失败阶段标 `'error'`\n * ——该取值此前在全仓无任何生产者,`ProgressDialog` 的行内错误渲染是死代码,本函数激活它。\n *\n * 导出供单测覆盖(`install-terminal-state.test.ts`),不作为公开 API。\n */\nexport function computeTerminalProgress(\n result: {\n success: boolean\n error?: string\n failureKind?: 'conflict'\n results?: Array<{ success: boolean; name?: string; error?: string; failureKind?: 'conflict' }>\n },\n reloadError: string | undefined,\n unknownErrorLabel: string,\n): {\n status: 'success' | 'partial' | 'error'\n percent: number\n phaseStatuses: Array<'pending' | 'active' | 'done' | 'error'>\n errorMessage?: string\n nextStep?: { kind: 'force' | 'reload'; label: string }\n} {\n const subs = result.results\n const okCount = subs ? subs.filter((r) => r.success).length : result.success ? 1 : 0\n const failCount = subs ? subs.length - okCount : result.success ? 0 : 1\n\n // 全部失败 → error。失败阶段停在 `installing`(写盘阶段),而非 refreshing——\n // 安装根本没成功,不该显示\"刷新\"阶段被走到过。\n //\n // R3:错误文案按「顶层 error → 子项 error 还原 → 兜底文案」逐级降级。兜底只在\n // 上游连子项原因都没给时才允许出现,绝不用它掩盖上游缺口(本次事故的根因模式)。\n if (okCount === 0) {\n return {\n status: 'error',\n percent: 100,\n phaseStatuses: ['done', 'done', 'error', 'pending'],\n errorMessage: result.error ?? describeFailures(subs, unknownErrorLabel),\n ...(resolveConflictNextStep(result, subs) ?? {}),\n }\n }\n\n // 有成功项但也有失败项 → partial\n if (failCount > 0) {\n return {\n status: 'partial',\n percent: 100,\n phaseStatuses: ['done', 'done', 'error', 'done'],\n errorMessage:\n i18next.t('install.partialCounts', { ok: okCount, fail: failCount }) +\n describeFailures(subs, unknownErrorLabel),\n ...(resolveConflictNextStep(result, subs) ?? {}),\n }\n }\n\n // 全部装好但能力刷新失败 → degraded success(产物已落盘,不能报 error)\n if (reloadError) {\n return {\n status: 'partial',\n percent: 100,\n phaseStatuses: ['done', 'done', 'done', 'error'],\n errorMessage: i18next.t('cmd.installedRefreshErr', { err: reloadError }),\n nextStep: { kind: 'reload', label: i18next.t('install.nextStepReload') },\n }\n }\n\n return { status: 'success', percent: 100, phaseStatuses: ['done', 'done', 'done', 'done'] }\n}\n\nfunction applyTerminalProgress(\n ctx: CliContext,\n result: InstallResult,\n reloadError: string | undefined,\n): void {\n ctx.tui.modal.updateProgress(computeTerminalProgress(result, reloadError, i18next.t('cmd.unknownError')))\n}\n\nasync function executeInstall(\n source: string,\n options: InstallOptions,\n ctx: CliContext,\n): Promise<void> {\n const manager = new InstallManager()\n const label = i18next.t('cmd.titleInstall')\n\n // 打开进度条 dialog(聚焦阻断输入)\n // RFC-290 D5:阶段名与标题走 i18n——此前是硬编码英文字面量,中文环境下面板会突兀\n // 蹦出英文,与其余 20+ 弹窗不一致。\n const phases = [\n i18next.t('install.phaseResolving'),\n i18next.t('install.phaseDetecting'),\n i18next.t('install.phaseInstalling'),\n i18next.t('install.phaseRefreshing'),\n ]\n ctx.tui.modal.openModal({ kind: 'progress', title: i18next.t('install.title', { source }), phases })\n\n // 安装进度 → dialog 更新\n const phaseMap: Record<string, number> = { resolving: 0, detecting: 1, installing: 2, refreshing: 3 }\n const progressOptions: InstallOptions = {\n ...options,\n onProgress: (p) => {\n const idx = phaseMap[p.phase] ?? -1\n if (idx >= 0) {\n const statuses = phases.map((_, i) => {\n if (i < idx) return 'done' as const\n if (i === idx) return 'active' as const\n return 'pending' as const\n })\n const fraction = computeProgressFraction(p)\n const detail = formatProgressDetail(p)\n ctx.tui.modal.updateProgress({\n phaseIndex: idx,\n phaseStatuses: statuses,\n percent: Math.round(((idx + fraction) / phases.length) * 90),\n detail,\n subProgress: p.current !== undefined && p.total !== undefined ? { current: p.current, total: p.total } : undefined,\n })\n }\n },\n }\n\n const result = await manager.install(\n source,\n progressOptions,\n () => ctx.app.settings.get('mcp_servers') as Record<string, unknown> | undefined,\n async (servers) => {\n await ctx.app.settings.persist({ mcp_servers: servers as McpServersConfig })\n },\n )\n\n // ── 安装写盘结束 → 真实能力刷新(RFC-286 D4/D5) ──\n //\n // `refreshing` 阶段由本处 fire——`InstallManager` 不再自行 fire(它没有 App 句柄,\n // 其 refreshing 与真实 reload 无关,属伪进度)。谁执行谁上报:面板上的\n // \"Refreshing…\" 从此对应下方 `reloadCapabilities()` 的真实耗时。\n //\n // 时序变更:终态裁决移到 reload **之后**(此前在 reload 前就置 success 并开始\n // 2s 自动关闭倒计时)。这样 reload 失败才能降级为 partial 呈现给用户。\n const installOk = result.success || (result.results?.some((r) => r.success) ?? false)\n let reloadError: string | undefined\n if (installOk) {\n ctx.tui.modal.updateProgress({\n phaseIndex: REFRESHING_PHASE_INDEX,\n phaseStatuses: ['done', 'done', 'done', 'active'],\n percent: 90,\n detail: i18next.t('install.reloadingDetail'),\n })\n try {\n await ctx.app.reloadCapabilities()\n ctx.tui.plugins.setPluginRegistrations(ctx.app.getPluginRegistrations())\n } catch (err) {\n reloadError = err instanceof Error ? err.message : String(err)\n }\n }\n\n // 安装结束 → dialog 终态(RFC-286 D2/D3/R4:终态必须同时下发 percent 与\n // phaseStatuses,不得让面板继承中途快照——此前只传 status/errorMessage,\n // 导致失败面板冻结在 68% 且阶段行仍是 active,看起来像还在刷新)。\n applyTerminalProgress(ctx, result, reloadError)\n\n if (!result.success) {\n // 多 skill 部分失败时继续(D5:独立安装不回滚)\n // 全部失败时展示第一个错误\n if (!result.results || result.results.every((r) => !r.success)) {\n ctx.tui.info.showToast(label, i18next.t('cmd.installFailed', { err: result.error ?? i18next.t('cmd.unknownError') }))\n if (!result.results) return\n // 多 skill 全部失败但仍然有 results→继续执行 toast 汇总\n }\n }\n\n const scopeLabel = options.global ? i18next.t('cmd.scopeGlobal') : i18next.t('cmd.scopeProject')\n\n // RFC-130: 多 skill → 逐条 toast + 汇总\n if (result.results) {\n let ok = 0\n let fail = 0\n for (const r of result.results) {\n if (r.success) {\n ok++\n ctx.tui.info.showToast(\n label,\n i18next.t('cmd.installedTo', { name: r.name ?? 'extension', scope: scopeLabel, seg: r.targetPath ? ` (${r.targetPath})` : '' }),\n )\n } else {\n fail++\n ctx.tui.info.showToast(label, i18next.t('cmd.installFailed', { err: r.error ?? i18next.t('cmd.unknownError') }))\n }\n }\n const summary = `${ok} installed${fail > 0 ? `, ${fail} failed` : ''}`\n ctx.tui.info.showToast(label, summary)\n\n // RFC-286 D6/R5:失败清单必须落到消息流。上面的逐条 toast 是单槽通道\n // (`main.ts` 的 showToast 每次都覆盖上一条),多个失败项之间会互相顶掉,\n // 面板终态又只列前 3 条——没有这条 command_echo,超出部分将彻底不可见。\n if (fail > 0) {\n const failedLines = result.results\n .filter((r) => !r.success)\n .map((r) => `✗ ${r.name ?? '?'} —— ${r.error ?? i18next.t('cmd.unknownError')}`)\n ctx.tui.messages.pushMessage({\n role: 'command_echo',\n command: '/install',\n status: 'error',\n summary,\n detail: failedLines.join('\\n'),\n })\n }\n\n // 聚合 warnings\n if (result.warnings && result.warnings.length > 0) {\n ctx.tui.messages.pushMessage({\n role: 'command_echo',\n command: '/install',\n status: 'warn',\n summary: `${result.warnings.length} 项警告`,\n detail: result.warnings.map((w) => `⚠ ${w}`).join('\\n'),\n })\n }\n\n // 能力刷新已在终态裁决前统一执行(RFC-286 D4,见上方 `refreshing` 阶段),\n // 失败已降级为 partial 面板终态;此处只补一条 command_echo 供回溯(R5:\n // 关键失败信息不依赖 toast 单槽通道)。\n reportReloadOutcome(ctx, reloadError)\n return\n }\n\n // RFC-080:postinstall 钩子结果反馈\n if (result.postinstall) {\n if (result.postinstall.success) {\n ctx.tui.info.showToast(label, `\"${result.name}\" installed (${scopeLabel}). postinstall: ok`)\n } else {\n const reason = result.postinstall.timeout\n ? 'timed out (30s)'\n : result.postinstall.error ?? `exit=${result.postinstall.exitCode ?? 'N/A'}`\n ctx.tui.info.showToast(label, `\"${result.name}\" installed (${scopeLabel}). postinstall: FAILED (${reason})`)\n\n const lines = [`Plugin \"${result.name}\" postinstall hook failed:`]\n lines.push(` command: ${result.postinstall.command}`)\n if (result.postinstall.exitCode != null) lines.push(` exit code: ${result.postinstall.exitCode}`)\n if (result.postinstall.stderr) lines.push(` stderr: ${result.postinstall.stderr}`)\n if (result.postinstall.truncated) lines.push(' (output truncated)')\n ctx.tui.messages.pushMessage({\n role: 'command_echo',\n command: '/install',\n status: 'error',\n summary: `\"${result.name}\" postinstall hook failed`,\n detail: lines.join('\\n'),\n })\n }\n } else {\n ctx.tui.info.showToast(\n label,\n i18next.t('cmd.installedTo', { name: result.name ?? 'extension', scope: scopeLabel, seg: result.targetPath ? ` (${result.targetPath})` : '' }),\n )\n }\n\n // warnings(RFC-080:信任持久化失败等非致命警告)\n if (result.warnings && result.warnings.length > 0) {\n ctx.tui.messages.pushMessage({\n role: 'command_echo',\n command: '/install',\n status: 'warn',\n summary: `${result.warnings.length} 项警告`,\n detail: result.warnings.map((w) => `⚠ ${w}`).join('\\n'),\n })\n }\n\n // 能力刷新已在终态裁决前统一执行(RFC-286 D4)。\n reportReloadOutcome(ctx, reloadError)\n}\n\n/**\n * RFC-286 D4/R5:能力刷新结果的消息通道反馈。\n *\n * 刷新本身已在终态裁决前执行完毕(面板 `Refreshing…` 阶段对应其真实耗时),失败时面板\n * 已降级为 `partial`。此处只负责消息流留痕:成功走 toast(非关键信息,可被覆盖);\n * 失败走 command_echo(关键信息,不依赖 toast 单槽通道)。\n */\nfunction reportReloadOutcome(ctx: CliContext, reloadError: string | undefined): void {\n if (reloadError) {\n ctx.tui.messages.pushMessage({\n role: 'command_echo',\n command: '/install',\n status: 'error',\n summary: i18next.t('cmd.installedRefreshErr', { err: reloadError }),\n })\n return\n }\n ctx.tui.info.showToast(i18next.t('cmd.titleInstall'), i18next.t('cmd.capRefreshedInstall'))\n}\n"],"mappings":"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"}
@@ -9,4 +9,4 @@ import{OTTO_PROJECT_CONFIG_RELPATH as e}from"@x-otto/env";import{SettingsManager
9
9
  logout <name> Remove stored credentials
10
10
  list List authenticated MCP servers
11
11
  `),1)}export{s as runMcpCommand};
12
- //# sourceMappingURL=mcp-DbTYT7hB.js.map
12
+ //# sourceMappingURL=mcp-BDfBf6gx.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"mcp-DbTYT7hB.js","names":[],"sources":["../src/commands/mcp.ts"],"sourcesContent":["import { OTTO_PROJECT_CONFIG_RELPATH } from '@x-otto/env'\nimport { createFileTokenStore, login } from '@x-otto/mcp'\nimport { SettingsManager } from '@x-otto/setting'\n\nimport type { McpServerConfig } from '@x-otto/mcp'\n\nexport interface McpDeps {\n stderr?: (s: string) => void\n}\n\nexport async function runMcpList(): Promise<void> {\n const store = createFileTokenStore()\n const servers = await store.list()\n\n if (servers.length === 0) {\n process.stdout.write('No authenticated MCP servers.\\n')\n return\n }\n\n process.stdout.write('Authenticated MCP servers:\\n')\n for (const name of servers) {\n const creds = await store.load(name)\n const status = creds?.expiresAt\n ? creds.expiresAt > Date.now()\n ? 'valid'\n : 'expired'\n : 'no expiry'\n process.stdout.write(` ${name} (${status})\\n`)\n }\n}\n\nexport async function runMcpLogout(serverName: string): Promise<void> {\n const store = createFileTokenStore()\n await store.delete(serverName)\n process.stdout.write(`Logged out of '${serverName}'.\\n`)\n}\n\nexport async function runMcpLogin(serverName: string): Promise<void> {\n const settings = new SettingsManager()\n await settings.load()\n const servers = settings.get('mcp_servers') as Record<string, McpServerConfig> | undefined\n\n if (!servers?.[serverName]) {\n process.stderr.write(\n `MCP server '${serverName}' not found in settings. Add it to ${OTTO_PROJECT_CONFIG_RELPATH}:\\n` +\n ` \"mcp_servers\": { \"${serverName}\": { \"type\": \"http\", \"url\": \"https://...\" } }\\n`,\n )\n process.exit(1)\n }\n\n const config = servers[serverName]\n if (config.type === 'stdio' || (!config.url && config.command)) {\n process.stderr.write(\n `MCP server '${serverName}' is a stdio server. OAuth login only works with HTTP-based MCP servers.\\n`,\n )\n process.exit(1)\n }\n if (!config.url) {\n process.stderr.write(`MCP server '${serverName}' has no URL configured.\\n`)\n process.exit(1)\n }\n\n process.stdout.write(`Logging into MCP server '${serverName}' (${config.url})...\\n`)\n process.stdout.write('A browser window will open for authentication.\\n')\n\n try {\n const result = await login({\n serverUrl: config.url,\n serverName,\n })\n\n process.stdout.write(`✓ Login successful.\\n`)\n if (result.registered) {\n process.stdout.write(' (Dynamic Client Registration completed)\\n')\n }\n } catch (err) {\n process.stderr.write(`Login failed: ${(err as Error).message}\\n`)\n process.exit(1)\n }\n}\n\n/**\n * `otto mcp <action> [name]` sub-command dispatcher.\n * Extracted from the inline if-chain in cli.ts.\n */\nexport async function runMcpCommand(args: string[], deps: McpDeps = {}): Promise<number> {\n const stderr = deps.stderr ?? ((s) => process.stderr.write(s))\n const action = args[0]\n const serverName = args[1]\n\n if (action === 'login' && serverName) {\n await runMcpLogin(serverName)\n return 0\n }\n if (action === 'logout' && serverName) {\n await runMcpLogout(serverName)\n return 0\n }\n if (action === 'list') {\n await runMcpList()\n return 0\n }\n\n stderr(\n 'Usage: otto mcp <login|logout|list> [server-name]\\n' +\n ' login <name> Authenticate with an MCP server (OAuth 2.1)\\n' +\n ' logout <name> Remove stored credentials\\n' +\n ' list List authenticated MCP servers\\n',\n )\n return 1\n}\n"],"mappings":";0KAUA,eAAsB,GAA4B,CAChD,IAAM,EAAQ,GAAsB,CAC9B,EAAU,MAAM,EAAM,MAAM,CAElC,GAAI,EAAQ,SAAW,EAAG,CACxB,QAAQ,OAAO,MAAM;EAAkC,CACvD,OAGF,QAAQ,OAAO,MAAM;EAA+B,CACpD,IAAK,IAAM,KAAQ,EAAS,CAC1B,IAAM,EAAQ,MAAM,EAAM,KAAK,EAAK,CAC9B,EAAS,GAAO,UAClB,EAAM,UAAY,KAAK,KAAK,CAC1B,QACA,UACF,YACJ,QAAQ,OAAO,MAAM,KAAK,EAAK,IAAI,EAAO,KAAK,EAInD,eAAsB,EAAa,EAAmC,CAEpE,MADc,GAAsB,CACxB,OAAO,EAAW,CAC9B,QAAQ,OAAO,MAAM,kBAAkB,EAAW,MAAM,CAG1D,eAAsB,EAAY,EAAmC,CACnE,IAAM,EAAW,IAAI,EACrB,MAAM,EAAS,MAAM,CACrB,IAAM,EAAU,EAAS,IAAI,cAAc,CAEtC,IAAU,KACb,QAAQ,OAAO,MACb,eAAe,EAAW,qCAAqC,EAA4B,yBAClE,EAAW,iDACrC,CACD,QAAQ,KAAK,EAAE,EAGjB,IAAM,EAAS,EAAQ,IACnB,EAAO,OAAS,SAAY,CAAC,EAAO,KAAO,EAAO,WACpD,QAAQ,OAAO,MACb,eAAe,EAAW,4EAC3B,CACD,QAAQ,KAAK,EAAE,EAEZ,EAAO,MACV,QAAQ,OAAO,MAAM,eAAe,EAAW,4BAA4B,CAC3E,QAAQ,KAAK,EAAE,EAGjB,QAAQ,OAAO,MAAM,4BAA4B,EAAW,KAAK,EAAO,IAAI,QAAQ,CACpF,QAAQ,OAAO,MAAM;EAAmD,CAExE,GAAI,CACF,IAAM,EAAS,MAAM,EAAM,CACzB,UAAW,EAAO,IAClB,aACD,CAAC,CAEF,QAAQ,OAAO,MAAM;EAAwB,CACzC,EAAO,YACT,QAAQ,OAAO,MAAM;EAA8C,OAE9D,EAAK,CACZ,QAAQ,OAAO,MAAM,iBAAkB,EAAc,QAAQ,IAAI,CACjE,QAAQ,KAAK,EAAE,EAQnB,eAAsB,EAAc,EAAgB,EAAgB,EAAE,CAAmB,CACvF,IAAM,EAAS,EAAK,SAAY,GAAM,QAAQ,OAAO,MAAM,EAAE,EACvD,EAAS,EAAK,GACd,EAAa,EAAK,GAqBxB,OAnBI,IAAW,SAAW,GACxB,MAAM,EAAY,EAAW,CACtB,GAEL,IAAW,UAAY,GACzB,MAAM,EAAa,EAAW,CACvB,GAEL,IAAW,QACb,MAAM,GAAY,CACX,IAGT,EACE;;;;EAID,CACM"}
1
+ {"version":3,"file":"mcp-BDfBf6gx.js","names":[],"sources":["../src/commands/mcp.ts"],"sourcesContent":["import { OTTO_PROJECT_CONFIG_RELPATH } from '@x-otto/env'\nimport { createFileTokenStore, login } from '@x-otto/mcp'\nimport { SettingsManager } from '@x-otto/setting'\n\nimport type { McpServerConfig } from '@x-otto/mcp'\n\nexport interface McpDeps {\n stderr?: (s: string) => void\n}\n\nexport async function runMcpList(): Promise<void> {\n const store = createFileTokenStore()\n const servers = await store.list()\n\n if (servers.length === 0) {\n process.stdout.write('No authenticated MCP servers.\\n')\n return\n }\n\n process.stdout.write('Authenticated MCP servers:\\n')\n for (const name of servers) {\n const creds = await store.load(name)\n const status = creds?.expiresAt\n ? creds.expiresAt > Date.now()\n ? 'valid'\n : 'expired'\n : 'no expiry'\n process.stdout.write(` ${name} (${status})\\n`)\n }\n}\n\nexport async function runMcpLogout(serverName: string): Promise<void> {\n const store = createFileTokenStore()\n await store.delete(serverName)\n process.stdout.write(`Logged out of '${serverName}'.\\n`)\n}\n\nexport async function runMcpLogin(serverName: string): Promise<void> {\n const settings = new SettingsManager()\n await settings.load()\n const servers = settings.get('mcp_servers') as Record<string, McpServerConfig> | undefined\n\n if (!servers?.[serverName]) {\n process.stderr.write(\n `MCP server '${serverName}' not found in settings. Add it to ${OTTO_PROJECT_CONFIG_RELPATH}:\\n` +\n ` \"mcp_servers\": { \"${serverName}\": { \"type\": \"http\", \"url\": \"https://...\" } }\\n`,\n )\n process.exit(1)\n }\n\n const config = servers[serverName]\n if (config.type === 'stdio' || (!config.url && config.command)) {\n process.stderr.write(\n `MCP server '${serverName}' is a stdio server. OAuth login only works with HTTP-based MCP servers.\\n`,\n )\n process.exit(1)\n }\n if (!config.url) {\n process.stderr.write(`MCP server '${serverName}' has no URL configured.\\n`)\n process.exit(1)\n }\n\n process.stdout.write(`Logging into MCP server '${serverName}' (${config.url})...\\n`)\n process.stdout.write('A browser window will open for authentication.\\n')\n\n try {\n const result = await login({\n serverUrl: config.url,\n serverName,\n })\n\n process.stdout.write(`✓ Login successful.\\n`)\n if (result.registered) {\n process.stdout.write(' (Dynamic Client Registration completed)\\n')\n }\n } catch (err) {\n process.stderr.write(`Login failed: ${(err as Error).message}\\n`)\n process.exit(1)\n }\n}\n\n/**\n * `otto mcp <action> [name]` sub-command dispatcher.\n * Extracted from the inline if-chain in cli.ts.\n */\nexport async function runMcpCommand(args: string[], deps: McpDeps = {}): Promise<number> {\n const stderr = deps.stderr ?? ((s) => process.stderr.write(s))\n const action = args[0]\n const serverName = args[1]\n\n if (action === 'login' && serverName) {\n await runMcpLogin(serverName)\n return 0\n }\n if (action === 'logout' && serverName) {\n await runMcpLogout(serverName)\n return 0\n }\n if (action === 'list') {\n await runMcpList()\n return 0\n }\n\n stderr(\n 'Usage: otto mcp <login|logout|list> [server-name]\\n' +\n ' login <name> Authenticate with an MCP server (OAuth 2.1)\\n' +\n ' logout <name> Remove stored credentials\\n' +\n ' list List authenticated MCP servers\\n',\n )\n return 1\n}\n"],"mappings":";0KAUA,eAAsB,GAA4B,CAChD,IAAM,EAAQ,GAAsB,CAC9B,EAAU,MAAM,EAAM,MAAM,CAElC,GAAI,EAAQ,SAAW,EAAG,CACxB,QAAQ,OAAO,MAAM;EAAkC,CACvD,OAGF,QAAQ,OAAO,MAAM;EAA+B,CACpD,IAAK,IAAM,KAAQ,EAAS,CAC1B,IAAM,EAAQ,MAAM,EAAM,KAAK,EAAK,CAC9B,EAAS,GAAO,UAClB,EAAM,UAAY,KAAK,KAAK,CAC1B,QACA,UACF,YACJ,QAAQ,OAAO,MAAM,KAAK,EAAK,IAAI,EAAO,KAAK,EAInD,eAAsB,EAAa,EAAmC,CAEpE,MADc,GAAsB,CACxB,OAAO,EAAW,CAC9B,QAAQ,OAAO,MAAM,kBAAkB,EAAW,MAAM,CAG1D,eAAsB,EAAY,EAAmC,CACnE,IAAM,EAAW,IAAI,EACrB,MAAM,EAAS,MAAM,CACrB,IAAM,EAAU,EAAS,IAAI,cAAc,CAEtC,IAAU,KACb,QAAQ,OAAO,MACb,eAAe,EAAW,qCAAqC,EAA4B,yBAClE,EAAW,iDACrC,CACD,QAAQ,KAAK,EAAE,EAGjB,IAAM,EAAS,EAAQ,IACnB,EAAO,OAAS,SAAY,CAAC,EAAO,KAAO,EAAO,WACpD,QAAQ,OAAO,MACb,eAAe,EAAW,4EAC3B,CACD,QAAQ,KAAK,EAAE,EAEZ,EAAO,MACV,QAAQ,OAAO,MAAM,eAAe,EAAW,4BAA4B,CAC3E,QAAQ,KAAK,EAAE,EAGjB,QAAQ,OAAO,MAAM,4BAA4B,EAAW,KAAK,EAAO,IAAI,QAAQ,CACpF,QAAQ,OAAO,MAAM;EAAmD,CAExE,GAAI,CACF,IAAM,EAAS,MAAM,EAAM,CACzB,UAAW,EAAO,IAClB,aACD,CAAC,CAEF,QAAQ,OAAO,MAAM;EAAwB,CACzC,EAAO,YACT,QAAQ,OAAO,MAAM;EAA8C,OAE9D,EAAK,CACZ,QAAQ,OAAO,MAAM,iBAAkB,EAAc,QAAQ,IAAI,CACjE,QAAQ,KAAK,EAAE,EAQnB,eAAsB,EAAc,EAAgB,EAAgB,EAAE,CAAmB,CACvF,IAAM,EAAS,EAAK,SAAY,GAAM,QAAQ,OAAO,MAAM,EAAE,EACvD,EAAS,EAAK,GACd,EAAa,EAAK,GAqBxB,OAnBI,IAAW,SAAW,GACxB,MAAM,EAAY,EAAW,CACtB,GAEL,IAAW,UAAY,GACzB,MAAM,EAAa,EAAW,CACvB,GAEL,IAAW,QACb,MAAM,GAAY,CACX,IAGT,EACE;;;;EAID,CACM"}
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import{runConfigMigrationCommand as e}from"./config-migration-DatflIxs.js";import{hostname as t}from"node:os";import{checkArchiveBomb as n,cleanTempDir as r,createArchive as i,createTempDir as a,extractArchive as o,validateExtractedPaths as s}from"@x-otto/coding";import{OTTO_HOME as c,OTTO_SESSIONS_DIR as l}from"@x-otto/env";import{join as u,resolve as d}from"node:path";import{cpSync as f,existsSync as p,mkdirSync as m,readdirSync as h,statSync as g,writeFileSync as _}from"node:fs";import{SettingsManager as v,buildExportPayload as y}from"@x-otto/setting";function b(e){let[t,...n]=e,r=[],i=new Set,a=new Map;for(let e=0;e<n.length;e++){let t=n[e];if(t===`-o`||t===`--output`){let t=n[++e];t&&a.set(`output`,t);continue}if(t.startsWith(`--`)){i.add(t);continue}r.push(t)}return{sub:t,positional:r,flags:i,values:a}}function x(){return new Date().toISOString().replace(/[:.]/g,`-`)}const S=new Set([`locks`,`trace`]);function C(){let e=u(l,`sessions.db`);if(!p(e))return!1;try{return g(e).size>0}catch{return!1}}async function w(e,n){let o=e.flags.has(`--no-sessions`),s=a();try{let r=new v({projectConfigPath:u(c,`config.json`)});await r.load();let a=y(r.config,`global`,t());if(m(s,{recursive:!0}),_(u(s,`config.json`),JSON.stringify(a,null,2),`utf-8`),!o&&p(l)){let e=u(s,`sessions`);m(e,{recursive:!0});let t=h(l);for(let n of t)S.has(n)||f(u(l,n),u(e,n),{recursive:!0})}let g=e.values.get(`output`)?d(e.values.get(`output`)):u(process.cwd(),`otto-migrate-${x()}.tar.gz`);return m(u(g,`..`),{recursive:!0}),await i(s,g),n.stdout(`迁移包已导出: ${g}\n`),o?n.stdout(`(--no-sessions:未包含会话数据)
2
+ import{runConfigMigrationCommand as e}from"./config-migration-0zdXsEDB.js";import{hostname as t}from"node:os";import{checkArchiveBomb as n,cleanTempDir as r,createArchive as i,createTempDir as a,extractArchive as o,validateExtractedPaths as s}from"@x-otto/coding";import{OTTO_HOME as c,OTTO_SESSIONS_DIR as l}from"@x-otto/env";import{join as u,resolve as d}from"node:path";import{cpSync as f,existsSync as p,mkdirSync as m,readdirSync as h,statSync as g,writeFileSync as _}from"node:fs";import{SettingsManager as v,buildExportPayload as y}from"@x-otto/setting";function b(e){let[t,...n]=e,r=[],i=new Set,a=new Map;for(let e=0;e<n.length;e++){let t=n[e];if(t===`-o`||t===`--output`){let t=n[++e];t&&a.set(`output`,t);continue}if(t.startsWith(`--`)){i.add(t);continue}r.push(t)}return{sub:t,positional:r,flags:i,values:a}}function x(){return new Date().toISOString().replace(/[:.]/g,`-`)}const S=new Set([`locks`,`trace`]);function C(){let e=u(l,`sessions.db`);if(!p(e))return!1;try{return g(e).size>0}catch{return!1}}async function w(e,n){let o=e.flags.has(`--no-sessions`),s=a();try{let r=new v({projectConfigPath:u(c,`config.json`)});await r.load();let a=y(r.config,`global`,t());if(m(s,{recursive:!0}),_(u(s,`config.json`),JSON.stringify(a,null,2),`utf-8`),!o&&p(l)){let e=u(s,`sessions`);m(e,{recursive:!0});let t=h(l);for(let n of t)S.has(n)||f(u(l,n),u(e,n),{recursive:!0})}let g=e.values.get(`output`)?d(e.values.get(`output`)):u(process.cwd(),`otto-migrate-${x()}.tar.gz`);return m(u(g,`..`),{recursive:!0}),await i(s,g),n.stdout(`迁移包已导出: ${g}\n`),o?n.stdout(`(--no-sessions:未包含会话数据)
3
3
  `):n.stdout(`(包含会话数据:对话历史、草稿与粘贴内容原文——分享前请确认其中无敏感信息)
4
4
  `),0}finally{r(s)}}async function T(t,i){let c=t.positional[0];if(!c)return i.stderr(`otto migrate import <archive> [--no-sessions]
5
5
  `),1;let h=d(c);if(!p(h))return i.stderr(`迁移包不存在:${h}\n`),1;let g=a();try{await o(h,g);let{totalBytes:r}=s(g);n(h,r);let a=u(g,`config.json`);if(!p(a))return i.stderr(`迁移包缺少 config.json(非合法 otto migrate 产物)
@@ -11,4 +11,4 @@ import{runConfigMigrationCommand as e}from"./config-migration-DatflIxs.js";impor
11
11
  export [--no-sessions] [-o <path>] 导出配置+插件清单+会话数据为迁移包
12
12
  import <archive> [--no-sessions] 导入迁移包(目标已有会话数据时跳过会话部分)
13
13
  `),n.sub?1:0}}export{E as runMigrateCommand};
14
- //# sourceMappingURL=migrate-boDXhEyV.js.map
14
+ //# sourceMappingURL=migrate-4QDwYLf6.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"migrate-boDXhEyV.js","names":[],"sources":["../src/commands/migrate.ts"],"sourcesContent":["/**\n * /migrate 命令:旧版配置文件 → 新版格式在线迁移向导。\n */\nimport { existsSync, mkdirSync, writeFileSync, readdirSync, statSync, cpSync } from 'node:fs'\nimport { hostname } from 'node:os'\nimport { join, resolve } from 'node:path'\nimport { OTTO_HOME, OTTO_SESSIONS_DIR } from '@x-otto/env'\nimport {\n createArchive,\n extractArchive,\n validateExtractedPaths,\n checkArchiveBomb,\n createTempDir,\n cleanTempDir,\n} from '@x-otto/coding'\nimport { buildExportPayload, SettingsManager } from '@x-otto/setting'\nimport { runConfigMigrationCommand } from './config-migration'\n\nexport interface MigrateIO {\n stdout: (s: string) => void\n stderr: (s: string) => void\n}\n\ninterface ParsedArgs {\n sub: string | undefined\n positional: string[]\n flags: Set<string>\n values: Map<string, string>\n}\n\nfunction parseArgs(args: string[]): ParsedArgs {\n const [sub, ...rest] = args\n const positional: string[] = []\n const flags = new Set<string>()\n const values = new Map<string, string>()\n for (let i = 0; i < rest.length; i++) {\n const a = rest[i]!\n if (a === '-o' || a === '--output') {\n const next = rest[++i]\n if (next) values.set('output', next)\n continue\n }\n if (a.startsWith('--')) {\n flags.add(a)\n continue\n }\n positional.push(a)\n }\n return { sub, positional, flags, values }\n}\n\nfunction timestampNow(): string {\n return new Date().toISOString().replace(/[:.]/g, '-')\n}\n\n/**\n * `OTTO_SESSIONS_DIR` 下机器绑定/纯诊断子目录,`migrate export` 打包时排除\n * (对抗审核 M4 完成审核发现的 RFC 漂移修复:初版实现只排除 `locks/`,用\"排除法\"\n * 会把新增子目录默认打包进迁移包,是脆弱的开放式假设)。\n *\n * - `locks`:写租约信息,随进程/机器绑定,新机器上无意义(`OTTO_SESSION_LOCKS_DIR`)。\n * - `trace`:append-log 诊断轨迹,非会话内容本身,实测可堆积至 25GB+(见\n * `packages/coding/src/app/trace-orphan-sweeper.ts` 头注释记录的真实生产数据),\n * 打包会让迁移包体积失控且无迁移价值(新机器上这些轨迹对应的是旧机器的执行历史)。\n *\n * `panel-state/` **不**在排除清单——它是真实用户数据(面板草稿/todoList 等持久化\n * 状态,见 `packages/runtime/src/create-agent-runtime.ts` `DiskPanelStatePersistence`),\n * 随会话搬迁是用户期望的行为,全量打包/仅打包\"sessions.db + 固定白名单\"两种方案都\n * 会静默丢失该数据(前者不排除、后者是未预料到 panel-state 存在的过度收紧)。\n * `sessions.db-wal`/`sessions.db-shm`(SQLite WAL 模式伴生文件)也需随 `sessions.db`\n * 一起打包——它们不是独立目录而是文件,`readdirSync` 顶层遍历天然包含,无需特殊处理。\n */\nconst MACHINE_BOUND_SESSION_SUBDIRS = new Set(['locks', 'trace'])\n\n/** 目标 sessions.db 是否已存在且非空(RFC-219 重要事项规则 7 判定依据)。 */\nfunction targetSessionsExist(): boolean {\n const dbPath = join(OTTO_SESSIONS_DIR, 'sessions.db')\n if (!existsSync(dbPath)) return false\n try {\n return statSync(dbPath).size > 0\n } catch {\n return false\n }\n}\n\n// ── otto migrate export ──\n\nasync function runMigrateExport(parsed: ParsedArgs, io: MigrateIO): Promise<number> {\n const noSessions = parsed.flags.has('--no-sessions')\n const stagingDir = createTempDir()\n\n try {\n const settings = new SettingsManager({ projectConfigPath: join(OTTO_HOME, 'config.json') })\n await settings.load()\n const payload = buildExportPayload(settings.config, 'global', hostname())\n mkdirSync(stagingDir, { recursive: true })\n writeFileSync(join(stagingDir, 'config.json'), JSON.stringify(payload, null, 2), 'utf-8')\n\n if (!noSessions && existsSync(OTTO_SESSIONS_DIR)) {\n const sessionsStaging = join(stagingDir, 'sessions')\n mkdirSync(sessionsStaging, { recursive: true })\n const entries = readdirSync(OTTO_SESSIONS_DIR)\n for (const entry of entries) {\n // 对抗审核 M4 完成审核修复(RFC 漂移):排除机器绑定/纯诊断子目录,非仅 locks。\n if (MACHINE_BOUND_SESSION_SUBDIRS.has(entry)) continue\n cpSync(join(OTTO_SESSIONS_DIR, entry), join(sessionsStaging, entry), { recursive: true })\n }\n }\n\n const outputPath = parsed.values.get('output')\n ? resolve(parsed.values.get('output')!)\n : join(process.cwd(), `otto-migrate-${timestampNow()}.tar.gz`)\n mkdirSync(join(outputPath, '..'), { recursive: true })\n\n await createArchive(stagingDir, outputPath)\n\n io.stdout(`迁移包已导出: ${outputPath}\\n`)\n if (noSessions) {\n io.stdout('(--no-sessions:未包含会话数据)\\n')\n } else {\n // RFC-338 Q2(用户定稿方案 A):粘贴原文与草稿随包属既定行为,但**必须显式声明**——\n // 这些内容可能含密钥/日志/隐私代码,用户在分享迁移包前有权知情。\n // 该提示有测试守卫(migrate.test.ts),防止后续改动静默移除。\n io.stdout('(包含会话数据:对话历史、草稿与粘贴内容原文——分享前请确认其中无敏感信息)\\n')\n }\n return 0\n } finally {\n cleanTempDir(stagingDir)\n }\n}\n\n// ── otto migrate import ──\n\nasync function runMigrateImport(parsed: ParsedArgs, io: MigrateIO): Promise<number> {\n const archivePath = parsed.positional[0]\n if (!archivePath) {\n io.stderr('otto migrate import <archive> [--no-sessions]\\n')\n return 1\n }\n const resolvedArchive = resolve(archivePath)\n if (!existsSync(resolvedArchive)) {\n io.stderr(`迁移包不存在:${resolvedArchive}\\n`)\n return 1\n }\n\n const extractDir = createTempDir()\n try {\n await extractArchive(resolvedArchive, extractDir)\n const { totalBytes } = validateExtractedPaths(extractDir)\n checkArchiveBomb(resolvedArchive, totalBytes)\n\n const configPath = join(extractDir, 'config.json')\n if (!existsSync(configPath)) {\n io.stderr('迁移包缺少 config.json(非合法 otto migrate 产物)\\n')\n return 1\n }\n\n // 复用 M3 config import 完整逻辑(差异计算+合并+安全门+备份+插件安装),\n // 不重新实现——RFC-219 M3→M4 接线设计,见本文件头注释。\n const configExitCode = await runConfigMigrationCommand(\n ['import', configPath, '--yes'],\n { stdout: io.stdout, stderr: io.stderr },\n )\n if (configExitCode !== 0) {\n return configExitCode\n }\n\n // 会话导入 v1 严格边界(重要事项规则 7):目标已有非空 sessions.db → 跳过,不合并。\n const noSessions = parsed.flags.has('--no-sessions')\n const sessionsStaging = join(extractDir, 'sessions')\n if (noSessions) {\n io.stdout('(--no-sessions:跳过会话导入)\\n')\n } else if (!existsSync(sessionsStaging)) {\n io.stdout('迁移包不含会话数据,跳过\\n')\n } else if (targetSessionsExist()) {\n io.stdout('目标已有会话数据,跳过会话导入(不合并/不覆盖,避免数据冲突)\\n')\n } else {\n mkdirSync(OTTO_SESSIONS_DIR, { recursive: true })\n cpSync(sessionsStaging, OTTO_SESSIONS_DIR, { recursive: true })\n io.stdout('✓ 会话数据已导入\\n')\n }\n\n return 0\n } catch (err) {\n io.stderr(`迁移导入失败:${err instanceof Error ? err.message : String(err)}\\n`)\n return 1\n } finally {\n cleanTempDir(extractDir)\n }\n}\n\n// ── 入口 ──\n\nexport async function runMigrateCommand(args: string[], io: MigrateIO): Promise<number> {\n const parsed = parseArgs(args)\n switch (parsed.sub) {\n case 'export':\n return runMigrateExport(parsed, io)\n case 'import':\n return runMigrateImport(parsed, io)\n default:\n io.stdout(\n 'otto migrate <命令>\\n' +\n ' export [--no-sessions] [-o <path>] 导出配置+插件清单+会话数据为迁移包\\n' +\n ' import <archive> [--no-sessions] 导入迁移包(目标已有会话数据时跳过会话部分)\\n',\n )\n return parsed.sub ? 1 : 0\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"migrate-4QDwYLf6.js","names":[],"sources":["../src/commands/migrate.ts"],"sourcesContent":["/**\n * /migrate 命令:旧版配置文件 → 新版格式在线迁移向导。\n */\nimport { existsSync, mkdirSync, writeFileSync, readdirSync, statSync, cpSync } from 'node:fs'\nimport { hostname } from 'node:os'\nimport { join, resolve } from 'node:path'\nimport { OTTO_HOME, OTTO_SESSIONS_DIR } from '@x-otto/env'\nimport {\n createArchive,\n extractArchive,\n validateExtractedPaths,\n checkArchiveBomb,\n createTempDir,\n cleanTempDir,\n} from '@x-otto/coding'\nimport { buildExportPayload, SettingsManager } from '@x-otto/setting'\nimport { runConfigMigrationCommand } from './config-migration'\n\nexport interface MigrateIO {\n stdout: (s: string) => void\n stderr: (s: string) => void\n}\n\ninterface ParsedArgs {\n sub: string | undefined\n positional: string[]\n flags: Set<string>\n values: Map<string, string>\n}\n\nfunction parseArgs(args: string[]): ParsedArgs {\n const [sub, ...rest] = args\n const positional: string[] = []\n const flags = new Set<string>()\n const values = new Map<string, string>()\n for (let i = 0; i < rest.length; i++) {\n const a = rest[i]!\n if (a === '-o' || a === '--output') {\n const next = rest[++i]\n if (next) values.set('output', next)\n continue\n }\n if (a.startsWith('--')) {\n flags.add(a)\n continue\n }\n positional.push(a)\n }\n return { sub, positional, flags, values }\n}\n\nfunction timestampNow(): string {\n return new Date().toISOString().replace(/[:.]/g, '-')\n}\n\n/**\n * `OTTO_SESSIONS_DIR` 下机器绑定/纯诊断子目录,`migrate export` 打包时排除\n * (对抗审核 M4 完成审核发现的 RFC 漂移修复:初版实现只排除 `locks/`,用\"排除法\"\n * 会把新增子目录默认打包进迁移包,是脆弱的开放式假设)。\n *\n * - `locks`:写租约信息,随进程/机器绑定,新机器上无意义(`OTTO_SESSION_LOCKS_DIR`)。\n * - `trace`:append-log 诊断轨迹,非会话内容本身,实测可堆积至 25GB+(见\n * `packages/coding/src/app/trace-orphan-sweeper.ts` 头注释记录的真实生产数据),\n * 打包会让迁移包体积失控且无迁移价值(新机器上这些轨迹对应的是旧机器的执行历史)。\n *\n * `panel-state/` **不**在排除清单——它是真实用户数据(面板草稿/todoList 等持久化\n * 状态,见 `packages/runtime/src/create-agent-runtime.ts` `DiskPanelStatePersistence`),\n * 随会话搬迁是用户期望的行为,全量打包/仅打包\"sessions.db + 固定白名单\"两种方案都\n * 会静默丢失该数据(前者不排除、后者是未预料到 panel-state 存在的过度收紧)。\n * `sessions.db-wal`/`sessions.db-shm`(SQLite WAL 模式伴生文件)也需随 `sessions.db`\n * 一起打包——它们不是独立目录而是文件,`readdirSync` 顶层遍历天然包含,无需特殊处理。\n */\nconst MACHINE_BOUND_SESSION_SUBDIRS = new Set(['locks', 'trace'])\n\n/** 目标 sessions.db 是否已存在且非空(RFC-219 重要事项规则 7 判定依据)。 */\nfunction targetSessionsExist(): boolean {\n const dbPath = join(OTTO_SESSIONS_DIR, 'sessions.db')\n if (!existsSync(dbPath)) return false\n try {\n return statSync(dbPath).size > 0\n } catch {\n return false\n }\n}\n\n// ── otto migrate export ──\n\nasync function runMigrateExport(parsed: ParsedArgs, io: MigrateIO): Promise<number> {\n const noSessions = parsed.flags.has('--no-sessions')\n const stagingDir = createTempDir()\n\n try {\n const settings = new SettingsManager({ projectConfigPath: join(OTTO_HOME, 'config.json') })\n await settings.load()\n const payload = buildExportPayload(settings.config, 'global', hostname())\n mkdirSync(stagingDir, { recursive: true })\n writeFileSync(join(stagingDir, 'config.json'), JSON.stringify(payload, null, 2), 'utf-8')\n\n if (!noSessions && existsSync(OTTO_SESSIONS_DIR)) {\n const sessionsStaging = join(stagingDir, 'sessions')\n mkdirSync(sessionsStaging, { recursive: true })\n const entries = readdirSync(OTTO_SESSIONS_DIR)\n for (const entry of entries) {\n // 对抗审核 M4 完成审核修复(RFC 漂移):排除机器绑定/纯诊断子目录,非仅 locks。\n if (MACHINE_BOUND_SESSION_SUBDIRS.has(entry)) continue\n cpSync(join(OTTO_SESSIONS_DIR, entry), join(sessionsStaging, entry), { recursive: true })\n }\n }\n\n const outputPath = parsed.values.get('output')\n ? resolve(parsed.values.get('output')!)\n : join(process.cwd(), `otto-migrate-${timestampNow()}.tar.gz`)\n mkdirSync(join(outputPath, '..'), { recursive: true })\n\n await createArchive(stagingDir, outputPath)\n\n io.stdout(`迁移包已导出: ${outputPath}\\n`)\n if (noSessions) {\n io.stdout('(--no-sessions:未包含会话数据)\\n')\n } else {\n // RFC-338 Q2(用户定稿方案 A):粘贴原文与草稿随包属既定行为,但**必须显式声明**——\n // 这些内容可能含密钥/日志/隐私代码,用户在分享迁移包前有权知情。\n // 该提示有测试守卫(migrate.test.ts),防止后续改动静默移除。\n io.stdout('(包含会话数据:对话历史、草稿与粘贴内容原文——分享前请确认其中无敏感信息)\\n')\n }\n return 0\n } finally {\n cleanTempDir(stagingDir)\n }\n}\n\n// ── otto migrate import ──\n\nasync function runMigrateImport(parsed: ParsedArgs, io: MigrateIO): Promise<number> {\n const archivePath = parsed.positional[0]\n if (!archivePath) {\n io.stderr('otto migrate import <archive> [--no-sessions]\\n')\n return 1\n }\n const resolvedArchive = resolve(archivePath)\n if (!existsSync(resolvedArchive)) {\n io.stderr(`迁移包不存在:${resolvedArchive}\\n`)\n return 1\n }\n\n const extractDir = createTempDir()\n try {\n await extractArchive(resolvedArchive, extractDir)\n const { totalBytes } = validateExtractedPaths(extractDir)\n checkArchiveBomb(resolvedArchive, totalBytes)\n\n const configPath = join(extractDir, 'config.json')\n if (!existsSync(configPath)) {\n io.stderr('迁移包缺少 config.json(非合法 otto migrate 产物)\\n')\n return 1\n }\n\n // 复用 M3 config import 完整逻辑(差异计算+合并+安全门+备份+插件安装),\n // 不重新实现——RFC-219 M3→M4 接线设计,见本文件头注释。\n const configExitCode = await runConfigMigrationCommand(\n ['import', configPath, '--yes'],\n { stdout: io.stdout, stderr: io.stderr },\n )\n if (configExitCode !== 0) {\n return configExitCode\n }\n\n // 会话导入 v1 严格边界(重要事项规则 7):目标已有非空 sessions.db → 跳过,不合并。\n const noSessions = parsed.flags.has('--no-sessions')\n const sessionsStaging = join(extractDir, 'sessions')\n if (noSessions) {\n io.stdout('(--no-sessions:跳过会话导入)\\n')\n } else if (!existsSync(sessionsStaging)) {\n io.stdout('迁移包不含会话数据,跳过\\n')\n } else if (targetSessionsExist()) {\n io.stdout('目标已有会话数据,跳过会话导入(不合并/不覆盖,避免数据冲突)\\n')\n } else {\n mkdirSync(OTTO_SESSIONS_DIR, { recursive: true })\n cpSync(sessionsStaging, OTTO_SESSIONS_DIR, { recursive: true })\n io.stdout('✓ 会话数据已导入\\n')\n }\n\n return 0\n } catch (err) {\n io.stderr(`迁移导入失败:${err instanceof Error ? err.message : String(err)}\\n`)\n return 1\n } finally {\n cleanTempDir(extractDir)\n }\n}\n\n// ── 入口 ──\n\nexport async function runMigrateCommand(args: string[], io: MigrateIO): Promise<number> {\n const parsed = parseArgs(args)\n switch (parsed.sub) {\n case 'export':\n return runMigrateExport(parsed, io)\n case 'import':\n return runMigrateImport(parsed, io)\n default:\n io.stdout(\n 'otto migrate <命令>\\n' +\n ' export [--no-sessions] [-o <path>] 导出配置+插件清单+会话数据为迁移包\\n' +\n ' import <archive> [--no-sessions] 导入迁移包(目标已有会话数据时跳过会话部分)\\n',\n )\n return parsed.sub ? 1 : 0\n }\n}\n"],"mappings":"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"}
@@ -1,3 +1,3 @@
1
1
  #!/usr/bin/env node
2
- import{i as e}from"./extension-plugin-D7gnpllF.js";import{openBrowser as t}from"@x-otto/shared";import{i18next as n}from"@x-otto/tui";import{getProviderDescriptor as r}from"@x-otto/ai";const i=`__otto_model_login__`,a=`__otto_model_setkey__`,o=`__otto_model_grant_capability__`,s=`__otto_model_install_plugin__`;async function c(t,r,c){let u=t[0];if(u===`refresh`||u===`--refresh`){let t=await r.app.refreshModels();r.state.models=r.app.modelRegistry.getAll().filter(e=>e.id!==`__otto_setup__`).map(e=>({id:e.id,name:e.name,provider:e.provider,api:e.api,strengths:e.strengths??[],contextWindow:e.contextWindow}));for(let e of t)for(let t of e.availableIds??[])r.state.availableModelIds.add(t);r.callbacks.persistAvailable();let i=Math.max(...t.map(e=>e.provider.length),0),a=e=>e.padEnd(i),o={},s=t.map((e,t)=>e.status===`ok`?(o[t]=`success`,n.t(`cmd.modelRefreshLineOk`,{provider:a(e.provider),added:e.added,kept:e.kept,total:e.total})):e.status===`skipped`&&e.reason===`not authenticated`?(o[t]=`muted`,n.t(`cmd.modelRefreshLineMuted`,{provider:a(e.provider),reason:n.t(`cmd.modelRefreshReasonNotAuthenticated`)})):e.reason?.includes(`capability not granted`)?(o[t]=`warning`,n.t(`cmd.modelRefreshLineWarning`,{provider:a(e.provider),reason:n.t(`cmd.modelRefreshReasonPendingCapability`)})):(o[t]=`warning`,n.t(`cmd.modelRefreshLineErr`,{provider:a(e.provider),status:e.status,reason:e.reason}))),c=t.some(e=>e.status===`skipped`&&e.reason?.includes(`capability not granted`));r.tui.modal.openModal({kind:`info`,title:n.t(`cmd.titleModels`),lines:[...s,``,n.t(`cmd.modelRefreshHint`)],action:c?{kind:`extension`,run:()=>{e([],r)}}:void 0,lineTones:o});return}if(u){let e=r.state.models.find(e=>e.id===u||e.name===u);if(!e){let e=u.includes(`/`)?u.split(`/`)[0]:u;if(!r.state.models.some(t=>t.provider.toLowerCase()===e.toLowerCase())&&/^[a-z0-9][\w-]*$/i.test(e)){r.tui.modal.openModal({kind:`select`,title:n.t(`cmd.modelDelegateProviderTitle`,{cand:e}),options:[{label:n.t(`cmd.modelDelegateProviderYes`,{cand:e}),value:`yes`},{label:n.t(`cmd.commonNo`),value:`no`}],onSelect:t=>{if(t!==`yes`)return;let i=`为 "${e}" 实现一个 provider 适配器:参照 packages/ai/src 下现有 provider 的实现,接 model-fetch 的 resolveModelsRequest、注册进 provider/model registry,并写测试跑绿。`;try{r.app.agentJobs.start({title:`write provider: ${e}`,prompt:i,sessionId:r.state.session.id,origin:`user`}),r.tui.info.showToast(n.t(`cmd.titleDelegated`),n.t(`cmd.modelProviderDelegated`,{cand:e}))}catch(e){r.tui.messages.pushMessage({role:`command_echo`,command:`/model`,status:`error`,summary:n.t(`cmd.delegateFailed`,{err:e instanceof Error?e.message:String(e)})})}},onCancel:()=>{}});return}r.tui.info.showToast(n.t(`cmd.titleModel`),n.t(`cmd.modelNotFoundUse`,{arg:u}));return}r.tui.info.showToast(n.t(`cmd.titleModel`),await r.callbacks.applyModel(e.id)),r.state.session.model?.id===e.id&&c?.afterSwitch?.();return}r.state.models=r.app.modelRegistry.getAll().filter(e=>e.id!==`__otto_setup__`).map(e=>({id:e.id,name:e.name,provider:e.provider,api:e.api,strengths:e.strengths??[],contextWindow:e.contextWindow}));let d=e=>r.state.availableModelIds.has(e),f=[...new Set(r.state.models.map(e=>r.callbacks.apiCredentialId(e.api??``,e.provider)))],p=new Map(await Promise.all(f.map(async e=>[e,await r.app.authStore.getAuthSource(e).catch(()=>null)]))),m=e=>{let t=r.callbacks.apiCredentialId(e.api??``,e.provider),i=p.get(t);if(!i)return``;let a=n.t(`pref.authModeLabel`,{context:i});return`${a===i?``:` · `}${a}`},h=[...r.state.models].sort((e,t)=>Number(d(t.id))-Number(d(e.id))||e.provider.localeCompare(t.provider)||e.name.localeCompare(t.name)),g=h.length>0?Math.max(...h.map(e=>e.name.length),8):8,_=e=>e?e>=1e6?`${(e/1e6).toFixed(1)}M`:`${Math.round(e/1e3)}k`:``,v=e=>{let t=d(e.id),n=e.id===r.state.currentModelId?`●`:` `,i=_(e.contextWindow),a=`${n} ${e.name.padEnd(g)}`+(i?` ${i}`:``),o=[m(e).replace(/^ · /,``),e.strengths.length>0?e.strengths.join(`/`):``].filter(Boolean).join(` `);return{isAvail:t,label:a,detail:o&&` ${o}`}},y=r.app.settings.get(`recent_models`)??[],b=new Map(r.state.models.map(e=>[e.id,e])),x=y.map(e=>b.get(e)).filter(e=>e!==void 0).map(e=>{let{isAvail:t,label:r,detail:i}=v(e);return{label:r,value:e.id,disabled:!t,group:n.t(`cmd.modelRecentGroup`),detail:i||void 0,searchText:e.name}}),S=r.state.availableModelIds.size===0,C=!r.app.modelRegistry.hasAnyPluginSourcedModel()&&S,w=(r.app.pendingProviderCapabilities??[]).length,T=[];C&&w>0?(T.push(n.t(`model.noProviderPlugin`)),T.push(n.t(`model.pendingCapabilities`,{count:w}))):C?T.push(n.t(`model.noProviderPlugin`)):S&&T.push(n.t(`model.noneAvailable`)),!C&&w>0&&T.push(n.t(`model.pendingCapabilities`,{count:w})),r.tui.modal.openModal({kind:`select`,searchable:!0,maxVisible:13,title:S?n.t(`cmd.modelSelectTitle`):n.t(`cmd.modelSelectTitleAvail`),subtitle:T.length>0?T.join(` · `):void 0,subtitleTone:w>0?`warning`:`default`,subtitleActions:C||S||w>0?[...C?[{label:n.t(`model.goInstallPlugin`),value:s}]:S?[{label:n.t(`model.goLogin`),value:i},{label:n.t(`model.setKey`),value:a}]:[],...w>0?[{label:n.t(`model.goGrantCapability`),value:o}]:[]]:void 0,options:[...x,...h.map(e=>{let{isAvail:t,label:n,detail:r}=v(e);return{label:n,value:e.id,disabled:!t,group:e.provider,detail:r||void 0,searchText:e.name}})],onSelect:async t=>{if(t===i){await l([],r,{method:`oauth`});return}if(t===a){await l([],r,{method:`key`});return}if(t===o){await e([],r);return}if(t===s){let{EXTENSION_ALIASES:e}=await import(`./extension-C3AP3KxC.js`),{runPluginInstall:t}=await import(`./extension-plugin-fPDz7XlG.js`),i=Object.entries(e).filter(([,e])=>e.capabilities.includes(`provider`)).map(([e])=>({label:e,value:e}));if(i.length===1){await t(i[0].value,r);return}r.tui.modal.openModal({kind:`select`,title:n.t(`cmd.modelSelectTitle`),options:i.map(e=>({label:e.label,value:e.value})),onSelect:async e=>{await t(e,r)},onCancel:()=>{}});return}r.tui.info.showToast(n.t(`cmd.titleModel`),await r.callbacks.applyModel(t)),r.state.session.model?.id===t&&c?.afterSwitch?.()}})}async function l(e,i,a){if(e.includes(`--service`))return d(e,i);let o=i.app.authStore,s=e=>new Promise(t=>i.tui.modal.openModal({kind:`select`,...e,title:e.title??n.t(`cmd.commonSelect`),detailPlacement:`inline`,onSelect:t,onCancel:()=>t(null)})),c=o.listProviders();if(c.length===0){i.tui.info.showToast(n.t(`cmd.titleLogin`),n.t(`cmd.loginNoProviders`));return}let l=e[0]||null,u=e=>o.oauth.get(e)?.name??r(e)?.name??i.callbacks.PROVIDER_DISPLAY_NAMES[e]??e,f=e=>u(e),p=e=>e?n.t(`pref.authModeLabel`,{context:e}):n.t(`cmd.loginNotSet`),m=(e,t)=>{let n=p(t);return u(e).toLowerCase()===e.toLowerCase()?n:`[${e}] · ${n}`};if(!l){let e=await Promise.all(c.map(e=>o.getAuthSource(e)));l=await s({title:n.t(`cmd.loginPickProvider`),options:c.map((t,n)=>({label:f(t),value:t,detail:m(t,e[n]??null)}))})}if(!l)return;let h=o.oauth.get(l)?.name??l,g=!!o.oauth.get(l),_=`key`;if(a?.method)_=a.method===`oauth`&&g?`oauth`:`key`;else if(g){let e=await s({title:n.t(`cmd.loginAuthMethod`,{name:h}),options:[{label:n.t(`cmd.loginMethodOauth`),value:`oauth`},{label:n.t(`cmd.loginMethodKey`),value:`key`}]});if(!e)return;_=e}if(_===`key`){let e=await i.tui.modal.openModal({kind:`input`,title:n.t(`cmd.loginKeyTitle`,{provider:l}),placeholder:n.t(`cmd.loginKeyPlaceholder`),mask:!0});if(!e||!e.trim()){i.tui.info.showToast(n.t(`cmd.titleLogin`),n.t(`cmd.loginCancelledNoKey`));return}await o.setCredentialKey(l,e.trim()),await o.save(),i.tui.info.showToast(n.t(`cmd.titleLogin`),n.t(`cmd.loginKeySaved`,{provider:l})),await i.callbacks.refreshModelsNow(e=>i.tui.info.showToast(n.t(`cmd.titleModels`),e));return}try{let e={};await o.login(l,{onAuth:r=>{if(e=r,r.url)if(t(r.url))i.tui.info.showToast(n.t(`cmd.titleLogin`),n.t(`cmd.loginOauthBrowserOpened`));else{let e=[n.t(`cmd.loginOauthUrlAuthorize`),`\n${r.url}`];r.instructions&&e.push(`\n${r.instructions}`),i.tui.modal.openModal({kind:`info`,title:n.t(`cmd.loginOauthTitle`,{provider:l}),lines:e})}if(r.code){let e=[n.t(`cmd.loginOauthCodeIntro`)];r.url&&e.push(`\n${r.url}`),e.push(`\n${n.t(`cmd.loginOauthCode`,{code:r.code})}`),r.instructions&&e.push(`\n${r.instructions}`),i.tui.modal.openModal({kind:`info`,title:n.t(`cmd.loginOauthTitle`,{provider:l}),lines:e})}},onProgress:e=>i.tui.info.showToast(n.t(`cmd.titleLogin`),e),onPrompt:async e=>await i.tui.modal.openModal({kind:`input`,title:e.message,placeholder:e.placeholder})??``,onManualCode:async r=>{let a=[];return e.url&&(a.push(n.t(`cmd.loginOauthUrlAuthorize`)),a.push(` `),a.push(e.url)),e.instructions&&(a.length>0&&a.push(` `),a.push(e.instructions)),await i.tui.modal.openModal({kind:`input`,title:n.t(`cmd.loginOauthPasteCode`,{provider:l}),description:a.length>0?a:void 0,placeholder:`code#state`,signal:r,actionButton:e.url?{label:n.t(`cmd.loginOauthOpenBrowserButton`),onPress:()=>{t(e.url)&&i.tui.info.showToast(n.t(`cmd.titleLogin`),n.t(`cmd.loginOauthBrowserOpened`))}}:void 0})??``}}),i.tui.info.showToast(n.t(`cmd.titleLogin`),n.t(`cmd.loginSuccess`,{provider:l})),await i.callbacks.refreshModelsNow(e=>i.tui.info.showToast(n.t(`cmd.titleModels`),e))}catch(e){i.tui.messages.pushMessage({role:`command_echo`,command:`/auth`,status:`error`,summary:n.t(`cmd.loginFailed`,{msg:e.message})})}}async function u(e,t){if(e.includes(`--service`))return f(e,t);let i=t.app.authStore,a=e[0]||null;if(!a){let e=i.listProviders(),o=await Promise.all(e.map(e=>i.getAuthSource(e))),s=e.filter((e,t)=>o[t]===`oauth`||o[t]===`api_key`);if(s.length===0){t.tui.info.showToast(n.t(`cmd.titleLogout`),n.t(`cmd.logoutNoStored`));return}a=await new Promise(a=>t.tui.modal.openModal({kind:`select`,title:n.t(`cmd.logoutPickProvider`),detailPlacement:`inline`,options:s.map(a=>{let s=i.oauth.get(a)?.name??r(a)?.name??t.callbacks.PROVIDER_DISPLAY_NAMES[a]??a,c=o[e.indexOf(a)];return{label:s.toLowerCase()===a.toLowerCase()?s:`${s} [${a}]`,value:a,detail:c?n.t(`pref.authModeLabel`,{context:c}):void 0}}),onSelect:a,onCancel:()=>a(null)}))}a&&(await i.logout(a),t.tui.info.showToast(n.t(`cmd.titleLogout`),n.t(`cmd.logoutSuccess`,{provider:a})))}async function d(e,t){let r=e.indexOf(`--service`),i=e.indexOf(`--token`);if(r===-1||i===-1){t.tui.info.showToast(n.t(`cmd.titleLogin`),n.t(`cmd.serviceLoginUsage`));return}let a=e[r+1],o=e[i+1];if(!a||!o){t.tui.info.showToast(n.t(`cmd.titleLogin`),n.t(`cmd.serviceLoginNeedValues`));return}let s=a.replace(/\/+$/,``);try{let e=await fetch(`${s}/auth/login`,{method:`POST`,headers:{Authorization:`Bearer ${o}`,Accept:`application/json`}});if(!e.ok){let r=(await e.json().catch(()=>({}))).error??e.statusText;t.tui.info.showToast(n.t(`cmd.titleLogin`),n.t(`cmd.serviceLoginFailed`,{err:String(r)}));return}let r=await e.json();if(typeof r.user_id!=`string`||typeof r.token!=`string`){t.tui.info.showToast(n.t(`cmd.titleLogin`),n.t(`cmd.serviceLoginBadResponse`));return}let{OTTO_IDENTITY_PATH:i,isShadowModeEnabled:a}=await import(`@x-otto/env`);if(a()){t.tui.info.showToast(n.t(`cmd.titleLogin`),n.t(`cmd.serviceLoginSuccess`,{url:s,user:r.user_id.slice(0,12)}));return}let{mkdir:c}=await import(`node:fs/promises`),{dirname:l}=await import(`node:path`),{atomicWriteFile:u}=await import(`@x-otto/persistence`),d={userId:r.user_id,token:r.token,serviceUrl:s,loginAt:Date.now()};await c(l(i),{recursive:!0}),await u(i,JSON.stringify(d,null,2)),t.tui.info.showToast(n.t(`cmd.titleLogin`),n.t(`cmd.serviceLoginSuccess`,{url:s,user:r.user_id.slice(0,12)}))}catch(e){t.tui.info.showToast(n.t(`cmd.titleLogin`),n.t(`cmd.serviceLoginFailed`,{err:e instanceof Error?e.message:String(e)}))}}async function f(e,t){try{let{OTTO_IDENTITY_PATH:e}=await import(`@x-otto/env`),{unlink:r}=await import(`node:fs/promises`);await r(e),t.tui.info.showToast(n.t(`cmd.titleLogout`),n.t(`cmd.serviceLogoutRemoved`))}catch(e){if(e.code===`ENOENT`){t.tui.info.showToast(n.t(`cmd.titleLogout`),n.t(`cmd.serviceLogoutNotFound`));return}t.tui.info.showToast(n.t(`cmd.titleLogout`),n.t(`cmd.serviceLogoutFailed`,{err:e instanceof Error?e.message:String(e)}))}}export{f as a,d as i,c as n,l as r,u as t};
3
- //# sourceMappingURL=model-auth-Dih25xOB.js.map
2
+ import{i as e}from"./extension-plugin-DoEcnDu5.js";import{openBrowser as t}from"@x-otto/shared";import{i18next as n}from"@x-otto/tui";import{getProviderDescriptor as r}from"@x-otto/ai";const i=`__otto_model_login__`,a=`__otto_model_setkey__`,o=`__otto_model_grant_capability__`,s=`__otto_model_install_plugin__`;async function c(t,r,c){let u=t[0];if(u===`refresh`||u===`--refresh`){let t=await r.app.refreshModels();r.state.models=r.app.modelRegistry.getAll().filter(e=>e.id!==`__otto_setup__`).map(e=>({id:e.id,name:e.name,provider:e.provider,api:e.api,strengths:e.strengths??[],contextWindow:e.contextWindow}));for(let e of t)for(let t of e.availableIds??[])r.state.availableModelIds.add(t);r.callbacks.persistAvailable();let i=Math.max(...t.map(e=>e.provider.length),0),a=e=>e.padEnd(i),o={},s=t.map((e,t)=>e.status===`ok`?(o[t]=`success`,n.t(`cmd.modelRefreshLineOk`,{provider:a(e.provider),added:e.added,kept:e.kept,total:e.total})):e.status===`skipped`&&e.reason===`not authenticated`?(o[t]=`muted`,n.t(`cmd.modelRefreshLineMuted`,{provider:a(e.provider),reason:n.t(`cmd.modelRefreshReasonNotAuthenticated`)})):e.reason?.includes(`capability not granted`)?(o[t]=`warning`,n.t(`cmd.modelRefreshLineWarning`,{provider:a(e.provider),reason:n.t(`cmd.modelRefreshReasonPendingCapability`)})):(o[t]=`warning`,n.t(`cmd.modelRefreshLineErr`,{provider:a(e.provider),status:e.status,reason:e.reason}))),c=t.some(e=>e.status===`skipped`&&e.reason?.includes(`capability not granted`));r.tui.modal.openModal({kind:`info`,title:n.t(`cmd.titleModels`),lines:[...s,``,n.t(`cmd.modelRefreshHint`)],action:c?{kind:`extension`,run:()=>{e([],r)}}:void 0,lineTones:o});return}if(u){let e=r.state.models.find(e=>e.id===u||e.name===u);if(!e){let e=u.includes(`/`)?u.split(`/`)[0]:u;if(!r.state.models.some(t=>t.provider.toLowerCase()===e.toLowerCase())&&/^[a-z0-9][\w-]*$/i.test(e)){r.tui.modal.openModal({kind:`select`,title:n.t(`cmd.modelDelegateProviderTitle`,{cand:e}),options:[{label:n.t(`cmd.modelDelegateProviderYes`,{cand:e}),value:`yes`},{label:n.t(`cmd.commonNo`),value:`no`}],onSelect:t=>{if(t!==`yes`)return;let i=`为 "${e}" 实现一个 provider 适配器:参照 packages/ai/src 下现有 provider 的实现,接 model-fetch 的 resolveModelsRequest、注册进 provider/model registry,并写测试跑绿。`;try{r.app.agentJobs.start({title:`write provider: ${e}`,prompt:i,sessionId:r.state.session.id,origin:`user`}),r.tui.info.showToast(n.t(`cmd.titleDelegated`),n.t(`cmd.modelProviderDelegated`,{cand:e}))}catch(e){r.tui.messages.pushMessage({role:`command_echo`,command:`/model`,status:`error`,summary:n.t(`cmd.delegateFailed`,{err:e instanceof Error?e.message:String(e)})})}},onCancel:()=>{}});return}r.tui.info.showToast(n.t(`cmd.titleModel`),n.t(`cmd.modelNotFoundUse`,{arg:u}));return}r.tui.info.showToast(n.t(`cmd.titleModel`),await r.callbacks.applyModel(e.id)),r.state.session.model?.id===e.id&&c?.afterSwitch?.();return}r.state.models=r.app.modelRegistry.getAll().filter(e=>e.id!==`__otto_setup__`).map(e=>({id:e.id,name:e.name,provider:e.provider,api:e.api,strengths:e.strengths??[],contextWindow:e.contextWindow}));let d=e=>r.state.availableModelIds.has(e),f=[...new Set(r.state.models.map(e=>r.callbacks.apiCredentialId(e.api??``,e.provider)))],p=new Map(await Promise.all(f.map(async e=>[e,await r.app.authStore.getAuthSource(e).catch(()=>null)]))),m=e=>{let t=r.callbacks.apiCredentialId(e.api??``,e.provider),i=p.get(t);if(!i)return``;let a=n.t(`pref.authModeLabel`,{context:i});return`${a===i?``:` · `}${a}`},h=[...r.state.models].sort((e,t)=>Number(d(t.id))-Number(d(e.id))||e.provider.localeCompare(t.provider)||e.name.localeCompare(t.name)),g=h.length>0?Math.max(...h.map(e=>e.name.length),8):8,_=e=>e?e>=1e6?`${(e/1e6).toFixed(1)}M`:`${Math.round(e/1e3)}k`:``,v=e=>{let t=d(e.id),n=e.id===r.state.currentModelId?`●`:` `,i=_(e.contextWindow),a=`${n} ${e.name.padEnd(g)}`+(i?` ${i}`:``),o=[m(e).replace(/^ · /,``),e.strengths.length>0?e.strengths.join(`/`):``].filter(Boolean).join(` `);return{isAvail:t,label:a,detail:o&&` ${o}`}},y=r.app.settings.get(`recent_models`)??[],b=new Map(r.state.models.map(e=>[e.id,e])),x=y.map(e=>b.get(e)).filter(e=>e!==void 0).map(e=>{let{isAvail:t,label:r,detail:i}=v(e);return{label:r,value:e.id,disabled:!t,group:n.t(`cmd.modelRecentGroup`),detail:i||void 0,searchText:e.name}}),S=r.state.availableModelIds.size===0,C=!r.app.modelRegistry.hasAnyPluginSourcedModel()&&S,w=(r.app.pendingProviderCapabilities??[]).length,T=[];C&&w>0?(T.push(n.t(`model.noProviderPlugin`)),T.push(n.t(`model.pendingCapabilities`,{count:w}))):C?T.push(n.t(`model.noProviderPlugin`)):S&&T.push(n.t(`model.noneAvailable`)),!C&&w>0&&T.push(n.t(`model.pendingCapabilities`,{count:w})),r.tui.modal.openModal({kind:`select`,searchable:!0,maxVisible:13,title:S?n.t(`cmd.modelSelectTitle`):n.t(`cmd.modelSelectTitleAvail`),subtitle:T.length>0?T.join(` · `):void 0,subtitleTone:w>0?`warning`:`default`,subtitleActions:C||S||w>0?[...C?[{label:n.t(`model.goInstallPlugin`),value:s}]:S?[{label:n.t(`model.goLogin`),value:i},{label:n.t(`model.setKey`),value:a}]:[],...w>0?[{label:n.t(`model.goGrantCapability`),value:o}]:[]]:void 0,options:[...x,...h.map(e=>{let{isAvail:t,label:n,detail:r}=v(e);return{label:n,value:e.id,disabled:!t,group:e.provider,detail:r||void 0,searchText:e.name}})],onSelect:async t=>{if(t===i){await l([],r,{method:`oauth`});return}if(t===a){await l([],r,{method:`key`});return}if(t===o){await e([],r);return}if(t===s){let{EXTENSION_ALIASES:e}=await import(`./extension-I87MkZYp.js`),{runPluginInstall:t}=await import(`./extension-plugin-VUdbaAN7.js`),i=Object.entries(e).filter(([,e])=>e.capabilities.includes(`provider`)).map(([e])=>({label:e,value:e}));if(i.length===1){await t(i[0].value,r);return}r.tui.modal.openModal({kind:`select`,title:n.t(`cmd.modelSelectTitle`),options:i.map(e=>({label:e.label,value:e.value})),onSelect:async e=>{await t(e,r)},onCancel:()=>{}});return}r.tui.info.showToast(n.t(`cmd.titleModel`),await r.callbacks.applyModel(t)),r.state.session.model?.id===t&&c?.afterSwitch?.()}})}async function l(e,i,a){if(e.includes(`--service`))return d(e,i);let o=i.app.authStore,s=e=>new Promise(t=>i.tui.modal.openModal({kind:`select`,...e,title:e.title??n.t(`cmd.commonSelect`),detailPlacement:`inline`,onSelect:t,onCancel:()=>t(null)})),c=o.listProviders();if(c.length===0){i.tui.info.showToast(n.t(`cmd.titleLogin`),n.t(`cmd.loginNoProviders`));return}let l=e[0]||null,u=e=>o.oauth.get(e)?.name??r(e)?.name??i.callbacks.PROVIDER_DISPLAY_NAMES[e]??e,f=e=>u(e),p=e=>e?n.t(`pref.authModeLabel`,{context:e}):n.t(`cmd.loginNotSet`),m=(e,t)=>{let n=p(t);return u(e).toLowerCase()===e.toLowerCase()?n:`[${e}] · ${n}`};if(!l){let e=await Promise.all(c.map(e=>o.getAuthSource(e)));l=await s({title:n.t(`cmd.loginPickProvider`),options:c.map((t,n)=>({label:f(t),value:t,detail:m(t,e[n]??null)}))})}if(!l)return;let h=o.oauth.get(l)?.name??l,g=!!o.oauth.get(l),_=`key`;if(a?.method)_=a.method===`oauth`&&g?`oauth`:`key`;else if(g){let e=await s({title:n.t(`cmd.loginAuthMethod`,{name:h}),options:[{label:n.t(`cmd.loginMethodOauth`),value:`oauth`},{label:n.t(`cmd.loginMethodKey`),value:`key`}]});if(!e)return;_=e}if(_===`key`){let e=await i.tui.modal.openModal({kind:`input`,title:n.t(`cmd.loginKeyTitle`,{provider:l}),placeholder:n.t(`cmd.loginKeyPlaceholder`),mask:!0});if(!e||!e.trim()){i.tui.info.showToast(n.t(`cmd.titleLogin`),n.t(`cmd.loginCancelledNoKey`));return}await o.setCredentialKey(l,e.trim()),await o.save(),i.tui.info.showToast(n.t(`cmd.titleLogin`),n.t(`cmd.loginKeySaved`,{provider:l})),await i.callbacks.refreshModelsNow(e=>i.tui.info.showToast(n.t(`cmd.titleModels`),e));return}try{let e={};await o.login(l,{onAuth:r=>{if(e=r,r.url)if(t(r.url))i.tui.info.showToast(n.t(`cmd.titleLogin`),n.t(`cmd.loginOauthBrowserOpened`));else{let e=[n.t(`cmd.loginOauthUrlAuthorize`),`\n${r.url}`];r.instructions&&e.push(`\n${r.instructions}`),i.tui.modal.openModal({kind:`info`,title:n.t(`cmd.loginOauthTitle`,{provider:l}),lines:e})}if(r.code){let e=[n.t(`cmd.loginOauthCodeIntro`)];r.url&&e.push(`\n${r.url}`),e.push(`\n${n.t(`cmd.loginOauthCode`,{code:r.code})}`),r.instructions&&e.push(`\n${r.instructions}`),i.tui.modal.openModal({kind:`info`,title:n.t(`cmd.loginOauthTitle`,{provider:l}),lines:e})}},onProgress:e=>i.tui.info.showToast(n.t(`cmd.titleLogin`),e),onPrompt:async e=>await i.tui.modal.openModal({kind:`input`,title:e.message,placeholder:e.placeholder})??``,onManualCode:async r=>{let a=[];return e.url&&(a.push(n.t(`cmd.loginOauthUrlAuthorize`)),a.push(` `),a.push(e.url)),e.instructions&&(a.length>0&&a.push(` `),a.push(e.instructions)),await i.tui.modal.openModal({kind:`input`,title:n.t(`cmd.loginOauthPasteCode`,{provider:l}),description:a.length>0?a:void 0,placeholder:`code#state`,signal:r,actionButton:e.url?{label:n.t(`cmd.loginOauthOpenBrowserButton`),onPress:()=>{t(e.url)&&i.tui.info.showToast(n.t(`cmd.titleLogin`),n.t(`cmd.loginOauthBrowserOpened`))}}:void 0})??``}}),i.tui.info.showToast(n.t(`cmd.titleLogin`),n.t(`cmd.loginSuccess`,{provider:l})),await i.callbacks.refreshModelsNow(e=>i.tui.info.showToast(n.t(`cmd.titleModels`),e))}catch(e){i.tui.messages.pushMessage({role:`command_echo`,command:`/auth`,status:`error`,summary:n.t(`cmd.loginFailed`,{msg:e.message})})}}async function u(e,t){if(e.includes(`--service`))return f(e,t);let i=t.app.authStore,a=e[0]||null;if(!a){let e=i.listProviders(),o=await Promise.all(e.map(e=>i.getAuthSource(e))),s=e.filter((e,t)=>o[t]===`oauth`||o[t]===`api_key`);if(s.length===0){t.tui.info.showToast(n.t(`cmd.titleLogout`),n.t(`cmd.logoutNoStored`));return}a=await new Promise(a=>t.tui.modal.openModal({kind:`select`,title:n.t(`cmd.logoutPickProvider`),detailPlacement:`inline`,options:s.map(a=>{let s=i.oauth.get(a)?.name??r(a)?.name??t.callbacks.PROVIDER_DISPLAY_NAMES[a]??a,c=o[e.indexOf(a)];return{label:s.toLowerCase()===a.toLowerCase()?s:`${s} [${a}]`,value:a,detail:c?n.t(`pref.authModeLabel`,{context:c}):void 0}}),onSelect:a,onCancel:()=>a(null)}))}a&&(await i.logout(a),t.tui.info.showToast(n.t(`cmd.titleLogout`),n.t(`cmd.logoutSuccess`,{provider:a})))}async function d(e,t){let r=e.indexOf(`--service`),i=e.indexOf(`--token`);if(r===-1||i===-1){t.tui.info.showToast(n.t(`cmd.titleLogin`),n.t(`cmd.serviceLoginUsage`));return}let a=e[r+1],o=e[i+1];if(!a||!o){t.tui.info.showToast(n.t(`cmd.titleLogin`),n.t(`cmd.serviceLoginNeedValues`));return}let s=a.replace(/\/+$/,``);try{let e=await fetch(`${s}/auth/login`,{method:`POST`,headers:{Authorization:`Bearer ${o}`,Accept:`application/json`}});if(!e.ok){let r=(await e.json().catch(()=>({}))).error??e.statusText;t.tui.info.showToast(n.t(`cmd.titleLogin`),n.t(`cmd.serviceLoginFailed`,{err:String(r)}));return}let r=await e.json();if(typeof r.user_id!=`string`||typeof r.token!=`string`){t.tui.info.showToast(n.t(`cmd.titleLogin`),n.t(`cmd.serviceLoginBadResponse`));return}let{OTTO_IDENTITY_PATH:i,isShadowModeEnabled:a}=await import(`@x-otto/env`);if(a()){t.tui.info.showToast(n.t(`cmd.titleLogin`),n.t(`cmd.serviceLoginSuccess`,{url:s,user:r.user_id.slice(0,12)}));return}let{mkdir:c}=await import(`node:fs/promises`),{dirname:l}=await import(`node:path`),{atomicWriteFile:u}=await import(`@x-otto/persistence`),d={userId:r.user_id,token:r.token,serviceUrl:s,loginAt:Date.now()};await c(l(i),{recursive:!0}),await u(i,JSON.stringify(d,null,2)),t.tui.info.showToast(n.t(`cmd.titleLogin`),n.t(`cmd.serviceLoginSuccess`,{url:s,user:r.user_id.slice(0,12)}))}catch(e){t.tui.info.showToast(n.t(`cmd.titleLogin`),n.t(`cmd.serviceLoginFailed`,{err:e instanceof Error?e.message:String(e)}))}}async function f(e,t){try{let{OTTO_IDENTITY_PATH:e}=await import(`@x-otto/env`),{unlink:r}=await import(`node:fs/promises`);await r(e),t.tui.info.showToast(n.t(`cmd.titleLogout`),n.t(`cmd.serviceLogoutRemoved`))}catch(e){if(e.code===`ENOENT`){t.tui.info.showToast(n.t(`cmd.titleLogout`),n.t(`cmd.serviceLogoutNotFound`));return}t.tui.info.showToast(n.t(`cmd.titleLogout`),n.t(`cmd.serviceLogoutFailed`,{err:e instanceof Error?e.message:String(e)}))}}export{f as a,d as i,c as n,l as r,u as t};
3
+ //# sourceMappingURL=model-auth-B6ufdM5K.js.map