@dsh-workbench/shared 0.3.10 → 1.0.0-beta.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 (391) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +97 -0
  3. package/dist/api/blocker-table.d.ts +3 -3
  4. package/dist/api/blocker-table.js +47 -23
  5. package/dist/api/dsh.d.ts +19 -1
  6. package/dist/api/host-write.d.ts +0 -15
  7. package/dist/api/host-write.js +1 -35
  8. package/dist/api/workbench-client.js +2 -1
  9. package/dist/components/Activity.css +0 -1
  10. package/dist/components/ApprovalDetail.css +1 -2
  11. package/dist/components/ApprovalDetail.d.ts +1 -1
  12. package/dist/components/ApprovalPlan.css +0 -1
  13. package/dist/components/Card.css +0 -1
  14. package/dist/components/Card.d.ts +1 -1
  15. package/dist/components/CardBoundary.d.ts +1 -1
  16. package/dist/components/CardHead.css +0 -1
  17. package/dist/components/CardHead.d.ts +1 -1
  18. package/dist/components/ChoiceGroup.css +1 -2
  19. package/dist/components/ConfirmBar.d.ts +1 -1
  20. package/dist/components/ConfirmBar.js +1 -1
  21. package/dist/components/Form.css +1 -1
  22. package/dist/components/FormActions.js +1 -1
  23. package/dist/components/LongTaskRow.css +1 -2
  24. package/dist/components/LongTaskRow.d.ts +1 -1
  25. package/dist/components/LongTaskRow.js +1 -1
  26. package/dist/components/Note.css +0 -1
  27. package/dist/components/ResourceStates.css +0 -1
  28. package/dist/components/SettingsSection.css +0 -1
  29. package/dist/components/Spinner.css +0 -1
  30. package/dist/components/Stack.css +0 -1
  31. package/dist/components/StateBlock.css +0 -1
  32. package/dist/components/board-module.d.ts +3 -3
  33. package/dist/components/icons.d.ts +1 -7
  34. package/dist/components/icons.js +11 -16
  35. package/dist/config/constants.d.ts +2 -2
  36. package/dist/config/constants.js +2 -2
  37. package/dist/config/storage.d.ts +0 -4
  38. package/dist/config/storage.js +0 -11
  39. package/dist/contract/adapters.d.ts +10 -0
  40. package/dist/contract/cli.d.ts +3 -0
  41. package/dist/contract/data.d.ts +126 -0
  42. package/dist/{api → contract}/facts.d.ts +12 -3
  43. package/dist/{api → contract}/facts.js +19 -1
  44. package/dist/contract/needs.d.ts +132 -0
  45. package/dist/contract/needs.js +232 -0
  46. package/dist/contract/plugin-source.d.ts +1 -0
  47. package/dist/contract/plugin-source.js +1 -0
  48. package/dist/contract/protocol/bundle-install.d.ts +136 -0
  49. package/dist/contract/protocol/bundle-install.js +12 -0
  50. package/dist/contract/protocol/config-sync.d.ts +55 -0
  51. package/dist/contract/protocol/config-sync.js +2 -0
  52. package/dist/contract/protocol/diy.d.ts +4 -0
  53. package/dist/contract/protocol/diy.js +4 -0
  54. package/dist/contract/protocol/dsh-plugins.d.ts +11 -0
  55. package/dist/contract/protocol/dsh-plugins.js +1 -0
  56. package/dist/contract/protocol/long-tasks.d.ts +13 -0
  57. package/dist/contract/protocol/long-tasks.js +1 -0
  58. package/dist/contract/protocol/market-index.d.ts +195 -0
  59. package/dist/contract/protocol/market-index.js +7 -0
  60. package/dist/contract/protocol/platform.d.ts +44 -0
  61. package/dist/contract/protocol/platform.js +1 -0
  62. package/dist/contract/protocol/preflight-wake.d.ts +7 -0
  63. package/dist/contract/protocol/preflight-wake.js +1 -0
  64. package/dist/contract/protocol/profile-create.d.ts +30 -0
  65. package/dist/contract/protocol/profile-create.js +2 -0
  66. package/dist/contract/protocol/sidebar-edition.d.ts +14 -0
  67. package/dist/contract/protocol/sidebar-edition.js +2 -0
  68. package/dist/contract/protocol/unit-manifests.d.ts +6 -0
  69. package/dist/contract/protocol/unit-manifests.js +1 -0
  70. package/dist/contract/unit-board.d.ts +18 -0
  71. package/dist/contract/unit-board.js +1 -0
  72. package/dist/contract/unit-capability.d.ts +34 -0
  73. package/dist/contract/unit-capability.js +1 -0
  74. package/dist/{api → contract}/unit-declaration.d.ts +7 -5
  75. package/dist/{api → contract}/unit-declaration.js +19 -13
  76. package/dist/contract/unit.d.ts +57 -0
  77. package/dist/contract/unit.js +24 -0
  78. package/dist/contract/workbench-contract.d.ts +15 -0
  79. package/dist/contract/workbench-contract.js +1 -0
  80. package/dist/host/adapter-set.d.ts +26 -0
  81. package/dist/host/adapter-set.js +243 -0
  82. package/dist/host/adapter-types.d.ts +79 -0
  83. package/dist/host/adapter-types.js +23 -0
  84. package/dist/host/adapters.d.ts +22 -0
  85. package/dist/host/adapters.js +9 -0
  86. package/dist/host/browser.d.ts +3 -0
  87. package/dist/host/browser.js +3 -0
  88. package/dist/host/changelog.d.ts +21 -0
  89. package/dist/host/changelog.js +188 -0
  90. package/dist/host/channel-plugin.d.ts +15 -0
  91. package/dist/host/channel-plugin.js +42 -0
  92. package/dist/host/cli.d.ts +4 -0
  93. package/dist/host/cli.js +7 -0
  94. package/dist/host/command-name.d.ts +1 -0
  95. package/dist/host/command-name.js +14 -0
  96. package/dist/host/data-error.d.ts +13 -0
  97. package/dist/host/data-error.js +20 -0
  98. package/dist/host/data-root.js +3 -0
  99. package/dist/host/diy-debug.js +2 -1
  100. package/dist/host/diy-record.js +2 -1
  101. package/dist/host/facts.d.ts +1 -1
  102. package/dist/host/facts.js +3 -2
  103. package/dist/host/git.js +2 -2
  104. package/dist/host/http.js +1 -1
  105. package/dist/host/lib-capabilities.d.ts +22 -0
  106. package/dist/host/lib-capabilities.js +37 -0
  107. package/dist/host/lib-install.d.ts +9 -0
  108. package/dist/host/lib-install.js +10 -0
  109. package/dist/host/long-task.d.ts +1 -1
  110. package/dist/host/long-task.js +4 -3
  111. package/dist/host/market-index.d.ts +22 -0
  112. package/dist/host/market-index.js +420 -0
  113. package/dist/host/needs.d.ts +4 -0
  114. package/dist/host/needs.js +2 -0
  115. package/dist/host/patch.d.ts +9 -0
  116. package/dist/host/patch.js +78 -0
  117. package/dist/host/paths.d.ts +12 -0
  118. package/dist/host/paths.js +28 -0
  119. package/dist/host/platform.d.ts +14 -0
  120. package/dist/host/platform.js +40 -0
  121. package/dist/host/plugin-source.d.ts +8 -6
  122. package/dist/host/plugin-source.js +43 -33
  123. package/dist/host/plugin.d.ts +4 -0
  124. package/dist/host/plugin.js +39 -0
  125. package/dist/host/port-types.d.ts +191 -0
  126. package/dist/host/port-types.js +1 -0
  127. package/dist/host/preflight.d.ts +28 -0
  128. package/dist/host/preflight.js +1 -0
  129. package/dist/host/profile-port.d.ts +4 -0
  130. package/dist/host/profile-port.js +4 -0
  131. package/dist/host/provision.d.ts +2 -2
  132. package/dist/host/provision.js +5 -3
  133. package/dist/host/recipe-rules.d.ts +5 -0
  134. package/dist/host/recipe-rules.js +382 -0
  135. package/dist/host/recipe.d.ts +14 -0
  136. package/dist/host/recipe.js +283 -0
  137. package/dist/host/shell-text.d.ts +9 -2
  138. package/dist/host/shell-text.js +15 -3
  139. package/dist/host/shell.d.ts +2 -0
  140. package/dist/host/shell.js +7 -0
  141. package/dist/host/spec.d.ts +8 -0
  142. package/dist/host/spec.js +24 -0
  143. package/dist/host/supply-index.d.ts +34 -0
  144. package/dist/host/supply-index.js +202 -0
  145. package/dist/host/supply-port.d.ts +5 -0
  146. package/dist/host/supply-port.js +39 -0
  147. package/dist/host/time.d.ts +12 -0
  148. package/dist/host/time.js +19 -0
  149. package/dist/host/tokens.d.ts +26 -0
  150. package/dist/host/tokens.js +154 -0
  151. package/dist/host/tool-approval.d.ts +12 -0
  152. package/dist/host/tool-approval.js +4 -0
  153. package/dist/host/tool-kit.js +1 -1
  154. package/dist/host/unit-face.d.ts +5 -4
  155. package/dist/host/version-range.d.ts +2 -0
  156. package/dist/host/version-range.js +73 -0
  157. package/dist/host/version.d.ts +3 -0
  158. package/dist/host/version.js +29 -0
  159. package/dist/host.d.ts +47 -16
  160. package/dist/host.js +47 -16
  161. package/dist/i18n/en.js +259 -86
  162. package/dist/i18n/index.d.ts +333 -88
  163. package/dist/i18n/merge.js +1 -1
  164. package/dist/i18n/need-text.d.ts +26 -0
  165. package/dist/i18n/need-text.js +26 -0
  166. package/dist/i18n/settings.d.ts +70 -0
  167. package/dist/i18n/settings.js +136 -0
  168. package/dist/i18n/zh.d.ts +332 -87
  169. package/dist/i18n/zh.js +260 -87
  170. package/dist/index.d.ts +24 -8
  171. package/dist/index.js +24 -8
  172. package/dist/lib/blocker-action.d.ts +0 -15
  173. package/dist/lib/blocker-action.js +1 -25
  174. package/dist/{api → lib}/dsh-client.d.ts +12 -1
  175. package/dist/lib/dsh-client.js +1 -0
  176. package/dist/lib/market-index.d.ts +7 -0
  177. package/dist/lib/market-index.js +25 -0
  178. package/dist/lib/react-types.d.ts +5 -0
  179. package/dist/lib/react-types.js +1 -0
  180. package/dist/lib/react.d.ts +1 -40
  181. package/dist/lib/react.js +0 -36
  182. package/dist/lib/ui.js +3 -9
  183. package/dist/testing/data.d.ts +13 -0
  184. package/dist/testing/data.js +46 -0
  185. package/dist/testing/declaration.d.ts +1 -1
  186. package/dist/testing/declaration.js +1 -1
  187. package/dist/testing/host-fakes.d.ts +0 -13
  188. package/dist/testing/host-fakes.js +0 -24
  189. package/dist/testing/needs.d.ts +16 -0
  190. package/dist/testing/needs.js +46 -0
  191. package/dist/testing.d.ts +2 -0
  192. package/dist/testing.js +2 -0
  193. package/package.json +18 -2
  194. package/src/api/__tests__/blocker-table.test.ts +135 -86
  195. package/src/api/blocker-table.ts +52 -26
  196. package/src/api/dsh.ts +17 -1
  197. package/src/api/host-write.ts +0 -43
  198. package/src/api/workbench-client.test.ts +2 -2
  199. package/src/api/workbench-client.ts +2 -1
  200. package/src/components/Activity.css +0 -1
  201. package/src/components/ApprovalDetail.css +1 -2
  202. package/src/components/ApprovalDetail.tsx +1 -1
  203. package/src/components/ApprovalPlan.css +0 -1
  204. package/src/components/Card.css +0 -1
  205. package/src/components/Card.tsx +1 -1
  206. package/src/components/CardAnchor.test.tsx +1 -1
  207. package/src/components/CardBoundary.tsx +1 -1
  208. package/src/components/CardHead.css +0 -1
  209. package/src/components/CardHead.tsx +1 -1
  210. package/src/components/ChoiceGroup.css +1 -2
  211. package/src/components/ConfirmBar.tsx +1 -1
  212. package/src/components/Form.css +1 -1
  213. package/src/components/Form.tsx +1 -1
  214. package/src/components/FormActions.tsx +1 -1
  215. package/src/components/LongTaskRow.css +1 -2
  216. package/src/components/LongTaskRow.tsx +1 -1
  217. package/src/components/Note.css +0 -1
  218. package/src/components/ResourceStates.css +0 -1
  219. package/src/components/SelectFieldNodes.tsx +0 -1
  220. package/src/components/SettingsSection.css +0 -1
  221. package/src/components/Spinner.css +0 -1
  222. package/src/components/Stack.css +0 -1
  223. package/src/components/StateBlock.css +0 -1
  224. package/src/components/__tests__/long-task-row.test.ts +1 -1
  225. package/src/components/board-module.ts +3 -3
  226. package/src/components/icons.tsx +16 -24
  227. package/src/config/constants.ts +2 -2
  228. package/src/config/storage.test.ts +1 -25
  229. package/src/config/storage.ts +0 -12
  230. package/src/{api → contract}/__tests__/facts.test.ts +73 -25
  231. package/src/{api → contract}/__tests__/long-task.test.ts +3 -3
  232. package/src/contract/__tests__/needs.test.ts +74 -0
  233. package/src/{host → contract}/__tests__/unit-declaration.test.ts +1 -1
  234. package/src/contract/__tests__/unit.test.ts +43 -0
  235. package/src/contract/adapters.ts +9 -0
  236. package/src/contract/cli.ts +3 -0
  237. package/src/contract/data.ts +145 -0
  238. package/src/{api → contract}/facts.ts +34 -4
  239. package/src/contract/needs.ts +331 -0
  240. package/src/contract/plugin-source.ts +1 -0
  241. package/src/contract/protocol/bundle-install.ts +198 -0
  242. package/src/contract/protocol/config-sync.ts +65 -0
  243. package/src/contract/protocol/diy.ts +4 -0
  244. package/src/contract/protocol/dsh-plugins.ts +13 -0
  245. package/src/contract/protocol/long-tasks.ts +16 -0
  246. package/src/contract/protocol/market-index.ts +246 -0
  247. package/src/contract/protocol/platform.ts +47 -0
  248. package/src/contract/protocol/preflight-wake.ts +17 -0
  249. package/src/contract/protocol/profile-create.ts +36 -0
  250. package/src/contract/protocol/sidebar-edition.ts +19 -0
  251. package/src/contract/protocol/unit-manifests.ts +8 -0
  252. package/src/contract/unit-board.ts +23 -0
  253. package/src/contract/unit-capability.ts +37 -0
  254. package/src/{host → contract}/unit-declaration.ts +22 -18
  255. package/src/contract/unit.ts +76 -0
  256. package/src/contract/workbench-contract.ts +30 -0
  257. package/src/host/__tests__/adapter-set.test.ts +228 -0
  258. package/src/host/__tests__/adapters.test.ts +27 -0
  259. package/src/host/__tests__/changelog.test.ts +102 -0
  260. package/src/host/__tests__/command-name.test.ts +16 -0
  261. package/src/host/__tests__/diy-record.test.ts +3 -3
  262. package/src/host/__tests__/facts.test.ts +5 -5
  263. package/src/host/__tests__/long-task.test.ts +1 -1
  264. package/src/host/__tests__/market-index.test.ts +179 -0
  265. package/src/host/__tests__/plugin-ledger.test.ts +10 -10
  266. package/src/host/__tests__/provision.test.ts +7 -6
  267. package/src/host/__tests__/shell-text.test.ts +16 -1
  268. package/src/host/__tests__/supply-index.test.ts +137 -0
  269. package/src/host/__tests__/tokens.test.ts +110 -0
  270. package/src/host/__tests__/version-range.test.ts +21 -0
  271. package/src/host/adapter-set.ts +298 -0
  272. package/src/host/adapter-types.ts +105 -0
  273. package/src/host/adapters.ts +44 -0
  274. package/src/host/browser.ts +3 -0
  275. package/src/host/changelog.ts +212 -0
  276. package/src/host/channel-plugin.ts +55 -0
  277. package/src/host/cli.ts +11 -0
  278. package/src/host/command-name.ts +15 -0
  279. package/src/host/data-error.ts +21 -0
  280. package/src/host/data-root.ts +2 -0
  281. package/src/host/diy-debug.ts +2 -1
  282. package/src/host/diy-record.ts +2 -1
  283. package/src/host/facts.ts +4 -3
  284. package/src/host/git.ts +2 -2
  285. package/src/host/http.ts +1 -1
  286. package/src/host/lib-capabilities.ts +52 -0
  287. package/src/host/lib-install.ts +18 -0
  288. package/src/host/long-task.ts +4 -3
  289. package/src/host/market-index.ts +413 -0
  290. package/src/host/needs.ts +6 -0
  291. package/src/host/patch.ts +93 -0
  292. package/src/host/paths.ts +37 -0
  293. package/src/host/platform.ts +48 -0
  294. package/src/host/plugin-source.ts +48 -56
  295. package/src/host/plugin.ts +39 -0
  296. package/src/host/port-types.ts +222 -0
  297. package/src/host/preflight.ts +31 -0
  298. package/src/host/profile-port.ts +9 -0
  299. package/src/host/provision.ts +7 -4
  300. package/src/host/recipe-rules.ts +330 -0
  301. package/src/host/recipe.test.ts +334 -0
  302. package/src/host/recipe.ts +323 -0
  303. package/src/host/shell-text.ts +15 -3
  304. package/src/host/shell.ts +9 -0
  305. package/src/host/spec.ts +32 -0
  306. package/src/host/supply-index.ts +256 -0
  307. package/src/host/supply-port.ts +45 -0
  308. package/src/host/time.ts +22 -0
  309. package/src/host/tokens.ts +186 -0
  310. package/src/host/tool-approval.ts +17 -0
  311. package/src/host/tool-kit.ts +1 -1
  312. package/src/host/unit-face.ts +5 -4
  313. package/src/host/version-range.ts +71 -0
  314. package/src/host/version.ts +30 -0
  315. package/src/host.ts +47 -16
  316. package/src/i18n/en.ts +261 -86
  317. package/src/i18n/merge.test.ts +6 -6
  318. package/src/i18n/merge.ts +1 -1
  319. package/src/i18n/need-text.ts +27 -0
  320. package/src/i18n/settings.ts +141 -0
  321. package/src/i18n/zh.ts +263 -87
  322. package/src/index.ts +24 -8
  323. package/src/lib/blocker-action.ts +0 -28
  324. package/src/{api → lib}/dsh-client.ts +16 -2
  325. package/src/lib/market-index.ts +36 -0
  326. package/src/lib/react-types.ts +5 -0
  327. package/src/lib/react.ts +1 -40
  328. package/src/lib/text.test.ts +2 -2
  329. package/src/lib/ui.test.ts +6 -5
  330. package/src/lib/ui.ts +3 -9
  331. package/src/testing/__tests__/declaration.test.ts +1 -1
  332. package/src/testing/data.ts +66 -0
  333. package/src/testing/declaration.ts +2 -2
  334. package/src/testing/host-fakes.ts +0 -38
  335. package/src/testing/needs.ts +80 -0
  336. package/src/testing.ts +2 -0
  337. package/dist/api/unit-board.d.ts +0 -39
  338. package/dist/api/unit-board.js +0 -45
  339. package/dist/api/unit.d.ts +0 -85
  340. package/dist/api/unit.js +0 -8
  341. package/dist/api/workbench-contract.d.ts +0 -44
  342. package/dist/components/MockStrip.css +0 -39
  343. package/dist/components/MockStrip.d.ts +0 -15
  344. package/dist/components/MockStrip.js +0 -8
  345. package/dist/components/SaveBar.css +0 -7
  346. package/dist/host/browser-find.d.ts +0 -38
  347. package/dist/host/browser-find.js +0 -242
  348. package/dist/host/browser-read.d.ts +0 -28
  349. package/dist/host/browser-read.js +0 -243
  350. package/dist/host/opencli-runtime.d.ts +0 -25
  351. package/dist/host/opencli-runtime.js +0 -56
  352. package/dist/host/opencli-window.d.ts +0 -2
  353. package/dist/host/opencli-window.js +0 -8
  354. package/dist/host/opencli.d.ts +0 -37
  355. package/dist/host/opencli.js +0 -155
  356. package/dist/host/unit-declaration.d.ts +0 -65
  357. package/dist/host/unit-declaration.js +0 -213
  358. package/dist/host/unit-libs.d.ts +0 -37
  359. package/dist/lib/diy-debug.d.ts +0 -80
  360. package/dist/lib/diy-debug.js +0 -88
  361. package/dist/lib/diy-record.d.ts +0 -135
  362. package/dist/lib/diy-record.js +0 -162
  363. package/dist/lib/lint-probe.d.ts +0 -1
  364. package/dist/lib/lint-probe.js +0 -6
  365. package/dist/lib/panel-state.d.ts +0 -120
  366. package/dist/lib/panel-state.js +0 -176
  367. package/src/api/__tests__/host-write.test.ts +0 -75
  368. package/src/api/__tests__/unit-board.test.ts +0 -76
  369. package/src/api/__tests__/unit.test.ts +0 -38
  370. package/src/api/unit-board.ts +0 -69
  371. package/src/api/unit.ts +0 -93
  372. package/src/api/workbench-contract.ts +0 -59
  373. package/src/host/__tests__/browser-find.test.ts +0 -330
  374. package/src/host/__tests__/browser-read.test.ts +0 -228
  375. package/src/host/__tests__/opencli-runtime.test.ts +0 -46
  376. package/src/host/browser-find.ts +0 -311
  377. package/src/host/browser-read.ts +0 -311
  378. package/src/host/opencli-runtime.ts +0 -79
  379. package/src/host/opencli-window.ts +0 -11
  380. package/src/host/opencli.ts +0 -213
  381. package/src/host/unit-libs.ts +0 -41
  382. package/src/lib/blocker-action.test.ts +0 -40
  383. /package/dist/{api/blocker.js → contract/adapters.js} +0 -0
  384. /package/dist/{api → contract}/blocker.d.ts +0 -0
  385. /package/dist/{api/dsh-client.js → contract/blocker.js} +0 -0
  386. /package/dist/{api/workbench-contract.js → contract/cli.js} +0 -0
  387. /package/dist/{host/unit-libs.js → contract/data.js} +0 -0
  388. /package/dist/{api → contract}/long-task.d.ts +0 -0
  389. /package/dist/{api → contract}/long-task.js +0 -0
  390. /package/src/{api → contract}/blocker.ts +0 -0
  391. /package/src/{api → contract}/long-task.ts +0 -0
