@x-otto/cli 0.0.1-alpha.49 → 0.0.1-alpha.50

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/dist/{auth-command-D1T16ed-.js → auth-command-BYaFM76w.js} +2 -2
  2. package/dist/{auth-command-D1T16ed-.js.map → auth-command-BYaFM76w.js.map} +1 -1
  3. package/dist/backend-handler-DUafuNWw.js +3 -0
  4. package/dist/backend-handler-DUafuNWw.js.map +1 -0
  5. package/dist/build-id-DjE6eVpQ.js +3 -0
  6. package/dist/build-id-DjE6eVpQ.js.map +1 -0
  7. package/dist/bundled-extensions/plugin-feedback/otto-plugin.json +24 -0
  8. package/dist/bundled-extensions/plugin-feedback/panels/feedback.tsx +348 -0
  9. package/dist/bundled-extensions/plugin-feedback/panels/panel-host-globals.d.ts +67 -0
  10. package/dist/bundled-extensions/plugin-feedback/panels-dist/feedback.mjs +257 -0
  11. package/dist/bundled-extensions/plugin-feedback/plugin-dist/meta.json +1 -0
  12. package/dist/bundled-extensions/plugin-feedback/plugin-dist/plugin.cjs +11304 -0
  13. package/dist/bundled-extensions/plugin-feedback/plugin.ts +198 -0
  14. package/dist/bundled-extensions/plugin-feedback/src/coding-adapter.ts +183 -0
  15. package/dist/bundled-extensions/plugin-feedback/src/coding-lister.ts +26 -0
  16. package/dist/bundled-extensions/plugin-feedback/src/github-lister.ts +230 -0
  17. package/dist/bundled-extensions/plugin-feedback/src/issue-aggregator.ts +109 -0
  18. package/dist/bundled-extensions/plugin-feedback/src/issue-submitter.ts +241 -0
  19. package/dist/bundled-extensions/plugin-feedback/src/platform-resolver.ts +76 -0
  20. package/dist/bundled-extensions/plugin-feedback/src/pulse-survey-sink.ts +114 -0
  21. package/dist/bundled-extensions/plugin-feedback/src/resolved-sink.ts +163 -0
  22. package/dist/bundled-extensions/plugin-otto-wire-protocols/plugin-dist/meta.json +1 -1
  23. package/dist/bundled-extensions/plugin-otto-wire-protocols/plugin-dist/plugin.cjs +605 -725
  24. package/dist/bundled-extensions/plugin-otto-wire-protocols/src/anthropic-messages.ts +4 -22
  25. package/dist/bundled-extensions/plugin-otto-wire-protocols/src/openai-completions.ts +5 -5
  26. package/dist/bundled-extensions/plugin-otto-wire-protocols/src/openai-responses.ts +3 -15
  27. package/dist/command-dispatcher-BLT8IO4K.js +18 -0
  28. package/dist/command-dispatcher-BLT8IO4K.js.map +1 -0
  29. package/dist/create-host-app-CbJ8ptv-.js +3 -0
  30. package/dist/{create-host-app-BXQ8bU-B.js.map → create-host-app-CbJ8ptv-.js.map} +1 -1
  31. package/dist/{daemon-base-Y1XI61JY.js → daemon-base-D015ew5y.js} +1 -1
  32. package/dist/{daemon-base-Y1XI61JY.js.map → daemon-base-D015ew5y.js.map} +1 -1
  33. package/dist/{doctor-DXBLkbkP.js → doctor-EndB-ycb.js} +3 -3
  34. package/dist/{doctor-DXBLkbkP.js.map → doctor-EndB-ycb.js.map} +1 -1
  35. package/dist/{doctor-attach-DEMuyBOk.js → doctor-attach-DEoE7eFx.js} +2 -2
  36. package/dist/{doctor-attach-DEMuyBOk.js.map → doctor-attach-DEoE7eFx.js.map} +1 -1
  37. package/dist/{doctor-cdp-CnJUVNOB.js → doctor-cdp-Cj9IFpEP.js} +1 -1
  38. package/dist/{doctor-cdp-CnJUVNOB.js.map → doctor-cdp-Cj9IFpEP.js.map} +1 -1
  39. package/dist/{doctor-heap-Eu3LZj-F.js → doctor-heap-SIxiRvL8.js} +2 -2
  40. package/dist/{doctor-heap-Eu3LZj-F.js.map → doctor-heap-SIxiRvL8.js.map} +1 -1
  41. package/dist/{doctor-ps-JBgCH77n.js → doctor-ps-DNVOF6yY.js} +2 -2
  42. package/dist/{doctor-ps-JBgCH77n.js.map → doctor-ps-DNVOF6yY.js.map} +1 -1
  43. package/dist/extension-CoJqqktf.js +2 -0
  44. package/dist/extension-Dpn8gltG.js +179 -0
  45. package/dist/extension-Dpn8gltG.js.map +1 -0
  46. package/dist/feedback-QcONRnO3.js +30 -0
  47. package/dist/feedback-QcONRnO3.js.map +1 -0
  48. package/dist/feedback-backend-methods-BNm38ddr.js +3 -0
  49. package/dist/feedback-backend-methods-BNm38ddr.js.map +1 -0
  50. package/dist/{feishu-setup-5ZoxtH0J.js → feishu-setup-DbAq_BRH.js} +1 -1
  51. package/dist/feishu-setup-DbAq_BRH.js.map +1 -0
  52. package/dist/{ftue-doctor-recovery-8FeDBFOc.js → ftue-doctor-recovery-CKTMnxM8.js} +1 -1
  53. package/dist/{ftue-doctor-recovery-8FeDBFOc.js.map → ftue-doctor-recovery-CKTMnxM8.js.map} +1 -1
  54. package/dist/index.d.ts.map +1 -1
  55. package/dist/index.js +11 -11
  56. package/dist/index.js.map +1 -1
  57. package/dist/install-CEPczuet.js +11 -0
  58. package/dist/install-CEPczuet.js.map +1 -0
  59. package/dist/install-CG6b6fox.js +2 -0
  60. package/dist/interactive-schedule-Bv__2Kwc.js +3 -0
  61. package/dist/interactive-schedule-Bv__2Kwc.js.map +1 -0
  62. package/dist/{mcp-Cm4-OhXI.js → mcp-DjReRjuu.js} +1 -1
  63. package/dist/{mcp-Cm4-OhXI.js.map → mcp-DjReRjuu.js.map} +1 -1
  64. package/dist/{mcp-server-Dpza3yn2.js → mcp-server-Rr__aWO7.js} +3 -3
  65. package/dist/mcp-server-Rr__aWO7.js.map +1 -0
  66. package/dist/{migrate-RlBJqJnB.js → migrate-BBi3Txu2.js} +1 -1
  67. package/dist/{migrate-RlBJqJnB.js.map → migrate-BBi3Txu2.js.map} +1 -1
  68. package/dist/{observe-Dxfud-t7.js → observe-BqLCpmFD.js} +2 -2
  69. package/dist/{observe-Dxfud-t7.js.map → observe-BqLCpmFD.js.map} +1 -1
  70. package/dist/parse-cli-Db8pF_Ya.js +3 -0
  71. package/dist/parse-cli-Db8pF_Ya.js.map +1 -0
  72. package/dist/{persistence-CRFhOYx6.js → persistence-BvLRnpfi.js} +2 -2
  73. package/dist/{persistence-CRFhOYx6.js.map → persistence-BvLRnpfi.js.map} +1 -1
  74. package/dist/{plugin-assembly-check-DEozmcJ7.js → plugin-assembly-check-BntT5W6H.js} +1 -1
  75. package/dist/{plugin-assembly-check-DEozmcJ7.js.map → plugin-assembly-check-BntT5W6H.js.map} +1 -1
  76. package/dist/{proxy-evg7ZvoD.js → proxy-CFZDVIwI.js} +1 -1
  77. package/dist/proxy-CFZDVIwI.js.map +1 -0
  78. package/dist/proxy-CkSf8ziQ.js +2 -0
  79. package/dist/{ps-DiKCLKbs.js → ps-BVKv6rAo.js} +1 -1
  80. package/dist/{ps-DiKCLKbs.js.map → ps-BVKv6rAo.js.map} +1 -1
  81. package/dist/{registry-BdCcwyGy.js → registry-BUvu-es1.js} +4 -4
  82. package/dist/registry-BUvu-es1.js.map +1 -0
  83. package/dist/registry-DYHR-ONu.js +2 -0
  84. package/dist/{run-attach-DDpMk2_N.js → run-attach-X35puJ_n.js} +2 -2
  85. package/dist/{run-attach-DDpMk2_N.js.map → run-attach-X35puJ_n.js.map} +1 -1
  86. package/dist/schedule-Ke0jwatM.js +5 -0
  87. package/dist/schedule-Ke0jwatM.js.map +1 -0
  88. package/dist/serve-K6BOT5K_.js +21 -0
  89. package/dist/serve-K6BOT5K_.js.map +1 -0
  90. package/dist/{service-client-Cr4sxDVM.js → service-client-cfUUP4EE.js} +1 -1
  91. package/dist/{service-client-Cr4sxDVM.js.map → service-client-cfUUP4EE.js.map} +1 -1
  92. package/dist/{version-BrSIyY6s.js → version-DrCy32wR.js} +2 -2
  93. package/dist/{version-BrSIyY6s.js.map → version-DrCy32wR.js.map} +1 -1
  94. package/package.json +16 -13
  95. package/dist/build-id-DxfuzdUE.js +0 -3
  96. package/dist/build-id-DxfuzdUE.js.map +0 -1
  97. package/dist/command-dispatcher-DTOPjtZP.js +0 -21
  98. package/dist/command-dispatcher-DTOPjtZP.js.map +0 -1
  99. package/dist/create-host-app-BXQ8bU-B.js +0 -3
  100. package/dist/create-host-app-DG58-vRA.js +0 -2
  101. package/dist/extension-CTyrcdT1.js +0 -2
  102. package/dist/extension-DVEml5IN.js +0 -179
  103. package/dist/extension-DVEml5IN.js.map +0 -1
  104. package/dist/feedback-DvWUvSc4.js +0 -21
  105. package/dist/feedback-DvWUvSc4.js.map +0 -1
  106. package/dist/feedback-DzPLfcfN.js +0 -2
  107. package/dist/feishu-setup-5ZoxtH0J.js.map +0 -1
  108. package/dist/install-DlVJRHRT.js +0 -11
  109. package/dist/install-DlVJRHRT.js.map +0 -1
  110. package/dist/install-WCcQ9k9M.js +0 -2
  111. package/dist/mcp-server-Dpza3yn2.js.map +0 -1
  112. package/dist/parse-cli-BvcxTK88.js +0 -3
  113. package/dist/parse-cli-BvcxTK88.js.map +0 -1
  114. package/dist/proxy-BvdBsB1O.js +0 -2
  115. package/dist/proxy-evg7ZvoD.js.map +0 -1
  116. package/dist/registry-BOX7DaxS.js +0 -2
  117. package/dist/registry-BdCcwyGy.js.map +0 -1
  118. package/dist/serve-Ke0nwN9Y.js +0 -21
  119. package/dist/serve-Ke0nwN9Y.js.map +0 -1
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import{at as e}from"./command-dispatcher-BLT8IO4K.js";import{t}from"./bundled-extensions-C3n8jg20.js";import{createApp as n}from"@x-otto/coding";var r=e({createHostApp:()=>a,hostSelfDescribingOptions:()=>i});function i(){let e=t();return{pluginBundledDirs:e?[e]:[]}}function a(e){return n({...i(),...e})}export{r as n,a as t};
3
+ //# sourceMappingURL=create-host-app-CbJ8ptv-.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"create-host-app-BXQ8bU-B.js","names":[],"sources":["../src/create-host-app.ts"],"sourcesContent":["/**\n * create-host-app.ts —— RFC-364 M1:cli 侧**唯一**的 App 装配入口。\n *\n * ## 要解决的问题\n * `createApp()` 有一类参数属于「**宿主自描述**」——otto 这个发布物自己带了什么、\n * 布局长什么样(当前是 `pluginBundledDirs`,即随 cli 分发的内置扩展目录)。这类信息\n * 调用方无从得知、也不该关心,但它此前是每个调用点都必须**记得传**的可选参数。\n *\n * 后果(RFC-364 §1.2 实测):RFC-341 引入 bundled 层时只接线了 `cli.ts` 一处,\n * `serve`/`feedback`/`auth` 三处全漏,表现为**静默少装插件**:\n *\n * ```\n * [serve 方式] createApp({workspaceDir}) → activePlugins: 12, bundled: []\n * [cli 方式] createApp({..., pluginBundledDirs}) → activePlugins: 12, bundled: ['plugin-otto-wire-protocols'](skill-inductor 2026-08-13 起外置)\n * ```\n *\n * 纯 npm 安装环境下更严重:任何 `dependsOn` bundled 插件的第三方 provider 插件会被\n * 拓扑排序判定 missingDeps 并 fail-closed 剔除。\n *\n * ## 为什么是「收敛装配点」而不是「把知识搬到下游包」\n * 初版方案曾想把 `bundledExtensionsDir()` 下沉到 `@x-otto/env`(coding 与 cli 的共同\n * 依赖),已**实测证伪**:该函数靠 `import.meta.url` 相对定位 cli 自己的 dist,而 env\n * 与 cli 的物理相对关系两种形态下都不固定——\n * pnpm workspace: packages/cli/node_modules/@x-otto/env -> ../../../env(符号链接)\n * npm 安装形态 : node_modules/@x-otto/env(hoist,与 cli/dist 无固定相对关系)\n * 即「宿主发布物布局」这个知识**只有 cli 有资格拥有**,搬到任何下游包都会把确定的 bug\n * 换成更隐蔽的路径 bug。所以正解是收敛**装配点**,而非搬迁**知识**。\n *\n * ## 契约(RFC-364 §4 规则 1)\n * 宿主自描述参数不得做成调用方必传参数。cli 内部**任何**需要 App 的地方都应调用本函数,\n * 不得直接 `createApp()`——新增入口默认就是对的,而不是\"新增一次漏传机会\"。\n * 有架构门禁测试钉住这条(`create-host-app-sole-assembly.test.ts`)。\n */\nimport { createApp } from '@x-otto/coding'\n\nimport { bundledExtensionsDir } from './bundled-extensions'\n\nimport type { App, AppOptions } from '@x-otto/coding'\n\n/**\n * 宿主自描述参数——「otto 这个发布物自己带了什么」。\n *\n * 单独导出而非内联,是因为 engine 子进程(`coding/src/engine-main.ts`,在 coding 包内、\n * 拿不到 cli 的 dist 布局)需要由父进程把该值显式下传(RFC-364 T364-03)。\n */\nexport function hostSelfDescribingOptions(): Pick<AppOptions, 'pluginBundledDirs'> {\n const dir = bundledExtensionsDir()\n return { pluginBundledDirs: dir ? [dir] : [] }\n}\n\n/**\n * 装配一个 App:调用方给业务参数,宿主自描述参数由本函数补齐。\n *\n * 调用方**可以**显式覆盖 `pluginBundledDirs`(测试隔离、非常规部署),\n * 显式传入时以调用方为准——保留逃生舱,但缺省不再是\"什么都没有\"。\n */\nexport function createHostApp(options: AppOptions): App {\n return createApp({\n ...hostSelfDescribingOptions(),\n ...options,\n })\n}\n"],"mappings":";gGA6CA,SAAgB,GAAmE,CACjF,IAAM,EAAM,GAAsB,CAClC,MAAO,CAAE,kBAAmB,EAAM,CAAC,EAAI,CAAG,EAAE,CAAE,CAShD,SAAgB,EAAc,EAA0B,CACtD,OAAO,EAAU,CACf,GAAG,GAA2B,CAC9B,GAAG,EACJ,CAAC"}
1
+ {"version":3,"file":"create-host-app-CbJ8ptv-.js","names":[],"sources":["../src/create-host-app.ts"],"sourcesContent":["/**\n * create-host-app.ts —— RFC-364 M1:cli 侧**唯一**的 App 装配入口。\n *\n * ## 要解决的问题\n * `createApp()` 有一类参数属于「**宿主自描述**」——otto 这个发布物自己带了什么、\n * 布局长什么样(当前是 `pluginBundledDirs`,即随 cli 分发的内置扩展目录)。这类信息\n * 调用方无从得知、也不该关心,但它此前是每个调用点都必须**记得传**的可选参数。\n *\n * 后果(RFC-364 §1.2 实测):RFC-341 引入 bundled 层时只接线了 `cli.ts` 一处,\n * `serve`/`feedback`/`auth` 三处全漏,表现为**静默少装插件**:\n *\n * ```\n * [serve 方式] createApp({workspaceDir}) → activePlugins: 12, bundled: []\n * [cli 方式] createApp({..., pluginBundledDirs}) → activePlugins: 12, bundled: ['plugin-otto-wire-protocols'](skill-inductor 2026-08-13 起外置)\n * ```\n *\n * 纯 npm 安装环境下更严重:任何 `dependsOn` bundled 插件的第三方 provider 插件会被\n * 拓扑排序判定 missingDeps 并 fail-closed 剔除。\n *\n * ## 为什么是「收敛装配点」而不是「把知识搬到下游包」\n * 初版方案曾想把 `bundledExtensionsDir()` 下沉到 `@x-otto/env`(coding 与 cli 的共同\n * 依赖),已**实测证伪**:该函数靠 `import.meta.url` 相对定位 cli 自己的 dist,而 env\n * 与 cli 的物理相对关系两种形态下都不固定——\n * pnpm workspace: packages/cli/node_modules/@x-otto/env -> ../../../env(符号链接)\n * npm 安装形态 : node_modules/@x-otto/env(hoist,与 cli/dist 无固定相对关系)\n * 即「宿主发布物布局」这个知识**只有 cli 有资格拥有**,搬到任何下游包都会把确定的 bug\n * 换成更隐蔽的路径 bug。所以正解是收敛**装配点**,而非搬迁**知识**。\n *\n * ## 契约(RFC-364 §4 规则 1)\n * 宿主自描述参数不得做成调用方必传参数。cli 内部**任何**需要 App 的地方都应调用本函数,\n * 不得直接 `createApp()`——新增入口默认就是对的,而不是\"新增一次漏传机会\"。\n * 有架构门禁测试钉住这条(`create-host-app-sole-assembly.test.ts`)。\n */\nimport { createApp } from '@x-otto/coding'\n\nimport { bundledExtensionsDir } from './bundled-extensions'\n\nimport type { App, AppOptions } from '@x-otto/coding'\n\n/**\n * 宿主自描述参数——「otto 这个发布物自己带了什么」。\n *\n * 单独导出而非内联,是因为 engine 子进程(`coding/src/engine-main.ts`,在 coding 包内、\n * 拿不到 cli 的 dist 布局)需要由父进程把该值显式下传(RFC-364 T364-03)。\n */\nexport function hostSelfDescribingOptions(): Pick<AppOptions, 'pluginBundledDirs'> {\n const dir = bundledExtensionsDir()\n return { pluginBundledDirs: dir ? [dir] : [] }\n}\n\n/**\n * 装配一个 App:调用方给业务参数,宿主自描述参数由本函数补齐。\n *\n * 调用方**可以**显式覆盖 `pluginBundledDirs`(测试隔离、非常规部署),\n * 显式传入时以调用方为准——保留逃生舱,但缺省不再是\"什么都没有\"。\n */\nexport function createHostApp(options: AppOptions): App {\n return createApp({\n ...hostSelfDescribingOptions(),\n ...options,\n })\n}\n"],"mappings":";gNA6CA,SAAgB,GAAmE,CACjF,IAAM,EAAM,GAAsB,CAClC,MAAO,CAAE,kBAAmB,EAAM,CAAC,EAAI,CAAG,EAAE,CAAE,CAShD,SAAgB,EAAc,EAA0B,CACtD,OAAO,EAAU,CACf,GAAG,GAA2B,CAC9B,GAAG,EACJ,CAAC"}
@@ -1,3 +1,3 @@
1
1
  #!/usr/bin/env node
