@x-otto/cli 0.0.1-alpha.20 → 0.0.1-alpha.21
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.
- package/bin/otto.js +1 -1
- package/dist/{auth-command-kkJh-O5y.js → auth-command-D1T16ed-.js} +1 -1
- package/dist/{auth-command-kkJh-O5y.js.map → auth-command-D1T16ed-.js.map} +1 -1
- package/dist/bundled-extensions/plugin-otto-wire-protocols/plugin-dist/plugin.cjs +14 -12
- package/dist/command-dispatcher-ZyrfHgIk.js +21 -0
- package/dist/command-dispatcher-ZyrfHgIk.js.map +1 -0
- package/dist/{config-migration-0zdXsEDB.js → config-migration-DatflIxs.js} +1 -1
- package/dist/{config-migration-0zdXsEDB.js.map → config-migration-DatflIxs.js.map} +1 -1
- package/dist/{daemon-BtiaXSxn.js → daemon-COESl-zU.js} +1 -1
- package/dist/{daemon-BtiaXSxn.js.map → daemon-COESl-zU.js.map} +1 -1
- package/dist/{daemon-base-BzhYY4FS.js → daemon-base-YOWgYYN7.js} +1 -1
- package/dist/{daemon-base-BzhYY4FS.js.map → daemon-base-YOWgYYN7.js.map} +1 -1
- package/dist/{doctor-DbQC8Z7p.js → doctor-CuNvDmyA.js} +3 -3
- package/dist/{doctor-DbQC8Z7p.js.map → doctor-CuNvDmyA.js.map} +1 -1
- package/dist/{doctor-attach-OJHn8L52.js → doctor-attach-_zeIJewx.js} +2 -2
- package/dist/{doctor-attach-OJHn8L52.js.map → doctor-attach-_zeIJewx.js.map} +1 -1
- package/dist/{doctor-cdp-D66ChCCN.js → doctor-cdp-K_mIFRfA.js} +1 -1
- package/dist/{doctor-cdp-D66ChCCN.js.map → doctor-cdp-K_mIFRfA.js.map} +1 -1
- package/dist/{doctor-heap-Brsro5QJ.js → doctor-heap-Rr2nNvcw.js} +2 -2
- package/dist/{doctor-heap-Brsro5QJ.js.map → doctor-heap-Rr2nNvcw.js.map} +1 -1
- package/dist/{doctor-ps-B4Syy9qa.js → doctor-ps-OL3FGQYu.js} +2 -2
- package/dist/{doctor-ps-B4Syy9qa.js.map → doctor-ps-OL3FGQYu.js.map} +1 -1
- package/dist/feedback-B-d-GV2J.js +2 -0
- package/dist/{feedback-DrUjMuKw.js → feedback-DuTIx3-K.js} +1 -1
- package/dist/{feedback-DrUjMuKw.js.map → feedback-DuTIx3-K.js.map} +1 -1
- package/dist/{feishu-setup-C2UDdpIm.js → feishu-setup-B6ETf-8Y.js} +1 -1
- package/dist/{feishu-setup-C2UDdpIm.js.map → feishu-setup-B6ETf-8Y.js.map} +1 -1
- package/dist/{ftue-doctor-recovery-DhiseXnP.js → ftue-doctor-recovery-DeAf8jVo.js} +1 -1
- package/dist/{ftue-doctor-recovery-DhiseXnP.js.map → ftue-doctor-recovery-DeAf8jVo.js.map} +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -8
- package/dist/index.js.map +1 -1
- package/dist/{install-hTDjeo61.js → install-DlVJRHRT.js} +1 -1
- package/dist/{install-hTDjeo61.js.map → install-DlVJRHRT.js.map} +1 -1
- package/dist/install-WCcQ9k9M.js +2 -0
- package/dist/{mcp-BDfBf6gx.js → mcp-DbTYT7hB.js} +1 -1
- package/dist/{mcp-BDfBf6gx.js.map → mcp-DbTYT7hB.js.map} +1 -1
- package/dist/{migrate-4QDwYLf6.js → migrate-boDXhEyV.js} +2 -2
- package/dist/{migrate-4QDwYLf6.js.map → migrate-boDXhEyV.js.map} +1 -1
- package/dist/observe-CZ96Vre9.js +83 -0
- package/dist/observe-CZ96Vre9.js.map +1 -0
- package/dist/{parse-cli-klcuBpG2.js → parse-cli-CqocMnXY.js} +2 -2
- package/dist/{parse-cli-klcuBpG2.js.map → parse-cli-CqocMnXY.js.map} +1 -1
- package/dist/persistence-CTXa-OyC.js +85 -0
- package/dist/persistence-CTXa-OyC.js.map +1 -0
- package/dist/{plugin-assembly-check-CvuJ4dHf.js → plugin-assembly-check-C2TephMb.js} +1 -1
- package/dist/{plugin-assembly-check-CvuJ4dHf.js.map → plugin-assembly-check-C2TephMb.js.map} +1 -1
- package/dist/proxy-BvdBsB1O.js +2 -0
- package/dist/{proxy-CMQBvdZC.js → proxy-evg7ZvoD.js} +1 -1
- package/dist/{proxy-CMQBvdZC.js.map → proxy-evg7ZvoD.js.map} +1 -1
- package/dist/{ps-Kb6ZsEpC.js → ps-DKrluEuy.js} +1 -1
- package/dist/{ps-Kb6ZsEpC.js.map → ps-DKrluEuy.js.map} +1 -1
- package/dist/registry-BOX7DaxS.js +2 -0
- package/dist/{registry-Cr5To_5r.js → registry-BdCcwyGy.js} +2 -2
- package/dist/{registry-Cr5To_5r.js.map → registry-BdCcwyGy.js.map} +1 -1
- package/dist/{run-attach-CgYMem8s.js → run-attach-CvGCWF_i.js} +1 -1
- package/dist/{run-attach-CgYMem8s.js.map → run-attach-CvGCWF_i.js.map} +1 -1
- package/dist/{serve-Bq21zw1O.js → serve-Dbn3THez.js} +2 -2
- package/dist/{serve-Bq21zw1O.js.map → serve-Dbn3THez.js.map} +1 -1
- package/package.json +2 -2
- package/dist/command-dispatcher-CahuAJnO.js +0 -17
- package/dist/command-dispatcher-CahuAJnO.js.map +0 -1
- package/dist/debug-DfgcrVCI.js +0 -34
- package/dist/debug-DfgcrVCI.js.map +0 -1
- package/dist/extension-plugin-BEQlmUET.js +0 -2
- package/dist/extension-plugin-BYR-MJ1U.js +0 -7
- package/dist/extension-plugin-BYR-MJ1U.js.map +0 -1
- package/dist/feedback-DjNFsjeN.js +0 -2
- package/dist/install-DkJgbg2p.js +0 -2
- package/dist/model-auth-BMY86Bk4.js +0 -3
- package/dist/model-auth-BMY86Bk4.js.map +0 -1
- package/dist/model-auth-CSLiEFJ-.js +0 -2
- package/dist/observe-DSrUS2YI.js +0 -34
- package/dist/observe-DSrUS2YI.js.map +0 -1
- package/dist/persistenced-CW8sdhd0.js +0 -36
- package/dist/persistenced-CW8sdhd0.js.map +0 -1
- package/dist/proxy-TzOLqtkc.js +0 -2
- package/dist/registry-DAJ7oZTe.js +0 -2
- package/dist/remote-persistence-server-BLBIop0u.js +0 -35
- package/dist/remote-persistence-server-BLBIop0u.js.map +0 -1
- package/dist/time-travel-8X-uAgHw.js +0 -21
- package/dist/time-travel-8X-uAgHw.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"parse-cli-klcuBpG2.js","names":[],"sources":["../src/parse-cli.ts"],"sourcesContent":["import { resolve } from 'node:path'\n\nimport type { CLIOptions, RunMode } from './types'\nimport { getVersion } from './version'\n\nexport type SubCommand = 'run' | 'serve' | 'daemon' | 'doctor' | 'config' | 'migrate' | 'auth' | 'observe' | 'debug' | 'time-travel' | 'mcp' | 'persistenced' | 'remote-persistence-server' | 'persistence' | 'extension' | 'ps' | 'feedback'\n\nexport interface ParsedCLI {\n options: CLIOptions\n /** 未命中任何内建子命令时为 `undefined`(而非 `null` 哨兵——与 SUB_COMMAND_NAMES 的\n * 元素类型保持一致,消费方无需 `.filter(n => n !== null)` 剔除哨兵值)。 */\n subCommand: SubCommand | undefined\n subCommandArgs: string\n /**\n * RFC-112 D2:内建 `SUB_COMMAND_NAMES` 未命中的第一个位置参数(候选插件 CLI 命令名,\n * 如 `otto feishu ...` 里的 `\"feishu\"`)。`undefined` 表示第一个参数已命中内建命令,\n * 或压根没有位置参数。**不改变** `options.prompt`/`subCommand` 的既有构建逻辑——\n * 这两者仍然按\"当作 prompt\"的旧行为完整计算好,供插件命令名查无匹配时零改动回退。\n */\n unresolvedCommandName?: string\n /**\n * 与 `unresolvedCommandName` 配对:该候选名之后的**原始** args(未做 flag 过滤,含 `-h`\n * 等字面量),只在插件命令表命中该名字时使用(透传给插件可执行文件)。\n */\n unresolvedCommandArgs?: string[]\n}\n\n/** RFC-112 D2:导出供 `cli.ts` 构建插件 CLI 命令表时用作保留字集合(插件不得覆盖)。 */\nexport const SUB_COMMAND_NAMES = new Set<SubCommand>([\n 'run', 'serve', 'daemon', 'doctor', 'config', 'migrate', 'auth', 'observe', 'debug', 'time-travel', 'mcp', 'persistenced', 'remote-persistence-server', 'persistence', 'extension', 'ps', 'feedback',\n])\n\ntype OptionEntry =\n | { flags: string[]; type: 'boolean'; key: keyof CLIOptions }\n | { flags: string[]; type: 'mode'; value: RunMode }\n | { flags: string[]; type: 'string'; key: keyof CLIOptions }\n | { flags: string[]; type: 'number'; key: keyof CLIOptions }\n | { flags: string[]; type: 'path'; key: keyof CLIOptions }\n | { flags: string[]; type: 'action'; handler: () => void }\n\nconst OPTIONS: OptionEntry[] = [\n { flags: ['--print', '-p'], type: 'mode', value: 'print' },\n { flags: ['--interactive', '-i'], type: 'mode', value: 'interactive' },\n { flags: ['--json'], type: 'mode', value: 'json' },\n { flags: ['--model', '-m'], type: 'string', key: 'model' },\n { flags: ['--config', '-c'], type: 'string', key: 'configPath' },\n { flags: ['--project', '-d'], type: 'path', key: 'projectDir' },\n { flags: ['--quiet', '-q'], type: 'boolean', key: 'quiet' },\n { flags: ['--verbose'], type: 'boolean', key: 'verbose' },\n { flags: ['--max-tool-turns'], type: 'number', key: 'maxToolTurns' },\n { flags: ['--max-tool-turn-extensions'], type: 'number', key: 'maxToolTurnExtensions' },\n { flags: ['--prompt-token-budget'], type: 'number', key: 'promptTokenBudget' },\n { flags: ['--prompt-time-budget'], type: 'number', key: 'promptTimeBudgetMin' },\n { flags: ['--session-cost-budget'], type: 'number', key: 'sessionCostBudget' },\n { flags: ['--no-stall-detection'], type: 'boolean', key: 'noStallDetection' },\n { flags: ['--stall-window-turns'], type: 'number', key: 'stallWindowTurns' },\n { flags: ['--stall-repeat-threshold'], type: 'number', key: 'stallRepeatThreshold' },\n { flags: ['--continue'], type: 'string', key: 'continueSession' },\n { flags: ['--attach'], type: 'string', key: 'attachSession' },\n { flags: ['--force-write-lease'], type: 'boolean', key: 'forceWriteLease' },\n // RFC-115 M3(P1-1 修正):仅由 cli.ts 的内存治理触发重启 chaperone 附加,不对外文档化——\n // 区分\"内存压力触发的自动重启\"与\"用户手动 otto --continue\",前者才应起算 D5 宽限期。\n { flags: ['--memory-restart-grace'], type: 'boolean', key: 'memoryRestartGrace' },\n // RFC-115 M4:仅由 cli.ts 的内存治理触发重启 chaperone 附加,不对外文档化——重启前\n // 会话正忙时的续跑原文,子进程启动后自动重新提交为续跑轮。\n { flags: ['--resume-prompt'], type: 'string', key: 'resumePrompt' },\n { flags: ['--session-url'], type: 'string', key: 'sessionUrl' },\n { flags: ['--session-token'], type: 'string', key: 'sessionToken' },\n { flags: ['--id'], type: 'string', key: 'workspaceId' },\n { flags: ['--shadow'], type: 'boolean', key: 'shadow' },\n { flags: ['--help', '-h'], type: 'boolean', key: 'showHelp' },\n { flags: ['--version', '-v'], type: 'action', handler: printVersion },\n]\n\nconst OPTION_LOOKUP: Map<string, OptionEntry> = new Map(\n OPTIONS.flatMap(e => e.flags.map(f => [f, e] as const)),\n)\n\nclass ArgCursor {\n private i: number\n constructor(private args: string[]) {\n this.i = 0\n }\n current(): string | undefined { return this.args[this.i] }\n next(): string | undefined { return this.args[++this.i] }\n advance(): void { this.i++ }\n skip(count: number): void { this.i += count }\n pos(): number { return this.i }\n}\n\nexport function parseCLI(argv: string[]): ParsedCLI {\n const args = argv.slice(2)\n const cursor = new ArgCursor(args)\n\n const options: CLIOptions = {\n mode: 'interactive',\n projectDir: process.cwd(),\n verbose: false,\n quiet: false,\n }\n\n const first = cursor.current()\n let subCommand: SubCommand | undefined\n let subCommandArgs = ''\n let unresolvedCommandName: string | undefined\n let unresolvedCommandArgs: string[] | undefined\n if (first && SUB_COMMAND_NAMES.has(first as SubCommand)) {\n subCommand = first as SubCommand\n subCommandArgs = args.slice(1).join(' ')\n cursor.skip(1)\n\n if (subCommand === 'run') {\n options.prompt = args.slice(1).filter(a => !a.startsWith('-')).join(' ')\n options.mode = 'print'\n }\n } else if (first && !first.startsWith('-')) {\n // RFC-112 D2:第一个位置参数不是内建命令,也不以 `-` 开头(不是 flag)——\n // 记录为候选插件 CLI 命令名,供 cli.ts 查插件表二次判定;不改变下方现有的\n // \"当作 prompt\" 逻辑(该逻辑仍会照常把 first 纳入 promptParts,回退路径零改动)。\n unresolvedCommandName = first\n unresolvedCommandArgs = args.slice(1)\n }\n\n let promptParts: string[] = options.prompt ? [options.prompt] : []\n\n while (cursor.current() !== undefined) {\n const arg = cursor.current()!\n\n if (arg === '--inspect') {\n options.inspect = true\n cursor.advance()\n continue\n }\n if (arg?.startsWith('--inspect=')) {\n const port = Number(arg.slice('--inspect='.length))\n options.inspect = Number.isInteger(port) && port > 0 ? port : true\n cursor.advance()\n continue\n }\n // RFC-196 M3:重启代际计数(chaperone 重启 spawn 注入,不文档化——内部可观测标记,\n // 供 ≥3 代告警与 otto ps GEN 列展示;不驱动任何行为分支)。\n if (arg?.startsWith('--restart-generation=')) {\n const gen = Number(arg.slice('--restart-generation='.length))\n if (Number.isInteger(gen) && gen > 0) options.restartGeneration = gen\n cursor.advance()\n continue\n }\n\n const entry = OPTION_LOOKUP.get(arg)\n if (entry) {\n cursor.advance()\n switch (entry.type) {\n case 'boolean':\n ;(options as unknown as Record<string, unknown>)[entry.key] = true\n break\n case 'mode':\n options.mode = entry.value\n break\n case 'string':\n ;(options as unknown as Record<string, unknown>)[entry.key] = cursor.current()\n cursor.advance()\n break\n case 'number':\n ;(options as unknown as Record<string, unknown>)[entry.key] = Number(cursor.current())\n cursor.advance()\n break\n case 'path':\n ;(options as unknown as Record<string, unknown>)[entry.key] = resolve(cursor.current() ?? process.cwd())\n cursor.advance()\n break\n case 'action':\n entry.handler()\n break\n }\n continue\n }\n\n if (arg && !arg.startsWith('-') && !subCommand) {\n if (options.prompt === undefined && options.mode === 'interactive') {\n options.mode = 'print'\n }\n promptParts.push(arg)\n }\n cursor.advance()\n }\n\n if (promptParts.length > 0) {\n options.prompt = promptParts.join(' ')\n }\n\n return { options, subCommand, subCommandArgs, unresolvedCommandName, unresolvedCommandArgs }\n}\n\n/** 帮助文本行(供 plain-text printHelp 与 TUI InfoDialog 共用)。 */\nexport function getHelpLines(): string[] {\n return [\n '',\n 'otto - AI 助理命令行工具',\n '',\n '用法:',\n ' otto [提示词] 使用提示词启动(print 模式)',\n ' otto 进入交互式 TUI 模式',\n ' otto --json \"query\" JSON 输出模式',\n '',\n '选项:',\n ' -i, --interactive Interactive 模式(默认)',\n ' -p, --print Print 模式(非交互)',\n ' --json JSON 输出模式',\n ' -m, --model <id> 指定模型',\n ' -c, --config <path> 配置文件路径',\n ' -d, --project <dir> 项目目录',\n ' --verbose 输出详细日志',\n ' -q, --quiet 只输出 error 日志',\n ' --shadow 影子模式:otto 自有状态(凭据/会话/记忆/教训/配置/trace/日志)本次会话零磁盘写入(不保证 provider 服务端日志与 shell/OS 层痕迹;RFC-345)',\n ' --max-tool-turns <n> 单个用户回合最大工具回合数(默认 120)',\n ' --max-tool-turn-extensions <n> 触顶后压缩续跑的扩展次数(默认主会话 3 / 子 agent 0)',\n ' --prompt-token-budget <n> 单回合输出 token 预算(配置后轮次上限 ×4 退位安全阀;RFC-104)',\n ' --prompt-time-budget <min> 单回合墙钟时间预算(分钟)',\n ' --session-cost-budget <usd> 会话累计成本软预算(美元,超过一次性提醒不阻断;RFC-204)',\n ' --no-stall-detection 关闭早期停滞检测(默认开启;RFC-203)',\n ' --stall-window-turns <n> 停滞检测滑动窗口大小(需与 --stall-repeat-threshold 同时提供,默认 8)',\n ' --stall-repeat-threshold <n> 停滞检测同签名重复阈值(需与 --stall-window-turns 同时提供,默认 4)',\n ' --continue <id> 继续已有会话',\n ' --session-url <url> 远端会话持久化地址(如 http://host:3001/sessions)',\n ' --session-token <token> 远端会话持久化 Bearer token',\n ' --inspect[=port] 启用 Node.js inspector(默认: 9229)',\n ' -h, --help 显示帮助',\n ' --version, -v 显示版本',\n '',\n '命令:',\n ' otto run <prompt> 执行一次性任务',\n ' otto serve [--port N] [--inspect] [--static <dir>] [--token <t>] 启动 HTTP/WS service(--static 同源托管前端 SPA,--token 启用鉴权)',\n ' otto doctor [--summary|--json] 环境健康检查(环境/配置/存储状态/连通性)',\n ' otto auth login <provider> 登录(cursor / anthropic / openai-codex / github-copilot)',\n ' otto auth status 查看各 provider 凭据来源',\n ' otto auth logout <provider> 注销并清除凭据',\n ' otto config export [--project] [-o <path>] 导出配置(RFC-219)',\n ' otto config import <file> [--dry-run] [--scope global|project] [--no-plugins] [--plugins-only] [--yes] 导入配置',\n ' otto config restore [--list] 撤销最近一次导入',\n ' otto migrate export [--no-sessions] [-o <path>] 导出配置+插件清单+会话数据为迁移包(换电脑用)',\n ' otto migrate import <archive> [--no-sessions] 导入迁移包(目标已有会话数据时跳过会话部分)',\n ' otto observe <action> <id> 观测面板(summary / timeline / spans)',\n ' otto observe debug <action> [...] 调试面板(breakpoints / points / state)',\n ' otto observe replay <action> <id> 时间旅行(checkpoints / scrubber / restore / fork / replay)',\n ' otto debug <action> [...] (alias: otto observe debug)',\n ' otto time-travel <action> <id> (alias: otto observe replay)',\n ' otto mcp login <name> MCP server OAuth 登录',\n ' otto mcp logout <name> 登出 MCP server',\n ' otto mcp list 列出已认证的 MCP server',\n ' otto extension create <name> 生成插件骨架(命令 + 面板 + manifest,开箱即用)',\n ' otto extension install <name> [--yes] 安装首方/自定义插件(anthropic-provider / deepseek-provider / browser-use / …)',\n ' otto extension update [name] 检查并重装已过期的已安装 skill/plugin',\n ' otto persistence local [...flags] 启动独立持久化服务(storage + events + sync)',\n ' otto persistence remote [...flags] 启动最小化远程持久化服务(5 REST 端点)',\n ' otto persistenced [...flags] (alias: otto persistence local)',\n ' otto remote-persistence-server [...flags] (alias: otto persistence remote)',\n ' otto ps [--sweep] [--include-weak] [--json] 进程群列表与孤儿清扫(SIGTERM only,RFC-196)',\n '',\n '面板命令默认连 `otto serve` 实例(http://127.0.0.1:8417,OTTO_SERVICE_URL/--remote 可覆盖)。',\n ]\n}\n\nfunction printVersion(): void {\n process.stdout.write(`otto ${getVersion()}\\n`)\n process.exit(0)\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"parse-cli-CqocMnXY.js","names":[],"sources":["../src/parse-cli.ts"],"sourcesContent":["import { resolve } from 'node:path'\n\nimport type { CLIOptions, RunMode } from './types'\nimport { getVersion } from './version'\n\nexport type SubCommand = 'run' | 'serve' | 'daemon' | 'doctor' | 'config' | 'migrate' | 'auth' | 'observe' | 'debug' | 'time-travel' | 'mcp' | 'persistenced' | 'remote-persistence-server' | 'persistence' | 'extension' | 'ps' | 'feedback'\n\nexport interface ParsedCLI {\n options: CLIOptions\n /** 未命中任何内建子命令时为 `undefined`(而非 `null` 哨兵——与 SUB_COMMAND_NAMES 的\n * 元素类型保持一致,消费方无需 `.filter(n => n !== null)` 剔除哨兵值)。 */\n subCommand: SubCommand | undefined\n subCommandArgs: string\n /**\n * RFC-112 D2:内建 `SUB_COMMAND_NAMES` 未命中的第一个位置参数(候选插件 CLI 命令名,\n * 如 `otto feishu ...` 里的 `\"feishu\"`)。`undefined` 表示第一个参数已命中内建命令,\n * 或压根没有位置参数。**不改变** `options.prompt`/`subCommand` 的既有构建逻辑——\n * 这两者仍然按\"当作 prompt\"的旧行为完整计算好,供插件命令名查无匹配时零改动回退。\n */\n unresolvedCommandName?: string\n /**\n * 与 `unresolvedCommandName` 配对:该候选名之后的**原始** args(未做 flag 过滤,含 `-h`\n * 等字面量),只在插件命令表命中该名字时使用(透传给插件可执行文件)。\n */\n unresolvedCommandArgs?: string[]\n}\n\n/** RFC-112 D2:导出供 `cli.ts` 构建插件 CLI 命令表时用作保留字集合(插件不得覆盖)。 */\nexport const SUB_COMMAND_NAMES = new Set<SubCommand>([\n 'run', 'serve', 'daemon', 'doctor', 'config', 'migrate', 'auth', 'observe', 'debug', 'time-travel', 'mcp', 'persistenced', 'remote-persistence-server', 'persistence', 'extension', 'ps', 'feedback',\n])\n\ntype OptionEntry =\n | { flags: string[]; type: 'boolean'; key: keyof CLIOptions }\n | { flags: string[]; type: 'mode'; value: RunMode }\n | { flags: string[]; type: 'string'; key: keyof CLIOptions }\n | { flags: string[]; type: 'number'; key: keyof CLIOptions }\n | { flags: string[]; type: 'path'; key: keyof CLIOptions }\n | { flags: string[]; type: 'action'; handler: () => void }\n\nconst OPTIONS: OptionEntry[] = [\n { flags: ['--print', '-p'], type: 'mode', value: 'print' },\n { flags: ['--interactive', '-i'], type: 'mode', value: 'interactive' },\n { flags: ['--json'], type: 'mode', value: 'json' },\n { flags: ['--model', '-m'], type: 'string', key: 'model' },\n { flags: ['--config', '-c'], type: 'string', key: 'configPath' },\n { flags: ['--project', '-d'], type: 'path', key: 'projectDir' },\n { flags: ['--quiet', '-q'], type: 'boolean', key: 'quiet' },\n { flags: ['--verbose'], type: 'boolean', key: 'verbose' },\n { flags: ['--max-tool-turns'], type: 'number', key: 'maxToolTurns' },\n { flags: ['--max-tool-turn-extensions'], type: 'number', key: 'maxToolTurnExtensions' },\n { flags: ['--prompt-token-budget'], type: 'number', key: 'promptTokenBudget' },\n { flags: ['--prompt-time-budget'], type: 'number', key: 'promptTimeBudgetMin' },\n { flags: ['--session-cost-budget'], type: 'number', key: 'sessionCostBudget' },\n { flags: ['--no-stall-detection'], type: 'boolean', key: 'noStallDetection' },\n { flags: ['--stall-window-turns'], type: 'number', key: 'stallWindowTurns' },\n { flags: ['--stall-repeat-threshold'], type: 'number', key: 'stallRepeatThreshold' },\n { flags: ['--continue'], type: 'string', key: 'continueSession' },\n { flags: ['--attach'], type: 'string', key: 'attachSession' },\n { flags: ['--force-write-lease'], type: 'boolean', key: 'forceWriteLease' },\n // RFC-115 M3(P1-1 修正):仅由 cli.ts 的内存治理触发重启 chaperone 附加,不对外文档化——\n // 区分\"内存压力触发的自动重启\"与\"用户手动 otto --continue\",前者才应起算 D5 宽限期。\n { flags: ['--memory-restart-grace'], type: 'boolean', key: 'memoryRestartGrace' },\n // RFC-115 M4:仅由 cli.ts 的内存治理触发重启 chaperone 附加,不对外文档化——重启前\n // 会话正忙时的续跑原文,子进程启动后自动重新提交为续跑轮。\n { flags: ['--resume-prompt'], type: 'string', key: 'resumePrompt' },\n { flags: ['--session-url'], type: 'string', key: 'sessionUrl' },\n { flags: ['--session-token'], type: 'string', key: 'sessionToken' },\n { flags: ['--id'], type: 'string', key: 'workspaceId' },\n { flags: ['--shadow'], type: 'boolean', key: 'shadow' },\n { flags: ['--help', '-h'], type: 'boolean', key: 'showHelp' },\n { flags: ['--version', '-v'], type: 'action', handler: printVersion },\n]\n\nconst OPTION_LOOKUP: Map<string, OptionEntry> = new Map(\n OPTIONS.flatMap(e => e.flags.map(f => [f, e] as const)),\n)\n\nclass ArgCursor {\n private i: number\n constructor(private args: string[]) {\n this.i = 0\n }\n current(): string | undefined { return this.args[this.i] }\n next(): string | undefined { return this.args[++this.i] }\n advance(): void { this.i++ }\n skip(count: number): void { this.i += count }\n pos(): number { return this.i }\n}\n\nexport function parseCLI(argv: string[]): ParsedCLI {\n const args = argv.slice(2)\n const cursor = new ArgCursor(args)\n\n const options: CLIOptions = {\n mode: 'interactive',\n projectDir: process.cwd(),\n verbose: false,\n quiet: false,\n }\n\n const first = cursor.current()\n let subCommand: SubCommand | undefined\n let subCommandArgs = ''\n let unresolvedCommandName: string | undefined\n let unresolvedCommandArgs: string[] | undefined\n if (first && SUB_COMMAND_NAMES.has(first as SubCommand)) {\n subCommand = first as SubCommand\n subCommandArgs = args.slice(1).join(' ')\n cursor.skip(1)\n\n if (subCommand === 'run') {\n options.prompt = args.slice(1).filter(a => !a.startsWith('-')).join(' ')\n options.mode = 'print'\n }\n } else if (first && !first.startsWith('-')) {\n // RFC-112 D2:第一个位置参数不是内建命令,也不以 `-` 开头(不是 flag)——\n // 记录为候选插件 CLI 命令名,供 cli.ts 查插件表二次判定;不改变下方现有的\n // \"当作 prompt\" 逻辑(该逻辑仍会照常把 first 纳入 promptParts,回退路径零改动)。\n unresolvedCommandName = first\n unresolvedCommandArgs = args.slice(1)\n }\n\n let promptParts: string[] = options.prompt ? [options.prompt] : []\n\n while (cursor.current() !== undefined) {\n const arg = cursor.current()!\n\n if (arg === '--inspect') {\n options.inspect = true\n cursor.advance()\n continue\n }\n if (arg?.startsWith('--inspect=')) {\n const port = Number(arg.slice('--inspect='.length))\n options.inspect = Number.isInteger(port) && port > 0 ? port : true\n cursor.advance()\n continue\n }\n // RFC-196 M3:重启代际计数(chaperone 重启 spawn 注入,不文档化——内部可观测标记,\n // 供 ≥3 代告警与 otto ps GEN 列展示;不驱动任何行为分支)。\n if (arg?.startsWith('--restart-generation=')) {\n const gen = Number(arg.slice('--restart-generation='.length))\n if (Number.isInteger(gen) && gen > 0) options.restartGeneration = gen\n cursor.advance()\n continue\n }\n\n const entry = OPTION_LOOKUP.get(arg)\n if (entry) {\n cursor.advance()\n switch (entry.type) {\n case 'boolean':\n ;(options as unknown as Record<string, unknown>)[entry.key] = true\n break\n case 'mode':\n options.mode = entry.value\n break\n case 'string':\n ;(options as unknown as Record<string, unknown>)[entry.key] = cursor.current()\n cursor.advance()\n break\n case 'number':\n ;(options as unknown as Record<string, unknown>)[entry.key] = Number(cursor.current())\n cursor.advance()\n break\n case 'path':\n ;(options as unknown as Record<string, unknown>)[entry.key] = resolve(cursor.current() ?? process.cwd())\n cursor.advance()\n break\n case 'action':\n entry.handler()\n break\n }\n continue\n }\n\n if (arg && !arg.startsWith('-') && !subCommand) {\n if (options.prompt === undefined && options.mode === 'interactive') {\n options.mode = 'print'\n }\n promptParts.push(arg)\n }\n cursor.advance()\n }\n\n if (promptParts.length > 0) {\n options.prompt = promptParts.join(' ')\n }\n\n return { options, subCommand, subCommandArgs, unresolvedCommandName, unresolvedCommandArgs }\n}\n\n/** 帮助文本行(供 plain-text printHelp 与 TUI InfoDialog 共用)。 */\nexport function getHelpLines(): string[] {\n return [\n '',\n 'otto - AI 助理命令行工具',\n '',\n '用法:',\n ' otto [提示词] 使用提示词启动(print 模式)',\n ' otto 进入交互式 TUI 模式',\n ' otto --json \"query\" JSON 输出模式',\n '',\n '选项:',\n ' -i, --interactive Interactive 模式(默认)',\n ' -p, --print Print 模式(非交互)',\n ' --json JSON 输出模式',\n ' -m, --model <id> 指定模型',\n ' -c, --config <path> 配置文件路径',\n ' -d, --project <dir> 项目目录',\n ' --verbose 输出详细日志',\n ' -q, --quiet 只输出 error 日志',\n ' --shadow 影子模式:otto 自有状态(凭据/会话/记忆/教训/配置/trace/日志)本次会话零磁盘写入(不保证 provider 服务端日志与 shell/OS 层痕迹;RFC-345)',\n ' --max-tool-turns <n> 单个用户回合最大工具回合数(默认 120)',\n ' --max-tool-turn-extensions <n> 触顶后压缩续跑的扩展次数(默认主会话 3 / 子 agent 0)',\n ' --prompt-token-budget <n> 单回合输出 token 预算(配置后轮次上限 ×4 退位安全阀;RFC-104)',\n ' --prompt-time-budget <min> 单回合墙钟时间预算(分钟)',\n ' --session-cost-budget <usd> 会话累计成本软预算(美元,超过一次性提醒不阻断;RFC-204)',\n ' --no-stall-detection 关闭早期停滞检测(默认开启;RFC-203)',\n ' --stall-window-turns <n> 停滞检测滑动窗口大小(需与 --stall-repeat-threshold 同时提供,默认 8)',\n ' --stall-repeat-threshold <n> 停滞检测同签名重复阈值(需与 --stall-window-turns 同时提供,默认 4)',\n ' --continue <id> 继续已有会话',\n ' --session-url <url> 远端会话持久化地址(如 http://host:3001/sessions)',\n ' --session-token <token> 远端会话持久化 Bearer token',\n ' --inspect[=port] 启用 Node.js inspector(默认: 9229)',\n ' -h, --help 显示帮助',\n ' --version, -v 显示版本',\n '',\n '命令:',\n ' otto run <prompt> 执行一次性任务',\n ' otto serve [--port N] [--inspect] [--static <dir>] [--token <t>] 启动 HTTP/WS service(--static 同源托管前端 SPA,--token 启用鉴权)',\n ' otto doctor [--summary|--json] 环境健康检查(环境/配置/存储状态/连通性)',\n ' otto auth login <provider> 登录(cursor / anthropic / openai-codex / github-copilot)',\n ' otto auth status 查看各 provider 凭据来源',\n ' otto auth logout <provider> 注销并清除凭据',\n ' otto config export [--project] [-o <path>] 导出配置(RFC-219)',\n ' otto config import <file> [--dry-run] [--scope global|project] [--no-plugins] [--plugins-only] [--yes] 导入配置',\n ' otto config restore [--list] 撤销最近一次导入',\n ' otto migrate export [--no-sessions] [-o <path>] 导出配置+插件清单+会话数据为迁移包(换电脑用)',\n ' otto migrate import <archive> [--no-sessions] 导入迁移包(目标已有会话数据时跳过会话部分)',\n ' otto observe <action> <id> 观测面板(summary / timeline / spans)',\n ' otto observe debug <action> [...] 调试面板(breakpoints / points / state)',\n ' otto observe replay <action> <id> 时间旅行(checkpoints / scrubber / restore / fork / replay)',\n ' otto debug <action> [...] (alias: otto observe debug)',\n ' otto time-travel <action> <id> (alias: otto observe replay)',\n ' otto mcp login <name> MCP server OAuth 登录',\n ' otto mcp logout <name> 登出 MCP server',\n ' otto mcp list 列出已认证的 MCP server',\n ' otto extension create <name> 生成插件骨架(命令 + 面板 + manifest,开箱即用)',\n ' otto extension install <name> [--yes] 安装首方/自定义插件(anthropic-provider / deepseek-provider / browser-use / …)',\n ' otto extension update [name] 检查并重装已过期的已安装 skill/plugin',\n ' otto persistence local [...flags] 启动独立持久化服务(storage + events + sync)',\n ' otto persistence remote [...flags] 启动最小化远程持久化服务(5 REST 端点)',\n ' otto persistenced [...flags] (alias: otto persistence local)',\n ' otto remote-persistence-server [...flags] (alias: otto persistence remote)',\n ' otto ps [--sweep] [--include-weak] [--json] 进程群列表与孤儿清扫(SIGTERM only,RFC-196)',\n '',\n '面板命令默认连 `otto serve` 实例(http://127.0.0.1:8417,OTTO_SERVICE_URL/--remote 可覆盖)。',\n ]\n}\n\nfunction printVersion(): void {\n process.stdout.write(`otto ${getVersion()}\\n`)\n process.exit(0)\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import{t as e}from"./daemon-base-YOWgYYN7.js";import{PERSISTENCED_DEFAULT_PORT as t,startPersistenced as n}from"@x-otto/service";import{REMOTE_PERSISTENCE_DEFAULT_PORT as r}from"@x-otto/remote-persistence-server";const i=t,a=`127.0.0.1`;async function o(t,r={}){let o=r.stdout??(e=>process.stdout.write(e)),c=r.stderr??(e=>process.stderr.write(e));if(t[0]===`help`||t[0]===`--help`)return s(o),0;let l,u=i,d=a,f;for(let e=0;e<t.length;e++){let n=t[e];if(n===`--dir`||n===`-d`)l=t[++e];else if(n===`--port`||n===`-p`){let n=t[++e],r=Number(n);if(!Number.isInteger(r)||r<1||r>65535)return c(`otto persistenced: invalid --port value: ${n??`(missing)`}\n`),1;u=r}else n===`--host`||n===`-h`?d=t[++e]??a:(n===`--token`||n===`-t`)&&(f=t[++e])}if(!l)return c(`otto persistenced: --dir is required
|
|
3
|
+
`),1;if(!f)return c(`otto persistenced: --token is required
|
|
4
|
+
`),1;try{let{url:t}=await n({dir:l,port:u,host:d,token:f});return o(`persistenced listening on ${t}
|
|
5
|
+
storage: ${t}/api/storage/:namespace
|
|
6
|
+
events: ${t}/api/events/:namespace (SSE)
|
|
7
|
+
sync: ${t}/api/sync/:namespace
|
|
8
|
+
health: ${t}/health
|
|
9
|
+
stop: Ctrl-C
|
|
10
|
+
`),await e(r.signal),o(`persistenced shutting down
|
|
11
|
+
`),0}catch(e){return c(`otto persistenced: ${e instanceof Error?e.message:String(e)}\n`),1}}function s(e){e(`otto persistenced — 独立持久化存储服务(SQLite + HTTP API)
|
|
12
|
+
|
|
13
|
+
用法:
|
|
14
|
+
otto persistenced --dir <path> --token <token> [--port <n>] [--host <ip>]
|
|
15
|
+
|
|
16
|
+
选项:
|
|
17
|
+
--dir, -d <path> 数据目录(SQLite db 文件存放位置,必填)
|
|
18
|
+
--token, -t <token> Bearer token(所有 /api/* 请求必须携带,必填)
|
|
19
|
+
--port, -p <n> 监听端口(默认 ${i})
|
|
20
|
+
--host, -h <ip> 监听地址(默认 ${a},0.0.0.0 = 公网暴露)
|
|
21
|
+
|
|
22
|
+
示例:
|
|
23
|
+
otto persistenced --dir /var/lib/otto/storage --token "\$TOKEN"
|
|
24
|
+
otto persistenced --dir ./data --port 3002 --token dev-secret
|
|
25
|
+
|
|
26
|
+
端点:
|
|
27
|
+
GET /health 健康检查
|
|
28
|
+
GET /api/storage/:namespace 列出文档 ID / 分页列表
|
|
29
|
+
GET /api/storage/:namespace/:id 读取文档
|
|
30
|
+
PUT /api/storage/:namespace/:id 写入文档
|
|
31
|
+
DELETE /api/storage/:namespace/:id 删除文档
|
|
32
|
+
GET /api/events/:namespace SSE 变更推送
|
|
33
|
+
POST /api/sync/:namespace/push 批量写入(离线同步)
|
|
34
|
+
GET /api/sync/:namespace/pull 增量拉取
|
|
35
|
+
`)}const c=r,l=`127.0.0.1`;async function u(t,n={}){let r=n.stdout??(e=>process.stdout.write(e)),i=n.stderr??(e=>process.stderr.write(e));if(t[0]===`help`||t[0]===`--help`)return d(r),0;let a,o=c,s=l,u;for(let e=0;e<t.length;e++){let n=t[e];if(n===`--dir`||n===`-d`)a=t[++e];else if(n===`--port`||n===`-p`){let n=t[++e],r=Number(n);if(!Number.isInteger(r)||r<1||r>65535)return i(`otto remote-persistence-server: invalid --port value: ${n??`(missing)`}\n`),1;o=r}else n===`--host`||n===`-h`?s=t[++e]??l:(n===`--token`||n===`-t`)&&(u=t[++e])}if(!a)return i(`otto remote-persistence-server: --dir is required
|
|
36
|
+
`),1;if(u===void 0)return i(`otto remote-persistence-server: --token is required (use --token "" to disable auth for dev only)
|
|
37
|
+
`),1;let{startServer:f}=await import(`@x-otto/remote-persistence-server`);try{let{url:t,server:i,store:c}=await f({dataDir:a,port:o,authToken:u},{host:s});return r(`remote-persistence-server listening on ${t}
|
|
38
|
+
sessions: ${t}/sessions
|
|
39
|
+
health: ${t}/health
|
|
40
|
+
stop: Ctrl-C
|
|
41
|
+
`),await e(n.signal),r(`remote-persistence-server shutting down
|
|
42
|
+
`),await new Promise(e=>i.close(()=>e())),c.close(),0}catch(e){return i(`otto remote-persistence-server: ${e instanceof Error?e.message:String(e)}\n`),1}}function d(e){e(`otto remote-persistence-server — 最小化远程持久化服务(Hono + SQLite,5 REST 端点)
|
|
43
|
+
|
|
44
|
+
用法:
|
|
45
|
+
otto remote-persistence-server --dir <path> --token <token> [--port <n>] [--host <ip>]
|
|
46
|
+
|
|
47
|
+
选项:
|
|
48
|
+
--dir, -d <path> 数据目录(SQLite db 文件存放位置,必填)
|
|
49
|
+
--token, -t <token> Bearer token(所有 /sessions/* 请求必须携带,必填)
|
|
50
|
+
--port, -p <n> 监听端口(默认 ${c})
|
|
51
|
+
--host, -h <ip> 监听地址(默认 ${l},0.0.0.0 = 公网暴露)
|
|
52
|
+
|
|
53
|
+
示例:
|
|
54
|
+
otto remote-persistence-server --dir ./data --token dev-secret
|
|
55
|
+
otto remote-persistence-server --dir /var/lib/otto/storage --port 3002 --token "\$TOKEN"
|
|
56
|
+
|
|
57
|
+
端点(:wsKey 为客户端工作区分区段,本服务 accept-and-ignore,按 token 派生 userId 分区):
|
|
58
|
+
GET /health 健康检查(无需认证)
|
|
59
|
+
GET /sessions/:wsKey 列出会话 ID(需要 Bearer token)
|
|
60
|
+
GET /sessions/:wsKey/:id 读取会话快照
|
|
61
|
+
PUT /sessions/:wsKey/:id 写入/更新会话快照
|
|
62
|
+
DELETE /sessions/:wsKey/:id 删除会话快照
|
|
63
|
+
|
|
64
|
+
对端客户端:
|
|
65
|
+
另一台机器上的 Otto CLI:
|
|
66
|
+
otto --session-url http://<host>:<port>/sessions --session-token <token>
|
|
67
|
+
`)}async function f(e,t={}){let n=t.stdout??(e=>process.stdout.write(e)),[r,...i]=e;switch(r){case`local`:return o(i,t);case`remote`:return u(i,t);default:return n(`otto persistence — 持久化服务族(local / remote)
|
|
68
|
+
|
|
69
|
+
用法:
|
|
70
|
+
otto persistence local [...flags] 启动独立持久化服务(storage + events + sync)
|
|
71
|
+
otto persistence remote [...flags] 启动最小化远程持久化服务(5 REST 端点)
|
|
72
|
+
|
|
73
|
+
子命令说明:
|
|
74
|
+
local 等价于旧 otto persistenced(SQLite + HTTP API,storage/events/sync)
|
|
75
|
+
remote 等价于旧 otto remote-persistence-server(Hono + SQLite,5 REST 端点)
|
|
76
|
+
|
|
77
|
+
通用选项(详见各子命令 --help):
|
|
78
|
+
--dir, -d <path> 数据目录(必填)
|
|
79
|
+
--token, -t <token> Bearer token(必填)
|
|
80
|
+
--port, -p <n> 监听端口(各子命令有各自默认端口)
|
|
81
|
+
--host, -h <ip> 监听地址(默认 127.0.0.1,0.0.0.0 = 公网暴露)
|
|
82
|
+
|
|
83
|
+
注:旧命令 otto persistenced / otto remote-persistence-server 仍可用(别名转发)。
|
|
84
|
+
`),0}}export{f as runPersistenceCommand};
|
|
85
|
+
//# sourceMappingURL=persistence-CTXa-OyC.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"persistence-CTXa-OyC.js","names":["DEFAULT_PORT","DEFAULT_HOST","printHelp"],"sources":["../src/commands/persistenced.ts","../src/commands/remote-persistence-server.ts","../src/commands/persistence.ts"],"sourcesContent":["import { startPersistenced, PERSISTENCED_DEFAULT_PORT } from '@x-otto/service'\n\nimport { waitForShutdown } from './daemon-base'\n\nexport interface PersistencedDeps {\n stdout?: (s: string) => void\n stderr?: (s: string) => void\n signal?: AbortSignal\n}\n\n/** 复用 @x-otto/service 的族内单源默认端口,消除双源漂移。 */\nconst DEFAULT_PORT = PERSISTENCED_DEFAULT_PORT\nconst DEFAULT_HOST = '127.0.0.1'\n\nexport async function runPersistencedCommand(\n args: string[],\n deps: PersistencedDeps = {},\n): Promise<number> {\n const stdout = deps.stdout ?? ((s) => process.stdout.write(s))\n const stderr = deps.stderr ?? ((s) => process.stderr.write(s))\n\n if (args[0] === 'help' || args[0] === '--help') {\n printHelp(stdout)\n return 0\n }\n\n let dir: string | undefined\n let port = DEFAULT_PORT\n let host = DEFAULT_HOST\n let token: string | undefined\n\n for (let i = 0; i < args.length; i++) {\n const arg = args[i]\n if (arg === '--dir' || arg === '-d') {\n dir = args[++i]\n } else if (arg === '--port' || arg === '-p') {\n const raw = args[++i]\n const n = Number(raw)\n if (!Number.isInteger(n) || n < 1 || n > 65535) {\n stderr(`otto persistenced: invalid --port value: ${raw ?? '(missing)'}\\n`)\n return 1\n }\n port = n\n } else if (arg === '--host' || arg === '-h') {\n host = args[++i] ?? DEFAULT_HOST\n } else if (arg === '--token' || arg === '-t') {\n token = args[++i]\n }\n }\n\n if (!dir) {\n stderr('otto persistenced: --dir is required\\n')\n return 1\n }\n\n if (!token) {\n stderr('otto persistenced: --token is required\\n')\n return 1\n }\n\n try {\n const { url } = await startPersistenced({ dir, port, host, token })\n\n stdout(`persistenced listening on ${url}\n storage: ${url}/api/storage/:namespace\n events: ${url}/api/events/:namespace (SSE)\n sync: ${url}/api/sync/:namespace\n health: ${url}/health\n stop: Ctrl-C\n`)\n\n await waitForShutdown(deps.signal)\n stdout('persistenced shutting down\\n')\n return 0\n } catch (error) {\n stderr(\n `otto persistenced: ${error instanceof Error ? error.message : String(error)}\\n`,\n )\n return 1\n }\n}\n\nfunction printHelp(stdout: (s: string) => void): void {\n stdout(`otto persistenced — 独立持久化存储服务(SQLite + HTTP API)\n\n用法:\n otto persistenced --dir <path> --token <token> [--port <n>] [--host <ip>]\n\n选项:\n --dir, -d <path> 数据目录(SQLite db 文件存放位置,必填)\n --token, -t <token> Bearer token(所有 /api/* 请求必须携带,必填)\n --port, -p <n> 监听端口(默认 ${DEFAULT_PORT})\n --host, -h <ip> 监听地址(默认 ${DEFAULT_HOST},0.0.0.0 = 公网暴露)\n\n示例:\n otto persistenced --dir /var/lib/otto/storage --token \"\\$TOKEN\"\n otto persistenced --dir ./data --port 3002 --token dev-secret\n\n端点:\n GET /health 健康检查\n GET /api/storage/:namespace 列出文档 ID / 分页列表\n GET /api/storage/:namespace/:id 读取文档\n PUT /api/storage/:namespace/:id 写入文档\n DELETE /api/storage/:namespace/:id 删除文档\n GET /api/events/:namespace SSE 变更推送\n POST /api/sync/:namespace/push 批量写入(离线同步)\n GET /api/sync/:namespace/pull 增量拉取\n`)\n}\n\n","import { REMOTE_PERSISTENCE_DEFAULT_PORT, type ServerConfig } from '@x-otto/remote-persistence-server'\n\nimport { waitForShutdown } from './daemon-base'\n\nexport interface RemotePersistenceServerDeps {\n stdout?: (s: string) => void\n stderr?: (s: string) => void\n signal?: AbortSignal\n}\n\n/** 复用 @x-otto/remote-persistence-server 的族内单源默认端口,消除双源漂移。 */\nconst DEFAULT_PORT = REMOTE_PERSISTENCE_DEFAULT_PORT\nconst DEFAULT_HOST = '127.0.0.1'\n\nexport async function runRemotePersistenceServerCommand(\n args: string[],\n deps: RemotePersistenceServerDeps = {},\n): Promise<number> {\n const stdout = deps.stdout ?? ((s) => process.stdout.write(s))\n const stderr = deps.stderr ?? ((s) => process.stderr.write(s))\n\n if (args[0] === 'help' || args[0] === '--help') {\n printHelp(stdout)\n return 0\n }\n\n let dir: string | undefined\n let port = DEFAULT_PORT\n let host = DEFAULT_HOST\n let token: string | undefined\n\n for (let i = 0; i < args.length; i++) {\n const arg = args[i]\n if (arg === '--dir' || arg === '-d') {\n dir = args[++i]\n } else if (arg === '--port' || arg === '-p') {\n const raw = args[++i]\n const n = Number(raw)\n if (!Number.isInteger(n) || n < 1 || n > 65535) {\n stderr(\n `otto remote-persistence-server: invalid --port value: ${raw ?? '(missing)'}\\n`,\n )\n return 1\n }\n port = n\n } else if (arg === '--host' || arg === '-h') {\n host = args[++i] ?? DEFAULT_HOST\n } else if (arg === '--token' || arg === '-t') {\n token = args[++i]\n }\n }\n\n if (!dir) {\n stderr('otto remote-persistence-server: --dir is required\\n')\n return 1\n }\n\n if (token === undefined) {\n stderr(\n 'otto remote-persistence-server: --token is required (use --token \"\" to disable auth for dev only)\\n',\n )\n return 1\n }\n\n const { startServer } = await import('@x-otto/remote-persistence-server')\n\n try {\n const config: ServerConfig = { dataDir: dir, port, authToken: token }\n const { url, server, store } = await startServer(config, { host })\n\n stdout(`remote-persistence-server listening on ${url}\n sessions: ${url}/sessions\n health: ${url}/health\n stop: Ctrl-C\n`)\n\n await waitForShutdown(deps.signal)\n stdout('remote-persistence-server shutting down\\n')\n await new Promise<void>((resolve) => server.close(() => resolve()))\n store.close()\n return 0\n } catch (error) {\n stderr(\n `otto remote-persistence-server: ${error instanceof Error ? error.message : String(error)}\\n`,\n )\n return 1\n }\n}\n\nfunction printHelp(stdout: (s: string) => void): void {\n stdout(`otto remote-persistence-server — 最小化远程持久化服务(Hono + SQLite,5 REST 端点)\n\n用法:\n otto remote-persistence-server --dir <path> --token <token> [--port <n>] [--host <ip>]\n\n选项:\n --dir, -d <path> 数据目录(SQLite db 文件存放位置,必填)\n --token, -t <token> Bearer token(所有 /sessions/* 请求必须携带,必填)\n --port, -p <n> 监听端口(默认 ${DEFAULT_PORT})\n --host, -h <ip> 监听地址(默认 ${DEFAULT_HOST},0.0.0.0 = 公网暴露)\n\n示例:\n otto remote-persistence-server --dir ./data --token dev-secret\n otto remote-persistence-server --dir /var/lib/otto/storage --port 3002 --token \"\\$TOKEN\"\n\n端点(:wsKey 为客户端工作区分区段,本服务 accept-and-ignore,按 token 派生 userId 分区):\n GET /health 健康检查(无需认证)\n GET /sessions/:wsKey 列出会话 ID(需要 Bearer token)\n GET /sessions/:wsKey/:id 读取会话快照\n PUT /sessions/:wsKey/:id 写入/更新会话快照\n DELETE /sessions/:wsKey/:id 删除会话快照\n\n对端客户端:\n 另一台机器上的 Otto CLI:\n otto --session-url http://<host>:<port>/sessions --session-token <token>\n`)\n}\n","/**\n * `otto persistence` 子命令 —— 持久化服务族(local / remote)的薄路由。\n *\n * RFC-367 M4:persistence 收编 persistenced(local)与 remote-persistence-server(remote)\n * 为子命令。旧入口 `otto persistenced` / `otto remote-persistence-server` 作为别名保留,\n * 内部分别转发到本命令的 `local` / `remote` 子路径。\n */\nimport { runPersistencedCommand, type PersistencedDeps } from './persistenced'\nimport {\n runRemotePersistenceServerCommand,\n type RemotePersistenceServerDeps,\n} from './remote-persistence-server'\n\n/**\n * persistence 路由的 deps —— 是 PersistencedDeps 与 RemotePersistenceServerDeps 的并集\n * (两者结构相同:stdout? / stderr? / signal?,并集保持兼容)。\n */\nexport type PersistenceDeps = PersistencedDeps & RemotePersistenceServerDeps\n\nexport async function runPersistenceCommand(\n args: string[],\n deps: PersistenceDeps = {},\n): Promise<number> {\n const stdout = deps.stdout ?? ((s) => process.stdout.write(s))\n\n const [subCommand, ...rest] = args\n\n switch (subCommand) {\n case 'local':\n return runPersistencedCommand(rest, deps)\n case 'remote':\n return runRemotePersistenceServerCommand(rest, deps)\n default:\n stdout(`otto persistence — 持久化服务族(local / remote)\n\n用法:\n otto persistence local [...flags] 启动独立持久化服务(storage + events + sync)\n otto persistence remote [...flags] 启动最小化远程持久化服务(5 REST 端点)\n\n子命令说明:\n local 等价于旧 otto persistenced(SQLite + HTTP API,storage/events/sync)\n remote 等价于旧 otto remote-persistence-server(Hono + SQLite,5 REST 端点)\n\n通用选项(详见各子命令 --help):\n --dir, -d <path> 数据目录(必填)\n --token, -t <token> Bearer token(必填)\n --port, -p <n> 监听端口(各子命令有各自默认端口)\n --host, -h <ip> 监听地址(默认 127.0.0.1,0.0.0.0 = 公网暴露)\n\n注:旧命令 otto persistenced / otto remote-persistence-server 仍可用(别名转发)。\n`)\n return 0\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
function e(e){let t=new Set(e.disabled??[]),n=new Set(e.active.map(e=>e.id)),r=e.discovered.filter(e=>!n.has(e.id)&&!t.has(e.id));return{missingFromRuntime:r,missingBundled:r.filter(e=>e.scope===`bundled`),hasGap:r.length>0}}function t(e){if(!e.hasGap)return[];let t=[];e.missingBundled.length>0&&t.push(`内置扩展未装载:${e.missingBundled.map(e=>e.id).join(`, `)}`,` → 宿主装配参数未传达(RFC-364)。若为嵌入式调用,检查 createApp 的 pluginBundledDirs。`);let n=e.missingFromRuntime.filter(e=>e.scope!==`bundled`);return n.length>0&&t.push(`插件已发现但未激活:${n.map(e=>e.id).join(`, `)}`,` → 可能是依赖缺失/版本不兼容/未信任。查看启动日志的 fail-closed 告警定位具体原因。`),t}export{e as checkPluginAssembly,t as formatAssemblyGap};
|
|
3
|
-
//# sourceMappingURL=plugin-assembly-check-
|
|
3
|
+
//# sourceMappingURL=plugin-assembly-check-C2TephMb.js.map
|
package/dist/{plugin-assembly-check-CvuJ4dHf.js.map → plugin-assembly-check-C2TephMb.js.map}
RENAMED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin-assembly-check-
|
|
1
|
+
{"version":3,"file":"plugin-assembly-check-C2TephMb.js","names":[],"sources":["../src/plugin-assembly-check.ts"],"sourcesContent":["/**\n * plugin-assembly-check.ts —— RFC-364 M2:装配一致性自检。\n *\n * ## 要解决的问题\n * M1 收敛了装配点,但那只修好了**当前**六处入口。真正的风险是**下一次**——任何新的\n * 装配路径若漏配宿主自描述参数,表现仍然是\"插件静默消失\",用户侧症状是\n * 「Setup Wizard 走完了但模型列表空白」,与真实根因(宿主没告诉 App 自己带了什么)\n * 相距甚远,极难诊断。\n *\n * RFC-364 §4 规则 2:装配缺失必须 fail-loud,不得只写 warn 日志了事。\n *\n * ## 判据\n * 比对两个视图:\n * - **应该可见**:`discoverPluginsWithBundled()`(cli 侧完整发现,含 bundled 层)\n * - **实际装载**:`app.activePlugins()`(App 运行时真正激活的集合)\n *\n * 二者的差集分两类,语义不同、处置不同:\n * - `missingFromRuntime`:发现得到却没装载 → **装配缺口或依赖 fail-closed**,需报告;\n * - 反向差集不检查:App 侧可能因 disabled 配置少装,那是用户意图,不是缺陷。\n *\n * ## 为什么放 cli 而不是 coding\n * 判据依赖 `discoverPluginsWithBundled()`(知道 bundled 目录 = cli 独有知识,见\n * `create-host-app.ts` 头注释论证)。放 coding 会重蹈\"下游包推导宿主布局\"的覆辙。\n *\n * 纯函数 + 端口注入:便于穷举单测,不触真实 App。\n */\n\n/** 一个插件的最小身份(两个视图的公共子集)。 */\nexport interface AssemblyPluginRef {\n id: string\n scope?: string\n}\n\nexport interface AssemblyCheckInput {\n /** cli 侧完整发现的插件(含 bundled 层)。 */\n discovered: readonly AssemblyPluginRef[]\n /** App 运行时实际激活的插件。 */\n active: readonly AssemblyPluginRef[]\n /** 用户显式禁用的插件 id(`disabled_plugins` 配置)——不计入缺口。 */\n disabled?: readonly string[]\n}\n\nexport interface AssemblyCheckResult {\n /** 发现得到但未装载(排除用户显式禁用者)。非空 = 装配不完整。 */\n missingFromRuntime: AssemblyPluginRef[]\n /** 其中属于 bundled 作用域的——这类最可能是\"漏传宿主自描述参数\"的信号。 */\n missingBundled: AssemblyPluginRef[]\n /** 是否存在缺口(供调用方决定是否 fail-loud)。 */\n hasGap: boolean\n}\n\n/**\n * 比对「应该可见」与「实际装载」,给出装配缺口。\n *\n * 用户显式 `disabled` 的插件被排除——那是用户意图,不是装配缺陷。这条很重要:\n * 否则任何禁用过插件的用户都会看到假告警,告警一旦有噪音就会被忽略,\n * 这道门也就失去意义了。\n */\nexport function checkPluginAssembly(input: AssemblyCheckInput): AssemblyCheckResult {\n const disabled = new Set(input.disabled ?? [])\n const activeIds = new Set(input.active.map((p) => p.id))\n\n const missingFromRuntime = input.discovered.filter(\n (p) => !activeIds.has(p.id) && !disabled.has(p.id),\n )\n const missingBundled = missingFromRuntime.filter((p) => p.scope === 'bundled')\n\n return {\n missingFromRuntime,\n missingBundled,\n hasGap: missingFromRuntime.length > 0,\n }\n}\n\n/**\n * 把缺口渲染成面向用户的可执行诊断文本(`otto doctor` 消费)。\n *\n * 措辞指向**根因方向**而非症状:bundled 缺失几乎必然是装配漏配(宿主自描述参数没传到),\n * 其余缺失更可能是依赖 fail-closed(missingDeps/版本门/信任门)。两者给不同的下一步,\n * 避免用户拿着\"插件不见了\"去猜。\n */\nexport function formatAssemblyGap(result: AssemblyCheckResult): string[] {\n if (!result.hasGap) return []\n\n const lines: string[] = []\n if (result.missingBundled.length > 0) {\n lines.push(\n `内置扩展未装载:${result.missingBundled.map((p) => p.id).join(', ')}`,\n ' → 宿主装配参数未传达(RFC-364)。若为嵌入式调用,检查 createApp 的 pluginBundledDirs。',\n )\n }\n const others = result.missingFromRuntime.filter((p) => p.scope !== 'bundled')\n if (others.length > 0) {\n lines.push(\n `插件已发现但未激活:${others.map((p) => p.id).join(', ')}`,\n ' → 可能是依赖缺失/版本不兼容/未信任。查看启动日志的 fail-closed 告警定位具体原因。',\n )\n }\n return lines\n}\n"],"mappings":";AA0DA,SAAgB,EAAoB,EAAgD,CAClF,IAAM,EAAW,IAAI,IAAI,EAAM,UAAY,EAAE,CAAC,CACxC,EAAY,IAAI,IAAI,EAAM,OAAO,IAAK,GAAM,EAAE,GAAG,CAAC,CAElD,EAAqB,EAAM,WAAW,OACzC,GAAM,CAAC,EAAU,IAAI,EAAE,GAAG,EAAI,CAAC,EAAS,IAAI,EAAE,GAAG,CACnD,CAGD,MAAO,CACL,qBACA,eAJqB,EAAmB,OAAQ,GAAM,EAAE,QAAU,UAAU,CAK5E,OAAQ,EAAmB,OAAS,EACrC,CAUH,SAAgB,EAAkB,EAAuC,CACvE,GAAI,CAAC,EAAO,OAAQ,MAAO,EAAE,CAE7B,IAAM,EAAkB,EAAE,CACtB,EAAO,eAAe,OAAS,GACjC,EAAM,KACJ,WAAW,EAAO,eAAe,IAAK,GAAM,EAAE,GAAG,CAAC,KAAK,KAAK,GAC5D,mEACD,CAEH,IAAM,EAAS,EAAO,mBAAmB,OAAQ,GAAM,EAAE,QAAU,UAAU,CAO7E,OANI,EAAO,OAAS,GAClB,EAAM,KACJ,aAAa,EAAO,IAAK,GAAM,EAAE,GAAG,CAAC,KAAK,KAAK,GAC/C,sDACD,CAEI"}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import{getDefaultProxy as e,probeProxy as t}from"@x-otto/shared";import{i18next as n}from"@x-otto/tui";const r=/^https?:\/\/.+/;function i(e){return e.app.settings.get(`proxy`)}let a;function o(e,t){a={value:e,source:t}}let s;function c(t){let n=i(t);t.tui.stores.proxy.setView({url:n?.url,enabled:n?.enabled,configPath:`~/.otto/config.json`,effectiveUrl:e(),detectedEnvProxy:a,probe:s})}function l(e){let t=e.state.currentModelId;if(t)try{return e.app.modelRegistry.resolve(t).baseUrl||void 0}catch{return}}async function u(e){let r=l(e);if(!r){s={status:`failed`,reason:n.t(`proxy.probeNoModel`),blamesProxy:!1,target:`-`},c(e);return}s={status:`probing`},c(e);let i=await t(r),a=d(r);s=i.ok?{status:`ok`,latencyMs:i.latencyMs,target:a}:{status:`failed`,reason:i.errorMessage??i.errorKind??`unknown`,blamesProxy:i.errorKind===`proxy-unreachable`,target:a},c(e)}function d(e){try{return new URL(e).host}catch{return e}}async function f(e,t){let a=e[0],o=t.app.settings;if(!a){c(t),t.tui.panes.togglePane(`proxy`);return}if(a===`status`){let e=i(t);if(e?.url){let n=e.enabled===!1?`已停用(配置保留)`:`已启用`;t.tui.messages.pushMessage({role:`command_echo`,command:`/proxy status`,summary:`代理状态:${n}`,detail:`代理地址:${e.url}\n作用范围:全局(所有出站 HTTP 请求,本机地址除外)`})}else t.tui.messages.pushMessage({role:`command_echo`,command:`/proxy status`,status:`warn`,summary:`代理未配置。使用 /proxy <url> 设置代理。`});return}if(a===`on`){let e=i(t);if(!e?.url){t.tui.info.showToast(`无代理地址`,`请先使用 /proxy <url> 设置代理地址`);return}await o.persist({proxy:{url:e.url,enabled:!0}}),c(t),t.tui.info.showToast(`代理已启用`,e.url);return}if(a===`off`){let e=i(t);if(!e?.url){t.tui.info.showToast(`无代理配置`,`尚未设置代理`);return}await o.persist({proxy:{url:e.url,enabled:!1}}),c(t),t.tui.info.showToast(`代理已停用`,`配置保留(${e.url})`);return}if(a===`test`){await u(t);let e=s;e?.status===`ok`?t.tui.info.showToast(n.t(`proxy.actionTest`),n.t(`proxy.probeOk`,{latency:e.latencyMs,target:e.target})):e?.status===`failed`&&t.tui.info.showToast(n.t(`proxy.actionTest`),n.t(e.blamesProxy?`proxy.probeFailedProxy`:`proxy.probeFailedTarget`,{reason:e.reason}));return}if(a===`clear`){await o.persist({proxy:{}}),c(t),t.tui.info.showToast(`代理已清除`,``);return}if(r.test(a)){await o.persist({proxy:{url:a,enabled:!0}}),c(t),t.tui.info.showToast(`代理已设置`,a);return}if(a.includes(`://`)||a.includes(`.`)||a.includes(`:`)){t.tui.info.showToast(`代理地址格式无效`,`需以 http:// 或 https:// 开头,例如 http://127.0.0.1:7890(收到:${a})`);return}t.tui.info.showToast(`用法`,`/proxy [<url>|on|off|status|clear]`)}export{o as n,f as t};
|
|
3
|
-
//# sourceMappingURL=proxy-
|
|
3
|
+
//# sourceMappingURL=proxy-evg7ZvoD.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"proxy-
|
|
1
|
+
{"version":3,"file":"proxy-evg7ZvoD.js","names":[],"sources":["../src/commands/handlers/proxy.ts"],"sourcesContent":["/**\n * /proxy 命令:代理配置管理(增删改查 + 测通)\n */\nimport { getDefaultProxy, probeProxy } from '@x-otto/shared'\nimport { i18next } from '@x-otto/tui'\n\nimport type { ProxyConfig } from '@x-otto/setting'\nimport type { CliContext } from '../../command-dispatcher'\n\n/**\n * 视图形状取自 `TuiBridge.stores.proxy.setView` 的参数类型——`ProxyPanelView` 是 tui 的\n * 内部视图契约,未从其 barrel 导出,也不该为了 cli 一处消费就扩大公共 API。用\n * `Parameters<>` 反查即可拿到同一份类型,且 tui 侧改字段时这里会同步编译报错。\n */\ntype ProxyView = Parameters<CliContext['tui']['stores']['proxy']['setView']>[0]\n\nconst PROXY_URL_RE = /^https?:\\/\\/.+/\n\nfunction getProxy(ctx: CliContext): ProxyConfig | undefined {\n return ctx.app.settings.get('proxy') as ProxyConfig | undefined\n}\n\n/**\n * 启动时从环境变量检测到的系统代理(RFC-288 D5)。由 `interactive-setup.ts` 在检测后\n * 写入——此前那里检测到只弹一次 toast 就丢弃,面板完全不知情,用户环境里明明有可用地址\n * 却仍需手打。**仅作建议值**:必须用户在面板上显式确认才落盘(沿用前身 RFC 规则 5)。\n */\nlet detectedEnvProxy: ProxyView['detectedEnvProxy']\n\nexport function setDetectedEnvProxy(value: string, source: string): void {\n detectedEnvProxy = { value, source }\n}\n\n/** 最近一次探测结果(面板显示用)。仅进程内瞬态,不落盘。 */\nlet lastProbe: ProxyView['probe']\n\nfunction pushProxyView(ctx: CliContext): void {\n const proxy = getProxy(ctx)\n ctx.tui.stores.proxy.setView({\n url: proxy?.url,\n enabled: proxy?.enabled,\n configPath: '~/.otto/config.json',\n // 配置态(上面两行)之外再给出**进程内实际生效**的代理,面板据此判断二者是否一致\n // (RFC-288 D3)。不一致=改了配置但未重启,这正是用户答不出\"我设了为什么没用\"的时刻。\n effectiveUrl: getDefaultProxy(),\n detectedEnvProxy,\n probe: lastProbe,\n })\n}\n\n/**\n * 探测目标 = **当前模型的 baseURL**(RFC-288 D4),不是任何固定的第三方端点。\n * 用户配代理的唯一目的就是访问他正在用的那个模型端点,探测它才直接回答用户的真实问题;\n * 固定端点在墙内两头不讨好(境外端点自身可能被墙=假阴性,国内端点不走代理也通=假阳性)。\n */\nfunction resolveProbeTarget(ctx: CliContext): string | undefined {\n const modelId = ctx.state.currentModelId\n if (!modelId) return undefined\n try {\n const model = ctx.app.modelRegistry.resolve(modelId)\n return model.baseUrl || undefined\n } catch {\n return undefined\n }\n}\n\nasync function runProbe(ctx: CliContext): Promise<void> {\n const target = resolveProbeTarget(ctx)\n if (!target) {\n lastProbe = { status: 'failed', reason: i18next.t('proxy.probeNoModel'), blamesProxy: false, target: '-' }\n pushProxyView(ctx)\n return\n }\n\n lastProbe = { status: 'probing' }\n pushProxyView(ctx)\n\n const result = await probeProxy(target)\n const host = safeHost(target)\n lastProbe = result.ok\n ? { status: 'ok', latencyMs: result.latencyMs, target: host }\n : {\n status: 'failed',\n reason: result.errorMessage ?? result.errorKind ?? 'unknown',\n // 只有\"连不上代理\"才归咎代理;目标不可达/超时不能让用户去反复检查一个正常的代理。\n blamesProxy: result.errorKind === 'proxy-unreachable',\n target: host,\n }\n pushProxyView(ctx)\n}\n\n/** 取 host 用于展示(完整 URL 太长会撑破面板行宽);解析失败时原样返回。 */\nfunction safeHost(url: string): string {\n try {\n return new URL(url).host\n } catch {\n return url\n }\n}\n\nexport async function handleProxy(args: string[], ctx: CliContext): Promise<void> {\n const arg = args[0]\n const settings = ctx.app.settings\n\n // 无参数 → toggle 面板\n if (!arg) {\n pushProxyView(ctx)\n ctx.tui.panes.togglePane('proxy')\n return\n }\n\n // /proxy status\n if (arg === 'status') {\n const proxy = getProxy(ctx)\n if (proxy?.url) {\n const status = proxy.enabled !== false ? '已启用' : '已停用(配置保留)'\n ctx.tui.messages.pushMessage({\n role: 'command_echo',\n command: '/proxy status',\n summary: `代理状态:${status}`,\n detail: `代理地址:${proxy.url}\\n作用范围:全局(所有出站 HTTP 请求,本机地址除外)`,\n })\n } else {\n ctx.tui.messages.pushMessage({\n role: 'command_echo',\n command: '/proxy status',\n status: 'warn',\n summary: '代理未配置。使用 /proxy <url> 设置代理。',\n })\n }\n return\n }\n\n // /proxy on\n if (arg === 'on') {\n const proxy = getProxy(ctx)\n if (!proxy?.url) {\n ctx.tui.info.showToast('无代理地址', '请先使用 /proxy <url> 设置代理地址')\n return\n }\n await settings.persist({ proxy: { url: proxy.url, enabled: true } })\n pushProxyView(ctx)\n ctx.tui.info.showToast('代理已启用', proxy.url)\n return\n }\n\n // /proxy off\n if (arg === 'off') {\n const proxy = getProxy(ctx)\n if (!proxy?.url) {\n ctx.tui.info.showToast('无代理配置', '尚未设置代理')\n return\n }\n await settings.persist({ proxy: { url: proxy.url, enabled: false } })\n pushProxyView(ctx)\n ctx.tui.info.showToast('代理已停用', `配置保留(${proxy.url})`)\n return\n }\n\n // /proxy test —— 连通性探测(面板\"测试连接\"动作行走的也是这条)\n if (arg === 'test') {\n await runProbe(ctx)\n const p = lastProbe\n if (p?.status === 'ok') {\n ctx.tui.info.showToast(i18next.t('proxy.actionTest'), i18next.t('proxy.probeOk', { latency: p.latencyMs, target: p.target }))\n } else if (p?.status === 'failed') {\n ctx.tui.info.showToast(\n i18next.t('proxy.actionTest'),\n i18next.t(p.blamesProxy ? 'proxy.probeFailedProxy' : 'proxy.probeFailedTarget', { reason: p.reason }),\n )\n }\n return\n }\n\n // /proxy clear\n if (arg === 'clear') {\n await settings.persist({ proxy: {} })\n pushProxyView(ctx)\n ctx.tui.info.showToast('代理已清除', '')\n return\n }\n\n // /proxy <url>\n if (PROXY_URL_RE.test(arg)) {\n await settings.persist({ proxy: { url: arg, enabled: true } })\n pushProxyView(ctx)\n ctx.tui.info.showToast('代理已设置', arg)\n return\n }\n\n // 不认识的参数:区分「看起来想设地址但格式不对」与「敲错了子命令」——前者给出具体的\n // 格式要求(面板 InputDialog 提交也走这条路径,是用户输错地址时唯一的反馈点),\n // 后者才回退到用法列表。此前两种情况都只弹泛泛的「用法」,用户不知道自己错在哪。\n if (arg.includes('://') || arg.includes('.') || arg.includes(':')) {\n ctx.tui.info.showToast('代理地址格式无效', `需以 http:// 或 https:// 开头,例如 http://127.0.0.1:7890(收到:${arg})`)\n return\n }\n ctx.tui.info.showToast('用法', '/proxy [<url>|on|off|status|clear]')\n}\n"],"mappings":"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"}
|
|
@@ -6,4 +6,4 @@ import{OTTO_SESSION_LOCKS_DIR as e,isContainerEnvironment as t,isProcessAlive as
|
|
|
6
6
|
`)}if(t.stdout(`\n${s.length} processes, total RSS ${l(p)}; ${m.length} strong orphan(s), ${h.length} weak orphan(s)\n`),!r)return(m.length>0||h.length>0)&&t.stdout("run `otto ps --sweep` to SIGTERM strong orphans"+(h.length>0?` (--include-weak for weak ones)`:``)+`
|
|
7
7
|
`),0;let _=[...m,...i?h:[]].filter(e=>e.pid!==f&&e.tty===`??`);if(_.length===0)return t.stdout(`nothing to sweep
|
|
8
8
|
`),0;let v=0,y=[];for(let e of _)try{o(e.pid),v++,y.push(e),t.stdout(`SIGTERM ${e.pid} (${d(e)}, ${l(e.rssKb)})\n`)}catch(n){t.stderr(`failed to signal ${e.pid}: ${n instanceof Error?n.message:String(n)}\n`)}let b=t.probeAlive??n;await(t.sleep??(e=>new Promise(t=>setTimeout(t,e))))(t.verifyDelayMs??3e3);let x=y.filter(e=>b(e.pid)),S=v-x.length;return t.stdout(`swept ${S}/${_.length}\n`),x.length>0?(t.stdout(`${x.length} process(es) ignored SIGTERM (likely chaperone signal guard): `+x.map(e=>e.pid).join(`, `)+`\nescalate manually if needed: kill -9 ${x.map(e=>e.pid).join(` `)}\n`),1):v===_.length?0:1}export{f as runPsCommand};
|
|
9
|
-
//# sourceMappingURL=ps-
|
|
9
|
+
//# sourceMappingURL=ps-DKrluEuy.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ps-Kb6ZsEpC.js","names":[],"sources":["../src/commands/fleet-scan.ts","../src/commands/ps.ts"],"sourcesContent":["/**\n * 进程舰队全量扫描工具(ps/kill/signal + 按会话分代)。纯模块——不持状态、不新建 child。\n */\n\nimport { OTTO_SESSION_LOCKS_DIR, isContainerEnvironment, listOttoProcesses } from '@x-otto/env'\nimport { createSessionWriteLeaseManager } from '@x-otto/session'\n\nimport type { OttoProcessInfo } from '@x-otto/env'\n\nexport type OrphanVerdict = 'strong' | 'weak' | 'none'\n\nexport interface FleetProcess extends OttoProcessInfo {\n readonly verdict: OrphanVerdict\n /** 判定理由(展示用,如 `lease-stale`/`lease-other-owner`/`lease-self-heartbeat`)。 */\n readonly reason: string\n}\n\n/** 弱孤儿的最小运行时长(无租约可核时的辅助信号)。 */\nconst WEAK_ORPHAN_MIN_ETIME_MS = 60 * 60 * 1000\n\n/**\n * serve 类常驻子命令——合法 detach 形态,永不判孤儿。\n *\n * 关于 `daemon`(RFC-335 M2):`otto daemon start` 拉起的**后台进程命令行是 `serve`**\n * (见 `daemon.ts` 的 `spawnDetached`),因此它实际由上面的 `'serve'` 一项保护;\n * `daemon` 自身只是前台短命进程(发完 spawn 即退出),不会被扫描为常驻。\n * 这里仍列出 `daemon` 只为表达意图,**不是**该保护生效的原因——真正的防护是 `serve`。\n */\nconst RESIDENT_SUBCOMMANDS = ['serve', 'daemon', 'persistenced', 'remote-persistence-server', 'observe']\n\nexport interface FleetScanDeps {\n /** 进程枚举(测试注入)。默认 `listOttoProcesses()`。 */\n listProcesses?: () => OttoProcessInfo[]\n /** 租约检视(测试注入)。默认按 OTTO_HOME/sessions/locks 组装只读 lease manager。 */\n inspectLease?: (sessionId: string) => { locked: boolean; pid?: number; alive?: boolean; mtimeAgoMs?: number }\n /** 容器环境探测(测试注入)。默认 `isContainerEnvironment()`。 */\n isContainer?: () => boolean\n /** 租约 stale 阈值(对齐 session-write-lease DEFAULT_STALE_MS)。 */\n staleMs?: number\n}\n\nfunction isResidentCommand(command: string): boolean {\n // 子命令是 bin/otto.js 之后的第一个非 flag 参数\n const afterEntry = command.split('bin/otto.js')[1] ?? ''\n const firstArg = afterEntry.trim().split(/\\s+/)[0] ?? ''\n return RESIDENT_SUBCOMMANDS.includes(firstArg)\n}\n\n/** 扫描全机 otto 主进程并逐个判定孤儿三态。 */\nexport function scanFleet(deps: FleetScanDeps = {}): FleetProcess[] {\n const listProcesses = deps.listProcesses ?? listOttoProcesses\n const isContainer = deps.isContainer ?? isContainerEnvironment\n const staleMs = deps.staleMs ?? 60_000\n // review P2 修复:lease manager 提到判定循环外一次组装(此前每进程 new 一个,含\n // 重复 mkdirSync)。只读组装:installExitHook:false 无退出钩子副作用;inspect() 无\n // acquire/touch。lockDir 与 cli.ts 的会话存储组装同源 OTTO_HOME(P0 修复后一致)。\n const defaultManager = deps.inspectLease\n ? undefined\n : createSessionWriteLeaseManager({\n lockDir: OTTO_SESSION_LOCKS_DIR,\n installExitHook: false,\n })\n const inspectLease = deps.inspectLease ?? ((sessionId: string) => defaultManager!.inspect(sessionId))\n\n const container = isContainer()\n const processes = listProcesses()\n\n return processes.map((p): FleetProcess => {\n // 安全不变式:有 TTY 一律非孤儿。\n if (p.tty !== '??') return { ...p, verdict: 'none', reason: 'has-tty' }\n if (p.ppid !== 1) return { ...p, verdict: 'none', reason: 'has-parent' }\n if (isResidentCommand(p.command)) return { ...p, verdict: 'none', reason: 'resident-service' }\n // 容器环境:ppid==1 是常态,强判定失效(F6 豁免)。\n if (container) {\n return p.etimeMs > WEAK_ORPHAN_MIN_ETIME_MS\n ? { ...p, verdict: 'weak', reason: 'container-ppid1-longrun' }\n : { ...p, verdict: 'none', reason: 'container-ppid1' }\n }\n\n if (!p.sessionId) {\n // 无 --continue:无租约可核 → 弱孤儿(>1h 辅助信号)。\n return p.etimeMs > WEAK_ORPHAN_MIN_ETIME_MS\n ? { ...p, verdict: 'weak', reason: 'no-session-longrun' }\n : { ...p, verdict: 'none', reason: 'no-session-young' }\n }\n\n const lease = inspectLease(p.sessionId)\n if (!lease.locked) {\n // 锁文件不存在——会话早已正常释放,进程却还挂着 → 强孤儿。\n return { ...p, verdict: 'strong', reason: 'lease-released' }\n }\n if (lease.pid === p.pid) {\n // (c) 自持租约的活孤儿——心跳还在但不可交互(见文件头语义说明)。\n return { ...p, verdict: 'strong', reason: 'lease-self-heartbeat' }\n }\n if (lease.mtimeAgoMs !== undefined && lease.mtimeAgoMs > staleMs) {\n // (b) 租约 stale——持有者已死。\n return { ...p, verdict: 'strong', reason: 'lease-stale' }\n }\n // (a) 租约被其他进程持有且 fresh——本候选是废弃副本。\n return { ...p, verdict: 'strong', reason: 'lease-other-owner' }\n })\n}\n","/**\n * /ps 命令:进程舰队列表视图(按会话/TTY 分组、代际计数/内存)。\n */\nimport { isProcessAlive } from '@x-otto/env'\nimport { scanFleet } from './fleet-scan'\n\nimport type { FleetProcess } from './fleet-scan'\n\nexport interface PsCommandIo {\n stdout: (s: string) => void\n stderr: (s: string) => void\n /** SIGTERM 发送(测试注入)。默认 `process.kill(pid, 'SIGTERM')`。 */\n kill?: (pid: number) => void\n /** 存活探测(测试注入)。默认 `isProcessAlive`(@x-otto/env,kill(pid,0) 语义)。 */\n probeAlive?: (pid: number) => boolean\n /** sweep 后存活复查的等待毫秒(测试注入缩短)。默认 3000。 */\n verifyDelayMs?: number\n /** 延迟原语(测试注入 fake)。默认真实 setTimeout。 */\n sleep?: (ms: number) => Promise<void>\n}\n\nfunction formatRss(rssKb: number): string {\n return rssKb >= 1024 ? `${(rssKb / 1024).toFixed(0)}MB` : `${rssKb}KB`\n}\n\nfunction formatEtime(ms: number): string {\n const h = Math.floor(ms / 3_600_000)\n if (h >= 24) return `${Math.floor(h / 24)}d${h % 24}h`\n const m = Math.floor((ms % 3_600_000) / 60_000)\n return h > 0 ? `${h}h${m}m` : `${m}m`\n}\n\nfunction verdictLabel(p: FleetProcess): string {\n if (p.verdict === 'strong') return `ORPHAN(${p.reason})`\n if (p.verdict === 'weak') return `orphan?(${p.reason})`\n return '-'\n}\n\nexport async function runPsCommand(args: string[], io: PsCommandIo): Promise<number> {\n const sweep = args.includes('--sweep')\n const includeWeak = args.includes('--include-weak')\n const json = args.includes('--json')\n const kill = io.kill ?? ((pid: number) => process.kill(pid, 'SIGTERM'))\n\n const fleet = scanFleet()\n const self = process.pid\n\n if (json) {\n io.stdout(JSON.stringify(fleet, null, 2) + '\\n')\n return 0\n }\n\n if (fleet.length === 0) {\n io.stdout('no otto processes found\\n')\n return 0\n }\n\n // 列表输出\n const totalRssKb = fleet.reduce((acc, p) => acc + p.rssKb, 0)\n const strong = fleet.filter((p) => p.verdict === 'strong')\n const weak = fleet.filter((p) => p.verdict === 'weak')\n\n const header = ['PID', 'PPID', 'RSS', 'TTY', 'UP', 'GEN', 'SESSION', 'VERDICT'].join('\\t')\n io.stdout(header + '\\n')\n for (const p of fleet) {\n const selfMark = p.pid === self ? '*' : ''\n io.stdout(\n [\n `${p.pid}${selfMark}`,\n p.ppid,\n formatRss(p.rssKb),\n p.tty,\n formatEtime(p.etimeMs),\n p.generation ?? '-',\n p.sessionId ? p.sessionId.slice(0, 8) : '-',\n verdictLabel(p),\n ].join('\\t') + '\\n',\n )\n }\n io.stdout(\n `\\n${fleet.length} processes, total RSS ${formatRss(totalRssKb)}; ` +\n `${strong.length} strong orphan(s), ${weak.length} weak orphan(s)\\n`,\n )\n\n if (!sweep) {\n if (strong.length > 0 || weak.length > 0) {\n io.stdout('run `otto ps --sweep` to SIGTERM strong orphans' +\n (weak.length > 0 ? ' (--include-weak for weak ones)' : '') + '\\n')\n }\n return 0\n }\n\n // sweep:SIGTERM only;自进程/有 TTY 双保险排除(判定层已保证有 TTY 不入选,此处复核)。\n const targets = [...strong, ...(includeWeak ? weak : [])].filter(\n (p) => p.pid !== self && p.tty === '??',\n )\n if (targets.length === 0) {\n io.stdout('nothing to sweep\\n')\n return 0\n }\n let signalled = 0\n const signalledPids: FleetProcess[] = []\n for (const p of targets) {\n try {\n kill(p.pid)\n signalled++\n signalledPids.push(p)\n io.stdout(`SIGTERM ${p.pid} (${verdictLabel(p)}, ${formatRss(p.rssKb)})\\n`)\n } catch (err) {\n io.stderr(`failed to signal ${p.pid}: ${err instanceof Error ? err.message : String(err)}\\n`)\n }\n }\n\n // review P1-a 修复:SIGTERM 送达 ≠ 进程退出——chaperone 态孤儿装着空函数 signal guard\n // (cli.ts chaperoneSignalGuard,防前台组误杀的既有设计)会吞掉 SIGTERM,进程纹丝不动。\n // 送信号后延迟复查存活,把\"未退出\"名单如实报告并提示手动升级(不自动 SIGKILL——保持\n // RFC-196 \"SIGTERM only\" 不变式,kill -9 决策权留给用户)。\n const probeAlive = io.probeAlive ?? isProcessAlive\n const sleep = io.sleep ?? ((ms: number) => new Promise<void>((r) => setTimeout(r, ms)))\n await sleep(io.verifyDelayMs ?? 3000)\n const survivors = signalledPids.filter((p) => probeAlive(p.pid))\n const exited = signalled - survivors.length\n io.stdout(`swept ${exited}/${targets.length}\\n`)\n if (survivors.length > 0) {\n io.stdout(\n `${survivors.length} process(es) ignored SIGTERM (likely chaperone signal guard): ` +\n survivors.map((p) => p.pid).join(', ') +\n `\\nescalate manually if needed: kill -9 ${survivors.map((p) => p.pid).join(' ')}\\n`,\n )\n return 1\n }\n return signalled === targets.length ? 0 : 1\n}\n"],"mappings":";6LAkBA,MAAM,EAA2B,KAAU,IAUrC,EAAuB,CAAC,QAAS,SAAU,eAAgB,4BAA6B,UAAU,CAaxG,SAAS,EAAkB,EAA0B,CAGnD,IAAM,GADa,EAAQ,MAAM,cAAc,CAAC,IAAM,IAC1B,MAAM,CAAC,MAAM,MAAM,CAAC,IAAM,GACtD,OAAO,EAAqB,SAAS,EAAS,CAIhD,SAAgB,EAAU,EAAsB,EAAE,CAAkB,CAClE,IAAM,EAAgB,EAAK,eAAiB,EACtC,EAAc,EAAK,aAAe,EAClC,EAAU,EAAK,SAAW,IAI1B,EAAiB,EAAK,aACxB,IAAA,GACA,EAA+B,CAC7B,QAAS,EACT,gBAAiB,GAClB,CAAC,CACA,EAAe,EAAK,eAAkB,GAAsB,EAAgB,QAAQ,EAAU,EAE9F,EAAY,GAAa,CAG/B,OAFkB,GAAe,CAEhB,IAAK,GAAoB,CAExC,GAAI,EAAE,MAAQ,KAAM,MAAO,CAAE,GAAG,EAAG,QAAS,OAAQ,OAAQ,UAAW,CACvE,GAAI,EAAE,OAAS,EAAG,MAAO,CAAE,GAAG,EAAG,QAAS,OAAQ,OAAQ,aAAc,CACxE,GAAI,EAAkB,EAAE,QAAQ,CAAE,MAAO,CAAE,GAAG,EAAG,QAAS,OAAQ,OAAQ,mBAAoB,CAE9F,GAAI,EACF,OAAO,EAAE,QAAU,EACf,CAAE,GAAG,EAAG,QAAS,OAAQ,OAAQ,0BAA2B,CAC5D,CAAE,GAAG,EAAG,QAAS,OAAQ,OAAQ,kBAAmB,CAG1D,GAAI,CAAC,EAAE,UAEL,OAAO,EAAE,QAAU,EACf,CAAE,GAAG,EAAG,QAAS,OAAQ,OAAQ,qBAAsB,CACvD,CAAE,GAAG,EAAG,QAAS,OAAQ,OAAQ,mBAAoB,CAG3D,IAAM,EAAQ,EAAa,EAAE,UAAU,CAcvC,OAbK,EAAM,OAIP,EAAM,MAAQ,EAAE,IAEX,CAAE,GAAG,EAAG,QAAS,SAAU,OAAQ,uBAAwB,CAEhE,EAAM,aAAe,IAAA,IAAa,EAAM,WAAa,EAEhD,CAAE,GAAG,EAAG,QAAS,SAAU,OAAQ,cAAe,CAGpD,CAAE,GAAG,EAAG,QAAS,SAAU,OAAQ,oBAAqB,CAXtD,CAAE,GAAG,EAAG,QAAS,SAAU,OAAQ,iBAAkB,EAY9D,CChFJ,SAAS,EAAU,EAAuB,CACxC,OAAO,GAAS,KAAO,IAAI,EAAQ,MAAM,QAAQ,EAAE,CAAC,IAAM,GAAG,EAAM,IAGrE,SAAS,EAAY,EAAoB,CACvC,IAAM,EAAI,KAAK,MAAM,EAAK,KAAU,CACpC,GAAI,GAAK,GAAI,MAAO,GAAG,KAAK,MAAM,EAAI,GAAG,CAAC,GAAG,EAAI,GAAG,GACpD,IAAM,EAAI,KAAK,MAAO,EAAK,KAAa,IAAO,CAC/C,OAAO,EAAI,EAAI,GAAG,EAAE,GAAG,EAAE,GAAK,GAAG,EAAE,GAGrC,SAAS,EAAa,EAAyB,CAG7C,OAFI,EAAE,UAAY,SAAiB,UAAU,EAAE,OAAO,GAClD,EAAE,UAAY,OAAe,WAAW,EAAE,OAAO,GAC9C,IAGT,eAAsB,EAAa,EAAgB,EAAkC,CACnF,IAAM,EAAQ,EAAK,SAAS,UAAU,CAChC,EAAc,EAAK,SAAS,iBAAiB,CAC7C,EAAO,EAAK,SAAS,SAAS,CAC9B,EAAO,EAAG,OAAU,GAAgB,QAAQ,KAAK,EAAK,UAAU,EAEhE,EAAQ,GAAW,CACnB,EAAO,QAAQ,IAErB,GAAI,EAEF,OADA,EAAG,OAAO,KAAK,UAAU,EAAO,KAAM,EAAE,CAAG;EAAK,CACzC,EAGT,GAAI,EAAM,SAAW,EAEnB,OADA,EAAG,OAAO;EAA4B,CAC/B,EAIT,IAAM,EAAa,EAAM,QAAQ,EAAK,IAAM,EAAM,EAAE,MAAO,EAAE,CACvD,EAAS,EAAM,OAAQ,GAAM,EAAE,UAAY,SAAS,CACpD,EAAO,EAAM,OAAQ,GAAM,EAAE,UAAY,OAAO,CAEhD,EAAS,CAAC,MAAO,OAAQ,MAAO,MAAO,KAAM,MAAO,UAAW,UAAU,CAAC,KAAK,IAAK,CAC1F,EAAG,OAAO,EAAS;EAAK,CACxB,IAAK,IAAM,KAAK,EAAO,CACrB,IAAM,EAAW,EAAE,MAAQ,EAAO,IAAM,GACxC,EAAG,OACD,CACE,GAAG,EAAE,MAAM,IACX,EAAE,KACF,EAAU,EAAE,MAAM,CAClB,EAAE,IACF,EAAY,EAAE,QAAQ,CACtB,EAAE,YAAc,IAChB,EAAE,UAAY,EAAE,UAAU,MAAM,EAAG,EAAE,CAAG,IACxC,EAAa,EAAE,CAChB,CAAC,KAAK,IAAK,CAAG;EAChB,CAOH,GALA,EAAG,OACD,KAAK,EAAM,OAAO,wBAAwB,EAAU,EAAW,CAAC,IAC3D,EAAO,OAAO,qBAAqB,EAAK,OAAO,mBACrD,CAEG,CAAC,EAKH,OAJI,EAAO,OAAS,GAAK,EAAK,OAAS,IACrC,EAAG,OAAO,mDACP,EAAK,OAAS,EAAI,kCAAoC,IAAM;EAAK,CAE/D,EAIT,IAAM,EAAU,CAAC,GAAG,EAAQ,GAAI,EAAc,EAAO,EAAE,CAAE,CAAC,OACvD,GAAM,EAAE,MAAQ,GAAQ,EAAE,MAAQ,KACpC,CACD,GAAI,EAAQ,SAAW,EAErB,OADA,EAAG,OAAO;EAAqB,CACxB,EAET,IAAI,EAAY,EACV,EAAgC,EAAE,CACxC,IAAK,IAAM,KAAK,EACd,GAAI,CACF,EAAK,EAAE,IAAI,CACX,IACA,EAAc,KAAK,EAAE,CACrB,EAAG,OAAO,WAAW,EAAE,IAAI,IAAI,EAAa,EAAE,CAAC,IAAI,EAAU,EAAE,MAAM,CAAC,KAAK,OACpE,EAAK,CACZ,EAAG,OAAO,oBAAoB,EAAE,IAAI,IAAI,aAAe,MAAQ,EAAI,QAAU,OAAO,EAAI,CAAC,IAAI,CAQjG,IAAM,EAAa,EAAG,YAAc,EAEpC,MADc,EAAG,QAAW,GAAe,IAAI,QAAe,GAAM,WAAW,EAAG,EAAG,CAAC,GAC1E,EAAG,eAAiB,IAAK,CACrC,IAAM,EAAY,EAAc,OAAQ,GAAM,EAAW,EAAE,IAAI,CAAC,CAC1D,EAAS,EAAY,EAAU,OAUrC,OATA,EAAG,OAAO,SAAS,EAAO,GAAG,EAAQ,OAAO,IAAI,CAC5C,EAAU,OAAS,GACrB,EAAG,OACD,GAAG,EAAU,OAAO,gEAClB,EAAU,IAAK,GAAM,EAAE,IAAI,CAAC,KAAK,KAAK,CACtC,0CAA0C,EAAU,IAAK,GAAM,EAAE,IAAI,CAAC,KAAK,IAAI,CAAC,IACnF,CACM,GAEF,IAAc,EAAQ,OAAS,EAAI"}
|
|
1
|
+
{"version":3,"file":"ps-DKrluEuy.js","names":[],"sources":["../src/commands/fleet-scan.ts","../src/commands/ps.ts"],"sourcesContent":["/**\n * 进程舰队全量扫描工具(ps/kill/signal + 按会话分代)。纯模块——不持状态、不新建 child。\n */\n\nimport { OTTO_SESSION_LOCKS_DIR, isContainerEnvironment, listOttoProcesses } from '@x-otto/env'\nimport { createSessionWriteLeaseManager } from '@x-otto/session'\n\nimport type { OttoProcessInfo } from '@x-otto/env'\n\nexport type OrphanVerdict = 'strong' | 'weak' | 'none'\n\nexport interface FleetProcess extends OttoProcessInfo {\n readonly verdict: OrphanVerdict\n /** 判定理由(展示用,如 `lease-stale`/`lease-other-owner`/`lease-self-heartbeat`)。 */\n readonly reason: string\n}\n\n/** 弱孤儿的最小运行时长(无租约可核时的辅助信号)。 */\nconst WEAK_ORPHAN_MIN_ETIME_MS = 60 * 60 * 1000\n\n/**\n * serve 类常驻子命令——合法 detach 形态,永不判孤儿。\n *\n * 关于 `daemon`(RFC-335 M2):`otto daemon start` 拉起的**后台进程命令行是 `serve`**\n * (见 `daemon.ts` 的 `spawnDetached`),因此它实际由上面的 `'serve'` 一项保护;\n * `daemon` 自身只是前台短命进程(发完 spawn 即退出),不会被扫描为常驻。\n * 这里仍列出 `daemon` 只为表达意图,**不是**该保护生效的原因——真正的防护是 `serve`。\n */\nconst RESIDENT_SUBCOMMANDS = ['serve', 'daemon', 'persistenced', 'remote-persistence-server', 'observe']\n\nexport interface FleetScanDeps {\n /** 进程枚举(测试注入)。默认 `listOttoProcesses()`。 */\n listProcesses?: () => OttoProcessInfo[]\n /** 租约检视(测试注入)。默认按 OTTO_HOME/sessions/locks 组装只读 lease manager。 */\n inspectLease?: (sessionId: string) => { locked: boolean; pid?: number; alive?: boolean; mtimeAgoMs?: number }\n /** 容器环境探测(测试注入)。默认 `isContainerEnvironment()`。 */\n isContainer?: () => boolean\n /** 租约 stale 阈值(对齐 session-write-lease DEFAULT_STALE_MS)。 */\n staleMs?: number\n}\n\nfunction isResidentCommand(command: string): boolean {\n // 子命令是 bin/otto.js 之后的第一个非 flag 参数\n const afterEntry = command.split('bin/otto.js')[1] ?? ''\n const firstArg = afterEntry.trim().split(/\\s+/)[0] ?? ''\n return RESIDENT_SUBCOMMANDS.includes(firstArg)\n}\n\n/** 扫描全机 otto 主进程并逐个判定孤儿三态。 */\nexport function scanFleet(deps: FleetScanDeps = {}): FleetProcess[] {\n const listProcesses = deps.listProcesses ?? listOttoProcesses\n const isContainer = deps.isContainer ?? isContainerEnvironment\n const staleMs = deps.staleMs ?? 60_000\n // review P2 修复:lease manager 提到判定循环外一次组装(此前每进程 new 一个,含\n // 重复 mkdirSync)。只读组装:installExitHook:false 无退出钩子副作用;inspect() 无\n // acquire/touch。lockDir 与 cli.ts 的会话存储组装同源 OTTO_HOME(P0 修复后一致)。\n const defaultManager = deps.inspectLease\n ? undefined\n : createSessionWriteLeaseManager({\n lockDir: OTTO_SESSION_LOCKS_DIR,\n installExitHook: false,\n })\n const inspectLease = deps.inspectLease ?? ((sessionId: string) => defaultManager!.inspect(sessionId))\n\n const container = isContainer()\n const processes = listProcesses()\n\n return processes.map((p): FleetProcess => {\n // 安全不变式:有 TTY 一律非孤儿。\n if (p.tty !== '??') return { ...p, verdict: 'none', reason: 'has-tty' }\n if (p.ppid !== 1) return { ...p, verdict: 'none', reason: 'has-parent' }\n if (isResidentCommand(p.command)) return { ...p, verdict: 'none', reason: 'resident-service' }\n // 容器环境:ppid==1 是常态,强判定失效(F6 豁免)。\n if (container) {\n return p.etimeMs > WEAK_ORPHAN_MIN_ETIME_MS\n ? { ...p, verdict: 'weak', reason: 'container-ppid1-longrun' }\n : { ...p, verdict: 'none', reason: 'container-ppid1' }\n }\n\n if (!p.sessionId) {\n // 无 --continue:无租约可核 → 弱孤儿(>1h 辅助信号)。\n return p.etimeMs > WEAK_ORPHAN_MIN_ETIME_MS\n ? { ...p, verdict: 'weak', reason: 'no-session-longrun' }\n : { ...p, verdict: 'none', reason: 'no-session-young' }\n }\n\n const lease = inspectLease(p.sessionId)\n if (!lease.locked) {\n // 锁文件不存在——会话早已正常释放,进程却还挂着 → 强孤儿。\n return { ...p, verdict: 'strong', reason: 'lease-released' }\n }\n if (lease.pid === p.pid) {\n // (c) 自持租约的活孤儿——心跳还在但不可交互(见文件头语义说明)。\n return { ...p, verdict: 'strong', reason: 'lease-self-heartbeat' }\n }\n if (lease.mtimeAgoMs !== undefined && lease.mtimeAgoMs > staleMs) {\n // (b) 租约 stale——持有者已死。\n return { ...p, verdict: 'strong', reason: 'lease-stale' }\n }\n // (a) 租约被其他进程持有且 fresh——本候选是废弃副本。\n return { ...p, verdict: 'strong', reason: 'lease-other-owner' }\n })\n}\n","/**\n * /ps 命令:进程舰队列表视图(按会话/TTY 分组、代际计数/内存)。\n */\nimport { isProcessAlive } from '@x-otto/env'\nimport { scanFleet } from './fleet-scan'\n\nimport type { FleetProcess } from './fleet-scan'\n\nexport interface PsCommandIo {\n stdout: (s: string) => void\n stderr: (s: string) => void\n /** SIGTERM 发送(测试注入)。默认 `process.kill(pid, 'SIGTERM')`。 */\n kill?: (pid: number) => void\n /** 存活探测(测试注入)。默认 `isProcessAlive`(@x-otto/env,kill(pid,0) 语义)。 */\n probeAlive?: (pid: number) => boolean\n /** sweep 后存活复查的等待毫秒(测试注入缩短)。默认 3000。 */\n verifyDelayMs?: number\n /** 延迟原语(测试注入 fake)。默认真实 setTimeout。 */\n sleep?: (ms: number) => Promise<void>\n}\n\nfunction formatRss(rssKb: number): string {\n return rssKb >= 1024 ? `${(rssKb / 1024).toFixed(0)}MB` : `${rssKb}KB`\n}\n\nfunction formatEtime(ms: number): string {\n const h = Math.floor(ms / 3_600_000)\n if (h >= 24) return `${Math.floor(h / 24)}d${h % 24}h`\n const m = Math.floor((ms % 3_600_000) / 60_000)\n return h > 0 ? `${h}h${m}m` : `${m}m`\n}\n\nfunction verdictLabel(p: FleetProcess): string {\n if (p.verdict === 'strong') return `ORPHAN(${p.reason})`\n if (p.verdict === 'weak') return `orphan?(${p.reason})`\n return '-'\n}\n\nexport async function runPsCommand(args: string[], io: PsCommandIo): Promise<number> {\n const sweep = args.includes('--sweep')\n const includeWeak = args.includes('--include-weak')\n const json = args.includes('--json')\n const kill = io.kill ?? ((pid: number) => process.kill(pid, 'SIGTERM'))\n\n const fleet = scanFleet()\n const self = process.pid\n\n if (json) {\n io.stdout(JSON.stringify(fleet, null, 2) + '\\n')\n return 0\n }\n\n if (fleet.length === 0) {\n io.stdout('no otto processes found\\n')\n return 0\n }\n\n // 列表输出\n const totalRssKb = fleet.reduce((acc, p) => acc + p.rssKb, 0)\n const strong = fleet.filter((p) => p.verdict === 'strong')\n const weak = fleet.filter((p) => p.verdict === 'weak')\n\n const header = ['PID', 'PPID', 'RSS', 'TTY', 'UP', 'GEN', 'SESSION', 'VERDICT'].join('\\t')\n io.stdout(header + '\\n')\n for (const p of fleet) {\n const selfMark = p.pid === self ? '*' : ''\n io.stdout(\n [\n `${p.pid}${selfMark}`,\n p.ppid,\n formatRss(p.rssKb),\n p.tty,\n formatEtime(p.etimeMs),\n p.generation ?? '-',\n p.sessionId ? p.sessionId.slice(0, 8) : '-',\n verdictLabel(p),\n ].join('\\t') + '\\n',\n )\n }\n io.stdout(\n `\\n${fleet.length} processes, total RSS ${formatRss(totalRssKb)}; ` +\n `${strong.length} strong orphan(s), ${weak.length} weak orphan(s)\\n`,\n )\n\n if (!sweep) {\n if (strong.length > 0 || weak.length > 0) {\n io.stdout('run `otto ps --sweep` to SIGTERM strong orphans' +\n (weak.length > 0 ? ' (--include-weak for weak ones)' : '') + '\\n')\n }\n return 0\n }\n\n // sweep:SIGTERM only;自进程/有 TTY 双保险排除(判定层已保证有 TTY 不入选,此处复核)。\n const targets = [...strong, ...(includeWeak ? weak : [])].filter(\n (p) => p.pid !== self && p.tty === '??',\n )\n if (targets.length === 0) {\n io.stdout('nothing to sweep\\n')\n return 0\n }\n let signalled = 0\n const signalledPids: FleetProcess[] = []\n for (const p of targets) {\n try {\n kill(p.pid)\n signalled++\n signalledPids.push(p)\n io.stdout(`SIGTERM ${p.pid} (${verdictLabel(p)}, ${formatRss(p.rssKb)})\\n`)\n } catch (err) {\n io.stderr(`failed to signal ${p.pid}: ${err instanceof Error ? err.message : String(err)}\\n`)\n }\n }\n\n // review P1-a 修复:SIGTERM 送达 ≠ 进程退出——chaperone 态孤儿装着空函数 signal guard\n // (cli.ts chaperoneSignalGuard,防前台组误杀的既有设计)会吞掉 SIGTERM,进程纹丝不动。\n // 送信号后延迟复查存活,把\"未退出\"名单如实报告并提示手动升级(不自动 SIGKILL——保持\n // RFC-196 \"SIGTERM only\" 不变式,kill -9 决策权留给用户)。\n const probeAlive = io.probeAlive ?? isProcessAlive\n const sleep = io.sleep ?? ((ms: number) => new Promise<void>((r) => setTimeout(r, ms)))\n await sleep(io.verifyDelayMs ?? 3000)\n const survivors = signalledPids.filter((p) => probeAlive(p.pid))\n const exited = signalled - survivors.length\n io.stdout(`swept ${exited}/${targets.length}\\n`)\n if (survivors.length > 0) {\n io.stdout(\n `${survivors.length} process(es) ignored SIGTERM (likely chaperone signal guard): ` +\n survivors.map((p) => p.pid).join(', ') +\n `\\nescalate manually if needed: kill -9 ${survivors.map((p) => p.pid).join(' ')}\\n`,\n )\n return 1\n }\n return signalled === targets.length ? 0 : 1\n}\n"],"mappings":"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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import{RegistryClient as e,builtinOfficialSource as t}from"@x-otto/coding";import{rankCandidates as n}from"@x-otto/shared";import{OTTO_HOME as r,OTTO_USER_CONFIG_DIR as i}from"@x-otto/env";function a(e){return process.env.OTTO_OFFICIAL_REGISTRY_URL??`https://x-ottojs.github.io/otto-registry/registry.json`}function o(n){return new e({configDir:i,ottoHome:r,officialSource:t(a(n))})}function s(e){return o(e.app)}async function c(e,t){let n=s(t),[r,...i]=e.filter(e=>!e.startsWith(`-`));try{if(r===`add`){let[e,r]=i;if(!e||!r){t.tui.info.showToast(`Registry`,`Usage: /registry add <name> <url>`);return}n.addSource(e,r),t.tui.info.showToast(`Registry`,`已注册 '${e}' → ${r}`);return}if(r===`remove`){let[e]=i;if(!e){t.tui.info.showToast(`Registry`,`Usage: /registry remove <name>`);return}n.removeSource(e),t.tui.info.showToast(`Registry`,`已移除 '${e}'`);return}let e=await n.listSourcesAsync(),{registries:a,warnings:o}=await n.loadAll(),s=new Map,c=new Map;for(let e of a)s.set(e.sourceName,e.entries.length),c.set(e.sourceName,e.stale);let l=e.map(e=>{let t=s.get(e.name),n=t===void 0?``:`${t} entries`,r=c.get(e.name)?` (stale cache)`:``,i=e.disabled?`✗ disabled`:`✓ enabled`,a=e.builtin?` · builtin`:``;return{label:e.name,value:e.name,detail:`${i}${a}${n?` · ${n}`:``}${r}\n${e.url}`,disabled:!1}}),u=e.filter(e=>!e.disabled).length;t.tui.modal.openModal({kind:`select`,title:`Registry Sources (${u}/${e.length} enabled)`,subtitle:o.length>0?o.map(e=>`⚠ ${e}`).join(`
|
|
3
|
-
`):void 0,options:l,onSelect:n=>{let r=e.find(e=>e.name===n);if(!r)return;let i=s.get(r.name),a=`${i===void 0?``:`${i} entries · `}${r.disabled?`disabled`:`enabled`}${r.builtin?` · builtin`:``}\n${r.url}`,o=[];r.builtin?o.push(r.disabled?`/registry re-enable (coming soon)`:`/registry disable (coming soon)`):o.push(`/registry remove ${r.name}`),t.tui.info.showToast(r.name,`${a}\n${o.join(` · `)}`)},onCancel:()=>{}})}catch(e){t.tui.info.showToast(`Registry`,e instanceof Error?e.message:String(e))}}async function l(e,t){let{registries:r,warnings:i}=await s(e).loadAll();return{hits:n(r.flatMap(e=>e.entries.map(t=>({entry:t,registry:e}))).map(e=>({text:e.entry.id,aliases:[e.entry.description,...e.entry.keywords??[]],payload:e})),t,{maxResults:20}).map(e=>({entry:e.item.payload.entry,sourceName:e.item.payload.registry.sourceName,stale:e.item.payload.registry.stale})),warnings:i}}async function u(e,t){let r=e.filter(e=>!e.startsWith(`-`)).join(` `).trim();if(!r){t.tui.info.showToast(`Search`,`Usage: /search <keyword>`);return}let i;t.tui.modal.openModal({kind:`select`,title:`Search '${r}'`,subtitle:``,options:[],searchable:!0,onSelect:e=>{if(!i)return;let n=i[Number(e)];if(!n)return;let{entry:r}=n.item.payload,a=[`--as`,r.type,...r.subPath?[`--sub-path`,r.subPath]:[],r.source];import(`./install-
|
|
3
|
+
`):void 0,options:l,onSelect:n=>{let r=e.find(e=>e.name===n);if(!r)return;let i=s.get(r.name),a=`${i===void 0?``:`${i} entries · `}${r.disabled?`disabled`:`enabled`}${r.builtin?` · builtin`:``}\n${r.url}`,o=[];r.builtin?o.push(r.disabled?`/registry re-enable (coming soon)`:`/registry disable (coming soon)`):o.push(`/registry remove ${r.name}`),t.tui.info.showToast(r.name,`${a}\n${o.join(` · `)}`)},onCancel:()=>{}})}catch(e){t.tui.info.showToast(`Registry`,e instanceof Error?e.message:String(e))}}async function l(e,t){let{registries:r,warnings:i}=await s(e).loadAll();return{hits:n(r.flatMap(e=>e.entries.map(t=>({entry:t,registry:e}))).map(e=>({text:e.entry.id,aliases:[e.entry.description,...e.entry.keywords??[]],payload:e})),t,{maxResults:20}).map(e=>({entry:e.item.payload.entry,sourceName:e.item.payload.registry.sourceName,stale:e.item.payload.registry.stale})),warnings:i}}async function u(e,t){let r=e.filter(e=>!e.startsWith(`-`)).join(` `).trim();if(!r){t.tui.info.showToast(`Search`,`Usage: /search <keyword>`);return}let i;t.tui.modal.openModal({kind:`select`,title:`Search '${r}'`,subtitle:``,options:[],searchable:!0,onSelect:e=>{if(!i)return;let n=i[Number(e)];if(!n)return;let{entry:r}=n.item.payload,a=[`--as`,r.type,...r.subPath?[`--sub-path`,r.subPath]:[],r.source];import(`./install-WCcQ9k9M.js`).then(({handleInstall:e})=>e(a,t))},onCancel:()=>{},async:(async()=>{let{registries:e,warnings:a}=await s(t).loadAll();if(i=n(e.flatMap(e=>e.entries.map(t=>({entry:t,registry:e}))).map(e=>({text:e.entry.id,aliases:[e.entry.description,...e.entry.keywords??[]],payload:e})),r,{maxResults:20}),i.length===0){let e=a.length>0?`\n${a.map(e=>`⚠ ${e}`).join(`
|
|
4
4
|
`)}`:``;return{title:`Search '${r}' — 0 hits`,subtitle:`No matches for '${r}'.${e}`,options:[]}}let o=new Map;for(let e of i){let t=e.item.payload.entry.id;o.set(t,(o.get(t)??0)+1)}let c=a.length>0?`\n${a.map(e=>`⚠ ${e}`).join(`
|
|
5
5
|
`)}`:``;return{title:`Search '${r}' — ${i.length} hits`,subtitle:`Enter install (preview first) · Esc cancel${c}`,options:i.map((e,t)=>{let{entry:n,registry:r}=e.item.payload,i=n.capabilities?.length?`[${n.capabilities.join(`, `)}]`:``,a=r.stale?` (stale cache)`:``,s=(o.get(n.id)??0)>1?` @${r.sourceName}`:``;return{label:`${n.id}${s} (${n.type}) ${i}${a}`,value:String(t),detail:n.description}})}})()})}export{l as i,u as n,o as r,c as t};
|
|
6
|
-
//# sourceMappingURL=registry-
|
|
6
|
+
//# sourceMappingURL=registry-BdCcwyGy.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"registry-Cr5To_5r.js","names":[],"sources":["../src/commands/handlers/registry.ts"],"sourcesContent":["/**\n * /registry 命令:插件注册源管理(add/remove/list + 检查)\n */\nimport { RegistryClient, builtinOfficialSource } from '@x-otto/coding'\nimport { OTTO_HOME, OTTO_USER_CONFIG_DIR } from '@x-otto/env'\nimport { rankCandidates } from '@x-otto/shared'\n\nimport type { App, LoadedRegistry, RegistryEntry } from '@x-otto/coding'\nimport type { CliContext } from '../../command-dispatcher'\nimport type { SelectOption } from '@x-otto/tui'\n\n/**\n * 官方源 URL(RFC-221 D1):registry.json 经独立发布仓库(`x-ottojs/otto-registry`)\n * 的 GitHub Pages 单独发布,仓库本身保持 private(路径 B,已 grill 确认)。\n * `OTTO_OFFICIAL_REGISTRY_URL` 环境变量覆盖机制保留(开发者本地测试/CI 调试用)。\n *\n * 发布机制(RFC-221 M1):CI workflow `.github/workflows/publish-registry.yml` 在\n * `docs/registry/registry.json` 变更推送到 main 后自动同步到 otto-registry 仓库。\n * 首次发布在 CI 验证完整链路前已手动推送完成(因 GitHub Actions 计费额度暂限制了\n * CI runner 执行,已直接 SSH push 验证内容可达,计费恢复后 CI 自动化同步即正常)。\n */\nfunction officialRegistryUrl(_app: Pick<App, 'workspaceDir'>): string {\n return process.env['OTTO_OFFICIAL_REGISTRY_URL'] ??\n 'https://x-ottojs.github.io/otto-registry/registry.json'\n}\n\n/**\n * `App` 版工厂(导出复用于 `/registry remove` 参数补全接线——interactive-setup.ts 只持有\n * `App`,不持有完整 `CliContext`,见 RFC-125 D3 同款模式)。\n */\nexport function makeRegistryClient(app: Pick<App, 'workspaceDir'>): RegistryClient {\n return new RegistryClient({\n configDir: OTTO_USER_CONFIG_DIR,\n ottoHome: OTTO_HOME,\n officialSource: builtinOfficialSource(officialRegistryUrl(app)),\n })\n}\n\nfunction makeClient(ctx: CliContext): RegistryClient {\n return makeRegistryClient(ctx.app)\n}\n\nexport async function handleRegistry(args: string[], ctx: CliContext): Promise<void> {\n const client = makeClient(ctx)\n const [sub, ...rest] = args.filter((a) => !a.startsWith('-'))\n\n try {\n if (sub === 'add') {\n const [name, url] = rest\n if (!name || !url) {\n ctx.tui.info.showToast('Registry', 'Usage: /registry add <name> <url>')\n return\n }\n client.addSource(name, url)\n ctx.tui.info.showToast('Registry', `已注册 '${name}' → ${url}`)\n return\n }\n if (sub === 'remove') {\n const [name] = rest\n if (!name) {\n ctx.tui.info.showToast('Registry', 'Usage: /registry remove <name>')\n return\n }\n client.removeSource(name)\n ctx.tui.info.showToast('Registry', `已移除 '${name}'`)\n return\n }\n // 缺省/list:升级为交互式选择面板(此前是纯文本 system_notification 推对话流,\n // 无上下选择/详情展开/启用禁用入口,无法快速浏览或操作多源)。\n // RFC-341 G2:用 listSourcesAsync 而非 listSources,与下方 loadAll 内部走的源列表\n // 一致(含内网可达时自动出现的 otto-official-internal)——否则面板选项与\n // entryCounts/staleFlags 会对不上号(loadAll 加载了内网源但列表没显示它)。\n const sources = await client.listSourcesAsync()\n\n // 异步加载各源的条目计数(用于面板内展示,fail-soft:加载失败不阻断面板打开)\n const { registries, warnings } = await client.loadAll()\n const entryCounts = new Map<string, number>()\n const staleFlags = new Map<string, boolean>()\n for (const reg of registries) {\n entryCounts.set(reg.sourceName, reg.entries.length)\n staleFlags.set(reg.sourceName, reg.stale)\n }\n\n const options: SelectOption[] = sources.map((s) => {\n const count = entryCounts.get(s.name)\n const countStr = count !== undefined ? `${count} entries` : ''\n const stale = staleFlags.get(s.name) ? ' (stale cache)' : ''\n const status = s.disabled ? '✗ disabled' : '✓ enabled'\n const builtin = s.builtin ? ' · builtin' : ''\n return {\n label: s.name,\n value: s.name,\n detail: `${status}${builtin}${countStr ? ` · ${countStr}` : ''}${stale}\\n${s.url}`,\n disabled: false, // disabled 源仍可选中查看详情/重新启用\n }\n })\n\n const enabledCount = sources.filter((s) => !s.disabled).length\n ctx.tui.modal.openModal({\n kind: 'select',\n title: `Registry Sources (${enabledCount}/${sources.length} enabled)`,\n subtitle: warnings.length > 0 ? warnings.map((w) => `⚠ ${w}`).join('\\n') : undefined,\n options,\n onSelect: (value: string) => {\n const source = sources.find((s) => s.name === value)\n if (!source) return\n // 选中后显示详情 toast + 操作提示(启用/禁用、删除非内置源)\n const count = entryCounts.get(source.name)\n const countStr = count !== undefined ? `${count} entries · ` : ''\n const details = `${countStr}${source.disabled ? 'disabled' : 'enabled'}${source.builtin ? ' · builtin' : ''}\\n${source.url}`\n const actionHints: string[] = []\n if (source.builtin) {\n actionHints.push(source.disabled ? '/registry re-enable (coming soon)' : '/registry disable (coming soon)')\n } else {\n actionHints.push(`/registry remove ${source.name}`)\n }\n ctx.tui.info.showToast(source.name, `${details}\\n${actionHints.join(' · ')}`)\n },\n onCancel: () => {},\n })\n } catch (error) {\n ctx.tui.info.showToast('Registry', error instanceof Error ? error.message : String(error))\n }\n}\n\n/** search 数据面(/search 命令与 /extension 发现入口共用;RFC-201 M3)。 */\nexport async function searchRegistries(\n ctx: CliContext,\n keyword: string,\n): Promise<{ hits: Array<{ entry: RegistryEntry; sourceName: string; stale: boolean }>; warnings: string[] }> {\n const client = makeClient(ctx)\n const { registries, warnings } = await client.loadAll()\n const candidates = registries.flatMap((registry) =>\n registry.entries.map((entry) => ({ entry, registry })),\n )\n const ranked = rankCandidates(\n candidates.map((hit) => ({\n text: hit.entry.id,\n aliases: [hit.entry.description, ...(hit.entry.keywords ?? [])],\n payload: hit,\n })),\n keyword,\n { maxResults: 20 },\n )\n return {\n hits: ranked.map((r) => ({\n entry: r.item.payload.entry,\n sourceName: r.item.payload.registry.sourceName,\n stale: r.item.payload.registry.stale,\n })),\n warnings,\n }\n}\n\nexport async function handleSearch(args: string[], ctx: CliContext): Promise<void> {\n const keyword = args.filter((a) => !a.startsWith('-')).join(' ').trim()\n if (!keyword) {\n ctx.tui.info.showToast('Search', 'Usage: /search <keyword>')\n return\n }\n\n // RFC-227:框架层 async loading 门——将\"await 全流程 → openModal\"改为\n // \"openModal + async 字段:promise 内做 loadAll+rank,resolve 返回内容,\n // 框架层自动处理 loading→内容过渡\"。ranked 在 outer scope 声明供 onSelect\n // 闭包引用,async promise 内赋值(lesson_104:透传,非重新计算)。\n interface Hit { entry: RegistryEntry; registry: LoadedRegistry }\n let ranked: Array<{ item: { payload: Hit } }> | undefined\n\n ctx.tui.modal.openModal({\n kind: 'select',\n title: `Search '${keyword}'`,\n subtitle: '', // loading 态框架层填充,此处占位\n options: [] as never, // async 存在时 options 由框架层管理\n searchable: true, // 先占 searchable 模式,等结果数决定是否真的 searchable\n onSelect: (v: string) => {\n if (!ranked) return\n const hit = ranked[Number(v)]\n if (!hit) return\n const { entry } = hit.item.payload\n const installArgs = ['--as', entry.type, ...(entry.subPath ? ['--sub-path', entry.subPath] : []), entry.source]\n void import('./install').then(({ handleInstall }) => handleInstall(installArgs, ctx))\n },\n onCancel: () => {},\n async: (async () => {\n const client = makeClient(ctx)\n const { registries, warnings } = await client.loadAll()\n\n interface Hit { entry: RegistryEntry; registry: LoadedRegistry }\n const candidates: Hit[] = registries.flatMap((registry) =>\n registry.entries.map((entry) => ({ entry, registry })),\n )\n\n ranked = rankCandidates(\n candidates.map((hit) => ({\n text: hit.entry.id,\n aliases: [hit.entry.description, ...(hit.entry.keywords ?? [])],\n payload: hit,\n })),\n keyword,\n { maxResults: 20 },\n )\n\n if (ranked.length === 0) {\n const extra = warnings.length > 0 ? `\\n${warnings.map((w) => `⚠ ${w}`).join('\\n')}` : ''\n // 零命中结果——返回空选项列表,subtitle 告知用户\n return {\n title: `Search '${keyword}' — 0 hits`,\n subtitle: `No matches for '${keyword}'.${extra}`,\n options: [],\n }\n }\n\n const idCounts = new Map<string, number>()\n for (const r of ranked) {\n const id = r.item.payload.entry.id\n idCounts.set(id, (idCounts.get(id) ?? 0) + 1)\n }\n\n const subtitleWarnings = warnings.length > 0\n ? `\\n${warnings.map((w) => `⚠ ${w}`).join('\\n')}`\n : ''\n\n return {\n title: `Search '${keyword}' — ${ranked.length} hits`,\n subtitle: `Enter install (preview first) · Esc cancel${subtitleWarnings}`,\n options: ranked.map((r, i) => {\n const { entry, registry } = r.item.payload\n const caps = entry.capabilities?.length ? `[${entry.capabilities.join(', ')}]` : ''\n const stale = registry.stale ? ' (stale cache)' : ''\n const dupMark = (idCounts.get(entry.id) ?? 0) > 1 ? ` @${registry.sourceName}` : ''\n return {\n label: `${entry.id}${dupMark} (${entry.type}) ${caps}${stale}`,\n value: String(i),\n detail: entry.description,\n }\n }),\n }\n })(),\n })\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"registry-BdCcwyGy.js","names":[],"sources":["../src/commands/handlers/registry.ts"],"sourcesContent":["/**\n * /registry 命令:插件注册源管理(add/remove/list + 检查)\n */\nimport { RegistryClient, builtinOfficialSource } from '@x-otto/coding'\nimport { OTTO_HOME, OTTO_USER_CONFIG_DIR } from '@x-otto/env'\nimport { rankCandidates } from '@x-otto/shared'\n\nimport type { App, LoadedRegistry, RegistryEntry } from '@x-otto/coding'\nimport type { CliContext } from '../../command-dispatcher'\nimport type { SelectOption } from '@x-otto/tui'\n\n/**\n * 官方源 URL(RFC-221 D1):registry.json 经独立发布仓库(`x-ottojs/otto-registry`)\n * 的 GitHub Pages 单独发布,仓库本身保持 private(路径 B,已 grill 确认)。\n * `OTTO_OFFICIAL_REGISTRY_URL` 环境变量覆盖机制保留(开发者本地测试/CI 调试用)。\n *\n * 发布机制(RFC-221 M1):CI workflow `.github/workflows/publish-registry.yml` 在\n * `docs/registry/registry.json` 变更推送到 main 后自动同步到 otto-registry 仓库。\n * 首次发布在 CI 验证完整链路前已手动推送完成(因 GitHub Actions 计费额度暂限制了\n * CI runner 执行,已直接 SSH push 验证内容可达,计费恢复后 CI 自动化同步即正常)。\n */\nfunction officialRegistryUrl(_app: Pick<App, 'workspaceDir'>): string {\n return process.env['OTTO_OFFICIAL_REGISTRY_URL'] ??\n 'https://x-ottojs.github.io/otto-registry/registry.json'\n}\n\n/**\n * `App` 版工厂(导出复用于 `/registry remove` 参数补全接线——interactive-setup.ts 只持有\n * `App`,不持有完整 `CliContext`,见 RFC-125 D3 同款模式)。\n */\nexport function makeRegistryClient(app: Pick<App, 'workspaceDir'>): RegistryClient {\n return new RegistryClient({\n configDir: OTTO_USER_CONFIG_DIR,\n ottoHome: OTTO_HOME,\n officialSource: builtinOfficialSource(officialRegistryUrl(app)),\n })\n}\n\nfunction makeClient(ctx: CliContext): RegistryClient {\n return makeRegistryClient(ctx.app)\n}\n\nexport async function handleRegistry(args: string[], ctx: CliContext): Promise<void> {\n const client = makeClient(ctx)\n const [sub, ...rest] = args.filter((a) => !a.startsWith('-'))\n\n try {\n if (sub === 'add') {\n const [name, url] = rest\n if (!name || !url) {\n ctx.tui.info.showToast('Registry', 'Usage: /registry add <name> <url>')\n return\n }\n client.addSource(name, url)\n ctx.tui.info.showToast('Registry', `已注册 '${name}' → ${url}`)\n return\n }\n if (sub === 'remove') {\n const [name] = rest\n if (!name) {\n ctx.tui.info.showToast('Registry', 'Usage: /registry remove <name>')\n return\n }\n client.removeSource(name)\n ctx.tui.info.showToast('Registry', `已移除 '${name}'`)\n return\n }\n // 缺省/list:升级为交互式选择面板(此前是纯文本 system_notification 推对话流,\n // 无上下选择/详情展开/启用禁用入口,无法快速浏览或操作多源)。\n // RFC-341 G2:用 listSourcesAsync 而非 listSources,与下方 loadAll 内部走的源列表\n // 一致(含内网可达时自动出现的 otto-official-internal)——否则面板选项与\n // entryCounts/staleFlags 会对不上号(loadAll 加载了内网源但列表没显示它)。\n const sources = await client.listSourcesAsync()\n\n // 异步加载各源的条目计数(用于面板内展示,fail-soft:加载失败不阻断面板打开)\n const { registries, warnings } = await client.loadAll()\n const entryCounts = new Map<string, number>()\n const staleFlags = new Map<string, boolean>()\n for (const reg of registries) {\n entryCounts.set(reg.sourceName, reg.entries.length)\n staleFlags.set(reg.sourceName, reg.stale)\n }\n\n const options: SelectOption[] = sources.map((s) => {\n const count = entryCounts.get(s.name)\n const countStr = count !== undefined ? `${count} entries` : ''\n const stale = staleFlags.get(s.name) ? ' (stale cache)' : ''\n const status = s.disabled ? '✗ disabled' : '✓ enabled'\n const builtin = s.builtin ? ' · builtin' : ''\n return {\n label: s.name,\n value: s.name,\n detail: `${status}${builtin}${countStr ? ` · ${countStr}` : ''}${stale}\\n${s.url}`,\n disabled: false, // disabled 源仍可选中查看详情/重新启用\n }\n })\n\n const enabledCount = sources.filter((s) => !s.disabled).length\n ctx.tui.modal.openModal({\n kind: 'select',\n title: `Registry Sources (${enabledCount}/${sources.length} enabled)`,\n subtitle: warnings.length > 0 ? warnings.map((w) => `⚠ ${w}`).join('\\n') : undefined,\n options,\n onSelect: (value: string) => {\n const source = sources.find((s) => s.name === value)\n if (!source) return\n // 选中后显示详情 toast + 操作提示(启用/禁用、删除非内置源)\n const count = entryCounts.get(source.name)\n const countStr = count !== undefined ? `${count} entries · ` : ''\n const details = `${countStr}${source.disabled ? 'disabled' : 'enabled'}${source.builtin ? ' · builtin' : ''}\\n${source.url}`\n const actionHints: string[] = []\n if (source.builtin) {\n actionHints.push(source.disabled ? '/registry re-enable (coming soon)' : '/registry disable (coming soon)')\n } else {\n actionHints.push(`/registry remove ${source.name}`)\n }\n ctx.tui.info.showToast(source.name, `${details}\\n${actionHints.join(' · ')}`)\n },\n onCancel: () => {},\n })\n } catch (error) {\n ctx.tui.info.showToast('Registry', error instanceof Error ? error.message : String(error))\n }\n}\n\n/** search 数据面(/search 命令与 /extension 发现入口共用;RFC-201 M3)。 */\nexport async function searchRegistries(\n ctx: CliContext,\n keyword: string,\n): Promise<{ hits: Array<{ entry: RegistryEntry; sourceName: string; stale: boolean }>; warnings: string[] }> {\n const client = makeClient(ctx)\n const { registries, warnings } = await client.loadAll()\n const candidates = registries.flatMap((registry) =>\n registry.entries.map((entry) => ({ entry, registry })),\n )\n const ranked = rankCandidates(\n candidates.map((hit) => ({\n text: hit.entry.id,\n aliases: [hit.entry.description, ...(hit.entry.keywords ?? [])],\n payload: hit,\n })),\n keyword,\n { maxResults: 20 },\n )\n return {\n hits: ranked.map((r) => ({\n entry: r.item.payload.entry,\n sourceName: r.item.payload.registry.sourceName,\n stale: r.item.payload.registry.stale,\n })),\n warnings,\n }\n}\n\nexport async function handleSearch(args: string[], ctx: CliContext): Promise<void> {\n const keyword = args.filter((a) => !a.startsWith('-')).join(' ').trim()\n if (!keyword) {\n ctx.tui.info.showToast('Search', 'Usage: /search <keyword>')\n return\n }\n\n // RFC-227:框架层 async loading 门——将\"await 全流程 → openModal\"改为\n // \"openModal + async 字段:promise 内做 loadAll+rank,resolve 返回内容,\n // 框架层自动处理 loading→内容过渡\"。ranked 在 outer scope 声明供 onSelect\n // 闭包引用,async promise 内赋值(lesson_104:透传,非重新计算)。\n interface Hit { entry: RegistryEntry; registry: LoadedRegistry }\n let ranked: Array<{ item: { payload: Hit } }> | undefined\n\n ctx.tui.modal.openModal({\n kind: 'select',\n title: `Search '${keyword}'`,\n subtitle: '', // loading 态框架层填充,此处占位\n options: [] as never, // async 存在时 options 由框架层管理\n searchable: true, // 先占 searchable 模式,等结果数决定是否真的 searchable\n onSelect: (v: string) => {\n if (!ranked) return\n const hit = ranked[Number(v)]\n if (!hit) return\n const { entry } = hit.item.payload\n const installArgs = ['--as', entry.type, ...(entry.subPath ? ['--sub-path', entry.subPath] : []), entry.source]\n void import('./install').then(({ handleInstall }) => handleInstall(installArgs, ctx))\n },\n onCancel: () => {},\n async: (async () => {\n const client = makeClient(ctx)\n const { registries, warnings } = await client.loadAll()\n\n interface Hit { entry: RegistryEntry; registry: LoadedRegistry }\n const candidates: Hit[] = registries.flatMap((registry) =>\n registry.entries.map((entry) => ({ entry, registry })),\n )\n\n ranked = rankCandidates(\n candidates.map((hit) => ({\n text: hit.entry.id,\n aliases: [hit.entry.description, ...(hit.entry.keywords ?? [])],\n payload: hit,\n })),\n keyword,\n { maxResults: 20 },\n )\n\n if (ranked.length === 0) {\n const extra = warnings.length > 0 ? `\\n${warnings.map((w) => `⚠ ${w}`).join('\\n')}` : ''\n // 零命中结果——返回空选项列表,subtitle 告知用户\n return {\n title: `Search '${keyword}' — 0 hits`,\n subtitle: `No matches for '${keyword}'.${extra}`,\n options: [],\n }\n }\n\n const idCounts = new Map<string, number>()\n for (const r of ranked) {\n const id = r.item.payload.entry.id\n idCounts.set(id, (idCounts.get(id) ?? 0) + 1)\n }\n\n const subtitleWarnings = warnings.length > 0\n ? `\\n${warnings.map((w) => `⚠ ${w}`).join('\\n')}`\n : ''\n\n return {\n title: `Search '${keyword}' — ${ranked.length} hits`,\n subtitle: `Enter install (preview first) · Esc cancel${subtitleWarnings}`,\n options: ranked.map((r, i) => {\n const { entry, registry } = r.item.payload\n const caps = entry.capabilities?.length ? `[${entry.capabilities.join(', ')}]` : ''\n const stale = registry.stale ? ' (stale cache)' : ''\n const dupMark = (idCounts.get(entry.id) ?? 0) > 1 ? ` @${registry.sourceName}` : ''\n return {\n label: `${entry.id}${dupMark} (${entry.type}) ${caps}${stale}`,\n value: String(i),\n detail: entry.description,\n }\n }),\n }\n })(),\n })\n}\n"],"mappings":"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"}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import{r as e}from"./service-client-Cr4sxDVM.js";import{createAttachSessionDataSource as t,createSessionEventReduceBridge as n,createStoreTuiSink as r,startTui as i}from"@x-otto/tui";const a=e=>typeof e==`object`&&!!e&&!Array.isArray(e);function o(e,t){let n;n=e.startsWith(`https://`)?`wss://${e.slice(8)}`:e.startsWith(`http://`)?`ws://${e.slice(7)}`:e.startsWith(`localhost`)||e.startsWith(`127.`)?`ws://${e}`:`wss://${e}`;let r=`${n.replace(/\/$/,``)}/ws`;return t?`${r}?token=${encodeURIComponent(t)}`:r}function s(e,t,n){let r=n.WebSocketImpl??globalThis.WebSocket,i=n.setTimeoutImpl??setTimeout,s=n.clearTimeoutImpl??clearTimeout,c=n.initialBackoffMs??500,l=n.maxBackoffMs??1e4,u=null,d=c,f=null,p=!1,m=0,h=null,g=null,_=1,v=new Map,y=null,b=e=>{e!==y&&(y=e,n.onStateChange?.(e))},x=(e=u)=>{!e||e.readyState!==e.OPEN||e.send(JSON.stringify({method:`Session.subscribe`,params:{sessionId:t,since:m}}))},S=()=>{p||f!==null||(f=i(()=>{f=null,b(`connecting`),w()},d),d=Math.min(d*2,l))},C=e=>{e.onopen=null,e.onmessage=null,e.onclose=null,e.onerror=null;try{e.close()}catch{}u===e&&(u=null)},w=()=>{if(p)return;b(`connecting`);let i;try{i=new r(o(e))}catch{S();return}u=i,i.onopen=()=>{d=c,b(`connected`),x(i)},i.onmessage=e=>{let r;try{r=JSON.parse(String(e.data))}catch{return}if(r.type===`Session.event`&&a(r.data)){let e=r.data;if(typeof e.sessionId!=`string`||e.sessionId!==t||!Number.isSafeInteger(e.seq)||e.seq<0)return;let n=e.seq;if(n<_||v.has(n))return;let{seq:i,...a}=e;for(v.set(n,{seq:n,payload:a});v.has(_);){let e=v.get(_);v.delete(_),h?.(e),m=_,_+=1}return}if(r.type===`Runtime.writeLease`&&a(r.data)){let e=r.data;n.onWriteLease?.({sessionId:typeof e.sessionId==`string`?e.sessionId:t,state:e.state??`not-held`,reason:typeof e.reason==`string`?e.reason:void 0,leaseOwner:typeof e.leaseOwner==`string`?e.leaseOwner:void 0});return}if(r.type===`Runtime.error`&&a(r.data)){let e=r.data,t=typeof e.message==`string`?e.message:``;if(e.code===`EVENT_LOG_GAP`){g?.({reason:`gap`,oldestAvailable:_}),C(i),S();return}n.onNotify?.(`[service] ${e.code??`runtime`}: ${t}`)}},i.onclose=()=>{u===i&&(u=null),p?b(`closed`):(b(`reconnecting`),S())},i.onerror=()=>{}};return{fetchSnapshot:e=>n.fetchSnapshot(e),subscribe(e,t,n,r){m=t,h=n,g=r,m=Math.max(m,t),_=m+1;for(let e of v.keys())e<=m&&v.delete(e);return u?u.readyState===u.OPEN&&x():w(),()=>{p=!0,f!==null&&(s(f),f=null),b(`closed`),u&&C(u),h=null,g=null}},sendVerb(e,t){p||!u||u.readyState!==u.OPEN||u.send(JSON.stringify({method:e,params:t}))}}}async function c(a){let o=a.stdout??process.stdout,c=a.stdin??process.stdin,l=a.fetchImpl??globalThis.fetch,u=!0,d,f=e(a.baseUrl,l,{token:a.token}),p;try{p=await f.fetchSnapshot(a.sessionId)}catch(e){throw o.write(`otto --attach: 无法连接 serve 上的会话 ${a.sessionId}:${e instanceof Error?e.message:String(e)}\n`),e}let m=p.session.id,h,g=e=>{if(!h)return;let t={role:`assistant`,content:[{type:`text`,text:e}]};h.messages.pushMessage(t)};h=await i({stdin:c,stdout:o,columns:o.columns??100,needsSetup:!1,welcome:{cwd:void 0},onSubmit:e=>{let t=e.trimStart();if(t.startsWith(`/`)){g(`[attach] \`${t.split(/\s+/)[0]}\` 为 attach 暂未支持命令。\n仅支持纯文本提交(经 Chat.query 下发到 serve);如需本地命令请退出 attach 后运行。`);return}if(h){if(!u){g(`[attach] 会话的写权已被另一 client 持有(${d??`?`})——本端变只读观察者。请另选 session 或让对方退出 attach。`);return}(async()=>{try{await f.sendPrompt(m,e)}catch(e){g(`[attach] 提交失败:${e instanceof Error?e.message:String(e)}`)}})()}}});let _=s(a.baseUrl,m,{fetchSnapshot:async e=>await f.fetchSnapshot(e),onNotify:e=>g(`[attach] ${e}`),onStateChange:e=>{e===`connected`?(g(`[attach] 已连接 serve。`),a.forceWriteLease?(g(`[attach] --force-write-lease:向服务端发起 force_acquire(kill -9 解锁通道)……`),_.sendVerb(`Session.writeLease.force_acquire`,{sessionId:m})):_.sendVerb(`Session.writeLease.acquire`,{sessionId:m})):e===`reconnecting`?(u=!1,g(`[attach] 连接断开,正在重连……`)):e===`closed`&&(u=!1,g(`[attach] 连接已关闭。`))},onWriteLease:e=>{e.state===`acquired`?(u=!0,d=e.leaseOwner,g(`[attach] 获会话写权——纯文本会走 Chat.query 到 service。`)):e.state===`denied`?(u=!1,d=e.leaseOwner,g(`[attach] 会话写权被另一 client 持有(${e.leaseOwner??`?`})——本端变只读观察者态;提交纯文本会被本端予拦截。\n若对方是 kill -9 残留,可重启带 \`--force-write-lease\` 强抢写权。`)):e.state===`released`&&(u=!1,d=e.leaseOwner,e.reason===`forced_by_other_client`?g(`[attach] 写权被另一 client 强行剥夺(force_acquire by ${e.leaseOwner??`?`})——本端变只读观察者态。`):g(`[attach] 写权已释放——重新 conn 上可申请。`))}}),v=n(r({pushMessage:h.messages.pushMessage,echoUserInput:h.messages.echoUserInput,pushEngineEvent:h.engine.pushEngineEvent,getNotifyStats:h.info.getNotifyStats,setTasks:h.stores.tasks.setTasks,addTurnBoundary:h.stores.tasks.addTurnBoundary})),y=t({transport:_,sessionId:m,onSessionEvent:v.handle,hydrateMessages:(e,t)=>{e.messageStore.setState(()=>({messages:[]})),t.length>0&&e.messageStore.setState(()=>({messages:[...t]})),v.reset(t.length)},onGap:()=>{g(`[attach] 会话事件日志 gap(cursor 被 service 逐出)——已重新水合 messages 快照。`)}});await h.lifecycle.attachSessionSource(y),g(`[attach] 已连接会话 ${m}(host=${p.authority.hostname} pid=${p.authority.pid})。\nsnapshot messages 已灌入 store;后续 Session.event 经 WS 实时应用;纯文本走 Chat.query;/xxx 斜杠命令暂未支持。`)}export{c as runAttach};
|
|
3
|
-
//# sourceMappingURL=run-attach-
|
|
3
|
+
//# sourceMappingURL=run-attach-CvGCWF_i.js.map
|