@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,214 @@
1
+ /**
2
+ * dsh-web-probe.mjs —— 「运行中的 dsh web 到底带了哪些 --trusted-host」的唯一判据(2026-10-01)
3
+ *
4
+ * ## 为什么单独抽一个模块
5
+ * `scripts/dsh-restart.sh` 与 `scripts/dsh-remote-check.sh` 都要回答同一个问题:
6
+ * **launchd 里声明的 trusted-host,运行中的进程真的采用了吗?**(改了 plist ≠ 生效:
7
+ * `kickstart -k` 会用缓存的旧定义,2026-09-22 实测新地址继续 403。)
8
+ *
9
+ * 之前两处各写一遍、而且都只看**一个**进程,实测两类假警报:
10
+ * ① **只看第一个匹配**(`head -1`)⇒ dsh web 是**父子两段**:
11
+ * 启动器 `node scripts/dsh-web-launch.mjs web …`(做鉴权/围栏参数拼装)
12
+ * + 真正的服务 `node …/@deepseek-ai/dsh/lib/bin.js web …`(采用参数的那个)。
13
+ * 父进程带了、子进程没带(或反之)时,单看一个进程必然误判。
14
+ * ② **正则锚在 `^node `**:launchd 起的进程 argv[0] 是**绝对路径**(`/opt/homebrew/bin/node`)
15
+ * ⇒ `^node ` 永不匹配,`RUN_ARGS` 恒为空 ⇒ 每个地址都被报成"未采用"(实测假警报)。
16
+ *
17
+ * 现在的判据只有一条:**收集全部 dsh web 相关进程,取 trusted-host 并集**,再与期望值比对。
18
+ * 「并集」在语义上是对的 —— 整个服务栈采用了的地址,就是这两段进程里出现过的地址。
19
+ *
20
+ * ## 判据的边界(宁可不报,也不误报)
21
+ * · 只认 argv[0] 是 `node`(基名)的进程 ⇒ **调用本脚本的那个 bash 命令行**(命令行文本里
22
+ * 恰好含 "dsh web" 字样)不会被算进来(2026-09-29 踩过的那个假阳性);
23
+ * · 认不出任何 dsh web 进程 ≠ "没采用":那是 **not-running / probe-failed**,分开报,
24
+ * 绝不打印空白的"进程="让人误解(2026-09-30 用户实际遇到的输出)。
25
+ *
26
+ * ## 用法
27
+ * ```bash
28
+ * node scripts/lib/dsh-web-probe.mjs --want "10.126.126.2:3080 10.126.126.1:3080"
29
+ * node scripts/lib/dsh-web-probe.mjs --want "…" --json # 机器可读
30
+ * node scripts/lib/dsh-web-probe.mjs --want "…" --ps-file fix.txt # 夹具(测试用,不跑真 ps)
31
+ * ```
32
+ * 退出码:0 通过(含"期望值为空 ⇒ 无需核对")| 1 真的缺失(唯一该告警的情形)
33
+ * | 2 没有运行中的 dsh web 进程 | 3 探测失败(ps 跑不了)
34
+ */
35
+ import { spawnSync } from 'node:child_process'
36
+ import { readFileSync } from 'node:fs'
37
+
38
+ /** 地址形状(`ip:port`)—— plist 与进程参数都用它。 */
39
+ export const HOST_RE = /^\d{1,3}(?:\.\d{1,3}){3}:\d+$/
40
+
41
+ /** 进程种类:launcher = 启动器(脚本);web = 真正跑服务的 dsh 子进程。 */
42
+ export const PROC_KINDS = ['launcher', 'web']
43
+
44
+ const str = (v, d = '') => (v === null || v === undefined ? d : String(v))
45
+
46
+ /** argv[0] 的基名是不是 node(launchd 起的 argv[0] 是**绝对路径**,形如 `<node 前缀>/bin/node`)。 */
47
+ function isNodeArgv0(argv0) {
48
+ const b = str(argv0).split('/').pop().toLowerCase()
49
+ return b === 'node' || b === 'node.exe'
50
+ }
51
+
52
+ /**
53
+ * 这个命令行是不是「dsh web 相关进程」?返回 `'launcher' | 'web' | ''`。
54
+ * 刻意只认三个形态(宁可漏认、不可把无关进程算进来):
55
+ * · `<…>/dsh-web-launch.mjs web …` —— 启动器(父)
56
+ * · `<…>/dsh/lib/bin.js web …` —— 真正的服务(子;`@deepseek-ai/dsh` 的 bin)
57
+ * · `dsh web …` —— 有人直接敲 CLI 的形态
58
+ */
59
+ export function classifyDshWeb(command) {
60
+ const c = str(command)
61
+ if (/(?:^|[/\s])dsh-web-launch\.mjs(?:\s|$)/.test(c)) return 'launcher'
62
+ if (/dsh\/(?:lib\/)?bin\.js\s+web(?:\s|$)/.test(c)) return 'web'
63
+ if (/(?:^|\s)dsh\s+web(?:\s|$)/.test(c)) return 'web'
64
+ return ''
65
+ }
66
+
67
+ /** 从一个命令行里抽出全部 `--trusted-host` 取值(`--trusted-host X` 与 `--trusted-host=X` 都认)。 */
68
+ export function extractTrustedHosts(command) {
69
+ const out = []
70
+ const c = str(command)
71
+ const re = /--trusted-host(?:=|\s+)([^\s'"]+)/g
72
+ let m
73
+ while ((m = re.exec(c)) !== null) {
74
+ const v = m[1].replace(/^['"]|['"]$/g, '')
75
+ if (HOST_RE.test(v)) out.push(v)
76
+ }
77
+ return out
78
+ }
79
+
80
+ /**
81
+ * 解析 `ps -ww -Ao pid=,ppid=,command=` 的输出(`ps` 的默认格式:pid、ppid 各占一列,
82
+ * 之后整行都是 command)。夹具测试直接喂文本,不跑真 ps。
83
+ * 返回**全部**进程行(含无关进程),过滤在 {@link pickDshWebProcs} 里做 —— 两件事分开才好测。
84
+ */
85
+ export function parsePsOutput(text) {
86
+ const rows = []
87
+ for (const line of str(text).split('\n')) {
88
+ const m = /^\s*(\d+)\s+(\d+)\s+(.*\S)\s*$/.exec(line)
89
+ if (!m) continue
90
+ rows.push({ pid: Number(m[1]), ppid: Number(m[2]), command: m[3] })
91
+ }
92
+ return rows
93
+ }
94
+
95
+ /** 从进程行里挑出 dsh web 相关进程(argv[0] 必须是 node),带 kind 与该进程自己带的 trusted-host。 */
96
+ export function pickDshWebProcs(rows, { selfPid = 0 } = {}) {
97
+ const out = []
98
+ for (const r of rows) {
99
+ if (selfPid && r.pid === selfPid) continue // 探测脚本自己:不算
100
+ const argv0 = str(r.command).split(/\s+/)[0] || ''
101
+ if (!isNodeArgv0(argv0)) continue // bash -c / grep / 编辑器 … 一律不算
102
+ const kind = classifyDshWeb(r.command)
103
+ if (!kind || !PROC_KINDS.includes(kind)) continue
104
+ out.push({ pid: r.pid, ppid: r.ppid, kind, command: r.command, hosts: extractTrustedHosts(r.command) })
105
+ }
106
+ // 先启动器后服务(读起来顺;pid 升序兜底),便于排障时按父子顺序看
107
+ out.sort((a, b) => (a.pid - b.pid))
108
+ return out
109
+ }
110
+
111
+ /**
112
+ * 核心判据:**全部相关进程的 trusted-host 并集** vs 期望值。
113
+ * @returns {{state:'ok'|'missing'|'not-running'|'probe-failed', want:string[], union:string[],
114
+ * missing:string[], procs:Array, detail:string}}
115
+ * · `ok` —— 期望值都在并集里(或期望值为空 = 无需核对)
116
+ * · `missing` —— **真的没采用**(唯一该告警的情形)
117
+ * · `not-running` —— 一个 dsh web 进程都没找到(未运行 / 进程名不符)
118
+ * · `probe-failed`—— ps 跑不了(探测失败)——与"没采用"是两回事
119
+ */
120
+ export function probeTrustedHosts(psText, wantHosts, opts = {}) {
121
+ const want = (Array.isArray(wantHosts) ? wantHosts : str(wantHosts).split(/\s+/))
122
+ .map((h) => str(h).trim()).filter(Boolean)
123
+ const rows = parsePsOutput(psText)
124
+ const procs = pickDshWebProcs(rows, opts)
125
+ const union = [...new Set(procs.flatMap((p) => p.hosts))]
126
+ const missing = want.filter((h) => !union.includes(h))
127
+ if (!procs.length) {
128
+ return {
129
+ state: 'not-running', want, union, missing: [], procs,
130
+ detail: want.length
131
+ ? `没找到运行中的 dsh web 进程(未运行,或进程名/启动方式与判据不符)⇒ **无法核对** ${want.join(' ')};这不是「未采用」`
132
+ : '没找到运行中的 dsh web 进程(未运行)',
133
+ }
134
+ }
135
+ if (missing.length) {
136
+ return {
137
+ state: 'missing', want, union, missing, procs,
138
+ detail: `运行中的 dsh web 进程**确实没带**这些 trusted-host:${missing.join(' ')}`,
139
+ }
140
+ }
141
+ return {
142
+ state: 'ok', want, union, missing: [], procs,
143
+ detail: want.length
144
+ ? `${procs.length} 个 dsh web 进程的 trusted-host 并集已覆盖 plist 声明的全部地址`
145
+ : 'plist 未声明 trusted-host(无需核对)',
146
+ }
147
+ }
148
+
149
+ /** 真跑一次 `ps`(`-ww` = 不截断命令行,macOS/Linux 都认)。 */
150
+ export function runPs() {
151
+ const r = spawnSync('ps', ['-ww', '-Ao', 'pid=,ppid=,command='], { encoding: 'utf8', timeout: 10000 })
152
+ if (r.error || r.status !== 0) {
153
+ return { ok: false, text: '', error: str(r.error && r.error.message) || `ps 退出码 ${r.status}` }
154
+ }
155
+ return { ok: true, text: str(r.stdout), error: '' }
156
+ }
157
+
158
+ /** 人读输出(两个脚本共用同一套文案,避免第二份口径)。 */
159
+ export function formatProbe(res, { prefix = ' ' } = {}) {
160
+ const lines = []
161
+ const procs = res.procs || []
162
+ if (res.state === 'ok') {
163
+ lines.push(`${prefix}✅ trusted-host 核对通过:${(res.want || []).join(' ') || '(plist 未声明)'}`)
164
+ for (const p of procs) {
165
+ lines.push(`${prefix} · pid ${p.pid}(${p.kind === 'launcher' ? '启动器' : 'dsh web 主体'}):${p.hosts.join(' ') || '(无 --trusted-host)'}`)
166
+ }
167
+ return lines.join('\n')
168
+ }
169
+ if (res.state === 'missing') {
170
+ lines.push(`${prefix}❌ ${res.detail}`)
171
+ lines.push(`${prefix} 期望(plist):${(res.want || []).join(' ')}`)
172
+ lines.push(`${prefix} 实际并集(${procs.length} 个进程):${(res.union || []).join(' ') || '(空)'}`)
173
+ for (const p of procs) {
174
+ const miss = (res.want || []).filter((h) => !p.hosts.includes(h))
175
+ lines.push(`${prefix} · pid ${p.pid}(${p.kind === 'launcher' ? '启动器' : 'dsh web 主体'}):`
176
+ + `${p.hosts.join(' ') || '(无 --trusted-host)'}${miss.length ? ` ← 缺 ${miss.join(' ')}` : ' ← 该进程已带全'}`)
177
+ }
178
+ lines.push(`${prefix} ↳ 说明 launchd 仍在用旧定义;可再跑一次 dsh-restart.sh,或用 bash scripts/dsh-remote-check.sh 复核`)
179
+ return lines.join('\n')
180
+ }
181
+ // not-running / probe-failed:**明确**说清"没核对上",不许留空白
182
+ lines.push(`${prefix}⚠️ ${res.detail}`)
183
+ return lines.join('\n')
184
+ }
185
+
186
+ // ── CLI ──────────────────────────────────────────────────────────────
187
+ function main(argv) {
188
+ const args = argv.slice(2)
189
+ const val = (name) => { const i = args.indexOf(name); return i >= 0 ? args[i + 1] : '' }
190
+ const want = val('--want')
191
+ const psFile = val('--ps-file')
192
+ const asJson = args.includes('--json')
193
+ const wantList = want.split(/\s+/).filter(Boolean)
194
+ // plist 里没有 trusted-host ⇒ 没有可核对的东西:直接通过(不要因为"没找到进程"报警)
195
+ if (!wantList.length) {
196
+ const res = { state: 'ok', want: [], union: [], missing: [], procs: [], detail: '期望值为空(plist 未声明 trusted-host)⇒ 无需核对' }
197
+ process.stdout.write(asJson ? `${JSON.stringify(res)}\n` : `${formatProbe(res)}\n`)
198
+ return 0
199
+ }
200
+ const ps = psFile ? { ok: true, text: readFileSync(psFile, 'utf8'), error: '' } : runPs()
201
+ if (!ps.ok) {
202
+ const res = { state: 'probe-failed', want: wantList, union: [], missing: [], procs: [], detail: `ps 跑不了(${ps.error})⇒ 无法核对运行参数` }
203
+ process.stdout.write(asJson ? `${JSON.stringify(res)}\n` : `${formatProbe(res)}\n`)
204
+ return 3
205
+ }
206
+ const res = probeTrustedHosts(ps.text, wantList, { selfPid: process.pid })
207
+ process.stdout.write(asJson ? `${JSON.stringify(res)}\n` : `${formatProbe(res)}\n`)
208
+ if (res.state === 'missing') return 1
209
+ if (res.state === 'not-running') return 2
210
+ return 0
211
+ }
212
+
213
+ const invokedDirectly = process.argv[1] && process.argv[1].endsWith('dsh-web-probe.mjs')
214
+ if (invokedDirectly) process.exit(main(process.argv))
@@ -0,0 +1,61 @@
1
+ /**
2
+ * is-direct-run.mjs —— **"本文件是否被直接执行"的唯一判据**(2026-10-04)
3
+ *
4
+ * ══════════════════════════════════════════════════════════════════════════
5
+ * 一、为什么需要它(一个**静默**的坑:脚本"跑了"却什么都没发生)
6
+ * ══════════════════════════════════════════════════════════════════════════
7
+ * 本仓库的 CLI 脚本几乎都是"可当模块 import(给测试用纯函数)+ 可直接执行(跑主逻辑)"两用形态,
8
+ * 于是每个文件末尾都有一道入口守卫。历史上它被写成四种形态,其中三种在**符号链接路径**下**假否定**:
9
+ *
10
+ * | 形态 | 软链下 | 说明 |
11
+ * |---|---|---|
12
+ * | `import.meta.url === pathToFileURL(process.argv[1]).href` | ❌ 假否定 | `argv[1]` 是**逻辑**路径(软链),`import.meta.url` 是 Node **已 realpath** 的物理路径 |
13
+ * | ``import.meta.url === `file://${process.argv[1]}` `` | ❌ 假否定 | 同上,且没做 URL 编码(路径含空格/中文时更早出错) |
14
+ * | `resolve(argv[1]) === resolve(fileURLToPath(import.meta.url))` | ❌ 假否定 | `resolve()` 只做 `..`/相对路径归一,**不解析软链** |
15
+ * | `import.meta.url.endsWith(basename(argv[1]))` | ⚠️ 碰巧能跑 | 但判据退化成"文件名相同"(同名文件即假肯定) |
16
+ *
17
+ * 为什么"软链路径"在本项目是**常态而非边角**:macOS 上 `/tmp` → `/private/tmp`、
18
+ * `/var/folders/...` → `/private/var/folders/...`(`mktemp -d` 的默认落点!),
19
+ * 而 Node 对**主模块**默认做 realpath(除非 `--preserve-symlinks-main`)
20
+ * ⇒ 任何"在 /tmp(逻辑路径)下执行脚本"的场景都会命中,**表现为脚本静默不干活**(没有报错、没有输出、
21
+ * 退出码 0)—— 2026-10-04 实测:升级器的夹具里 CLI 就是这么静默不跑的。
22
+ *
23
+ * 正确判据 = **两边都归一成物理路径再比**(`realpathSync`);比不了(文件不存在 / 权限 / 已删)⇒ `false`
24
+ * ("不确定"必须落到"不执行主逻辑",绝不落到"猜着执行")。
25
+ *
26
+ * ## 二、唯一实现(⛔ 不要在每个脚本里各写一份)
27
+ * 入口守卫是**判据**:四份实现就是四种行为,而它们的差异只在软链下才暴露(最难复现的那类)。
28
+ * 故抽到本文件,所有 `scripts/*.mjs` 的 CLI 一律 `import { isDirectRun } from './lib/is-direct-run.mjs'`。
29
+ * 回归:`scripts/tests/is-direct-run.test.mjs`(纯夹具:mkdtemp 里造软链,证明"修复前静默、修复后执行")。
30
+ *
31
+ * 用法:
32
+ * ```js
33
+ * import { isDirectRun } from './lib/is-direct-run.mjs' // scripts/lib 下则 './is-direct-run.mjs'
34
+ * if (isDirectRun(import.meta.url)) main() // ⚠️ 必须显式传 import.meta.url(见下)
35
+ * ```
36
+ *
37
+ * ⚠️ **第一个参数是必填的**:缺省值只能是"本 helper 自己的 URL",那会让判据**静默变错**
38
+ * (`if (isDirectRun()) main()` ⇒ 拿 helper 的路径去比调用方的 `argv[1]` ⇒ 恒 false ⇒ CLI 又不跑了)。
39
+ * 2026-10-04 实测踩到过一次,故缺参**直接抛错**(响亮,绝不静默)。
40
+ *
41
+ * @param {string} importMetaUrl 调用方的 `import.meta.url`(**必填**)
42
+ * @param {string} [argv1] 调用方的 `process.argv[1]`(缺省 `process.argv[1]`)
43
+ * @returns {boolean} 真的是"直接执行"才 `true`
44
+ */
45
+ import { realpathSync } from 'node:fs'
46
+ import { fileURLToPath } from 'node:url'
47
+
48
+ export function isDirectRun(importMetaUrl, argv1 = process.argv[1]) {
49
+ if (typeof importMetaUrl !== 'string' || !importMetaUrl) {
50
+ throw new TypeError('isDirectRun(importMetaUrl) 必须显式传 import.meta.url —— '
51
+ + '缺参会退化成"拿 helper 自己的 URL 比调用方的 argv[1]",判据静默变错(入口守卫恒 false)')
52
+ }
53
+ if (!argv1) return false
54
+ try {
55
+ return realpathSync(argv1) === realpathSync(fileURLToPath(importMetaUrl))
56
+ } catch {
57
+ return false // 归一失败(不存在/无权限/已删)⇒ 不执行主逻辑(fail-closed)
58
+ }
59
+ }
60
+
61
+ export default isDirectRun
@@ -0,0 +1,241 @@
1
+ /**
2
+ * cordis profile 组合编辑(**唯一真源**,纯字符串函数、可离线测试)。
3
+ *
4
+ * profile 组合是"多来源手工拼装"的(bundles + cordis.patch.yml + dependencies +
5
+ * node_modules),而 `cordis.patch.yml` 是**会把服务写崩**的地方:
6
+ * 重复挂载 ⇒ `duplicate loader entry id` ⇒ dsh web 启动即失败(KeepAlive 下 crash loop)。
7
+ * 所以这段编辑逻辑必须能被离线夹具反复验证,不能只活在升级器的内联代码里。
8
+ *
9
+ * 三条规则(2026-09-28 客户机 P3 实测逐条踩过):
10
+ * ① **bundle 层插件**(如 argent-skin):patch 行一律摘掉 —— 它由 `dsh.profile.bundles`
11
+ * 加载,patch 层再来一行就是重复挂载。⚠️ 这条必须**先于**「在目标组合里就保留」判断,
12
+ * 否则目标组合包含它时会被反复补回 patch 层(升级器补、doctor 删 ⇒ 乒乓,实测)。
13
+ * ② **退役行 / 悬挂行**(源码已删、包也没装):摘掉,否则启动时
14
+ * `failed to apply loader entry`(doctor 报 row-pkg-missing)。
15
+ * ③ **缺失的目标插件**:补一行(新装机器需要)。
16
+ *
17
+ * 幂等:把输出再喂回来必须一字不变。
18
+ *
19
+ * ── 2026-10-12 P1-3 追加:`- insert:` 块的**唯一渲染器**与**空块清理**(审计 §C.1 #1)─────────
20
+ * `- insert:` 后面一个子项都没有时,它在 YAML 里就是 `insert: null` —— 0.2 宿主每次启动都报一条
21
+ * `patch: id is required for non-insert patches`(未来 rc 可能升级成硬错)。此前"渲染"这件事在
22
+ * 仓库里有**两份**(本文件的 `composeNew` 内联 + `install-argent.mjs#patchSelfcheck` 的局部
23
+ * `insertBlock`),两份都**不考虑空数组** ⇒ 真实 profile 里留下了空块(本机实测:`web` 1 条 /
24
+ * `rows-trial` 1 条 / `headless` 0 / `tui` 0;⚠️ 审计原记的 `5/4/1/0` 是 `- insert:` 的**总条数**,
25
+ * 不是空块数)。
26
+ * 现在渲染收口到 {@link renderInsertBlock}(空 ⇒ 空串),清理收口到
27
+ * {@link stripEmptyInsertBlocks}(只删"真空白"的块,⛔ 不碰有子项的)。
28
+ */
29
+
30
+ /**
31
+ * 把「目标组合」条目归一到 `{id, name}`(**唯一归一化实现**)。
32
+ *
33
+ * 为什么目标可以是两种形态(W9,2026-10-12):插件发 npm 后,组合条目的
34
+ * **id**(`argent-core`)与 **name**(`@whyshu/argent-core`)不再是同一个串
35
+ * (口径见 `scripts/lib/plugin-packages.mjs`)。老调用方/夹具传纯字符串 ⇒
36
+ * id 与 name 同值(逐字保留老语义,⛔ 不"善意补 scope")。
37
+ * @param {Iterable<string|{id:string,name?:string}>} items
38
+ * @returns {Array<{id: string, name: string}>}
39
+ */
40
+ export function normalizeTargetRows(items) {
41
+ const out = []
42
+ for (const it of items ?? []) {
43
+ if (it === null || it === undefined) continue
44
+ if (typeof it === 'string') { out.push({ id: it, name: it }); continue }
45
+ const id = String(it.id ?? '')
46
+ if (!id) continue
47
+ out.push({ id, name: String(it.name ?? id) })
48
+ }
49
+ return out
50
+ }
51
+
52
+ /**
53
+ * @param {object} o
54
+ * @param {string} o.text 现有 cordis.patch.yml 内容
55
+ * @param {Array<string|{id:string,name?:string}>} o.target 目标组合(patch 层应存在的插件)
56
+ * @param {Iterable<string>} [o.bundleLayer] 由 bundles 加载、不该出现在 patch 层的插件
57
+ * @param {Iterable<string>} [o.retired] 已退役插件(patch 行要摘掉)
58
+ * @param {(plugin: string) => boolean} [o.dangling] 悬挂判定(包与源码都不存在)
59
+ * @param {string} [o.today] 追加段落用的日期(测试注入)
60
+ * @returns {{ text: string, changed: boolean, dropped: string[], migrated: string[], added: string[], renamed: Array<{id:string,from:string,to:string}> }}
61
+ */
62
+ export function composeFix({ text, target, bundleLayer = [], retired = [], dangling = () => false, today = null }) {
63
+ const rows = normalizeTargetRows(target)
64
+ const canonicalName = new Map(rows.map((r) => [r.id, r.name]))
65
+ const targetIds = rows.map((r) => r.id)
66
+ const inTarget = new Set(targetIds)
67
+ const inBundleLayer = new Set(bundleLayer)
68
+ const isRetired = retired instanceof Set ? retired : new Set(retired)
69
+ const lines = String(text ?? '').split('\n')
70
+ const kept = []
71
+ const seen = new Set()
72
+ const dropped = []
73
+ const migrated = []
74
+ const renamed = []
75
+
76
+ let i = 0
77
+ while (i < lines.length) {
78
+ const m = /^(\s*)- id: (argent-[\w-]+)\s*$/.exec(lines[i])
79
+ if (!m) { kept.push(lines[i]); i++; continue }
80
+ const plugin = m[2]
81
+ let j = i + 1
82
+ let nameLine = -1
83
+ if (j < lines.length && /^\s+name:/.test(lines[j])) { nameLine = j; j++ } // 吃掉紧随的 name 行
84
+ if (inBundleLayer.has(plugin)) {
85
+ migrated.push(plugin) // 规则①:bundle 层加载 ⇒ patch 行必摘
86
+ } else if (inTarget.has(plugin)) {
87
+ kept.push(lines[i])
88
+ // 规则④(W9):目标行的 `name:` 归一到**规范包名**(存量裸名 ⇒ npm 名;幂等)。
89
+ // 这是**迁移**而不是"补一行":id 不动、只改 name ⇒ 组合 id 稳定、依赖图可换名字。
90
+ const want = canonicalName.get(plugin)
91
+ const hasOwn = want !== undefined
92
+ if (nameLine >= 0) {
93
+ const cur = (/^\s+name:\s*['"]?([^'"]*)['"]?\s*$/.exec(lines[nameLine]) || [])[1]
94
+ if (hasOwn && cur !== want) {
95
+ const indent = lines[nameLine].match(/^\s*/)[0]
96
+ kept.push(`${indent}name: '${want}'`)
97
+ renamed.push({ id: plugin, from: cur ?? '', to: want })
98
+ } else {
99
+ kept.push(lines[nameLine])
100
+ }
101
+ } else if (hasOwn) {
102
+ const indent = lines[i].match(/^\s*/)[0]
103
+ kept.push(`${indent} name: '${want}'`)
104
+ renamed.push({ id: plugin, from: '(缺失)', to: want })
105
+ }
106
+ seen.add(plugin)
107
+ } else if (isRetired.has(plugin) || dangling(plugin)) {
108
+ dropped.push(plugin) // 规则②
109
+ } else {
110
+ for (let k = i; k < j; k++) kept.push(lines[k]) // 未知/非目标:保留(不越权)
111
+ }
112
+ i = j
113
+ }
114
+
115
+ // 规则③:缺的目标插件补上;已在 bundle 层的**不补**(否则又变重复挂载)
116
+ const missing = targetIds.filter((p) => !seen.has(p) && !inBundleLayer.has(p))
117
+ if (missing.length) {
118
+ const day = today || new Date().toISOString().slice(0, 10)
119
+ kept.push('')
120
+ kept.push(`# ── v0.9 完整升级自动补齐(${day})──`)
121
+ kept.push(renderInsertBlock(missing.map((id) => ({ id, name: canonicalName.get(id) ?? id })))) // 唯一渲染器(missing 非空 ⇒ 必产出块)
122
+ }
123
+
124
+ const changed = dropped.length > 0 || migrated.length > 0 || missing.length > 0 || renamed.length > 0
125
+ if (!changed) {
126
+ // 修复侧(2026-10-12 P1-3):本轮**没有**编辑,但文件里可能**已经**有空 `- insert:`
127
+ // (历史安装器/升级器留下的)⇒ 顺手清掉。清不掉任何东西时**逐字回原文**(⛔ 不做无谓重写,
128
+ // 否则"没改动也重写"会破坏调用方的 mtime/幂等判据)。
129
+ const r = stripEmptyInsertBlocks(String(text ?? ''))
130
+ return { text: r.removed ? r.text : String(text ?? ''), changed: r.removed > 0, dropped, migrated, added: missing, removedEmptyInserts: r.removed, renamed }
131
+ }
132
+ const out = kept.join('\n').replace(/\n{3,}/g, '\n\n').trimEnd() + '\n'
133
+ // ⚠️ **编辑本身可能把某个 `- insert:` 的最后一个子项摘掉**(实测 2026-10-12:bundle 层迁移
134
+ // 摘掉 `argent-skin` 之后,那个 `- insert:` 就成了空块 ⇒ 0.2 宿主启动告警)。
135
+ // ⇒ 写回前一律过一遍清理器(幂等;没有空块时**逐字节不回归**)。
136
+ const stripped = stripEmptyInsertBlocks(out)
137
+ return { text: stripped.text, changed: true, dropped, migrated, added: missing, removedEmptyInserts: stripped.removed, renamed }
138
+ }
139
+
140
+ /**
141
+ * 把 patch 里**已知插件行**的 `name:` 归一到规范包名(W9 迁移;**幂等**)。
142
+ *
143
+ * 与 {@link composeFix} 的分工:`composeFix` 管"目标组合该有哪些行",本函数只管
144
+ * "已存在的行名字对不对" —— 安装器(`patchSelfcheck`)只做后者,⛔ 不引入第二份组合判据。
145
+ * 与 `composeFix` 的规则④**同一语义**(id 不动、只改 name),实现各自独立是因为两者的
146
+ * 输入不同(前者是"目标表 + 保留/摘除判据",后者是"所有已知 id 一律归一")。
147
+ *
148
+ * `nameOf(id)` 返回 `undefined`/`''` ⇒ **该行原样保留**(未知插件不越权)。
149
+ * @param {{text: string, nameOf: (id: string) => (string|undefined)}} o
150
+ * @returns {{text: string, renamed: Array<{id:string,from:string,to:string}>, changed: boolean}}
151
+ */
152
+ export function migratePluginRowNames({ text, nameOf }) {
153
+ const lines = String(text ?? '').split('\n')
154
+ const out = []
155
+ const renamed = []
156
+ let i = 0
157
+ while (i < lines.length) {
158
+ const m = /^(\s*)- id: (argent-[\w-]+)\s*$/.exec(lines[i])
159
+ if (!m) { out.push(lines[i]); i += 1; continue }
160
+ const id = m[2]
161
+ const indent = m[1]
162
+ out.push(lines[i])
163
+ i += 1
164
+ let want = ''
165
+ try { want = String(nameOf(id) ?? '') } catch { want = '' }
166
+ if (i < lines.length && /^\s+name:/.test(lines[i])) {
167
+ const cur = (/^\s+name:\s*['"]?([^'"]*)['"]?\s*$/.exec(lines[i]) || [])[1] ?? ''
168
+ if (want && cur !== want) {
169
+ out.push(`${lines[i].match(/^\s*/)[0]}name: '${want}'`)
170
+ renamed.push({ id, from: cur, to: want })
171
+ } else {
172
+ out.push(lines[i])
173
+ }
174
+ i += 1
175
+ continue
176
+ }
177
+ if (want) { out.push(`${indent} name: '${want}'`); renamed.push({ id, from: '(缺失)', to: want }) }
178
+ }
179
+ return { text: out.join('\n'), renamed, changed: renamed.length > 0 }
180
+ }
181
+
182
+ /**
183
+ * 渲染一个 `- insert:` 块(**唯一实现**;`composeNew` 与 `install-argent.mjs` 都用它)。
184
+ *
185
+ * ⚠️ **空数组 ⇒ 返回空串**(2026-10-12 P1-3,审计 §C.1 #1):一个子项都没有的 `- insert:`
186
+ * 在 YAML 里是 `insert: null`,0.2 宿主每次启动报一条
187
+ * `patch: id is required for non-insert patches`。⇒ 调用方据此**不要**把空块写进文件
188
+ * (`if (!block) …`),而不是"渲染出来再想办法擦掉"。
189
+ *
190
+ * @param {Iterable<string|{id:string,name?:string}>} items 要 insert 的条目
191
+ * (纯字符串 ⇒ id 与 name 同值,老语义逐字保留;`{id,name}` ⇒ npm 名与组合 id 分开)
192
+ * @returns {string} `"- insert:\n - id: X\n name: 'X'…"`;`items` 为空 ⇒ `''`
193
+ */
194
+ export function renderInsertBlock(items) {
195
+ const rows = normalizeTargetRows(items)
196
+ if (!rows.length) return ''
197
+ return `- insert:\n${rows.map((r) => ` - id: ${r.id}\n name: '${r.name}'`).join('\n')}`
198
+ }
199
+
200
+
201
+ /**
202
+ * 清理**空** `- insert:` 条目(修复侧;审计 §C.1 #1)。
203
+ *
204
+ * 判据(刻意保守 —— 只删"真空白"的块,⛔ **绝不**碰有子项的 insert):
205
+ * 「某行是 `- insert:`(行尾无值)」且「它之后**第一个非空行**不存在(EOF),或缩进 **不比自己深**」。
206
+ * · `- insert:` + 空行 + 下一条目 / EOF ⇒ **删**(本机 `web` / `rows-trial` 各 1 条就是这两种形态);
207
+ * · `- insert:` + 更深缩进的子项(含注释)⇒ **留**(哪怕子项只有注释 —— 宁可不动)。
208
+ *
209
+ * 幂等:产物再喂进来必须一字不变(第二次跑 `removed === 0`、文本逐字节相同)。
210
+ * 顺带把 3 个以上连续换行压成 1 个空行(删行留下的空行),同样幂等。
211
+ *
212
+ * @param {string} text
213
+ * @returns {{text: string, removed: number, removedLines: number[]}} `removedLines` = 1-based 行号
214
+ */
215
+ export function stripEmptyInsertBlocks(text) {
216
+ const lines = String(text ?? '').split('\n')
217
+ const kept = []
218
+ const removedLines = []
219
+ for (let i = 0; i < lines.length; i += 1) {
220
+ const m = /^(\s*)-\s*insert:\s*$/.exec(lines[i])
221
+ if (m) {
222
+ const indent = m[1].length
223
+ let j = i + 1
224
+ while (j < lines.length && lines[j].trim() === '') j += 1 // 跳过空行
225
+ const next = j < lines.length ? lines[j] : undefined
226
+ const nextIndent = next === undefined ? -1 : next.match(/^\s*/)[0].length
227
+ const hasChild = next !== undefined && nextIndent > indent
228
+ if (!hasChild) { removedLines.push(i + 1); continue }
229
+ }
230
+ kept.push(lines[i])
231
+ }
232
+ return { text: kept.join('\n').replace(/\n{3,}/g, '\n\n'), removed: removedLines.length, removedLines }
233
+ }
234
+
235
+ /** 生成全新的组合文件内容(首次安装/文件缺失时)。⛔ 目标为空时**不出**空 `- insert:`。 */
236
+ export function composeNew({ target, type, bundleLayer = [] }) {
237
+ const rows = normalizeTargetRows(target).filter((r) => !bundleLayer.includes(r.id))
238
+ const block = renderInsertBlock(rows)
239
+ const head = `# Argent on DSH(v0.9 完整升级生成的目标组合:${type})\n`
240
+ return block ? `${head}${block}\n` : head
241
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * 补丁目标路径重定位(**唯一真源**)。
3
+ *
4
+ * 事故(2026-09-28 客户机 P3):`scripts/patches/*.patch` 是在**开发机**打的,
5
+ * 头里写死了 `/opt/homebrew/lib/node_modules/@deepseek-ai/...`;客户机 dsh 装在
6
+ * `~/node/lib/node_modules/` ⇒ 升级器 `existsSync(headerPath)` 恒 false ⇒
7
+ * 三个 hotfix(含会话毒化加固)**静默跳过**,只打印一行 warn,人不在现场根本看不见。
8
+ * 现场绕法是 sed 改补丁头,但那会让工作区变脏(升级器预检直接拒绝跑)。
9
+ *
10
+ * 正解:头里 `@deepseek-ai/` 之后的部分是**包内相对路径**,与机器无关;
11
+ * 拼到当前机器的 scope 上即可。本函数据此重定位,并对"头里不是 scope 内文件"
12
+ * 与"本机确实缺该文件"两种情况分别回落,避免误伤。
13
+ */
14
+ import { existsSync } from 'node:fs'
15
+ import { join } from 'node:path'
16
+
17
+ const MARKER = '@deepseek-ai/'
18
+
19
+ /**
20
+ * @param {string} header 补丁头里的路径(`+++ ` 行)
21
+ * @param {string} scopeDir 当前机器的 `@deepseek-ai` 目录
22
+ * @param {(p: string) => boolean} exists 存在性判定(测试可注入)
23
+ * @returns {string} 实际应当打补丁的目标文件路径;无法解析时返回 ""
24
+ */
25
+ export function relocatePatchTarget(header, scopeDir, exists = existsSync) {
26
+ const h = (header || '').trim()
27
+ if (!h) return ''
28
+ const idx = h.lastIndexOf(MARKER)
29
+ if (idx === -1) return h // 非 scope 内文件(如 /tmp/xxx.orig 之类)⇒ 原样交给调用方判断
30
+ const candidate = join(scopeDir, h.slice(idx + MARKER.length))
31
+ if (exists(candidate)) return candidate
32
+ // 回落:头里那条路径在本机**恰好**存在(例如仍在开发机上跑)⇒ 直接用原路径。
33
+ if (exists(h)) return h
34
+ return candidate // 两条都不存在:返回重定位结果,让调用方按"上游已改动"报错
35
+ }