@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,500 @@
1
+ /**
2
+ * dsh 全局安装位置解析(**唯一真源**)。
3
+ *
4
+ * 由来(2026-09-28 客户机实测):本机(开发机)dsh 走 homebrew,
5
+ * `SCOPE_DIR = /opt/homebrew/lib/node_modules/@deepseek-ai`;客户机是 node 官方包,
6
+ * `~/node/lib/node_modules/@deepseek-ai`。历史上有 4 个脚本各自写死 homebrew 路径,
7
+ * 到客户机就"文件不存在/门禁假红"。这里收敛成一处,取值优先级:
8
+ * --scope-dir/--global-bin → 环境变量 DSH_SCOPE/DSH_BIN → npm root -g 自动探测 → 候选兜底。
9
+ *
10
+ * ⚠️ 本文件必须**零第三方依赖**(升级链路上的脚本要在半坏的机器上也能跑);
11
+ * 唯一的本地 import 是 `./dsh-host-resolve.mjs`(同为引擎面文件、同样零第三方依赖),
12
+ * 只为复用"npx 缓存目录在哪"这一个判据(⛔ 不再写第二份)。
13
+ *
14
+ * ── 2026-10-12 P1-3(审计 §C.1 #7)追加两件事 ────────────────────────────────────
15
+ * ① `scopeBinCandidates()` —— 由目标 scope 推导 `dsh` 可执行入口的候选序列(**唯一实现**)。
16
+ * P1-0 曾把其中的「平铺档 `<dir>/node_modules/.bin/dsh`」**暂存**在
17
+ * `scripts/lib/bootcheck-wiring.mjs#bootcheckBinCandidates()`(因为本文件在能力包的
18
+ * `pack.engine` 清单里:改一个字节就要给三个能力包提版 + 重盖章)。P1-3 本就是"要提版"
19
+ * 的那一批 ⇒ **上移到真源**,bootcheck 那边的副本已删除(两处不再各自实现)。
20
+ * ② `resolveDshScopeDir()` —— **找得到 / 找不到都要说清楚**的 scope 定位器,
21
+ * 给 `install-argent.mjs#scopeDirForPreset()` 用。它覆盖三种真实布局:
22
+ * · 标准 node 布局 `<prefix>/lib/node_modules/@deepseek-ai`(开发机 homebrew / 官方 pkg);
23
+ * · **邻居布局** `<prefix>/node_modules/@deepseek-ai`(客户机 `~/.local/node/node_modules` ——
24
+ * 这正是 `npm root -g` **探错**的那个陷阱:npm 报 `<prefix>/lib/node_modules`);
25
+ * · **平铺 / npx 布局** `<dir>/node_modules/@deepseek-ai`(预检树、`npx` 的
26
+ * `~/.npm/_npx/<hash>/node_modules/@deepseek-ai`)。
27
+ * ⛔ 探不到就**返回 null**(由调用方响亮失败),⛔ **不**回一个"猜的" `npm root -g` 路径。
28
+ *
29
+ * ── 2026-10-14 F1 修复批(MBP 真机实测)─────────────────────────────────────────
30
+ * ③ `resolveDshScope()`(**遗留定位器**)**同一个病**:末尾 `cands.find(exists) || cands[last]`
31
+ * ⇒ ① **不感知 npx**(`npx @deepseek-ai/dsh@0.2.0-rc.2 web` 的模块树在
32
+ * `~/.npm/_npx/<hash>/node_modules/@deepseek-ai`,一条候选都不命中);② 探不到时回一个**可能不存在**的
33
+ * 路径 ⇒ 调用方**无法区分"没有"与"探错"**。实测后果:0.6.0 的"native 定时"在 npx 宿主上恒判 `unknown`。
34
+ * **本批修法(三条,逐字对应缺陷)**:
35
+ * a. 候选链**新增 npx 形态**('~/.npm/_npx/<hash>/node_modules/@deepseek-ai',复用
36
+ * `dsh-host-resolve.mjs#npxCacheDirs()/scanNpxCache()/pickNpxHost()` —— 判据只有一份);
37
+ * b. 命中的判据是「**该 scope 里真的有 dsh 包**」(默认 `dsh-base`/`dsh-session`,
38
+ * 可用 `probePackages` 收窄;⛔ 不是"目录存在"就算 —— 空壳目录/别人的 scope 不算);
39
+ * c. **探不到 ⇒ 返回 `null`**(显式"未知")+ 路径在但内容不合法 ⇒ `{scope, source, stale:true}`;
40
+ * 返回值显式带 `source: 'profile'|'npx'|'global'` 与 `how`(选择理由,可诊断)。
41
+ * ⚠️ **兼容性**:老调用方拿到对象时 `scope` 字段就是过去那个字符串(语义不变),
42
+ * 只是**新增**了 `source`/`how`/`stale`;而"探不到不再是假路径"是**有意的破坏性修复** ——
43
+ * 所有调用点都必须显式处理 `null`(清单与判据见 `scripts/tests/dsh-scope-npx.test.mjs`)。
44
+ */
45
+ import { execSync } from 'node:child_process'
46
+ import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs'
47
+ import { homedir } from 'node:os'
48
+ import { dirname, join } from 'node:path'
49
+ import { npxCacheDirs, pickNpxHost, scanNpxCache } from './dsh-host-resolve.mjs'
50
+
51
+ /** 取 `--name value` 形式的命令行参数值(缺省 null)。 */
52
+ export function argValue(argv, name) {
53
+ const i = argv.indexOf(name)
54
+ return i > -1 ? argv[i + 1] ?? null : null
55
+ }
56
+
57
+ /** `npm root -g` 的默认探测(注入用 ⇒ 测试零环境依赖;npm 不在 PATH 时回 `''`)。 */
58
+ export function defaultNpmRootG() {
59
+ try {
60
+ return execSync('npm root -g', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim()
61
+ } catch { return '' }
62
+ }
63
+
64
+ /** PATH 上的 `dsh`(默认探测;注入用 ⇒ 测试零环境依赖)。 */
65
+ export function defaultPathProbe() {
66
+ try {
67
+ return execSync('command -v dsh', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim()
68
+ } catch { return '' }
69
+ }
70
+
71
+ /** 候选 scope 目录(**顺序即优先级**;最后一项是兜底,允许不存在)。
72
+ * ⚠️ 0.6.0 F1 修复批:末尾补上 **npx 缓存**那一档(`npx @deepseek-ai/dsh@… web` 的树在
73
+ * `~/.npm/_npx/<hash>/node_modules/@deepseek-ai`)—— 否则"只需要一份候选清单"的调用方
74
+ * (`dsh-api-client` 回落到宿主树的 `ws` 包)在 npx 机器上永远回落到不存在的路径。
75
+ * ⛔ 本函数**只产候选**(不判内容合法性、不保证存在):判据在 `resolveDshScope()`/`hasDshPackage()`。 */
76
+ export function scopeCandidates(env = process.env, { home = '', readdir = readdirSync, npxDirs = null } = {}) {
77
+ const cands = []
78
+ const root = defaultNpmRootG()
79
+ if (root) cands.push(join(root, '@deepseek-ai'))
80
+ cands.push(
81
+ '/opt/homebrew/lib/node_modules/@deepseek-ai', // 开发机 homebrew
82
+ join(env.HOME || homedir(), 'node', 'lib', 'node_modules', '@deepseek-ai'), // 客户机 node 官方包
83
+ '/usr/local/lib/node_modules/@deepseek-ai', // Intel mac / 官方 pkg
84
+ )
85
+ for (const cache of (npxDirs || npxCacheDirs({ home: home || env.HOME || homedir(), env }))) {
86
+ let hashes = []
87
+ try { hashes = readdir(cache) } catch { continue }
88
+ for (const h of [...hashes].sort()) {
89
+ if (h.startsWith('.')) continue
90
+ cands.push(join(cache, h, 'node_modules', '@deepseek-ai'))
91
+ }
92
+ }
93
+ return cands
94
+ }
95
+
96
+ /**
97
+ * 解析 dsh scope 目录(**遗留定位器**;0.6.0 F1 修复后 = npx 感知 + 未知即 `null`)。
98
+ *
99
+ * @returns {{scope: string, source: 'profile'|'npx'|'global', how: string, stale?: true} | null}
100
+ * · 命中 ⇒ 对象;`scope` 字段就是**过去那个字符串**(老调用方语义不变),
101
+ * `source` = 命中来自哪一档,`how` = **选择理由**(人可读,可诊断"为什么选了这棵");
102
+ * · 路径都在、但**没有一棵是合法的 dsh scope** ⇒ `{scope, source, how, stale: true}`
103
+ * ("有目录没内容" —— 例如 `file:` 依赖失效后留空壳的 profile 树);
104
+ * · **一个都没有 ⇒ `null`**(显式"未知"),⛔ **绝不**回一个 passthrough 的假路径
105
+ * (那正是 MBP 上 native 定时恒判 unknown 的成因)。
106
+ */
107
+ export function resolveDshScope({
108
+ argv = process.argv, env = process.env, home = '',
109
+ exists = existsSync, readdir = readdirSync, read = defaultReadText, stat = defaultStat,
110
+ npmRootG = defaultNpmRootG, pathProbe = defaultPathProbe,
111
+ npxDirs = null, probePackages = DEFAULT_PROBE_PACKAGES, hostVersion = '',
112
+ } = {}) {
113
+ const H = home || env.HOME || homedir()
114
+ const npxRoots = npxDirs || npxCacheDirs({ home: H, env })
115
+ const probe = (dir) => hasDshPackage(dir, { exists, read, packages: probePackages })
116
+
117
+ // ① 显式 `--scope-dir` / `DSH_SCOPE`:说一不二 —— **原样采用**,⛔ 不回落到别的树。
118
+ // ⚠️ 但要**如实标注内容合法性**(`stale`):调用方若走 `requireDshScope()`,指了空壳/不存在的树
119
+ // 会**响亮失败**(点了名的路径出错 = 拼错/树被移走;⛔ 静默换一棵才是灾难)。
120
+ const explicit = argValue(argv, '--scope-dir') || env.DSH_SCOPE
121
+ if (explicit) {
122
+ return {
123
+ scope: explicit,
124
+ source: scopeSourceOf(explicit, npxRoots),
125
+ how: '`--scope-dir` / `DSH_SCOPE`(显式指定)',
126
+ ...(probe(explicit) ? {} : { stale: true }),
127
+ }
128
+ }
129
+
130
+ // ①′ 显式 `--global-bin` / `DSH_BIN`:**也是显式指向**("我告诉了你哪个 dsh")⇒ 只认它反推出的树,
131
+ // 反推不出存在的 scope 就回 `null`(⛔ 不偷偷改用自动探测的另一棵树 —— 那会让"你指的那个 dsh"
132
+ // 与"我们判机制用的 scope"不是同一棵,正是本函数要消灭的那类静默错)。
133
+ const binHint = argValue(argv, '--global-bin') || env.DSH_BIN
134
+ if (binHint) {
135
+ for (const d of scopeCandidatesFromBin(binHint)) {
136
+ if (exists(d) && probe(d)) {
137
+ return { scope: d, source: scopeSourceOf(d, npxRoots), how: `由 \`--global-bin\` / \`DSH_BIN\` 反推(${binHint})` }
138
+ }
139
+ }
140
+ return null
141
+ }
142
+
143
+ // ② 自动链(顺序 = `autoScopeCandidates()`):第一棵**真的有 dsh 包**的胜出。
144
+ const chain = autoScopeCandidates({ env, home: H, exists, readdir, read, npmRootG, pathProbe, npxDirs: npxRoots })
145
+ const dirs = new Map() // dir → 链上的第一处来由(去重,保留最先出现的档)
146
+ for (const c of chain) if (!dirs.has(c.dir)) dirs.set(c.dir, c)
147
+ let firstExisting = null
148
+ for (const [dir, c] of dirs) {
149
+ if (!exists(dir)) continue
150
+ if (firstExisting === null) firstExisting = { dir, from: c.from }
151
+ if (!probe(dir)) continue
152
+ const hit = { scope: dir, source: c.from.startsWith('npx 缓存') ? 'npx' : scopeSourceOf(dir, npxRoots), how: c.from }
153
+ // npx 档:多个候选命中时按 `dsh-host-resolve.mjs#pickNpxHost()` 的**既有唯一规则**择优
154
+ //(最近使用优先 mtime,同刻路径升序;⛔ 不按版本号挑 —— 判据不再写第二份)
155
+ if (hit.source === 'npx') {
156
+ const picked = pickNpxScope({ dirs: [...dirs.keys()], probe, env, home: H, exists, readdir, read, stat, hostVersion: hostVersion || c.version, npxDirs: npxRoots })
157
+ if (picked) { hit.scope = picked.scope; hit.how = picked.how }
158
+ }
159
+ return hit
160
+ }
161
+ // ③ 一个合法的都没有:有目录 ⇒ stale("有壳没内容");连目录都没有 ⇒ null(未知)
162
+ if (firstExisting) {
163
+ return {
164
+ scope: firstExisting.dir, source: scopeSourceOf(firstExisting.dir, npxRoots), stale: true,
165
+ how: `${firstExisting.from} —— 目录在,但其中**没有** dsh 包(${probePackages.map((p) => `@deepseek-ai/${p}`).join(' / ')})`,
166
+ }
167
+ }
168
+ return null
169
+ }
170
+
171
+ /** 默认探测用的 dsh 包名(**存在性 = "这棵树真的是 dsh 宿主树"** 的最小判据)。 */
172
+ export const DEFAULT_PROBE_PACKAGES = ['dsh-base', 'dsh-session']
173
+
174
+ /**
175
+ * **调用方纪律**(F1 修复批,2026-10-14):要干活就必须有 scope —— 拿不到 ⇒ **响亮失败**。
176
+ *
177
+ * 统一文案(⛔ 别在 8 个调用点各写一份):点名"探不到宿主 scope(可能是 npx 形态)"、
178
+ * 列出**试过的档位**、给出**下一步命令**。`resolveDshScope()` 返回 `null`(未知)或
179
+ * `stale:true`(有壳没内容)都会抛这一条。
180
+ *
181
+ * @param {{what?: string, opts?: object}} [o] `what` = 调用方要干什么(进首行,便于排障)
182
+ * @returns {string} scope 目录(保证非空)
183
+ * @throws {Error} 探不到 / 探到空壳时
184
+ */
185
+ export function requireDshScope({ what = '本脚本', opts = {} } = {}) {
186
+ const hit = resolveDshScope(opts)
187
+ if (hit && !hit.stale) return hit.scope
188
+ const why = hit && hit.stale
189
+ ? `探到的那棵是**空壳**(${hit.scope}:目录在、里面没有 dsh 包)`
190
+ : '候选作用域**一棵都不存在**'
191
+ throw new Error(
192
+ `${what}:⛔ 探不到宿主 scope(${why})—— 可能是 **npx 形态**\n` +
193
+ ' 已按序试过:`--scope-dir`/`DSH_SCOPE` → `--global-bin`/`DSH_BIN` 反推 → `npm root -g` → PATH 上的 dsh →\n' +
194
+ ' HOME 相对布局(邻居 → 标准)→ 当前 node 前缀 → **npx 缓存(~/.npm/_npx/<hash>/…)** → homebrew / Intel 绝对兜底。\n' +
195
+ ' ⛔ 拿一个**可能不存在**的路径继续干活 = 假绿(旧实现 `resolveDshScope()` 就是回 passthrough 假路径)。\n' +
196
+ ' 下一步(三选一):① `--scope-dir <…/@deepseek-ai>`;② `DSH_SCOPE=<…>`;③ `--global-bin <该 scope 自带的 dsh>`。\n' +
197
+ ' npx 形态参考:`ls -d ~/.npm/_npx/*/node_modules/@deepseek-ai`(取**含** `dsh-base`/`dsh-session` 的那个)。')
198
+ }
199
+
200
+ /**
201
+ * **要 import 宿主内部文件**的调用方的守卫(同上,但口子在"文件在不在"这一层)。
202
+ *
203
+ * 为什么单独一条:这一类调用点(`patches/verify-*.mjs`)本来就**允许**某个包不在(降级/跳过),
204
+ * 但**不允许**"整棵树都探不到" —— 后者会让 `existsSync(<假路径>)` 恒 false ⇒ 一路"跳过" ⇒
205
+ * **假绿**(门禁报"通过",其实什么都没验)。故:scope 探不到 ⇒ 响亮失败;探到了但文件不在 ⇒
206
+ * 返回 null,由调用方按自己的语义降级(打印原因)。
207
+ *
208
+ * @returns {{scope: string, path: string} | null} `path` 不存在 ⇒ `null`(scope 有效)
209
+ * @throws {Error} scope 探不到 / 探到空壳时(与 {@link requireDshScope} 同一份文案)
210
+ */
211
+ export function requireImportTarget(rel, { what = '本脚本', opts = {}, exists = existsSync } = {}) {
212
+ const scope = requireDshScope({ what, opts })
213
+ const p = `${scope}/${String(rel || '').replace(/^\/+/, '')}`
214
+ return exists(p) ? { scope, path: p } : null
215
+ }
216
+
217
+ /** 读 UTF-8 文本(注入用;默认实现)。 */
218
+ export function defaultReadText(p) { return readFileSync(p, 'utf8') }
219
+
220
+ /** `stat`(注入用;默认实现;失败 ⇒ 抛,由调用方 try 住)。 */
221
+ export function defaultStat(p) { return statSync(p) }
222
+
223
+ /**
224
+ * **这棵 scope 里真的有 dsh 包吗**("命中"的唯一判据;⛔ 不是"目录存在")。
225
+ *
226
+ * 认三种形态(任一命中即算 —— 越靠前越强):
227
+ * ① `<dir>/<pkg>/package.json` 的 `name === '@deepseek-ai/<pkg>'`(**真包**,带版本号);
228
+ * ② `<dir>/dsh/package.json` 的 `name === '@deepseek-ai/dsh'`(宿主主包);
229
+ * ③ `<dir>/<pkg>` 目录存在(包名目录 —— 老树/裁剪树里常见,无 package.json 也算);
230
+ * ④ `<dir>` 目录里**存在任一 `dsh-*` 包目录**(最宽的兜底:宿主树里必然有一堆 `dsh-*`)。
231
+ * ⛔ 坏 JSON **不**当"没有"(那会把好树判死)—— JSON 读坏时退到 ③/④ 的目录判据。
232
+ * @returns {boolean}
233
+ */
234
+ export function hasDshPackage(dir, { exists = existsSync, read = defaultReadText, readdir = readdirSync, packages = DEFAULT_PROBE_PACKAGES } = {}) {
235
+ if (!dir) return false
236
+ const names = [...new Set([...(packages || []), 'dsh'])]
237
+ for (const pkg of names) {
238
+ const pkgDir = join(dir, pkg)
239
+ if (!exists(pkgDir)) continue
240
+ const meta = readPkgJson(join(pkgDir, 'package.json'), { exists, read })
241
+ if (meta && meta.name === `@deepseek-ai/${pkg}`) return true // ①/② 真包
242
+ return true // ③ 包名目录在(无/坏 package.json 也算)
243
+ }
244
+ let entries = []
245
+ try { entries = readdir(dir) || [] } catch { return false }
246
+ return entries.some((e) => /^dsh-[a-z0-9]/.test(String(e))) // ④ 有任一 dsh-* 包目录
247
+ }
248
+
249
+ /** 读一个 package.json(不存在/坏 JSON ⇒ null;⛔ 不抛)。 */
250
+ function readPkgJson(p, { exists = existsSync, read = defaultReadText } = {}) {
251
+ if (!exists(p)) return null
252
+ try { return JSON.parse(read(p)) } catch { return null }
253
+ }
254
+
255
+ /** 命中来自哪一档(`profile` / `npx` / `global`)—— **判据只有这一处**。
256
+ * `npxRoots` = 本次探测用到的 npx 缓存根(`…/_npx`);给了就按它精确判定
257
+ * (⛔ 不靠路径里恰好有 `_npx` 字样 —— 夹具/`npm_config_cache` 覆盖会把那个字样换掉)。 */
258
+ export function scopeSourceOf(dir, npxRoots = []) {
259
+ const d = String(dir || '').replace(/\\/g, '/')
260
+ for (const r of npxRoots || []) {
261
+ const root = String(r || '').replace(/\\/g, '/').replace(/\/+$/, '')
262
+ // ⚠️ 必须要求"真的是一条 scope 目录"(`<root>/<hash>/node_modules/@deepseek-ai`)——
263
+ // 否则链上任何以该缓存根开头的路径都会被误标成 npx(实测:当 npx 缓存根退化成 HOME 时,
264
+ // `<HOME>/node/node_modules/@deepseek-ai` 那种**邻居布局**会被错标成 npx)。
265
+ if (root && d.startsWith(`${root}/`) && /\/node_modules\/@deepseek-ai\/?$/.test(d)) return 'npx'
266
+ }
267
+ if (/\/profiles\/[^/]+\/node_modules\/@deepseek-ai\/?$/.test(d)) return 'profile'
268
+ if (/(^|\/)_npx\//.test(d)) return 'npx'
269
+ return 'global'
270
+ }
271
+
272
+ /**
273
+ * npx 档择优(**判据复用 `dsh-host-resolve.mjs`,⛔ 不写第二份**)。
274
+ *
275
+ * 规则 = `scanNpxCache()` 扫全部 npx 候选(只认能读出 `@deepseek-ai/dsh` 的 hash);
276
+ * ①(本批新增的"最匹配宿主版本")能读出与**当前 npx 宿主同版本**的那个优先;
277
+ * ② 其余按 `pickNpxHost()` 的既有规则(mtime 最新 → 同刻路径升序)。
278
+ * 选择理由写进 `how`(含落选者,便于诊断"为什么不是另一个")。
279
+ * @returns {{scope: string, how: string} | null}
280
+ */
281
+ function pickNpxScope({ dirs = [], probe, env, home, exists, readdir, read, stat, hostVersion = '', npxDirs = null } = {}) {
282
+ const allNpxDirs = npxDirs || npxCacheDirs({ home, env })
283
+ const npxDirsOnChain = dirs.filter((d) => scopeSourceOf(d, allNpxDirs) === 'npx' && exists(d) && probe(d))
284
+ if (!npxDirsOnChain.length) return null
285
+ if (npxDirsOnChain.length === 1) {
286
+ return { scope: npxDirsOnChain[0], how: `npx 缓存(${dirname(dirname(npxDirsOnChain[0]))})—— 唯一命中且含 dsh 包` }
287
+ }
288
+ const io = { exists, readdir, read: (p) => read(p), stat: (p) => stat(p) }
289
+ const cands = scanNpxCache(allNpxDirs, io)
290
+ .filter((c) => npxDirsOnChain.includes(dirname(c.pkgDir)))
291
+ const { chosen, considered } = pickNpxHost(cands)
292
+ const versionMatch = hostVersion ? cands.find((c) => c.version === hostVersion) : null
293
+ // ① "最匹配宿主版本" ② `pickNpxHost()` 的既有规则(mtime 最新 → 同刻路径升序)③ 兜底按候选序
294
+ const scope = versionMatch ? dirname(versionMatch.pkgDir)
295
+ : chosen ? dirname(chosen.pkgDir) : npxDirsOnChain[0]
296
+ const why = versionMatch
297
+ ? `与当前宿主同版本(${hostVersion})`
298
+ : '最近使用优先(mtime 最新;npx 每次跑会刷新)'
299
+ return {
300
+ scope,
301
+ how: `npx 缓存里 ${npxDirsOnChain.length} 个候选命中 ⇒ 按「${why}」选 \`${scope}\``
302
+ + `(考虑过:${considered.map((c) => `${c.version}@${c.pkgDir}`).join('、') || '—'})`,
303
+ }
304
+ }
305
+
306
+ /**
307
+ * 由某个 `dsh` 可执行文件路径**反推**它所属的 scope 目录候选(顺序即优先级;可能为空)。
308
+ *
309
+ * 认这几种形态(客户机/预检树/npx 缓存实测):
310
+ * · `…/@deepseek-ai/<pkg>/…` ⇒ 该 `@deepseek-ai` 目录本身
311
+ * · `<dir>/node_modules/.bin/dsh[.cmd]` ⇒ `<dir>/node_modules/@deepseek-ai`(平铺 / npx)
312
+ * · `<prefix>/bin/dsh[.cmd]` ⇒ `<prefix>/lib/node_modules/@deepseek-ai`(标准)
313
+ * 再试 `<prefix>/node_modules/@deepseek-ai`(邻居)
314
+ * @param {string} bin
315
+ * @returns {string[]}
316
+ */
317
+ export function scopeCandidatesFromBin(bin) {
318
+ const b = String(bin || '').replace(/\\/g, '/').replace(/\/+$/, '')
319
+ if (!b) return []
320
+ const out = []
321
+ const push = (p) => { if (p && !out.includes(p)) out.push(p) }
322
+ const inScope = /^(.*\/@deepseek-ai)(?:\/|$)/.exec(b)
323
+ if (inScope) { push(inScope[1]); return out }
324
+ const flat = /^(.*)\/node_modules\/\.bin\/dsh(?:\.(?:cmd|ps1|exe))?$/.exec(b)
325
+ if (flat) { push(`${flat[1]}/node_modules/@deepseek-ai`); return out }
326
+ const inBin = /^(.*)\/bin\/dsh(?:\.(?:cmd|ps1|exe))?$/.exec(b)
327
+ if (inBin) {
328
+ push(`${inBin[1]}/lib/node_modules/@deepseek-ai`) // 标准 node 布局
329
+ push(`${inBin[1]}/node_modules/@deepseek-ai`) // 邻居布局(客户机 `~/.local/node`)
330
+ }
331
+ return out
332
+ }
333
+
334
+ /**
335
+ * 自动探测时**按序**试的 scope 目录(含**出自哪条判据**,便于把结论打给人看)。
336
+ * 顺序刻意是「越贴近"这台机器实际在用的那个"越靠前」:
337
+ * `npm root -g` → PATH 上的 dsh 反推 → HOME 相对(邻居 → 标准)→ 当前 node 前缀 → npx 缓存 → 绝对兜底。
338
+ * ⚠️ `npm root -g` 只是**候选之一**:客户机上它报的是 `<prefix>/lib/node_modules`(`@deepseek-ai` 不在那儿)
339
+ * ⇒ 它会自然落选,而**不会**把结论带偏(这正是旧实现的病根)。
340
+ * @param {{npxDirs?: string[]|null, read?: Function}} [o]
341
+ * `npxDirs` —— 覆盖 npx 缓存目录(缺省 `npxCacheDirs()`;夹具用,⛔ 不改判据本身)。
342
+ * @returns {Array<{dir: string, from: string, version?: string}>}
343
+ */
344
+ export function autoScopeCandidates({ env = process.env, home = '', exists = existsSync, readdir = readdirSync, npmRootG = defaultNpmRootG, pathProbe = defaultPathProbe, npxDirs = null, read = defaultReadText } = {}) {
345
+ const H = home || env.HOME || homedir()
346
+ const out = []
347
+ const push = (dir, from, version = '') => { if (dir) out.push(version ? { dir, from, version } : { dir, from }) }
348
+
349
+ // ① npm 自己说的全局根(开发机 / 官方 pkg;客户机是错的 ⇒ 只会落选)
350
+ const root = npmRootG()
351
+ if (root) push(join(root, '@deepseek-ai'), '`npm root -g`')
352
+
353
+ // ② PATH 上的 dsh ⇒ 反推(最接近"用户实际在用的那个")
354
+ const bin = pathProbe()
355
+ for (const d of scopeCandidatesFromBin(bin)) push(d, `PATH 上的 dsh(${bin})`)
356
+
357
+ // ③ HOME 相对:**邻居布局在前**(客户机 `~/.local/node/node_modules/@deepseek-ai`),标准布局在后
358
+ push(join(H, '.local', 'node', 'node_modules', '@deepseek-ai'), '邻居布局(~/.local/node/node_modules)')
359
+ push(join(H, 'node', 'node_modules', '@deepseek-ai'), '邻居布局(~/node/node_modules)')
360
+ push(join(H, '.local', 'node', 'lib', 'node_modules', '@deepseek-ai'), '标准布局(~/.local/node/lib/node_modules)')
361
+ push(join(H, 'node', 'lib', 'node_modules', '@deepseek-ai'), '标准布局(~/node/lib/node_modules)')
362
+
363
+ // ④ 当前 node 前缀(nvm / 官方 pkg:`<prefix>/lib/node_modules` 与邻居布局都试)
364
+ if (process.execPath) {
365
+ const prefix = dirname(dirname(process.execPath))
366
+ push(join(prefix, 'lib', 'node_modules', '@deepseek-ai'), `当前 node 前缀(${prefix})`)
367
+ push(join(prefix, 'node_modules', '@deepseek-ai'), `当前 node 前缀的邻居布局(${prefix})`)
368
+ }
369
+
370
+ // ⑤ **平铺 / npx 布局**:`npx @deepseek-ai/dsh@<版本> web` 跑过之后的痕迹(MBP 就是这一形态)。
371
+ // 带 `version`(能读出 `@deepseek-ai/dsh` 时)⇒ 多个候选时按"最匹配宿主版本"择优(见 `pickNpxScope()`)。
372
+ for (const cache of (npxDirs || npxCacheDirs({ home: H, env }))) {
373
+ let hashes = []
374
+ try { hashes = readdir(cache) } catch { continue }
375
+ for (const h of [...hashes].sort()) {
376
+ if (h.startsWith('.')) continue
377
+ const tree = join(cache, h)
378
+ const meta = readPkgJson(join(tree, 'node_modules', '@deepseek-ai', 'dsh', 'package.json'), { exists, read })
379
+ push(join(tree, 'node_modules', '@deepseek-ai'), `npx 缓存(${tree})`, meta && meta.version ? String(meta.version).trim() : '')
380
+ }
381
+ }
382
+
383
+ // ⑥ 绝对兜底(开发机 homebrew / Intel mac)
384
+ push('/opt/homebrew/lib/node_modules/@deepseek-ai', '标准布局(homebrew)')
385
+ push('/opt/homebrew/node_modules/@deepseek-ai', '邻居布局(homebrew)')
386
+ push('/usr/local/lib/node_modules/@deepseek-ai', '标准布局(Intel mac / 官方 pkg)')
387
+ return out
388
+ }
389
+
390
+ /**
391
+ * **定位 dsh scope 目录**(bootcheck / 安装器判 preset 机制用)—— 找得到/找不到都**说得清**。
392
+ *
393
+ * ⚠️ 0.6.0 F1 修复批起:**自动链委托给 {@link resolveDshScope}**(单一人选:npx 感知 + 多候选择优),
394
+ * 本函数只做**契约适配**(`{scope,…}` → `{dir, from}`;`stale` 对它不算命中 ⇒ 沿用老口径回 `null`)。
395
+ * ⛔ 不再自己拼候选、⛔ 不再"目录存在就算" —— 两处各自实现过正是旧缺陷的温床。
396
+ *
397
+ * 优先级:`--scope-dir`/`DSH_SCOPE`(**显式**)→ `--global-bin`/`DSH_BIN` 反推 → 自动探测。
398
+ * ⚠️ 显式给了但**不存在** ⇒ 返回 `null`(调用方响亮失败)—— ⛔ **绝不**"静默回退到
399
+ * `npm root -g` 猜一个"(那正是客户机把 preset 机制判错的成因)。
400
+ * ⚠️ `--global-bin`/`DSH_BIN` 同理:它也是**显式**的宿主指向("我告诉了你哪个 dsh"),
401
+ * 反推不出存在的 scope ⇒ 也回 `null`(⛔ 不悄悄改用另一棵树 —— 那会让"你指的那个 dsh"
402
+ * 与"我们判机制用的 scope"**不是同一棵**,正是本函数要消灭的那类静默错)。
403
+ * @returns {{dir: string, from: string} | null}
404
+ */
405
+ export function resolveDshScopeDir({ argv = process.argv, env = process.env, home = '', exists = existsSync, readdir = readdirSync, npmRootG = defaultNpmRootG, pathProbe = defaultPathProbe, npxDirs = null } = {}) {
406
+ // ① 显式 `--scope-dir`/`DSH_SCOPE`:**存在就原样采用**(本函数是"判目标树"的口,
407
+ // ⛔ 不替调用方做 content 合法性判断,也 ⛔ 不换成别的树);不存在 ⇒ `null`(响亮失败)。
408
+ const explicit = argValue(argv, '--scope-dir') || env.DSH_SCOPE
409
+ if (explicit) return exists(explicit) ? { dir: explicit, from: '`--scope-dir` / `DSH_SCOPE`(显式指定)' } : null
410
+ // ② 显式 `--global-bin`/`DSH_BIN`:只认它反推出的树(**存在**即可,同样不要求有 dsh 包 ——
411
+ // "你指的那个 dsh 在哪儿"这个问题与"树里有什么"无关);反推不出 ⇒ `null`。
412
+ const binHint = argValue(argv, '--global-bin') || env.DSH_BIN
413
+ if (binHint) {
414
+ for (const d of scopeCandidatesFromBin(binHint)) {
415
+ if (exists(d)) return { dir: d, from: `由 \`--global-bin\` / \`DSH_BIN\` 反推(${binHint})` }
416
+ }
417
+ return null
418
+ }
419
+ // ③ 自动链:委托真源单一人选(npx 感知 + 多候选择优 + **内容合法性**判据)。
420
+ // 空壳 ⇒ `null`(本函数的调用方要拿它去判机制/装 preset ⇒ 空壳等于"探不到")。
421
+ const hit = resolveDshScope({ argv, env, home, exists, readdir, npmRootG, pathProbe, npxDirs })
422
+ return hit && !hit.stale ? { dir: hit.scope, from: hit.how } : null
423
+ }
424
+
425
+ /**
426
+ * 由**目标 scope** 推导 `dsh` 可执行入口的候选序列(**唯一实现**;顺序即优先级)。
427
+ *
428
+ * | scope 形态 | 对应 bin |
429
+ * |---|---|
430
+ * | 标准 node 布局 `<prefix>/lib/node_modules/@deepseek-ai` | `<prefix>/bin/dsh`(homebrew / 官方 pkg) |
431
+ * | 邻居布局 `<dir>/node_modules/@deepseek-ai` | `<dir>/bin/dsh`(客户机 `~/.local/node/…`) |
432
+ * | **平铺 / `--prefix` / `npx` 布局** | **`<dir>/node_modules/.bin/dsh`**(预检树、`_npx/<hash>/…`) |
433
+ *
434
+ * 平铺档由来的完整交代见文件头(P1-0 暂存 → P1-3 上移,bootcheck 侧副本已删)。
435
+ * @param {string} scopeDir
436
+ * @returns {string[]}
437
+ */
438
+ export function scopeBinCandidates(scopeDir) {
439
+ if (!scopeDir) return []
440
+ const cands = []
441
+ const m = /^(.*)\/lib\/node_modules\/@deepseek-ai\/?$/.exec(scopeDir)
442
+ if (m) cands.push(join(m[1], 'bin', 'dsh')) // 标准 node 布局(homebrew / node 官方包)
443
+ cands.push(join(dirname(dirname(scopeDir)), 'bin', 'dsh')) // 邻居布局兜底
444
+ cands.push(join(dirname(scopeDir), '.bin', 'dsh')) // 平铺布局(预检树 / npx 缓存)
445
+ return cands
446
+ }
447
+
448
+ /**
449
+ * **按目标 scope 解析**被测 `dsh`(命中存在的候选即返回;一个都不存在 ⇒ **null**,
450
+ * ⛔ **不掺** PATH/全局兜底)。
451
+ *
452
+ * 与 `resolveDshBin()` 的分工:后者是"**给我一个能跑的 dsh**"(PATH → 推导 → 全局兜底);
453
+ * 本函数是"**这个 scope 自带的那个 dsh 是谁**"——插件预检(`plugin-bootcheck`)的硬前置需要一个
454
+ * **与目标 scope 同源**的二进制,掺进 PATH 上那个别的版本就等于把"宿主↔scope 不同源"制造出来
455
+ * (实测:宿主 0.1.7 + 插件 0.2 ⇒ 0.2 的 peer 门禁把 profile 插件行**逐条** disable ×110,
456
+ * 输出与"Argent 坏了"无法区分)。
457
+ *
458
+ * ⚠️ **住在这里的原因(P1-3 收口,2026-10-12)**:候选序列的唯一实现是 {@link scopeBinCandidates}
459
+ * (本文件),而本函数只是它的"取第一个存在的"。P1-0 曾把这段逻辑放在
460
+ * `scripts/lib/bootcheck-wiring.mjs`(宿主预检的判据文件),但那个文件被
461
+ * `scripts/tests/kernel-purity.test.mjs` 显式认领为 **L1**("纯 node 内建、零宿主 import"),
462
+ * 而按该门禁的既有约定 `scripts/lib/dsh-*.mjs` 是 **L2**(宿主运维档)⇒ **L1 不得 import
463
+ * `dsh-*` 模块**。两条纪律("候选序列只许一份实现" + "L1 零宿主 import")同时成立的办法就是
464
+ * 把本函数搬进真源(L2):bootcheck 侧不再有副本,也不必为绕过门禁抬任何基线。
465
+ * 回归:`scripts/tests/bootcheck-wiring.test.mjs` 的 C1/C2/C7 + `scripts/tests/dsh-scope-resolve.test.mjs`。
466
+ */
467
+ export function resolveBootcheckBin(scopeDir, { exists = existsSync } = {}) {
468
+ return scopeBinCandidates(scopeDir).find((c) => exists(c)) || null
469
+ }
470
+
471
+ /**
472
+ * 解析 `dsh` 可执行入口。
473
+ *
474
+ * ⚠️ 别用「scope 上跳两级 + bin/dsh」—— 实测两种布局都会算错(2026-09-28):
475
+ * 开发机 scope `/opt/homebrew/lib/node_modules/@deepseek-ai` → 上跳两级得 `/opt/homebrew/lib`(错)
476
+ * 客户机 scope `~/node/lib/node_modules/@deepseek-ai` → 上跳两级得 `~/node/lib`(错)
477
+ * 正确关系是 `<prefix>/lib/node_modules/@deepseek-ai` 对应 `<prefix>/bin/dsh`:
478
+ * homebrew → `/opt/homebrew/bin/dsh`;客户机 → `~/node/bin/dsh`。
479
+ * 优先级:显式参数/环境变量 → PATH 上的 dsh(用户实际在用的那个最可靠)→ 按 scope 推导
480
+ * (标准 → 邻居 → **平铺**)→ 硬编码全局兜底。
481
+ */
482
+ export function resolveDshBin({ argv = process.argv, env = process.env, scopeDir = null, exists = existsSync, pathProbe = defaultPathProbe } = {}) {
483
+ const explicit = argValue(argv, '--global-bin') || env.DSH_BIN
484
+ if (explicit) return explicit
485
+ try {
486
+ const onPath = pathProbe()
487
+ if (onPath && exists(onPath)) return onPath
488
+ } catch { /* dsh 不在 PATH(cron/launchd 常见)→ 继续 */ }
489
+ // ⚠️ `resolveDshScope()` 自 F1 修复起返回 `{scope,…} | null` ⇒ 这里**必须取 `.scope`**;
490
+ // 取不到(null / 空壳)⇒ 空串(`scopeBinCandidates('')` 返回空候选 ⇒ 直接走下面三条硬编码兜底)。
491
+ const hit = scopeDir ? { scope: scopeDir } : resolveDshScope({ argv, env })
492
+ const scope = hit && !hit.stale ? hit.scope : ''
493
+ const cands = scopeBinCandidates(scope)
494
+ cands.push(
495
+ '/opt/homebrew/bin/dsh',
496
+ join(env.HOME || homedir(), 'node', 'bin', 'dsh'),
497
+ '/usr/local/bin/dsh',
498
+ )
499
+ return cands.find((c) => exists(c)) || cands[0]
500
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * dsh 版本快照的**真实落点**解析(唯一真源:升级器加载与打印共用这一份)。
3
+ *
4
+ * 由来(2026-10-12 P1-0,审计 §C.1 #8):升级器原来打印
5
+ * `快照: ${snapshotFile || join(LOCK_DIR, target + ".json")}` ——
6
+ * 候选线(未晋级)的快照实际在 `dsh-version-lock/candidate/<ver>.json`,
7
+ * 打印出来的却是**根目录**路径。**加载**逻辑本来认 candidate(`:208`),
8
+ * **打印**却编了另一个路径 ⇒ 排障时按打印去找,必然找不到,白跑一趟。
9
+ *
10
+ * 现在:加载与打印都调 {@link locateSnapshot},命中即给**真路径 + 它在哪一档**;
11
+ * 都没有 ⇒ `path: null` 并附**找过哪些路径**(由调用方明说"未找到快照")。
12
+ *
13
+ * ⚠️ 本文件只做路径判据,**不改写快照、不碰晋级逻辑**(晋级是人工/独立批次的事)。
14
+ */
15
+ import { existsSync } from 'node:fs'
16
+ import { join } from 'node:path'
17
+
18
+ /**
19
+ * @param {string} lockDir `dsh-version-lock` 目录
20
+ * @param {string|null} target 目标版本(如 `0.2.0-rc.2`)
21
+ * @param {string|null} explicitFile 显式 `--snapshot <file>`(优先,且**不检查存在性**——保持旧行为:不存在即报错)
22
+ * @param {(p:string)=>boolean} exists 注入以便纯夹具测试
23
+ * @returns {{path:string|null, tier:string|null, searched:string[]}}
24
+ */
25
+ export function locateSnapshot(lockDir, target, explicitFile = null, exists = existsSync) {
26
+ if (explicitFile) return { path: explicitFile, tier: '显式 --snapshot', searched: [explicitFile] }
27
+ const cands = [
28
+ { path: join(lockDir, `${target}.json`), tier: '已晋级(dsh-version-lock/ 根)' },
29
+ { path: join(lockDir, 'candidate', `${target}.json`), tier: '候选·未晋级(dsh-version-lock/candidate/)' },
30
+ ]
31
+ const searched = cands.map((c) => c.path)
32
+ if (!target) return { path: null, tier: null, searched }
33
+ for (const c of cands) if (exists(c.path)) return { ...c, searched }
34
+ return { path: null, tier: null, searched }
35
+ }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * dsh"已验证版本清单"的解析与挑选(**唯一真源**,纯函数,可离线测试)。
3
+ *
4
+ * 规则(2026-09-28 补):
5
+ * · `dsh-version-lock/*.json` = **已验证可用**版本(安装器会从里面挑最高的自动装);
6
+ * · `dsh-version-lock/candidate/` = 已做升级前审查、**尚未验收**(隔离试跑 + 五项复核未过)
7
+ * 的候选版本 ⇒ **绝不参与自动挑选**。
8
+ *
9
+ * 由来:0.1.7-rc.2 的快照刚生成就被放进 lock 根目录,而 `pickLatestVerified()` 取最高版本 ⇒
10
+ * 任何**新装机**(含未来新客户)会自动装 0.1.7 —— 但隔离试跑已证明该版本上我们的 13 个
11
+ * preset **全部挂不上**(0.1.7 改成"preset = 组合里的一行插件",不再扫描目录)⇒
12
+ * 新机器一装就是坏的。候选与已验证必须物理分开,不能靠人记得。
13
+ */
14
+
15
+ /**
16
+ * **内置已验证版本基线**(安装器读不到 lock 目录时用它,例如全新机器上还没克隆仓库)。
17
+ *
18
+ * ⚠️ 纪律(缺陷 2,2026-10-12 真机实测):**列表必须包含 `dsh-version-lock/` 里已晋级的最高版本**,
19
+ * 否则现役版本会被报成"未在已验证版本列表"(winpc 现场:dsh 0.1.7-rc.2 是当时**唯一现役**版本,
20
+ * 却因为安装器跑在 boot 克隆里、`~/.argent-dsh` 还不存在而走了这份 fallback ⇒ 列表里没有 0.1.7-rc.2
21
+ * ⇒ 客户看到一条毫无意义的告警)。防漂移门禁 = `verifiedListGaps()`(回归
22
+ * `scripts/tests/dsh-verified-list.test.mjs` 钉住"缺项即红")。
23
+ */
24
+ export const DSH_VERIFIED_FALLBACK = ['0.1.0-rc.6', '0.1.0-rc.7', '0.1.1-rc.1', '0.1.5-rc.1', '0.1.7-rc.2']
25
+
26
+ /**
27
+ * **防漂移门禁**:`verified`(安装器实际会用的清单)是否覆盖 `lockDir` 里**已晋级**的每个版本。
28
+ *
29
+ * 判据刻意是"**全部**已晋级版本"而不是只有最高版本 —— 缺任何一个都说明列表与 lock 漂移了
30
+ * (只为"最高版本"设卡时,漏掉一个中间版本仍然会被放过去)。
31
+ * `candidate/` 下的快照**不参与**(它们是"已审查、未验收",见 `listCandidateVersions`)。
32
+ * @returns {{ok:boolean, highest:string|null, promoted:string[], missing:string[]}}
33
+ */
34
+ export function verifiedListGaps(verified, lockDir, io) {
35
+ const promoted = [...new Set(listVerifiedVersions(lockDir, io))].sort(cmpVer)
36
+ const missing = promoted.filter((v) => !(verified || []).includes(v))
37
+ return { ok: missing.length === 0, highest: promoted.length ? promoted[promoted.length - 1] : null, promoted, missing }
38
+ }
39
+
40
+ /** 版本比较:0.1.5-rc.1 < 0.1.7-rc.2 < 0.2.0-rc.1(无 rc 号视为 Infinity = 正式版更大)。 */
41
+ export function cmpVer(a, b) {
42
+ const pa = String(a).split('-')
43
+ const pb = String(b).split('-')
44
+ const na = pa[0].split('.').map(Number)
45
+ const nb = pb[0].split('.').map(Number)
46
+ for (let i = 0; i < 3; i += 1) {
47
+ if ((na[i] || 0) !== (nb[i] || 0)) return (na[i] || 0) > (nb[i] || 0) ? 1 : -1
48
+ }
49
+ const ra = /(\d+)/.exec(pa[1] || '')
50
+ const rb = /(\d+)/.exec(pb[1] || '')
51
+ const ka = ra ? Number(ra[1]) : Infinity
52
+ const kb = rb ? Number(rb[1]) : Infinity
53
+ if (ka !== kb) return ka > kb ? 1 : -1
54
+ return 0
55
+ }
56
+
57
+ /**
58
+ * 列出**已验证**版本(只读 lockDir 顶层的 *.json;子目录一律忽略)。
59
+ * @param {string} lockDir
60
+ * @param {(p: string) => boolean} exists
61
+ * @param {(p: string) => string[]} readdir
62
+ * @param {(p: string) => string} read
63
+ */
64
+ export function listVerifiedVersions(lockDir, { exists, readdir, read }) {
65
+ if (!exists(lockDir)) return []
66
+ const out = []
67
+ for (const f of readdir(lockDir)) {
68
+ if (!f.endsWith('.json')) continue
69
+ try {
70
+ const target = JSON.parse(read(joinPath(lockDir, f))).target
71
+ if (target) out.push(target)
72
+ } catch { /* 坏文件跳过 */ }
73
+ }
74
+ return out
75
+ }
76
+
77
+ /** 候选目录(已审查未验收):永不参与自动挑选。 */
78
+ export const CANDIDATE_DIR = 'candidate'
79
+
80
+ /** 挑出最新的已验证版本;无清单时返回 null(调用方用自己的 fallback)。 */
81
+ export function pickLatestVerified(lockDir, io) {
82
+ const vers = listVerifiedVersions(lockDir, io)
83
+ if (!vers.length) return null
84
+ return [...vers].sort(cmpVer)[vers.length - 1]
85
+ }
86
+
87
+ /** 列出候选版本(`candidate/` 下的快照)——仅供人工/文档查询,不参与挑选。 */
88
+ export function listCandidateVersions(lockDir, { exists, readdir, read }) {
89
+ const dir = joinPath(lockDir, CANDIDATE_DIR)
90
+ if (!exists(dir)) return []
91
+ return readdir(dir).filter((f) => f.endsWith('.json'))
92
+ .map((f) => { try { return JSON.parse(read(joinPath(dir, f))).target } catch { return null } })
93
+ .filter(Boolean)
94
+ }
95
+
96
+ function joinPath(a, b) { return `${a.replace(/\/$/, '')}/${b}` }