@whyshu/argent-engine 0.0.0-stage → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (305) hide show
  1. package/MANIFEST.json +307 -0
  2. package/engine/VERSION +1 -0
  3. package/engine/agents/catalog.json +454 -0
  4. package/engine/configs/_aliases.proposed.json +651 -0
  5. package/engine/configs/_migration-plan.proposed.json +899 -0
  6. package/engine/configs/account.json +272 -0
  7. package/engine/configs/advanced.json +205 -0
  8. package/engine/configs/host.json +149 -0
  9. package/engine/configs/hr.json +27 -0
  10. package/engine/configs/notify.json +264 -0
  11. package/engine/core/cli/argent.mjs +277 -0
  12. package/engine/core/contracts/argent.contracts.json +1065 -0
  13. package/engine/core/contracts/verbs.schema.json +2539 -0
  14. package/engine/core/lib/argent-datadir.mjs +223 -0
  15. package/engine/core/lib/audit.mjs +187 -0
  16. package/engine/core/lib/claim.mjs +258 -0
  17. package/engine/core/lib/config-catalog.mjs +1172 -0
  18. package/engine/core/lib/doctor-customer.mjs +577 -0
  19. package/engine/core/lib/flow-list.mjs +138 -0
  20. package/engine/core/lib/flow-registry.mjs +48 -0
  21. package/engine/core/lib/init.mjs +247 -0
  22. package/engine/core/lib/ledger.mjs +124 -0
  23. package/engine/core/lib/license-activate.mjs +813 -0
  24. package/engine/core/lib/license-device.mjs +314 -0
  25. package/engine/core/lib/license-file.mjs +349 -0
  26. package/engine/core/lib/license-guard.mjs +123 -0
  27. package/engine/core/lib/license-ops.mjs +1645 -0
  28. package/engine/core/lib/license-verdict.mjs +133 -0
  29. package/engine/core/lib/license.mjs +577 -0
  30. package/engine/core/lib/notify/dedup.mjs +101 -0
  31. package/engine/core/lib/notify/feishu.mjs +38 -0
  32. package/engine/core/lib/notify/index.mjs +95 -0
  33. package/engine/core/lib/notify/verb.mjs +327 -0
  34. package/engine/core/lib/notify/webhook.mjs +135 -0
  35. package/engine/core/lib/notify/wecom.mjs +36 -0
  36. package/engine/core/lib/python.mjs +145 -0
  37. package/engine/core/lib/sop.mjs +116 -0
  38. package/engine/core/lib/versions.mjs +205 -0
  39. package/engine/core/mcp/client.mjs +67 -0
  40. package/engine/core/mcp/http.mjs +138 -0
  41. package/engine/core/mcp/server.mjs +702 -0
  42. package/engine/scripts/_engine_rules_probe.py +15 -0
  43. package/engine/scripts/analyze-full-day.py +568 -0
  44. package/engine/scripts/archive/feishu-bridge.js +246 -0
  45. package/engine/scripts/argent-agents.mjs +527 -0
  46. package/engine/scripts/argent-capabilities.mjs +776 -0
  47. package/engine/scripts/argent-catalog.mjs +186 -0
  48. package/engine/scripts/argent-datadir.py +62 -0
  49. package/engine/scripts/argent-doctor.mjs +624 -0
  50. package/engine/scripts/argent-env-write.mjs +302 -0
  51. package/engine/scripts/argent-schedule.mjs +620 -0
  52. package/engine/scripts/argent-setup-check.mjs +369 -0
  53. package/engine/scripts/argent-upgrade.mjs +1046 -0
  54. package/engine/scripts/base-bootstrap.sh +575 -0
  55. package/engine/scripts/billing-mode-daily.sh +12 -0
  56. package/engine/scripts/build-engine-package.mjs +123 -0
  57. package/engine/scripts/capability-packs.lock.json +484 -0
  58. package/engine/scripts/capability_pack.py +1242 -0
  59. package/engine/scripts/check-1.0-criteria.mjs +355 -0
  60. package/engine/scripts/check-agent-catalog.mjs +141 -0
  61. package/engine/scripts/check-billing-mode.sh +377 -0
  62. package/engine/scripts/check-child-sessions.mjs +83 -0
  63. package/engine/scripts/check-config-catalog.mjs +677 -0
  64. package/engine/scripts/check-docs-drift.mjs +522 -0
  65. package/engine/scripts/check-dsh-web-expose.sh +98 -0
  66. package/engine/scripts/check-judgment-drift.mjs +403 -0
  67. package/engine/scripts/check-license.mjs +199 -0
  68. package/engine/scripts/check-model-auth.py +443 -0
  69. package/engine/scripts/check-pack-closure.mjs +661 -0
  70. package/engine/scripts/check-portable-symbols.mjs +116 -0
  71. package/engine/scripts/check-preset-landing.mjs +124 -0
  72. package/engine/scripts/check-production-home.mjs +602 -0
  73. package/engine/scripts/check-role-mapping.mjs +461 -0
  74. package/engine/scripts/check-sample-redaction.mjs +320 -0
  75. package/engine/scripts/check-skill-classification.mjs +188 -0
  76. package/engine/scripts/check-sop-consistency.mjs +431 -0
  77. package/engine/scripts/check-test-env-safety.mjs +627 -0
  78. package/engine/scripts/check-version-consistency.mjs +452 -0
  79. package/engine/scripts/ci-run.sh +558 -0
  80. package/engine/scripts/config-explain.mjs +278 -0
  81. package/engine/scripts/configure-provider.rb +29 -0
  82. package/engine/scripts/customer-config-migrate.mjs +179 -0
  83. package/engine/scripts/customer-preflight.py +892 -0
  84. package/engine/scripts/customer-readiness.sh +134 -0
  85. package/engine/scripts/daily-report-inbox.py +242 -0
  86. package/engine/scripts/daily-report-reconcile.mjs +312 -0
  87. package/engine/scripts/daily-report.mjs +1310 -0
  88. package/engine/scripts/daily-report.sh +46 -0
  89. package/engine/scripts/data/lip-product-dict.json +306 -0
  90. package/engine/scripts/deliver-customer.mjs +932 -0
  91. package/engine/scripts/delivery-faces.txt +31 -0
  92. package/engine/scripts/desktop-trial.sh +146 -0
  93. package/engine/scripts/dev-window.sh +260 -0
  94. package/engine/scripts/drill-clean-machine.sh +315 -0
  95. package/engine/scripts/dsh-api-client.mjs +215 -0
  96. package/engine/scripts/dsh-backup-scope.sh +83 -0
  97. package/engine/scripts/dsh-offline-pack.mjs +77 -0
  98. package/engine/scripts/dsh-overlay-pin.sh +87 -0
  99. package/engine/scripts/dsh-plane.mjs +153 -0
  100. package/engine/scripts/dsh-portforward.py +223 -0
  101. package/engine/scripts/dsh-preflight.mjs +179 -0
  102. package/engine/scripts/dsh-release-watch-daily.sh +19 -0
  103. package/engine/scripts/dsh-release-watch.mjs +458 -0
  104. package/engine/scripts/dsh-remote-check.sh +99 -0
  105. package/engine/scripts/dsh-restart.sh +179 -0
  106. package/engine/scripts/dsh-restore.sh +111 -0
  107. package/engine/scripts/dsh-scope-rewire.mjs +803 -0
  108. package/engine/scripts/dsh-snapshot.mjs +116 -0
  109. package/engine/scripts/dsh-upgrade-readiness.mjs +165 -0
  110. package/engine/scripts/dsh-upgrade.mjs +612 -0
  111. package/engine/scripts/dsh-url.sh +86 -0
  112. package/engine/scripts/dsh-web-launch.mjs +155 -0
  113. package/engine/scripts/ensure-store-windows.py +509 -0
  114. package/engine/scripts/entitlements.py +458 -0
  115. package/engine/scripts/env_probe.py +411 -0
  116. package/engine/scripts/feishu-card.mjs +252 -0
  117. package/engine/scripts/fix-skill-frontmatter.py +209 -0
  118. package/engine/scripts/flow-auto-retry.py +454 -0
  119. package/engine/scripts/flow-requirements.json +720 -0
  120. package/engine/scripts/flow-scaffold.py +275 -0
  121. package/engine/scripts/flow-timers-plist.sh +86 -0
  122. package/engine/scripts/flow-timers.sh +182 -0
  123. package/engine/scripts/flow-trigger-wake.mjs +284 -0
  124. package/engine/scripts/flow-trigger.sh +571 -0
  125. package/engine/scripts/flow_billing.py +354 -0
  126. package/engine/scripts/flow_capability_stats.py +314 -0
  127. package/engine/scripts/flow_case.py +194 -0
  128. package/engine/scripts/flow_claim.py +497 -0
  129. package/engine/scripts/flow_config.py +1024 -0
  130. package/engine/scripts/flow_failure.py +588 -0
  131. package/engine/scripts/flow_learn.py +105 -0
  132. package/engine/scripts/flow_ledger.py +235 -0
  133. package/engine/scripts/flow_policy.py +238 -0
  134. package/engine/scripts/flow_preflight.py +339 -0
  135. package/engine/scripts/flow_registry.py +341 -0
  136. package/engine/scripts/flow_retry.py +304 -0
  137. package/engine/scripts/flow_return_to_ops.py +191 -0
  138. package/engine/scripts/flow_source.py +315 -0
  139. package/engine/scripts/flow_sources/__init__.py +53 -0
  140. package/engine/scripts/flow_sources/cases_json.py +106 -0
  141. package/engine/scripts/flow_sources/common.py +428 -0
  142. package/engine/scripts/flow_sources/feishu_base.py +273 -0
  143. package/engine/scripts/flow_stale.py +185 -0
  144. package/engine/scripts/flow_status_reconcile.py +344 -0
  145. package/engine/scripts/flow_triage.py +849 -0
  146. package/engine/scripts/flow_write_verify.py +496 -0
  147. package/engine/scripts/fulfillment.js +586 -0
  148. package/engine/scripts/gen-agent-bundles.mjs +276 -0
  149. package/engine/scripts/gen-preset-rows.mjs +259 -0
  150. package/engine/scripts/init-ml.sh +2275 -0
  151. package/engine/scripts/install-internal-role.sh +190 -0
  152. package/engine/scripts/install-license.mjs +641 -0
  153. package/engine/scripts/install-one-shot.mjs +665 -0
  154. package/engine/scripts/integration_contract.py +220 -0
  155. package/engine/scripts/integrations/wenyao/adapter.py +98 -0
  156. package/engine/scripts/integrations/xzh/adapter.py +75 -0
  157. package/engine/scripts/invoice-mapping.example.json +40 -0
  158. package/engine/scripts/invoice-table.sh +194 -0
  159. package/engine/scripts/invoice_carrier_notice.py +279 -0
  160. package/engine/scripts/invoice_fail_closeout.py +297 -0
  161. package/engine/scripts/invoice_lip_runner.py +1508 -0
  162. package/engine/scripts/invoice_lip_runner.py.bak-20260922-1710-freready +1090 -0
  163. package/engine/scripts/invoice_ready_audit.py +191 -0
  164. package/engine/scripts/invoice_runner.py +675 -0
  165. package/engine/scripts/invoice_stageb.py +516 -0
  166. package/engine/scripts/launchd/com.whyshu.argent-bridge.plist.example +42 -0
  167. package/engine/scripts/launchd/com.whyshu.fulfillment-poll.plist.example +29 -0
  168. package/engine/scripts/lease.py +377 -0
  169. package/engine/scripts/lib/agent-install.mjs +649 -0
  170. package/engine/scripts/lib/argent-datadir.mjs +16 -0
  171. package/engine/scripts/lib/argent_datadir.py +262 -0
  172. package/engine/scripts/lib/argent_filelock.py +507 -0
  173. package/engine/scripts/lib/bootcheck-wiring.mjs +510 -0
  174. package/engine/scripts/lib/cdp_ports.py +197 -0
  175. package/engine/scripts/lib/customer-env.mjs +102 -0
  176. package/engine/scripts/lib/daily-report-core.mjs +517 -0
  177. package/engine/scripts/lib/dep-declaration.mjs +81 -0
  178. package/engine/scripts/lib/deps-migrate.mjs +310 -0
  179. package/engine/scripts/lib/dsh-host-resolve.mjs +109 -0
  180. package/engine/scripts/lib/dsh-scope.mjs +500 -0
  181. package/engine/scripts/lib/dsh-snapshot-path.mjs +35 -0
  182. package/engine/scripts/lib/dsh-versions.mjs +96 -0
  183. package/engine/scripts/lib/dsh-web-probe.mjs +214 -0
  184. package/engine/scripts/lib/is-direct-run.mjs +61 -0
  185. package/engine/scripts/lib/patch-compose.mjs +241 -0
  186. package/engine/scripts/lib/patch-target.mjs +35 -0
  187. package/engine/scripts/lib/plugin-lifecycle.mjs +128 -0
  188. package/engine/scripts/lib/plugin-packages.mjs +332 -0
  189. package/engine/scripts/lib/preset-compat.mjs +127 -0
  190. package/engine/scripts/lib/preset-install.mjs +575 -0
  191. package/engine/scripts/lib/preset-mechanism.mjs +56 -0
  192. package/engine/scripts/lib/preset-sync.mjs +59 -0
  193. package/engine/scripts/lib/profile-retire.mjs +97 -0
  194. package/engine/scripts/lib/python-runtime.mjs +190 -0
  195. package/engine/scripts/lib/registry.mjs +72 -0
  196. package/engine/scripts/lib/repo-root.sh +56 -0
  197. package/engine/scripts/lib/schedule-mechanism.mjs +659 -0
  198. package/engine/scripts/lib/schedule-probe.mjs +835 -0
  199. package/engine/scripts/lib/scope-pointer.mjs +164 -0
  200. package/engine/scripts/lib/settings-panels.mjs +816 -0
  201. package/engine/scripts/lib/stdio_utf8.py +86 -0
  202. package/engine/scripts/lib/wake-auth.mjs +392 -0
  203. package/engine/scripts/lib/webdriver_ipc.py +264 -0
  204. package/engine/scripts/lib/zstd-frames.mjs +104 -0
  205. package/engine/scripts/license-e2e-drill.mjs +426 -0
  206. package/engine/scripts/license-gate.mjs +245 -0
  207. package/engine/scripts/lip_backlog.py +171 -0
  208. package/engine/scripts/lip_table.py +185 -0
  209. package/engine/scripts/local-usage.mjs +214 -0
  210. package/engine/scripts/migrate-providers.mjs +127 -0
  211. package/engine/scripts/migrate-skills.sh +65 -0
  212. package/engine/scripts/ml-oauth-setup.mjs +550 -0
  213. package/engine/scripts/ml-store-ids.json +26 -0
  214. package/engine/scripts/ml_artifact_attribution.py +333 -0
  215. package/engine/scripts/ml_capacity_probe.py +1158 -0
  216. package/engine/scripts/ml_hub_status.py +1556 -0
  217. package/engine/scripts/ml_schema_check.py +115 -0
  218. package/engine/scripts/ml_store_ids.py +139 -0
  219. package/engine/scripts/npm-publish-argent.mjs +499 -0
  220. package/engine/scripts/npm-publish-argent.sh +20 -0
  221. package/engine/scripts/orchestrator.py +13811 -0
  222. package/engine/scripts/pack-plugins.mjs +140 -0
  223. package/engine/scripts/patches/dsh-llm-deepseek--transport-diagnostics.patch +297 -0
  224. package/engine/scripts/patches/dsh-session--append-message-shape.patch +15 -0
  225. package/engine/scripts/patches/dsh-session--tool-result-block-017.patch +28 -0
  226. package/engine/scripts/patches/dsh-session-format-v0-to-v1--permission-preset-origin.patch +11 -0
  227. package/engine/scripts/patches/dsh-session-format-v3-to-v4--turn-outside-replace.patch +20 -0
  228. package/engine/scripts/patches/dsh-session-persistence-jsonl--worker-bundled-migrators.patch +39 -0
  229. package/engine/scripts/patches/dsh-tools--render-return-shape.patch +50 -0
  230. package/engine/scripts/patches/verify-bundled-migrators.mjs +79 -0
  231. package/engine/scripts/patches/verify-hardening.mjs +167 -0
  232. package/engine/scripts/patches/verify-session-migration.mjs +149 -0
  233. package/engine/scripts/patches/verify-transport-diagnostics.mjs +227 -0
  234. package/engine/scripts/plan-role-rename.mjs +125 -0
  235. package/engine/scripts/plugin-bootcheck.mjs +451 -0
  236. package/engine/scripts/plugin-composition.json +15 -0
  237. package/engine/scripts/poc-mcp-host.mjs +455 -0
  238. package/engine/scripts/poll-fulfillment.sh +293 -0
  239. package/engine/scripts/poll-invoice.sh +74 -0
  240. package/engine/scripts/profile-doctor.mjs +686 -0
  241. package/engine/scripts/provider-select.sh +70 -0
  242. package/engine/scripts/provider-switch.sh +146 -0
  243. package/engine/scripts/provider-sync-target.check.mjs +64 -0
  244. package/engine/scripts/python-runtime.sh +78 -0
  245. package/engine/scripts/query_runner.py +310 -0
  246. package/engine/scripts/release-argent.sh +574 -0
  247. package/engine/scripts/repair-unresolved-tools.mjs +178 -0
  248. package/engine/scripts/run-tests.sh +256 -0
  249. package/engine/scripts/self-sign-license.mjs +179 -0
  250. package/engine/scripts/session-health-scan.mjs +63 -0
  251. package/engine/scripts/session-repair-tool-result-content.mjs +181 -0
  252. package/engine/scripts/setup.sh +22 -0
  253. package/engine/scripts/skill-install.mjs +629 -0
  254. package/engine/scripts/sku_translate.py +148 -0
  255. package/engine/scripts/sync-plugins.sh +90 -0
  256. package/engine/scripts/sync-presets.sh +27 -0
  257. package/engine/scripts/sync-scripts.sh +445 -0
  258. package/engine/scripts/telemetry.py +873 -0
  259. package/engine/scripts/ui-copy-count.mjs +220 -0
  260. package/engine/scripts/usage-anchor.sh +136 -0
  261. package/engine/scripts/usage_report.py +531 -0
  262. package/engine/scripts/validate-preset-schema.mjs +208 -0
  263. package/engine/scripts/vertical-init.mjs +323 -0
  264. package/engine/scripts/vertical-lint.mjs +482 -0
  265. package/engine/scripts/wenyao-browser.sh +73 -0
  266. package/engine/scripts/wenyao-fixture-export.py +116 -0
  267. package/engine/scripts/wenyao-invoice-writeback.mjs +540 -0
  268. package/engine/scripts/wenyao-login.sh +34 -0
  269. package/engine/scripts/wenyao-template-map.json +292 -0
  270. package/engine/scripts/wenyao_attach.py +131 -0
  271. package/engine/scripts/wenyao_download_template.py +144 -0
  272. package/engine/scripts/wenyao_fill_template.py +568 -0
  273. package/engine/scripts/wenyao_login.py +145 -0
  274. package/engine/scripts/wenyao_portal.py +633 -0
  275. package/engine/scripts/wenyao_recon.py +230 -0
  276. package/engine/scripts/wenyao_run.py +503 -0
  277. package/engine/scripts/wenyao_shadow_diff.py +160 -0
  278. package/engine/scripts/wenyao_skill_contract.py +156 -0
  279. package/engine/scripts/wenyao_template_fetch.py +175 -0
  280. package/engine/scripts/win/dsh-web-autostart.ps1 +372 -0
  281. package/engine/scripts/ziniao-auth-probe.sh +268 -0
  282. package/engine/scripts/ziniao-keepalive.sh +216 -0
  283. package/engine/scripts/ziniao-post-upgrade-check.sh +120 -0
  284. package/engine/scripts/ziniao-windows.sh +153 -0
  285. package/engine/version.mjs +1 -0
  286. package/engine/verticals/README.md +103 -0
  287. package/engine/verticals/_engine-words.baseline.txt +237 -0
  288. package/engine/verticals/_engine-words.json +19 -0
  289. package/engine/verticals/mercadolibre/README.md +43 -0
  290. package/engine/verticals/mercadolibre/config.json +709 -0
  291. package/engine/verticals/mercadolibre/flows.json +706 -0
  292. package/engine/verticals/mercadolibre/manifest.json +538 -0
  293. package/engine/verticals/mercadolibre/migrate-machine-state.mjs +138 -0
  294. package/engine/verticals/mercadolibre/preflight.py +143 -0
  295. package/engine/verticals/mercadolibre/prompts/daily-report.failure.md +69 -0
  296. package/engine/verticals/mercadolibre/prompts/daily-report.md +47 -0
  297. package/engine/verticals/mercadolibre/prompts/daily-report.scheduled.md +79 -0
  298. package/engine/verticals/mercadolibre/prompts/full.failure.md +125 -0
  299. package/engine/verticals/mercadolibre/prompts/full.md +91 -0
  300. package/engine/verticals/mercadolibre/prompts/wenyao-invoice.failure.md +88 -0
  301. package/engine/verticals/mercadolibre/prompts/wenyao-invoice.md +64 -0
  302. package/engine/verticals/mercadolibre/prompts/xzh-invoice.failure.md +91 -0
  303. package/engine/verticals/mercadolibre/prompts/xzh-invoice.md +46 -0
  304. package/package.json +21 -4
  305. package/README.md +0 -3