2
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-Y1XI61JY.js.map
3
+ //# sourceMappingURL=daemon-base-D015ew5y.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"daemon-base-Y1XI61JY.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"}
1
+ {"version":3,"file":"daemon-base-D015ew5y.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"}
@@ -1,6 +1,6 @@
1
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,constants as l,existsSync as u,statSync as d}from"node:fs";import{SESSION_MAX_HISTORY_MESSAGES as f,SESSION_MAX_PER_SESSION_BYTES as p,SESSION_MIN_PER_SESSION_BYTES as m,SESSION_RESUME_WINDOW_MESSAGES as h,SESSION_TOTAL_RESIDENCY_BUDGET as g}from"@x-otto/session-contract";const _=s(r(),`.otto`);async function v(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-JBgCH77n.js`);return e(n,t)}case`attach`:{let{runDoctorAttach:e}=await import(`./doctor-attach-DEMuyBOk.js`);return e(n,t)}case`heap`:{let{runDoctorHeap:e}=await import(`./doctor-heap-Eu3LZj-F.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 A(n),0;let a=e.includes(`--json`)?`json`:e.includes(`--summary`)?`summary`:`detailed`;try{let e=await y();return a===`json`?n(JSON.stringify(e,null,2)+`
3
- `):a===`summary`?O(n,e):k(n,e),0}catch(e){return r(`otto doctor: ${e instanceof Error?e.message:String(e)}\n`),1}}async function y(){let e=await b(),t=await x(),n=await w(),r=await T(),{plugins:i,providerPluginDiscovered:a}=await S();return{environment:e,configuration:t,state:n,connectivity:r,plugins:i,ftue:await C(a)}}async function b(){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 x(){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:_,hasRipgrep:await E(`rg`),hasGit:await E(`git`),authMode:e,residencyBudget:{totalBytes:g,minPerSessionBytes:m,maxPerSessionBytes:p,historyMessages:f,resumeWindowMessages:h}}}async function S(){try{let{discoverPluginsWithBundled:e}=await import(`./discover-with-bundled-B5d4q___.js`),{hostSelfDescribingOptions:t}=await import(`./create-host-app-DG58-vRA.js`),{checkPluginAssembly:n,formatAssemblyGap:i}=await import(`./plugin-assembly-check-DEozmcJ7.js`),a=e({cwd:process.cwd(),homedir:r()}),o=t().pluginBundledDirs??[],s=a.some(e=>e.manifest.capabilities?.includes(`provider`)),c=a.filter(e=>e.scope===`bundled`).length>0&&o.length===0,l=n({discovered:a.map(e=>({id:e.id,scope:e.scope})),active:c?a.filter(e=>e.scope!==`bundled`).map(e=>({id:e.id,scope:e.scope})):a.map(e=>({id:e.id,scope:e.scope}))});return{plugins:{discovered:a.length,gaps:i(l),missing:l.missingFromRuntime},providerPluginDiscovered:s}}catch{return{plugins:void 0,providerPluginDiscovered:!1}}}async function C(e){try{let{createDefaultAuthStore:t}=await import(`@x-otto/ai`),{deriveDoctorFtueGuidance:n}=await import(`./ftue-doctor-recovery-8FeDBFOc.js`),{SettingsManager:r}=await import(`@x-otto/setting`),{OTTO_CONFIG_FILENAME:i}=await import(`@x-otto/env`),a=t({watchForExternalChanges:!1});await a.ensureInitialized();let o=a.listProviders().length>0,c=new r({projectConfigPath:s(_,i)});return await c.load(),n({providerPluginDiscovered:e,hasAnyCredential:o,firstPromptSubmitted:typeof c.get(`first_prompt_submitted_at`)==`number`})}catch{return}}async function w(){let e=s(_,`storage.db`);if(!u(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:D(e),...t}}finally{n.close()}}catch{return{dbPath:e,dbExists:!0,integrity:`error (unable to open)`}}}async function T(){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 E(e){let t=new Set;for(let e of(process.env.PATH??``).split(`:`))e&&t.add(e);t.add(`/usr/local/bin`),t.add(`/usr/bin`);for(let n of t)try{return c(s(n,e),l.X_OK),!0}catch{}return!1}function D(e){try{return d(e).size}catch{return 0}}function O(e,t){let{environment:n,configuration:r,state:i,connectivity:a}=t,o=i?.dbExists&&i.integrity===`ok`?`✅`:`❌`,s=a.providerReachable?`✅`:`❌`,c=t.plugins&&t.plugins.gaps.length>0?` ⚠ 插件装配缺口 ${t.plugins.missing.length} 项(otto doctor 查看详情)`:``,l=t.ftue&&t.ftue.status!==`looks-configured`?` ⚠ ${t.ftue.summary} → ${t.ftue.recoveryAction}`:``;e([`otto doctor — ${n.runtime} | ${r.storageDir} | DB:${o} ${s}`,i?` ${i.snapshots??`?`} snapshots, ${i.changeLogEntries??`?`} change_log`:``,c,l].filter(Boolean).join(`
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,constants as l,existsSync as u,statSync as d}from"node:fs";import{SESSION_MAX_HISTORY_MESSAGES as f,SESSION_MAX_PER_SESSION_BYTES as p,SESSION_MIN_PER_SESSION_BYTES as m,SESSION_RESUME_WINDOW_MESSAGES as h,SESSION_TOTAL_RESIDENCY_BUDGET as g}from"@x-otto/session-contract";const _=s(r(),`.otto`);async function v(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-DNVOF6yY.js`);return e(n,t)}case`attach`:{let{runDoctorAttach:e}=await import(`./doctor-attach-DEoE7eFx.js`);return e(n,t)}case`heap`:{let{runDoctorHeap:e}=await import(`./doctor-heap-SIxiRvL8.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 A(n),0;let a=e.includes(`--json`)?`json`:e.includes(`--summary`)?`summary`:`detailed`;try{let e=await y();return a===`json`?n(JSON.stringify(e,null,2)+`
3
+ `):a===`summary`?O(n,e):k(n,e),0}catch(e){return r(`otto doctor: ${e instanceof Error?e.message:String(e)}\n`),1}}async function y(){let e=await b(),t=await x(),n=await w(),r=await T(),{plugins:i,providerPluginDiscovered:a}=await S();return{environment:e,configuration:t,state:n,connectivity:r,plugins:i,ftue:await C(a)}}async function b(){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 x(){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:_,hasRipgrep:await E(`rg`),hasGit:await E(`git`),authMode:e,residencyBudget:{totalBytes:g,minPerSessionBytes:m,maxPerSessionBytes:p,historyMessages:f,resumeWindowMessages:h}}}async function S(){try{let{discoverPluginsWithBundled:e}=await import(`./discover-with-bundled-B5d4q___.js`),{hostSelfDescribingOptions:t}=await import(`./create-host-app-CbJ8ptv-.js`).then(e=>e.n),{checkPluginAssembly:n,formatAssemblyGap:i}=await import(`./plugin-assembly-check-BntT5W6H.js`),a=e({cwd:process.cwd(),homedir:r()}),o=t().pluginBundledDirs??[],s=a.some(e=>e.manifest.capabilities?.includes(`provider`)),c=a.filter(e=>e.scope===`bundled`).length>0&&o.length===0,l=n({discovered:a.map(e=>({id:e.id,scope:e.scope})),active:c?a.filter(e=>e.scope!==`bundled`).map(e=>({id:e.id,scope:e.scope})):a.map(e=>({id:e.id,scope:e.scope}))});return{plugins:{discovered:a.length,gaps:i(l),missing:l.missingFromRuntime},providerPluginDiscovered:s}}catch{return{plugins:void 0,providerPluginDiscovered:!1}}}async function C(e){try{let{createDefaultAuthStore:t}=await import(`@x-otto/ai`),{deriveDoctorFtueGuidance:n}=await import(`./ftue-doctor-recovery-CKTMnxM8.js`),{SettingsManager:r}=await import(`@x-otto/setting`),{OTTO_CONFIG_FILENAME:i}=await import(`@x-otto/env`),a=t({watchForExternalChanges:!1});await a.ensureInitialized();let o=a.listProviders().length>0,c=new r({projectConfigPath:s(_,i)});return await c.load(),n({providerPluginDiscovered:e,hasAnyCredential:o,firstPromptSubmitted:typeof c.get(`first_prompt_submitted_at`)==`number`})}catch{return}}async function w(){let e=s(_,`storage.db`);if(!u(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:D(e),...t}}finally{n.close()}}catch{return{dbPath:e,dbExists:!0,integrity:`error (unable to open)`}}}async function T(){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 E(e){let t=new Set;for(let e of(process.env.PATH??``).split(`:`))e&&t.add(e);t.add(`/usr/local/bin`),t.add(`/usr/bin`);for(let n of t)try{return c(s(n,e),l.X_OK),!0}catch{}return!1}function D(e){try{return d(e).size}catch{return 0}}function O(e,t){let{environment:n,configuration:r,state:i,connectivity:a}=t,o=i?.dbExists&&i.integrity===`ok`?`✅`:`❌`,s=a.providerReachable?`✅`:`❌`,c=t.plugins&&t.plugins.gaps.length>0?` ⚠ 插件装配缺口 ${t.plugins.missing.length} 项(otto doctor 查看详情)`:``,l=t.ftue&&t.ftue.status!==`looks-configured`?` ⚠ ${t.ftue.summary} → ${t.ftue.recoveryAction}`:``;e([`otto doctor — ${n.runtime} | ${r.storageDir} | DB:${o} ${s}`,i?` ${i.snapshots??`?`} snapshots, ${i.changeLogEntries??`?`} change_log`:``,c,l].filter(Boolean).join(`
4
4
  `)+`
5
5
  `)}function k(e,t){let{environment:n,configuration:r,state:i,connectivity:a}=t;if(e(`
6
6
  Environment
@@ -33,4 +33,4 @@ First-time setup
33
33
  otto doctor --summary
34
34
  otto doctor --json
35
35
  `)}function j(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{v as runDoctorCommand};
36
- //# sourceMappingURL=doctor-DXBLkbkP.js.map
36
+ //# sourceMappingURL=doctor-EndB-ycb.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"doctor-DXBLkbkP.js","names":[],"sources":["../src/commands/doctor.ts"],"sourcesContent":["import { homedir, release, platform, arch, cpus, totalmem, freemem } from 'node:os'\nimport { existsSync, statSync, accessSync, constants } 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 /** RFC-364 M2:插件装配一致性——发现得到却未装载的缺口。 */\n plugins?: PluginsSection\n /** RFC-326 M6-01:FTUE 环境自检(廉价事实推导,非权威 readiness 判定,见模块头注释)。 */\n ftue?: import('./ftue/ftue-doctor-recovery').DoctorFtueGuidance\n}\n\ninterface PluginsSection {\n discovered: number\n /** 装配缺口的可执行诊断行(空数组 = 无缺口)。 */\n gaps: string[]\n /** 缺口涉及的插件 id(供 --json 消费方精确处理)。 */\n missing: Array<{ id: string; scope?: string }>\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 const { plugins, providerPluginDiscovered } = await collectPluginsAndProviderFlag()\n const ftue = await collectFtueGuidance(providerPluginDiscovered)\n\n return { environment: env, configuration: config, state, connectivity, plugins, ftue }\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\n/**\n * RFC-364 M2:插件装配一致性自检。\n *\n * 判据是「cli 侧完整发现(含 bundled 层)」与「App 实际装载」的差集。但 doctor 是\n * 轻量诊断命令(RFC-150 D3 明确要求不承担重依赖),构造真实 App 代价过高——\n * 故此处只做**发现侧**的自检:确认 bundled 目录可解析且其中的扩展能被发现。\n *\n * 真正的「发现 vs 装载」全量比对由运行时侧承担(App 启动路径),本处提供的是\n * 用户可主动运行的诊断入口:当用户报告\"插件不见了\"时,`otto doctor` 能立刻回答\n * \"otto 这一侧到底看见了哪些\"。\n */\n/**\n * RFC-326 M6-01:与 `providerPluginDiscovered` 标志合并计算——`discoverPluginsWithBundled`\n * 是本函数唯一的重量级调用,`collectFtueGuidance` 需要同一份发现结果的 `provider`\n * capability 过滤,若各自重新扫描一次会重复触发文件系统遍历。`providerPluginDiscovered`\n * 在扫描失败时缺省 `false`(诊断失败不阻断其余部分,与既有 fail-soft 语义一致;\n * FTUE 分支据此会诚实报告\"未发现 provider 插件\"而非静默跳过整个 ftue 字段)。\n */\nasync function collectPluginsAndProviderFlag(): Promise<{\n plugins: PluginsSection | undefined\n providerPluginDiscovered: boolean\n}> {\n try {\n const { discoverPluginsWithBundled } = await import('../discover-with-bundled')\n const { hostSelfDescribingOptions } = await import('../create-host-app')\n const { checkPluginAssembly, formatAssemblyGap } = await import('../plugin-assembly-check')\n\n const discovered = discoverPluginsWithBundled({ cwd: process.cwd(), homedir: homedir() })\n const bundledDirs = hostSelfDescribingOptions().pluginBundledDirs ?? []\n const providerPluginDiscovered = discovered.some((p) => p.manifest.capabilities?.includes('provider'))\n\n // 宿主自描述参数解析不出目录,但发现层却报告了 bundled 作用域插件(或反之)——\n // 这正是 M1 修的那类装配漂移,在此处显式暴露。\n const bundledSeen = discovered.filter((p) => p.scope === 'bundled')\n const assemblyBroken = bundledSeen.length > 0 && bundledDirs.length === 0\n\n const result = checkPluginAssembly({\n discovered: discovered.map((p) => ({ id: p.id, scope: p.scope })),\n // 发现侧自检:把「宿主参数解析失败」建模为\"全部 bundled 未装载\"\n active: assemblyBroken\n ? discovered.filter((p) => p.scope !== 'bundled').map((p) => ({ id: p.id, scope: p.scope }))\n : discovered.map((p) => ({ id: p.id, scope: p.scope })),\n })\n\n return {\n plugins: {\n discovered: discovered.length,\n gaps: formatAssemblyGap(result),\n missing: result.missingFromRuntime,\n },\n providerPluginDiscovered,\n }\n } catch {\n // 诊断失败不阻断 doctor 其余部分(与既有 collectState 的 fail-soft 一致)。\n return { plugins: undefined, providerPluginDiscovered: false }\n }\n}\n\n/**\n * RFC-326 M6-01:FTUE 环境自检——三个廉价事实推导诊断状态(见\n * `ftue-doctor-recovery.ts` 头注释的判据边界)。`AuthStore` 构造零 App 依赖\n * (默认读全局 `AUTH_PATH`),`listProviders()` 只读本地文件/env,不发网络请求。\n */\nasync function collectFtueGuidance(\n providerPluginDiscovered: boolean,\n): Promise<import('./ftue/ftue-doctor-recovery').DoctorFtueGuidance | undefined> {\n try {\n const { createDefaultAuthStore } = await import('@x-otto/ai')\n const { deriveDoctorFtueGuidance } = await import('./ftue/ftue-doctor-recovery')\n const { SettingsManager } = await import('@x-otto/setting')\n const { OTTO_CONFIG_FILENAME } = await import('@x-otto/env')\n\n // watchForExternalChanges:false —— doctor 是一次性命令,跑完即退出,不需要\n // fs.watch(同 auth-command.ts 的既有约定,见其头注释)。\n const authStore = createDefaultAuthStore({ watchForExternalChanges: false })\n // listProviders() 读内存缓存(this.cache),不像 getAuthSource/hasCredential 那样\n // 自带 ensureInitialized() 前置——首次调用前必须显式触发一次磁盘加载,否则读到空态。\n await authStore.ensureInitialized()\n const hasAnyCredential = authStore.listProviders().length > 0\n\n const settings = new SettingsManager({ projectConfigPath: join(OTTO_DIR, OTTO_CONFIG_FILENAME) })\n await settings.load()\n const firstPromptSubmitted = typeof settings.get('first_prompt_submitted_at') === 'number'\n\n return deriveDoctorFtueGuidance({ providerPluginDiscovered, hasAnyCredential, firstPromptSubmitted })\n } catch {\n // 诊断失败不阻断 doctor 其余部分。\n return undefined\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\n/**\n * 二进制探测(08-13 backlog triage 修复):此前只扫 /usr/local/bin 与 /usr/bin——\n * Homebrew 原生(/opt/homebrew/bin)、nix(~/.nix-profile/bin)等 PATH 布局被误报缺失。\n * 改为遍历 process.env.PATH 拆分项逐一探测(X_OK 替换魔数 1),并保留两个系统目录\n * 作为 PATH 未含时的兜底(部分守护进程环境 PATH 极简)。\n */\nexport async function checkBinary(name: string): Promise<boolean> {\n const dirs = new Set<string>()\n for (const p of (process.env['PATH'] ?? '').split(':')) {\n if (p) dirs.add(p)\n }\n dirs.add('/usr/local/bin')\n dirs.add('/usr/bin')\n for (const dir of dirs) {\n try {\n accessSync(join(dir, name), constants.X_OK)\n return true\n } catch {\n // 目录不存在或无可执行位——继续下一个候选。\n }\n }\n return false\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 const pluginGap = report.plugins && report.plugins.gaps.length > 0\n ? ` ⚠ 插件装配缺口 ${report.plugins.missing.length} 项(otto doctor 查看详情)`\n : ''\n // RFC-326 M6-01:只在未配置(非 looks-configured)时提示——已配置用户不需要看\n // 这行噪音,未配置用户能立刻看到下一步命令,不必翻到 detailed 输出。\n const ftueHint = report.ftue && report.ftue.status !== 'looks-configured'\n ? ` ⚠ ${report.ftue.summary} → ${report.ftue.recoveryAction}`\n : ''\n stdout([\n `otto doctor — ${env.runtime} | ${config.storageDir} | DB:${dbOk} ${netOk}`,\n state ? ` ${state.snapshots ?? '?'} snapshots, ${state.changeLogEntries ?? '?'} change_log` : '',\n pluginGap,\n ftueHint,\n ].filter(Boolean).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 // RFC-364 M2:插件装配一致性——有缺口才打印(无缺口时不制造噪音)。\n if (report.plugins) {\n stdout('\\nPlugins\\n')\n stdout(` Discovered: ${report.plugins.discovered}\\n`)\n if (report.plugins.gaps.length > 0) {\n stdout(' Assembly: ❌ 检测到装配缺口\\n')\n for (const line of report.plugins.gaps) stdout(` ${line}\\n`)\n } else {\n stdout(' Assembly: ✅\\n')\n }\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\n // RFC-326 M6-01:FTUE 环境自检——廉价事实推导,非权威 readiness 判定\n // (见 ftue-doctor-recovery.ts 头注释;权威判定只在 TUI 运行时的 FtueController)。\n if (report.ftue) {\n const icon = report.ftue.status === 'looks-configured' ? '✅' : '⚠'\n stdout('\\nFirst-time setup\\n')\n stdout(` ${icon} ${report.ftue.summary}\\n`)\n stdout(` → ${report.ftue.recoveryAction}\\n`)\n }\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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"}
1
+ {"version":3,"file":"doctor-EndB-ycb.js","names":[],"sources":["../src/commands/doctor.ts"],"sourcesContent":["import { homedir, release, platform, arch, cpus, totalmem, freemem } from 'node:os'\nimport { existsSync, statSync, accessSync, constants } 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 /** RFC-364 M2:插件装配一致性——发现得到却未装载的缺口。 */\n plugins?: PluginsSection\n /** RFC-326 M6-01:FTUE 环境自检(廉价事实推导,非权威 readiness 判定,见模块头注释)。 */\n ftue?: import('./ftue/ftue-doctor-recovery').DoctorFtueGuidance\n}\n\ninterface PluginsSection {\n discovered: number\n /** 装配缺口的可执行诊断行(空数组 = 无缺口)。 */\n gaps: string[]\n /** 缺口涉及的插件 id(供 --json 消费方精确处理)。 */\n missing: Array<{ id: string; scope?: string }>\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 const { plugins, providerPluginDiscovered } = await collectPluginsAndProviderFlag()\n const ftue = await collectFtueGuidance(providerPluginDiscovered)\n\n return { environment: env, configuration: config, state, connectivity, plugins, ftue }\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\n/**\n * RFC-364 M2:插件装配一致性自检。\n *\n * 判据是「cli 侧完整发现(含 bundled 层)」与「App 实际装载」的差集。但 doctor 是\n * 轻量诊断命令(RFC-150 D3 明确要求不承担重依赖),构造真实 App 代价过高——\n * 故此处只做**发现侧**的自检:确认 bundled 目录可解析且其中的扩展能被发现。\n *\n * 真正的「发现 vs 装载」全量比对由运行时侧承担(App 启动路径),本处提供的是\n * 用户可主动运行的诊断入口:当用户报告\"插件不见了\"时,`otto doctor` 能立刻回答\n * \"otto 这一侧到底看见了哪些\"。\n */\n/**\n * RFC-326 M6-01:与 `providerPluginDiscovered` 标志合并计算——`discoverPluginsWithBundled`\n * 是本函数唯一的重量级调用,`collectFtueGuidance` 需要同一份发现结果的 `provider`\n * capability 过滤,若各自重新扫描一次会重复触发文件系统遍历。`providerPluginDiscovered`\n * 在扫描失败时缺省 `false`(诊断失败不阻断其余部分,与既有 fail-soft 语义一致;\n * FTUE 分支据此会诚实报告\"未发现 provider 插件\"而非静默跳过整个 ftue 字段)。\n */\nasync function collectPluginsAndProviderFlag(): Promise<{\n plugins: PluginsSection | undefined\n providerPluginDiscovered: boolean\n}> {\n try {\n const { discoverPluginsWithBundled } = await import('../discover-with-bundled')\n const { hostSelfDescribingOptions } = await import('../create-host-app')\n const { checkPluginAssembly, formatAssemblyGap } = await import('../plugin-assembly-check')\n\n const discovered = discoverPluginsWithBundled({ cwd: process.cwd(), homedir: homedir() })\n const bundledDirs = hostSelfDescribingOptions().pluginBundledDirs ?? []\n const providerPluginDiscovered = discovered.some((p) => p.manifest.capabilities?.includes('provider'))\n\n // 宿主自描述参数解析不出目录,但发现层却报告了 bundled 作用域插件(或反之)——\n // 这正是 M1 修的那类装配漂移,在此处显式暴露。\n const bundledSeen = discovered.filter((p) => p.scope === 'bundled')\n const assemblyBroken = bundledSeen.length > 0 && bundledDirs.length === 0\n\n const result = checkPluginAssembly({\n discovered: discovered.map((p) => ({ id: p.id, scope: p.scope })),\n // 发现侧自检:把「宿主参数解析失败」建模为\"全部 bundled 未装载\"\n active: assemblyBroken\n ? discovered.filter((p) => p.scope !== 'bundled').map((p) => ({ id: p.id, scope: p.scope }))\n : discovered.map((p) => ({ id: p.id, scope: p.scope })),\n })\n\n return {\n plugins: {\n discovered: discovered.length,\n gaps: formatAssemblyGap(result),\n missing: result.missingFromRuntime,\n },\n providerPluginDiscovered,\n }\n } catch {\n // 诊断失败不阻断 doctor 其余部分(与既有 collectState 的 fail-soft 一致)。\n return { plugins: undefined, providerPluginDiscovered: false }\n }\n}\n\n/**\n * RFC-326 M6-01:FTUE 环境自检——三个廉价事实推导诊断状态(见\n * `ftue-doctor-recovery.ts` 头注释的判据边界)。`AuthStore` 构造零 App 依赖\n * (默认读全局 `AUTH_PATH`),`listProviders()` 只读本地文件/env,不发网络请求。\n */\nasync function collectFtueGuidance(\n providerPluginDiscovered: boolean,\n): Promise<import('./ftue/ftue-doctor-recovery').DoctorFtueGuidance | undefined> {\n try {\n const { createDefaultAuthStore } = await import('@x-otto/ai')\n const { deriveDoctorFtueGuidance } = await import('./ftue/ftue-doctor-recovery')\n const { SettingsManager } = await import('@x-otto/setting')\n const { OTTO_CONFIG_FILENAME } = await import('@x-otto/env')\n\n // watchForExternalChanges:false —— doctor 是一次性命令,跑完即退出,不需要\n // fs.watch(同 auth-command.ts 的既有约定,见其头注释)。\n const authStore = createDefaultAuthStore({ watchForExternalChanges: false })\n // listProviders() 读内存缓存(this.cache),不像 getAuthSource/hasCredential 那样\n // 自带 ensureInitialized() 前置——首次调用前必须显式触发一次磁盘加载,否则读到空态。\n await authStore.ensureInitialized()\n const hasAnyCredential = authStore.listProviders().length > 0\n\n const settings = new SettingsManager({ projectConfigPath: join(OTTO_DIR, OTTO_CONFIG_FILENAME) })\n await settings.load()\n const firstPromptSubmitted = typeof settings.get('first_prompt_submitted_at') === 'number'\n\n return deriveDoctorFtueGuidance({ providerPluginDiscovered, hasAnyCredential, firstPromptSubmitted })\n } catch {\n // 诊断失败不阻断 doctor 其余部分。\n return undefined\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\n/**\n * 二进制探测(08-13 backlog triage 修复):此前只扫 /usr/local/bin 与 /usr/bin——\n * Homebrew 原生(/opt/homebrew/bin)、nix(~/.nix-profile/bin)等 PATH 布局被误报缺失。\n * 改为遍历 process.env.PATH 拆分项逐一探测(X_OK 替换魔数 1),并保留两个系统目录\n * 作为 PATH 未含时的兜底(部分守护进程环境 PATH 极简)。\n */\nexport async function checkBinary(name: string): Promise<boolean> {\n const dirs = new Set<string>()\n for (const p of (process.env['PATH'] ?? '').split(':')) {\n if (p) dirs.add(p)\n }\n dirs.add('/usr/local/bin')\n dirs.add('/usr/bin')\n for (const dir of dirs) {\n try {\n accessSync(join(dir, name), constants.X_OK)\n return true\n } catch {\n // 目录不存在或无可执行位——继续下一个候选。\n }\n }\n return false\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 const pluginGap = report.plugins && report.plugins.gaps.length > 0\n ? ` ⚠ 插件装配缺口 ${report.plugins.missing.length} 项(otto doctor 查看详情)`\n : ''\n // RFC-326 M6-01:只在未配置(非 looks-configured)时提示——已配置用户不需要看\n // 这行噪音,未配置用户能立刻看到下一步命令,不必翻到 detailed 输出。\n const ftueHint = report.ftue && report.ftue.status !== 'looks-configured'\n ? ` ⚠ ${report.ftue.summary} → ${report.ftue.recoveryAction}`\n : ''\n stdout([\n `otto doctor — ${env.runtime} | ${config.storageDir} | DB:${dbOk} ${netOk}`,\n state ? ` ${state.snapshots ?? '?'} snapshots, ${state.changeLogEntries ?? '?'} change_log` : '',\n pluginGap,\n ftueHint,\n ].filter(Boolean).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 // RFC-364 M2:插件装配一致性——有缺口才打印(无缺口时不制造噪音)。\n if (report.plugins) {\n stdout('\\nPlugins\\n')\n stdout(` Discovered: ${report.plugins.discovered}\\n`)\n if (report.plugins.gaps.length > 0) {\n stdout(' Assembly: ❌ 检测到装配缺口\\n')\n for (const line of report.plugins.gaps) stdout(` ${line}\\n`)\n } else {\n stdout(' Assembly: ✅\\n')\n }\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\n // RFC-326 M6-01:FTUE 环境自检——廉价事实推导,非权威 readiness 判定\n // (见 ftue-doctor-recovery.ts 头注释;权威判定只在 TUI 运行时的 FtueController)。\n if (report.ftue) {\n const icon = report.ftue.status === 'looks-configured' ? '✅' : '⚠'\n stdout('\\nFirst-time setup\\n')\n stdout(` ${icon} ${report.ftue.summary}\\n`)\n stdout(` → ${report.ftue.recoveryAction}\\n`)\n }\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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"}
@@ -1,5 +1,5 @@
1
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-CnJUVNOB.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]
2
+ import{a as e,i as t,o as n,r,s as i,t as a}from"./doctor-cdp-Cj9IFpEP.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
3
  (pid 可用 otto doctor ps 查看)
4
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
5
 
@@ -39,4 +39,4 @@ import{a as e,i as t,o as n,r,s as i,t as a}from"./doctor-cdp-CnJUVNOB.js";impor
39
39
  `:[...e.entries()].sort((e,t)=>t[1]-e[1]).map(([e,t])=>` ${e}: ${t}`).join(`
40
40
  `)+`
41
41
  `}function d(e){return`${(e/(1024*1024)).toFixed(1)}MB`}export{s as runDoctorAttach};
42
- //# sourceMappingURL=doctor-attach-DEMuyBOk.js.map
42
+ //# sourceMappingURL=doctor-attach-DEoE7eFx.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"doctor-attach-DEMuyBOk.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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"}
1
+ {"version":3,"file":"doctor-attach-DEoE7eFx.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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"}
@@ -2,4 +2,4 @@
2
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
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
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-CnJUVNOB.js.map
5
+ //# sourceMappingURL=doctor-cdp-Cj9IFpEP.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"doctor-cdp-CnJUVNOB.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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"}
1
+ {"version":3,"file":"doctor-cdp-Cj9IFpEP.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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"}
@@ -1,5 +1,5 @@
1
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-CnJUVNOB.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>]
2
+ import{a as e,i as t,o as n,r,s as i,t as a}from"./doctor-cdp-Cj9IFpEP.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
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
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
5
  判读提示(lesson_92):top 帧若是 measure @ node:internal/perf/performance → user-timing 无界缓冲
@@ -8,4 +8,4 @@ import{a as e,i as t,o as n,r,s as i,t as a}from"./doctor-cdp-CnJUVNOB.js";async
8
8
  降级配方:
9
9
  `+e(l)+`
10
10
  `),1}finally{p.close()}}export{o as runDoctorHeap};
11
- //# sourceMappingURL=doctor-heap-Eu3LZj-F.js.map
11
+ //# sourceMappingURL=doctor-heap-SIxiRvL8.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"doctor-heap-Eu3LZj-F.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"}
1
+ {"version":3,"file":"doctor-heap-SIxiRvL8.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"}
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
- import{i as e,n as t}from"./doctor-cdp-CnJUVNOB.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)+`
2
+ import{i as e,n as t}from"./doctor-cdp-Cj9IFpEP.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
3
  `),0;if(c.length===0)i.stdout(`otto doctor ps: 未发现运行中的 otto 主进程
4
4
  `);else{i.stdout(`PID PPID RSS(MB) UP(h) SESSION GEN FLAGS
5
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
6
  `:`(otto 进程)
7
7
  `))}return 0}export{r as runDoctorPs};
8
- //# sourceMappingURL=doctor-ps-JBgCH77n.js.map
8
+ //# sourceMappingURL=doctor-ps-DNVOF6yY.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"doctor-ps-JBgCH77n.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"}
1
+ {"version":3,"file":"doctor-ps-DNVOF6yY.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"}
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import{n as e,t}from"./extension-Dpn8gltG.js";export{e as EXTENSION_ALIASES,t as runExtensionCommand};