@x-otto/cli 0.0.1-alpha.0
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/README.md +120 -0
- package/bin/otto.js +61 -0
- package/dist/auth-command-CXAB3_hs.js +11 -0
- package/dist/auth-command-CXAB3_hs.js.map +1 -0
- package/dist/build-id-BX4QHzM0.js +3 -0
- package/dist/build-id-BX4QHzM0.js.map +1 -0
- package/dist/bundled-extensions/plugin-otto-wire-protocols/otto-plugin.json +8 -0
- package/dist/bundled-extensions/plugin-otto-wire-protocols/plugin-dist/meta.json +1 -0
- package/dist/bundled-extensions/plugin-otto-wire-protocols/plugin-dist/plugin.cjs +60023 -0
- package/dist/bundled-extensions/plugin-otto-wire-protocols/plugin.ts +64 -0
- package/dist/bundled-extensions/plugin-otto-wire-protocols/src/anthropic-messages.ts +916 -0
- package/dist/bundled-extensions/plugin-otto-wire-protocols/src/anthropic-usage.ts +81 -0
- package/dist/bundled-extensions/plugin-otto-wire-protocols/src/openai-completions.ts +158 -0
- package/dist/bundled-extensions/plugin-otto-wire-protocols/src/openai-responses.ts +575 -0
- package/dist/bundled-extensions/plugin-skill-inductor/otto-plugin.json +8 -0
- package/dist/bundled-extensions/plugin-skill-inductor/plugin-dist/meta.json +1 -0
- package/dist/bundled-extensions/plugin-skill-inductor/plugin-dist/plugin.cjs +52781 -0
- package/dist/bundled-extensions/plugin-skill-inductor/plugin.ts +77 -0
- package/dist/bundled-extensions/plugin-skill-inductor/src/candidate-detector.ts +210 -0
- package/dist/bundled-extensions/plugin-skill-inductor/src/candidate-digest.ts +146 -0
- package/dist/bundled-extensions/plugin-skill-inductor/src/candidate-store.ts +87 -0
- package/dist/bundled-extensions/plugin-skill-inductor/src/episode-store.ts +122 -0
- package/dist/bundled-extensions/plugin-skill-inductor/src/episode.ts +125 -0
- package/dist/bundled-extensions/plugin-skill-inductor/src/extraction-prompt.ts +97 -0
- package/dist/bundled-extensions/plugin-skill-inductor/src/inductor-service.ts +183 -0
- package/dist/bundled-extensions/plugin-skill-inductor/src/similarity.ts +58 -0
- package/dist/command-dispatcher-BgStVcCF.js +13 -0
- package/dist/command-dispatcher-BgStVcCF.js.map +1 -0
- package/dist/config-migration-DatflIxs.js +21 -0
- package/dist/config-migration-DatflIxs.js.map +1 -0
- package/dist/daemon-COESl-zU.js +21 -0
- package/dist/daemon-COESl-zU.js.map +1 -0
- package/dist/daemon-base-YOWgYYN7.js +3 -0
- package/dist/daemon-base-YOWgYYN7.js.map +1 -0
- package/dist/debug-CiosFwme.js +34 -0
- package/dist/debug-CiosFwme.js.map +1 -0
- package/dist/discover-with-bundled-CznLjkkw.js +3 -0
- package/dist/discover-with-bundled-CznLjkkw.js.map +1 -0
- package/dist/doctor-C84-hhEr.js +30 -0
- package/dist/doctor-C84-hhEr.js.map +1 -0
- package/dist/doctor-attach-_zeIJewx.js +42 -0
- package/dist/doctor-attach-_zeIJewx.js.map +1 -0
- package/dist/doctor-cdp-K_mIFRfA.js +5 -0
- package/dist/doctor-cdp-K_mIFRfA.js.map +1 -0
- package/dist/doctor-heap-Rr2nNvcw.js +11 -0
- package/dist/doctor-heap-Rr2nNvcw.js.map +1 -0
- package/dist/doctor-ps-OL3FGQYu.js +8 -0
- package/dist/doctor-ps-OL3FGQYu.js.map +1 -0
- package/dist/extension-Bhzoel6s.js +169 -0
- package/dist/extension-Bhzoel6s.js.map +1 -0
- package/dist/extension-plugin-CnF6O_zK.js +7 -0
- package/dist/extension-plugin-CnF6O_zK.js.map +1 -0
- package/dist/extension-plugin-DWcakwRx.js +2 -0
- package/dist/extension-v-liRnUX.js +2 -0
- package/dist/feedback-BES_6ebi.js +21 -0
- package/dist/feedback-BES_6ebi.js.map +1 -0
- package/dist/feedback-BRbLUn9u.js +2 -0
- package/dist/feishu-setup-D6vnuNLw.js +6 -0
- package/dist/feishu-setup-D6vnuNLw.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/install-DlVJRHRT.js +11 -0
- package/dist/install-DlVJRHRT.js.map +1 -0
- package/dist/install-WCcQ9k9M.js +2 -0
- package/dist/mcp-DbTYT7hB.js +12 -0
- package/dist/mcp-DbTYT7hB.js.map +1 -0
- package/dist/migrate-boDXhEyV.js +14 -0
- package/dist/migrate-boDXhEyV.js.map +1 -0
- package/dist/model-auth-C27iG6Jq.js +3 -0
- package/dist/model-auth-C27iG6Jq.js.map +1 -0
- package/dist/model-auth-C4bsWvvn.js +2 -0
- package/dist/observe-CVg-MRM2.js +32 -0
- package/dist/observe-CVg-MRM2.js.map +1 -0
- package/dist/parse-cli-tFDOeVU1.js +3 -0
- package/dist/parse-cli-tFDOeVU1.js.map +1 -0
- package/dist/persistenced-CCwHu1Ww.js +36 -0
- package/dist/persistenced-CCwHu1Ww.js.map +1 -0
- package/dist/proxy-BvdBsB1O.js +2 -0
- package/dist/proxy-evg7ZvoD.js +3 -0
- package/dist/proxy-evg7ZvoD.js.map +1 -0
- package/dist/ps-DKrluEuy.js +9 -0
- package/dist/ps-DKrluEuy.js.map +1 -0
- package/dist/registry-B3nfN62q.js +2 -0
- package/dist/registry-D0NONjLv.js +6 -0
- package/dist/registry-D0NONjLv.js.map +1 -0
- package/dist/remote-persistence-server-B_hONoVR.js +35 -0
- package/dist/remote-persistence-server-B_hONoVR.js.map +1 -0
- package/dist/run-attach-Ci5jYyCa.js +3 -0
- package/dist/run-attach-Ci5jYyCa.js.map +1 -0
- package/dist/serve-pVR6UIT9.js +21 -0
- package/dist/serve-pVR6UIT9.js.map +1 -0
- package/dist/service-client-BLNeugM9.js +3 -0
- package/dist/service-client-BLNeugM9.js.map +1 -0
- package/dist/time-travel-ZozuPPI-.js +21 -0
- package/dist/time-travel-ZozuPPI-.js.map +1 -0
- package/dist/version-C0qM-qJM.js +3 -0
- package/dist/version-C0qM-qJM.js.map +1 -0
- package/package.json +61 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import{HIGH_RISK_CAPABILITIES as e,installPluginFromPackage as t,recordPluginInstalled as n}from"@x-otto/coding";import{OTTO_CONFIG_FILENAME as r,OTTO_HOME as i,OTTO_PROJECT_CONFIG_RELPATH as a,currentHostname as o}from"@x-otto/env";import{dirname as s,join as c,resolve as l}from"node:path";import{existsSync as u,mkdirSync as d,readFileSync as f,readdirSync as p,writeFileSync as m}from"node:fs";import{createInterface as h}from"node:readline/promises";import{ExportFileParseError as g,ExportFileVersionError as _,SettingsManager as v,applyMergeStrategy as y,buildExportPayload as b,classifyPluginChanges as x,diffConfig as S,hasSecuritySensitiveChanges as C,parseExportFile as w}from"@x-otto/setting";function T(e){let[t,...n]=e,r=[],i=new Set,a=new Map;for(let e=0;e<n.length;e++){let t=n[e];if(t===`-o`||t===`--output`){let t=n[++e];t&&a.set(`output`,t);continue}if(t===`--scope`){let t=n[++e];t&&a.set(`scope`,t);continue}if(t.startsWith(`--`)){i.add(t);continue}r.push(t)}return{sub:t,positional:r,flags:i,values:a}}function E(){return c(i,r)}function D(e){return c(e,a)}function O(e,t){return`${e}.bak-${t}`}function k(){return new Date().toISOString().replace(/[:.]/g,`-`)}async function A(e,t){let n=process.cwd(),r=e.flags.has(`--project`),i=r?`project`:`global`,a=r?D(n):E();if(r&&!u(a))return t.stderr(`项目级配置不存在:${a}\n`),1;let f=r?new v({workspaceDir:n}):new v({projectConfigPath:E()});await f.load();let p=b(f.config,i,o()),h=e.values.get(`output`)?l(e.values.get(`output`)):c(process.cwd(),`otto-config-export-${k()}.json`);return d(s(h),{recursive:!0}),m(h,JSON.stringify(p,null,2),`utf-8`),t.stdout(`配置已导出: ${h}\n`),0}function j(e,t,n){return` ${e}\n ${M(t)} → ${M(n)}`}function M(e){return e===void 0?`(未设置)`:Array.isArray(e)?`[${e.length} 项]`:typeof e==`object`&&e?JSON.stringify(e):String(e)}function N(e){let t=[],n=e.diffs;if(n.length>0){t.push(`将更新 ${n.length} 个字段:`);for(let e of n)t.push(j(e.key,e.from,e.to))}return e.pluginDiff.toInstall.length>0&&(t.push(`\n将安装 ${e.pluginDiff.toInstall.length} 个插件:`),t.push(` ${e.pluginDiff.toInstall.map(e=>e.id).join(`, `)}`)),e.pluginDiff.localOnly.length>0&&(t.push(`\n${e.pluginDiff.localOnly.length} 个本地插件不受影响(导入不卸载):`),t.push(` ${e.pluginDiff.localOnly.map(e=>e.id).join(`, `)}`)),t.join(`
|
|
3
|
+
`)}function P(t){return(t??[]).some(t=>e.includes(t))}async function F(e,t,n){let r=t.diffs.filter(e=>e.risk===`sensitive`),i=t.pluginDiff.toInstall.filter(e=>P(e.capabilities)),a=[`安全相关变更:`];for(let e of r)a.push(j(e.key,e.from,e.to));for(let e of i)a.push(` + ${e.id} 声明高危能力: ${e.capabilities?.join(`, `)}`);if(e.stdout(`${a.join(`
|
|
4
|
+
`)}\n`),n)return e.stdout(`(--yes 已跳过交互确认,全部接受)
|
|
5
|
+
`),!0;if(!process.stdin.isTTY)return e.stdout(`(非交互终端,安全相关变更自动接受。可用 --yes 显式声明该意图)
|
|
6
|
+
`),!0;let o=h({input:process.stdin,output:process.stdout});try{let e=(await o.question(`接受以上安全相关变更?[y/N] `)).trim().toLowerCase();return e===`y`||e===`yes`}finally{o.close()}}async function I(e,t){let n=O(e,k());if(u(e)){let t=f(e,`utf-8`);d(s(n),{recursive:!0}),m(n,t,`utf-8`)}else d(s(n),{recursive:!0}),m(n,``,`utf-8`);return t.stdout(`备份: ${n}\n`),n}async function L(e,r){let i=e.positional[0];if(!i)return r.stderr(`otto config import <file> [--dry-run] [--scope global|project] [--no-plugins] [--plugins-only] [--yes]
|
|
7
|
+
`),1;let a;try{a=f(l(i),`utf-8`)}catch(e){return r.stderr(`无法读取导入文件:${e instanceof Error?e.message:String(e)}\n`),1}let o;try{o=w(a)}catch(e){if(e instanceof _||e instanceof g)return r.stderr(`${e.message}\n`),1;throw e}let s=e.values.get(`scope`),c=s===`global`||s===`project`?s:o.scope,u=process.cwd(),d=c===`project`?D(u):E(),p=c===`project`?new v({workspaceDir:u}):new v({projectConfigPath:E()});await p.load();let m=e.flags.has(`--no-plugins`),h=e.flags.has(`--plugins-only`),b=e.flags.has(`--dry-run`),T=e.flags.has(`--yes`),O=h?{}:o.fields,k=S(p.config,O),A=x(p.get(`installed_plugins`)??[],m?[]:o.fields.installed_plugins??[]),j={diffs:k,pluginDiff:A,targetScope:c};if(k.length===0&&A.toInstall.length===0)return r.stdout(`配置已是最新(与导入文件零差异),无需变更
|
|
8
|
+
`),0;if(r.stdout(`${N(j)}\n`),b)return r.stdout(`
|
|
9
|
+
(--dry-run 预览完毕,未执行任何写入)
|
|
10
|
+
`),0;let M=!0;C(k,A)&&(await F(r,j,T)||(M=!1,r.stdout(`已跳过安全相关变更,继续导入其余安全字段
|
|
11
|
+
`))),await I(d,r);let L=y(k,M?`all`:`safe-only`);Object.keys(L).length>0&&await p.persist(L,c);let R=M?A.toInstall:A.toInstall.filter(e=>!P(e.capabilities)),z=M?[]:A.toInstall.filter(e=>P(e.capabilities));if(R.length>0){let e=c===`global`?p:new v({projectConfigPath:E()});c!==`global`&&await e.load();let i=[],a=[];for(let r of R)try{let o=r.source===`npm`?await t({pkg:r.sourceSpec||r.id}):void 0;if(!o){a.push({id:r.id,error:`不支持从导入文件自动安装来源 "${r.source}"`});continue}await n(e,{id:o.id,source:`npm`,sourceSpec:r.sourceSpec||r.id,version:o.version,scope:`global`,capabilities:r.capabilities}),i.push(o.id)}catch(e){a.push({id:r.id,error:e instanceof Error?e.message:String(e)})}i.length>0&&r.stdout(`✓ 已安装: ${i.join(`, `)}\n`);for(let e of a)r.stdout(`✗ 安装失败: ${e.id} — ${e.error} → otto extension install ${e.id}\n`)}return z.length>0&&r.stdout(`- 跳过(用户拒绝安全相关变更): ${z.map(e=>e.id).join(`, `)} → 可稍后手动安装: otto extension install <name>\n`),r.stdout(`
|
|
12
|
+
撤销: otto config restore
|
|
13
|
+
`),0}function R(e){let t=s(e),n=`${r}.bak-`;return u(t)?p(t).filter(e=>e.startsWith(n)).sort().reverse().map(e=>c(t,e)):[]}async function z(e,t){let n=e.flags.has(`--project`)?D(process.cwd()):E(),r=R(n);if(r.length===0)return t.stdout(`未找到任何备份
|
|
14
|
+
`),0;if(e.flags.has(`--list`))return t.stdout(`可用备份(最新在前):\n${r.map(e=>` ${e}`).join(`
|
|
15
|
+
`)}\n`),0;let i=r[0],a=f(i,`utf-8`);if(a===``){let{unlinkSync:e}=await import(`node:fs`);return u(n)&&e(n),t.stdout(`已恢复: ${n} 不存在(撤销前该文件本不存在)\n`),0}return d(s(n),{recursive:!0}),m(n,a,`utf-8`),t.stdout(`已恢复: ${n} ← ${i}\n`),0}async function B(e,t){let n=T(e);switch(n.sub){case`export`:return A(n,t);case`import`:return L(n,t);case`restore`:return z(n,t);default:return t.stdout(`otto config <命令>
|
|
16
|
+
export [--project] [-o <path>] 导出配置(默认用户级)
|
|
17
|
+
import <file> [--dry-run] [--scope global|project]
|
|
18
|
+
[--no-plugins] [--plugins-only] [--yes] 导入配置
|
|
19
|
+
restore [--list] [--project] 撤销最近一次导入
|
|
20
|
+
`),n.sub?1:0}}export{B as runConfigMigrationCommand};
|
|
21
|
+
//# sourceMappingURL=config-migration-DatflIxs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config-migration-DatflIxs.js","names":[],"sources":["../src/commands/config-migration.ts"],"sourcesContent":["/**\n * 配置迁移工具:settings → config 升级 + 备份 + dry-run。\n */\nimport { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs'\nimport { createInterface } from 'node:readline/promises'\nimport { dirname, join, resolve } from 'node:path'\nimport {\n OTTO_HOME,\n OTTO_CONFIG_FILENAME,\n OTTO_PROJECT_CONFIG_RELPATH,\n currentHostname,\n} from '@x-otto/env'\nimport {\n SettingsManager,\n buildExportPayload,\n parseExportFile,\n diffConfig,\n classifyPluginChanges,\n hasSecuritySensitiveChanges,\n applyMergeStrategy,\n ExportFileVersionError,\n ExportFileParseError,\n} from '@x-otto/setting'\nimport { installPluginFromPackage, recordPluginInstalled, HIGH_RISK_CAPABILITIES } from '@x-otto/coding'\nimport type { Setting, SettingKey } from '@x-otto/setting'\n\nexport interface ConfigMigrationIO {\n stdout: (s: string) => void\n stderr: (s: string) => void\n}\n\ninterface ParsedArgs {\n sub: string | undefined\n positional: string[]\n flags: Set<string>\n values: Map<string, string>\n}\n\nfunction parseArgs(args: string[]): ParsedArgs {\n const [sub, ...rest] = args\n const positional: string[] = []\n const flags = new Set<string>()\n const values = new Map<string, string>()\n for (let i = 0; i < rest.length; i++) {\n const a = rest[i]!\n if (a === '-o' || a === '--output') {\n const next = rest[++i]\n if (next) values.set('output', next)\n continue\n }\n if (a === '--scope') {\n const next = rest[++i]\n if (next) values.set('scope', next)\n continue\n }\n if (a.startsWith('--')) {\n flags.add(a)\n continue\n }\n positional.push(a)\n }\n return { sub, positional, flags, values }\n}\n\n// ── 路径解析 ──\n\nfunction globalConfigPath(): string {\n return join(OTTO_HOME, OTTO_CONFIG_FILENAME)\n}\n\nfunction projectConfigPath(cwd: string): string {\n return join(cwd, OTTO_PROJECT_CONFIG_RELPATH)\n}\n\nfunction backupPath(configPath: string, timestamp: string): string {\n return `${configPath}.bak-${timestamp}`\n}\n\nfunction timestampNow(): string {\n return new Date().toISOString().replace(/[:.]/g, '-')\n}\n\n// ── otto config export ──\n\nasync function runExport(parsed: ParsedArgs, io: ConfigMigrationIO): Promise<number> {\n const cwd = process.cwd()\n const useProject = parsed.flags.has('--project')\n const scope: 'global' | 'project' = useProject ? 'project' : 'global'\n const configPath = useProject ? projectConfigPath(cwd) : globalConfigPath()\n\n if (useProject && !existsSync(configPath)) {\n io.stderr(`项目级配置不存在:${configPath}\\n`)\n return 1\n }\n\n const settings = useProject\n ? new SettingsManager({ workspaceDir: cwd })\n : new SettingsManager({ projectConfigPath: globalConfigPath() })\n await settings.load()\n\n const payload = buildExportPayload(settings.config, scope, currentHostname())\n const outputPath = parsed.values.get('output')\n ? resolve(parsed.values.get('output')!)\n : join(process.cwd(), `otto-config-export-${timestampNow()}.json`)\n\n mkdirSync(dirname(outputPath), { recursive: true })\n writeFileSync(outputPath, JSON.stringify(payload, null, 2), 'utf-8')\n\n io.stdout(`配置已导出: ${outputPath}\\n`)\n return 0\n}\n\n// ── otto config import ──\n\n/** 两行/字段格式展示单个差异(RFC-219 设计定稿:抗长文本折行,不用表格对齐假设)。 */\nfunction formatFieldDiff(key: SettingKey, from: unknown, to: unknown): string {\n const fromStr = formatValue(from)\n const toStr = formatValue(to)\n return ` ${key}\\n ${fromStr} → ${toStr}`\n}\n\nfunction formatValue(value: unknown): string {\n if (value === undefined) return '(未设置)'\n if (Array.isArray(value)) return `[${value.length} 项]`\n if (typeof value === 'object' && value !== null) return JSON.stringify(value)\n return String(value)\n}\n\ninterface ImportPlan {\n diffs: ReturnType<typeof diffConfig>\n pluginDiff: ReturnType<typeof classifyPluginChanges>\n targetScope: 'global' | 'project'\n}\n\nfunction buildImportReport(plan: ImportPlan): string {\n const lines: string[] = []\n const changedNonPlugin = plan.diffs\n if (changedNonPlugin.length > 0) {\n lines.push(`将更新 ${changedNonPlugin.length} 个字段:`)\n for (const d of changedNonPlugin) {\n lines.push(formatFieldDiff(d.key, d.from, d.to))\n }\n }\n if (plan.pluginDiff.toInstall.length > 0) {\n lines.push(`\\n将安装 ${plan.pluginDiff.toInstall.length} 个插件:`)\n lines.push(` ${plan.pluginDiff.toInstall.map((p) => p.id).join(', ')}`)\n }\n if (plan.pluginDiff.localOnly.length > 0) {\n lines.push(`\\n${plan.pluginDiff.localOnly.length} 个本地插件不受影响(导入不卸载):`)\n lines.push(` ${plan.pluginDiff.localOnly.map((p) => p.id).join(', ')}`)\n }\n return lines.join('\\n')\n}\n\n/** 判定插件是否声明了真正的 `HIGH_RISK_CAPABILITIES`(对抗审核 P2 修复:不是\"任意非空 capabilities\")。 */\nfunction isHighRiskPlugin(capabilities: readonly string[] | undefined): boolean {\n return (capabilities ?? []).some((c) => (HIGH_RISK_CAPABILITIES as readonly string[]).includes(c))\n}\n\nasync function confirmSecuritySensitive(\n io: ConfigMigrationIO,\n plan: ImportPlan,\n yes: boolean,\n): Promise<boolean> {\n const sensitiveDiffs = plan.diffs.filter((d) => d.risk === 'sensitive')\n const sensitivePlugins = plan.pluginDiff.toInstall.filter((p) => isHighRiskPlugin(p.capabilities))\n\n const lines = ['安全相关变更:']\n for (const d of sensitiveDiffs) {\n lines.push(formatFieldDiff(d.key, d.from, d.to))\n }\n for (const p of sensitivePlugins) {\n lines.push(` + ${p.id} 声明高危能力: ${p.capabilities?.join(', ')}`)\n }\n io.stdout(`${lines.join('\\n')}\\n`)\n\n if (yes) {\n io.stdout('(--yes 已跳过交互确认,全部接受)\\n')\n return true\n }\n\n // 对抗审核 P1 修复:CI/管道场景无 TTY 时,readline.question() 会挂起等待 EOF 或产出\n // 不可预期结果——按 RFC-219 milestone M3 Grill 确认约定,无 TTY 时自动接受(`--yes`\n // 本就是\"信任本次导入\"的显式声明,无 TTY 环境视为等价于隐式给出该信号)。\n if (!process.stdin.isTTY) {\n io.stdout('(非交互终端,安全相关变更自动接受。可用 --yes 显式声明该意图)\\n')\n return true\n }\n\n const rl = createInterface({ input: process.stdin, output: process.stdout })\n try {\n const answer = (await rl.question('接受以上安全相关变更?[y/N] ')).trim().toLowerCase()\n return answer === 'y' || answer === 'yes'\n } finally {\n rl.close()\n }\n}\n\nasync function ensureBackup(configPath: string, io: ConfigMigrationIO): Promise<string> {\n const timestamp = timestampNow()\n const target = backupPath(configPath, timestamp)\n if (existsSync(configPath)) {\n const content = readFileSync(configPath, 'utf-8')\n mkdirSync(dirname(target), { recursive: true })\n writeFileSync(target, content, 'utf-8')\n } else {\n // 目标配置文件此前不存在(全新机器场景)——备份为空文件占位,restore 时按空文件处理\n // (等价于\"撤销即删除该配置文件\",见 runRestore 对空备份文件的处理)。\n mkdirSync(dirname(target), { recursive: true })\n writeFileSync(target, '', 'utf-8')\n }\n io.stdout(`备份: ${target}\\n`)\n return target\n}\n\nasync function runImport(parsed: ParsedArgs, io: ConfigMigrationIO): Promise<number> {\n const filePath = parsed.positional[0]\n if (!filePath) {\n io.stderr('otto config import <file> [--dry-run] [--scope global|project] [--no-plugins] [--plugins-only] [--yes]\\n')\n return 1\n }\n\n let raw: string\n try {\n raw = readFileSync(resolve(filePath), 'utf-8')\n } catch (err) {\n io.stderr(`无法读取导入文件:${err instanceof Error ? err.message : String(err)}\\n`)\n return 1\n }\n\n let exportFile: ReturnType<typeof parseExportFile>\n try {\n exportFile = parseExportFile(raw)\n } catch (err) {\n if (err instanceof ExportFileVersionError || err instanceof ExportFileParseError) {\n io.stderr(`${err.message}\\n`)\n return 1\n }\n throw err\n }\n\n // RFC-219 重要事项规则 4:scope 判定用确定性规则——默认遵循文件声明的 scope,\n // --scope 可显式覆盖,不做\"检测到歧义弹窗询问\"。\n const scopeOverride = parsed.values.get('scope')\n const targetScope: 'global' | 'project' =\n scopeOverride === 'global' || scopeOverride === 'project' ? scopeOverride : exportFile.scope\n\n const cwd = process.cwd()\n const configPath = targetScope === 'project' ? projectConfigPath(cwd) : globalConfigPath()\n const settings =\n targetScope === 'project'\n ? new SettingsManager({ workspaceDir: cwd })\n : new SettingsManager({ projectConfigPath: globalConfigPath() })\n await settings.load()\n\n const noPlugins = parsed.flags.has('--no-plugins')\n const pluginsOnly = parsed.flags.has('--plugins-only')\n const dryRun = parsed.flags.has('--dry-run')\n const yes = parsed.flags.has('--yes')\n\n // RFC-219 重要事项规则 5:installed_plugins 恒定路由 user 级配置,不受 targetScope 影响。\n const incomingFields = pluginsOnly ? {} : exportFile.fields\n const diffs = diffConfig(settings.config, incomingFields)\n const currentPlugins = settings.get('installed_plugins') ?? []\n const incomingPlugins = noPlugins ? [] : (exportFile.fields.installed_plugins ?? [])\n const pluginDiff = classifyPluginChanges(currentPlugins, incomingPlugins)\n\n const plan: ImportPlan = { diffs, pluginDiff, targetScope }\n\n // RFC-219 重要事项规则 3:零差异快路径。\n if (diffs.length === 0 && pluginDiff.toInstall.length === 0) {\n io.stdout('配置已是最新(与导入文件零差异),无需变更\\n')\n return 0\n }\n\n io.stdout(`${buildImportReport(plan)}\\n`)\n\n if (dryRun) {\n io.stdout('\\n(--dry-run 预览完毕,未执行任何写入)\\n')\n return 0\n }\n\n // RFC-219 D4:唯一合法确认——安全敏感变更批量门。\n let acceptAll = true\n if (hasSecuritySensitiveChanges(diffs, pluginDiff)) {\n const accepted = await confirmSecuritySensitive(io, plan, yes)\n if (!accepted) {\n acceptAll = false\n io.stdout('已跳过安全相关变更,继续导入其余安全字段\\n')\n }\n }\n\n // RFC-219 重要事项规则 6:强制自动备份(不可通过 flag 跳过)。\n await ensureBackup(configPath, io)\n\n // 应用配置字段变更。\n const toApply = applyMergeStrategy(diffs, acceptAll ? 'all' : 'safe-only')\n if (Object.keys(toApply).length > 0) {\n await settings.persist(toApply as Partial<Setting>, targetScope)\n }\n\n // installed_plugins 恒定路由 user 级(重要事项规则 5),与 targetScope 无关。\n // 对抗审核 P0 修复(安全门绕过):`acceptAll=false`(用户在安全门拒绝)时,声明\n // HIGH_RISK_CAPABILITIES 的插件必须被跳过——此前实现只对 applyMergeStrategy 应用了\n // 'safe-only',插件安装循环无条件跑全部 toInstall,等价于安全门形同虚设。\n const pluginsToInstall = acceptAll\n ? pluginDiff.toInstall\n : pluginDiff.toInstall.filter((p) => !isHighRiskPlugin(p.capabilities))\n const skippedHighRiskPlugins = acceptAll\n ? []\n : pluginDiff.toInstall.filter((p) => isHighRiskPlugin(p.capabilities))\n\n if (pluginsToInstall.length > 0) {\n const userSettings =\n targetScope === 'global' ? settings : new SettingsManager({ projectConfigPath: globalConfigPath() })\n if (targetScope !== 'global') await userSettings.load()\n\n const installed: string[] = []\n const failed: Array<{ id: string; error: string }> = []\n for (const entry of pluginsToInstall) {\n try {\n const res =\n entry.source === 'npm'\n ? await installPluginFromPackage({ pkg: entry.sourceSpec || entry.id })\n : undefined\n if (!res) {\n failed.push({ id: entry.id, error: `不支持从导入文件自动安装来源 \"${entry.source}\"` })\n continue\n }\n await recordPluginInstalled(userSettings, {\n id: res.id,\n source: 'npm',\n sourceSpec: entry.sourceSpec || entry.id,\n version: res.version,\n scope: 'global',\n capabilities: entry.capabilities,\n })\n installed.push(res.id)\n } catch (err) {\n failed.push({ id: entry.id, error: err instanceof Error ? err.message : String(err) })\n }\n }\n\n if (installed.length > 0) io.stdout(`✓ 已安装: ${installed.join(', ')}\\n`)\n for (const f of failed) {\n io.stdout(`✗ 安装失败: ${f.id} — ${f.error} → otto extension install ${f.id}\\n`)\n }\n }\n if (skippedHighRiskPlugins.length > 0) {\n io.stdout(\n `- 跳过(用户拒绝安全相关变更): ${skippedHighRiskPlugins.map((p) => p.id).join(', ')} ` +\n `→ 可稍后手动安装: otto extension install <name>\\n`,\n )\n }\n\n io.stdout(`\\n撤销: otto config restore\\n`)\n return 0\n}\n\n// ── otto config restore ──\n\nfunction listBackups(configPath: string): string[] {\n const dir = dirname(configPath)\n const prefix = `${OTTO_CONFIG_FILENAME}.bak-`\n if (!existsSync(dir)) return []\n return readdirSync(dir)\n .filter((f) => f.startsWith(prefix))\n .sort()\n .reverse()\n .map((f) => join(dir, f))\n}\n\nasync function runRestore(parsed: ParsedArgs, io: ConfigMigrationIO): Promise<number> {\n const useProject = parsed.flags.has('--project')\n const configPath = useProject ? projectConfigPath(process.cwd()) : globalConfigPath()\n const backups = listBackups(configPath)\n\n if (backups.length === 0) {\n io.stdout('未找到任何备份\\n')\n return 0\n }\n\n if (parsed.flags.has('--list')) {\n io.stdout(`可用备份(最新在前):\\n${backups.map((b) => ` ${b}`).join('\\n')}\\n`)\n return 0\n }\n\n const latest = backups[0]!\n const content = readFileSync(latest, 'utf-8')\n if (content === '') {\n // 空备份 = 撤销前配置文件本不存在(全新机器场景),撤销即删除。\n const { unlinkSync } = await import('node:fs')\n if (existsSync(configPath)) unlinkSync(configPath)\n io.stdout(`已恢复: ${configPath} 不存在(撤销前该文件本不存在)\\n`)\n return 0\n }\n mkdirSync(dirname(configPath), { recursive: true })\n writeFileSync(configPath, content, 'utf-8')\n io.stdout(`已恢复: ${configPath} ← ${latest}\\n`)\n return 0\n}\n\n// ── 入口 ──\n\nexport async function runConfigMigrationCommand(args: string[], io: ConfigMigrationIO): Promise<number> {\n const parsed = parseArgs(args)\n switch (parsed.sub) {\n case 'export':\n return runExport(parsed, io)\n case 'import':\n return runImport(parsed, io)\n case 'restore':\n return runRestore(parsed, io)\n default:\n io.stdout(\n 'otto config <命令>\\n' +\n ' export [--project] [-o <path>] 导出配置(默认用户级)\\n' +\n ' import <file> [--dry-run] [--scope global|project]\\n' +\n ' [--no-plugins] [--plugins-only] [--yes] 导入配置\\n' +\n ' restore [--list] [--project] 撤销最近一次导入\\n',\n )\n return parsed.sub ? 1 : 0\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import{spawn as e}from"node:child_process";import{OTTO_DAEMON_DIR as t,SERVICE_PORT as n,currentHostname as r,isProcessAlive as i,isShadowModeEnabled as a}from"@x-otto/env";import{join as o,resolve as s}from"node:path";import{mkdir as c,readFile as l,rm as u}from"node:fs/promises";import{randomUUID as d}from"node:crypto";import{atomicWriteFile as f}from"@x-otto/persistence";function p(e){return o(t,`${e}.json`)}function m(e){return Buffer.from(e).toString(`hex`)}async function h(e){a()||(await c(t,{recursive:!0}),await f(p(m(e.workspaceDir)),JSON.stringify(e,null,2)))}async function g(e){try{let t=await l(p(m(e)),`utf-8`),n=JSON.parse(t);return n.version!==1||typeof n.pid!=`number`||!Number.isFinite(n.pid)||n.pid<=0||typeof n.hostname!=`string`||typeof n.instanceToken!=`string`||!n.instanceToken?void 0:{inspect:!1,...n}}catch{return}}async function _(e){await u(p(m(e)),{force:!0})}function v(e){return e?e.hostname===r()?i(e.pid)?{status:`running`,state:e}:{status:`stale`,state:e,reason:`dead`}:{status:`stale`,state:e,reason:`foreign-host`}:{status:`not-running`}}async function y(e,t={}){let n=t.stdout??(e=>process.stdout.write(e)),r=t.stderr??(e=>process.stderr.write(e)),i=s(t.projectDir??process.cwd()),[a,...o]=e;switch(a){case`start`:return b(o,i,n,r,t);case`stop`:return x(i,n,r,t);case`status`:return S(i,n);case void 0:case`help`:case`--help`:return E(n),0;default:return r(`otto daemon: unknown action '${a}'. Use start | stop | status.\n`),1}}async function b(e,t,i,a,o){let s=v(await g(t));if(s.status===`running`)return a(`otto daemon: already running for this workspace (pid ${s.state.pid}, port ${s.state.port}).\nUse 'otto daemon stop' first, or 'otto daemon status' to inspect.\n`),1;s.status===`stale`&&await _(t);let c=T(e)??n,l=e.includes(`--inspect`);if(c===0)return a(`otto daemon: --port 0 is not supported (the shell cannot learn the kernel-assigned port).
|
|
3
|
+
Pick an explicit port, e.g. \`otto daemon start --port 4123\`.
|
|
4
|
+
`),1;let u=d(),f=d(),p=[`serve`,`--port`,String(c)];l&&p.push(`--inspect`);let m=o.spawnAuthority?o.spawnAuthority(p,{...process.env,OTTO_AUTHORITY_ID:f}):C(p,f,t);if(!m.pid)return a(`otto daemon: failed to spawn the authority process.
|
|
5
|
+
`),1;let y={version:1,pid:m.pid,hostname:r(),instanceToken:u,authorityId:f,port:c,workspaceDir:t,inspect:l,startedAt:new Date().toISOString()};return await h(y),m.unref(),i(`otto daemon started (pid ${y.pid}, port ${y.port})\n workspace: ${t}\n stop: otto daemon stop\n`),0}async function x(e,t,n,r){let i=v(await g(e));if(i.status===`not-running`)return t(`otto daemon: not running for this workspace.
|
|
6
|
+
`),0;if(i.status===`stale`)return await _(e),t(i.reason===`foreign-host`?`otto daemon: state belongs to host '${i.state.hostname}' — cleared locally, not signalled.\n`:`otto daemon: stale state cleared (process was not alive).
|
|
7
|
+
`),0;let{pid:a}=i.state,o=r.sendSignal??((e,t)=>process.kill(e,t)),s=r.waitForExit??w;try{o(a,`SIGTERM`)}catch(e){return n(`otto daemon: failed to signal pid ${a}: ${e instanceof Error?e.message:String(e)}\n`),1}return await s(a,1e4)?(await _(e),t(`otto daemon stopped (pid ${a}).\n`),0):(n(`otto daemon: pid ${a} did not exit within 10s after SIGTERM.\nState kept. Retry 'otto daemon stop', or inspect the process before forcing.\n`),1)}async function S(e,t){let n=v(await g(e));if(n.status===`not-running`)return t(`otto daemon: not running
|
|
8
|
+
`),0;if(n.status===`stale`)return t(`otto daemon: stale (${n.reason})\n recorded pid: ${n.state.pid}\n recorded host: ${n.state.hostname}\n run 'otto daemon stop' to clear\n`),0;let{state:r}=n;return t(`otto daemon: running\n pid: ${r.pid}\n port: ${r.port}\n authority: ${r.authorityId}\n debugger: ${r.inspect?`enabled (--inspect)`:`disabled`}\n workspace: ${r.workspaceDir}\n startedAt: ${r.startedAt}\n`),0}function C(t,n,r){let i=e(process.execPath,[process.argv[1]??``,...t],{cwd:r,detached:!0,stdio:`ignore`,env:{...process.env,OTTO_AUTHORITY_ID:n}});return{pid:i.pid??0,unref:()=>i.unref()}}async function w(e,t){let n=Date.now()+t;for(;Date.now()<n;){if(!i(e))return!0;await new Promise(e=>setTimeout(e,100))}return!i(e)}function T(e){let t=e.indexOf(`--port`);if(t===-1)return;let n=e[t+1];if(n===void 0||n.startsWith(`--`))return;let r=Number(n);return Number.isInteger(r)&&r>=0&&r<=65535?r:void 0}function E(e){e(`otto daemon — Session Authority 后台生命周期管理(RFC-335)
|
|
9
|
+
|
|
10
|
+
用法:
|
|
11
|
+
otto daemon start [--port <n>] [--inspect]
|
|
12
|
+
后台启动本 workspace 的 Session Authority
|
|
13
|
+
--inspect 启用调试器(Web Debug Console 需要)
|
|
14
|
+
otto daemon stop 安全停止(三重身份校验后发 SIGTERM)
|
|
15
|
+
otto daemon status 查看运行状态
|
|
16
|
+
|
|
17
|
+
说明:
|
|
18
|
+
daemon 不是独立的服务实现——它后台运行的就是 \`otto serve\`(同一个 CodingService /
|
|
19
|
+
SessionPool)。一个 workspace 同时只允许一个 Authority,避免同会话状态分叉。
|
|
20
|
+
`)}export{y as runDaemonCommand};
|
|
21
|
+
//# sourceMappingURL=daemon-COESl-zU.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"daemon-COESl-zU.js","names":[],"sources":["../src/commands/daemon-state.ts","../src/commands/daemon.ts"],"sourcesContent":["/**\n * daemon-state.ts — Session Authority daemon 的运行态文件(RFC-335 M2)。\n *\n * 职责:记录\"当前这台机器上,哪个进程正在充当某 workspace 的 Session Authority\",\n * 使 `otto daemon stop|status` 能找到并**安全地**操作它。\n *\n * ## 为什么不是简单的 PID 文件\n *\n * 裸 PID 文件有一个致命缺陷:PID 会被操作系统复用。daemon 崩溃后,同一个 PID 可能已经\n * 属于**完全无关的进程**(编辑器、构建任务、别人的服务)——此时 `stop` 会误杀它。\n * 这不是理论风险:本仓 RFC-196 已发生过同类 P0 事故(`otto ps --sweep` 误判租约、\n * 误杀活进程,见 `OTTO_SESSION_LOCKS_DIR` 注释)。\n *\n * 因此 stop 采用**双重身份校验**(见 `inspectDaemon`),顺序不可颠倒:\n * 1. hostname 一致——共享 `OTTO_HOME`(NFS/容器挂载)时,别的机器写的 pid 在本机指向\n * 的是无关进程,且很可能恰好存活;若先判存活会得出\"运行中\"的错误结论并误杀。\n * 2. pid 存活(`isProcessAlive`)——不存活即 stale,直接清理不杀。\n *\n * 任一不满足即拒绝发信号(只清理 stale state),宁可让用户手动处理,也不误杀。\n *\n * ## instanceToken 的现状(不夸大其作用)\n *\n * `instanceToken` 目前**仅被写入与格式校验,未参与 stop 判定**——真正的 PID 复用防护\n * 来自上面的双重校验。要让 token 发挥\"PID 复用时新进程不可能持有它\"的作用,需要一条\n * 让目标进程自报 token 的通道(如 authority HTTP 端点回报 instanceToken 后比对),\n * 那属于 M3 客户端协议范围。在此之前不应宣称它是校验环节。\n *\n * ## 与 authority identity 的关系\n *\n * `authorityId` 是 service 侧 `AuthorityDescriptor.instanceId`(RFC-335 M1),写进 state\n * 使客户端能验证\"我连的 HTTP authority\"与\"state 里记录的 daemon\"是同一个实例。\n * 它**不是**凭证,不授予任何能力。\n */\nimport { mkdir, readFile, rm } from 'node:fs/promises'\nimport { join } from 'node:path'\nimport { OTTO_DAEMON_DIR, currentHostname, isProcessAlive, isShadowModeEnabled } from '@x-otto/env'\nimport { atomicWriteFile } from '@x-otto/persistence'\n\n/** 磁盘上的 daemon 运行态。全部字段可序列化,且不含任何凭证。 */\nexport interface DaemonState {\n /** 协议版本——未来结构变更时用于安全拒绝无法理解的旧/新格式,而非误读。 */\n version: 1\n pid: number\n /** 写入该 state 的机器;跨机共享 OTTO_HOME 时用于拒绝跨机操作。 */\n hostname: string\n /**\n * daemon 自生成的一次性随机标识。**非凭证**,且当前**未参与 stop 校验**\n * (见文件头注释\"instanceToken 的现状\")——预留给 M3 的进程自报比对。\n */\n instanceToken: string\n /** service 侧 AuthorityDescriptor.instanceId(RFC-335 M1),供客户端交叉验证。 */\n authorityId: string\n /**\n * **请求**的监听端口,不保证等于实际监听端口。\n *\n * `--port 0` 时内核会分配随机端口,而本字段记录的仍是 0——daemon 壳无法得知子进程\n * 的真实绑定结果(需要 IPC 或 serve 侧回写 state,属 M3 范围)。故 `--port 0` 下\n * `status` 显示的端口不可用于连接;固定端口(默认 `SERVICE_PORT`)无此问题。\n */\n port: number\n workspaceDir: string\n /**\n * 该 daemon 是否以 `--inspect` 启动(调试器/Web Debug Console 是否可用)。\n * 记入 state 使 `status` 能回答\"当前 daemon 能不能调试\"——否则用户没有任何工具\n * 确认自己启动的 daemon 带没带 flag,与 Web Console 的 503 指引形成盲区。\n */\n inspect: boolean\n startedAt: string\n}\n\n/** state 文件路径按 workspace 分隔——一台机器可为多个 workspace 各跑一个 authority。 */\nexport function daemonStatePath(workspaceKey: string): string {\n return join(OTTO_DAEMON_DIR, `${workspaceKey}.json`)\n}\n\n/**\n * 把 workspace 路径映射为文件名安全的 key。\n *\n * 用十六进制编码而非替换非法字符:`/a/b` 与 `/a_b` 在\"替换法\"下会撞成同一个 key,\n * 导致两个 workspace 互相覆盖 state(进而 stop 误杀对方的 daemon)。编码是单射的。\n */\nexport function workspaceKeyOf(workspaceDir: string): string {\n return Buffer.from(workspaceDir).toString('hex')\n}\n\nexport async function writeDaemonState(state: DaemonState): Promise<void> {\n // RFC-345 §D9:影子会话不参与 daemon 竞选、不写 daemon state——避免暴露\"otto 曾在此运行\"\n // 的痕迹,也不与普通进程的跨进程协调纠缠。\n if (isShadowModeEnabled()) {\n return\n }\n await mkdir(OTTO_DAEMON_DIR, { recursive: true })\n // 原子写:崩溃/并发下不产生半截 JSON——读到损坏 state 会让 stop 无法识别自己的进程。\n await atomicWriteFile(daemonStatePath(workspaceKeyOf(state.workspaceDir)), JSON.stringify(state, null, 2))\n}\n\n/** 读取 state。文件不存在/损坏/版本不符一律返回 undefined(安全降级,不抛)。 */\nexport async function readDaemonState(workspaceDir: string): Promise<DaemonState | undefined> {\n try {\n const raw = await readFile(daemonStatePath(workspaceKeyOf(workspaceDir)), 'utf-8')\n const parsed = JSON.parse(raw) as Partial<DaemonState>\n if (parsed.version !== 1) return undefined\n if (\n typeof parsed.pid !== 'number' ||\n !Number.isFinite(parsed.pid) ||\n parsed.pid <= 0 ||\n typeof parsed.hostname !== 'string' ||\n typeof parsed.instanceToken !== 'string' ||\n !parsed.instanceToken\n ) {\n return undefined\n }\n return { inspect: false, ...parsed } as DaemonState\n } catch {\n return undefined\n }\n}\n\nexport async function clearDaemonState(workspaceDir: string): Promise<void> {\n await rm(daemonStatePath(workspaceKeyOf(workspaceDir)), { force: true })\n}\n\nexport type DaemonLiveness =\n | { status: 'running'; state: DaemonState }\n /** state 存在但进程已死/属于别的机器 —— 可安全清理,**不可**据此杀 pid。 */\n | { status: 'stale'; state: DaemonState; reason: 'dead' | 'foreign-host' }\n | { status: 'not-running' }\n\n/**\n * 判定 state 描述的 daemon 是否真的活着。\n *\n * 注意顺序:先判 hostname 再判存活——跨机共享 OTTO_HOME 时,本机同号 pid 可能恰好存活,\n * 若先判存活会得出\"运行中\"的错误结论,并在 stop 时误杀本机无关进程。\n */\nexport function inspectDaemon(state: DaemonState | undefined): DaemonLiveness {\n if (!state) return { status: 'not-running' }\n if (state.hostname !== currentHostname()) {\n return { status: 'stale', state, reason: 'foreign-host' }\n }\n if (!isProcessAlive(state.pid)) {\n return { status: 'stale', state, reason: 'dead' }\n }\n return { status: 'running', state }\n}\n","/**\n * `otto daemon` 子命令 —— Session Authority 的生命周期外壳(RFC-335 M2)。\n *\n * **进程模型(RFC-335 D1,已定稿)**:daemon **不是**第二套服务内核。它只是把已有的\n * `otto serve`(`CodingService`,唯一 Session Authority)放到后台并管理其生命周期。\n * 全仓只有一个 SessionPool 实现、一个事件桥、一套 WS 协议——绝不复制。\n *\n * 之所以需要这层壳:`otto serve` 在前台运行,关掉终端即结束;而\"关终端后会话继续跑\"\n * 是 daemon 的唯一差异化价值(RFC-234 §1.3 演进③)。壳负责 detach、记录运行态、\n * 安全停止。\n *\n * ## stop 的安全模型\n *\n * 见 `daemon-state.ts` 头注释:PID 会被 OS 复用,裸 PID 文件会误杀无关进程(本仓\n * RFC-196 有同类 P0 事故)。故 stop 必须三重校验(pid 存活 + hostname + instanceToken)\n * 全部通过才发信号,否则只清理 stale state。\n */\nimport { spawn } from 'node:child_process'\nimport { randomUUID } from 'node:crypto'\nimport { resolve } from 'node:path'\nimport { SERVICE_PORT } from '@x-otto/env'\nimport { currentHostname, isProcessAlive } from '@x-otto/env'\nimport {\n clearDaemonState,\n inspectDaemon,\n readDaemonState,\n writeDaemonState,\n type DaemonState,\n} from './daemon-state'\n\nexport interface DaemonDeps {\n stdout?: (s: string) => void\n stderr?: (s: string) => void\n projectDir?: string\n /** 测试注入:替代真实 spawn(返回被启动进程的 pid)。 */\n spawnAuthority?: (args: string[], env: NodeJS.ProcessEnv) => { pid: number; unref: () => void }\n /** 测试注入:替代 `process.kill` 发送停止信号。 */\n sendSignal?: (pid: number, signal: NodeJS.Signals) => void\n /** 测试注入:等待进程退出的轮询实现。 */\n waitForExit?: (pid: number, timeoutMs: number) => Promise<boolean>\n}\n\nexport async function runDaemonCommand(args: string[], deps: DaemonDeps = {}): Promise<number> {\n const stdout = deps.stdout ?? ((s) => process.stdout.write(s))\n const stderr = deps.stderr ?? ((s) => process.stderr.write(s))\n const workspaceDir = resolve(deps.projectDir ?? process.cwd())\n\n const [action, ...rest] = args\n\n switch (action) {\n case 'start':\n return startDaemon(rest, workspaceDir, stdout, stderr, deps)\n case 'stop':\n return stopDaemon(workspaceDir, stdout, stderr, deps)\n case 'status':\n return statusDaemon(workspaceDir, stdout)\n case undefined:\n case 'help':\n case '--help':\n printHelp(stdout)\n return 0\n default:\n stderr(`otto daemon: unknown action '${action}'. Use start | stop | status.\\n`)\n return 1\n }\n}\n\nasync function startDaemon(\n args: string[],\n workspaceDir: string,\n stdout: (s: string) => void,\n stderr: (s: string) => void,\n deps: DaemonDeps,\n): Promise<number> {\n const existing = inspectDaemon(await readDaemonState(workspaceDir))\n\n // 已在运行 → 拒绝启动第二个。两个 authority 各持一份同 id 的 AgentSession 会造成\n // 状态分叉(RFC-234 §1.3 场景B,写租约只防丢数据、防不了分叉),这是架构禁止项。\n if (existing.status === 'running') {\n stderr(\n `otto daemon: already running for this workspace (pid ${existing.state.pid}, port ${existing.state.port}).\\n` +\n `Use 'otto daemon stop' first, or 'otto daemon status' to inspect.\\n`,\n )\n return 1\n }\n // stale state(进程已死或属别的机器)→ 清理后继续,不阻塞用户。\n if (existing.status === 'stale') {\n await clearDaemonState(workspaceDir)\n }\n\n const port = parsePort(args) ?? SERVICE_PORT\n // `--inspect` 必须能透传到后台 serve:daemon 是\"关终端后继续跑\"的推荐姿态,\n // 若它无法启用调试器,Web Debug Console 在最常用的模式下永远 503(评审 P0)。\n const inspect = args.includes('--inspect')\n\n // `--port 0` 会让内核分配随机端口,而 daemon 壳拿不到子进程的真实绑定结果\n // (需 IPC/serve 回写 state,属 M3)。此时 state 里记的 0 对 status 与客户端连接\n // 毫无用处 —— 与其静默写入一个不可用端口,不如直接拒绝并给出可行替代。\n if (port === 0) {\n stderr(\n 'otto daemon: --port 0 is not supported (the shell cannot learn the kernel-assigned port).\\n' +\n 'Pick an explicit port, e.g. `otto daemon start --port 4123`.\\n',\n )\n return 1\n }\n const instanceToken = randomUUID()\n const authorityId = randomUUID()\n\n // authorityId 经环境变量下传,使 service 的 AuthorityDescriptor.instanceId 与 state\n // 记录一致——客户端据此验证\"连上的 authority\"就是\"state 里的 daemon\"。\n const serveArgs = ['serve', '--port', String(port)]\n if (inspect) serveArgs.push('--inspect')\n const child = deps.spawnAuthority\n ? deps.spawnAuthority(serveArgs, { ...process.env, OTTO_AUTHORITY_ID: authorityId })\n : spawnDetached(serveArgs, authorityId, workspaceDir)\n\n if (!child.pid) {\n stderr('otto daemon: failed to spawn the authority process.\\n')\n return 1\n }\n\n const state: DaemonState = {\n version: 1,\n pid: child.pid,\n hostname: currentHostname(),\n instanceToken,\n authorityId,\n port,\n workspaceDir,\n inspect,\n startedAt: new Date().toISOString(),\n }\n await writeDaemonState(state)\n child.unref()\n\n stdout(\n `otto daemon started (pid ${state.pid}, port ${state.port})\\n` +\n ` workspace: ${workspaceDir}\\n` +\n ` stop: otto daemon stop\\n`,\n )\n return 0\n}\n\nasync function stopDaemon(\n workspaceDir: string,\n stdout: (s: string) => void,\n stderr: (s: string) => void,\n deps: DaemonDeps,\n): Promise<number> {\n const state = await readDaemonState(workspaceDir)\n const liveness = inspectDaemon(state)\n\n if (liveness.status === 'not-running') {\n stdout('otto daemon: not running for this workspace.\\n')\n return 0\n }\n\n // stale:**绝不**对该 pid 发信号——它可能已被 OS 复用给无关进程(RFC-196 类事故)。\n if (liveness.status === 'stale') {\n await clearDaemonState(workspaceDir)\n stdout(\n liveness.reason === 'foreign-host'\n ? `otto daemon: state belongs to host '${liveness.state.hostname}' — cleared locally, not signalled.\\n`\n : 'otto daemon: stale state cleared (process was not alive).\\n',\n )\n return 0\n }\n\n const { pid } = liveness.state\n const sendSignal = deps.sendSignal ?? ((p, sig) => process.kill(p, sig))\n const waitForExit = deps.waitForExit ?? defaultWaitForExit\n\n try {\n sendSignal(pid, 'SIGTERM')\n } catch (error) {\n stderr(`otto daemon: failed to signal pid ${pid}: ${error instanceof Error ? error.message : String(error)}\\n`)\n return 1\n }\n\n const exited = await waitForExit(pid, 10_000)\n if (!exited) {\n // 不自动升级到 SIGKILL:authority 持有会话与写租约,强杀可能留下未落盘状态。\n // 交由用户决定,并保留 state 以便再次尝试。\n stderr(\n `otto daemon: pid ${pid} did not exit within 10s after SIGTERM.\\n` +\n `State kept. Retry 'otto daemon stop', or inspect the process before forcing.\\n`,\n )\n return 1\n }\n\n await clearDaemonState(workspaceDir)\n stdout(`otto daemon stopped (pid ${pid}).\\n`)\n return 0\n}\n\nasync function statusDaemon(workspaceDir: string, stdout: (s: string) => void): Promise<number> {\n const liveness = inspectDaemon(await readDaemonState(workspaceDir))\n\n if (liveness.status === 'not-running') {\n stdout('otto daemon: not running\\n')\n return 0\n }\n if (liveness.status === 'stale') {\n stdout(\n `otto daemon: stale (${liveness.reason})\\n` +\n ` recorded pid: ${liveness.state.pid}\\n` +\n ` recorded host: ${liveness.state.hostname}\\n` +\n ` run 'otto daemon stop' to clear\\n`,\n )\n return 0\n }\n\n const { state } = liveness\n stdout(\n `otto daemon: running\\n` +\n ` pid: ${state.pid}\\n` +\n ` port: ${state.port}\\n` +\n ` authority: ${state.authorityId}\\n` +\n ` debugger: ${state.inspect ? 'enabled (--inspect)' : 'disabled'}\\n` +\n ` workspace: ${state.workspaceDir}\\n` +\n ` startedAt: ${state.startedAt}\\n`,\n )\n return 0\n}\n\nfunction spawnDetached(\n serveArgs: string[],\n authorityId: string,\n workspaceDir: string,\n): { pid: number; unref: () => void } {\n const child = spawn(process.execPath, [process.argv[1] ?? '', ...serveArgs], {\n cwd: workspaceDir,\n detached: true,\n // 关终端后继续运行:stdio 必须与终端解耦,否则父终端关闭会连带终止子进程。\n stdio: 'ignore',\n env: { ...process.env, OTTO_AUTHORITY_ID: authorityId },\n })\n return { pid: child.pid ?? 0, unref: () => child.unref() }\n}\n\nasync function defaultWaitForExit(pid: number, timeoutMs: number): Promise<boolean> {\n const deadline = Date.now() + timeoutMs\n while (Date.now() < deadline) {\n if (!isProcessAlive(pid)) return true\n await new Promise((r) => setTimeout(r, 100))\n }\n return !isProcessAlive(pid)\n}\n\nfunction parsePort(args: string[]): number | undefined {\n const i = args.indexOf('--port')\n if (i === -1) return undefined\n const raw = args[i + 1]\n if (raw === undefined || raw.startsWith('--')) return undefined\n const n = Number(raw)\n return Number.isInteger(n) && n >= 0 && n <= 65535 ? n : undefined\n}\n\nfunction printHelp(stdout: (s: string) => void): void {\n stdout(`otto daemon — Session Authority 后台生命周期管理(RFC-335)\n\n用法:\n otto daemon start [--port <n>] [--inspect]\n 后台启动本 workspace 的 Session Authority\n --inspect 启用调试器(Web Debug Console 需要)\n otto daemon stop 安全停止(三重身份校验后发 SIGTERM)\n otto daemon status 查看运行状态\n\n说明:\n daemon 不是独立的服务实现——它后台运行的就是 \\`otto serve\\`(同一个 CodingService /\n SessionPool)。一个 workspace 同时只允许一个 Authority,避免同会话状态分叉。\n`)\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
function e(e){return new Promise(t=>{if(e){if(e.aborted){t();return}e.addEventListener(`abort`,()=>t(),{once:!0});return}let n=()=>{process.off(`SIGINT`,n),process.off(`SIGTERM`,n),t()};process.on(`SIGINT`,n),process.on(`SIGTERM`,n)})}export{e as t};
|
|
3
|
+
//# sourceMappingURL=daemon-base-YOWgYYN7.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"daemon-base-YOWgYYN7.js","names":[],"sources":["../src/commands/daemon-base.ts"],"sourcesContent":["/**\n * daemon-base.ts — 常驻进程启动器共享基座。\n *\n * `serve`/`persistenced`/`remote-persistence-server` 三个子命令各自启动一个\n * 常驻服务进程,生命周期形状相同(启动 → 打印监听地址 → 等待信号 → 优雅关闭),\n * 唯一差异是启动的具体服务。`waitForShutdown` 此前在三个文件里逐字重复(终局架构\n * review 发现),此处收敛为单一真源。\n */\n\n/**\n * 等待进程收到关闭信号(外部 AbortSignal,或退化到监听 SIGINT/SIGTERM)。\n *\n * `signal` 由测试注入(模拟主动关闭);生产路径缺省走 SIGINT/SIGTERM,\n * 监听器在触发后自行摘除,不留悬挂 listener。\n */\nexport function waitForShutdown(signal?: AbortSignal): Promise<void> {\n return new Promise((resolve) => {\n if (signal) {\n if (signal.aborted) {\n resolve()\n return\n }\n signal.addEventListener('abort', () => resolve(), { once: true })\n return\n }\n const onSignal = () => {\n process.off('SIGINT', onSignal)\n process.off('SIGTERM', onSignal)\n resolve()\n }\n process.on('SIGINT', onSignal)\n process.on('SIGTERM', onSignal)\n })\n}\n"],"mappings":";AAeA,SAAgB,EAAgB,EAAqC,CACnE,OAAO,IAAI,QAAS,GAAY,CAC9B,GAAI,EAAQ,CACV,GAAI,EAAO,QAAS,CAClB,GAAS,CACT,OAEF,EAAO,iBAAiB,YAAe,GAAS,CAAE,CAAE,KAAM,GAAM,CAAC,CACjE,OAEF,IAAM,MAAiB,CACrB,QAAQ,IAAI,SAAU,EAAS,CAC/B,QAAQ,IAAI,UAAW,EAAS,CAChC,GAAS,EAEX,QAAQ,GAAG,SAAU,EAAS,CAC9B,QAAQ,GAAG,UAAW,EAAS,EAC/B"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import{a as e,i as t,r as n}from"./service-client-BLNeugM9.js";import{BREAKPOINT_POINTS as r}from"@x-otto/agent";const i=r.map(e=>e.point);async function a(r,a={}){let s=a.stdout??(e=>process.stdout.write(e)),d=a.stderr??(e=>process.stderr.write(e)),f,p;try{({baseUrl:f,rest:p}=e(r))}catch(e){return t(`debug`,e,d)}let[m,...h]=p;if(!m||m===`help`)return s(`otto debug — 调试面板(断点管理 / 状态查看)
|
|
3
|
+
|
|
4
|
+
用法:
|
|
5
|
+
otto debug breakpoints list 列出所有断点及启用状态
|
|
6
|
+
otto debug breakpoints set <point> 设置断点(默认启用)
|
|
7
|
+
otto debug breakpoints remove <point> 移除断点
|
|
8
|
+
otto debug breakpoints enable <point> 启用断点
|
|
9
|
+
otto debug breakpoints disable <point> 禁用断点
|
|
10
|
+
otto debug breakpoints enable-all 全部启用
|
|
11
|
+
otto debug breakpoints disable-all 全部禁用
|
|
12
|
+
otto debug points 列出所有可用断点位
|
|
13
|
+
otto debug state 调试器状态(断点统计 + 挂起的暂停)
|
|
14
|
+
|
|
15
|
+
选项:
|
|
16
|
+
--remote <url> service 地址(默认 OTTO_SERVICE_URL 或 http:
|
|
17
|
+
|
|
18
|
+
断点位(BREAKPOINT_POINTS):
|
|
19
|
+
${i.map(e=>` ${e}`).join(`
|
|
20
|
+
`)}
|
|
21
|
+
|
|
22
|
+
注:需要 \`otto serve --inspect\` 启动的 service(调试器默认关闭)。
|
|
23
|
+
resume/inject 等运行时命令经 REST:POST /api/debugger/resume | /inject。
|
|
24
|
+
`),0;let g=n(f,a.fetch);try{switch(m){case`breakpoints`:{let e=h[0];switch(e){case`list`:return await o(g,s);case`set`:return await c(g,h[1],!0,s,d);case`remove`:return await c(g,h[1],!1,s,d,`removed`);case`enable`:return await c(g,h[1],!0,s,d,`enabled`);case`disable`:return await c(g,h[1],!1,s,d,`disabled`);case`enable-all`:return await g.post(`/api/debugger/breakpoints/enable-all`),s(`All breakpoints enabled.
|
|
25
|
+
`),0;case`disable-all`:return await g.post(`/api/debugger/breakpoints/disable-all`),s(`All breakpoints disabled.
|
|
26
|
+
`),0;default:return d(`otto debug breakpoints: unknown sub-action '${e}'.\n`),1}}case`points`:return await l(g,s);case`state`:return await u(g,s);default:return d(`otto debug: unknown action '${m}'. Use breakpoints | points | state.\n`),1}}catch(e){return t(`debug`,e,d)}}async function o(e,t){let{breakpoints:n}=await e.get(`/api/debugger/breakpoints`);if(n.length===0)return t(`No breakpoints set.
|
|
27
|
+
`),0;t(`${`point`.padEnd(36)} enabled\n${`-`.repeat(46)}\n`);for(let e of n)t(`${e.point.padEnd(36)} ${e.enabled?`✓`:`✗`}\n`);return 0}function s(e){return!!e&&i.includes(e)}async function c(e,t,n,r,i,a){return s(t)?(await e.put(`/api/debugger/breakpoints/${t}`,{enabled:n}),r(`Breakpoint ${a??`set: ${t} (${n?`enabled`:`disabled`})`}${a?`: ${t}`:``}\n`),0):(i(`otto debug: invalid or missing breakpoint point.
|
|
28
|
+
Use 'otto debug points' to list valid points.
|
|
29
|
+
`),1)}async function l(e,t){let{breakpoints:n}=await e.get(`/api/debugger/breakpoints`),i=new Map(n.map(e=>[e.point,e]));t(`Available breakpoint points:
|
|
30
|
+
`);for(let e of r){let n=i.get(e.point),r=n?.set?n.enabled?`enabled`:`disabled`:`not set`;t(` ${e.point.padEnd(36)} ${r}\n`)}return 0}async function u(e,t){let n=await e.get(`/api/debugger/state`);t(`Debugger state
|
|
31
|
+
breakpoints: ${n.breakpoints.enabled}/${n.breakpoints.total} enabled
|
|
32
|
+
paused: ${n.paused?`yes`:`no`}
|
|
33
|
+
`);for(let e of n.pendingPauses)t(` ⏸ ${e.point} (turn ${e.turn}) pauseId=${e.pauseId}\n`);return 0}export{a as runDebugCommand};
|
|
34
|
+
//# sourceMappingURL=debug-CiosFwme.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"debug-CiosFwme.js","names":[],"sources":["../src/commands/debug.ts"],"sourcesContent":["/**\n * `otto debug` 子命令 —— 调试面板:断点管理 / 状态查看。\n *\n * 设计依据:design/observability-platform.md §4/§5 + design/m17-platform-activation.md §1/§3。\n * 数据源 = service REST(/api/debugger 路由;此前各自起 in-memory 进程,\n * 断点状态随进程消失、对运行中引擎无效)。默认连 `otto serve --inspect`\n * 实例,`--remote <url>` 覆盖。断点缺省 disabled,设置即对运行中会话生效。\n */\nimport { BREAKPOINT_POINTS } from '@x-otto/agent'\nimport {\n createServiceClient,\n reportServiceError,\n resolveRemote,\n type ServiceClient,\n} from './service-client'\n\nconst POINT_NAMES: readonly string[] = BREAKPOINT_POINTS.map((d) => d.point)\n\nexport interface DebugDeps {\n stdout?: (s: string) => void\n stderr?: (s: string) => void\n /** fetch 注入(测试用)。 */\n fetch?: typeof globalThis.fetch\n}\n\ninterface BreakpointEntry {\n point: string\n enabled: boolean\n set?: boolean\n}\n\nexport async function runDebugCommand(\n args: string[],\n deps: DebugDeps = {},\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 let baseUrl: string\n let rest: string[]\n try {\n ;({ baseUrl, rest } = resolveRemote(args))\n } catch (error) {\n return reportServiceError('debug', error, stderr)\n }\n\n const [action, ...sub] = rest\n\n if (!action || action === 'help') {\n stdout(`otto debug — 调试面板(断点管理 / 状态查看)\n\n用法:\n otto debug breakpoints list 列出所有断点及启用状态\n otto debug breakpoints set <point> 设置断点(默认启用)\n otto debug breakpoints remove <point> 移除断点\n otto debug breakpoints enable <point> 启用断点\n otto debug breakpoints disable <point> 禁用断点\n otto debug breakpoints enable-all 全部启用\n otto debug breakpoints disable-all 全部禁用\n otto debug points 列出所有可用断点位\n otto debug state 调试器状态(断点统计 + 挂起的暂停)\n\n选项:\n --remote <url> service 地址(默认 OTTO_SERVICE_URL 或 http:\n\n断点位(BREAKPOINT_POINTS):\n${POINT_NAMES.map((p) => ` ${p}`).join('\\n')}\n\n注:需要 \\`otto serve --inspect\\` 启动的 service(调试器默认关闭)。\nresume/inject 等运行时命令经 REST:POST /api/debugger/resume | /inject。\n`)\n return 0\n }\n\n const client = createServiceClient(baseUrl, deps.fetch)\n\n try {\n switch (action) {\n case 'breakpoints': {\n const op = sub[0]\n switch (op) {\n case 'list':\n return await listBreakpoints(client, stdout)\n case 'set':\n return await setBreakpoint(client, sub[1], true, stdout, stderr)\n case 'remove':\n return await setBreakpoint(client, sub[1], false, stdout, stderr, 'removed')\n case 'enable':\n return await setBreakpoint(client, sub[1], true, stdout, stderr, 'enabled')\n case 'disable':\n return await setBreakpoint(client, sub[1], false, stdout, stderr, 'disabled')\n case 'enable-all':\n await client.post('/api/debugger/breakpoints/enable-all')\n stdout('All breakpoints enabled.\\n')\n return 0\n case 'disable-all':\n await client.post('/api/debugger/breakpoints/disable-all')\n stdout('All breakpoints disabled.\\n')\n return 0\n default:\n stderr(`otto debug breakpoints: unknown sub-action '${op}'.\\n`)\n return 1\n }\n }\n case 'points':\n return await listPoints(client, stdout)\n case 'state':\n return await showState(client, stdout)\n default:\n stderr(`otto debug: unknown action '${action}'. Use breakpoints | points | state.\\n`)\n return 1\n }\n } catch (error) {\n return reportServiceError('debug', error, stderr)\n }\n}\n\nasync function listBreakpoints(client: ServiceClient, stdout: (s: string) => void): Promise<number> {\n const { breakpoints } = await client.get<{ breakpoints: BreakpointEntry[] }>(\n '/api/debugger/breakpoints',\n )\n if (breakpoints.length === 0) {\n stdout('No breakpoints set.\\n')\n return 0\n }\n stdout(`${'point'.padEnd(36)} enabled\\n${'-'.repeat(46)}\\n`)\n for (const bp of breakpoints) {\n stdout(`${bp.point.padEnd(36)} ${bp.enabled ? '✓' : '✗'}\\n`)\n }\n return 0\n}\n\nfunction isValidPoint(point: string | undefined): point is string {\n return !!point && POINT_NAMES.includes(point)\n}\n\nasync function setBreakpoint(\n client: ServiceClient,\n point: string | undefined,\n enabled: boolean,\n stdout: (s: string) => void,\n stderr: (s: string) => void,\n verb?: string,\n): Promise<number> {\n if (!isValidPoint(point)) {\n stderr(\n `otto debug: invalid or missing breakpoint point.\\nUse 'otto debug points' to list valid points.\\n`,\n )\n return 1\n }\n await client.put(`/api/debugger/breakpoints/${point}`, { enabled })\n stdout(`Breakpoint ${verb ?? `set: ${point} (${enabled ? 'enabled' : 'disabled'})`}${verb ? `: ${point}` : ''}\\n`)\n return 0\n}\n\nasync function listPoints(client: ServiceClient, stdout: (s: string) => void): Promise<number> {\n const { breakpoints } = await client.get<{ breakpoints: BreakpointEntry[] }>(\n '/api/debugger/breakpoints',\n )\n const byPoint = new Map(breakpoints.map((bp) => [bp.point, bp]))\n\n stdout('Available breakpoint points:\\n')\n for (const def of BREAKPOINT_POINTS) {\n const bp = byPoint.get(def.point)\n const status = bp?.set ? (bp.enabled ? 'enabled' : 'disabled') : 'not set'\n stdout(` ${def.point.padEnd(36)} ${status}\\n`)\n }\n return 0\n}\n\nasync function showState(client: ServiceClient, stdout: (s: string) => void): Promise<number> {\n const state = await client.get<{\n breakpoints: { total: number; enabled: number }\n paused: boolean\n pendingPauses: { pauseId: string; point: string; turn: number }[]\n }>('/api/debugger/state')\n\n stdout(`Debugger state\n breakpoints: ${state.breakpoints.enabled}/${state.breakpoints.total} enabled\n paused: ${state.paused ? 'yes' : 'no'}\n`)\n for (const p of state.pendingPauses) {\n stdout(` ⏸ ${p.point} (turn ${p.turn}) pauseId=${p.pauseId}\\n`)\n }\n return 0\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import{discoverPlugins as e}from"@x-otto/coding";import{dirname as t,join as n}from"node:path";import{existsSync as r}from"node:fs";import{fileURLToPath as i}from"node:url";function a(){let e=n(t(i(import.meta.url)),`bundled-extensions`);return r(e)?e:``}function o(t){let n=a();return e({...t,bundledDirs:n?[n]:[]})}export{o as t};
|
|
3
|
+
//# sourceMappingURL=discover-with-bundled-CznLjkkw.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"discover-with-bundled-CznLjkkw.js","names":[],"sources":["../src/bundled-extensions.ts","../src/discover-with-bundled.ts"],"sourcesContent":["/**\n * bundled-extensions.ts —— 随 otto 分发的内置扩展目录解析。\n *\n * cli 构建时(scripts/bundle-builtin-extensions.mjs)把内置扩展的编译产物拷进\n * `dist/bundled-extensions/<id>/`。本模块解析该目录,供 `discoverPlugins` 作为\n * 最高优先扫描层传入。\n *\n * ## 为什么在 cli 层解析而不是 plugin 层\n * `@x-otto/plugin` 是库,不该知道 cli 的 dist 布局(反向依赖)。目录位置只有 cli\n * 自己知道(它拥有 `import.meta.url` → dist 的路径)。\n *\n * ## 分布式的可移植性\n * 发布物 = `dist/`(package.json files:[\"dist\"])。本目录与 dist 同级,\n * 打包后相对路径关系保持不变——`import.meta.url` 解析到 dist 内的一个 js,\n * `../bundled-extensions` 即发布物内的扩展目录。\n */\nimport { existsSync } from 'node:fs'\nimport { dirname, join } from 'node:path'\nimport { fileURLToPath } from 'node:url'\n\n/** cli 发布物内内置扩展目录(不存在 = 该发布物没带内置扩展,返回空)。 */\nexport function bundledExtensionsDir(): string {\n // dist 里的这个 js 文件 → dist/bundled-extensions(同目录,非父级——这是 cli 构建\n // 时拷进去的发布物内目录,见 scripts/bundle-builtin-extensions.mjs)。\n const dir = join(dirname(fileURLToPath(import.meta.url)), 'bundled-extensions')\n return existsSync(dir) ? dir : ''\n}\n","/**\n * discover-with-bundled.ts —— `discoverPlugins` 的 cli 侧统一包装。\n *\n * 所有 cli 内部调用 `discoverPlugins` 的地方都应该走这里:把 cli 自己的\n * bundled-extensions 目录作为最高优先扫描层传入(否则内置扩展只在部分调用点可见,\n * 行为分裂)。\n */\n// cli 不直接依赖 @x-otto/plugin(经 @x-otto/coding 间接),从 coding 的 barrel 取。\nimport { discoverPlugins } from '@x-otto/coding'\n\nimport { bundledExtensionsDir } from './bundled-extensions'\n\nimport type { DiscoveredPlugin, DiscoverPluginsOptions } from '@x-otto/coding'\n\nexport function discoverPluginsWithBundled(opts: DiscoverPluginsOptions): DiscoveredPlugin[] {\n const bundled = bundledExtensionsDir()\n return discoverPlugins({\n ...opts,\n bundledDirs: bundled ? [bundled] : [],\n })\n}\n"],"mappings":";6KAqBA,SAAgB,GAA+B,CAG7C,IAAM,EAAM,EAAK,EAAQ,EAAc,OAAO,KAAK,IAAI,CAAC,CAAE,qBAAqB,CAC/E,OAAO,EAAW,EAAI,CAAG,EAAM,GCXjC,SAAgB,EAA2B,EAAkD,CAC3F,IAAM,EAAU,GAAsB,CACtC,OAAO,EAAgB,CACrB,GAAG,EACH,YAAa,EAAU,CAAC,EAAQ,CAAG,EAAE,CACtC,CAAC"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import{arch as e,cpus as t,freemem as n,homedir as r,platform as i,release as a,totalmem as o}from"node:os";import{join as s}from"node:path";import{accessSync as c,existsSync as l,statSync as u}from"node:fs";import{SESSION_MAX_HISTORY_MESSAGES as d,SESSION_MAX_PER_SESSION_BYTES as f,SESSION_MIN_PER_SESSION_BYTES as p,SESSION_RESUME_WINDOW_MESSAGES as m,SESSION_TOTAL_RESIDENCY_BUDGET as h}from"@x-otto/session-contract";const g=s(r(),`.otto`);async function _(e,t){let n=t.stdout,r=t.stderr,i=e.find(e=>!e.startsWith(`-`));if(i===`ps`||i===`attach`||i===`heap`){let n=e.filter(e=>e!==i);switch(i){case`ps`:{let{runDoctorPs:e}=await import(`./doctor-ps-OL3FGQYu.js`);return e(n,t)}case`attach`:{let{runDoctorAttach:e}=await import(`./doctor-attach-_zeIJewx.js`);return e(n,t)}case`heap`:{let{runDoctorHeap:e}=await import(`./doctor-heap-Rr2nNvcw.js`);return e(n,t)}}}if(i!==void 0)return r(`otto doctor: unknown subcommand "${i}" (available: ps, attach, heap)\n`),1;if(e.includes(`--help`)||e.includes(`-h`))return D(n),0;let a=e.includes(`--json`)?`json`:e.includes(`--summary`)?`summary`:`detailed`;try{let e=await v();return a===`json`?n(JSON.stringify(e,null,2)+`
|
|
3
|
+
`):a===`summary`?T(n,e):E(n,e),0}catch(e){return r(`otto doctor: ${e instanceof Error?e.message:String(e)}\n`),1}}async function v(){return{environment:await y(),configuration:await b(),state:await x(),connectivity:await S()}}async function y(){return{runtime:`Node.js ${process.version}`,installPath:process.execPath,shell:process.env.SHELL??`(unknown)`,terminal:process.env.TERM??`(unknown)`,cpu:`${t().length}×${e()}`,memory:{total:o(),free:n()},platform:i(),release:a()}}async function b(){let e=process.env.ANTHROPIC_API_KEY?`api-key (ANTHROPIC_API_KEY)`:process.env.OTTO_API_KEY?`api-key (OTTO_API_KEY)`:process.env.AWS_ACCESS_KEY_ID?`aws-bedrock (env keys)`:`not configured`;return{workspaceDir:process.cwd(),storageDir:g,hasRipgrep:await C(`rg`),hasGit:await C(`git`),authMode:e,residencyBudget:{totalBytes:h,minPerSessionBytes:p,maxPerSessionBytes:f,historyMessages:d,resumeWindowMessages:m}}}async function x(){let e=s(g,`storage.db`);if(!l(e))return{dbPath:e,dbExists:!1};try{let{SqlitePersistence:t}=await import(`@x-otto/persistence`),n=new t({dbPath:e,namespace:`doctor`,wal:!1});try{let t=n.getInfo();return{dbPath:e,dbExists:!0,dbSize:w(e),...t}}finally{n.close()}}catch{return{dbPath:e,dbExists:!0,integrity:`error (unable to open)`}}}async function S(){let e=`api.anthropic.com`,t=!1,n,r=Date.now();try{let i=new AbortController,a=setTimeout(()=>i.abort(),5e3);await fetch(`https://${e}`,{method:`HEAD`,signal:i.signal}),clearTimeout(a),t=!0,n=Date.now()-r}catch{t=!1}return{providerHost:e,providerReachable:t,providerLatency:n,storageHostConfigured:!1}}async function C(e){try{return c(s(`/usr/local/bin`,e),1),!0}catch{try{return c(s(`/usr/bin`,e),1),!0}catch{return!1}}}function w(e){try{return u(e).size}catch{return 0}}function T(e,t){let{environment:n,configuration:r,state:i,connectivity:a}=t,o=i?.dbExists&&i.integrity===`ok`?`✅`:`❌`,s=a.providerReachable?`✅`:`❌`;e([`otto doctor — ${n.runtime} | ${r.storageDir} | DB:${o} ${s}`,i?` ${i.snapshots??`?`} snapshots, ${i.changeLogEntries??`?`} change_log`:``].join(`
|
|
4
|
+
`)+`
|
|
5
|
+
`)}function E(e,t){let{environment:n,configuration:r,state:i,connectivity:a}=t;if(e(`
|
|
6
|
+
Environment
|
|
7
|
+
`),e(` Runtime: ${n.runtime}\n`),e(` Install: ${n.installPath}\n`),e(` Shell: ${n.shell}\n`),e(` Terminal: ${n.terminal} (${n.release})\n`),e(` CPU: ${n.cpu}\n`),e(` Memory: ${O(n.memory.free)} / ${O(n.memory.total)} free\n`),r.residencyBudget){let t=r.residencyBudget;e(` Residency: budget=${O(t.totalBytes)} per-session=${O(t.minPerSessionBytes)}~${O(t.maxPerSessionBytes)} history=${t.historyMessages} resume=${t.resumeWindowMessages}\n`)}e(`
|
|
8
|
+
Configuration
|
|
9
|
+
`),e(` Workspace: ${r.workspaceDir}\n`),e(` Storage: ${r.storageDir} ${l(r.storageDir)?`✅`:`❌`}\n`),e(` ripgrep: ${r.hasRipgrep?`✅`:`❌`}\n`),e(` git: ${r.hasGit?`✅`:`❌`}\n`),e(` Auth: ${r.authMode}\n`),i&&(e(`
|
|
10
|
+
State
|
|
11
|
+
`),e(` SQLite DB: ${i.dbPath}\n`),i.dbExists?(e(` size: ${O(i.dbSize??0)}\n`),e(` integrity: ${i.integrity===`ok`?`ok ✅`:i.integrity??`N/A`}\n`),i.snapshots!==void 0&&e(` snapshots: ${i.snapshots}\n`),i.changeLogEntries!==void 0&&e(` change_log: ${i.changeLogEntries} entries\n`),i.sessions!==void 0&&e(` sessions: ${i.sessions}\n`),i.walEnabled!==void 0&&e(` WAL: ${i.walEnabled?`enabled`:`disabled`}\n`)):e(` (no storage.db found — start otto to create one)
|
|
12
|
+
`)),e(`
|
|
13
|
+
Connectivity
|
|
14
|
+
`),e(` ${a.providerHost}: ${a.providerReachable?`reachable (${a.providerLatency}ms) ✅`:`unreachable ❌`}\n`),e(` storage host: ${a.storageHostConfigured?`configured`:`not configured (local only)`}\n`),e(`
|
|
15
|
+
`)}function D(e){e(`otto doctor — 环境健康诊断
|
|
16
|
+
|
|
17
|
+
用法:
|
|
18
|
+
otto doctor [--summary|--json]
|
|
19
|
+
|
|
20
|
+
选项:
|
|
21
|
+
--summary 紧凑输出(一行摘要)
|
|
22
|
+
--json 结构化 JSON(redacted)
|
|
23
|
+
--help, -h 帮助信息
|
|
24
|
+
|
|
25
|
+
示例:
|
|
26
|
+
otto doctor
|
|
27
|
+
otto doctor --summary
|
|
28
|
+
otto doctor --json
|
|
29
|
+
`)}function O(e){if(e===0)return`0 B`;let t=[`B`,`KB`,`MB`,`GB`,`TB`],n=Math.floor(Math.log(e)/Math.log(1024));return`${(e/1024**n).toFixed(1)} ${t[n]}`}export{_ as runDoctorCommand};
|
|
30
|
+
//# sourceMappingURL=doctor-C84-hhEr.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"doctor-C84-hhEr.js","names":[],"sources":["../src/commands/doctor.ts"],"sourcesContent":["import { homedir, release, platform, arch, cpus, totalmem, freemem } from 'node:os'\nimport { existsSync, statSync, accessSync } from 'node:fs'\nimport { join } from 'node:path'\nimport {\n SESSION_TOTAL_RESIDENCY_BUDGET,\n SESSION_MIN_PER_SESSION_BYTES,\n SESSION_MAX_PER_SESSION_BYTES,\n SESSION_MAX_HISTORY_MESSAGES,\n SESSION_RESUME_WINDOW_MESSAGES,\n} from '@x-otto/session-contract'\nexport interface DoctorDeps {\n stdout: (s: string) => void\n stderr: (s: string) => void\n}\n\nexport interface DoctorReport {\n environment: EnvironmentSection\n configuration: ConfigurationSection\n state?: StateSection\n connectivity: ConnectivitySection\n}\n\ninterface EnvironmentSection {\n runtime: string\n installPath: string\n shell: string\n terminal: string\n cpu: string\n memory: { total: number; free: number }\n platform: string\n release: string\n}\n\ninterface ConfigurationSection {\n workspaceDir: string\n storageDir: string\n hasRipgrep: boolean\n hasGit: boolean\n authMode: string\n /** RFC-323:驻留预算配置(只读展示,使基线估算偏差可被校准) */\n residencyBudget?: {\n totalBytes: number\n minPerSessionBytes: number\n maxPerSessionBytes: number\n historyMessages: number\n resumeWindowMessages: number\n }\n}\n\ninterface StateSection {\n dbPath: string\n dbExists: boolean\n dbSize?: number\n walEnabled?: boolean\n snapshots?: number\n changeLogEntries?: number\n sessions?: number\n integrity?: string\n}\n\ninterface ConnectivitySection {\n providerHost: string\n providerReachable: boolean\n providerLatency?: number\n storageHostConfigured: boolean\n}\n\nconst OTTO_DIR = join(homedir(), '.otto')\n\nexport async function runDoctorCommand(\n args: string[],\n deps: DoctorDeps,\n): Promise<number> {\n const stdout = deps.stdout\n const stderr = deps.stderr\n\n // RFC-200 D1.0:子命令路由——首个位置参数(非 -- 开头)分发到诊断子命令;\n // 无子命令时走既有健康检查(行为零变化)。\n const sub = args.find((a) => !a.startsWith('-'))\n if (sub === 'ps' || sub === 'attach' || sub === 'heap') {\n const rest = args.filter((a) => a !== sub)\n switch (sub) {\n case 'ps': {\n const { runDoctorPs } = await import('./handlers/doctor-ps')\n return runDoctorPs(rest, deps)\n }\n case 'attach': {\n const { runDoctorAttach } = await import('./handlers/doctor-attach')\n return runDoctorAttach(rest, deps)\n }\n case 'heap': {\n const { runDoctorHeap } = await import('./handlers/doctor-heap')\n return runDoctorHeap(rest, deps)\n }\n }\n }\n if (sub !== undefined) {\n stderr(`otto doctor: unknown subcommand \"${sub}\" (available: ps, attach, heap)\\n`)\n return 1\n }\n\n if (args.includes('--help') || args.includes('-h')) {\n printHelp(stdout)\n return 0\n }\n\n const outputFormat = args.includes('--json') ? 'json' : args.includes('--summary') ? 'summary' : 'detailed'\n\n try {\n const report = await collectReport()\n\n if (outputFormat === 'json') {\n stdout(JSON.stringify(report, null, 2) + '\\n')\n } else if (outputFormat === 'summary') {\n printSummary(stdout, report)\n } else {\n printDetailed(stdout, report)\n }\n\n return 0\n } catch (error) {\n stderr(`otto doctor: ${error instanceof Error ? error.message : String(error)}\\n`)\n return 1\n }\n}\n\n/** 收集结构化诊断报告(TUI /doctor 折叠档复用;不打印)。 */\nexport async function collectReport(): Promise<DoctorReport> {\n const env = await collectEnvironment()\n const config = await collectConfiguration()\n const state = await collectState()\n const connectivity = await collectConnectivity()\n\n return { environment: env, configuration: config, state, connectivity }\n}\n\nasync function collectEnvironment(): Promise<EnvironmentSection> {\n return {\n runtime: `Node.js ${process.version}`,\n installPath: process.execPath,\n shell: process.env.SHELL ?? '(unknown)',\n terminal: process.env.TERM ?? '(unknown)',\n cpu: `${cpus().length}×${arch()}`,\n memory: { total: totalmem(), free: freemem() },\n platform: platform(),\n release: release(),\n }\n}\n\nasync function collectConfiguration(): Promise<ConfigurationSection> {\n const authMode = process.env.ANTHROPIC_API_KEY\n ? 'api-key (ANTHROPIC_API_KEY)'\n : process.env.OTTO_API_KEY\n ? 'api-key (OTTO_API_KEY)'\n : process.env.AWS_ACCESS_KEY_ID\n ? 'aws-bedrock (env keys)'\n : 'not configured'\n\n return {\n workspaceDir: process.cwd(),\n storageDir: OTTO_DIR,\n hasRipgrep: await checkBinary('rg'),\n hasGit: await checkBinary('git'),\n authMode,\n // RFC-323 M3:暴露驻留预算配置,使\"基线 1500MB 估算偏差\"(R-2)可被真机数据校准。\n // 纯静态读常量,不接运行时状态——运行时有效分配值取决于活跃会话数 N,在 ps/doctor-ps 展示。\n residencyBudget: {\n totalBytes: SESSION_TOTAL_RESIDENCY_BUDGET,\n minPerSessionBytes: SESSION_MIN_PER_SESSION_BYTES,\n maxPerSessionBytes: SESSION_MAX_PER_SESSION_BYTES,\n historyMessages: SESSION_MAX_HISTORY_MESSAGES,\n resumeWindowMessages: SESSION_RESUME_WINDOW_MESSAGES,\n },\n }\n}\n\nasync function collectState(): Promise<StateSection | undefined> {\n const dbPath = join(OTTO_DIR, 'storage.db')\n if (!existsSync(dbPath)) {\n return { dbPath, dbExists: false }\n }\n\n try {\n const { SqlitePersistence } = await import('@x-otto/persistence')\n const store = new SqlitePersistence({ dbPath, namespace: 'doctor', wal: false })\n try {\n const info = store.getInfo() as {\n walEnabled: boolean\n snapshots: number\n changeLogEntries: number\n sessions: number\n integrity: string\n }\n\n return {\n dbPath,\n dbExists: true,\n dbSize: getFileSizeSync(dbPath),\n ...info,\n }\n } finally {\n store.close()\n }\n } catch {\n return { dbPath, dbExists: true, integrity: 'error (unable to open)' }\n }\n}\n\nasync function collectConnectivity(): Promise<ConnectivitySection> {\n let providerHost = 'api.anthropic.com'\n let providerReachable = false\n let providerLatency: number | undefined\n\n const start = Date.now()\n try {\n const controller = new AbortController()\n const timeout = setTimeout(() => controller.abort(), 5000)\n await fetch(`https://${providerHost}`, {\n method: 'HEAD',\n signal: controller.signal,\n })\n clearTimeout(timeout)\n providerReachable = true\n providerLatency = Date.now() - start\n } catch {\n providerReachable = false\n }\n\n return {\n providerHost,\n providerReachable,\n providerLatency,\n storageHostConfigured: false,\n }\n}\n\nasync function checkBinary(name: string): Promise<boolean> {\n try {\n accessSync(join('/usr/local/bin', name), 1)\n return true\n } catch {\n try {\n accessSync(join('/usr/bin', name), 1)\n return true\n } catch {\n return false\n }\n }\n}\n\nfunction getFileSizeSync(path: string): number {\n try {\n return statSync(path).size\n } catch {\n return 0\n }\n}\n\nfunction printSummary(stdout: (s: string) => void, report: DoctorReport): void {\n const { environment: env, configuration: config, state, connectivity: conn } = report\n const dbOk = state?.dbExists && state.integrity === 'ok' ? '✅' : '❌'\n const netOk = conn.providerReachable ? '✅' : '❌'\n stdout([\n `otto doctor — ${env.runtime} | ${config.storageDir} | DB:${dbOk} ${netOk}`,\n state ? ` ${state.snapshots ?? '?'} snapshots, ${state.changeLogEntries ?? '?'} change_log` : '',\n ].join('\\n') + '\\n')\n}\n\nfunction printDetailed(stdout: (s: string) => void, report: DoctorReport): void {\n const { environment: env, configuration: config, state, connectivity: conn } = report\n\n stdout('\\nEnvironment\\n')\n stdout(` Runtime: ${env.runtime}\\n`)\n stdout(` Install: ${env.installPath}\\n`)\n stdout(` Shell: ${env.shell}\\n`)\n stdout(` Terminal: ${env.terminal} (${env.release})\\n`)\n stdout(` CPU: ${env.cpu}\\n`)\n stdout(` Memory: ${formatBytes(env.memory.free)} / ${formatBytes(env.memory.total)} free\\n`)\n if (config.residencyBudget) {\n const rb = config.residencyBudget\n stdout(` Residency: budget=${formatBytes(rb.totalBytes)} per-session=${formatBytes(rb.minPerSessionBytes)}~${formatBytes(rb.maxPerSessionBytes)} history=${rb.historyMessages} resume=${rb.resumeWindowMessages}\\n`)\n }\n\n stdout('\\nConfiguration\\n')\n stdout(` Workspace: ${config.workspaceDir}\\n`)\n stdout(` Storage: ${config.storageDir} ${existsSync(config.storageDir) ? '✅' : '❌'}\\n`)\n stdout(` ripgrep: ${config.hasRipgrep ? '✅' : '❌'}\\n`)\n stdout(` git: ${config.hasGit ? '✅' : '❌'}\\n`)\n stdout(` Auth: ${config.authMode}\\n`)\n\n if (state) {\n stdout('\\nState\\n')\n stdout(` SQLite DB: ${state.dbPath}\\n`)\n if (state.dbExists) {\n stdout(` size: ${formatBytes(state.dbSize ?? 0)}\\n`)\n stdout(` integrity: ${state.integrity === 'ok' ? 'ok ✅' : state.integrity ?? 'N/A'}\\n`)\n if (state.snapshots !== undefined) stdout(` snapshots: ${state.snapshots}\\n`)\n if (state.changeLogEntries !== undefined) stdout(` change_log: ${state.changeLogEntries} entries\\n`)\n if (state.sessions !== undefined) stdout(` sessions: ${state.sessions}\\n`)\n if (state.walEnabled !== undefined) stdout(` WAL: ${state.walEnabled ? 'enabled' : 'disabled'}\\n`)\n } else {\n stdout(` (no storage.db found — start otto to create one)\\n`)\n }\n }\n\n stdout('\\nConnectivity\\n')\n stdout(` ${conn.providerHost}: ${conn.providerReachable ? `reachable (${conn.providerLatency}ms) ✅` : 'unreachable ❌'}\\n`)\n stdout(` storage host: ${conn.storageHostConfigured ? 'configured' : 'not configured (local only)'}\\n`)\n stdout('\\n')\n}\n\nfunction printHelp(stdout: (s: string) => void): void {\n stdout(`otto doctor — 环境健康诊断\n\n用法:\n otto doctor [--summary|--json]\n\n选项:\n --summary 紧凑输出(一行摘要)\n --json 结构化 JSON(redacted)\n --help, -h 帮助信息\n\n示例:\n otto doctor\n otto doctor --summary\n otto doctor --json\n`)\n}\n\nfunction formatBytes(bytes: number): string {\n if (bytes === 0) return '0 B'\n const units = ['B', 'KB', 'MB', 'GB', 'TB']\n const i = Math.floor(Math.log(bytes) / Math.log(1024))\n return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${units[i]}`\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import{a as e,i as t,o as n,r,s as i,t as a}from"./doctor-cdp-K_mIFRfA.js";import{execFileSync as o}from"node:child_process";async function s(o,s){let f=o.filter(e=>!e.startsWith(`-`)),p=Number(f[0]);if(!Number.isFinite(p)||p<=0)return s.stderr(`用法: otto doctor attach <pid> [--fix-user-timing]
|
|
3
|
+
(pid 可用 otto doctor ps 查看)
|
|
4
|
+
`),1;try{process.kill(p,0)}catch{return s.stderr(`otto doctor attach: pid=${p} 不存在或无权限\n`),1}try{r()}catch(e){return s.stderr(`${e instanceof Error?e.message:String(e)}\n`),1}let m=t();if(m!=null&&m!==p)return s.stderr(`inspector 端口被 pid=${m} 占用,同一时刻只能 CDP attach 一个进程。\n`),s.stdout(`自动降级为 macOS 原生工具输出:
|
|
5
|
+
|
|
6
|
+
`),c(p,s);m===null&&s.stdout(`(lsof 不可用,跳过端口预检——若下方身份核对失败请手动排查端口占用)
|
|
7
|
+
`);let h=new a;try{let e=await n(p);i(e,p),await h.connect(e.webSocketDebuggerUrl),s.stdout(`已附加 pid=${p}(${e.title})\n\n`);let t=await h.evaluate(`(() => { const p = process; return {
|
|
8
|
+
handles: (p._getActiveHandles?.() ?? []).map((h) => h?.constructor?.name ?? 'unknown'),
|
|
9
|
+
resources: (p.getActiveResourcesInfo?.() ?? []),
|
|
10
|
+
} })()`),r=l(t.handles);s.stdout(`— Active handles(事件循环 pinner,lesson_85)—\n${u(r)}\n`),s.stdout(`— Active resources —\n${u(l(t.resources))}\n`);let a=await h.evaluate(`(() => ({
|
|
11
|
+
measures: performance.getEntriesByType('measure').length,
|
|
12
|
+
marks: performance.getEntriesByType('mark').length,
|
|
13
|
+
nodeEnv: process.env.NODE_ENV ?? '(unset)',
|
|
14
|
+
}))()`);s.stdout(`— User-timing 缓冲(lesson_92)—\nmeasures: ${a.measures} marks: ${a.marks} NODE_ENV: ${a.nodeEnv}\n`),a.measures>1e5&&s.stdout(`⚠ measures ${a.measures} 条异常累积(lesson_92 事故特征:React dev 构建 user-timing 无界缓冲)。\n 建议:otto doctor attach ${p} --fix-user-timing 清空缓冲(已验证安全),并核查 NODE_ENV 未设为 production 的原因。\n`);let c=await h.evaluate(`(() => { const m = process.memoryUsage(); const v8 = require('v8').getHeapStatistics(); return {
|
|
15
|
+
heapUsedMb: Math.round(m.heapUsed / 1048576),
|
|
16
|
+
heapLimitMb: Math.round(v8.heap_size_limit / 1048576),
|
|
17
|
+
externalMb: Math.round(m.external / 1048576),
|
|
18
|
+
rssMb: Math.round(m.rss / 1048576),
|
|
19
|
+
} })()`);s.stdout(`— Heap —\nheapUsed: ${c.heapUsedMb}MB / limit ${c.heapLimitMb}MB external: ${c.externalMb}MB rss: ${c.rssMb}MB\n`);let f=await h.evaluate(`(() => { const d = globalThis.__ottoResidencyDiagnostics; return typeof d?.getResidency === 'function'
|
|
20
|
+
? { available: true, ...d.getResidency() }
|
|
21
|
+
: { available: false } })()`);f.available?s.stdout(`— Residency(RFC-325)—\nactive: ${f.activeSessionCount} resident: ${d(f.totalResidentBytes)} pressure: ${Math.round(f.globalPressure*100)}% per-session: ${d(f.maxBytesPerSession)} level: ${f.pressureLevel}${f.isGlobalPressure?` ⚠ GLOBAL`:``}\n`):s.stdout(`— Residency(RFC-325)—
|
|
22
|
+
(unavailable: target has no running App diagnostics bridge)
|
|
23
|
+
`);let m=await h.evaluate(`(() => { try {
|
|
24
|
+
const { Worker } = require('worker_threads')
|
|
25
|
+
void Worker
|
|
26
|
+
// 无公开 API 枚举 worker——用 process._getActiveHandles 里的 Worker 计数替代(保守但零侵入)
|
|
27
|
+
return (process._getActiveHandles?.() ?? [])
|
|
28
|
+
.filter((h) => h?.constructor?.name === 'Worker' || h?.constructor?.name === 'MessagePort')
|
|
29
|
+
.map((h) => h.constructor.name)
|
|
30
|
+
} catch { return [] } })()`);if(s.stdout(`— Worker/MessagePort handles —\n${m.length===0?`(none)`:u(l(m))}\n`),o.includes(`--fix-user-timing`)){let e=await h.evaluate(`(() => { const n = performance.getEntriesByType('measure').length; performance.clearMeasures(); performance.clearMarks(); return n })()`);s.stdout(`\n--fix-user-timing:已清空 ${e} 条 measures(及全部 marks)。\n`)}return 0}catch(t){return s.stderr(`otto doctor attach 失败: ${t instanceof Error?t.message:String(t)}\n`),s.stdout(`
|
|
31
|
+
降级配方:
|
|
32
|
+
`+e(p)+`
|
|
33
|
+
`),1}finally{h.close()}}function c(t,n){n.stdout(e(t)+`
|
|
34
|
+
|
|
35
|
+
`);try{let e=o(`sample`,[String(t),`2`,`-file`,`/dev/stdout`],{encoding:`utf-8`,timeout:15e3,maxBuffer:8*1024*1024}).split(`
|
|
36
|
+
`).slice(0,40).join(`
|
|
37
|
+
`);return n.stdout(`— sample ${t}(2s CPU 栈头部)—\n${e}\n`),0}catch{return n.stdout(`(sample 不可用——非 macOS 或无权限;请按上方配方手动执行)
|
|
38
|
+
`),1}}function l(e){let t=new Map;for(let n of e)t.set(n,(t.get(n)??0)+1);return t}function u(e){return e.size===0?`(none)
|
|
39
|
+
`:[...e.entries()].sort((e,t)=>t[1]-e[1]).map(([e,t])=>` ${e}: ${t}`).join(`
|
|
40
|
+
`)+`
|
|
41
|
+
`}function d(e){return`${(e/(1024*1024)).toFixed(1)}MB`}export{s as runDoctorAttach};
|
|
42
|
+
//# sourceMappingURL=doctor-attach-_zeIJewx.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"doctor-attach-_zeIJewx.js","names":[],"sources":["../src/commands/handlers/doctor-attach.ts"],"sourcesContent":["/**\n * doctor-attach.ts — 安全附加 + 预置诊断查询(RFC-200 D1 / T200-2)。\n *\n * lesson_69/85/92 三次事故的手搓 CDP 配方机械化:\n * 端口预检 → USR1 → 身份核对 → 预置查询(active handles / user-timing / heap 统计 / worker 列表)。\n * 只读红线:唯一治理动作是显式 `--fix-user-timing`(lesson_92 已验证 clearMeasures 安全性)。\n * 9229 被占时自动降级 macOS 原生工具配方输出(不是只给提示退出——评审修订 4)。\n */\nimport { execFileSync } from 'node:child_process'\n\nimport {\n CdpSession,\n assertBuiltinWebSocket,\n inspectorPortOwner,\n nativeFallbackHint,\n openInspector,\n verifyTargetIdentity,\n} from './doctor-cdp'\n\nimport type { DoctorDeps } from '../doctor'\n\nexport async function runDoctorAttach(args: string[], deps: DoctorDeps): Promise<number> {\n const positional = args.filter((a) => !a.startsWith('-'))\n const pid = Number(positional[0])\n if (!Number.isFinite(pid) || pid <= 0) {\n deps.stderr('用法: otto doctor attach <pid> [--fix-user-timing]\\n(pid 可用 otto doctor ps 查看)\\n')\n return 1\n }\n\n try {\n process.kill(pid, 0) // 存活探测(不发信号)\n } catch {\n deps.stderr(`otto doctor attach: pid=${pid} 不存在或无权限\\n`)\n return 1\n }\n\n try {\n assertBuiltinWebSocket()\n } catch (error) {\n deps.stderr(`${error instanceof Error ? error.message : String(error)}\\n`)\n return 1\n }\n\n // ① 端口预检(lesson_69 第一步)。被占 → 自动降级原生工具输出(评审修订 4)。\n const owner = inspectorPortOwner()\n if (owner != null && owner !== pid) {\n deps.stderr(`inspector 端口被 pid=${owner} 占用,同一时刻只能 CDP attach 一个进程。\\n`)\n deps.stdout('自动降级为 macOS 原生工具输出:\\n\\n')\n return runNativeFallback(pid, deps)\n }\n if (owner === null) {\n deps.stdout('(lsof 不可用,跳过端口预检——若下方身份核对失败请手动排查端口占用)\\n')\n }\n\n const session = new CdpSession()\n try {\n // ② USR1 开 inspector + ③ 身份核对(lesson_69 第二/三步)\n const target = await openInspector(pid)\n verifyTargetIdentity(target, pid)\n await session.connect(target.webSocketDebuggerUrl)\n deps.stdout(`已附加 pid=${pid}(${target.title})\\n\\n`)\n\n // 预置查询 1:active handles/resources(lesson_85 僵尸进程钉死配方)\n const handles = await session.evaluate<{ handles: string[]; resources: string[] }>(\n `(() => { const p = process; return {\n handles: (p._getActiveHandles?.() ?? []).map((h) => h?.constructor?.name ?? 'unknown'),\n resources: (p.getActiveResourcesInfo?.() ?? []),\n } })()`,\n )\n const handleCounts = countBy(handles.handles)\n deps.stdout(`— Active handles(事件循环 pinner,lesson_85)—\\n${fmtCounts(handleCounts)}\\n`)\n deps.stdout(`— Active resources —\\n${fmtCounts(countBy(handles.resources))}\\n`)\n\n // 预置查询 2:user-timing 缓冲(lesson_92 React dev-mode 2GB 驻留配方)\n const userTiming = await session.evaluate<{ measures: number; marks: number; nodeEnv: string }>(\n `(() => ({\n measures: performance.getEntriesByType('measure').length,\n marks: performance.getEntriesByType('mark').length,\n nodeEnv: process.env.NODE_ENV ?? '(unset)',\n }))()`,\n )\n deps.stdout(\n `— User-timing 缓冲(lesson_92)—\\nmeasures: ${userTiming.measures} marks: ${userTiming.marks} NODE_ENV: ${userTiming.nodeEnv}\\n`,\n )\n if (userTiming.measures > 100_000) {\n deps.stdout(\n `⚠ measures ${userTiming.measures} 条异常累积(lesson_92 事故特征:React dev 构建 user-timing 无界缓冲)。\\n` +\n ` 建议:otto doctor attach ${pid} --fix-user-timing 清空缓冲(已验证安全),并核查 NODE_ENV 未设为 production 的原因。\\n`,\n )\n }\n\n // 预置查询 3:heap 统计\n const heap = await session.evaluate<{ heapUsedMb: number; heapLimitMb: number; externalMb: number; rssMb: number }>(\n `(() => { const m = process.memoryUsage(); const v8 = require('v8').getHeapStatistics(); return {\n heapUsedMb: Math.round(m.heapUsed / 1048576),\n heapLimitMb: Math.round(v8.heap_size_limit / 1048576),\n externalMb: Math.round(m.external / 1048576),\n rssMb: Math.round(m.rss / 1048576),\n } })()`,\n )\n deps.stdout(\n `— Heap —\\nheapUsed: ${heap.heapUsedMb}MB / limit ${heap.heapLimitMb}MB external: ${heap.externalMb}MB rss: ${heap.rssMb}MB\\n`,\n )\n\n // 预置查询 4:RFC-325 M5 全局驻留快照。桥只暴露聚合数字,缺席时降级为未初始化。\n const residency = await session.evaluate<{\n available: boolean\n activeSessionCount?: number\n totalResidentBytes?: number\n globalPressure?: number\n isGlobalPressure?: boolean\n maxBytesPerSession?: number\n pressureLevel?: string\n }>(\n `(() => { const d = globalThis.__ottoResidencyDiagnostics; return typeof d?.getResidency === 'function'\n ? { available: true, ...d.getResidency() }\n : { available: false } })()`,\n )\n if (residency.available) {\n deps.stdout(\n `— Residency(RFC-325)—\\nactive: ${residency.activeSessionCount} resident: ${formatMb(residency.totalResidentBytes!)} ` +\n `pressure: ${Math.round(residency.globalPressure! * 100)}% per-session: ${formatMb(residency.maxBytesPerSession!)} ` +\n `level: ${residency.pressureLevel}${residency.isGlobalPressure ? ' ⚠ GLOBAL' : ''}\\n`,\n )\n } else {\n deps.stdout('— Residency(RFC-325)—\\n(unavailable: target has no running App diagnostics bridge)\\n')\n }\n\n // 预置查询 5:worker 线程列表(lesson_85 的 NodeWorker 归属定位,经 module 层枚举)\n const workers = await session.evaluate<string[]>(\n `(() => { try {\n const { Worker } = require('worker_threads')\n void Worker\n // 无公开 API 枚举 worker——用 process._getActiveHandles 里的 Worker 计数替代(保守但零侵入)\n return (process._getActiveHandles?.() ?? [])\n .filter((h) => h?.constructor?.name === 'Worker' || h?.constructor?.name === 'MessagePort')\n .map((h) => h.constructor.name)\n } catch { return [] } })()`,\n )\n deps.stdout(`— Worker/MessagePort handles —\\n${workers.length === 0 ? '(none)' : fmtCounts(countBy(workers))}\\n`)\n\n // 显式 opt-in 治理动作(唯一例外,lesson_92 验证过安全性)\n if (args.includes('--fix-user-timing')) {\n const cleared = await session.evaluate<number>(\n `(() => { const n = performance.getEntriesByType('measure').length; performance.clearMeasures(); performance.clearMarks(); return n })()`,\n )\n deps.stdout(`\\n--fix-user-timing:已清空 ${cleared} 条 measures(及全部 marks)。\\n`)\n }\n\n return 0\n } catch (error) {\n deps.stderr(`otto doctor attach 失败: ${error instanceof Error ? error.message : String(error)}\\n`)\n deps.stdout('\\n降级配方:\\n' + nativeFallbackHint(pid) + '\\n')\n return 1\n } finally {\n // 风险 3:退出即断开;目标进程的 inspector 由其自身生命周期管理(我们不主动 close 它的\n // server——那需要目标进程内执行 inspector.close(),超出只读边界;端口在断连后可复用)。\n session.close()\n }\n}\n\nfunction runNativeFallback(pid: number, deps: DoctorDeps): number {\n deps.stdout(nativeFallbackHint(pid) + '\\n\\n')\n try {\n // sample 5 秒采样输出裁剪到调用栈头部——等价于手动配方的第一步\n const out = execFileSync('sample', [String(pid), '2', '-file', '/dev/stdout'], {\n encoding: 'utf-8',\n timeout: 15_000,\n maxBuffer: 8 * 1024 * 1024,\n })\n const head = out.split('\\n').slice(0, 40).join('\\n')\n deps.stdout(`— sample ${pid}(2s CPU 栈头部)—\\n${head}\\n`)\n return 0\n } catch {\n deps.stdout('(sample 不可用——非 macOS 或无权限;请按上方配方手动执行)\\n')\n return 1\n }\n}\n\nfunction countBy(items: string[]): Map<string, number> {\n const map = new Map<string, number>()\n for (const item of items) map.set(item, (map.get(item) ?? 0) + 1)\n return map\n}\n\nfunction fmtCounts(map: Map<string, number>): string {\n if (map.size === 0) return '(none)\\n'\n return [...map.entries()]\n .sort((a, b) => b[1] - a[1])\n .map(([name, count]) => ` ${name}: ${count}`)\n .join('\\n') + '\\n'\n}\n\n\nfunction formatMb(bytes: number): string {\n return `${(bytes / (1024 * 1024)).toFixed(1)}MB`\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import{execFileSync as e}from"node:child_process";const t=9229;function n(){if(typeof WebSocket!=`function`)throw Error(`otto doctor attach 需要 Node >= 22(内建 WebSocket);当前 ${process.version}。低版本可改用手动配方:kill -USR1 <pid> 后用 Chrome DevTools 连 chrome://inspect。`)}function r(){try{let n=e(`lsof`,[`-nP`,`-iTCP:${t}`,`-sTCP:LISTEN`,`-t`],{encoding:`utf-8`,timeout:5e3}).trim();if(!n)return;let r=Number(n.split(`
|
|
3
|
+
`)[0]);return Number.isFinite(r)?r:void 0}catch(e){return e.status===1?void 0:null}}async function i(e,n=3e3){process.kill(e,`SIGUSR1`);let r=Date.now()+n,i;for(;Date.now()<r;){try{let e=(await(await fetch(`http://127.0.0.1:${t}/json`)).json()).find(e=>e.webSocketDebuggerUrl);if(e?.webSocketDebuggerUrl){let t=/\[(\d+)\]/.exec(e.title??``);return{webSocketDebuggerUrl:e.webSocketDebuggerUrl,title:e.title??``,pid:t?Number(t[1]):void 0}}}catch(e){i=e}await new Promise(e=>setTimeout(e,100))}throw Error(`inspector 未在 ${n}ms 内就绪(pid=${e}):${i instanceof Error?i.message:`目标未监听`}`)}function a(e,t){if(e.pid!==void 0&&e.pid!==t)throw Error(`9229 端口上的 inspector 属于 pid=${e.pid}("${e.title}"),非目标 pid=${t}——端口被其他进程占用,拒绝连接(lesson_69:误连他进程数据会污染分析)。`)}var o=class{ws;nextId=1;pending=new Map;async connect(e){this.ws=new WebSocket(e),await new Promise((e,t)=>{this.ws.addEventListener(`open`,()=>e(),{once:!0}),this.ws.addEventListener(`error`,()=>t(Error(`CDP WebSocket 连接失败`)),{once:!0})}),this.ws.addEventListener(`message`,e=>{try{let t=JSON.parse(String(e.data));if(t.id===void 0)return;let n=this.pending.get(t.id);if(!n)return;this.pending.delete(t.id),t.error?n.reject(Error(t.error.message)):n.resolve(t.result)}catch{}})}send(e,t){let n=this.nextId++,r=new Promise((t,r)=>{this.pending.set(n,{resolve:t,reject:r}),setTimeout(()=>{this.pending.delete(n)&&r(Error(`CDP ${e} 超时(10s)`))},1e4).unref?.()});return this.ws.send(JSON.stringify({id:n,method:e,params:t})),r}async evaluate(e){await this.send(`Runtime.enable`);let t=await this.send(`Runtime.evaluate`,{expression:e,returnByValue:!0,includeCommandLineAPI:!1});if(t.exceptionDetails)throw Error(`目标进程内求值异常: ${t.exceptionDetails.text}`);return t.result.value}close(){try{this.ws.close()}catch{}}};function s(e){return[`# 9229 已被占用或 CDP 不可用——macOS 原生工具等价配方(lesson_69):`,`sample ${e} 5 -file /dev/stdout | head -60 # CPU 调用栈(5s 采样)`,`vmmap --summary ${e} | head -30 # 内存分区(V8 heap 在 Memory Tag 255)`,`heap ${e} | head -40 # malloc zone 统计`].join(`
|
|
4
|
+
`)}export{s as a,r as i,t as n,i as o,n as r,a as s,o as t};
|
|
5
|
+
//# sourceMappingURL=doctor-cdp-K_mIFRfA.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"doctor-cdp-K_mIFRfA.js","names":[],"sources":["../src/commands/handlers/doctor-cdp.ts"],"sourcesContent":["/**\n * /doctor cdp 命令:CDP 探针(--url / --list-targets / --heap-sample / --evaluate)。\n */\nimport { execFileSync } from 'node:child_process'\n\nexport const INSPECTOR_PORT = 9229\nexport const INSPECTOR_HOST = '127.0.0.1'\n\nexport interface InspectorTarget {\n webSocketDebuggerUrl: string\n title: string\n /** /json 里的 title 通常含 pid(如 \"node[86152]\");此处解析出的 pid,解析失败为 undefined。 */\n pid?: number\n}\n\n/** Node ≥22 内建 WebSocket 存在性检查(启动前置,低版本给明确报错)。 */\nexport function assertBuiltinWebSocket(): void {\n if (typeof WebSocket !== 'function') {\n throw new Error(\n `otto doctor attach 需要 Node >= 22(内建 WebSocket);当前 ${process.version}。` +\n '低版本可改用手动配方:kill -USR1 <pid> 后用 Chrome DevTools 连 chrome://inspect。',\n )\n }\n}\n\n/** lsof 查 9229 端口占用者 pid;空闲返回 undefined。lsof 不可用(非 macOS/Linux)返回 null。 */\nexport function inspectorPortOwner(): number | undefined | null {\n try {\n const out = execFileSync('lsof', ['-nP', `-iTCP:${INSPECTOR_PORT}`, '-sTCP:LISTEN', '-t'], {\n encoding: 'utf-8',\n timeout: 5_000,\n }).trim()\n if (!out) return undefined\n const pid = Number(out.split('\\n')[0])\n return Number.isFinite(pid) ? pid : undefined\n } catch (error) {\n // lsof 无匹配时退出码 1(正常空闲);命令不存在等其他错误返回 null 让调用方降级。\n const status = (error as { status?: number }).status\n return status === 1 ? undefined : null\n }\n}\n\n/** 向目标进程发 SIGUSR1 打开 inspector,并轮询 /json 直到就绪(默认 3s 超时)。 */\nexport async function openInspector(pid: number, timeoutMs = 3_000): Promise<InspectorTarget> {\n process.kill(pid, 'SIGUSR1')\n const deadline = Date.now() + timeoutMs\n let lastError: unknown\n while (Date.now() < deadline) {\n try {\n const res = await fetch(`http://${INSPECTOR_HOST}:${INSPECTOR_PORT}/json`)\n const targets = (await res.json()) as Array<{ webSocketDebuggerUrl?: string; title?: string }>\n const target = targets.find((t) => t.webSocketDebuggerUrl)\n if (target?.webSocketDebuggerUrl) {\n const pidMatch = /\\[(\\d+)\\]/.exec(target.title ?? '')\n return {\n webSocketDebuggerUrl: target.webSocketDebuggerUrl,\n title: target.title ?? '',\n pid: pidMatch ? Number(pidMatch[1]) : undefined,\n }\n }\n } catch (error) {\n lastError = error\n }\n await new Promise((resolve) => setTimeout(resolve, 100))\n }\n throw new Error(\n `inspector 未在 ${timeoutMs}ms 内就绪(pid=${pid}):${lastError instanceof Error ? lastError.message : '目标未监听'}`,\n )\n}\n\n/**\n * 身份核对(lesson_69 第三步):/json title 含 pid 时必须与目标一致——\n * 端口被其他进程(如 Electron --inspect)占用时误连的数据会完全误导分析。\n */\nexport function verifyTargetIdentity(target: InspectorTarget, expectedPid: number): void {\n if (target.pid !== undefined && target.pid !== expectedPid) {\n throw new Error(\n `9229 端口上的 inspector 属于 pid=${target.pid}(\"${target.title}\"),非目标 pid=${expectedPid}——` +\n '端口被其他进程占用,拒绝连接(lesson_69:误连他进程数据会污染分析)。',\n )\n }\n}\n\n/** 极简 CDP 会话:内建 WebSocket + 自增 id 的请求-响应配对。 */\nexport class CdpSession {\n private ws!: WebSocket\n private nextId = 1\n private readonly pending = new Map<number, { resolve: (v: unknown) => void; reject: (e: Error) => void }>()\n\n async connect(url: string): Promise<void> {\n this.ws = new WebSocket(url)\n await new Promise<void>((resolve, reject) => {\n this.ws.addEventListener('open', () => resolve(), { once: true })\n this.ws.addEventListener('error', () => reject(new Error('CDP WebSocket 连接失败')), { once: true })\n })\n this.ws.addEventListener('message', (event) => {\n try {\n const msg = JSON.parse(String(event.data)) as { id?: number; result?: unknown; error?: { message: string } }\n if (msg.id === undefined) return // 事件通知,当前工具不消费\n const entry = this.pending.get(msg.id)\n if (!entry) return\n this.pending.delete(msg.id)\n if (msg.error) entry.reject(new Error(msg.error.message))\n else entry.resolve(msg.result)\n } catch { /* 非 JSON 帧忽略 */ }\n })\n }\n\n send<T = unknown>(method: string, params?: Record<string, unknown>): Promise<T> {\n const id = this.nextId++\n const promise = new Promise<T>((resolve, reject) => {\n this.pending.set(id, { resolve: resolve as (v: unknown) => void, reject })\n setTimeout(() => {\n if (this.pending.delete(id)) reject(new Error(`CDP ${method} 超时(10s)`))\n }, 10_000).unref?.()\n })\n this.ws.send(JSON.stringify({ id, method, params }))\n return promise\n }\n\n /** 在目标进程内求值表达式(returnByValue),返回结果值。 */\n async evaluate<T = unknown>(expression: string): Promise<T> {\n await this.send('Runtime.enable')\n const result = await this.send<{ result: { value?: T }; exceptionDetails?: { text: string } }>(\n 'Runtime.evaluate',\n { expression, returnByValue: true, includeCommandLineAPI: false },\n )\n if (result.exceptionDetails) throw new Error(`目标进程内求值异常: ${result.exceptionDetails.text}`)\n return result.result.value as T\n }\n\n close(): void {\n try { this.ws.close() } catch { /* 已断开 */ }\n }\n}\n\n/** macOS 原生工具降级路径(9229 被占/lsof 不可用时的等价输出,lesson_69 回退配方)。 */\nexport function nativeFallbackHint(pid: number): string {\n return [\n `# 9229 已被占用或 CDP 不可用——macOS 原生工具等价配方(lesson_69):`,\n `sample ${pid} 5 -file /dev/stdout | head -60 # CPU 调用栈(5s 采样)`,\n `vmmap --summary ${pid} | head -30 # 内存分区(V8 heap 在 Memory Tag 255)`,\n `heap ${pid} | head -40 # malloc zone 统计`,\n ].join('\\n')\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import{a as e,i as t,o as n,r,s as i,t as a}from"./doctor-cdp-K_mIFRfA.js";async function o(o,s){let c=o.filter(e=>!e.startsWith(`-`)),l=Number(c[0]);if(!Number.isFinite(l)||l<=0)return s.stderr(`用法: otto doctor heap <pid> [--window <秒,默认 10>]
|
|
3
|
+
`),1;let u=o.indexOf(`--window`),d=u===-1?10:Number(o[u+1])||10;try{r()}catch(e){return s.stderr(`${e instanceof Error?e.message:String(e)}\n`),1}let f=t();if(f!=null&&f!==l)return s.stderr(`inspector 端口被 pid=${f} 占用。\n${e(l)}\n`),1;let p=new a;try{let e=await n(l);i(e,l),await p.connect(e.webSocketDebuggerUrl),s.stdout(`采样中(${d}s 窗口,pid=${l})……\n`),await p.send(`HeapProfiler.enable`),await p.send(`HeapProfiler.startSampling`,{samplingInterval:16384}),await new Promise(e=>setTimeout(e,d*1e3));let{profile:t}=await p.send(`HeapProfiler.stopSampling`),r=new Map,a=e=>{if(e.selfSize>0){let t=`${e.callFrame.functionName||`(anonymous)`} @ ${e.callFrame.url.replace(/^file:\/\/.*\/(packages|node_modules)\//,`$1/`)||`(native)`}`;r.set(t,(r.get(t)??0)+e.selfSize)}for(let t of e.children??[])a(t)};a(t.head);let o=[...r.entries()].sort((e,t)=>t[1]-e[1]).slice(0,20),c=[...r.values()].reduce((e,t)=>e+t,0);s.stdout(`\n— Top 分配帧(采样窗口内共 ${(c/1048576).toFixed(1)}MB 采样分配)—\n`),s.stdout(`SIZE(MB) % FRAME
|
|
4
|
+
`);for(let[e,t]of o){let n=(t/1048576).toFixed(2),r=c>0?(t/c*100).toFixed(1):`0.0`;s.stdout(`${n.padEnd(9)} ${(r+`%`).padEnd(6)} ${e}\n`)}return s.stdout(`
|
|
5
|
+
判读提示(lesson_92):top 帧若是 measure @ node:internal/perf/performance → user-timing 无界缓冲
|
|
6
|
+
(React dev 构建特征,检查 NODE_ENV);若是业务代码帧 → 进入 otto doctor attach 看 active handles。
|
|
7
|
+
`),0}catch(t){return s.stderr(`otto doctor heap 失败: ${t instanceof Error?t.message:String(t)}\n`),s.stdout(`
|
|
8
|
+
降级配方:
|
|
9
|
+
`+e(l)+`
|
|
10
|
+
`),1}finally{p.close()}}export{o as runDoctorHeap};
|
|
11
|
+
//# sourceMappingURL=doctor-heap-Rr2nNvcw.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"doctor-heap-Rr2nNvcw.js","names":[],"sources":["../src/commands/handlers/doctor-heap.ts"],"sourcesContent":["/**\n * doctor-heap.ts — 短窗 heap 分配采样 + top 分配帧(RFC-200 D1 / T200-3)。\n *\n * lesson_92 方法论固化:「heap sampling 的 top 分配帧直接指向驻留源,比逐结构猜测快得多」\n * ——2GB user-timing 驻留正是靠 top 帧 `measure @ node:internal/perf/performance` 一击定位。\n * 流程:安全附加(复用 doctor-cdp 三步)→ HeapProfiler.startSampling → 等待采样窗口 →\n * stopSampling → 聚合调用树 → 按 selfSize 排序输出 top 帧。只读,不触发 GC。\n */\nimport {\n CdpSession,\n assertBuiltinWebSocket,\n inspectorPortOwner,\n nativeFallbackHint,\n openInspector,\n verifyTargetIdentity,\n} from './doctor-cdp'\n\nimport type { DoctorDeps } from '../doctor'\n\ninterface SamplingNode {\n callFrame: { functionName: string; url: string }\n selfSize: number\n children?: SamplingNode[]\n}\n\nexport async function runDoctorHeap(args: string[], deps: DoctorDeps): Promise<number> {\n const positional = args.filter((a) => !a.startsWith('-'))\n const pid = Number(positional[0])\n if (!Number.isFinite(pid) || pid <= 0) {\n deps.stderr('用法: otto doctor heap <pid> [--window <秒,默认 10>]\\n')\n return 1\n }\n\n const windowIdx = args.indexOf('--window')\n const windowSec = windowIdx !== -1 ? Number(args[windowIdx + 1]) || 10 : 10\n\n try {\n assertBuiltinWebSocket()\n } catch (error) {\n deps.stderr(`${error instanceof Error ? error.message : String(error)}\\n`)\n return 1\n }\n\n const owner = inspectorPortOwner()\n if (owner != null && owner !== pid) {\n deps.stderr(`inspector 端口被 pid=${owner} 占用。\\n${nativeFallbackHint(pid)}\\n`)\n return 1\n }\n\n const session = new CdpSession()\n try {\n const target = await openInspector(pid)\n verifyTargetIdentity(target, pid)\n await session.connect(target.webSocketDebuggerUrl)\n\n deps.stdout(`采样中(${windowSec}s 窗口,pid=${pid})……\\n`)\n await session.send('HeapProfiler.enable')\n await session.send('HeapProfiler.startSampling', { samplingInterval: 16384 })\n await new Promise((resolve) => setTimeout(resolve, windowSec * 1000))\n const { profile } = await session.send<{ profile: { head: SamplingNode } }>('HeapProfiler.stopSampling')\n\n // 聚合:同一 functionName@url 的 selfSize 求和(top 帧视角,不展开调用树)\n const frames = new Map<string, number>()\n const walk = (node: SamplingNode): void => {\n if (node.selfSize > 0) {\n const fn = node.callFrame.functionName || '(anonymous)'\n const url = node.callFrame.url.replace(/^file:\\/\\/.*\\/(packages|node_modules)\\//, '$1/')\n const key = `${fn} @ ${url || '(native)'}`\n frames.set(key, (frames.get(key) ?? 0) + node.selfSize)\n }\n for (const child of node.children ?? []) walk(child)\n }\n walk(profile.head)\n\n const top = [...frames.entries()].sort((a, b) => b[1] - a[1]).slice(0, 20)\n const total = [...frames.values()].reduce((a, b) => a + b, 0)\n deps.stdout(`\\n— Top 分配帧(采样窗口内共 ${(total / 1048576).toFixed(1)}MB 采样分配)—\\n`)\n deps.stdout('SIZE(MB) % FRAME\\n')\n for (const [frame, size] of top) {\n const mb = (size / 1048576).toFixed(2)\n const pct = total > 0 ? ((size / total) * 100).toFixed(1) : '0.0'\n deps.stdout(`${mb.padEnd(9)} ${(pct + '%').padEnd(6)} ${frame}\\n`)\n }\n deps.stdout(\n '\\n判读提示(lesson_92):top 帧若是 measure @ node:internal/perf/performance → user-timing 无界缓冲\\n' +\n '(React dev 构建特征,检查 NODE_ENV);若是业务代码帧 → 进入 otto doctor attach 看 active handles。\\n',\n )\n return 0\n } catch (error) {\n deps.stderr(`otto doctor heap 失败: ${error instanceof Error ? error.message : String(error)}\\n`)\n deps.stdout('\\n降级配方:\\n' + nativeFallbackHint(pid) + '\\n')\n return 1\n } finally {\n session.close()\n }\n}\n"],"mappings":";2EAyBA,eAAsB,EAAc,EAAgB,EAAmC,CACrF,IAAM,EAAa,EAAK,OAAQ,GAAM,CAAC,EAAE,WAAW,IAAI,CAAC,CACnD,EAAM,OAAO,EAAW,GAAG,CACjC,GAAI,CAAC,OAAO,SAAS,EAAI,EAAI,GAAO,EAElC,OADA,EAAK,OAAO;EAAoD,CACzD,EAGT,IAAM,EAAY,EAAK,QAAQ,WAAW,CACpC,EAAY,IAAc,GAAyC,GAApC,OAAO,EAAK,EAAY,GAAG,EAAI,GAEpE,GAAI,CACF,GAAwB,OACjB,EAAO,CAEd,OADA,EAAK,OAAO,GAAG,aAAiB,MAAQ,EAAM,QAAU,OAAO,EAAM,CAAC,IAAI,CACnE,EAGT,IAAM,EAAQ,GAAoB,CAClC,GAAI,GAAS,MAAQ,IAAU,EAE7B,OADA,EAAK,OAAO,qBAAqB,EAAM,QAAQ,EAAmB,EAAI,CAAC,IAAI,CACpE,EAGT,IAAM,EAAU,IAAI,EACpB,GAAI,CACF,IAAM,EAAS,MAAM,EAAc,EAAI,CACvC,EAAqB,EAAQ,EAAI,CACjC,MAAM,EAAQ,QAAQ,EAAO,qBAAqB,CAElD,EAAK,OAAO,OAAO,EAAU,WAAW,EAAI,OAAO,CACnD,MAAM,EAAQ,KAAK,sBAAsB,CACzC,MAAM,EAAQ,KAAK,6BAA8B,CAAE,iBAAkB,MAAO,CAAC,CAC7E,MAAM,IAAI,QAAS,GAAY,WAAW,EAAS,EAAY,IAAK,CAAC,CACrE,GAAM,CAAE,WAAY,MAAM,EAAQ,KAA0C,4BAA4B,CAGlG,EAAS,IAAI,IACb,EAAQ,GAA6B,CACzC,GAAI,EAAK,SAAW,EAAG,CAGrB,IAAM,EAAM,GAFD,EAAK,UAAU,cAAgB,cAExB,KADN,EAAK,UAAU,IAAI,QAAQ,0CAA2C,MAAM,EAC1D,aAC9B,EAAO,IAAI,GAAM,EAAO,IAAI,EAAI,EAAI,GAAK,EAAK,SAAS,CAEzD,IAAK,IAAM,KAAS,EAAK,UAAY,EAAE,CAAE,EAAK,EAAM,EAEtD,EAAK,EAAQ,KAAK,CAElB,IAAM,EAAM,CAAC,GAAG,EAAO,SAAS,CAAC,CAAC,MAAM,EAAG,IAAM,EAAE,GAAK,EAAE,GAAG,CAAC,MAAM,EAAG,GAAG,CACpE,EAAQ,CAAC,GAAG,EAAO,QAAQ,CAAC,CAAC,QAAQ,EAAG,IAAM,EAAI,EAAG,EAAE,CAC7D,EAAK,OAAO,uBAAuB,EAAQ,SAAS,QAAQ,EAAE,CAAC,aAAa,CAC5E,EAAK,OAAO;EAA2B,CACvC,IAAK,GAAM,CAAC,EAAO,KAAS,EAAK,CAC/B,IAAM,GAAM,EAAO,SAAS,QAAQ,EAAE,CAChC,EAAM,EAAQ,GAAM,EAAO,EAAS,KAAK,QAAQ,EAAE,CAAG,MAC5D,EAAK,OAAO,GAAG,EAAG,OAAO,EAAE,CAAC,IAAI,EAAM,KAAK,OAAO,EAAE,CAAC,GAAG,EAAM,IAAI,CAMpE,OAJA,EAAK,OACH;;;EAED,CACM,QACA,EAAO,CAGd,OAFA,EAAK,OAAO,wBAAwB,aAAiB,MAAQ,EAAM,QAAU,OAAO,EAAM,CAAC,IAAI,CAC/F,EAAK,OAAO;;EAAc,EAAmB,EAAI,CAAG;EAAK,CAClD,SACC,CACR,EAAQ,OAAO"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import{i as e,n as t}from"./doctor-cdp-K_mIFRfA.js";import{listOttoProcesses as n}from"@x-otto/env";async function r(r,i){let a=r.includes(`--json`),o=n(),s=e(),c=o.map(e=>({pid:e.pid,ppid:e.ppid,rssMb:Math.round(e.rssKb/1024),uptimeH:Number((e.etimeMs/36e5).toFixed(1)),sessionId:e.sessionId?e.sessionId.slice(0,8):void 0,generation:e.generation,zombieSuspect:e.ppid===1&&e.etimeMs>216e5,holdsInspectorPort:s===e.pid}));if(a)return i.stdout(JSON.stringify({processes:c,inspectorPort:{port:t,owner:s??null,ownedByForeignProcess:s!=null&&!o.some(e=>e.pid===s)}},null,2)+`
|
|
3
|
+
`),0;if(c.length===0)i.stdout(`otto doctor ps: 未发现运行中的 otto 主进程
|
|
4
|
+
`);else{i.stdout(`PID PPID RSS(MB) UP(h) SESSION GEN FLAGS
|
|
5
|
+
`);for(let e of c){let t=[e.zombieSuspect?`ZOMBIE?`:``,e.holdsInspectorPort?`INSPECTOR`:``].filter(Boolean).join(`,`)||`-`;i.stdout(`${String(e.pid).padEnd(8)} ${String(e.ppid).padEnd(6)} ${String(e.rssMb).padEnd(8)} ${String(e.uptimeH).padEnd(7)} ${(e.sessionId??`-`).padEnd(9)} ${String(e.generation??`-`).padEnd(4)} ${t}\n`)}}if(s===null)i.stdout(`\ninspector 端口 ${t}:无法检测(lsof 不可用)\n`);else if(s===void 0)i.stdout(`\ninspector 端口 ${t}:空闲(可安全 otto doctor attach <pid>)\n`);else{let e=!o.some(e=>e.pid===s);i.stdout(`\ninspector 端口 ${t}:被 pid=${s} 占用`+(e?`(⚠ 非 otto 进程——attach 前须先让占用者释放,lesson_69)
|
|
6
|
+
`:`(otto 进程)
|
|
7
|
+
`))}return 0}export{r as runDoctorPs};
|
|
8
|
+
//# sourceMappingURL=doctor-ps-OL3FGQYu.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"doctor-ps-OL3FGQYu.js","names":[],"sources":["../src/commands/handlers/doctor-ps.ts"],"sourcesContent":["/**\n * /doctor ps 命令:进程舰队巡检(Tree/RSS/重启代际)。\n */\nimport { listOttoProcesses } from '@x-otto/env'\n\nimport { INSPECTOR_PORT, inspectorPortOwner } from './doctor-cdp'\n\nimport type { DoctorDeps } from '../doctor'\n\n/** 僵尸嫌疑阈值:ppid=1(孤儿)且存活超 6 小时(lesson_85 的 19h 僵尸群特征收窄)。 */\nconst ZOMBIE_ETIME_MS = 6 * 3600 * 1000\n\nexport async function runDoctorPs(args: string[], deps: DoctorDeps): Promise<number> {\n const json = args.includes('--json')\n const procs = listOttoProcesses()\n const portOwner = inspectorPortOwner()\n\n const rows = procs.map((p) => ({\n pid: p.pid,\n ppid: p.ppid,\n rssMb: Math.round(p.rssKb / 1024),\n uptimeH: Number((p.etimeMs / 3600_000).toFixed(1)),\n sessionId: p.sessionId ? p.sessionId.slice(0, 8) : undefined,\n generation: p.generation,\n /** 诊断字段:孤儿 + 长存活 = 僵尸嫌疑(建议人工核实后处理,本命令不动手)。 */\n zombieSuspect: p.ppid === 1 && p.etimeMs > ZOMBIE_ETIME_MS,\n /** 诊断字段:该进程是否持有 9229 inspector 端口。 */\n holdsInspectorPort: portOwner === p.pid,\n }))\n\n if (json) {\n deps.stdout(JSON.stringify({\n processes: rows,\n inspectorPort: {\n port: INSPECTOR_PORT,\n owner: portOwner ?? null,\n // 端口被非 otto 进程占用(如 Electron --inspect)是 attach 的头号陷阱(lesson_69)\n ownedByForeignProcess: portOwner != null && !procs.some((p) => p.pid === portOwner),\n },\n }, null, 2) + '\\n')\n return 0\n }\n\n if (rows.length === 0) {\n deps.stdout('otto doctor ps: 未发现运行中的 otto 主进程\\n')\n } else {\n deps.stdout('PID PPID RSS(MB) UP(h) SESSION GEN FLAGS\\n')\n for (const r of rows) {\n const flags = [\n r.zombieSuspect ? 'ZOMBIE?' : '',\n r.holdsInspectorPort ? 'INSPECTOR' : '',\n ].filter(Boolean).join(',') || '-'\n deps.stdout(\n `${String(r.pid).padEnd(8)} ${String(r.ppid).padEnd(6)} ${String(r.rssMb).padEnd(8)} ` +\n `${String(r.uptimeH).padEnd(7)} ${(r.sessionId ?? '-').padEnd(9)} ${String(r.generation ?? '-').padEnd(4)} ${flags}\\n`,\n )\n }\n }\n\n if (portOwner === null) {\n deps.stdout(`\\ninspector 端口 ${INSPECTOR_PORT}:无法检测(lsof 不可用)\\n`)\n } else if (portOwner === undefined) {\n deps.stdout(`\\ninspector 端口 ${INSPECTOR_PORT}:空闲(可安全 otto doctor attach <pid>)\\n`)\n } else {\n const foreign = !procs.some((p) => p.pid === portOwner)\n deps.stdout(\n `\\ninspector 端口 ${INSPECTOR_PORT}:被 pid=${portOwner} 占用` +\n (foreign ? '(⚠ 非 otto 进程——attach 前须先让占用者释放,lesson_69)\\n' : '(otto 进程)\\n'),\n )\n }\n return 0\n}\n"],"mappings":";oGAYA,eAAsB,EAAY,EAAgB,EAAmC,CACnF,IAAM,EAAO,EAAK,SAAS,SAAS,CAC9B,EAAQ,GAAmB,CAC3B,EAAY,GAAoB,CAEhC,EAAO,EAAM,IAAK,IAAO,CAC7B,IAAK,EAAE,IACP,KAAM,EAAE,KACR,MAAO,KAAK,MAAM,EAAE,MAAQ,KAAK,CACjC,QAAS,QAAQ,EAAE,QAAU,MAAU,QAAQ,EAAE,CAAC,CAClD,UAAW,EAAE,UAAY,EAAE,UAAU,MAAM,EAAG,EAAE,CAAG,IAAA,GACnD,WAAY,EAAE,WAEd,cAAe,EAAE,OAAS,GAAK,EAAE,QAAU,MAE3C,mBAAoB,IAAc,EAAE,IACrC,EAAE,CAEH,GAAI,EAUF,OATA,EAAK,OAAO,KAAK,UAAU,CACzB,UAAW,EACX,cAAe,CACb,KAAM,EACN,MAAO,GAAa,KAEpB,sBAAuB,GAAa,MAAQ,CAAC,EAAM,KAAM,GAAM,EAAE,MAAQ,EAAU,CACpF,CACF,CAAE,KAAM,EAAE,CAAG;EAAK,CACZ,EAGT,GAAI,EAAK,SAAW,EAClB,EAAK,OAAO;EAAqC,KAC5C,CACL,EAAK,OAAO;EAA0D,CACtE,IAAK,IAAM,KAAK,EAAM,CACpB,IAAM,EAAQ,CACZ,EAAE,cAAgB,UAAY,GAC9B,EAAE,mBAAqB,YAAc,GACtC,CAAC,OAAO,QAAQ,CAAC,KAAK,IAAI,EAAI,IAC/B,EAAK,OACH,GAAG,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC,GAAG,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,CAAC,GAAG,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC,GACjF,OAAO,EAAE,QAAQ,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,WAAa,KAAK,OAAO,EAAE,CAAC,GAAG,OAAO,EAAE,YAAc,IAAI,CAAC,OAAO,EAAE,CAAC,GAAG,EAAM,IACpH,EAIL,GAAI,IAAc,KAChB,EAAK,OAAO,kBAAkB,EAAe,mBAAmB,SACvD,IAAc,IAAA,GACvB,EAAK,OAAO,kBAAkB,EAAe,qCAAqC,KAC7E,CACL,IAAM,EAAU,CAAC,EAAM,KAAM,GAAM,EAAE,MAAQ,EAAU,CACvD,EAAK,OACH,kBAAkB,EAAe,SAAS,EAAU,MACnD,EAAU;EAAgD;GAC5D,CAEH,MAAO"}
|