@@ -0,0 +1,612 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * dsh 版本快照锁定升级器(支持 rc.6 嵌套布局与 0.1.1+ flat-scope 布局)
4
+ *
5
+ * 用法:
6
+ * node scripts/dsh-upgrade.mjs <目标版本> [--snapshot <快照文件>]
7
+ * [--scope-dir <@deepseek-ai 目录>] [--global-bin <dsh 软链路径>]
8
+ * (不传则自动探测:npm root -g → homebrew → ~/node;客户机务必确认探测结果)
9
+ *
10
+ * 只读自检:
11
+ * node scripts/dsh-upgrade.mjs <目标版本> --print-snapshot
12
+ * 打印快照的**真实落点**与档位(根 / candidate)后立即退出,不改任何状态、不联网。
13
+ *
14
+ * 流程:
15
+ * 1. 预下载 tarball(离线安装)
16
+ * 2. 备份当前全局 @deepseek-ai scope → /tmp/dsh-backup-<日期>
17
+ * 3. 临时目录本地 npm install(file: tarball + overrides 快照,绝不 npm install -g)
18
+ * 4. 校验 @deepseek-ai 子包版本分布 == 快照
19
+ * 5. 替换全局(flat-scope: 整个 /opt/homebrew/lib/node_modules/@deepseek-ai/;
20
+ * nested: 仅主包 dsh/)+ 重建 bin 符号链接
21
+ * 6. 验证 dsh --version / 断链 / 页面 200
22
+ * 7. 失败自动回滚备份
23
+ *
24
+ * 注意: 本脚本不重启 dsh web 服务(重启需用户手动 launchctl kickstart)。
25
+ */
26
+ import { execSync, spawnSync } from "node:child_process";
27
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, rmSync, cpSync, readdirSync, symlinkSync, unlinkSync, statSync } from "node:fs";
28
+ import { join, dirname } from "node:path";
29
+ import { fileURLToPath } from "node:url";
30
+ import { resolveDshScopeDir, resolveDshBin } from "./lib/dsh-scope.mjs";
31
+ import { locateSnapshot } from "./lib/dsh-snapshot-path.mjs";
32
+ import { relocatePatchTarget } from "./lib/patch-target.mjs";
33
+
34
+ const __dirname = dirname(fileURLToPath(import.meta.url));
35
+ const REPO = join(__dirname, "..");
36
+ const LOCK_DIR = join(REPO, "dsh-version-lock");
37
+ // 2026-09-18:scope/bin 路径**不再写死 homebrew** —— 客户机的 dsh 装在
38
+ // `~/node/lib/node_modules`(node 官方包,无 homebrew)⇒ 写死会让升级脚本在客户机直接失效。
39
+ // 取值优先级:命令行 --scope-dir/--global-bin → 环境变量 DSH_SCOPE/DSH_BIN → 自动探测 → homebrew 兜底。
40
+ // 2026-09-28:解析逻辑收敛到 scripts/lib/dsh-scope.mjs(唯一真源,含命令行/环境变量/
41
+ // npm root -g 自动探测三级优先级)——此前 4 个脚本各写一份,客户机上各自失效。
42
+ // ── --scope-dir / --global-bin:把命令行覆盖**落到环境变量**再解析 ─────────────
43
+ // 脚本头部与《客户机部署与上线.md》一直宣称支持这两个旗标(且客户机升级那一步就靠它们
44
+ // 指定"装到哪棵树、重建哪个 bin 链接"),但此前**并未解析** ⇒ 传了会被静默忽略,
45
+ // 实际作用到自动探测出的全局 scope 上。这是最危险的一类偏差(意图与落点不一致),故在
46
+ // 解析 SCOPE_DIR/GLOBAL_BIN **之前**先落地。(DSH_SCOPE/DSH_BIN 仍可用,优先级:命令行 > 环境变量 > 探测)
47
+ {
48
+ const _scopeArg = process.argv.indexOf("--scope-dir");
49
+ const _binArg = process.argv.indexOf("--global-bin");
50
+ if (_scopeArg > -1 && process.argv[_scopeArg + 1]) process.env.DSH_SCOPE = process.argv[_scopeArg + 1];
51
+ if (_binArg > -1 && process.argv[_binArg + 1]) process.env.DSH_BIN = process.argv[_binArg + 1];
52
+ }
53
+
54
+ const PROFILES = join(process.env.HOME || "", ".dsh", "profiles");
55
+
56
+ const target = process.argv[2];
57
+ const snapArgIdx = process.argv.indexOf("--snapshot");
58
+ const snapshotFile = snapArgIdx > -1 ? process.argv[snapArgIdx + 1] : null;
59
+ const fromDirIdx = process.argv.indexOf("--from-dir");
60
+ const patchesOnly = process.argv.includes("--patches-only");
61
+ // ── P5(2026-09-29):三段式升级(用户批准的 D6/D7)────────────────
62
+ // --stage <root> :装到**旁路树**(`<root>/lib/node_modules/@deepseek-ai`)+ 在那棵树上跑门禁,
63
+ // **不碰全局 scope、不重建全局 bin、不重启**;
64
+ // --activate <root> :只改**指针**(可选同时重指全局 bin 以兼容尚未切包装脚本的 plist)+ 打印重启指引;
65
+ // --rollback :指针回到审计里的上一棵 known-good 树(同样只改链接,不重启)。
66
+ // 升级/回滚**永远不自己重启服务**(AGENTS.md「严禁自杀式重启」),重启交给人或熔断。
67
+ const stageIdx = process.argv.indexOf("--stage");
68
+ const STAGE_ROOT = stageIdx > -1 ? process.argv[stageIdx + 1] : null;
69
+ const activateIdx = process.argv.indexOf("--activate");
70
+ const ACTIVATE_ROOT = activateIdx > -1 ? process.argv[activateIdx + 1] : null;
71
+ const ROLLBACK = process.argv.includes("--rollback");
72
+ const verifyIdx = process.argv.indexOf("--verify");
73
+ const VERIFY_ROOT = verifyIdx > -1 ? process.argv[verifyIdx + 1] : null;
74
+ const NO_BIN_LINK = process.argv.includes("--no-bin-link");
75
+
76
+ const fromDir = fromDirIdx > -1 ? process.argv[fromDirIdx + 1] : null;
77
+
78
+ if (!target) {
79
+ console.error("用法: node scripts/dsh-upgrade.mjs <目标版本> [--snapshot <快照文件>] [--from-dir <已装目录>]");
80
+ console.error(" --from-dir: 复用现成安装目录(含 node_modules),跳过 npm install(推荐——file:+overrides 组合在 npm 下可能极慢/卡死,2026-08-21 实测)");
81
+ process.exit(1);
82
+ }
83
+
84
+ const run = (cmd, opts = {}) =>
85
+ execSync(cmd, { stdio: opts.silent ? "pipe" : "inherit", encoding: "utf8", ...opts });
86
+
87
+ const ok = (m) => console.log(" ✅ " + m);
88
+ const warn = (m) => console.log(" ⚠️ " + m);
89
+ const fail = (m) => { console.error(" ❌ " + m); process.exit(1); };
90
+
91
+ // ── 目标 scope / 被测 dsh 解析(P1-3b 2026-10-12)───────────────────────────────
92
+ // **真源** = `lib/dsh-scope.mjs#resolveDshScopeDir()`(**npx 感知**:`npm root -g` → PATH 上的 dsh →
93
+ // HOME 相对(邻居→标准)→ 当前 node 前缀 → **npx 缓存**(`~/.npm/_npx/<hash>/node_modules/@deepseek-ai`)
94
+ // → homebrew / Intel 绝对兜底;⛔ 探不到 ⇒ **null**)。
95
+ // **显式入参路径**(本脚本原有,语义未变):`--scope-dir` / `--global-bin` 在上面的块里先落到
96
+ // `DSH_SCOPE`/`DSH_BIN`,真源按「命令行 > 环境变量 > 自动探测」取值 ⇒ 显式仍然最高优先。
97
+ // 为什么必须换掉遗留 `resolveDshScope()`(同一缺陷的第二个入口,2026-10-12 用户 MBP 实测):
98
+ // 它在"纯 npx 机器"上**静默返回一个不存在的路径**(`/usr/local/lib/node_modules/@deepseek-ai`)
99
+ // ⇒ 本脚本要么在 `[2/7]` 报"全局 scope 不存在"(误导:机器上明明有 dsh)、要么在
100
+ // `--patches-only` 上拿这棵不存在的树去 `relocatePatchTarget`(错误更难读)。
101
+ // ⛔ 放在 `if (!target)` 的用法提示**之后**:`node scripts/dsh-upgrade.mjs`(不带参数)仍只打印用法。
102
+ // 例外:`--print-snapshot` 是**只读自检**(⛔ 不碰 scope、不联网、不改状态)⇒ 它允许 scope 缺席。
103
+ const _scopeResolved = resolveDshScopeDir();
104
+ const SCOPE_OPTIONAL = process.argv.includes("--print-snapshot");
105
+ if (!_scopeResolved && !SCOPE_OPTIONAL) {
106
+ console.error(" ❌ 解析不到 dsh 的全局 scope:这台机器上找不到任何 @deepseek-ai 农场");
107
+ console.error(" 已按序试过:`--scope-dir`/`DSH_SCOPE` → `--global-bin`/`DSH_BIN` 反推 → `npm root -g` → " +
108
+ "PATH 上的 dsh → HOME 相对布局 → 当前 node 前缀 → **npx 缓存(~/.npm/_npx/<hash>/…)** → homebrew / Intel 绝对路径。");
109
+ console.error(" ⛔ 旧实现会在这里**静默回一个不存在的路径**(实测 `/usr/local/lib/node_modules/@deepseek-ai`)—— 故不静默。");
110
+ console.error(" 出路:① `--scope-dir <…/@deepseek-ai>`;② `DSH_SCOPE=<…>`;③ `--global-bin <该 scope 自带的 dsh>`(由它反推)。");
111
+ process.exit(1);
112
+ }
113
+ const SCOPE_DIR = _scopeResolved ? _scopeResolved.dir : "";
114
+ const SCOPE_FROM = _scopeResolved ? _scopeResolved.from : "(--print-snapshot 不需要 scope)";
115
+ const GLOBAL_BIN = _scopeResolved ? resolveDshBin({ scopeDir: SCOPE_DIR }) : "";
116
+ if (_scopeResolved) {
117
+ console.log(` [scope] ${SCOPE_DIR}(来源:${SCOPE_FROM})`);
118
+ if (/_npx[\\/]/.test(SCOPE_DIR)) {
119
+ warn("目标 scope 在 **npx 缓存**里(npm 自己管的树):这就是 `npx @deepseek-ai/dsh` 实际在跑的那棵树。");
120
+ console.log(" · 打补丁(--patches-only)改的就是它 —— 合理;");
121
+ console.log(" · 整树替换会改动 npm 的缓存条目 —— 官方改法仍是 `npx @deepseek-ai/dsh@<版本>`。");
122
+ }
123
+ }
124
+
125
+ /**
126
+ * 补丁自检清单(**声明式**,2026-10-02)。
127
+ *
128
+ * 每个补丁的"真打上了吗 / 上游改动把语义改坏了吗"由同目录的 `verify-*.mjs` 回答;
129
+ * 这里只登记"跑哪个脚本、通过/失败时说什么"。新增补丁自检 ⇒ 加一行,**不要再改判定逻辑**
130
+ * (此前两处硬编码 + 三处汇总文案,加一个补丁容易漏改,漏了就是"门禁绿着但补丁没生效")。
131
+ * ⚠️ 必须在下面的 `patchesOnly` 块**之前**声明(那块会同步调用 applyHotfixPatches)。
132
+ */
133
+ const SELF_CHECKS = [
134
+ {
135
+ file: "verify-hardening.mjs",
136
+ label: "加固",
137
+ pass: "dsh 加固补丁校验通过(render 形状 + append 边界)",
138
+ fail: "dsh 加固补丁校验**失败** —— 补丁未生效或上游已改动,先别重启!见 AGENTS.md「会话毒化事故」",
139
+ },
140
+ {
141
+ // 打包副本门禁(2026-09-29 worker 事故):迁移器/校验器在
142
+ // dsh-session-persistence-jsonl 的 **worker.cjs 打包件**里还有一份内联副本,
143
+ // 只打 lib/index.js = 离线校验假绿、真实 resume 照旧失败(session-06732069)。
144
+ file: "verify-bundled-migrators.mjs",
145
+ label: "打包副本",
146
+ pass: "打包副本迁移器 hotfix 校验通过(worker.cjs 与模块一致)",
147
+ fail: "打包副本迁移器 hotfix **失守** —— worker.cjs 未同步,旧会话 resume 会报 stored log is corrupt,先别重启!见 AGENTS.md",
148
+ },
149
+ {
150
+ // TRANSPORT 可诊断性(2026-10-02):问述代理复用官方 dsh-llm-deepseek 适配器,
151
+ // 其 TRANSPORT 分支此前只留 message+code ⇒ 事后分不清 ECONNRESET/提前关闭/5xx 断流。
152
+ file: "verify-transport-diagnostics.mjs",
153
+ label: "TRANSPORT 可诊断性",
154
+ pass: "TRANSPORT 诊断补丁校验通过(cause / streamId / 已产出长度可观测,判定语义未变)",
155
+ fail: "TRANSPORT 诊断补丁**失败** —— 补丁未生效或上游已改动,先别重启!见 scripts/patches/dsh-llm-deepseek--transport-diagnostics.patch 头部说明",
156
+ },
157
+ ];
158
+
159
+ // --patches-only:只重打补丁 + 加固校验 + 校验旧 session 迁移 + preset 门禁,**不动全局 scope**(升级后/排查时可单独跑)
160
+ if (patchesOnly) {
161
+ console.log("[patches-only] 仅重打 scripts/patches/*.patch + 加固校验 + 旧 session 迁移校验 + preset 门禁(不替换 scope、不重启)");
162
+ const r = applyHotfixPatches();
163
+ const g = runPresetGates();
164
+ // 自检项由 applyHotfixPatches 内的 SELF_CHECKS 声明(新增一个 verify-*.mjs 只需在那里加一行)
165
+ const bad = r.migration === false || r.selfChecks.some((c) => c.state === false) || g.schema === false || g.boot === false;
166
+ const selfText = r.selfChecks.map((c) => `${c.label}=${c.state === undefined ? "跳过" : c.state ? "通过" : "失败"}`).join("、");
167
+ console.log(`\n[patches-only] 完成:补丁 ok=${r.ok} warn=${r.warn}、${selfText}`
168
+ + `、迁移校验=${r.migration === null ? "跳过" : r.migration ? "通过" : "失败"}`
169
+ + `、preset schema=${g.schema === null ? "跳过" : g.schema ? "通过" : "失败"}、启动冒烟=${g.boot === null ? "跳过" : g.boot ? "通过" : "失败"}`);
170
+ if (bad) console.log(" ⛔ 有门禁未通过 —— 先别重启服务!");
171
+ process.exit(bad ? 1 : 0);
172
+ }
173
+
174
+ // ── verify:在**隔离 home** 里对旁路树做真实启动冒烟 + preset 挂载探针 ──────
175
+ // 三段式的中间那一步(评审稿 §6):stage 装好、activate 之前,先证明"这棵树真能起来"。
176
+ // 复用既有 `plugin-bootcheck.mjs`(隔离 DSH_HOME + 临时端口 + 把真实 .agent-presets 链进去
177
+ // 逐个 preset 真实 mount)——不另造一套判据。
178
+ if (VERIFY_ROOT) {
179
+ const { looksLikeScope } = await import("./lib/scope-pointer.mjs");
180
+ const vScope = join(VERIFY_ROOT, "lib", "node_modules", "@deepseek-ai");
181
+ if (!looksLikeScope(vScope)) fail(`--verify 的目标不是可运行的 dsh 树(需要 <root>/lib/node_modules/@deepseek-ai 布局):${vScope}`);
182
+ const vBin = join(VERIFY_ROOT, "bin", "dsh");
183
+ console.log(`\n[verify] 旁路树:${vScope}`);
184
+ console.log(`[verify] 版本:${(() => { try { return run(`${JSON.stringify(vBin)} --version`, { silent: true }).trim() || "(无输出)"; } catch (e) { return `取版本失败(${e.status || "?"})`; } })()}`);
185
+ console.log("[verify] 隔离启动冒烟 + preset 挂载探针(不碰默认入口、不占线上端口)...");
186
+ let vCode = 0;
187
+ try {
188
+ run(`node ${JSON.stringify(join(REPO, "scripts", "plugin-bootcheck.mjs"))}`, {
189
+ env: { ...process.env, DSH_SCOPE: vScope, DSH_BIN: existsSync(vBin) ? vBin : undefined },
190
+ });
191
+ } catch (e) { vCode = e.status || 1; }
192
+ if (vCode !== 0) {
193
+ console.error("\n ⛔ 旁路树冒烟未通过 ⇒ **不要激活**(默认入口未动;修好或弃用这棵树)");
194
+ process.exit(1);
195
+ }
196
+ ok("旁路树冒烟通过(隔离 home + preset 逐个挂载 OK)");
197
+ console.log("\n下一步:node scripts/dsh-upgrade.mjs --activate " + VERIFY_ROOT);
198
+ process.exit(0);
199
+ }
200
+
201
+ // ── activate / rollback:只改指针与(可选的)全局 bin 链接 ──────
202
+ if (ACTIVATE_ROOT || ROLLBACK) {
203
+ const { readPointer, writePointer, previousScope, looksLikeScope } = await import("./lib/scope-pointer.mjs");
204
+ const scopeOf = (root) => join(root, "lib", "node_modules", "@deepseek-ai");
205
+ let dest;
206
+ if (ROLLBACK) {
207
+ const cur = readPointer();
208
+ dest = previousScope(process.env, cur);
209
+ if (!dest) fail("审计里找不到可回退的树(用 --activate <root> 显式指定,或 dsh-restore.sh --list 看候选)");
210
+ console.log(`回退:${cur || "(未设置)"} → ${dest}`);
211
+ } else {
212
+ dest = scopeOf(ACTIVATE_ROOT);
213
+ if (!looksLikeScope(dest)) fail(`目标不是可运行的 dsh 树(需要 <root>/lib/node_modules/@deepseek-ai 布局):${dest}`);
214
+ console.log(`激活:${readPointer() || "(未设置)"} → ${dest}`);
215
+ }
216
+ const before = readPointer();
217
+ writePointer(dest, { reason: ROLLBACK ? "dsh-upgrade --rollback" : "dsh-upgrade --activate", previous: before });
218
+ ok(`指针已写入(审计可查:dsh-restore.sh --list)`);
219
+ if (!NO_BIN_LINK) {
220
+ // 兼容期:plist 仍指向全局 bin 时,指针还不够 ⇒ 同时重指 bin(激活包装脚本后可用 --no-bin-link 关掉)
221
+ const bin = resolveDshBin({ scopeDir: dest });
222
+ const binEntry = (() => {
223
+ try {
224
+ const pkg = JSON.parse(readFileSync(join(dest, "dsh", "package.json"), "utf8"));
225
+ return pkg.bin ? (typeof pkg.bin === "string" ? pkg.bin : pkg.bin.dsh || Object.values(pkg.bin)[0]) : null;
226
+ } catch { return null; }
227
+ })();
228
+ if (binEntry && existsSync(join(dest, "dsh", binEntry))) {
229
+ try { unlinkSync(bin); } catch {}
230
+ symlinkSync(join(dest, "dsh", binEntry), bin);
231
+ ok(`全局 bin 已重指:${bin} → ${dest}/dsh/${binEntry}(加 --no-bin-link 可跳过)`);
232
+ } else warn(`未能重指全局 bin(主包无 bin 字段或目标不存在):${bin}`);
233
+ }
234
+ console.log("\n下一步(**不自动重启**):");
235
+ console.log(" bash scripts/dsh-restart.sh # 由你手动重启服务(或让熔断那一次自动重启生效)");
236
+ console.log(" 验收:新/旧会话可开、角色挂载、唤醒链路;不合格 ⇒ node scripts/dsh-upgrade.mjs --rollback");
237
+ process.exit(0);
238
+ }
239
+
240
+ // ── 0. 快照与布局 ────────────────────────────────────────────
241
+ // 快照**真实落点**解析(唯一真源在 scripts/lib/dsh-snapshot-path.mjs):加载与打印共用,
242
+ // 杜绝"打印根目录、实际读 candidate"这种误导排障的偏差(2026-10-12 P1-0 / 审计 §C.1 #8)。
243
+ const snapInfo = locateSnapshot(LOCK_DIR, target, snapshotFile);
244
+ let snapshot = null;
245
+ if (snapInfo.path) { try { snapshot = JSON.parse(readFileSync(snapInfo.path, "utf8")); } catch (e) { fail(`快照解析失败(${snapInfo.path}): ${e.message}`); } }
246
+ const layout = snapshot?.layout || "nested"; // flat-scope: 0.1.1+(顶层 197 包);nested: rc.6(主包内嵌)
247
+ /** 快照落点的那一行输出(**加载与打印同一判据**;`--print-snapshot` 复用它)。 */
248
+ function printSnapshotLine() {
249
+ if (snapshot) ok(`快照: ${snapInfo.path}(${snapInfo.tier};${Object.keys(snapshot.resolved || {}).length} 个子包)`);
250
+ else if (target) warn(`快照: 未找到(已找过 ${snapInfo.searched.join(" → ")})⇒ 本次不做版本分布锁定`);
251
+ }
252
+ // `--print-snapshot`:**只读**自检 —— 打印快照真实落点后立即退出(P1-0 验收用;不改任何状态、不联网)。
253
+ if (process.argv.includes("--print-snapshot")) { printSnapshotLine(); process.exit(0); }
254
+
255
+ // ── 1. 预下载 tarball ────────────────────────────────────────
256
+ console.log(`\n[1/7] 预下载 @deepseek-ai/dsh@${target} tarball ...`);
257
+ const tarball = join("/tmp", `deepseek-ai-dsh-${target}.tgz`);
258
+ // `--from-dir` 表示产物已在本地(离线/内网/演练)⇒ 不该也不需联网下载;
259
+ // 2026-09-29 实测:npm 不可达时这一步会把整条升级卡死在开头。
260
+ if (fromDir) {
261
+ ok(`--from-dir 已给(${fromDir})⇒ 跳过下载`);
262
+ } else if (!existsSync(tarball)) {
263
+ run(`cd /tmp && npm pack @deepseek-ai/dsh@${target} --silent`, { silent: true });
264
+ const found = readdirSync("/tmp").filter((f) => f.startsWith("deepseek-ai-dsh-") && f.endsWith(".tgz"));
265
+ if (!found.length) fail("tarball 下载失败");
266
+ const real = join("/tmp", found[found.length - 1]);
267
+ if (real !== tarball) run(`mv "${real}" "${tarball}"`, { silent: true });
268
+ }
269
+ ok(`tarball: ${tarball}(布局: ${layout})`);
270
+ if (snapshot || target) printSnapshotLine();
271
+
272
+ // ── 2. 备份当前全局 scope ────────────────────────────────────
273
+ // 暂存模式(--stage)不动全局 ⇒ 不需要先备份它(dsh-backup-scope.sh 已提供持久备份)。
274
+ if (STAGE_ROOT) {
275
+ console.log("\n[2/7] --stage:跳过全局备份(本模式不替换全局)");
276
+ } else {
277
+ console.log("\n[2/7] 备份当前全局 @deepseek-ai scope ...");
278
+ if (!existsSync(SCOPE_DIR)) fail(`全局 scope 不存在: ${SCOPE_DIR}`);
279
+ const backupDir = join("/tmp", `dsh-backup-${new Date().toISOString().slice(0, 10)}`);
280
+ mkdirSync(backupDir, { recursive: true });
281
+ const backupScope = join(backupDir, "scope");
282
+ if (existsSync(backupScope)) rmSync(backupScope, { recursive: true, force: true });
283
+ cpSync(SCOPE_DIR, backupScope, { recursive: true, dereference: true });
284
+ ok(`备份: ${backupScope}(含当前 dsh 全树)`);
285
+ }
286
+
287
+ // ── 3. 本地安装(from-dir 直接复用 / npm install + overrides)──
288
+ console.log("\n[3/7] 本地安装 ...");
289
+ let work;
290
+ let depRoot;
291
+ if (fromDir) {
292
+ work = fromDir;
293
+ depRoot = join(work, "node_modules", "@deepseek-ai");
294
+ if (!existsSync(depRoot)) fail(`--from-dir 缺少 node_modules/@deepseek-ai: ${depRoot}`);
295
+ ok(`复用已装目录: ${work}(跳过 npm install)`);
296
+ } else {
297
+ work = join("/tmp", `dsh-install-${target}`);
298
+ rmSync(work, { recursive: true, force: true });
299
+ mkdirSync(work, { recursive: true });
300
+ let pkgJson = { name: "dsh-upgrade-work", private: true, dependencies: { "@deepseek-ai/dsh": `file:${tarball}` } };
301
+ if (snapshot) {
302
+ pkgJson.overrides = {};
303
+ for (const [name, ver] of Object.entries(snapshot.resolved)) {
304
+ // ⚠️ 主包不能进 overrides:它是 `file:<tgz>` 直接依赖,同时 override 会让 npm 报
305
+ // `EOVERRIDE ... conflicts with direct dependency`(2026-09-29 实测,升级在替换全局前失败)。
306
+ if (name === "@deepseek-ai/dsh") { warn("快照里含主包 @deepseek-ai/dsh —— 已跳过其 override(应为生成器 bug)"); continue; }
307
+ pkgJson.overrides[name] = ver;
308
+ }
309
+ }
310
+ writeFileSync(join(work, "package.json"), JSON.stringify(pkgJson, null, 2));
311
+ warn("npm install 进行中(file:+overrides 组合可能极慢,2026-08-21 实测卡死过一次;建议改用 --from-dir)...");
312
+ run(`cd "${work}" && npm install --no-audit --no-fund --loglevel=error`);
313
+ ok("依赖已安装(本地目录,未触碰全局)");
314
+ }
315
+
316
+ // ── 4. 校验版本分布 ──────────────────────────────────────────
317
+ console.log("\n[4/7] 校验版本一致性 ...");
318
+ if (!depRoot) {
319
+ depRoot = layout === "flat-scope"
320
+ ? join(work, "node_modules", "@deepseek-ai")
321
+ : join(work, "node_modules", "@deepseek-ai", "dsh", "node_modules", "@deepseek-ai");
322
+ }
323
+ const installed = collectVersions(depRoot);
324
+ if (!installed) fail(`本地安装缺少依赖目录: ${depRoot}`);
325
+ const dist = {};
326
+ for (const ver of Object.values(installed)) dist[ver] = (dist[ver] || 0) + 1;
327
+ console.log(" 版本分布:", JSON.stringify(dist));
328
+ if (snapshot) {
329
+ const mismatches = [];
330
+ for (const [name, ver] of Object.entries(snapshot.resolved)) {
331
+ if (installed[name] && installed[name] !== ver) mismatches.push(`${name}: 期望 ${ver} 实际 ${installed[name]}`);
332
+ }
333
+ const missing = Object.keys(snapshot.resolved).filter((n) => !installed[n]);
334
+ if (mismatches.length || missing.length) {
335
+ warn("快照不匹配:");
336
+ [...mismatches, ...missing.map((n) => `${n}: 缺失`)].slice(0, 10).forEach((m) => warn(" " + m));
337
+ } else {
338
+ ok(`子包版本与快照 ${target} 完全一致(${Object.keys(installed).length} 个)`);
339
+ }
340
+ } else {
341
+ warn("未提供快照:安装为 npm 当前解析结果,建议装后生成快照存档");
342
+ }
343
+
344
+ // ── 5s. 暂存模式:装到旁路树 + 在那棵树上跑门禁(**不动全局、不重启**)──
345
+ if (STAGE_ROOT) {
346
+ const stageScope = join(STAGE_ROOT, "lib", "node_modules", "@deepseek-ai");
347
+ console.log(`\n[5s/7] --stage:装到旁路树 ${stageScope}`);
348
+ if (existsSync(stageScope)) rmSync(stageScope, { recursive: true, force: true });
349
+ mkdirSync(stageScope, { recursive: true });
350
+ cpSync(depRoot, stageScope, { recursive: true });
351
+ // flat-scope 布局下顶层还有非 @deepseek-ai 依赖(js-yaml 等)⇒ 一并放进旁路树
352
+ // 顶层依赖(flat-scope 布局下 js-yaml 等在 scope 之外)。注意:
353
+ // · 源目录可能混着**别的**全局包与**断链**(实测 `/opt/homebrew/lib/node_modules/dsh-rc6-global-install`
354
+ // 就是断链)—— 直接 cpSync 会 ENOENT 崩掉整条升级 ⇒ 逐个 try/catch,跳过并点名;
355
+ // · nested 布局没有顶层依赖可补 ⇒ 不做这一步。
356
+ if (layout === "flat-scope") {
357
+ const srcTop = join(work, "node_modules");
358
+ const destTop = dirname(stageScope);
359
+ const skipped = [];
360
+ if (existsSync(srcTop)) {
361
+ for (const item of readdirSync(srcTop)) {
362
+ if (item === "@deepseek-ai") continue;
363
+ const dest = join(destTop, item);
364
+ if (existsSync(dest)) continue;
365
+ try { cpSync(join(srcTop, item), dest, { recursive: true }); }
366
+ catch (e) { skipped.push(`${item}(${(e && e.code) || "ERR"})`); }
367
+ }
368
+ }
369
+ if (skipped.length) warn(`顶层依赖跳过 ${skipped.length} 项(断链/不可读,不影响 dsh 本体):${skipped.join("、")}`);
370
+ else ok("顶层依赖已补齐");
371
+ }
372
+ // 自包含入口:<root>/bin/dsh(不需要 sudo、不动全局 bin)
373
+ const stageBin = join(STAGE_ROOT, "bin", "dsh");
374
+ mkdirSync(dirname(stageBin), { recursive: true });
375
+ const stagePkg = JSON.parse(readFileSync(join(stageScope, "dsh", "package.json"), "utf8"));
376
+ const stageEntry = stagePkg.bin
377
+ ? (typeof stagePkg.bin === "string" ? stagePkg.bin : stagePkg.bin.dsh || Object.values(stagePkg.bin)[0])
378
+ : "lib/bin.js";
379
+ try { unlinkSync(stageBin); } catch {}
380
+ symlinkSync(join(stageScope, "dsh", stageEntry), stageBin);
381
+ ok(`旁路树就绪(入口 ${stageBin})`);
382
+
383
+ // 门禁**在那棵树上**跑:共享解析器认 DSH_SCOPE/DSH_BIN(复用 --patches-only 的全部判据)
384
+ console.log("\n[6s/7] 在旁路树上重打 hotfix 补丁 + 跑六项门禁(隔离,不碰全局)...");
385
+ let gateCode = 0;
386
+ try {
387
+ run(`node ${JSON.stringify(join(REPO, "scripts", "dsh-upgrade.mjs"))} --patches-only`, {
388
+ env: { ...process.env, DSH_SCOPE: stageScope, DSH_BIN: stageBin },
389
+ });
390
+ } catch (e) { gateCode = e.status || 1; }
391
+ if (gateCode !== 0) {
392
+ console.error("\n ⛔ 旁路树门禁未通过 ⇒ **不要激活**(默认入口完全没动,删掉旁路树即可)");
393
+ console.error(` 看详情:node scripts/dsh-upgrade.mjs --patches-only # 带 DSH_SCOPE=${stageScope}`);
394
+ process.exit(1);
395
+ }
396
+ ok("旁路树门禁全绿");
397
+ console.log("\n下一步(三段式的后两步,**都不自动重启**):");
398
+ console.log(` ① 真实形态冒烟(可选):用克隆的 home 在旁路树上起一次隔离实例`);
399
+ console.log(` ② 激活:node scripts/dsh-upgrade.mjs --activate ${STAGE_ROOT}`);
400
+ console.log(` 回退:node scripts/dsh-upgrade.mjs --rollback`);
401
+ process.exit(0);
402
+ }
403
+
404
+ // ── 5. 替换全局 scope ────────────────────────────────────────
405
+ console.log("\n[5/7] 替换全局 scope ...");
406
+ rmSync(SCOPE_DIR, { recursive: true, force: true });
407
+ mkdirSync(SCOPE_DIR, { recursive: true });
408
+ if (layout === "flat-scope") {
409
+ // 0.1.1+:顶层扁平 → 整个 scope 全部替换(197 包)
410
+ cpSync(depRoot, SCOPE_DIR, { recursive: true });
411
+ // 顶层非 scope 依赖(js-yaml 等)→ 补到全局顶层,跳过已存在(保护用户其他全局包)
412
+ const srcTop = join(work, "node_modules");
413
+ const destTop = dirname(SCOPE_DIR);
414
+ for (const item of readdirSync(srcTop)) {
415
+ const src = join(srcTop, item);
416
+ const dest = join(destTop, item);
417
+ if (item === "@deepseek-ai") continue;
418
+ if (!existsSync(dest)) cpSync(src, dest, { recursive: true });
419
+ }
420
+ ok("顶层依赖已补齐(跳过已存在的用户全局包)");
421
+ } else {
422
+ // rc.6:只主包,嵌套子包随主包带入
423
+ cpSync(join(work, "node_modules", "@deepseek-ai", "dsh"), join(SCOPE_DIR, "dsh"), { recursive: true });
424
+ }
425
+ // 重建 bin 链接(从主包 package.json 的 bin 字段解析)
426
+ try { unlinkSync(GLOBAL_BIN); } catch {}
427
+ const mainPkg = JSON.parse(readFileSync(join(SCOPE_DIR, "dsh", "package.json"), "utf8"));
428
+ const binEntry = mainPkg.bin && (typeof mainPkg.bin === "string" ? mainPkg.bin : mainPkg.bin.dsh || Object.values(mainPkg.bin)[0]);
429
+ if (binEntry) {
430
+ const binTarget = join(SCOPE_DIR, "dsh", binEntry);
431
+ if (existsSync(binTarget)) {
432
+ symlinkSync(binTarget, GLOBAL_BIN);
433
+ ok(`bin 链接: ${GLOBAL_BIN} → ${binTarget}`);
434
+ } else warn(`bin 目标不存在: ${binTarget}`);
435
+ } else warn("主包无 bin 字段,跳过 bin 链接");
436
+ ok("已替换全局,备份保留在 " + backupScope);
437
+
438
+ // ── 5.5 hotfix 补丁重打 + 旧 session 迁移校验 ────────────────
439
+ // 2026-09-10 教训:升级整树替换 scope ⇒ 打在包内的 hotfix 会被**静默冲掉**(0.1.5 的
440
+ // session-format v0→v1 漏认 `permission/preset.origin` 曾导致重启后旧 session 无法加载)。
441
+ // 故每次升级后必须重打 patches/ 下的补丁,并对**最旧真实 session** 跑完整迁移链校验。
442
+ console.log("\n[5.5/7] 重打 hotfix 补丁 + 旧 session 迁移校验 + preset 门禁 ...");
443
+ const hotfix = applyHotfixPatches();
444
+ const gates = runPresetGates();
445
+ // ── 6. 验证 ──────────────────────────────────────────────────
446
+ console.log("\n[6/7] 验证 ...");
447
+ try {
448
+ const v = run("dsh --version", { silent: true }).trim().split("\n")[0];
449
+ ok(`dsh --version = ${v}`);
450
+ } catch { fail("dsh --version 失败(已回滚,见下)"); }
451
+ const probes = ["dsh-client-ui-primitives", "dsh-client-ui-sidebar", "dsh-client-ui-conversation"];
452
+ for (const p of probes) {
453
+ if (!existsSync(join(SCOPE_DIR, p))) warn(`全局 scope 缺 ${p}`);
454
+ }
455
+ for (const p of probes) {
456
+ let found = false;
457
+ for (const profile of safeProfiles()) {
458
+ const link = join(PROFILES, profile, "node_modules", "@deepseek-ai", p);
459
+ if (existsSync(link)) { found = true; break; }
460
+ }
461
+ if (!found) warn(`profiles 中未发现 ${p}(重启后如卡加载屏,按 docs 恢复 profiles 依赖)`);
462
+ }
463
+ try {
464
+ const code = run("curl -s -o /dev/null -w %{http_code} --max-time 10 http://127.0.0.1:3080/", { silent: true }).trim();
465
+ if (code === "200") ok("页面 200(旧进程仍在跑,属正常)"); else warn(`页面状态码 ${code}`);
466
+ } catch { warn("页面检查失败"); }
467
+
468
+ // ── 7. 收尾 ──────────────────────────────────────────────────
469
+ console.log("\n[7/7] 完成。");
470
+ console.log(" ✅ 升级成功(未重启服务,旧进程仍用旧代码)");
471
+ const failedSelfChecks = hotfix.selfChecks.filter((c) => c.state === false);
472
+ if (hotfix.migration === false || failedSelfChecks.length > 0 || gates.schema === false || gates.boot === false) {
473
+ console.log(" ⛔ 门禁未全部通过 —— 先别重启服务!修好下面这些再重启:");
474
+ for (const c of failedSelfChecks) console.log(` · 补丁自检失败:${c.label}(node scripts/patches/${c.file} 看详情)`);
475
+ if (hotfix.migration === false) console.log(" · 旧 session 迁移校验失败(见 AGENTS.md「session 迁移 hotfix」)");
476
+ if (gates.schema === false) console.log(" · preset schema 门禁失败(node scripts/validate-preset-schema.mjs --runtime)");
477
+ if (gates.boot === false) console.log(" · 启动冒烟失败(node scripts/plugin-bootcheck.mjs,含 preset 挂载)");
478
+ console.log(" 或回滚(见下方回滚步骤)后再重启");
479
+ }
480
+ console.log(" ⏭ 生效:用户手动执行 bash ~/Code/argent-dsh/scripts/dsh-restart.sh");
481
+ console.log(" ⏭ 回滚:");
482
+ console.log(` sudo rm -rf ${SCOPE_DIR}`);
483
+ console.log(` sudo cp -R ${backupScope} ${SCOPE_DIR}`);
484
+ console.log(` sudo ln -sfn ${join(SCOPE_DIR, "dsh", binEntry || "lib/bin.js")} ${GLOBAL_BIN}`);
485
+ console.log(" 然后重启服务(同上 kickstart 命令)");
486
+
487
+ // ── helpers ──────────────────────────────────────────────────
488
+ function collectVersions(dir) {
489
+ if (!existsSync(dir)) return null;
490
+ const out = {};
491
+ for (const name of readdirSync(dir)) {
492
+ const pj = join(dir, name, "package.json");
493
+ if (existsSync(pj)) {
494
+ try { out[name] = JSON.parse(readFileSync(pj, "utf8")).version; } catch {}
495
+ }
496
+ }
497
+ return out;
498
+ }
499
+ function safeProfiles() {
500
+ try {
501
+ return readdirSync(PROFILES).filter((d) => {
502
+ try { return statSync(join(PROFILES, d)).isDirectory(); } catch { return false; }
503
+ });
504
+ } catch { return []; }
505
+ }
506
+
507
+ /**
508
+ * 重打 scripts/patches/*.patch 到全局 scope,并跑各补丁的**自检脚本**(`verify-*.mjs`)
509
+ * + 最旧真实 session 的迁移链校验。
510
+ * 返回值:{ ok, warn, selfChecks: [{ file, label, state }], migration }
511
+ * selfChecks[].state: true 通过 / false 失败 / undefined 无该脚本
512
+ * migration: true 通过 / false 失败 / null 跳过
513
+ *
514
+ * 2026-10-02:自检项从"两处硬编码"改成**声明式清单**(SELF_CHECKS)—— 新增一个补丁自检
515
+ * 只需在清单里加一行,不必再动这里的判定逻辑(此前每加一个补丁都要改三处汇总文案,
516
+ * 容易漏、漏了就是"门禁绿着但补丁没生效")。
517
+ */
518
+ function applyHotfixPatches() {
519
+ const out = { ok: 0, warn: 0, selfChecks: [], migration: null };
520
+ const patchDir = join(REPO, "scripts", "patches");
521
+ if (!existsSync(patchDir)) { warn(`无补丁目录:${patchDir}`); return out; }
522
+ for (const pf of readdirSync(patchDir).filter((f) => f.endsWith(".patch"))) {
523
+ const text = readFileSync(join(patchDir, pf), "utf8");
524
+ // 版本门(2026-09-29):补丁可声明 `# requires-dsh: >=0.1.7`。上游在不同版本里
525
+ // 结构不同——同一个补丁不可能适配两版;不适用的**静默跳过**(不是失败),
526
+ // 否则门禁常红、训练人忽略真红。
527
+ const req = /^#\s*requires-dsh:\s*(>=|>)?\s*([0-9][\w.-]*)\s*$/m.exec(text);
528
+ if (req) {
529
+ const instVer = JSON.parse(readFileSync(join(SCOPE_DIR, "dsh", "package.json"), "utf8")).version;
530
+ const okVer = req[1] === ">" ? cmpVerText(instVer, req[2]) > 0 : cmpVerText(instVer, req[2]) >= 0;
531
+ if (!okVer) { console.log(` ⏭ 跳过补丁 ${pf}(要求 dsh ${req[1] || ">="}${req[2]},当前 ${instVer})`); continue; }
532
+ }
533
+ const header = ((/^\+\+\+\s+(\S+)/m.exec(text) || [])[1] || "").trim();
534
+ // 2026-09-28 客户机教训:补丁头里写的是**打补丁那台机器**的绝对路径
535
+ // (/opt/homebrew/lib/node_modules/@deepseek-ai/...),客户机 dsh 装在
536
+ // ~/node/lib/node_modules/ ⇒ 直接 existsSync 必然 false,补丁静默跳过。
537
+ // 正解:按 @deepseek-ai/ 之后的包内相对路径重定位到**当前 scope**(lib/patch-target.mjs)。
538
+ const target = relocatePatchTarget(header, SCOPE_DIR);
539
+ if (!target || !existsSync(target)) {
540
+ warn(`补丁 ${pf}: 目标文件不存在(上游可能已改动)→ ${header || "无法解析"}`);
541
+ out.warn++;
542
+ continue;
543
+ }
544
+ const r = spawnSync("patch", ["--forward", "--batch", "-i", join(patchDir, pf), target], { encoding: "utf8" });
545
+ const log = `${r.stdout || ""}${r.stderr || ""}`;
546
+ if (r.status === 0) { ok(`补丁已重打: ${pf} → ${target.replace(SCOPE_DIR + "/", "")}`); out.ok++; }
547
+ else if (/previously applied|Reversed \(or previously applied\)|Skipping patch/i.test(log)) { ok(`补丁已是最新(无需重打): ${pf}`); out.ok++; }
548
+ else { warn(`补丁应用失败(需人工核对,可能上游已修复): ${pf} :: ${log.trim().split("\n").pop() || ""}`); out.warn++; }
549
+ }
550
+ // 补丁自检(声明式清单):每个补丁的 `verify-*.mjs` 在这里登记一行即可。
551
+ // 放在迁移校验**之前**,这样它们不依赖"本机存在 session 文件"这个前提。
552
+ for (const check of SELF_CHECKS) {
553
+ const script = join(patchDir, check.file);
554
+ out.selfChecks.push({ file: check.file, label: check.label, state: undefined });
555
+ const slot = out.selfChecks[out.selfChecks.length - 1];
556
+ if (!existsSync(script)) { warn(`缺补丁自检脚本 ${check.file},跳过「${check.label}」`); continue; }
557
+ try {
558
+ run(`node ${script}`, { silent: true });
559
+ ok(check.pass);
560
+ slot.state = true;
561
+ } catch {
562
+ warn(check.fail);
563
+ slot.state = false;
564
+ }
565
+ }
566
+ const verify = join(patchDir, "verify-session-migration.mjs");
567
+ if (existsSync(verify)) {
568
+ const sessRoot = join(process.env.DSH_HOME || join(process.env.HOME || "", ".dsh"), "sessions");
569
+ // 抽样本(2026-09-29 修正):原来只取 `session.jsonl.zstd`(**未版本化=v0**)里最旧的一个
570
+ // ⇒ v3/v4 一个都没覆盖,而当天真正炸的正是 v3→v4 迁移(门禁全绿、会话照样打不开)。
571
+ // 现在按"未版本化 / 版本化"两组各取**最新+最旧**,钉住 v0 与 v3/v4 两条迁移链。
572
+ const samples = [];
573
+ for (const pattern of ["session.jsonl.zstd", "session.v*.jsonl.zstd"]) {
574
+ let list = "";
575
+ try { list = run(`ls -t ${sessRoot}/*/*/${pattern} 2>/dev/null`, { silent: true }).trim(); } catch { /* 无该形态 */ }
576
+ if (!list) continue;
577
+ const arr = list.split("\n").map((s) => s.trim()).filter(Boolean);
578
+ for (const f of [arr[0], arr[arr.length - 1]]) if (f && !samples.includes(f)) samples.push(f);
579
+ }
580
+ if (samples.length === 0) { warn("未找到 session 文件,跳过旧 session 迁移校验"); return out; }
581
+ try {
582
+ run(`node ${verify} ${samples.join(" ")}`, { silent: true });
583
+ ok(`旧 session 迁移校验通过(${samples.length} 个样本:v0 与 v3/v4 各最新/最旧)`);
584
+ out.migration = true;
585
+ } catch {
586
+ warn("旧 session 迁移校验**失败** —— 升级后旧会话可能无法加载,先别重启!见 AGENTS.md「0.1.5/0.1.7 session 迁移 hotfix」");
587
+ out.migration = false;
588
+ }
589
+ }
590
+ return out;
591
+ }
592
+
593
+ /**
594
+ * 升级后 preset 门禁:0.1.5 起 preset 挂载失败是**致命**的(旧 session resume 直接报错,
595
+ * 2026-09-10 事故:persona 行 `text:` 应为 `prefix:`,0.1.1 容忍、0.1.5 拒绝)。
596
+ * 两项均为**非致命警告**:是否回滚/是否重启由人决定,但必须显式告知"先别重启"。
597
+ * 返回值:{ schema: true/false/null(跳过), boot: true/false/null }
598
+ */
599
+ function runPresetGates() {
600
+ const out = { schema: null, boot: null };
601
+ const schema = join(REPO, "scripts", "validate-preset-schema.mjs");
602
+ const boot = join(REPO, "scripts", "plugin-bootcheck.mjs");
603
+ if (existsSync(schema)) {
604
+ try { run(`node ${schema} --runtime`, { silent: true }); ok("preset schema 门禁通过(preset 行 config 全部合法)"); out.schema = true; }
605
+ catch { warn("preset schema 门禁**失败** —— 行 config 不合法,0.1.5 上该 preset 会挂载失败,先别重启!"); out.schema = false; }
606
+ } else warn("缺 scripts/validate-preset-schema.mjs,跳过 preset schema 门禁");
607
+ if (existsSync(boot)) {
608
+ try { run(`node ${boot}`, { silent: true }); ok("启动冒烟通过(隔离 home;含 preset 真实挂载)"); out.boot = true; }
609
+ catch { warn("启动冒烟**失败** —— 隔离 home 下 dsh 起不来或 preset 挂载失败,先别重启!(node scripts/plugin-bootcheck.mjs 看详情)"); out.boot = false; }
610
+ } else warn("缺 scripts/plugin-bootcheck.mjs,跳过启动冒烟");
611
+ return out;
612
+ }