@whyshu/argent-engine 0.0.0-stage → 0.6.1

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 (340) hide show
  1. package/MANIFEST.json +341 -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 +576 -0
  55. package/engine/scripts/billing-mode-daily.sh +13 -0
  56. package/engine/scripts/build-engine-package.mjs +126 -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 +378 -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/collect-evidence.mjs +42 -0
  81. package/engine/scripts/config-explain.mjs +278 -0
  82. package/engine/scripts/configure-provider.rb +29 -0
  83. package/engine/scripts/customer-config-migrate.mjs +179 -0
  84. package/engine/scripts/customer-preflight.py +892 -0
  85. package/engine/scripts/customer-readiness.sh +135 -0
  86. package/engine/scripts/daily-report-inbox.py +242 -0
  87. package/engine/scripts/daily-report-reconcile.mjs +312 -0
  88. package/engine/scripts/daily-report.mjs +1310 -0
  89. package/engine/scripts/daily-report.sh +47 -0
  90. package/engine/scripts/data/lip-product-dict.json +306 -0
  91. package/engine/scripts/deliver-customer.mjs +932 -0
  92. package/engine/scripts/delivery-faces.txt +31 -0
  93. package/engine/scripts/desktop-trial.sh +146 -0
  94. package/engine/scripts/dev-window.sh +261 -0
  95. package/engine/scripts/drill-clean-machine.sh +315 -0
  96. package/engine/scripts/dsh-api-client.mjs +215 -0
  97. package/engine/scripts/dsh-backup-scope.sh +83 -0
  98. package/engine/scripts/dsh-offline-pack.mjs +77 -0
  99. package/engine/scripts/dsh-overlay-pin.sh +87 -0
  100. package/engine/scripts/dsh-plane.mjs +153 -0
  101. package/engine/scripts/dsh-portforward.py +223 -0
  102. package/engine/scripts/dsh-preflight.mjs +179 -0
  103. package/engine/scripts/dsh-release-watch-daily.sh +19 -0
  104. package/engine/scripts/dsh-release-watch.mjs +458 -0
  105. package/engine/scripts/dsh-remote-check.sh +99 -0
  106. package/engine/scripts/dsh-restart.sh +179 -0
  107. package/engine/scripts/dsh-restore.sh +111 -0
  108. package/engine/scripts/dsh-scope-rewire.mjs +803 -0
  109. package/engine/scripts/dsh-snapshot.mjs +116 -0
  110. package/engine/scripts/dsh-upgrade-readiness.mjs +165 -0
  111. package/engine/scripts/dsh-upgrade.mjs +612 -0
  112. package/engine/scripts/dsh-url.sh +86 -0
  113. package/engine/scripts/dsh-web-launch.mjs +155 -0
  114. package/engine/scripts/ensure-store-windows.py +509 -0
  115. package/engine/scripts/entitlements.py +458 -0
  116. package/engine/scripts/env_probe.py +411 -0
  117. package/engine/scripts/feishu-card.mjs +252 -0
  118. package/engine/scripts/final-check.mjs +43 -0
  119. package/engine/scripts/fix-skill-frontmatter.py +209 -0
  120. package/engine/scripts/flow-auto-retry.py +454 -0
  121. package/engine/scripts/flow-requirements.json +735 -0
  122. package/engine/scripts/flow-scaffold.py +275 -0
  123. package/engine/scripts/flow-timers-plist.sh +87 -0
  124. package/engine/scripts/flow-timers.sh +184 -0
  125. package/engine/scripts/flow-trigger-wake.mjs +284 -0
  126. package/engine/scripts/flow-trigger.sh +571 -0
  127. package/engine/scripts/flow_billing.py +354 -0
  128. package/engine/scripts/flow_capability_stats.py +314 -0
  129. package/engine/scripts/flow_case.py +194 -0
  130. package/engine/scripts/flow_claim.py +497 -0
  131. package/engine/scripts/flow_config.py +1025 -0
  132. package/engine/scripts/flow_failure.py +588 -0
  133. package/engine/scripts/flow_learn.py +105 -0
  134. package/engine/scripts/flow_ledger.py +235 -0
  135. package/engine/scripts/flow_policy.py +238 -0
  136. package/engine/scripts/flow_preflight.py +339 -0
  137. package/engine/scripts/flow_registry.py +341 -0
  138. package/engine/scripts/flow_retry.py +304 -0
  139. package/engine/scripts/flow_return_to_ops.py +191 -0
  140. package/engine/scripts/flow_source.py +315 -0
  141. package/engine/scripts/flow_sources/__init__.py +53 -0
  142. package/engine/scripts/flow_sources/cases_json.py +106 -0
  143. package/engine/scripts/flow_sources/common.py +428 -0
  144. package/engine/scripts/flow_sources/feishu_base.py +273 -0
  145. package/engine/scripts/flow_stale.py +185 -0
  146. package/engine/scripts/flow_status_reconcile.py +344 -0
  147. package/engine/scripts/flow_triage.py +849 -0
  148. package/engine/scripts/flow_write_verify.py +496 -0
  149. package/engine/scripts/fulfillment.js +586 -0
  150. package/engine/scripts/gen-agent-bundles.mjs +276 -0
  151. package/engine/scripts/gen-preset-rows.mjs +259 -0
  152. package/engine/scripts/init-ml.sh +2278 -0
  153. package/engine/scripts/install-internal-role.sh +190 -0
  154. package/engine/scripts/install-license.mjs +641 -0
  155. package/engine/scripts/install-one-shot.mjs +665 -0
  156. package/engine/scripts/integration_contract.py +220 -0
  157. package/engine/scripts/integrations/wenyao/adapter.py +98 -0
  158. package/engine/scripts/integrations/xzh/adapter.py +75 -0
  159. package/engine/scripts/invoice-mapping.example.json +40 -0
  160. package/engine/scripts/invoice-table.sh +195 -0
  161. package/engine/scripts/invoice_carrier_notice.py +279 -0
  162. package/engine/scripts/invoice_fail_closeout.py +297 -0
  163. package/engine/scripts/invoice_lip_runner.py +1508 -0
  164. package/engine/scripts/invoice_lip_runner.py.bak-20260922-1710-freready +1090 -0
  165. package/engine/scripts/invoice_ready_audit.py +191 -0
  166. package/engine/scripts/invoice_runner.py +675 -0
  167. package/engine/scripts/invoice_stageb.py +516 -0
  168. package/engine/scripts/launchd/com.whyshu.argent-bridge.plist.example +42 -0
  169. package/engine/scripts/launchd/com.whyshu.fulfillment-poll.plist.example +29 -0
  170. package/engine/scripts/lease.py +377 -0
  171. package/engine/scripts/lib/agent-install.mjs +649 -0
  172. package/engine/scripts/lib/argent-datadir.mjs +16 -0
  173. package/engine/scripts/lib/argent-datadir.sh +18 -0
  174. package/engine/scripts/lib/argent_datadir.py +285 -0
  175. package/engine/scripts/lib/argent_filelock.py +507 -0
  176. package/engine/scripts/lib/bootcheck-wiring.mjs +510 -0
  177. package/engine/scripts/lib/cdp_ports.py +197 -0
  178. package/engine/scripts/lib/customer-env.mjs +102 -0
  179. package/engine/scripts/lib/daily-report-core.mjs +517 -0
  180. package/engine/scripts/lib/dep-declaration.mjs +81 -0
  181. package/engine/scripts/lib/deps-migrate.mjs +310 -0
  182. package/engine/scripts/lib/dsh-host-resolve.mjs +109 -0
  183. package/engine/scripts/lib/dsh-scope.mjs +500 -0
  184. package/engine/scripts/lib/dsh-snapshot-path.mjs +35 -0
  185. package/engine/scripts/lib/dsh-versions.mjs +96 -0
  186. package/engine/scripts/lib/dsh-web-probe.mjs +214 -0
  187. package/engine/scripts/lib/is-direct-run.mjs +61 -0
  188. package/engine/scripts/lib/patch-compose.mjs +241 -0
  189. package/engine/scripts/lib/patch-target.mjs +35 -0
  190. package/engine/scripts/lib/plugin-lifecycle.mjs +128 -0
  191. package/engine/scripts/lib/plugin-packages.mjs +332 -0
  192. package/engine/scripts/lib/preset-compat.mjs +127 -0
  193. package/engine/scripts/lib/preset-install.mjs +575 -0
  194. package/engine/scripts/lib/preset-mechanism.mjs +56 -0
  195. package/engine/scripts/lib/preset-sync.mjs +59 -0
  196. package/engine/scripts/lib/profile-retire.mjs +97 -0
  197. package/engine/scripts/lib/python-runtime.mjs +190 -0
  198. package/engine/scripts/lib/registry.mjs +72 -0
  199. package/engine/scripts/lib/repo-root.sh +56 -0
  200. package/engine/scripts/lib/schedule-mechanism.mjs +659 -0
  201. package/engine/scripts/lib/schedule-probe.mjs +835 -0
  202. package/engine/scripts/lib/scope-pointer.mjs +164 -0
  203. package/engine/scripts/lib/settings-panels.mjs +816 -0
  204. package/engine/scripts/lib/stdio_utf8.py +86 -0
  205. package/engine/scripts/lib/wake-auth.mjs +392 -0
  206. package/engine/scripts/lib/webdriver_ipc.py +264 -0
  207. package/engine/scripts/lib/zstd-frames.mjs +104 -0
  208. package/engine/scripts/license-e2e-drill.mjs +426 -0
  209. package/engine/scripts/license-gate.mjs +245 -0
  210. package/engine/scripts/lip_backlog.py +171 -0
  211. package/engine/scripts/lip_table.py +185 -0
  212. package/engine/scripts/local-usage.mjs +214 -0
  213. package/engine/scripts/migrate-providers.mjs +127 -0
  214. package/engine/scripts/migrate-skills.sh +65 -0
  215. package/engine/scripts/ml-oauth-setup.mjs +550 -0
  216. package/engine/scripts/ml-store-ids.json +26 -0
  217. package/engine/scripts/ml_artifact_attribution.py +333 -0
  218. package/engine/scripts/ml_capacity_probe.py +1158 -0
  219. package/engine/scripts/ml_hub_status.py +1556 -0
  220. package/engine/scripts/ml_schema_check.py +115 -0
  221. package/engine/scripts/ml_store_ids.py +139 -0
  222. package/engine/scripts/npm-publish-argent.mjs +499 -0
  223. package/engine/scripts/npm-publish-argent.sh +20 -0
  224. package/engine/scripts/orchestrator.py +13811 -0
  225. package/engine/scripts/pack-plugins.mjs +140 -0
  226. package/engine/scripts/patches/dsh-llm-deepseek--transport-diagnostics.patch +297 -0
  227. package/engine/scripts/patches/dsh-session--append-message-shape.patch +15 -0
  228. package/engine/scripts/patches/dsh-session--tool-result-block-017.patch +28 -0
  229. package/engine/scripts/patches/dsh-session-format-v0-to-v1--permission-preset-origin.patch +11 -0
  230. package/engine/scripts/patches/dsh-session-format-v3-to-v4--turn-outside-replace.patch +20 -0
  231. package/engine/scripts/patches/dsh-session-persistence-jsonl--worker-bundled-migrators.patch +39 -0
  232. package/engine/scripts/patches/dsh-tools--render-return-shape.patch +50 -0
  233. package/engine/scripts/patches/verify-bundled-migrators.mjs +79 -0
  234. package/engine/scripts/patches/verify-hardening.mjs +167 -0
  235. package/engine/scripts/patches/verify-session-migration.mjs +149 -0
  236. package/engine/scripts/patches/verify-transport-diagnostics.mjs +227 -0
  237. package/engine/scripts/plan-role-rename.mjs +125 -0
  238. package/engine/scripts/plugin-bootcheck.mjs +451 -0
  239. package/engine/scripts/plugin-composition.json +15 -0
  240. package/engine/scripts/poc-mcp-host.mjs +455 -0
  241. package/engine/scripts/poll-fulfillment.sh +295 -0
  242. package/engine/scripts/poll-invoice.sh +76 -0
  243. package/engine/scripts/profile-doctor.mjs +686 -0
  244. package/engine/scripts/provider-select.sh +71 -0
  245. package/engine/scripts/provider-switch.sh +146 -0
  246. package/engine/scripts/provider-sync-target.check.mjs +64 -0
  247. package/engine/scripts/python-runtime.sh +78 -0
  248. package/engine/scripts/query_runner.py +310 -0
  249. package/engine/scripts/release-argent.sh +574 -0
  250. package/engine/scripts/repair-unresolved-tools.mjs +178 -0
  251. package/engine/scripts/retire-inventory.mjs +40 -0
  252. package/engine/scripts/run-tests.sh +257 -0
  253. package/engine/scripts/s1-audit.mjs +85 -0
  254. package/engine/scripts/self-sign-license.mjs +179 -0
  255. package/engine/scripts/session-health-scan.mjs +63 -0
  256. package/engine/scripts/session-repair-tool-result-content.mjs +181 -0
  257. package/engine/scripts/setup.sh +22 -0
  258. package/engine/scripts/skill-install.mjs +629 -0
  259. package/engine/scripts/sku_translate.py +148 -0
  260. package/engine/scripts/sync-plugins.sh +90 -0
  261. package/engine/scripts/sync-presets.sh +27 -0
  262. package/engine/scripts/sync-scripts.sh +445 -0
  263. package/engine/scripts/telemetry.py +873 -0
  264. package/engine/scripts/ui-copy-count.mjs +220 -0
  265. package/engine/scripts/usage-anchor.sh +137 -0
  266. package/engine/scripts/usage_report.py +531 -0
  267. package/engine/scripts/validate-preset-schema.mjs +208 -0
  268. package/engine/scripts/vertical-init.mjs +323 -0
  269. package/engine/scripts/vertical-lint.mjs +482 -0
  270. package/engine/scripts/wenyao-browser.sh +74 -0
  271. package/engine/scripts/wenyao-fixture-export.py +116 -0
  272. package/engine/scripts/wenyao-invoice-writeback.mjs +540 -0
  273. package/engine/scripts/wenyao-login.sh +35 -0
  274. package/engine/scripts/wenyao-template-map.json +292 -0
  275. package/engine/scripts/wenyao_attach.py +131 -0
  276. package/engine/scripts/wenyao_download_template.py +144 -0
  277. package/engine/scripts/wenyao_fill_template.py +568 -0
  278. package/engine/scripts/wenyao_login.py +145 -0
  279. package/engine/scripts/wenyao_portal.py +633 -0
  280. package/engine/scripts/wenyao_recon.py +230 -0
  281. package/engine/scripts/wenyao_run.py +503 -0
  282. package/engine/scripts/wenyao_shadow_diff.py +160 -0
  283. package/engine/scripts/wenyao_skill_contract.py +156 -0
  284. package/engine/scripts/wenyao_template_fetch.py +175 -0
  285. package/engine/scripts/win/dsh-web-autostart.ps1 +372 -0
  286. package/engine/scripts/ziniao-auth-probe.sh +268 -0
  287. package/engine/scripts/ziniao-keepalive.sh +217 -0
  288. package/engine/scripts/ziniao-post-upgrade-check.sh +121 -0
  289. package/engine/scripts/ziniao-windows.sh +154 -0
  290. package/engine/skills/README.md +86 -0
  291. package/engine/skills/argent-dev/SKILL.md +41 -0
  292. package/engine/skills/internal.json +37 -0
  293. package/engine/skills/lktop-pm/SKILL.md +35 -0
  294. package/engine/skills/lktopapp-dev/SKILL.md +34 -0
  295. package/engine/skills/ml-config-guide/SKILL.md +50 -0
  296. package/engine/skills/ml-fulfillment-sop/SKILL.md +178 -0
  297. package/engine/skills/ml-fulfillment-sop/references/andes-components.md +69 -0
  298. package/engine/skills/ml-fulfillment-sop/references/cdp-port-mapping.md +35 -0
  299. package/engine/skills/ml-fulfillment-sop/references/feishu-base-fulfillment-schema.md +136 -0
  300. package/engine/skills/ml-fulfillment-sop/references/multi-window-concurrency.md +69 -0
  301. package/engine/skills/ml-fulfillment-sop/references/pitfalls.md +77 -0
  302. package/engine/skills/ml-fulfillment-sop/references/selectors.md +243 -0
  303. package/engine/skills/ml-fulfillment-sop/scripts/fulfillment.js +586 -0
  304. package/engine/skills/ml-fulfillment-sop/scripts/poll-and-execute.sh +271 -0
  305. package/engine/skills/ml-fulfillment-sop/templates/report.md +55 -0
  306. package/engine/skills/ml-invoice/SKILL.md +506 -0
  307. package/engine/skills/ml-invoice/references/pitfalls.md +268 -0
  308. package/engine/skills/ml-invoice/references/portal-flow.md +360 -0
  309. package/engine/skills/ml-invoice/references/template-fill-contract.md +133 -0
  310. package/engine/skills/ml-invoice-xzh/SKILL.md +133 -0
  311. package/engine/skills/ml-invoice-xzh/references/pitfalls.md +230 -0
  312. package/engine/skills/ml-store-daily-report/SKILL.md +224 -0
  313. package/engine/skills/ml-store-daily-report/scripts/README.md +25 -0
  314. package/engine/skills/ml-store-daily-report/scripts/extract.js +691 -0
  315. package/engine/skills/ml-store-daily-report/scripts/generate.js +299 -0
  316. package/engine/skills/pet-dev/SKILL.md +42 -0
  317. package/engine/skills/whyshu-pm/SKILL.md +35 -0
  318. package/engine/skills/whyshu-web-dev/SKILL.md +50 -0
  319. package/engine/version.mjs +1 -0
  320. package/engine/verticals/README.md +103 -0
  321. package/engine/verticals/_engine-words.baseline.txt +237 -0
  322. package/engine/verticals/_engine-words.json +19 -0
  323. package/engine/verticals/mercadolibre/README.md +43 -0
  324. package/engine/verticals/mercadolibre/config.json +709 -0
  325. package/engine/verticals/mercadolibre/flows.json +738 -0
  326. package/engine/verticals/mercadolibre/manifest.json +538 -0
  327. package/engine/verticals/mercadolibre/migrate-machine-state.mjs +138 -0
  328. package/engine/verticals/mercadolibre/preflight.py +143 -0
  329. package/engine/verticals/mercadolibre/prompts/daily-report.failure.md +69 -0
  330. package/engine/verticals/mercadolibre/prompts/daily-report.md +47 -0
  331. package/engine/verticals/mercadolibre/prompts/daily-report.scheduled.md +79 -0
  332. package/engine/verticals/mercadolibre/prompts/full.failure.md +125 -0
  333. package/engine/verticals/mercadolibre/prompts/full.md +91 -0
  334. package/engine/verticals/mercadolibre/prompts/wenyao-invoice.failure.md +88 -0
  335. package/engine/verticals/mercadolibre/prompts/wenyao-invoice.md +64 -0
  336. package/engine/verticals/mercadolibre/prompts/xzh-invoice.failure.md +91 -0
  337. package/engine/verticals/mercadolibre/prompts/xzh-invoice.md +46 -0
  338. package/package.json +22 -4
  339. package/requirements.txt +5 -0
  340. package/README.md +0 -3