@@ -0,0 +1,323 @@
1
+ import type { CallOverrides, RecipeAuth, RecipeCalls, RequestRecipe } from '../contract/data.js';
2
+ import { callsProblemOf, DEFAULT_RECIPE_MAX_BYTES, recipeProblemOf } from './recipe-rules.js';
3
+
4
+ export interface RecipeDescription {
5
+ readonly method: string;
6
+ readonly url: string;
7
+ readonly body?: unknown;
8
+ readonly auth?:
9
+ | {
10
+ readonly source: string;
11
+ readonly place: string;
12
+ readonly name?: string;
13
+ }
14
+ | undefined;
15
+ }
16
+
17
+
18
+ function authOf(recipe: RequestRecipe): RecipeAuth {
19
+ return recipe.auth ?? { source: { kind: 'cookie' }, place: { in: 'cookie' } };
20
+ }
21
+
22
+ function needsToken(auth: RecipeAuth): boolean {
23
+ return auth.kind !== 'none' && auth.source.kind !== 'cookie';
24
+ }
25
+
26
+ function mergedHeaders(recipe: RequestRecipe, overrides: CallOverrides): Record<string, string> {
27
+ return { ...(recipe.request.headers ?? {}), ...(overrides.headers ?? {}) };
28
+ }
29
+
30
+ function mergedQuery(recipe: RequestRecipe, overrides: CallOverrides): Record<string, string> {
31
+ const query: Record<string, string> = {};
32
+ for (const [name, value] of Object.entries(recipe.request.query ?? {})) query[name] = String(value);
33
+ for (const [name, value] of Object.entries(overrides.query ?? {})) query[name] = String(value);
34
+ return query;
35
+ }
36
+
37
+ function baseUrlOf(recipe: RequestRecipe, overrides: CallOverrides): string {
38
+ const path = overrides.path ?? recipe.request.path;
39
+ return path.startsWith('/') ? `${recipe.origin}${path}` : path;
40
+ }
41
+
42
+ function urlOf(recipe: RequestRecipe, overrides: CallOverrides): string {
43
+ const url = new URL(baseUrlOf(recipe, overrides));
44
+ for (const [name, value] of Object.entries(mergedQuery(recipe, overrides))) {
45
+ url.searchParams.set(name, value);
46
+ }
47
+ return url.toString();
48
+ }
49
+
50
+ function responseOf(
51
+ recipe: RequestRecipe,
52
+ overrides: CallOverrides,
53
+ ): { kind: 'json' | 'text'; path: string | undefined; maxBytes: number } {
54
+ const base = recipe.response;
55
+ const extra = overrides.response ?? {};
56
+ return {
57
+ kind: extra.kind ?? base?.kind ?? 'json',
58
+ path: extra.path ?? base?.path,
59
+ maxBytes: extra.maxBytes ?? base?.maxBytes ?? DEFAULT_RECIPE_MAX_BYTES,
60
+ };
61
+ }
62
+
63
+ function bodyOf(recipe: RequestRecipe, overrides: CallOverrides): unknown {
64
+ return overrides.body !== undefined ? overrides.body : recipe.request.body;
65
+ }
66
+
67
+ function quote(value: string): string {
68
+ return JSON.stringify(value);
69
+ }
70
+
71
+ function literal(value: unknown): string {
72
+ return JSON.stringify(value);
73
+ }
74
+
75
+ function encodedBody(value: unknown): string {
76
+ return typeof value === 'string' ? literal(value) : `JSON.stringify(${literal(value)})`;
77
+ }
78
+
79
+ function tokenLines(recipe: RequestRecipe, auth: RecipeAuth): string[] {
80
+ if (!needsToken(auth)) return [];
81
+ const source = auth.kind === 'none' ? undefined : auth.source;
82
+ if (source === undefined) return [];
83
+ if (source.kind === 'cookieValue') {
84
+ return [
85
+ ' const cookie = (name) => {',
86
+ ' const hit = document.cookie.split("; ").find((one) => one.startsWith(name + "="));',
87
+ ' return hit === undefined ? "" : decodeURIComponent(hit.slice(name.length + 1));',
88
+ ' };',
89
+ ` const token = cookie(${quote(source.name)});`,
90
+ ' if (token === "") return { ok: false, code: "auth-failed", message: "页面里没有这个 cookie" };',
91
+ ];
92
+ }
93
+ if (source.kind === 'meta') {
94
+ return [
95
+ ` const meta = document.querySelector(${quote(`meta[name="${source.name}"]`)});`,
96
+ ' const token = meta === null ? "" : (meta.getAttribute("content") || "");',
97
+ ' if (token === "") return { ok: false, code: "auth-failed", message: "页面里没有这个 meta" };',
98
+ ];
99
+ }
100
+ if (source.kind === 'global') {
101
+ return [
102
+ ` const token = pick(globalThis, ${quote(source.path)});`,
103
+ ' if (typeof token !== "string" || token === "") return { ok: false, code: "auth-failed", message: "页面里没有这个全局变量" };',
104
+ ];
105
+ }
106
+ if (source.kind === 'storage') {
107
+ const store = source.area === 'local' ? 'localStorage' : 'sessionStorage';
108
+ const path = source.path === undefined ? '' : source.path;
109
+ return [
110
+ ` const raw = ${store}.getItem(${quote(source.key)});`,
111
+ ' let parsed = raw;',
112
+ ' try { parsed = raw === null ? null : JSON.parse(raw); } catch (error) { parsed = raw; }',
113
+ path === ''
114
+ ? ' const token = parsed;'
115
+ : ` const token = pick(parsed, ${quote(path)});`,
116
+ ' if (typeof token !== "string" || token === "") return { ok: false, code: "auth-failed", message: "页面里没有这个 token" };',
117
+ ];
118
+ }
119
+ if (source.kind === 'endpoint') {
120
+ const method = source.method ?? 'GET';
121
+ const hasBody = source.body !== undefined;
122
+ const endpointUrl = source.url.startsWith('/') ? `${recipe.origin}${source.url}` : source.url;
123
+ const lines = [
124
+ ' let pre;',
125
+ ' try {',
126
+ ` pre = await fetch(${quote(endpointUrl)}, { method: ${quote(method)}, credentials: "include", headers: { "content-type": "application/json" }${hasBody ? `, body: ${encodedBody(source.body)}` : ''} });`,
127
+ ' } catch (error) {',
128
+ ' return { ok: false, code: "fetch-failed", message: String(error) };',
129
+ ' }',
130
+ ' if (pre.status === 401 || pre.status === 403) return { ok: false, code: "not-logged-in" };',
131
+ ' const preText = await pre.text();',
132
+ ' if (!pre.ok) return { ok: false, code: "http-" + pre.status, message: preText.slice(0, 200) };',
133
+ ' let preJson;',
134
+ ' try { preJson = JSON.parse(preText); } catch (error) {',
135
+ ' return { ok: false, code: "shape", message: "换登录态时返回的不是 JSON" };',
136
+ ' }',
137
+ ` const token = pick(preJson, ${quote(source.path)});`,
138
+ ' if (typeof token !== "string" || token === "") return { ok: false, code: "auth-failed", message: "换登录态时没拿到 token" };',
139
+ ];
140
+ return lines;
141
+ }
142
+ return [];
143
+ }
144
+
145
+ function fetchLine(recipe: RequestRecipe, hasBody: boolean): string {
146
+ return ` res = await fetch(url, { method: ${quote(recipe.request.method)}, credentials: "include", headers${hasBody ? ', body' : ''} });`;
147
+ }
148
+
149
+ export function scriptOf(recipe: RequestRecipe, overrides: CallOverrides = {}): string {
150
+ const problem = recipeProblemOf(recipe, overrides);
151
+ if (problem !== undefined) throw new Error(problem);
152
+ const auth = authOf(recipe);
153
+ const body = bodyOf(recipe, overrides);
154
+ const contentType = recipe.request.contentType ?? 'json';
155
+ const place = auth.kind === 'none' ? undefined : auth.place;
156
+ const bodyPlace = place?.in === 'body' ? place : undefined;
157
+ const hasBody = body !== undefined || bodyPlace !== undefined;
158
+ const headers = mergedHeaders(recipe, overrides);
159
+ const headerNames = Object.keys(headers).map((name) => name.toLowerCase());
160
+ if (hasBody && !headerNames.includes('content-type')) {
161
+ headers['content-type'] = contentType === 'form' ? 'application/x-www-form-urlencoded' : 'application/json';
162
+ }
163
+ const url = urlOf(recipe, overrides);
164
+ const { kind: responseKind, path: responsePath, maxBytes } = responseOf(recipe, overrides);
165
+ const lines: string[] = [];
166
+ lines.push('(async () => {');
167
+ lines.push(` if (location.origin !== ${quote(recipe.origin)}) return { ok: false, code: "not-on-site" };`);
168
+ lines.push(
169
+ ' const pick = (value, path) => path.split(".").reduce((one, key) => (one === null || one === undefined ? one : one[key]), value);',
170
+ );
171
+ lines.push(
172
+ ' const set = (target, path, value) => { const parts = path.split("."); let at = target; for (let index = 0; index < parts.length - 1; index += 1) { const key = parts[index]; if (at[key] === null || typeof at[key] !== "object") at[key] = {}; at = at[key]; } at[parts[parts.length - 1]] = value; return target; };',
173
+ );
174
+ lines.push(...tokenLines(recipe, auth));
175
+ lines.push(` const headers = ${literal(headers)};`);
176
+ if (place?.in === 'header' && needsToken(auth)) {
177
+ lines.push(` headers[${quote(place.name)}] = ${quote(place.prefix ?? '')} + token;`);
178
+ }
179
+ if (hasBody) {
180
+ if (contentType === 'form') {
181
+ lines.push(` const body = new URLSearchParams(${literal(body ?? {})});`);
182
+ if (bodyPlace !== undefined) lines.push(` body.set(${quote(bodyPlace.field)}, token);`);
183
+ } else {
184
+ lines.push(` let body = ${literal(body ?? {})};`);
185
+ if (bodyPlace !== undefined) lines.push(` set(body, ${quote(bodyPlace.field)}, token);`);
186
+ lines.push(' if (typeof body !== "string") body = JSON.stringify(body);');
187
+ }
188
+ }
189
+ lines.push(` let url = ${quote(url)};`);
190
+ if (place?.in === 'query' && needsToken(auth)) {
191
+ lines.push(
192
+ ` url += (url.indexOf("?") === -1 ? "?" : "&") + ${quote(`${place.name}=`)} + encodeURIComponent(token);`,
193
+ );
194
+ }
195
+ lines.push(' let res;');
196
+ lines.push(' try {');
197
+ lines.push(fetchLine(recipe, hasBody));
198
+ lines.push(' } catch (error) {');
199
+ lines.push(' return { ok: false, code: "fetch-failed", message: String(error) };');
200
+ lines.push(' }');
201
+ lines.push(' const text = await res.text();');
202
+ lines.push(
203
+ ` if (new TextEncoder().encode(text).length > ${maxBytes}) return { ok: false, code: "too-large", message: "返回超过 ${Math.round(maxBytes / 1024)} KB" };`,
204
+ );
205
+ lines.push(' if (!res.ok) return { ok: false, code: "http-" + res.status, message: text.slice(0, 200) };');
206
+ if (responseKind === 'text') {
207
+ lines.push(' return { ok: true, text };');
208
+ } else {
209
+ lines.push(' let data;');
210
+ lines.push(' try {');
211
+ lines.push(' data = JSON.parse(text);');
212
+ lines.push(' } catch (error) {');
213
+ lines.push(' return { ok: false, code: "shape", message: "返回的不是 JSON(可能跳到登录页了)" };');
214
+ lines.push(' }');
215
+ if (responsePath === undefined) {
216
+ lines.push(' return { ok: true, data };');
217
+ } else {
218
+ lines.push(` const value = pick(data, ${quote(responsePath)});`);
219
+ lines.push(
220
+ ` if (value === undefined) return { ok: false, code: "shape", message: "返回里没有 ${responsePath} 这一格" };`,
221
+ );
222
+ lines.push(' return { ok: true, data: value };');
223
+ }
224
+ }
225
+ lines.push('})()');
226
+ return lines.join('\n');
227
+ }
228
+
229
+ export function describeRecipe(recipe: RequestRecipe, overrides: CallOverrides = {}): RecipeDescription {
230
+ const problem = recipeProblemOf(recipe, overrides);
231
+ if (problem !== undefined) throw new Error(problem);
232
+ const auth = authOf(recipe);
233
+ const body = bodyOf(recipe, overrides);
234
+ const placement = auth.kind === 'none' ? undefined : auth.place;
235
+ const name =
236
+ placement === undefined || placement.in === 'cookie'
237
+ ? undefined
238
+ : placement.in === 'body'
239
+ ? placement.field
240
+ : placement.name;
241
+ return {
242
+ method: recipe.request.method,
243
+ url: urlOf(recipe, overrides),
244
+ ...(body === undefined ? {} : { body }),
245
+ ...(placement === undefined
246
+ ? {}
247
+ : {
248
+ auth: {
249
+ source: auth.kind === 'none' ? 'none' : auth.source.kind,
250
+ place: placement.in,
251
+ ...(name === undefined ? {} : { name }),
252
+ },
253
+ }),
254
+ };
255
+ }
256
+
257
+
258
+ export function scriptOfMany(recipe: RequestRecipe, calls: RecipeCalls): string {
259
+ const problem = callsProblemOf(recipe, calls);
260
+ if (problem !== undefined) throw new Error(problem);
261
+ if (recipe.request.method !== 'GET') throw new Error('批量读只认 GET,写一条一条走 submit');
262
+ const auth = authOf(recipe);
263
+ const place = auth.kind === 'none' ? undefined : auth.place;
264
+ if (place?.in === 'body') throw new Error('批量读不带 body 里的凭据');
265
+ const lines: string[] = [];
266
+ lines.push('(async () => {');
267
+ lines.push(` if (location.origin !== ${quote(recipe.origin)}) return { ok: false, code: "not-on-site" };`);
268
+ lines.push(
269
+ ' const pick = (value, path) => path.split(".").reduce((one, key) => (one === null || one === undefined ? one : one[key]), value);',
270
+ );
271
+ lines.push(...tokenLines(recipe, auth));
272
+ lines.push(` const headers = ${literal(recipe.request.headers ?? {})};`);
273
+ if (place?.in === 'header' && needsToken(auth)) {
274
+ lines.push(` headers[${quote(place.name)}] = ${quote(place.prefix ?? '')} + token;`);
275
+ }
276
+ if (place?.in === 'query' && needsToken(auth)) {
277
+ lines.push(
278
+ ` const withToken = (url) => url + (url.indexOf("?") === -1 ? "?" : "&") + ${quote(`${place.name}=`)} + encodeURIComponent(token);`,
279
+ );
280
+ } else {
281
+ lines.push(' const withToken = (url) => url;');
282
+ }
283
+ lines.push(' const one = async (url, headers, kind, path, maxBytes) => {');
284
+ lines.push(' let res;');
285
+ lines.push(' try {');
286
+ lines.push(' res = await fetch(url, { method: "GET", credentials: "include", headers });');
287
+ lines.push(' } catch (error) {');
288
+ lines.push(' return { ok: false, code: "fetch-failed", message: String(error) };');
289
+ lines.push(' }');
290
+ lines.push(' const text = await res.text();');
291
+ lines.push(
292
+ ' if (new TextEncoder().encode(text).length > maxBytes) return { ok: false, code: "too-large", message: "返回超过 " + Math.round(maxBytes / 1024) + " KB" };',
293
+ );
294
+ lines.push(' if (!res.ok) return { ok: false, code: "http-" + res.status, message: text.slice(0, 200) };');
295
+ lines.push(' if (kind === "text") return { ok: true, text };');
296
+ lines.push(' let data;');
297
+ lines.push(' try {');
298
+ lines.push(' data = JSON.parse(text);');
299
+ lines.push(' } catch (error) {');
300
+ lines.push(' return { ok: false, code: "shape", message: "返回的不是 JSON(可能跳到登录页了)" };');
301
+ lines.push(' }');
302
+ lines.push(' if (path === null) return { ok: true, data };');
303
+ lines.push(' const value = pick(data, path);');
304
+ lines.push(' if (value === undefined) return { ok: false, code: "shape", message: "返回里没有 " + path + " 这一格" };');
305
+ lines.push(' return { ok: true, data: value };');
306
+ lines.push(' };');
307
+ lines.push(' const results = {};');
308
+ lines.push(' await Promise.all([');
309
+ for (const [name, call] of Object.entries(calls)) {
310
+ const extra = call.headers ?? {};
311
+ const headersArg =
312
+ Object.keys(extra).length === 0 ? 'headers' : `Object.assign({}, headers, ${literal(extra)})`;
313
+ const { kind, path, maxBytes } = responseOf(recipe, call);
314
+ const pathArg = path === undefined ? 'null' : quote(path);
315
+ lines.push(
316
+ ` one(withToken(${quote(urlOf(recipe, call))}), ${headersArg}, ${quote(kind)}, ${pathArg}, ${maxBytes}).then((result) => { results[${quote(name)}] = result; }),`,
317
+ );
318
+ }
319
+ lines.push(' ]);');
320
+ lines.push(' return { ok: true, results };');
321
+ lines.push('})()');
322
+ return lines.join('\n');
323
+ }
@@ -1,10 +1,22 @@
1
1
  /**
2
- * 把一个值裹成 shell 里的单引号字面量(值里的单引号一并转义),保证元字符按字面量传递。
2
+ * 按目标平台把一个值裹成 shell 字面量:POSIX 用单引号,win32 用双引号(cmd 与 PowerShell 都认)。
3
3
  * @param value - 任意值;不是字符串的先 `String()`。
4
- * @returns 可安全拼进 `/bin/sh -c` 的一段文本 —— 含空格 / `$()` / `;` / 通配符的值都不会被当成语法。
4
+ * @param platform - 目标平台,默认当前 `process.platform`。
5
+ * @returns 可安全拼进该平台 shell 命令的一段文本;含空格 / 元字符的值不会被当成语法。
6
+ */
7
+ export function quoteShellArgFor(value: unknown, platform: NodeJS.Platform = process.platform): string {
8
+ const text = String(value);
9
+ if (platform === 'win32') return `"${text.replace(/"/g, '\\"')}"`;
10
+ return `'${text.replace(/'/g, `'\\''`)}'`;
11
+ }
12
+
13
+ /**
14
+ * 按当前平台把值裹成 shell 字面量;跨平台代码要固定平台时用 `quoteShellArgFor`。
15
+ * @param value - 任意值;不是字符串的先 `String()`。
16
+ * @returns 可安全拼进当前平台 shell 命令的一段文本。
5
17
  */
6
18
  export function quoteShellArg(value: unknown): string {
7
- return `'${String(value).replace(/'/g, `'\\''`)}'`;
19
+ return quoteShellArgFor(value, process.platform);
8
20
  }
