opensip-cli 0.1.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 (348) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +8 -0
  3. package/README.md +51 -0
  4. package/dist/api.d.ts +17 -0
  5. package/dist/api.d.ts.map +1 -0
  6. package/dist/api.js +16 -0
  7. package/dist/api.js.map +1 -0
  8. package/dist/bootstrap/admit-tool-package.d.ts +117 -0
  9. package/dist/bootstrap/admit-tool-package.d.ts.map +1 -0
  10. package/dist/bootstrap/admit-tool-package.js +170 -0
  11. package/dist/bootstrap/admit-tool-package.js.map +1 -0
  12. package/dist/bootstrap/baseline-seams.d.ts +30 -0
  13. package/dist/bootstrap/baseline-seams.d.ts.map +1 -0
  14. package/dist/bootstrap/baseline-seams.js +156 -0
  15. package/dist/bootstrap/baseline-seams.js.map +1 -0
  16. package/dist/bootstrap/bootstrap-error.d.ts +41 -0
  17. package/dist/bootstrap/bootstrap-error.d.ts.map +1 -0
  18. package/dist/bootstrap/bootstrap-error.js +33 -0
  19. package/dist/bootstrap/bootstrap-error.js.map +1 -0
  20. package/dist/bootstrap/build-command-registration-input.d.ts +34 -0
  21. package/dist/bootstrap/build-command-registration-input.d.ts.map +1 -0
  22. package/dist/bootstrap/build-command-registration-input.js +73 -0
  23. package/dist/bootstrap/build-command-registration-input.js.map +1 -0
  24. package/dist/bootstrap/build-per-run-scope.d.ts +62 -0
  25. package/dist/bootstrap/build-per-run-scope.d.ts.map +1 -0
  26. package/dist/bootstrap/build-per-run-scope.js +152 -0
  27. package/dist/bootstrap/build-per-run-scope.js.map +1 -0
  28. package/dist/bootstrap/build-targets.d.ts +42 -0
  29. package/dist/bootstrap/build-targets.d.ts.map +1 -0
  30. package/dist/bootstrap/build-targets.js +117 -0
  31. package/dist/bootstrap/build-targets.js.map +1 -0
  32. package/dist/bootstrap/cli-defaults.d.ts +35 -0
  33. package/dist/bootstrap/cli-defaults.d.ts.map +1 -0
  34. package/dist/bootstrap/cli-defaults.js +65 -0
  35. package/dist/bootstrap/cli-defaults.js.map +1 -0
  36. package/dist/bootstrap/config-and-capabilities.d.ts +74 -0
  37. package/dist/bootstrap/config-and-capabilities.d.ts.map +1 -0
  38. package/dist/bootstrap/config-and-capabilities.js +224 -0
  39. package/dist/bootstrap/config-and-capabilities.js.map +1 -0
  40. package/dist/bootstrap/deliver-envelope.d.ts +80 -0
  41. package/dist/bootstrap/deliver-envelope.d.ts.map +1 -0
  42. package/dist/bootstrap/deliver-envelope.js +195 -0
  43. package/dist/bootstrap/deliver-envelope.js.map +1 -0
  44. package/dist/bootstrap/egress-plane.d.ts +22 -0
  45. package/dist/bootstrap/egress-plane.d.ts.map +1 -0
  46. package/dist/bootstrap/egress-plane.js +37 -0
  47. package/dist/bootstrap/egress-plane.js.map +1 -0
  48. package/dist/bootstrap/host-planes.d.ts +28 -0
  49. package/dist/bootstrap/host-planes.d.ts.map +1 -0
  50. package/dist/bootstrap/host-planes.js +152 -0
  51. package/dist/bootstrap/host-planes.js.map +1 -0
  52. package/dist/bootstrap/index.d.ts +76 -0
  53. package/dist/bootstrap/index.d.ts.map +1 -0
  54. package/dist/bootstrap/index.js +109 -0
  55. package/dist/bootstrap/index.js.map +1 -0
  56. package/dist/bootstrap/live-plane.d.ts +51 -0
  57. package/dist/bootstrap/live-plane.d.ts.map +1 -0
  58. package/dist/bootstrap/live-plane.js +72 -0
  59. package/dist/bootstrap/live-plane.js.map +1 -0
  60. package/dist/bootstrap/load-tool-capabilities.d.ts +42 -0
  61. package/dist/bootstrap/load-tool-capabilities.d.ts.map +1 -0
  62. package/dist/bootstrap/load-tool-capabilities.js +76 -0
  63. package/dist/bootstrap/load-tool-capabilities.js.map +1 -0
  64. package/dist/bootstrap/output-plane.d.ts +37 -0
  65. package/dist/bootstrap/output-plane.d.ts.map +1 -0
  66. package/dist/bootstrap/output-plane.js +114 -0
  67. package/dist/bootstrap/output-plane.js.map +1 -0
  68. package/dist/bootstrap/owning-tool-init.d.ts +32 -0
  69. package/dist/bootstrap/owning-tool-init.d.ts.map +1 -0
  70. package/dist/bootstrap/owning-tool-init.js +69 -0
  71. package/dist/bootstrap/owning-tool-init.js.map +1 -0
  72. package/dist/bootstrap/pre-action-guards.d.ts +44 -0
  73. package/dist/bootstrap/pre-action-guards.d.ts.map +1 -0
  74. package/dist/bootstrap/pre-action-guards.js +136 -0
  75. package/dist/bootstrap/pre-action-guards.js.map +1 -0
  76. package/dist/bootstrap/pre-action-hook.d.ts +68 -0
  77. package/dist/bootstrap/pre-action-hook.d.ts.map +1 -0
  78. package/dist/bootstrap/pre-action-hook.js +289 -0
  79. package/dist/bootstrap/pre-action-hook.js.map +1 -0
  80. package/dist/bootstrap/pre-action-messages.d.ts +32 -0
  81. package/dist/bootstrap/pre-action-messages.d.ts.map +1 -0
  82. package/dist/bootstrap/pre-action-messages.js +49 -0
  83. package/dist/bootstrap/pre-action-messages.js.map +1 -0
  84. package/dist/bootstrap/process-idempotency.d.ts +17 -0
  85. package/dist/bootstrap/process-idempotency.d.ts.map +1 -0
  86. package/dist/bootstrap/process-idempotency.js +20 -0
  87. package/dist/bootstrap/process-idempotency.js.map +1 -0
  88. package/dist/bootstrap/register-language-adapters.d.ts +23 -0
  89. package/dist/bootstrap/register-language-adapters.d.ts.map +1 -0
  90. package/dist/bootstrap/register-language-adapters.js +35 -0
  91. package/dist/bootstrap/register-language-adapters.js.map +1 -0
  92. package/dist/bootstrap/register-tools.d.ts +228 -0
  93. package/dist/bootstrap/register-tools.d.ts.map +1 -0
  94. package/dist/bootstrap/register-tools.js +696 -0
  95. package/dist/bootstrap/register-tools.js.map +1 -0
  96. package/dist/bootstrap/render.d.ts +27 -0
  97. package/dist/bootstrap/render.d.ts.map +1 -0
  98. package/dist/bootstrap/render.js +53 -0
  99. package/dist/bootstrap/render.js.map +1 -0
  100. package/dist/bootstrap/report.d.ts +34 -0
  101. package/dist/bootstrap/report.d.ts.map +1 -0
  102. package/dist/bootstrap/report.js +47 -0
  103. package/dist/bootstrap/report.js.map +1 -0
  104. package/dist/bootstrap/run-plane.d.ts +105 -0
  105. package/dist/bootstrap/run-plane.d.ts.map +1 -0
  106. package/dist/bootstrap/run-plane.js +190 -0
  107. package/dist/bootstrap/run-plane.js.map +1 -0
  108. package/dist/bootstrap/scope-access.d.ts +68 -0
  109. package/dist/bootstrap/scope-access.d.ts.map +1 -0
  110. package/dist/bootstrap/scope-access.js +115 -0
  111. package/dist/bootstrap/scope-access.js.map +1 -0
  112. package/dist/bootstrap/state-seams.d.ts +14 -0
  113. package/dist/bootstrap/state-seams.d.ts.map +1 -0
  114. package/dist/bootstrap/state-seams.js +26 -0
  115. package/dist/bootstrap/state-seams.js.map +1 -0
  116. package/dist/bootstrap/tool-lifecycle.d.ts +102 -0
  117. package/dist/bootstrap/tool-lifecycle.d.ts.map +1 -0
  118. package/dist/bootstrap/tool-lifecycle.js +103 -0
  119. package/dist/bootstrap/tool-lifecycle.js.map +1 -0
  120. package/dist/bootstrap/tool-trust.d.ts +49 -0
  121. package/dist/bootstrap/tool-trust.d.ts.map +1 -0
  122. package/dist/bootstrap/tool-trust.js +65 -0
  123. package/dist/bootstrap/tool-trust.js.map +1 -0
  124. package/dist/bootstrap/validate-tool.d.ts +22 -0
  125. package/dist/bootstrap/validate-tool.d.ts.map +1 -0
  126. package/dist/bootstrap/validate-tool.js +38 -0
  127. package/dist/bootstrap/validate-tool.js.map +1 -0
  128. package/dist/cli-context.d.ts +38 -0
  129. package/dist/cli-context.d.ts.map +1 -0
  130. package/dist/cli-context.js +134 -0
  131. package/dist/cli-context.js.map +1 -0
  132. package/dist/commands/agent-catalog.d.ts +45 -0
  133. package/dist/commands/agent-catalog.d.ts.map +1 -0
  134. package/dist/commands/agent-catalog.js +115 -0
  135. package/dist/commands/agent-catalog.js.map +1 -0
  136. package/dist/commands/assemble-outcome.d.ts +69 -0
  137. package/dist/commands/assemble-outcome.d.ts.map +1 -0
  138. package/dist/commands/assemble-outcome.js +121 -0
  139. package/dist/commands/assemble-outcome.js.map +1 -0
  140. package/dist/commands/clear.d.ts +32 -0
  141. package/dist/commands/clear.d.ts.map +1 -0
  142. package/dist/commands/clear.js +73 -0
  143. package/dist/commands/clear.js.map +1 -0
  144. package/dist/commands/completion.d.ts +90 -0
  145. package/dist/commands/completion.d.ts.map +1 -0
  146. package/dist/commands/completion.js +233 -0
  147. package/dist/commands/completion.js.map +1 -0
  148. package/dist/commands/configure.d.ts +32 -0
  149. package/dist/commands/configure.d.ts.map +1 -0
  150. package/dist/commands/configure.js +94 -0
  151. package/dist/commands/configure.js.map +1 -0
  152. package/dist/commands/history.d.ts +18 -0
  153. package/dist/commands/history.d.ts.map +1 -0
  154. package/dist/commands/history.js +48 -0
  155. package/dist/commands/history.js.map +1 -0
  156. package/dist/commands/host-command-specs.d.ts +49 -0
  157. package/dist/commands/host-command-specs.d.ts.map +1 -0
  158. package/dist/commands/host-command-specs.js +331 -0
  159. package/dist/commands/host-command-specs.js.map +1 -0
  160. package/dist/commands/host-subcommand-groups.d.ts +69 -0
  161. package/dist/commands/host-subcommand-groups.d.ts.map +1 -0
  162. package/dist/commands/host-subcommand-groups.js +374 -0
  163. package/dist/commands/host-subcommand-groups.js.map +1 -0
  164. package/dist/commands/index.d.ts +36 -0
  165. package/dist/commands/index.d.ts.map +1 -0
  166. package/dist/commands/index.js +36 -0
  167. package/dist/commands/index.js.map +1 -0
  168. package/dist/commands/init/config-templates.d.ts +16 -0
  169. package/dist/commands/init/config-templates.d.ts.map +1 -0
  170. package/dist/commands/init/config-templates.js +108 -0
  171. package/dist/commands/init/config-templates.js.map +1 -0
  172. package/dist/commands/init/file-classifier.d.ts +40 -0
  173. package/dist/commands/init/file-classifier.d.ts.map +1 -0
  174. package/dist/commands/init/file-classifier.js +155 -0
  175. package/dist/commands/init/file-classifier.js.map +1 -0
  176. package/dist/commands/init/language-detection.d.ts +44 -0
  177. package/dist/commands/init/language-detection.d.ts.map +1 -0
  178. package/dist/commands/init/language-detection.js +124 -0
  179. package/dist/commands/init/language-detection.js.map +1 -0
  180. package/dist/commands/init/scaffold-writer.d.ts +26 -0
  181. package/dist/commands/init/scaffold-writer.d.ts.map +1 -0
  182. package/dist/commands/init/scaffold-writer.js +102 -0
  183. package/dist/commands/init/scaffold-writer.js.map +1 -0
  184. package/dist/commands/init/state-machine.d.ts +32 -0
  185. package/dist/commands/init/state-machine.d.ts.map +1 -0
  186. package/dist/commands/init/state-machine.js +105 -0
  187. package/dist/commands/init/state-machine.js.map +1 -0
  188. package/dist/commands/init.d.ts +95 -0
  189. package/dist/commands/init.d.ts.map +1 -0
  190. package/dist/commands/init.js +209 -0
  191. package/dist/commands/init.js.map +1 -0
  192. package/dist/commands/mount-command-spec.d.ts +106 -0
  193. package/dist/commands/mount-command-spec.d.ts.map +1 -0
  194. package/dist/commands/mount-command-spec.js +313 -0
  195. package/dist/commands/mount-command-spec.js.map +1 -0
  196. package/dist/commands/mount-result-command.d.ts +71 -0
  197. package/dist/commands/mount-result-command.d.ts.map +1 -0
  198. package/dist/commands/mount-result-command.js +76 -0
  199. package/dist/commands/mount-result-command.js.map +1 -0
  200. package/dist/commands/plugin/config-edit.d.ts +20 -0
  201. package/dist/commands/plugin/config-edit.d.ts.map +1 -0
  202. package/dist/commands/plugin/config-edit.js +102 -0
  203. package/dist/commands/plugin/config-edit.js.map +1 -0
  204. package/dist/commands/plugin/domain-resolution.d.ts +38 -0
  205. package/dist/commands/plugin/domain-resolution.d.ts.map +1 -0
  206. package/dist/commands/plugin/domain-resolution.js +98 -0
  207. package/dist/commands/plugin/domain-resolution.js.map +1 -0
  208. package/dist/commands/plugin/host-dir.d.ts +42 -0
  209. package/dist/commands/plugin/host-dir.d.ts.map +1 -0
  210. package/dist/commands/plugin/host-dir.js +168 -0
  211. package/dist/commands/plugin/host-dir.js.map +1 -0
  212. package/dist/commands/plugin-host-ops.d.ts +41 -0
  213. package/dist/commands/plugin-host-ops.d.ts.map +1 -0
  214. package/dist/commands/plugin-host-ops.js +114 -0
  215. package/dist/commands/plugin-host-ops.js.map +1 -0
  216. package/dist/commands/plugin.d.ts +81 -0
  217. package/dist/commands/plugin.d.ts.map +1 -0
  218. package/dist/commands/plugin.js +287 -0
  219. package/dist/commands/plugin.js.map +1 -0
  220. package/dist/commands/render-outcome.d.ts +52 -0
  221. package/dist/commands/render-outcome.d.ts.map +1 -0
  222. package/dist/commands/render-outcome.js +55 -0
  223. package/dist/commands/render-outcome.js.map +1 -0
  224. package/dist/commands/session-show.d.ts +27 -0
  225. package/dist/commands/session-show.d.ts.map +1 -0
  226. package/dist/commands/session-show.js +166 -0
  227. package/dist/commands/session-show.js.map +1 -0
  228. package/dist/commands/shared.d.ts +107 -0
  229. package/dist/commands/shared.d.ts.map +1 -0
  230. package/dist/commands/shared.js +13 -0
  231. package/dist/commands/shared.js.map +1 -0
  232. package/dist/commands/tools/data-purge.d.ts +20 -0
  233. package/dist/commands/tools/data-purge.d.ts.map +1 -0
  234. package/dist/commands/tools/data-purge.js +59 -0
  235. package/dist/commands/tools/data-purge.js.map +1 -0
  236. package/dist/commands/tools/index.d.ts +16 -0
  237. package/dist/commands/tools/index.d.ts.map +1 -0
  238. package/dist/commands/tools/index.js +213 -0
  239. package/dist/commands/tools/index.js.map +1 -0
  240. package/dist/commands/tools/install.d.ts +24 -0
  241. package/dist/commands/tools/install.d.ts.map +1 -0
  242. package/dist/commands/tools/install.js +83 -0
  243. package/dist/commands/tools/install.js.map +1 -0
  244. package/dist/commands/tools/list.d.ts +41 -0
  245. package/dist/commands/tools/list.d.ts.map +1 -0
  246. package/dist/commands/tools/list.js +103 -0
  247. package/dist/commands/tools/list.js.map +1 -0
  248. package/dist/commands/tools/runtime-probe-entry.d.ts +14 -0
  249. package/dist/commands/tools/runtime-probe-entry.d.ts.map +1 -0
  250. package/dist/commands/tools/runtime-probe-entry.js +36 -0
  251. package/dist/commands/tools/runtime-probe-entry.js.map +1 -0
  252. package/dist/commands/tools/runtime-probe.d.ts +29 -0
  253. package/dist/commands/tools/runtime-probe.d.ts.map +1 -0
  254. package/dist/commands/tools/runtime-probe.js +66 -0
  255. package/dist/commands/tools/runtime-probe.js.map +1 -0
  256. package/dist/commands/tools/storage-contract-checks.d.ts +37 -0
  257. package/dist/commands/tools/storage-contract-checks.d.ts.map +1 -0
  258. package/dist/commands/tools/storage-contract-checks.js +91 -0
  259. package/dist/commands/tools/storage-contract-checks.js.map +1 -0
  260. package/dist/commands/tools/uninstall.d.ts +29 -0
  261. package/dist/commands/tools/uninstall.d.ts.map +1 -0
  262. package/dist/commands/tools/uninstall.js +77 -0
  263. package/dist/commands/tools/uninstall.js.map +1 -0
  264. package/dist/commands/tools/validate.d.ts +44 -0
  265. package/dist/commands/tools/validate.d.ts.map +1 -0
  266. package/dist/commands/tools/validate.js +202 -0
  267. package/dist/commands/tools/validate.js.map +1 -0
  268. package/dist/commands/uninstall/targets.d.ts +53 -0
  269. package/dist/commands/uninstall/targets.d.ts.map +1 -0
  270. package/dist/commands/uninstall/targets.js +205 -0
  271. package/dist/commands/uninstall/targets.js.map +1 -0
  272. package/dist/commands/uninstall.d.ts +88 -0
  273. package/dist/commands/uninstall.d.ts.map +1 -0
  274. package/dist/commands/uninstall.js +184 -0
  275. package/dist/commands/uninstall.js.map +1 -0
  276. package/dist/env/host-env-specs.d.ts +52 -0
  277. package/dist/env/host-env-specs.d.ts.map +1 -0
  278. package/dist/env/host-env-specs.js +129 -0
  279. package/dist/env/host-env-specs.js.map +1 -0
  280. package/dist/error-handler.d.ts +64 -0
  281. package/dist/error-handler.d.ts.map +1 -0
  282. package/dist/error-handler.js +180 -0
  283. package/dist/error-handler.js.map +1 -0
  284. package/dist/index.d.ts +21 -0
  285. package/dist/index.d.ts.map +1 -0
  286. package/dist/index.js +154 -0
  287. package/dist/index.js.map +1 -0
  288. package/dist/open-report.d.ts +40 -0
  289. package/dist/open-report.d.ts.map +1 -0
  290. package/dist/open-report.js +54 -0
  291. package/dist/open-report.js.map +1 -0
  292. package/dist/report-compose.d.ts +35 -0
  293. package/dist/report-compose.d.ts.map +1 -0
  294. package/dist/report-compose.js +103 -0
  295. package/dist/report-compose.js.map +1 -0
  296. package/dist/session-replay-registry.d.ts +20 -0
  297. package/dist/session-replay-registry.d.ts.map +1 -0
  298. package/dist/session-replay-registry.js +38 -0
  299. package/dist/session-replay-registry.js.map +1 -0
  300. package/dist/telemetry/profiling.d.ts +42 -0
  301. package/dist/telemetry/profiling.d.ts.map +1 -0
  302. package/dist/telemetry/profiling.js +160 -0
  303. package/dist/telemetry/profiling.js.map +1 -0
  304. package/dist/telemetry/sdk-init.d.ts +87 -0
  305. package/dist/telemetry/sdk-init.d.ts.map +1 -0
  306. package/dist/telemetry/sdk-init.js +235 -0
  307. package/dist/telemetry/sdk-init.js.map +1 -0
  308. package/dist/ui/App.d.ts +32 -0
  309. package/dist/ui/App.d.ts.map +1 -0
  310. package/dist/ui/App.js +35 -0
  311. package/dist/ui/App.js.map +1 -0
  312. package/dist/ui/render.d.ts +15 -0
  313. package/dist/ui/render.d.ts.map +1 -0
  314. package/dist/ui/render.js +21 -0
  315. package/dist/ui/render.js.map +1 -0
  316. package/dist/ui/result-to-view.d.ts +40 -0
  317. package/dist/ui/result-to-view.d.ts.map +1 -0
  318. package/dist/ui/result-to-view.js +389 -0
  319. package/dist/ui/result-to-view.js.map +1 -0
  320. package/dist/ui/views/init-view.d.ts +9 -0
  321. package/dist/ui/views/init-view.d.ts.map +1 -0
  322. package/dist/ui/views/init-view.js +119 -0
  323. package/dist/ui/views/init-view.js.map +1 -0
  324. package/dist/ui/views/misc-views.d.ts +18 -0
  325. package/dist/ui/views/misc-views.d.ts.map +1 -0
  326. package/dist/ui/views/misc-views.js +244 -0
  327. package/dist/ui/views/misc-views.js.map +1 -0
  328. package/dist/ui/views/plugin-view.d.ts +8 -0
  329. package/dist/ui/views/plugin-view.d.ts.map +1 -0
  330. package/dist/ui/views/plugin-view.js +135 -0
  331. package/dist/ui/views/plugin-view.js.map +1 -0
  332. package/dist/ui/views/tools-views.d.ts +12 -0
  333. package/dist/ui/views/tools-views.d.ts.map +1 -0
  334. package/dist/ui/views/tools-views.js +152 -0
  335. package/dist/ui/views/tools-views.js.map +1 -0
  336. package/dist/update-notifier.d.ts +108 -0
  337. package/dist/update-notifier.d.ts.map +1 -0
  338. package/dist/update-notifier.js +188 -0
  339. package/dist/update-notifier.js.map +1 -0
  340. package/dist/update-state.d.ts +40 -0
  341. package/dist/update-state.d.ts.map +1 -0
  342. package/dist/update-state.js +81 -0
  343. package/dist/update-state.js.map +1 -0
  344. package/dist/welcome.d.ts +53 -0
  345. package/dist/welcome.d.ts.map +1 -0
  346. package/dist/welcome.js +89 -0
  347. package/dist/welcome.js.map +1 -0
  348. package/package.json +100 -0