@@ -0,0 +1,310 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * deps-migrate —— 存量机器「遗留 `file:` 依赖 ⇒ npm 名」的**唯一实现**(0.6.0 W8,2026-10-14)
4
+ *
5
+ * ══════════════════════════════════════════════════════════════════════════
6
+ * 它解决什么(现场形态,2026-10-03 实测)
7
+ * ══════════════════════════════════════════════════════════════════════════
8
+ * 存量客户机的 profile `package.json` 里,Argent 插件是按**本机路径**装的:
9
+ * `"argent-core": "file:/private/var/folders/…/tmp.yHwjWMOjNV/repo/plugins/argent-core"`
10
+ * 那个临时目录早没了 ⇒ 依赖永远解析不到;而 npm 名(`@whyshu/argent-core@1.2.3`)是**随机器走**的。
11
+ * 本模块把这件事做成一条**可干跑、可回滚、幂等**的迁移:
12
+ *
13
+ * | 纪律 | 判据 |
14
+ * |---|---|
15
+ * | **默认干跑** | `plan()` 是纯函数(零 IO 副作用);只有 `apply()` 才写盘 |
16
+ * | **先备份** | 写前 `cp` 到 `<数据目录>/backup/<名>.<UTC 时间戳>` + **sha256** + **逐字回滚命令** |
17
+ * | **写后读回** | 写完立刻重新解析并断言目标形态(⛔ 不"写完就算成功") |
18
+ * | **幂等** | 已经迁过的机器 ⇒ `plan().changed === 0` ⇒ **零写入**(连 mtime 都不动) |
19
+ * | **⛔ 不裸跑包管理器** | 默认**不跑**;要跑必须显式 `--allow-package-manager`(AGENTS 红线:会把 profile 弄坏) |
20
+ * | **审计** | 每次真写追加一行 JSONL(谁/何时/改了什么/备份在哪/回滚命令) |
21
+ *
22
+ * ══════════════════════════════════════════════════════════════════════════
23
+ * 边界(⛔ 改本文件前先读)
24
+ * ══════════════════════════════════════════════════════════════════════════
25
+ * 1. **只碰已知 Argent 插件**(判据 = `scripts/lib/plugin-packages.mjs#pluginIdOf`,**唯一真源**)。
26
+ * 别的 `file:`、别人的 scope、官方 `@deepseek-ai/*` ⇒ 一律 `unknown`/`keep`,⛔ 不越权。
27
+ * 2. **只迁"顶层包目录"形态**(`file:…/plugins/<id>` / `file:…/packs-cache/<id>.tgz`)。
28
+ * **子路径导出**(`file:…/plugins/argent-core/llm-whyshu`)在 npm 上**没有对应包**
29
+ * (它是 `argent-core` 的子导出)⇒ 必须**原样留着**(⛔ 迁了就是断链)。
30
+ * 3. **不改版本语义**:目标版本 = **本机克隆/包留档里那个真实版本**(读不到 ⇒ 该条判 `blocked`,
31
+ * ⛔ 不编一个版本号)。壳/智能体用**精确版本**,与安装器 `npmSpecOf()` 同口径。
32
+ * 4. **两条线共用**:升级器(`scripts/argent-upgrade.mjs migrate-deps`)与安装器的
33
+ * 依赖键迁移**都调这里的纯函数** —— ⛔ 不许在别处写第二份"file: 怎么迁"。
34
+ */
35
+
36
+ import { createHash } from 'node:crypto'
37
+ import { copyFileSync, existsSync, mkdirSync, appendFileSync, readFileSync, writeFileSync } from 'node:fs'
38
+ import { hostname, userInfo } from 'node:os'
39
+ import { dirname, join } from 'node:path'
40
+ import { pluginIdOf, PLUGIN_PACKAGES, npmNameOf, npmSpecOf, readPluginVersion, pluginPackageDir } from './plugin-packages.mjs'
41
+
42
+ const isObj = (v) => v !== null && typeof v === 'object' && !Array.isArray(v)
43
+ export const sha256 = (text) => createHash('sha256').update(text).digest('hex')
44
+
45
+ /** 一条依赖的形态判据(**纯**:只按字符串判,⛔ 不读盘)。 */
46
+ export function classifySpec(spec) {
47
+ const s = String(spec ?? '').trim()
48
+ if (!s) return { kind: 'empty' }
49
+ if (!s.startsWith('file:')) return { kind: 'npm', spec: s }
50
+ const p = s.slice('file:'.length).replace(/\\/g, '/').replace(/\/+$/, '')
51
+ const m = p.match(/(?:^|\/)(plugins|packs-cache)\/([^/]+?)(?:\.tgz)?$/)
52
+ if (!m) {
53
+ // `file:…/plugins/<id>/<子路径>`(子导出)或任意别的路径
54
+ const sub = p.match(/(?:^|\/)plugins\/([^/]+)\/.+$/)
55
+ if (sub) return { kind: 'file-subpath', spec: s, base: sub[1], path: p }
56
+ return { kind: 'file-other', spec: s, path: p }
57
+ }
58
+ return {
59
+ kind: m[1] === 'packs-cache' ? 'file-tgz' : 'file-plugin',
60
+ spec: s, base: m[2], path: p,
61
+ }
62
+ }
63
+
64
+ /**
65
+ * 算迁移计划(**纯函数**:只读调用方传进来的 `pkg`;版本从 `versionOf` 回调拿)。
66
+ *
67
+ * @param {object} opts
68
+ * @param {object} opts.pkg profile 的 `package.json`(已解析)
69
+ * @param {(id:string)=>string} [opts.versionOf] 取某插件的**真实版本**(读不到 ⇒ '')
70
+ * @param {(id:string)=>boolean} [opts.knownId] 该 id 是不是已知 Argent 插件(缺省用 pluginIdOf)
71
+ * @returns {{ok:boolean, changed:number, entries:Array, deps:object, blocked:string[], notes:string[]}}
72
+ */
73
+ export function planDepsMigration({ pkg, versionOf = () => '', knownId = null, source = 'profile' } = {}) {
74
+ const deps = isObj(pkg && pkg.dependencies) ? pkg.dependencies : {}
75
+ const entries = []
76
+ const blocked = []
77
+ const notes = []
78
+ if (!isObj(pkg && pkg.dependencies)) {
79
+ notes.push('profile 的 `dependencies` 不是对象(或不存在)⇒ 无可迁移之物')
80
+ return { ok: true, changed: 0, entries, deps: { ...deps }, blocked, notes, source }
81
+ }
82
+ const out = { ...deps }
83
+ for (const [key, spec] of Object.entries(deps)) {
84
+ const c = classifySpec(spec)
85
+ const id = knownId ? (knownId(key) ? knownId(key) : null) : pluginIdOf(key)
86
+ const row = { key, spec: String(spec), kind: c.kind, id: id || null, action: 'keep', to: null, why: '' }
87
+
88
+ if (c.kind === 'file-subpath') {
89
+ row.action = 'keep'
90
+ row.why = `子路径导出(${c.base} 的子路径)在 npm 上没有对应包 ⇒ 原样留着(迁了就是断链)`
91
+ entries.push(row); continue
92
+ }
93
+ if (c.kind === 'file-other' || c.kind === 'empty') {
94
+ row.action = 'keep'
95
+ row.why = c.kind === 'empty' ? '空值 ⇒ 不动' : '`file:` 指向的既不是插件目录也不是包留档 ⇒ ⛔ 不越权'
96
+ entries.push(row); continue
97
+ }
98
+ if (!id) {
99
+ row.action = c.kind === 'npm' ? 'already-npm' : 'keep'
100
+ row.why = c.kind === 'npm'
101
+ ? '已是非 `file:` 规格且不是已知 Argent 插件 ⇒ 原样(⛔ 不越权)'
102
+ : `\`file:\` 的 base 不是已知 Argent 插件(${c.base})⇒ ⛔ 不越权改`
103
+ entries.push(row); continue
104
+ }
105
+ const npm = npmNameOf(id)
106
+ const needNpmSpec = c.kind === 'file-plugin' || c.kind === 'file-tgz'
107
+ if (!needNpmSpec) {
108
+ // 非 `file:`(npm 范围 / tarball 之外的写法):**只迁键**(spec 原样 —— 来源不是本模块的事)
109
+ if (key === npm) { row.action = 'already-npm'; row.why = `键已是 npm 名(${npm})⇒ 无需迁移`; entries.push(row); continue }
110
+ delete out[key]
111
+ out[npm] = String(spec)
112
+ row.action = 'migrate'
113
+ row.to = `${npm}(spec 原样:${String(spec)})`
114
+ row.renamed = true
115
+ row.why = `裸名键 ⇒ npm 名(spec 原样保留;来源不是本次迁移的对象)`
116
+ entries.push(row); continue
117
+ }
118
+ if (key === npm) {
119
+ row.action = 'already-npm'
120
+ row.why = `键已是 npm 名(${npm})⇒ 无需迁移`
121
+ entries.push(row); continue
122
+ }
123
+ const ver = String(versionOf(id) || '')
124
+ if (!ver) {
125
+ row.action = 'blocked'
126
+ row.why = `读不到 ${id} 的**真实版本**(本机克隆/包留档里都没有)⇒ ⛔ 不编版本号;先补克隆或包留档`
127
+ blocked.push(`${key} → ${npm}:${row.why}`)
128
+ entries.push(row); continue
129
+ }
130
+ const to = npmSpecOf(id, ver)
131
+ if (!to) {
132
+ row.action = 'blocked'
133
+ row.why = `拼不出 npm 规格(${id})`
134
+ blocked.push(`${key}:${row.why}`)
135
+ entries.push(row); continue
136
+ }
137
+ row.action = 'migrate'
138
+ row.to = to
139
+ row.why = `遗留 ${c.kind}(${c.path})⇒ npm 名 ${to}(版本取自本机真实包)`
140
+ if (key !== npm) {
141
+ delete out[key]
142
+ out[npm] = to
143
+ row.renamed = true
144
+ row.why += ';**键同时改名**(裸名 ⇒ npm 名)'
145
+ } else {
146
+ out[key] = to
147
+ }
148
+ entries.push(row)
149
+ }
150
+ const changed = entries.filter((e) => e.action === 'migrate').length
151
+ if (!changed) notes.push('没有需要迁移的依赖 ⇒ 计划为空(幂等:这一步**零写入**)')
152
+ return { ok: blocked.length === 0, changed, entries, deps: out, blocked, notes, source }
153
+ }
154
+
155
+ /** 序列化 profile `package.json`(与仓库既有写盘口径一致:2 空格 + 末尾换行)。 */
156
+ export const serializePkg = (pkg, deps) => `${JSON.stringify({ ...pkg, dependencies: deps }, null, 2)}\n`
157
+
158
+ /**
159
+ * 备份文件名(UTC 时间戳 + 原文件名;与 `backupFile()` 同风格)。
160
+ * ⚠️ `reason` 会被 **slug 化**(非 `[A-Za-z0-9._-]` ⇒ `-`):备份名会被写进**回滚命令**,
161
+ * 带空白/箭头/中文的文件名在 shell 里是另一个坑(`cp -p "a b" c` 一旦有人少写引号就炸)。
162
+ */
163
+ export function backupName(file, now = new Date(), reason = '') {
164
+ const ts = now.toISOString().replace(/[-:]/g, '').replace(/\..+$/, '').replace('T', '-')
165
+ const slug = String(reason).replace(/[^A-Za-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '')
166
+ const tail = slug ? `.${slug}` : ''
167
+ return `${file.split('/').pop()}.deps-migrate-${ts}${tail}`
168
+ }
169
+
170
+ /**
171
+ * 干跑/真跑的统一渲染(**CLI 与审计共用**;⛔ 别处不再拼一遍)。
172
+ * @returns {{lines:string[], rollback:string, summary:string}}
173
+ */
174
+ export function renderPlan(plan, { profilePkgPath = '', apply = false, runPackageManager = false } = {}) {
175
+ const lines = []
176
+ const mig = plan.entries.filter((e) => e.action === 'migrate')
177
+ const kee = plan.entries.filter((e) => e.action === 'keep')
178
+ const blk = plan.entries.filter((e) => e.action === 'blocked')
179
+ lines.push(`${apply ? '执行' : '干跑(dry-run)'}:将改 ${mig.length} 条依赖|不动 ${kee.length} 条|受阻 ${blk.length} 条`)
180
+ for (const e of plan.entries) {
181
+ if (e.action === 'migrate') lines.push(` → ${e.key}:${e.spec} ⇒ ${e.to}`)
182
+ else if (e.action === 'blocked') lines.push(` ✗ ${e.key}:${e.why}`)
183
+ else if (e.action === 'keep') lines.push(` · ${e.key}:保持(${e.why})`)
184
+ }
185
+ lines.push(`包管理器:${runPackageManager ? '**会跑**(--allow-package-manager 显式开启)' : '不跑(默认;⛔ 裸跑 pnpm 会把 profile 弄坏)'}`)
186
+ lines.push(`影响面(face):profile package.json${profilePkgPath ? `(${profilePkgPath})` : ''}`
187
+ + `${runPackageManager ? ' + node_modules(包管理器装上/更新后同步)' : '(node_modules 不变 —— 未跑包管理器,落位仍是旧来源)'}`)
188
+ const rollback = profilePkgPath
189
+ ? `cp -p "${profilePkgPath}.deps-migrate-<时间戳>" "${profilePkgPath}" # 或:cp -p "<备份路径>" "${profilePkgPath}"`
190
+ : '(没有 profile package.json ⇒ 无需回滚)'
191
+ return { lines, rollback, summary: `${mig.length} 条待迁 / ${blk.length} 条受阻` }
192
+ }
193
+
194
+ /**
195
+ * 真跑:**先备份 → 改 manifest → 读回验收 → 审计**(幂等:`changed === 0` ⇒ 立刻返回,零写入)。
196
+ *
197
+ * ⛔ 本函数**不跑任何包管理器**(那是调用方在显式开关下的职责)。
198
+ * @returns {{ok:boolean, wrote:boolean, backup:string, backupSha:string, rollback:string,
199
+ * audit:string, verify:string, error:string}}
200
+ */
201
+ export function applyDepsMigration({
202
+ pkgPath, pkg, plan, auditPath, backupDir, now = new Date(), reason = 'W8 file:→npm',
203
+ fs = { existsSync, readFileSync, writeFileSync, copyFileSync, mkdirSync, appendFileSync, dirname },
204
+ by = '',
205
+ /**
206
+ * 读回验收用的**重算器**(可选)。⚠️ 必须与产出 `plan` 的那次用**同一套判据**
207
+ * (同样的 `knownId`/`versionOf`)—— 否则"缺版本"这类条目在两套判据下结论不同,
208
+ * 验收会**假红**(实测:夹具里没传 knownId ⇒ 重算时裸键也算待迁 ⇒ 明明迁对了却判失败)。
209
+ * 缺省 = `(pkg) => planDepsMigration({ pkg })`(仅"spec 还是不是 `file:`"这一层判据)。
210
+ */
211
+ replan = null,
212
+ } = {}) {
213
+ const result = { ok: false, wrote: false, backup: '', backupSha: '', rollback: '', audit: '', verify: '', error: '' }
214
+ if (!plan || plan.changed === 0) {
215
+ result.ok = true
216
+ result.verify = 'nothing-to-do(幂等:零写入)'
217
+ return result
218
+ }
219
+ if (plan.blocked && plan.blocked.length) {
220
+ result.error = `有 ${plan.blocked.length} 条受阻(${plan.blocked[0]})⇒ ⛔ 拒绝半截迁移(要么全可迁、要么先补证据)`
221
+ return result
222
+ }
223
+ const before = fs.readFileSync(pkgPath, 'utf8')
224
+ if (!backupDir) { result.error = '没有备份目录(backupDir 必填)⇒ ⛔ 不写'; return result }
225
+ const backup = join(backupDir, backupName(pkgPath, now, reason))
226
+ try {
227
+ fs.mkdirSync(dirname(backup), { recursive: true })
228
+ fs.copyFileSync(pkgPath, backup)
229
+ } catch (e) {
230
+ result.error = `备份失败(${String((e && e.message) || e)})⇒ ⛔ 不写`
231
+ return result
232
+ }
233
+ result.backup = backup
234
+ result.backupSha = sha256(before)
235
+ result.rollback = `cp -p "${backup}" "${pkgPath}"`
236
+ const text = serializePkg(pkg, plan.deps)
237
+ try {
238
+ fs.writeFileSync(pkgPath, text)
239
+ } catch (e) {
240
+ result.error = `写盘失败(${String((e && e.message) || e)});备份在 ${backup}`
241
+ return result
242
+ }
243
+ // ── 读回验收(⛔ 不"写完就算成功")──────────────────────────────────────────
244
+ let after = null
245
+ try { after = fs.readFileSync(pkgPath, 'utf8') } catch (e) { result.error = `写后读回失败(${String((e && e.message) || e)})`; return result }
246
+ let re = null
247
+ try {
248
+ re = (typeof replan === 'function' ? replan : (doc) => planDepsMigration({ pkg: doc }))(JSON.parse(after))
249
+ } catch (e) {
250
+ result.error = `写后读回解析失败(${String((e && e.message) || e)})⇒ 逐字节回滚:${result.rollback}`
251
+ try { fs.writeFileSync(pkgPath, before) } catch { /* 回滚失败已由 error 表达 */ }
252
+ return result
253
+ }
254
+ if (!re || re.changed !== 0) {
255
+ result.error = `写后读回验收失败:重算仍显示 ${re ? re.changed : '?'} 条待迁 ⇒ 逐字节回滚:${result.rollback}`
256
+ try { fs.writeFileSync(pkgPath, before) } catch { /* 回滚失败已由 error 表达 */ }
257
+ return result
258
+ }
259
+ // ⛔ 第二条判据(**内容真的变了**):写入被静默吞掉 / 写成同一份字节 ⇒ 不算迁移成功。
260
+ // 只看"重算待迁 0 条"是不够的:**判据本身依赖磁盘内容** ⇒ 内容没变时那条判据会**自洽地通过**。
261
+ if (sha256(after) === result.backupSha) {
262
+ result.error = `写后读回:文件**逐字节未变**(写入被静默吞掉)⇒ 不算迁移成功;回滚/排查:${result.rollback}`
263
+ return result
264
+ }
265
+ result.verify = `写后读回:重算待迁 0 条 ∧ 内容已变(before sha256 ${result.backupSha.slice(0, 12)}…)`
266
+ result.wrote = true
267
+ if (auditPath) {
268
+ try {
269
+ fs.mkdirSync(dirname(auditPath), { recursive: true })
270
+ const rec = {
271
+ ts: now.toISOString(),
272
+ tool: 'deps-migrate',
273
+ reason,
274
+ pkgPath,
275
+ backup: result.backup,
276
+ backupSha256: result.backupSha,
277
+ rollback: result.rollback,
278
+ changed: plan.entries.filter((e) => e.action === 'migrate').map((e) => ({ from: `${e.key}=${e.spec}`, to: e.to })),
279
+ kept: plan.entries.filter((e) => e.action === 'keep').map((e) => e.key),
280
+ packageManager: false,
281
+ by: by || `${(() => { try { return userInfo().username } catch { return 'unknown' } })()}@${hostname()}`,
282
+ }
283
+ if (!fs.existsSync(auditPath)) fs.appendFileSync(auditPath, `${JSON.stringify({ _doc: '依赖迁移审计(W8):谁在什么时候把 file: 依赖迁成了 npm 名。JSONL 追加;与 schedule-audit.jsonl 同风格。' })}\n`)
284
+ fs.appendFileSync(auditPath, `${JSON.stringify(rec)}\n`)
285
+ result.audit = auditPath
286
+ } catch (e) {
287
+ result.error = `审计追加失败(${String((e && e.message) || e)})—— 迁移本身已成功并已验收`
288
+ }
289
+ }
290
+ result.ok = true
291
+ return result
292
+ }
293
+
294
+ /** 取某插件的**真实版本**(克隆里的 `plugins/<id>/package.json`;读不到 ⇒ '',⛔ 不编)。 */
295
+ export function realVersionOf(repoRoot, id) {
296
+ if (!repoRoot) return ''
297
+ const dir = pluginPackageDir(repoRoot, id)
298
+ if (!dir || !existsSync(dir)) return ''
299
+ return readPluginVersion(repoRoot, id) || ''
300
+ }
301
+
302
+ /** 已知 id 判据(**写侧严格**:只认裸名/本仓 npm 名,⛔ 不认路径形态与别的 scope)。 */
303
+ export function knownPluginId(key) {
304
+ const s = String(key ?? '').trim()
305
+ if (!s) return null
306
+ for (const p of PLUGIN_PACKAGES) {
307
+ if (s === p.id || s === p.npm) return p.id
308
+ }
309
+ return null
310
+ }
@@ -0,0 +1,109 @@
1
+ /**
2
+ * 宿主(dsh)**解析链** —— 唯一真源(纯函数,可离线测试;2026-10-12 用户决定)。
3
+ *
4
+ * ══════════════════════════════════════════════════════════════════════════
5
+ * 一、为什么有它
6
+ * ══════════════════════════════════════════════════════════════════════════
7
+ * 用户 2026-10-12 定的三条政策:
8
+ * ① **彻底删掉 `--install-host`**:Argent ⛔ 不代装宿主(宿主迭代频繁、副本维护不动;装不上客户会
9
+ * 找我们,而我们解决不了)。理由写进设计 §8.4 的实现注记。
10
+ * ② 被拒时给的是 dsh **官方**运行方式(官方 README「运行」):首选
11
+ * **`npx @deepseek-ai/dsh web`**(通过 npm 运行),备选**从源码运行**。
12
+ * ③ **npx 形态是官方首选 ⇒ 必须被识别为"有宿主"**(旧口径"npx 不算已安装宿主"**作废**)。
13
+ *
14
+ * 解析链(顺序即优先级;**确定且可解释**,每一步都能在输出里看到):
15
+ * PATH 上的 `dsh` → `DSH_BIN` → **npx 缓存形态** → 都没有 ⇒ 调用方拒绝 + 官方指引。
16
+ *
17
+ * ══════════════════════════════════════════════════════════════════════════
18
+ * 二、npx 缓存形态长什么样
19
+ * ══════════════════════════════════════════════════════════════════════════
20
+ * `npx @deepseek-ai/dsh@<版本> web` 跑过之后,npm 会把整棵依赖树留在自己的 `_npx` 缓存里:
21
+ * `<npm cache>/_npx/<hash>/node_modules/@deepseek-ai/dsh/package.json` ← 这里读 version
22
+ * 所以判据是**读一个 package.json**(纯只读、不联网、不执行 npx)——⛔ 不跑 `npx --version`:
23
+ * 那可能触发一次真实下载(慢、且把"零网络"变成假的),而版本号在文件里就有。
24
+ *
25
+ * 多个候选时的选择规则(`NPX_PICK_RULE`,调用方必须**打印选了哪个**):
26
+ * ① 只认能读出的 `@deepseek-ai/dsh`(坏 JSON / 别的包 ⇒ 跳过,不猜);
27
+ * ② **最近使用优先** = `package.json` 的 mtime 最新(npx 每次跑会刷新它 ⇒ 最接近"你实际在跑的
28
+ * 那一个");③ 同刻(mtime 相同)按**路径升序** —— 保证结果与目录遍历顺序无关、可复现。
29
+ * ⚠️ 刻意**不**按"版本号最大"或"优先窗口内版本"挑:客户实际在跑哪个版本,就该按哪个版本判门禁
30
+ * (否则我们会在一台实际跑着窗口外版本的机器上"装成功",且客户不会知道)。
31
+ */
32
+
33
+ /**
34
+ * 一个 package.json 对象是不是 dsh 主包(判据只认 name + 非空 version)。
35
+ * @param {any} obj
36
+ * @returns {boolean}
37
+ */
38
+ export function isDshPackageJson(obj) {
39
+ return Boolean(obj) && typeof obj === 'object'
40
+ && obj.name === '@deepseek-ai/dsh'
41
+ && typeof obj.version === 'string' && obj.version.trim() !== ''
42
+ }
43
+
44
+ /**
45
+ * npx 缓存候选目录(按查序返回,已去重)。
46
+ * · `npm_config_cache` / `NPM_CONFIG_CACHE`(npm 自己的覆盖)⇒ `<cache>/_npx`
47
+ * · `~/.npm/_npx`(macOS/Linux 与 npm 默认)
48
+ * · `%LOCALAPPDATA%\npm-cache\_npx`(Windows 默认)
49
+ * @param {{home?: string, env?: Record<string,string|undefined>}} o
50
+ * @returns {string[]}
51
+ */
52
+ export function npxCacheDirs({ home = '', env = {} } = {}) {
53
+ const out = []
54
+ const push = (p) => { const v = String(p || '').replace(/[\\/]+$/, ''); if (v && !out.includes(v)) out.push(v) }
55
+ const cache = env.npm_config_cache || env.NPM_CONFIG_CACHE || ''
56
+ if (cache) push(`${cache}/_npx`)
57
+ if (home) push(`${home}/.npm/_npx`)
58
+ const la = env.LOCALAPPDATA || env.LocalAppData || ''
59
+ if (la) push(`${la}/npm-cache/_npx`)
60
+ return out
61
+ }
62
+
63
+ /**
64
+ * 扫缓存目录,返回所有"看起来装过 dsh"的候选(**只读**;坏 JSON / 别的包一律跳过,不猜)。
65
+ * @param {string[]} dirs npxCacheDirs() 的产物
66
+ * @param {{exists:(p:string)=>boolean, readdir:(p:string)=>string[], read:(p:string)=>string, stat:(p:string)=>{mtimeMs:number}}} io
67
+ * @returns {Array<{pkgDir:string, pkgJson:string, version:string, mtimeMs:number, cacheDir:string, hash:string}>}
68
+ */
69
+ export function scanNpxCache(dirs, io) {
70
+ const out = []
71
+ for (const dir of dirs || []) {
72
+ if (!dir || !io.exists(dir)) continue
73
+ let hashes = []
74
+ try { hashes = io.readdir(dir) } catch { continue }
75
+ for (const hash of [...hashes].sort()) {
76
+ const pkgDir = `${dir}/${hash}/node_modules/@deepseek-ai/dsh`
77
+ const pkgJson = `${pkgDir}/package.json`
78
+ if (!io.exists(pkgJson)) continue
79
+ let meta = null
80
+ try { meta = JSON.parse(io.read(pkgJson)) } catch { continue } // 坏 JSON ⇒ 跳过(不猜版本)
81
+ if (!isDshPackageJson(meta)) continue
82
+ let mtimeMs = 0
83
+ try { mtimeMs = Number(io.stat(pkgJson).mtimeMs) || 0 } catch { mtimeMs = 0 }
84
+ out.push({ pkgDir, pkgJson, version: String(meta.version).trim(), mtimeMs, cacheDir: dir, hash })
85
+ }
86
+ }
87
+ return out
88
+ }
89
+
90
+ /**
91
+ * 确定性挑选(规则见文件头 `NPX_PICK_RULE`):mtime 最新优先,同刻按路径升序。
92
+ * ⛔ 不做"优先窗口内版本"的二次挑选 —— 这台机器实际在跑哪个版本,就按哪个版本判门禁。
93
+ * @param {Array<object>} cands scanNpxCache() 的产物
94
+ * @returns {{chosen: object|null, considered: Array<{version:string,pkgDir:string,mtimeMs:number}>}}
95
+ */
96
+ export function pickNpxHost(cands) {
97
+ const list = [...(cands || [])].sort((a, b) => {
98
+ if (b.mtimeMs !== a.mtimeMs) return b.mtimeMs - a.mtimeMs
99
+ return a.pkgDir < b.pkgDir ? -1 : a.pkgDir > b.pkgDir ? 1 : 0
100
+ })
101
+ return {
102
+ chosen: list.length ? list[0] : null,
103
+ considered: list.map((c) => ({ version: c.version, pkgDir: c.pkgDir, mtimeMs: c.mtimeMs })),
104
+ }
105
+ }
106
+
107
+ /** 选择规则的**人话**(调用方打印它 ⇒ "确定且可解释");⛔ 别在别处再写第二份措辞。 */
108
+ export const NPX_PICK_RULE = 'npx 缓存里可能有多个 dsh:按「最近使用优先(package.json mtime 最新),同刻按路径升序」'
109
+ + '选定一个(⛔ 不按版本号挑:你这台机器实际在跑哪个版本,就按哪个版本判门禁)'