9
21
 
10
22
  /**
package/src/host/shell.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { DshShell } from '../api/dsh.js';
2
2
  import { GIT_TIMEOUT_MS } from '../config/constants.js';
3
+ import { quoteShellArg } from './shell-text.js';
3
4
 
4
5
  export interface ShellResult {
5
6
  readonly exitCode: number | null;
@@ -40,3 +41,11 @@ export async function runShell(
40
41
  truncated: result.stdout.truncated || result.stderr.truncated,
41
42
  };
42
43
  }
44
+
45
+ export function backgroundPrefix(): readonly string[] {
46
+ return [quoteShellArg('env'), quoteShellArg('OPENCLI_WINDOW=background')];
47
+ }
48
+
49
+ export function backgroundLine(command: string, argv: readonly string[]): string {
50
+ return [...backgroundPrefix(), quoteShellArg(command), ...argv.map(quoteShellArg)].join(' ');
51
+ }
@@ -0,0 +1,32 @@
1
+ import path from 'node:path';
2
+
3
+ const PACKAGE_NAME = /^(?:@[a-z0-9][a-z0-9._~-]*\/)?[a-z0-9][a-z0-9._~-]*$/;
4
+ const VERSION = /^[a-zA-Z0-9][a-zA-Z0-9._+-]*$/;
5
+
6
+ export interface NpmRef {
7
+ readonly name: string;
8
+ readonly version?: string;
9
+ }
10
+
11
+ export function isPackageName(value: string): boolean {
12
+ return value.length <= 214 && PACKAGE_NAME.test(value);
13
+ }
14
+
15
+ export function splitNpmSpec(spec: string): NpmRef | undefined {
16
+ const value = spec.trim();
17
+ const at = value.lastIndexOf('@');
18
+ const name = at > 0 ? value.slice(0, at) : value;
19
+ const version = at > 0 ? value.slice(at + 1) : '';
20
+ if (!isPackageName(name)) return undefined;
21
+ if (version !== '' && !VERSION.test(version)) return undefined;
22
+ return { name, ...(version === '' ? {} : { version }) };
23
+ }
24
+
25
+ export function localDirOf(spec: string): string | undefined {
26
+ const dir = spec.trim().replace(/^(?:file|link):/, '');
27
+ return path.isAbsolute(dir) ? dir : undefined;
28
+ }
29
+
30
+ export function refusalReason(): string {
31
+ return '这个包既不是工作台的模块,也不是 opencli 的插件,装了也用不上';
32
+ }
@@ -0,0 +1,256 @@
1
+ import type { ExtensionNeed, LibNeed, Need, SupplySource } from '../contract/needs.js';
2
+
3
+ export interface SupplyDeclaration {
4
+ readonly id: string;
5
+ readonly needs?: readonly Need[];
6
+ }
7
+
8
+ export interface SupplyEntry {
9
+ readonly key: string;
10
+ readonly kind: 'extension' | 'lib';
11
+ readonly names: readonly string[];
12
+ readonly package: string;
13
+ readonly runtime?: string;
14
+ readonly sources: readonly SupplySource[];
15
+ readonly declarers: readonly string[];
16
+ readonly commands: readonly string[];
17
+ readonly conflicted: boolean;
18
+ }
19
+
20
+ export interface SupplyConflict {
21
+ readonly kind: 'source-mismatch' | 'name-mismatch';
22
+ readonly key: string;
23
+ readonly names: readonly string[];
24
+ readonly declarers: readonly string[];
25
+ readonly detail: string;
26
+ }
27
+
28
+ export interface SupplyIndex {
29
+ readonly entries: ReadonlyMap<string, SupplyEntry>;
30
+ readonly conflicts: readonly SupplyConflict[];
31
+ readonly problems: readonly string[];
32
+ readonly entryOf: (kind: 'extension' | 'lib', name: string) => SupplyEntry | undefined;
33
+ readonly sourcesOf: (kind: 'extension' | 'lib', name: string) => readonly SupplySource[];
34
+ }
35
+
36
+ interface MutableEntry {
37
+ key: string;
38
+ kind: 'extension' | 'lib';
39
+ package: string;
40
+ runtime?: string;
41
+ declared: boolean;
42
+ names: string[];
43
+ sources: SupplySource[];
44
+ declarers: string[];
45
+ commands: string[];
46
+ conflicted: boolean;
47
+ }
48
+
49
+ export function npmSourceOf(
50
+ sources: readonly SupplySource[] | undefined,
51
+ ): Extract<SupplySource, { kind: 'npm' }> | undefined {
52
+ return sources?.find((source): source is Extract<SupplySource, { kind: 'npm' }> => source.kind === 'npm');
53
+ }
54
+
55
+ function text(value: string | undefined): string {
56
+ return value === undefined ? '' : value.trim();
57
+ }
58
+
59
+ function conventionPackage(name: string): string {
60
+ return `opencli-plugin-${name}`;
61
+ }
62
+
63
+ function nativeSourcePackage(source: SupplySource): string {
64
+ if (source.kind === 'npm') return text(source.package);
65
+ if (source.kind === 'git') return `git:${text(source.url)}`;
66
+ return `local:${text(source.env) || text(source.path)}`;
67
+ }
68
+
69
+ function packageOf(need: ExtensionNeed | LibNeed): string {
70
+ if (need.kind === 'lib') return text(need.name);
71
+ const first = need.sources?.[0];
72
+ if (first === undefined) return conventionPackage(need.name);
73
+ if (first.kind === 'npm') {
74
+ const pkg = text(first.package);
75
+ return pkg === '' ? conventionPackage(need.name) : pkg;
76
+ }
77
+ return nativeSourcePackage(first);
78
+ }
79
+
80
+ function keyOf(kind: 'extension' | 'lib', pkg: string): string {
81
+ return `${kind}:${pkg}`;
82
+ }
83
+
84
+ function sourceCompatible(left: SupplySource, right: SupplySource): boolean {
85
+ if (left.kind !== right.kind) return false;
86
+ if (left.kind === 'npm' && right.kind === 'npm') {
87
+ if (text(left.package) !== '' && text(right.package) !== '' && text(left.package) !== text(right.package)) {
88
+ return false;
89
+ }
90
+ if (text(left.registryEnv) !== text(right.registryEnv)) return false;
91
+ const l = text(left.registryDefault);
92
+ const r = text(right.registryDefault);
93
+ return l === '' || r === '' || l === r;
94
+ }
95
+ if (left.kind === 'git' && right.kind === 'git') return text(left.url) === text(right.url);
96
+ if (left.kind === 'local' && right.kind === 'local') {
97
+ return text(left.env) === text(right.env) && text(left.path) === text(right.path);
98
+ }
99
+ return false;
100
+ }
101
+
102
+ function sourcesCompatible(left: readonly SupplySource[], right: readonly SupplySource[]): boolean {
103
+ if (left.length !== right.length) return false;
104
+ return left.every((source, index) => {
105
+ const other = right[index];
106
+ return other !== undefined && sourceCompatible(source, other);
107
+ });
108
+ }
109
+
110
+ function mergedSource(left: SupplySource, right: SupplySource): SupplySource {
111
+ if (left.kind === 'npm' && right.kind === 'npm') {
112
+ const pkg = text(left.package) === '' ? right.package : left.package;
113
+ const registryDefault = text(left.registryDefault) === '' ? right.registryDefault : left.registryDefault;
114
+ return {
115
+ ...left,
116
+ ...(pkg === undefined ? {} : { package: pkg }),
117
+ ...(registryDefault === undefined ? {} : { registryDefault }),
118
+ };
119
+ }
120
+ return left;
121
+ }
122
+
123
+ function mergeSources(entry: MutableEntry, sources: readonly SupplySource[]): boolean {
124
+ if (!entry.declared) {
125
+ entry.declared = true;
126
+ entry.sources = [...sources];
127
+ return true;
128
+ }
129
+ if (!sourcesCompatible(entry.sources, sources)) {
130
+ entry.conflicted = true;
131
+ return false;
132
+ }
133
+ entry.sources = entry.sources.map((source, index) => {
134
+ const other = sources[index];
135
+ return other === undefined ? source : mergedSource(source, other);
136
+ });
137
+ return true;
138
+ }
139
+
140
+ function sourceText(source: SupplySource): string {
141
+ if (source.kind === 'npm') {
142
+ return `${source.package === undefined ? '按包名约定' : source.package}(${source.registryEnv ?? '默认源'})`;
143
+ }
144
+ if (source.kind === 'git') return `git:${source.url}`;
145
+ return `local:${source.env ?? source.path ?? ''}`;
146
+ }
147
+
148
+ function addDeclarer(entry: MutableEntry, unit: string, commands: readonly string[]): void {
149
+ if (!entry.declarers.includes(unit)) entry.declarers.push(unit);
150
+ for (const command of commands) if (!entry.commands.includes(command)) entry.commands.push(command);
151
+ }
152
+
153
+ export function buildSupplyIndex(declarations: readonly SupplyDeclaration[]): SupplyIndex {
154
+ const entries = new Map<string, MutableEntry>();
155
+ const byName = new Map<string, MutableEntry>();
156
+ const conflicts: SupplyConflict[] = [];
157
+
158
+ const ensure = (kind: 'extension' | 'lib', pkg: string): MutableEntry => {
159
+ const key = keyOf(kind, pkg);
160
+ const found = entries.get(key);
161
+ if (found !== undefined) return found;
162
+ const created: MutableEntry = {
163
+ key,
164
+ kind,
165
+ package: pkg,
166
+ declared: false,
167
+ names: [],
168
+ sources: [],
169
+ declarers: [],
170
+ commands: [],
171
+ conflicted: false,
172
+ };
173
+ entries.set(key, created);
174
+ return created;
175
+ };
176
+
177
+ for (const declaration of declarations) {
178
+ for (const need of declaration.needs ?? []) {
179
+ if (need.kind !== 'extension' && need.kind !== 'lib') continue;
180
+ const pkg = packageOf(need);
181
+ const entry = ensure(need.kind, pkg);
182
+ if (!entry.names.includes(need.name)) entry.names.push(need.name);
183
+ if (need.kind === 'extension') {
184
+ entry.runtime = need.runtime;
185
+ addDeclarer(entry, declaration.id, need.commands ?? []);
186
+ } else {
187
+ addDeclarer(entry, declaration.id, []);
188
+ }
189
+ const same = mergeSources(entry, need.sources ?? []);
190
+ if (!same) {
191
+ conflicts.push({
192
+ kind: 'source-mismatch',
193
+ key: entry.key,
194
+ names: [...entry.names],
195
+ declarers: [...entry.declarers],
196
+ detail:
197
+ `${entry.kind === 'extension' ? '插件' : '共用库'} ${entry.names.join('、')} 被 ` +
198
+ `${entry.declarers.join('、')} 声明成了不同的来源(` +
199
+ `${entry.sources.map(sourceText).join(' / ') || '无来源'})`,
200
+ });
201
+ }
202
+ const nameKey = `${need.kind}:${need.name}`;
203
+ const seen = byName.get(nameKey);
204
+ if (seen !== undefined && seen.key !== entry.key) {
205
+ const detail =
206
+ `${need.kind === 'extension' ? '插件' : '共用库'} ${need.name} 来自不同的包:` +
207
+ `${seen.package} / ${entry.package}`;
208
+ entry.conflicted = true;
209
+ seen.conflicted = true;
210
+ conflicts.push({
211
+ kind: 'name-mismatch',
212
+ key: entry.key,
213
+ names: [need.name],
214
+ declarers: [...new Set([...seen.declarers, ...entry.declarers])],
215
+ detail,
216
+ });
217
+ } else if (seen === undefined) {
218
+ byName.set(nameKey, entry);
219
+ }
220
+ }
221
+ }
222
+
223
+ const deduped = new Map<string, SupplyConflict>();
224
+ for (const conflict of conflicts) {
225
+ deduped.set(`${conflict.kind}:${conflict.key}`, conflict);
226
+ }
227
+
228
+ const frozenEntries = new Map<string, SupplyEntry>();
229
+ for (const [key, entry] of entries) {
230
+ frozenEntries.set(key, {
231
+ key: entry.key,
232
+ kind: entry.kind,
233
+ names: [...entry.names],
234
+ package: entry.package,
235
+ ...(entry.runtime === undefined ? {} : { runtime: entry.runtime }),
236
+ sources: entry.sources.map((source) => ({ ...source })),
237
+ declarers: [...entry.declarers],
238
+ commands: [...entry.commands],
239
+ conflicted: entry.conflicted,
240
+ });
241
+ }
242
+
243
+ const entryOf = (kind: 'extension' | 'lib', name: string): SupplyEntry | undefined =>
244
+ byName.get(`${kind}:${name}`);
245
+
246
+ return {
247
+ entries: frozenEntries,
248
+ conflicts: [...deduped.values()],
249
+ problems: [...deduped.values()].map((conflict) => conflict.detail),
250
+ entryOf,
251
+ sourcesOf: (kind, name) => {
252
+ const entry = entryOf(kind, name);
253
+ return entry === undefined || entry.conflicted ? [] : entry.sources;
254
+ },
255
+ };
256
+ }