@@ -0,0 +1,696 @@
1
+ // @fitness-ignore-file performance-anti-patterns -- sequential await across discovered tool packages preserves load order for plugin-conflict detection; bounded by installed plugin count
2
+ // @fitness-ignore-file file-length-limit -- bootstrap composition root: one cohesive tool-admission lifecycle (resolve bundled dir → loadToolManifest → admitTool, across bundled / installed / project-local sources) plus discovery-source ordering and command mounting; splitting fragments the unified admission dispatch (cf. graph.ts's identical waiver for its subcommand-dispatch surface).
3
+ /**
4
+ * register-tools — populate the kernel `ToolRegistry` with first-party
5
+ * tools (fitness / simulation / graph) plus any third-party tool
6
+ * packages discovered on disk.
7
+ *
8
+ * Extracted from `index.ts`. The bundled-id skip below is defense in
9
+ * depth: as of Layer 1 Phase 1 the registry itself enforces
10
+ * first-writer-wins on duplicate ids and logs a structured
11
+ * `tool.registry.duplicate` warning. Keeping the explicit guard avoids
12
+ * a noisy warning when a third-party package happens to ship under a
13
+ * built-in id.
14
+ */
15
+ import { existsSync, readFileSync } from 'node:fs';
16
+ import { createRequire } from 'node:module';
17
+ import { dirname, join } from 'node:path';
18
+ import { admitTool, assertManifestMatchesTool, discoverAuthoredToolSidecars, discoverToolPackagesFromAnchors, loadToolManifest, logger, PluginIncompatibleError, PROJECT_LOCAL_MANIFEST_FILE, resolveProjectContext, resolveProjectPaths, resolveUserPaths, } from '@opensip-cli/core';
19
+ import { mountCommandSpec } from '../commands/mount-command-spec.js';
20
+ import { admitToolPackage, importToolRuntime, } from './admit-tool-package.js';
21
+ import { isProjectLocalToolTrusted } from './tool-trust.js';
22
+ /** `module` field on every structured log event emitted from this file. */
23
+ const BOOTSTRAP_MODULE = 'cli:bootstrap';
24
+ /**
25
+ * Bundled first-party tool PACKAGES — declared as direct deps of
26
+ * opensip-cli. Order is registration order (and thus help/listing order).
27
+ *
28
+ * launch cutover: these are package NAMES, not imported tool runtimes. The
29
+ * host no longer statically `import`s `fitnessTool`/`graphTool`/`simulationTool`
30
+ * — bundled tools are resolved on disk and loaded by DYNAMIC IMPORT through the
31
+ * exact same manifest → `admitTool` → import → register path an installed or
32
+ * project-local tool travels (north-star §2.1, Figure 7). "Bundled" is now a
33
+ * provenance/trust posture, not a privileged load path: install-source
34
+ * independence is structural, not merely tested (`no-bootstrap-tool-import`
35
+ * guards this file against a static tool-runtime import creeping back).
36
+ */
37
+ export const BUNDLED_TOOL_PACKAGES = [
38
+ '@opensip-cli/fitness',
39
+ '@opensip-cli/simulation',
40
+ '@opensip-cli/graph',
41
+ ];
42
+ // ^ Editing this list? EXPECTED_SCAFFOLDING_TOOL_IDS below pins the historical
43
+ // `init`-scaffold expectation against exactly this kind of edit — keep the
44
+ // two in agreement deliberately, not accidentally.
45
+ /**
46
+ * The ADR-0038 back-compat pin: the tool IDS whose `init` scaffold dirs the
47
+ * pre-registry-driven CLI ALWAYS created (fit/sim). The composition root warns
48
+ * (`cli.tool.expected_bundled_absent`) when one of these is missing from the
49
+ * populated registry, so a build whose {@link BUNDLED_TOOL_PACKAGES} drifted
50
+ * (a tool removed, a packaging variant) under-scaffolds LOUDLY instead of
51
+ * silently.
52
+ *
53
+ * Deliberately a HISTORICAL CONSTANT, not derived from the loaded bundled
54
+ * manifests: bundled tools fail CLOSED in {@link registerFirstPartyTools}
55
+ * (every load failure throws before the warning could run), so "bundled
56
+ * manifests absent from the registry" is structurally empty — a derived list
57
+ * could never fire. The only drift this diagnostic exists to catch is an edit
58
+ * to the package list itself, which is precisely what a derivation would
59
+ * follow rather than flag. Co-located with the package list (one module owns
60
+ * both encodings) so an editor of either sees the other. `graph` is correctly
61
+ * absent: it never scaffolded (`pluginLayout` undefined).
62
+ */
63
+ export const EXPECTED_SCAFFOLDING_TOOL_IDS = ['fitness', 'simulation'];
64
+ /** Used to resolve the bundled engine package dirs from the CLI's own module graph. */
65
+ const requireFromHere = createRequire(import.meta.url);
66
+ /**
67
+ * Resolve a bundled tool's PACKAGE DIR — the directory whose `package.json`
68
+ * carries the `opensipTools` manifest.
69
+ *
70
+ * The `./package.json` subpath is not declared in each engine's `exports`,
71
+ * so `require.resolve('<pkg>/package.json')` throws. Instead we resolve the
72
+ * package's MAIN entry (a bare-name resolve, always permitted by `exports`)
73
+ * and walk up to the nearest ancestor directory that has a `package.json`
74
+ * whose `name` matches `packageName`. That ancestor IS the tool's own
75
+ * package dir under both the source layout and pnpm's workspace-injected
76
+ * `node_modules` layout (verified against fitness/simulation/graph here).
77
+ *
78
+ * @returns the resolved package directory, or `undefined` when the package
79
+ * cannot be resolved (should never happen for a bundled direct dep).
80
+ */
81
+ function resolveBundledPackageDir(packageName) {
82
+ let resolvedEntry;
83
+ try {
84
+ resolvedEntry = requireFromHere.resolve(packageName);
85
+ }
86
+ catch (error) {
87
+ // A bundled direct dep failing to resolve is a packaging fault — log it
88
+ // so the subsequent fail-closed throw is diagnosable, then signal the
89
+ // unresolved state to the caller (which raises PluginIncompatibleError).
90
+ logger.debug({
91
+ evt: 'cli.tool.bundled_unresolved',
92
+ module: BOOTSTRAP_MODULE,
93
+ packageName,
94
+ error: error instanceof Error ? error.message : String(error),
95
+ });
96
+ return undefined;
97
+ }
98
+ let dir = dirname(resolvedEntry);
99
+ for (let i = 0; i < 50; i++) {
100
+ const pkgPath = join(dir, 'package.json');
101
+ if (existsSync(pkgPath)) {
102
+ try {
103
+ const json = JSON.parse(readFileSync(pkgPath, 'utf8'));
104
+ if (json.name === packageName)
105
+ return dir;
106
+ }
107
+ catch {
108
+ // @swallow-ok unreadable package.json on the walk-up — keep climbing.
109
+ }
110
+ }
111
+ const parent = dirname(dir);
112
+ if (parent === dir)
113
+ break;
114
+ dir = parent;
115
+ }
116
+ return undefined;
117
+ }
118
+ /**
119
+ * Resolve a bundled tool package's on-disk directory, requiring success.
120
+ *
121
+ * @throws {PluginIncompatibleError} when the package directory cannot be
122
+ * resolved on disk (its manifest is unreadable).
123
+ */
124
+ function resolveRequiredBundledPackageDir(packageName) {
125
+ const dir = resolveBundledPackageDir(packageName);
126
+ if (dir !== undefined)
127
+ return dir;
128
+ throw new PluginIncompatibleError(`bundled tool '${packageName}' could not be resolved on disk; its manifest is unreadable`, { diagnostic: 'package directory not resolvable' });
129
+ }
130
+ // The runtime-load primitive (`importToolRuntime` + `ToolRuntimeLoad`) and the
131
+ // full admission SEQUENCE (`admitToolPackage`) live in `admit-tool-package.ts`
132
+ // (ADR-0041: one validator, four consumers). This file keeps the per-source
133
+ // POLICY: bundled fails closed below; the installed/authored legs skip with
134
+ // diagnostics.
135
+ /**
136
+ * Register the bundled first-party tools into the supplied registry, each one
137
+ * flowing through the SAME admit → dynamic-import → register path the external
138
+ * path uses (launch cutover — replaces the static-import + gate path).
139
+ *
140
+ * Per package name: `resolveBundledPackageDir` → `loadToolManifest('bundled')`
141
+ * → `admitTool({ source: 'bundled', explicitlyRequested: true })` →
142
+ * `importToolRuntime` (dynamic import + shape validation) → drift guard →
143
+ * `registry.register`. A bundled tool ships with the CLI, so it is always
144
+ * explicitly present: a missing/incompatible manifest or a runtime that fails
145
+ * to load is FAIL-CLOSED (never a silent skip). The recorded `ToolProvenance`
146
+ * (source `'bundled'`, trusted-by-shipping) and manifest are pushed onto the
147
+ * optional collectors so the composition root can surface provenance
148
+ * (`plugin list`) and seed the per-run capability registry (§5.3).
149
+ *
150
+ * @param registry The per-invocation tool registry to populate.
151
+ * @param provenance Optional sink for the admitted tools' provenance records.
152
+ * @param manifests Optional sink for the admitted tools' manifests (§5.3).
153
+ * @param packages The bundled package names to load (defaults to
154
+ * {@link BUNDLED_TOOL_PACKAGES}; injectable so the fail-closed paths are
155
+ * testable with fixture packages).
156
+ * @throws {PluginIncompatibleError} when a bundled tool cannot be resolved,
157
+ * has no conformant manifest, is out of range, or its runtime fails to load
158
+ * — mapped to `EXIT_CODES.PLUGIN_INCOMPATIBLE` (exit 5) by the CLI boundary.
159
+ */
160
+ export async function registerFirstPartyTools(registry, provenance = [], manifests = [], packages = BUNDLED_TOOL_PACKAGES) {
161
+ for (const packageName of packages) {
162
+ const dir = resolveRequiredBundledPackageDir(packageName);
163
+ // The shared admission SEQUENCE (ADR-0041). The bundled POLICY below maps
164
+ // each failed section to the exact fail-closed error this path always
165
+ // threw — a bundled tool ships with the CLI, so every failure is a
166
+ // packaging fault, never a silent skip.
167
+ const report = await admitToolPackage({
168
+ dir,
169
+ source: 'bundled',
170
+ packageName,
171
+ // A bundled tool ships with the CLI; it is always explicitly present,
172
+ // so an incompatible manifest fails the run rather than skipping.
173
+ explicitlyRequested: true,
174
+ });
175
+ if (!report.ok) {
176
+ // @fitness-ignore-next-line detached-promises -- synchronous never-returning thrower; the heuristic mistakes the bare call for an unawaited promise
177
+ throwBundledAdmissionFailure(packageName, report);
178
+ }
179
+ /* v8 ignore next 3 -- throwBundledAdmissionFailure never returns on a failed report; this guard narrows types */
180
+ if (report.tool === undefined ||
181
+ report.provenance === undefined ||
182
+ report.manifest === undefined) {
183
+ throw new PluginIncompatibleError(`bundled tool '${packageName}' produced an incomplete admission report`, { diagnostic: 'incomplete admission report' });
184
+ }
185
+ registry.register(report.tool);
186
+ provenance.push(report.provenance);
187
+ // Record the manifest so the pre-action-hook can register this tool's
188
+ // declared capability domains into the per-run capability registry
189
+ // (launch, §5.3).
190
+ manifests.push(report.manifest);
191
+ }
192
+ }
193
+ /**
194
+ * The bundled FAIL-CLOSED policy: convert a failed {@link AdmissionReport}
195
+ * into the same `PluginIncompatibleError` (message + diagnostic) the inline
196
+ * pipeline threw before the ADR-0041 factoring. Never returns.
197
+ *
198
+ * @throws {PluginIncompatibleError} always (or rethrows the original
199
+ * coherence error from `assertManifestMatchesTool`, preserving its type).
200
+ */
201
+ function throwBundledAdmissionFailure(packageName, report) {
202
+ const failed = report.sections.find((s) => !s.ok);
203
+ const failedSection = failed?.section;
204
+ if (failedSection === 'manifest') {
205
+ throw new PluginIncompatibleError(`bundled tool '${packageName}' has no conformant package.json#opensipTools manifest`, { diagnostic: 'manifest missing or malformed' });
206
+ }
207
+ const id = report.manifest?.id ?? packageName;
208
+ if (failedSection === 'compatibility') {
209
+ if (report.compatibilityDecision === 'fail-closed') {
210
+ throw new PluginIncompatibleError(`bundled tool '${id}' is incompatible: ${failed?.diagnostic ?? 'compatibility gate rejected it'}`, { diagnostic: failed?.diagnostic });
211
+ }
212
+ if (report.compatibilityDecision === 'skip') {
213
+ // Should not happen for an in-range bundled tool, but never silently
214
+ // drop a bundled tool — surface it loudly.
215
+ throw new PluginIncompatibleError(`bundled tool '${id}' was skipped by the compatibility gate: ${failed?.diagnostic ?? 'unknown reason'}`, { diagnostic: failed?.diagnostic });
216
+ }
217
+ throw new PluginIncompatibleError(`bundled tool '${id}' reached an unknown admission decision`, { diagnostic: 'unknown admission decision' });
218
+ }
219
+ if (failedSection === 'runtime-load' || failedSection === 'tool-shape') {
220
+ const reason = report.runtimeLoadReason ?? 'import-failed';
221
+ const detailSuffix = report.runtimeLoadDetail ? `: ${report.runtimeLoadDetail}` : '';
222
+ throw new PluginIncompatibleError(`bundled tool '${id}' failed to load via the plugin path (${reason}${detailSuffix})`, { diagnostic: `bundled tool runtime load failed: ${reason}` });
223
+ }
224
+ if (failedSection === 'manifest-runtime-coherence' && report.coherenceError instanceof Error) {
225
+ // Preserve the original drift-guard error type + message untouched.
226
+ // (assertManifestMatchesTool always throws Error subclasses; the
227
+ // instanceof narrowing satisfies only-throw-error without a cast.)
228
+ throw report.coherenceError;
229
+ }
230
+ /* v8 ignore next 4 -- defensive: a failed report always carries a failed section */
231
+ throw new PluginIncompatibleError(`bundled tool '${packageName}' failed admission for an unknown reason`, { diagnostic: 'unknown admission failure' });
232
+ }
233
+ /**
234
+ * Build the ordered tool-discovery sources. Order is precedence
235
+ * (first-occurrence-wins on duplicate name):
236
+ *
237
+ * 1. project-local `.runtime/plugins/tool` — `plugin add --project`
238
+ * 2. project tree (walk up from cwd) — plain `npm install @tool`
239
+ * 3. user-global `~/.opensip-cli/plugins/tool` — `plugin add` (default)
240
+ * 4. CLI install dir (walk up) — `npm i -g @tool`
241
+ *
242
+ * A project-local pin therefore shadows a user-global install of the same
243
+ * tool. Project-root resolution is best-effort: an unresolvable context
244
+ * (e.g. running outside any project) simply contributes no `.runtime`
245
+ * source.
246
+ */
247
+ export function buildToolDiscoverySources(cwd, cliInstallDir) {
248
+ const sources = [];
249
+ try {
250
+ const project = resolveProjectContext({ cwd, cwdExplicit: false });
251
+ if (project.scope === 'project') {
252
+ sources.push({
253
+ dir: resolveProjectPaths(project.projectRoot).pluginsDir('tool'),
254
+ mode: 'scanDir',
255
+ });
256
+ }
257
+ }
258
+ catch {
259
+ // @swallow-ok no resolvable project context (e.g. running outside any
260
+ // project) → contribute no project-local tool source. Best-effort by
261
+ // documented contract; see the JSDoc on buildToolDiscoverySources.
262
+ }
263
+ sources.push({ dir: cwd, mode: 'walkUp' }, { dir: resolveUserPaths().pluginsDir('tool'), mode: 'scanDir' }, { dir: cliInstallDir, mode: 'walkUp' });
264
+ return sources;
265
+ }
266
+ /**
267
+ * Run the admission gate over a discovered INSTALLED tool package
268
+ * before its module is imported. Reads the static
269
+ * `package.json#opensipTools` manifest and runs the shared `admitTool` gate
270
+ * (source `'installed'`, best-effort `explicitlyRequested: false` so an
271
+ * incompatible installed tool skips rather than failing the whole CLI).
272
+ *
273
+ * Returns:
274
+ * - `undefined` — skip this package (no conformant manifest, gate skipped
275
+ * it, or its id collides with a built-in). The reason is logged.
276
+ * - the admission `{ provenance, manifest }` — the manifest is conformant +
277
+ * compatible; the caller continues to import + register, and records the
278
+ * provenance/manifest only AFTER the runtime actually registered (so
279
+ * `plugin list` and the capability registry never see a tool whose import
280
+ * subsequently failed — matching the bundled/authored legs).
281
+ *
282
+ * Public launch: the grace window ended. A discovered `kind:'tool'` package whose
283
+ * manifest is missing/malformed (`loadToolManifest` → undefined) or declares no
284
+ * `apiVersion` (`admitTool` → skip via {@link checkCompatibility}) is no longer
285
+ * admitted off the marker alone — it is rejected with a diagnostic.
286
+ */
287
+ function admitInstalledTool(pkg, builtInIds) {
288
+ const manifest = loadToolManifest('installed', pkg.packageDir);
289
+ if (manifest === undefined) {
290
+ // Launch: a discovered tool with no conformant manifest is no longer admitted
291
+ // off the `kind:'tool'` marker alone (the grace window ended) — skip it.
292
+ process.stderr.write(`opensip: tool package ${pkg.name} has no conformant package.json#opensipTools manifest — skipping\n`);
293
+ logger.warn({
294
+ evt: 'cli.tool.manifest_invalid',
295
+ module: BOOTSTRAP_MODULE,
296
+ name: pkg.name,
297
+ });
298
+ return undefined;
299
+ }
300
+ if (builtInIds.has(manifest.id))
301
+ return undefined; // builtInIds are human ids from manifests (compat)
302
+ const result = admitTool({
303
+ manifest,
304
+ source: 'installed',
305
+ dir: pkg.packageDir,
306
+ packageName: pkg.name,
307
+ // Best-effort: discovery alone can't tell whether THIS run targets this
308
+ // tool's command, so default false → incompatible installed tools skip.
309
+ explicitlyRequested: false,
310
+ });
311
+ if (result.decision !== 'admit')
312
+ return undefined;
313
+ return { provenance: result.provenance, manifest: result.manifest };
314
+ }
315
+ /**
316
+ * Discover and register third-party tool packages from npm — any
317
+ * `package.json` declaring `opensipTools.kind === 'tool'`. Built-in
318
+ * ids are skipped to avoid double-registration warnings. Discovery spans
319
+ * the supplied sources (the user-global tool host dir, the project tree +
320
+ * its `.runtime` tool host dir, and the CLI install dir — see
321
+ * {@link buildToolDiscoverySources}).
322
+ *
323
+ * Each discovered package runs through the SAME `admitTool` gate the
324
+ * bundled path uses (launch, Phase 3) BEFORE its module is imported:
325
+ * the static `package.json#opensipTools` manifest is read with source
326
+ * `'installed'`, the compatibility gate runs, and only an `admit` verdict
327
+ * proceeds to import + register. An installed tool is best-effort
328
+ * `explicitlyRequested: false`, so an incompatible one `skip`s (logged)
329
+ * rather than failing the whole CLI — a stray incompatible plugin must not
330
+ * take fit/graph/sim down. Admitted tools' `ToolProvenance` is pushed onto
331
+ * the optional `provenance` collector for Phase 4's `plugin list`.
332
+ *
333
+ * @param provenance Optional sink for admitted tools' provenance records.
334
+ */
335
+ /**
336
+ * Emit the best-effort stderr line + structured warning for a discovered
337
+ * INSTALLED tool whose runtime failed to load. Each `ToolRuntimeLoad` failure
338
+ * reason maps to its own message + event (preserving the admission diagnostics) —
339
+ * an installed tool's load failure skips it, never crashing the CLI.
340
+ */
341
+ function emitInstalledLoadFailure(name, load) {
342
+ if (load.reason === 'no-entry') {
343
+ process.stderr.write(`opensip: tool package ${name} has no resolvable entry point — skipping\n`);
344
+ logger.warn({ evt: 'cli.tool.no_entry', module: BOOTSTRAP_MODULE, name });
345
+ return;
346
+ }
347
+ if (load.reason === 'invalid-shape') {
348
+ process.stderr.write(`opensip: tool package ${name} does not export a valid \`tool\` — skipping\n`);
349
+ logger.warn({
350
+ evt: 'cli.tool.invalid_shape',
351
+ module: BOOTSTRAP_MODULE,
352
+ name,
353
+ });
354
+ return;
355
+ }
356
+ process.stderr.write(`opensip: failed to load tool ${name}: ${load.detail ?? 'import failed'}\n`);
357
+ logger.warn({
358
+ evt: 'cli.tool.load_failed',
359
+ module: BOOTSTRAP_MODULE,
360
+ name,
361
+ error: load.detail,
362
+ });
363
+ }
364
+ export async function discoverAndRegisterToolPackages(registry, opts, builtInIds, provenance = [], manifests = []) {
365
+ // `builtInIds` is the set of already-registered bundled-tool *human ids* (manifest.id)
366
+ // to skip on a name collision (launch — passed explicitly by the composition root, which
367
+ // derives it from the bundled MANIFESTS it just loaded; compare against runtime
368
+ // metadata.name for the human key).
369
+ const discovered = discoverToolPackagesFromAnchors(opts.sources);
370
+ for (const pkg of discovered) {
371
+ try {
372
+ // Compatibility gate BEFORE import (launch). `undefined` means the
373
+ // gate skipped it (or it's a built-in id); an admission means import +
374
+ // register as before.
375
+ const admission = admitInstalledTool(pkg, builtInIds);
376
+ if (admission === undefined)
377
+ continue;
378
+ // Load the runtime through the SHARED dynamic-import path (launch) — the
379
+ // same `importToolRuntime` the bundled path uses. Resolves the entry
380
+ // from `packageDir` so a tool living in a host dir off the CLI's own
381
+ // module-resolution path still loads. An installed tool is best-effort:
382
+ // any load failure skips-with-diagnostic (it must not take fit/graph/sim
383
+ // down), in contrast to the bundled path's fail-closed.
384
+ const load = await importToolRuntime(pkg.packageDir);
385
+ if (!load.ok) {
386
+ emitInstalledLoadFailure(pkg.name, load);
387
+ continue;
388
+ }
389
+ // builtInIds holds human ids (from bundled manifests); compare against runtime human name
390
+ if (builtInIds.has(load.tool.metadata.name ?? load.tool.metadata.id))
391
+ continue;
392
+ // Drift guard — the SAME manifest⇔runtime identity check the bundled and
393
+ // authored legs run. For an installed tool a mismatch throws into the
394
+ // surrounding catch (skip-with-diagnostic posture), never crashing the CLI.
395
+ assertManifestMatchesTool(admission.manifest, load.tool);
396
+ registry.register(load.tool, { sourcePackage: pkg.name });
397
+ // Record provenance + manifest only now that the tool actually
398
+ // registered — `plugin list` and the per-run capability registry must
399
+ // never include a tool whose runtime failed to load (parity with the
400
+ // bundled/authored legs, which also record after registration).
401
+ provenance.push(admission.provenance);
402
+ manifests.push(admission.manifest);
403
+ }
404
+ catch (error) {
405
+ const msg = error instanceof Error ? error.message : String(error);
406
+ process.stderr.write(`opensip: failed to load tool ${pkg.name}: ${msg}\n`);
407
+ logger.warn({
408
+ evt: 'cli.tool.load_failed',
409
+ module: BOOTSTRAP_MODULE,
410
+ name: pkg.name,
411
+ error: msg,
412
+ });
413
+ }
414
+ }
415
+ }
416
+ /**
417
+ * The shared admission TAIL for both authored sources.
418
+ * When `preloadedManifest` is supplied we use that snapshot (no re-read) so a
419
+ * prior trust decision (project-local) and the compat gate see the identical
420
+ * declaration. This removes the TOCTOU between the trust-bearing read and the
421
+ * gate read for the deny-by-default authored path.
422
+ *
423
+ * @throws {PluginIncompatibleError} when the sidecar is missing/malformed or
424
+ * the tool is compatibility-incompatible.
425
+ */
426
+ function admitAuthoredTool(source, dir, preloadedManifest) {
427
+ // When a preloaded manifest is supplied (project-local trust path), we use
428
+ // that exact snapshot for the compat gate. This eliminates a TOCTOU between
429
+ // the read that decided "this id is allowlisted" and the read that feeds the
430
+ // compatibility check. The later dynamic import of the .mjs still sees
431
+ // whatever is on disk at execution time (inherent for authored code).
432
+ const rawManifest = preloadedManifest ?? loadToolManifest(source, dir);
433
+ if (rawManifest === undefined) {
434
+ throw new PluginIncompatibleError(`${source} tool at '${dir}' has no conformant ${PROJECT_LOCAL_MANIFEST_FILE} sidecar`, { diagnostic: 'manifest missing or malformed' });
435
+ }
436
+ const result = admitTool({
437
+ manifest: rawManifest,
438
+ source,
439
+ dir,
440
+ // An authored tool (placed in the project tree or the user's home dir) was
441
+ // explicitly authored by the user, so an incompatible one fails the run
442
+ // rather than skipping silently.
443
+ explicitlyRequested: true,
444
+ });
445
+ if (result.decision !== 'admit') {
446
+ throw new PluginIncompatibleError(`${source} tool '${rawManifest.id}' is incompatible: ${result.diagnostic ?? 'compatibility gate rejected it'}`, { diagnostic: result.diagnostic });
447
+ }
448
+ return { provenance: result.provenance, manifest: result.manifest };
449
+ }
450
+ /**
451
+ * Admit (or reject) a single PROJECT-LOCAL authored tool under the
452
+ * deny-by-default trust policy (launch, Phase 3 Task 3.2; wired into
453
+ * production discovery in the launch contract).
454
+ *
455
+ * A project-local tool is authored code under
456
+ * `<project>/opensip-cli/tools/<name>/` declaring its identity via a JSON
457
+ * sidecar (`opensip-tool.manifest.json`). It is read + gated WITHOUT importing
458
+ * its module:
459
+ *
460
+ * 1. `loadToolManifest('project-local', dir)` — identity only, no code run.
461
+ * 2. Trust check — {@link isProjectLocalToolTrusted}. Not allowlisted ⇒
462
+ * throw {@link PluginIncompatibleError} (fail-closed, exit 5) before any
463
+ * import. Allowlisted ⇒ run the shared compatibility tail; an incompatible
464
+ * explicitly-trusted tool is likewise fail-closed.
465
+ *
466
+ * Returns the admitted tool's `{ provenance, manifest }` on success. The trust
467
+ * decision always precedes import (it is the FIRST statement here, ahead of the
468
+ * shared {@link admitAuthoredTool} tail).
469
+ *
470
+ * @throws {PluginIncompatibleError} when the tool has no conformant sidecar
471
+ * manifest, is not allowlisted, or is compatibility-incompatible.
472
+ */
473
+ export function admitProjectLocalTool(args) {
474
+ // Trust decision FIRST — deny-by-default, before any compatibility maths
475
+ // and (critically) before the tool's module could ever be imported. The id
476
+ // is read from the sidecar identity, so load the manifest once here for the
477
+ // trust check, then hand the same dir to the shared tail.
478
+ const manifest = loadToolManifest('project-local', args.dir);
479
+ if (manifest === undefined) {
480
+ throw new PluginIncompatibleError(`project-local tool at '${args.dir}' has no conformant ${PROJECT_LOCAL_MANIFEST_FILE} sidecar`, { diagnostic: 'manifest missing or malformed' });
481
+ }
482
+ if (!isProjectLocalToolTrusted(manifest.id, args.env)) {
483
+ throw new PluginIncompatibleError(`project-local tool '${manifest.id}' is not trusted to load (deny-by-default). ` +
484
+ `Allowlist it via OPENSIP_CLI_ALLOW_PROJECT_TOOLS='${manifest.id}' to admit it.`, { diagnostic: 'project-local tool not allowlisted (deny-by-default)' });
485
+ }
486
+ // Pass the manifest we just loaded (and whose id we just trusted) so the
487
+ // compat gate in the tail sees the identical declaration. Closes the
488
+ // read-re-read TOCTOU for the deny-by-default path.
489
+ return admitAuthoredTool('project-local', args.dir, manifest);
490
+ }
491
+ /**
492
+ * Admit a single USER-GLOBAL authored tool — the trusted-by-default sibling of
493
+ * {@link admitProjectLocalTool}.
494
+ *
495
+ * A user-global tool is an authored sidecar under
496
+ * `~/.opensip-cli/tools/<name>/`. The user deliberately placed it in their
497
+ * own home dir (the `npm i -g` analogue for authored code), so there is **no
498
+ * allowlist gate** — it is trusted-by-default. It still reads the static
499
+ * sidecar and runs `admitTool` BEFORE the module could be imported (the shared
500
+ * {@link admitAuthoredTool} tail), so trust-before-import holds for this leg
501
+ * too: a global tool the user explicitly authored is fail-closed on a
502
+ * missing/incompatible manifest, never a silent skip.
503
+ *
504
+ * @throws {PluginIncompatibleError} when the tool has no conformant sidecar
505
+ * manifest or is compatibility-incompatible.
506
+ */
507
+ export function admitUserGlobalTool(args) {
508
+ // No trust gate — `user-global` is trusted-by-shipping-into-$HOME.
509
+ return admitAuthoredTool('user-global', args.dir);
510
+ }
511
+ /**
512
+ * Discover + admit + register AUTHORED Tool sidecars from the two authored
513
+ * roots, then dynamic-import each admitted runtime through the shared
514
+ * `importToolRuntime` seam — the same admit → import → register path the
515
+ * bundled and installed legs travel (ADR-0027; this is the leg that makes the
516
+ * dormant {@link admitProjectLocalTool} live).
517
+ *
518
+ * Two roots, two trust postures:
519
+ * - **global** (`~/.opensip-cli/tools/`) → {@link admitUserGlobalTool},
520
+ * trusted-by-default.
521
+ * - **project** (`<project>/opensip-cli/tools/`) → {@link admitProjectLocalTool},
522
+ * deny-by-default (allowlist via `OPENSIP_CLI_ALLOW_PROJECT_TOOLS`).
523
+ *
524
+ * Global is processed FIRST so a project-authored tool cannot shadow a same-id
525
+ * global one — matching the `~/.opensip-cli/plugins` precedence note in
526
+ * {@link buildToolDiscoverySources} (first-writer-wins via the registry).
527
+ * `builtInIds` are skipped so an authored tool never shadows a bundled one.
528
+ *
529
+ * **Trust-before-import.** For each candidate, the admit step (which EMBEDS the
530
+ * trust decision — deny-by-default inside `admitProjectLocalTool`) runs to
531
+ * completion BEFORE `importToolRuntime`. A non-allowlisted project tool THROWS
532
+ * `PluginIncompatibleError` (exit 5) here, propagated out of the walk: it must
533
+ * fail the run loudly — that is the clone-protection contract.
534
+ *
535
+ * **Error-posture asymmetry (deliberate).** An un-allowlisted *project* tool is
536
+ * fail-closed by policy (clone-risk; the user must opt in). A *global* tool that
537
+ * fails to load is also fail-closed (the user explicitly authored it into
538
+ * `$HOME`). This differs from the *installed* npm leg, where a stray bad plugin
539
+ * skips-with-diagnostic so it can't take fit/graph/sim down — authored tools are
540
+ * first-party-intent, installed tools are ambient.
541
+ *
542
+ * @param registry The per-invocation tool registry to populate.
543
+ * @param opts.projectAuthoredDir `resolveProjectPaths(root).authoredToolsDir`,
544
+ * or `undefined` when there is no resolvable project context.
545
+ * @param opts.globalAuthoredDir `resolveUserPaths().authoredToolsDir`.
546
+ * @param opts.env Environment carrying the project allowlist (default
547
+ * `process.env`); injectable for tests.
548
+ * @param builtInIds Bundled-tool ids to skip on a name collision.
549
+ * @param provenance Sink for admitted authored tools' provenance records.
550
+ * @param manifests Sink for admitted authored tools' manifests (§5.3).
551
+ * @throws {PluginIncompatibleError} for an un-allowlisted project tool, or any
552
+ * authored tool whose sidecar/runtime is missing/incompatible (fail-closed).
553
+ */
554
+ export async function discoverAndRegisterAuthoredTools(registry, opts, builtInIds, provenance = [], manifests = []) {
555
+ // Global FIRST (trusted-by-default), then project (deny-by-default).
556
+ for (const candidate of discoverAuthoredToolSidecars(opts.globalAuthoredDir)) {
557
+ await admitAndRegisterAuthored({
558
+ registry,
559
+ admission: admitUserGlobalTool({ dir: candidate.dir }),
560
+ dir: candidate.dir,
561
+ builtInIds,
562
+ provenance,
563
+ manifests,
564
+ });
565
+ }
566
+ if (opts.projectAuthoredDir !== undefined) {
567
+ for (const candidate of discoverAuthoredToolSidecars(opts.projectAuthoredDir)) {
568
+ // admitProjectLocalTool embeds the deny-by-default trust gate; a
569
+ // non-allowlisted tool THROWS here, BEFORE importToolRuntime below.
570
+ await admitAndRegisterAuthored({
571
+ registry,
572
+ admission: admitProjectLocalTool({ dir: candidate.dir, env: opts.env }),
573
+ dir: candidate.dir,
574
+ builtInIds,
575
+ provenance,
576
+ manifests,
577
+ });
578
+ }
579
+ }
580
+ }
581
+ /**
582
+ * Shared register-step for an already-ADMITTED authored tool: skip a
583
+ * built-in-id collision, dynamic-import the runtime (fail-closed on failure —
584
+ * an authored tool is first-party-intent), run the manifest⇔runtime drift
585
+ * guard, register, and record provenance + manifest. Admission (incl. the trust
586
+ * decision) has already happened by the time this is called — so import here
587
+ * never precedes a trust decision.
588
+ *
589
+ * @throws {PluginIncompatibleError} when the authored tool's runtime fails to
590
+ * load via the plugin path — an authored tool is first-party-intent, so a
591
+ * load failure is fail-closed (surfaced), never silently skipped.
592
+ */
593
+ async function admitAndRegisterAuthored(args) {
594
+ const { registry, admission, dir, builtInIds, provenance, manifests } = args;
595
+ const { provenance: prov, manifest } = admission;
596
+ // Never shadow a bundled tool (defense in depth; the registry also dedupes).
597
+ if (builtInIds.has(prov.id))
598
+ return;
599
+ const load = await importToolRuntime(dir);
600
+ if (!load.ok) {
601
+ const detailSuffix = load.detail ? `: ${load.detail}` : '';
602
+ throw new PluginIncompatibleError(`${prov.source} tool '${prov.id}' failed to load via the plugin path (${load.reason}${detailSuffix})`, { diagnostic: `authored tool runtime load failed: ${load.reason}` });
603
+ }
604
+ // Drift guard: the static sidecar and the runtime tool are two declarations
605
+ // of the same identity — catch a sidecar that fell out of sync.
606
+ assertManifestMatchesTool(manifest, load.tool);
607
+ registry.register(load.tool);
608
+ provenance.push(prov);
609
+ manifests.push(manifest);
610
+ }
611
+ /**
612
+ * Walk the registry and mount each tool's commands onto `program`. This is
613
+ * **step 8** of the tool lifecycle (launch, §5.4) — see
614
+ * {@link runToolLifecycle}.
615
+ *
616
+ * Public launch: there is ONE command surface — the tool's declared `commandSpecs`,
617
+ * mounted by `mountCommandSpec`. `register()` and the raw-Commander `program`
618
+ * handle on the tool context are gone, so the host owns `program` and passes it
619
+ * in here (the tool never touches Commander). A tool with no `commandSpecs` is a
620
+ * mis-declaration: it contributes no commands, surfaced loudly via
621
+ * `cli.tool.no_command_surface`.
622
+ *
623
+ * Failures are isolated per tool — one tool whose spec fails to mount must not
624
+ * take the whole CLI down. The failure is logged + stderr-warned, then we
625
+ * continue with the next tool.
626
+ *
627
+ * @param registry The per-invocation tool registry to walk.
628
+ * @param program The root Commander program (host-owned; the composition root
629
+ * passes it — it is no longer reachable through the tool context, §8).
630
+ * @param ctx The per-invocation handler context (render/emit/scope — no program).
631
+ */
632
+ export function mountAllToolCommands(registry, program, ctx) {
633
+ for (const tool of registry.list()) {
634
+ try {
635
+ mountOneTool(program, tool, ctx);
636
+ }
637
+ catch (error) {
638
+ const msg = error instanceof Error ? error.message : String(error);
639
+ const human = tool.metadata.name ?? tool.metadata.id;
640
+ process.stderr.write(`opensip: tool ${human} failed to mount: ${msg}\n`);
641
+ logger.warn({
642
+ evt: 'cli.tool.register_failed',
643
+ module: BOOTSTRAP_MODULE,
644
+ toolId: tool.metadata.id, // stable UUID
645
+ toolName: human,
646
+ error: msg,
647
+ });
648
+ }
649
+ }
650
+ // ADR-0021: one shared help shape across every mounted command — uniform
651
+ // option/subcommand ordering and a docs footer — applied here (the single
652
+ // place that has walked every tool's commands) rather than per tool.
653
+ applySharedHelpConfiguration(program);
654
+ }
655
+ /**
656
+ * Mount ONE tool's commands from its declared `commandSpecs` — the only command
657
+ * surface (public launch). Extracted so {@link mountAllToolCommands} keeps its
658
+ * per-tool failure isolation around a single call. A tool with no `commandSpecs`
659
+ * contributes nothing and is surfaced via `cli.tool.no_command_surface`.
660
+ */
661
+ function mountOneTool(program, tool, ctx) {
662
+ if (tool.commandSpecs !== undefined && tool.commandSpecs.length > 0) {
663
+ for (const spec of tool.commandSpecs) {
664
+ // `Tool.commandSpecs` is `CommandSpec<unknown, ToolCliContext>[]`, which
665
+ // is assignable to the mounter's `HostCommandSpec` (handler contravariance
666
+ // — an `unknown`-opts handler accepts a `Record`-opts call). No cast.
667
+ mountCommandSpec(program, spec, ctx);
668
+ }
669
+ return;
670
+ }
671
+ // No declarative command surface — a mis-declared tool contributes no commands.
672
+ // Surface it rather than silently mounting nothing.
673
+ logger.warn({
674
+ evt: 'cli.tool.no_command_surface',
675
+ module: BOOTSTRAP_MODULE,
676
+ toolId: tool.metadata.id, // stable
677
+ toolName: tool.metadata.name ?? tool.metadata.id,
678
+ detail: 'tool declares no commandSpecs; no commands mounted',
679
+ });
680
+ }
681
+ const DOCS_HELP_FOOTER = '\nDocs: https://opensip.ai/docs/opensip-cli';
682
+ /**
683
+ * Apply one help configuration to the root program and every (sub)command:
684
+ * options + subcommands sort alphabetically so the help reads the same across
685
+ * `fit`/`graph`/`sim`, and the root help ends with a docs pointer (ADR-0021).
686
+ */
687
+ function applySharedHelpConfiguration(program) {
688
+ const configure = (cmd) => {
689
+ cmd.configureHelp({ sortOptions: true, sortSubcommands: true });
690
+ for (const sub of cmd.commands)
691
+ configure(sub);
692
+ };
693
+ configure(program);
694
+ program.addHelpText('after', DOCS_HELP_FOOTER);
695
+ }
696
+ //# sourceMappingURL=register-tools.js.map