@dsh-workbench/shared 1.1.7 → 1.1.9

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 (239) hide show
  1. package/dist/api/blocker-table.d.ts +1 -0
  2. package/dist/api/blocker-table.js +1 -1
  3. package/dist/components/SelectField.css +0 -6
  4. package/dist/components/SelectField.d.ts +2 -3
  5. package/dist/components/SelectField.js +9 -30
  6. package/dist/components/SelectFieldNodes.d.ts +3 -1
  7. package/dist/components/SelectFieldNodes.js +2 -1
  8. package/dist/components/SelectFieldWheel.d.ts +9 -0
  9. package/dist/components/SelectFieldWheel.js +35 -0
  10. package/dist/config/constants.d.ts +4 -4
  11. package/dist/config/constants.js +4 -4
  12. package/dist/contract/capabilities.d.ts +29 -0
  13. package/dist/contract/capabilities.js +1 -0
  14. package/dist/contract/needs.d.ts +3 -3
  15. package/dist/contract/needs.js +1 -1
  16. package/dist/contract/plugin-source.d.ts +19 -0
  17. package/dist/contract/protocol/bundle-install.d.ts +3 -1
  18. package/dist/contract/protocol/bundle-install.js +3 -0
  19. package/dist/contract/protocol/market-index.d.ts +1 -0
  20. package/dist/contract/protocol/platform.d.ts +1 -1
  21. package/dist/contract/runner.d.ts +6 -0
  22. package/dist/contract/runner.js +1 -0
  23. package/dist/contract/unit.d.ts +4 -4
  24. package/dist/host/adapter-contract.d.ts +3 -0
  25. package/dist/host/adapter-contract.js +93 -0
  26. package/dist/host/adapter-set.js +6 -4
  27. package/dist/host/adapter-types.d.ts +1 -1
  28. package/dist/host/bundle-manager.d.ts +3 -0
  29. package/dist/host/bundle-manager.js +181 -0
  30. package/dist/host/cli.js +3 -1
  31. package/dist/host/config-sync.d.ts +48 -0
  32. package/dist/host/config-sync.js +342 -0
  33. package/dist/host/dsh-plugins.d.ts +5 -0
  34. package/dist/host/dsh-plugins.js +51 -0
  35. package/dist/host/extension.js +1 -1
  36. package/dist/host/flow/check.d.ts +9 -0
  37. package/dist/host/flow/check.js +179 -0
  38. package/dist/host/flow/expr.d.ts +30 -0
  39. package/dist/host/flow/expr.js +99 -0
  40. package/dist/host/flow/kit.d.ts +14 -0
  41. package/dist/host/flow/kit.js +13 -0
  42. package/dist/host/flow/run.d.ts +13 -0
  43. package/dist/host/flow/run.js +287 -0
  44. package/dist/host/flow/types.d.ts +188 -0
  45. package/dist/host/flow/types.js +1 -0
  46. package/dist/host/host-actions.d.ts +2 -0
  47. package/dist/host/host-actions.js +5 -0
  48. package/dist/host/host-kit.d.ts +19 -0
  49. package/dist/host/host-kit.js +14 -0
  50. package/dist/host/http.d.ts +2 -6
  51. package/dist/host/http.js +2 -2
  52. package/dist/host/installed-module.d.ts +23 -0
  53. package/dist/host/installed-module.js +264 -0
  54. package/dist/host/lib-install.d.ts +7 -9
  55. package/dist/host/lib-install.js +9 -12
  56. package/dist/host/lib-runners.d.ts +3 -5
  57. package/dist/host/lib-runners.js +2 -2
  58. package/dist/host/local-declaration.d.ts +7 -0
  59. package/dist/host/local-declaration.js +33 -0
  60. package/dist/host/market-index-claim.d.ts +0 -1
  61. package/dist/host/market-index-claim.js +0 -3
  62. package/dist/host/market-index-doc.d.ts +1 -45
  63. package/dist/host/market-index-parse.d.ts +9 -4
  64. package/dist/host/market-index-parse.js +17 -92
  65. package/dist/host/market-index.d.ts +5 -6
  66. package/dist/host/market-index.js +29 -87
  67. package/dist/host/module-root.d.ts +1 -0
  68. package/dist/host/module-root.js +5 -0
  69. package/dist/host/npm-registry.d.ts +10 -0
  70. package/dist/host/npm-registry.js +16 -0
  71. package/dist/host/opencli-runner.d.ts +2 -1
  72. package/dist/host/package-content.d.ts +7 -0
  73. package/dist/host/package-content.js +69 -0
  74. package/dist/host/package-kind.d.ts +13 -0
  75. package/dist/host/package-kind.js +51 -0
  76. package/dist/host/package-route.d.ts +31 -0
  77. package/dist/host/package-route.js +57 -0
  78. package/dist/host/paths.d.ts +11 -19
  79. package/dist/host/paths.js +16 -43
  80. package/dist/host/platform.js +1 -1
  81. package/dist/host/plugin-ledger.d.ts +8 -8
  82. package/dist/host/plugin-ledger.js +23 -16
  83. package/dist/host/plugin.d.ts +1 -2
  84. package/dist/host/plugin.js +4 -5
  85. package/dist/host/port-types.d.ts +42 -9
  86. package/dist/host/preflight.d.ts +3 -2
  87. package/dist/host/profile-create.d.ts +18 -0
  88. package/dist/host/profile-create.js +118 -0
  89. package/dist/host/profile-registries.d.ts +10 -0
  90. package/dist/host/profile-registries.js +150 -0
  91. package/dist/host/registry-declaration.d.ts +21 -0
  92. package/dist/host/registry-declaration.js +33 -0
  93. package/dist/host/release-probe.d.ts +34 -0
  94. package/dist/host/release-probe.js +88 -0
  95. package/dist/host/runner.d.ts +26 -0
  96. package/dist/host/runner.js +50 -0
  97. package/dist/host/shell.d.ts +0 -2
  98. package/dist/host/shell.js +0 -7
  99. package/dist/host/spec.js +1 -1
  100. package/dist/host/supply-index.d.ts +1 -0
  101. package/dist/host/supply-index.js +8 -7
  102. package/dist/host/supply-port.js +13 -3
  103. package/dist/host/unit-face.d.ts +2 -1
  104. package/dist/host/version.js +11 -6
  105. package/dist/host.d.ts +24 -6
  106. package/dist/host.js +23 -6
  107. package/dist/i18n/en.js +19 -20
  108. package/dist/i18n/index.d.ts +20 -21
  109. package/dist/i18n/runtime-terms.d.ts +15 -0
  110. package/dist/i18n/runtime-terms.js +22 -0
  111. package/dist/i18n/zh.d.ts +19 -20
  112. package/dist/i18n/zh.js +19 -20
  113. package/dist/index.d.ts +4 -0
  114. package/dist/index.js +4 -0
  115. package/dist/testing/fake-adapter-set.d.ts +5 -0
  116. package/dist/testing/fake-adapter-set.js +150 -0
  117. package/dist/testing/needs.js +1 -1
  118. package/dist/testing.d.ts +1 -0
  119. package/dist/testing.js +1 -0
  120. package/package.json +1 -1
  121. package/src/api/__tests__/blocker-table.test.ts +25 -0
  122. package/src/api/{workbench-client.test.ts → __tests__/workbench-client.test.ts} +2 -2
  123. package/src/api/blocker-table.ts +6 -2
  124. package/src/components/SelectField.css +0 -6
  125. package/src/components/SelectField.tsx +7 -36
  126. package/src/components/SelectFieldNodes.tsx +4 -1
  127. package/src/components/{Card.test.tsx → __tests__/Card.test.tsx} +2 -2
  128. package/src/components/{CardAnchor.test.tsx → __tests__/CardAnchor.test.tsx} +3 -3
  129. package/src/components/{CardHead.test.tsx → __tests__/CardHead.test.tsx} +1 -1
  130. package/src/components/{ChoiceGroup.test.tsx → __tests__/ChoiceGroup.test.tsx} +2 -2
  131. package/src/components/{ConfirmBar.test.tsx → __tests__/ConfirmBar.test.tsx} +5 -5
  132. package/src/components/{Field.test.tsx → __tests__/Field.test.tsx} +1 -1
  133. package/src/components/{Form.test.tsx → __tests__/Form.test.tsx} +5 -5
  134. package/src/components/{FormActions.test.tsx → __tests__/FormActions.test.tsx} +3 -3
  135. package/src/components/{FormRows.test.tsx → __tests__/FormRows.test.tsx} +2 -2
  136. package/src/components/{FormTypes.test.ts → __tests__/FormTypes.test.ts} +1 -1
  137. package/src/components/{FormWiring.test.tsx → __tests__/FormWiring.test.tsx} +4 -4
  138. package/src/components/{Link.test.tsx → __tests__/Link.test.tsx} +1 -1
  139. package/src/components/{Rows.test.tsx → __tests__/Rows.test.tsx} +2 -2
  140. package/src/components/{SelectField.test.tsx → __tests__/SelectField.test.tsx} +7 -37
  141. package/src/components/{Spinner.test.tsx → __tests__/Spinner.test.tsx} +4 -4
  142. package/src/config/{constants.test.ts → __tests__/constants.test.ts} +1 -1
  143. package/src/config/{storage.test.ts → __tests__/storage.test.ts} +1 -1
  144. package/src/config/constants.ts +4 -4
  145. package/src/contract/capabilities.ts +30 -0
  146. package/src/contract/needs.ts +4 -4
  147. package/src/contract/plugin-source.ts +24 -0
  148. package/src/contract/protocol/bundle-install.ts +7 -1
  149. package/src/contract/protocol/market-index.ts +1 -0
  150. package/src/contract/protocol/platform.ts +1 -1
  151. package/src/contract/runner.ts +6 -0
  152. package/src/contract/unit.ts +4 -4
  153. package/src/host/{extension.test.ts → __tests__/extension.test.ts} +4 -4
  154. package/src/host/{install-failure.test.ts → __tests__/install-failure.test.ts} +1 -1
  155. package/src/host/{installed-sources.test.ts → __tests__/installed-sources.test.ts} +1 -1
  156. package/src/host/__tests__/lib-runners.test.ts +11 -2
  157. package/src/host/__tests__/market-index-assemble.test.ts +26 -0
  158. package/src/host/__tests__/market-index.test.ts +41 -51
  159. package/src/host/__tests__/plugin-ledger.test.ts +19 -6
  160. package/src/host/{recipe.test.ts → __tests__/recipe.test.ts} +3 -3
  161. package/src/host/__tests__/runtime-layout.test.ts +14 -0
  162. package/src/host/__tests__/supply-index.test.ts +2 -2
  163. package/src/host/__tests__/supply-port.test.ts +32 -0
  164. package/src/host/adapter-contract.ts +93 -0
  165. package/src/host/adapter-set.ts +6 -4
  166. package/src/host/adapter-types.ts +1 -1
  167. package/src/host/bundle-manager.ts +195 -0
  168. package/src/host/cli.ts +3 -1
  169. package/src/host/config-sync.ts +386 -0
  170. package/src/host/dsh-plugins.ts +60 -0
  171. package/src/host/extension.ts +1 -1
  172. package/src/host/flow/__tests__/advanced.test.ts +302 -0
  173. package/src/host/flow/__tests__/check.test.ts +169 -0
  174. package/src/host/flow/__tests__/run.test.ts +301 -0
  175. package/src/host/flow/check.ts +216 -0
  176. package/src/host/flow/expr.ts +94 -0
  177. package/src/host/flow/kit.ts +30 -0
  178. package/src/host/flow/run.ts +325 -0
  179. package/src/host/flow/types.ts +199 -0
  180. package/src/host/host-actions.ts +7 -0
  181. package/src/host/host-kit.ts +26 -0
  182. package/src/host/http.ts +6 -8
  183. package/src/host/installed-module.ts +272 -0
  184. package/src/host/lib-install.ts +14 -12
  185. package/src/host/lib-runners.ts +6 -8
  186. package/src/host/local-declaration.ts +37 -0
  187. package/src/host/market-index-claim.ts +0 -4
  188. package/src/host/market-index-doc.ts +1 -53
  189. package/src/host/market-index-parse.ts +21 -85
  190. package/src/host/market-index.ts +33 -127
  191. package/src/host/module-root.ts +6 -0
  192. package/src/host/npm-registry.ts +21 -0
  193. package/src/host/package-content.ts +76 -0
  194. package/src/host/package-kind.ts +63 -0
  195. package/src/host/package-route.ts +93 -0
  196. package/src/host/paths.ts +27 -52
  197. package/src/host/platform.ts +1 -1
  198. package/src/host/plugin-ledger.ts +25 -18
  199. package/src/host/plugin.ts +4 -5
  200. package/src/host/port-types.ts +43 -9
  201. package/src/host/preflight.ts +3 -2
  202. package/src/host/profile-create.ts +133 -0
  203. package/src/host/profile-registries.ts +145 -0
  204. package/src/host/registry-declaration.ts +70 -0
  205. package/src/host/release-probe.ts +202 -0
  206. package/src/host/shell.ts +0 -9
  207. package/src/host/spec.ts +1 -1
  208. package/src/host/supply-index.ts +9 -7
  209. package/src/host/supply-port.ts +13 -3
  210. package/src/host/unit-face.ts +2 -1
  211. package/src/host/version.ts +12 -7
  212. package/src/host.ts +34 -6
  213. package/src/i18n/{index.test.ts → __tests__/index.test.ts} +3 -3
  214. package/src/i18n/{merge.test.ts → __tests__/merge.test.ts} +1 -1
  215. package/src/i18n/{zh.test.ts → __tests__/zh.test.ts} +2 -2
  216. package/src/i18n/en.ts +19 -21
  217. package/src/i18n/runtime-terms.ts +26 -0
  218. package/src/i18n/zh.ts +19 -21
  219. package/src/index.ts +4 -0
  220. package/src/lib/{board-fact.test.ts → __tests__/board-fact.test.ts} +1 -1
  221. package/src/lib/{busy.test.ts → __tests__/busy.test.ts} +1 -1
  222. package/src/lib/{form-problem.test.ts → __tests__/form-problem.test.ts} +1 -1
  223. package/src/lib/{git-address.test.ts → __tests__/git-address.test.ts} +1 -1
  224. package/src/lib/{react.test.ts → __tests__/react.test.ts} +1 -1
  225. package/src/lib/{text.test.ts → __tests__/text.test.ts} +1 -1
  226. package/src/lib/{ui.test.ts → __tests__/ui.test.ts} +1 -1
  227. package/src/testing/__tests__/fake-adapter-set.test.ts +28 -0
  228. package/src/testing/fake-adapter-set.ts +173 -0
  229. package/src/testing/needs.ts +1 -1
  230. package/src/testing.ts +1 -0
  231. package/src/host/__tests__/command-name.test.ts +0 -16
  232. package/src/host/__tests__/opencli-runner.test.ts +0 -120
  233. package/src/host/channel-plugin.ts +0 -55
  234. package/src/host/command-name.ts +0 -15
  235. package/src/host/opencli-runner.ts +0 -76
  236. package/src/host/paths.test.ts +0 -26
  237. package/src/host/plugin-source.ts +0 -137
  238. package/src/host/runtime-links.test.ts +0 -31
  239. package/src/host/runtime-links.ts +0 -23
@@ -1,40 +1,26 @@
1
1
  import { existsSync, readdirSync, readFileSync } from 'node:fs';
2
2
  import { join, relative, sep } from 'node:path';
3
- import type {
4
- MarketIndex,
5
- MarketIndexEntry,
6
- MarketIndexProblem,
7
- MarketIndexVersion,
8
- } from '../contract/protocol/market-index.js';
3
+ import type { MarketIndex, MarketIndexEntry, MarketIndexProblem } from '../contract/protocol/market-index.js';
9
4
  import { MARKET_INDEX_DIR, MARKET_INDEX_FORMAT } from '../contract/protocol/market-index.js';
10
-
11
- import type {
12
- MarketDocFields,
13
- MarketEntryDoc,
14
- MarketEntryFile,
15
- MarketIndexAssemble,
16
- MarketMetaDoc,
17
- MarketVersionDoc,
18
- } from './market-index-doc.js';
19
- import {
20
- marketEntryPath,
21
- parseMarketEntryDoc,
22
- parseMarketIndexJson,
23
- sortMarketVersions,
24
- } from './market-index-parse.js';
5
+ import type { MarketEntryFile, MarketIndexAssemble } from './market-index-doc.js';
6
+ import { entryUnitOf } from './market-index-claim.js';
7
+ import { marketEntryPath, parseMarketEntryJson, parseMarketIndexJson, sortMarketVersions } from './market-index-parse.js';
25
8
 
26
9
  export const MARKET_ENTRIES_README_LINES: readonly string[] = [
27
10
  '# 索引目录',
28
11
  '',
29
- '一个包一个目录,一个版本一个文件:`market/<包名>/<版本>.json`;产物在 `artifacts/<包名>/<版本>/`。',
12
+ '一个包一个文件:`market/<包名>.json`,里面是这一个包的元数据与所有版本;',
13
+ '',
14
+ '产物在 `artifacts/<包名>/`(只放最新一版,老版本按条目里的 commit 取历史)。',
30
15
  '',
31
- '`meta-<epoch>-<随机后缀>.json` 是只改标题 / 署名 / 说明时追加的元数据快照,读取时按 epoch 取最新的一份。',
16
+ '发布时只改自己包的文件;包与包之间不共享文件。',
32
17
  ];
33
18
 
34
19
  export const MARKET_INDEX_README_LINES: readonly string[] = [
35
20
  '# 模块市场索引',
36
21
  '',
37
- '这个仓库给 DeepSeek Harness 工作台当模块市场索引:条目在 `market/<包名>/<版本>.json`,产物在 `artifacts/<包名>/<版本>/`。',
22
+ '这个仓库给 DeepSeek Harness 工作台当模块市场索引:一个包一个文件 `market/<包名>.json`,',
23
+ '产物在 `artifacts/<包名>/`;老仓库的 `market.json` 会在第一次发布时自动拆开。',
38
24
  '',
39
25
  '## 发布模块',
40
26
  '',
@@ -44,8 +30,7 @@ export const MARKET_INDEX_README_LINES: readonly string[] = [
44
30
  'dsh-wb-module release --index <本仓库地址> --index-policy artifacts-in-repo',
45
31
  '```',
46
32
  '',
47
- '产物会提交到 `artifacts/<包名>/<版本>/`,条目提交到 `market/<包名>/<版本>.json`。',
48
- '发布只新增文件、不改已有文件,两台机器同时发也不会冲突;老条目的 `packages/<包名>/` 路径继续可用。',
33
+ '发布只会改自己包的那一个文件;包与包之间不会碰同一个文件。',
49
34
  '',
50
35
  '一个包名归第一次发布的人(条目文件里的 `owner`)。换一个署名发同名会被拒;要换人,先把这一条的 `owner` 改成新的人。',
51
36
  '同一个市场里,一个模块 id(`metadata.unit`)只能被一个条目使用;另一个包名想用同一个 id / 产物目录也会被拒。',
@@ -56,14 +41,7 @@ export const MARKET_INDEX_README_LINES: readonly string[] = [
56
41
  '',
57
42
  ];
58
43
  export { marketBranchOk, marketInstallTarget, marketText } from '../lib/market-index.js';
59
- export type {
60
- MarketEntryDoc,
61
- MarketEntryDocParse,
62
- MarketEntryFile,
63
- MarketIndexAssemble,
64
- MarketMetaDoc,
65
- MarketVersionDoc,
66
- } from './market-index-doc.js';
44
+ export type { MarketEntryFile, MarketIndexAssemble } from './market-index-doc.js';
67
45
  export {
68
46
  compareSemver,
69
47
  filterMarketIndex,
@@ -71,9 +49,8 @@ export {
71
49
  httpUrlProblem,
72
50
  marketEntryLabels,
73
51
  marketEntryPath,
74
- marketMetaPath,
75
52
  marketSourceKey,
76
- parseMarketEntryDoc,
53
+ parseMarketEntryJson,
77
54
  parseMarketIndex,
78
55
  parseMarketIndexJson,
79
56
  parseSourceConfig,
@@ -82,116 +59,45 @@ export {
82
59
  } from './market-index-parse.js';
83
60
  export type { SourceConfigParse } from './market-index-parse.js';
84
61
 
85
-
86
62
  function problem(kind: MarketIndexProblem['kind'], detail: string, at?: string): MarketIndexProblem {
87
63
  return { kind, detail, ...(at === undefined ? {} : { at }) };
88
64
  }
89
65
 
90
- function docFieldsFromEntry(entry: MarketIndexEntry): MarketDocFields {
91
- return {
92
- name: entry.name,
93
- ...(entry.title === undefined ? {} : { title: entry.title }),
94
- ...(entry.description === undefined ? {} : { description: entry.description }),
95
- ...(entry.author === undefined ? {} : { author: entry.author }),
96
- ...(entry.owner === undefined ? {} : { owner: entry.owner }),
97
- ...(entry.homepage === undefined ? {} : { homepage: entry.homepage }),
98
- ...(entry.icon === undefined ? {} : { icon: entry.icon }),
99
- ...(entry.metadata === undefined ? {} : { metadata: entry.metadata }),
100
- };
101
- }
102
-
103
- export function marketVersionDocOf(entry: MarketIndexEntry, version: MarketIndexVersion): MarketVersionDoc {
104
- return { ...docFieldsFromEntry(entry), ...version };
105
- }
106
-
107
- export function marketMetaDocOf(entry: MarketIndexEntry, epoch: number, at: number): MarketMetaDoc {
108
- return { ...docFieldsFromEntry(entry), epoch, publishTimestamp: at };
109
- }
110
-
111
66
  export function assembleMarketIndex(files: readonly MarketEntryFile[]): MarketIndexAssemble {
112
67
  const problems: MarketIndexProblem[] = [];
113
- interface Group {
114
- versions: Map<string, MarketIndexVersion>;
115
- meta: { epoch: number; at: number; path: string; doc: MarketEntryDoc } | undefined;
116
- fallback: { at: number; path: string; doc: MarketEntryDoc } | undefined;
117
- }
118
- const groups = new Map<string, Group>();
68
+ const entries = new Map<string, MarketIndexEntry>();
119
69
  const ordered = [...files].sort((left, right) => (left.path < right.path ? -1 : left.path > right.path ? 1 : 0));
120
70
  for (const file of ordered) {
121
- let raw: unknown;
122
- try {
123
- raw = JSON.parse(file.text) as unknown;
124
- } catch (error) {
125
- problems.push(
126
- problem('not-json', `JSON 解析失败:${error instanceof Error ? error.message : String(error)}`, file.path),
127
- );
128
- continue;
129
- }
130
- const parsed = parseMarketEntryDoc(raw);
71
+ if (!file.path.endsWith('.json')) continue;
72
+ const parsed = parseMarketEntryJson(file.text);
131
73
  if (!parsed.ok) {
132
74
  for (const one of parsed.problems) problems.push({ ...one, at: file.path });
133
75
  continue;
134
76
  }
135
- const doc = parsed.doc;
136
- const group =
137
- groups.get(doc.name) ?? { versions: new Map<string, MarketIndexVersion>(), meta: undefined, fallback: undefined };
138
- const at = doc.publishTimestamp ?? 0;
139
- if ('version' in doc) {
140
- if (group.versions.has(doc.version)) {
141
- problems.push(problem('duplicate-version', `版本重复:${doc.version}`, file.path));
142
- }
143
- group.versions.set(doc.version, {
144
- version: doc.version,
145
- source: doc.source,
146
- ...(doc.publishTimestamp === undefined ? {} : { publishTimestamp: doc.publishTimestamp }),
147
- ...(doc.engines === undefined ? {} : { engines: doc.engines }),
148
- });
149
- const newerFallback =
150
- group.fallback === undefined ||
151
- at > group.fallback.at ||
152
- (at === group.fallback.at && file.path > group.fallback.path);
153
- if (newerFallback) group.fallback = { at, path: file.path, doc };
154
- } else {
155
- const epoch = doc.epoch ?? 0;
156
- const newer =
157
- group.meta === undefined ||
158
- epoch > group.meta.epoch ||
159
- (epoch === group.meta.epoch && at > group.meta.at) ||
160
- (epoch === group.meta.epoch && at === group.meta.at && file.path > group.meta.path);
161
- if (newer) group.meta = { epoch, at, path: file.path, doc };
77
+ const entry = parsed.entry;
78
+ if (entries.has(entry.name)) {
79
+ problems.push(problem('duplicate-name', `插件名重复:${entry.name}`, file.path));
80
+ continue;
162
81
  }
163
- groups.set(doc.name, group);
82
+ entries.set(entry.name, { ...entry, versions: sortMarketVersions(entry.versions) });
164
83
  }
165
84
  if (problems.length > 0) return { ok: false, problems };
166
- const plugins: MarketIndexEntry[] = [...groups.entries()].map(([name, group]) => {
167
- const meta = group.meta?.doc ?? group.fallback?.doc;
168
- return {
169
- name,
170
- versions: sortMarketVersions([...group.versions.values()]),
171
- ...(meta?.title === undefined ? {} : { title: meta.title }),
172
- ...(meta?.description === undefined ? {} : { description: meta.description }),
173
- ...(meta?.author === undefined ? {} : { author: meta.author }),
174
- ...(meta?.owner === undefined ? {} : { owner: meta.owner }),
175
- ...(meta?.homepage === undefined ? {} : { homepage: meta.homepage }),
176
- ...(meta?.icon === undefined ? {} : { icon: meta.icon }),
177
- ...(meta?.metadata === undefined ? {} : { metadata: meta.metadata }),
178
- };
179
- });
180
- plugins.sort((left, right) => (left.name < right.name ? -1 : left.name > right.name ? 1 : 0));
85
+ const plugins = [...entries.values()].sort((left, right) => (left.name < right.name ? -1 : left.name > right.name ? 1 : 0));
181
86
  return { ok: true, index: { format: MARKET_INDEX_FORMAT, plugins } };
182
87
  }
183
88
 
89
+ export function marketEntryFileOf(entry: MarketIndexEntry, dir: string = MARKET_INDEX_DIR): MarketEntryFile {
90
+ return { path: marketEntryPath(dir, entry.name), text: `${JSON.stringify(entry, null, 2)}\n` };
91
+ }
92
+
184
93
  export function marketEntryFilesOf(index: MarketIndex, dir: string = MARKET_INDEX_DIR): readonly MarketEntryFile[] {
185
- const files: MarketEntryFile[] = [];
186
- for (const entry of index.plugins) {
187
- for (const version of entry.versions) {
188
- files.push({
189
- path: marketEntryPath(dir, entry.name, version.version),
190
- text: `${JSON.stringify(marketVersionDocOf(entry, version), null, 2)}\n`,
191
- });
192
- }
193
- }
194
- return files;
94
+ return index.plugins.map((entry) => marketEntryFileOf(healedUnit(entry), dir));
95
+ }
96
+
97
+ function healedUnit(entry: MarketIndexEntry): MarketIndexEntry {
98
+ if (entry.metadata?.unit !== undefined) return entry;
99
+ const unit = entryUnitOf(entry);
100
+ return unit === undefined ? entry : { ...entry, metadata: { ...entry.metadata, unit } };
195
101
  }
196
102
 
197
103
  export function collectMarketEntryFiles(dir: string): readonly MarketEntryFile[] {
@@ -0,0 +1,6 @@
1
+ import { dirname } from 'node:path';
2
+ import { fileURLToPath } from 'node:url';
3
+
4
+ export function packageRootOf(moduleUrl: string): string {
5
+ return dirname(dirname(dirname(fileURLToPath(moduleUrl))));
6
+ }
@@ -0,0 +1,21 @@
1
+ function envValue(
2
+ env: Readonly<Record<string, string | undefined>>,
3
+ name: string,
4
+ ): { readonly ok: true; readonly value: string } | { readonly ok: false } {
5
+ const value = env[name]?.trim();
6
+ return value === undefined || value === '' ? { ok: false } : { ok: true, value };
7
+ }
8
+
9
+ export function npmRegistryOf(
10
+ source: { readonly registryEnv?: string; readonly registryDefault?: string },
11
+ env: Readonly<Record<string, string | undefined>>,
12
+ ): { readonly ok: true; readonly value: string } | { readonly ok: false; readonly env: string } {
13
+ const name = source.registryEnv ?? '';
14
+ if (name !== '') {
15
+ const fromEnv = envValue(env, name);
16
+ if (fromEnv.ok) return fromEnv;
17
+ }
18
+ const fallback = source.registryDefault?.trim();
19
+ if (fallback === undefined || fallback === '') return { ok: false, env: name };
20
+ return { ok: true, value: fallback };
21
+ }
@@ -0,0 +1,76 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import type { DshShell } from '../api/dsh.js';
5
+ import { INSTALL_TIMEOUT_MS } from '../config/constants.js';
6
+ import { parseJsonLoose } from '../lib/text.js';
7
+ import { quoteShellArg } from './shell-text.js';
8
+ import { runShell } from './shell.js';
9
+ import { splitNpmSpec } from './package-kind.js';
10
+ import { localDirOf } from './local-declaration.js';
11
+
12
+ function cacheFlag(cacheDir: string | undefined): string {
13
+ return cacheDir === undefined ? '' : ` --cache ${quoteShellArg(cacheDir)}`;
14
+ }
15
+
16
+ function packedFileFrom(stdout: string, root: string): string | undefined {
17
+ const parsed = parseJsonLoose(stdout);
18
+ if (Array.isArray(parsed)) {
19
+ const first: unknown = parsed[0];
20
+ const filename =
21
+ typeof first === 'object' && first !== null ? (first as { filename?: unknown }).filename : undefined;
22
+ if (typeof filename === 'string' && filename.trim() !== '') {
23
+ const at = path.join(root, path.basename(filename.trim()));
24
+ if (fs.existsSync(at)) return at;
25
+ }
26
+ }
27
+ try {
28
+ const found = fs.readdirSync(root).filter((name) => name.endsWith('.tgz'));
29
+ return found.length === 1 ? path.join(root, found[0] ?? '') : undefined;
30
+ } catch {
31
+ return undefined;
32
+ }
33
+ }
34
+
35
+ export async function withPackageDir<T>(
36
+ options: {
37
+ readonly shell?: DshShell | undefined;
38
+ readonly spec: string;
39
+ readonly registry?: string | undefined;
40
+ readonly cacheDir?: string | undefined;
41
+ },
42
+ visit: (dir: string) => T,
43
+ ): Promise<T | undefined> {
44
+ const local = localDirOf(options.spec);
45
+ if (local !== undefined) return visit(local);
46
+ if (splitNpmSpec(options.spec) === undefined) return undefined;
47
+ const shell = options.shell;
48
+ if (shell === undefined) return undefined;
49
+
50
+ let root: string;
51
+ try {
52
+ root = fs.mkdtempSync(path.join(os.tmpdir(), 'wb-package-'));
53
+ } catch {
54
+ return undefined;
55
+ }
56
+ try {
57
+ const at = options.registry === undefined ? '' : ` --registry ${quoteShellArg(options.registry)}`;
58
+ const packed = await runShell(
59
+ shell,
60
+ `npm pack ${quoteShellArg(options.spec)} --ignore-scripts --json --pack-destination ${quoteShellArg(root)}${cacheFlag(options.cacheDir)}${at}`,
61
+ { timeoutMs: INSTALL_TIMEOUT_MS },
62
+ ).catch(() => undefined);
63
+ if (packed === undefined || packed.exitCode !== 0) return undefined;
64
+ const file = packedFileFrom(packed.stdout, root);
65
+ if (file === undefined) return undefined;
66
+ const unpacked = await runShell(shell, `tar -xzf ${quoteShellArg(file)} -C ${quoteShellArg(root)}`, {
67
+ timeoutMs: INSTALL_TIMEOUT_MS,
68
+ }).catch(() => undefined);
69
+ if (unpacked === undefined || unpacked.exitCode !== 0) return undefined;
70
+ const dir = path.join(root, 'package');
71
+ if (!fs.existsSync(dir)) return undefined;
72
+ return visit(dir);
73
+ } finally {
74
+ fs.rmSync(root, { recursive: true, force: true });
75
+ }
76
+ }
@@ -0,0 +1,63 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ export { splitNpmSpec } from './spec.js';
4
+ export type { NpmRef } from './spec.js';
5
+ import { dshRecordOf, packageManifestOf, packageNameOf } from './local-declaration.js';
6
+ import { pluginAt } from './plugin.js';
7
+
8
+ export type PackageKind = 'runtime-plugin' | 'dsh-bundle' | 'both' | 'neither';
9
+
10
+ export interface PackageVerdict {
11
+ readonly kind: PackageKind;
12
+ readonly pkg?: string;
13
+ readonly pluginName?: string;
14
+ }
15
+
16
+ function asRecord(value: unknown): Readonly<Record<string, unknown>> | undefined {
17
+ return typeof value === 'object' && value !== null ? (value as Readonly<Record<string, unknown>>) : undefined;
18
+ }
19
+
20
+ export function isCommandFile(name: string): boolean {
21
+ if (name.endsWith('.d.ts') || name.endsWith('.test.ts')) return false;
22
+ if (name.endsWith('.d.js')) return false;
23
+ return name.endsWith('.ts') || name.endsWith('.js');
24
+ }
25
+
26
+ export function pluginManifestOf(dir: string, manifestFile: string): Readonly<Record<string, unknown>> | undefined {
27
+ try {
28
+ const parsed: unknown = JSON.parse(fs.readFileSync(path.join(dir, manifestFile), 'utf8'));
29
+ if (Array.isArray(parsed)) return undefined;
30
+ return asRecord(parsed);
31
+ } catch {
32
+ return undefined;
33
+ }
34
+ }
35
+
36
+ export function hasCommandFile(dir: string): boolean {
37
+ try {
38
+ return fs.readdirSync(dir).some(isCommandFile);
39
+ } catch {
40
+ return false;
41
+ }
42
+ }
43
+
44
+ export function dshMarkersOf(raw: unknown): boolean {
45
+ const record = asRecord(raw);
46
+ return record !== undefined && ('bundle' in record || 'workbench' in record);
47
+ }
48
+
49
+ export function verdictOf(dir: string, manifestFile: string): PackageVerdict | undefined {
50
+ const plugin = pluginAt(dir, manifestFile);
51
+ if (packageManifestOf(dir) === undefined && plugin === undefined) return undefined;
52
+ const dsh = dshMarkersOf(dshRecordOf(dir));
53
+ const kind: PackageKind =
54
+ plugin !== undefined && dsh ? 'both' : plugin !== undefined ? 'runtime-plugin' : dsh ? 'dsh-bundle' : 'neither';
55
+ const pkg = packageNameOf(dir);
56
+ return {
57
+ kind,
58
+ ...(pkg === undefined ? {} : { pkg }),
59
+ ...(plugin?.name === undefined ? {} : { pluginName: plugin.name }),
60
+ };
61
+ }
62
+
63
+
@@ -0,0 +1,93 @@
1
+ import type { DshShell } from '../api/dsh.js';
2
+ import { VERSION_CHECK_TTL_MS } from '../config/constants.js';
3
+ import { dshMarkersOf, splitNpmSpec, verdictOf, type PackageVerdict } from './package-kind.js';
4
+ import { withPackageDir } from './package-content.js';
5
+ import { localDirOf } from './local-declaration.js';
6
+
7
+ export type PackageRouteKind = 'runtime-plugin' | 'dsh-bundle' | 'neither';
8
+
9
+ export interface PackageRoute {
10
+ readonly kind: PackageRouteKind;
11
+ readonly pkg?: string;
12
+ readonly pluginName?: string;
13
+ }
14
+
15
+ function routeOfVerdict(verdict: PackageVerdict, fallback?: string): PackageRoute {
16
+ const pkg = verdict.pkg ?? fallback;
17
+ if (verdict.kind === 'neither') return { kind: 'neither', ...(pkg === undefined ? {} : { pkg }) };
18
+ if (verdict.kind === 'dsh-bundle' || verdict.kind === 'both') return { kind: 'dsh-bundle' };
19
+ return {
20
+ kind: 'runtime-plugin',
21
+ ...(pkg === undefined ? {} : { pkg }),
22
+ ...(verdict.pluginName === undefined ? {} : { pluginName: verdict.pluginName }),
23
+ };
24
+ }
25
+
26
+ export interface PackageRouter {
27
+ packageRouteOf(options: RouteOptions): Promise<PackageRoute | undefined>;
28
+ reset(): void;
29
+ }
30
+
31
+ interface RouteOptions {
32
+ readonly spec: string;
33
+ readonly shell?: DshShell | undefined;
34
+ readonly registry?: string | undefined;
35
+ readonly force?: boolean;
36
+ }
37
+
38
+ export interface PackageRouterOptions {
39
+ readonly manifestFile: string;
40
+ readonly cacheDir?: string | undefined;
41
+ readonly dshLookup?: ((options: { shell: DshShell; name: string; registry: string }) => Promise<{ readable: boolean; raw: unknown }>) | undefined;
42
+ }
43
+
44
+ export function createPackageRouter(options: PackageRouterOptions): PackageRouter {
45
+ const cache = new Map<string, { at: number; route: PackageRoute | null }>();
46
+ return {
47
+ packageRouteOf: (routeOptions) => routeOf(cache, options, routeOptions),
48
+ reset: () => {
49
+ cache.clear();
50
+ },
51
+ };
52
+ }
53
+
54
+ async function routeOf(
55
+ cache: Map<string, { at: number; route: PackageRoute | null }>,
56
+ options: PackageRouterOptions,
57
+ routeOptions: RouteOptions,
58
+ ): Promise<PackageRoute | undefined> {
59
+ const local = localDirOf(routeOptions.spec);
60
+ if (local !== undefined) {
61
+ const verdict = verdictOf(local, options.manifestFile);
62
+ return verdict === undefined ? undefined : routeOfVerdict(verdict, splitNpmSpec(routeOptions.spec)?.name);
63
+ }
64
+
65
+ const ref = splitNpmSpec(routeOptions.spec);
66
+ if (ref === undefined) return undefined;
67
+ const shell = routeOptions.shell;
68
+ const registry = routeOptions.registry;
69
+ if (shell === undefined || registry === undefined) return undefined;
70
+
71
+ const key = `${registry}|${routeOptions.spec}`;
72
+ const hit = cache.get(key);
73
+ if (routeOptions.force !== true && hit !== undefined && Date.now() - hit.at < VERSION_CHECK_TTL_MS) {
74
+ return hit.route ?? undefined;
75
+ }
76
+
77
+ const found =
78
+ options.dshLookup === undefined
79
+ ? { readable: false, raw: undefined }
80
+ : await options.dshLookup({ shell, name: routeOptions.spec, registry });
81
+ let route: PackageRoute | undefined;
82
+ if (found.readable && dshMarkersOf(found.raw)) {
83
+ route = { kind: 'dsh-bundle' };
84
+ } else {
85
+ const verdict = await withPackageDir(
86
+ { shell, spec: routeOptions.spec, registry, ...(options.cacheDir === undefined ? {} : { cacheDir: options.cacheDir }) },
87
+ (dir) => verdictOf(dir, options.manifestFile),
88
+ );
89
+ route = verdict === undefined ? undefined : routeOfVerdict(verdict, ref.name);
90
+ }
91
+ cache.set(key, { at: Date.now(), route: route ?? null });
92
+ return route;
93
+ }
package/src/host/paths.ts CHANGED
@@ -1,55 +1,30 @@
1
- import { delimiter } from 'node:path';
2
1
  import { workbenchRoot } from './data-root.js';
3
2
 
4
- export function isolatedRoot(): string {
5
- return `${workbenchRoot()}/opencli`;
6
- }
7
-
8
- export function isolatedHome(): string {
9
- return `${isolatedRoot()}/home`;
10
- }
11
-
12
- export function vendorPrefix(): string {
13
- return `${isolatedRoot()}/plugins-npm`;
14
- }
15
-
16
- export function vendorPluginsDir(): string {
17
- return `${vendorPrefix()}/node_modules`;
18
- }
19
-
20
- export function libsPrefix(): string {
21
- return `${isolatedRoot()}/libs`;
22
- }
23
-
24
- export const OPENCLI_NPM_PACKAGE = '@jackwener/opencli';
25
-
26
- export function toolchainOpencliDir(): string {
27
- return `${isolatedRoot()}/toolchain/node_modules/${OPENCLI_NPM_PACKAGE}`;
28
- }
29
-
30
- export function vendorPeerLink(): { readonly dir: string; readonly parent: string } {
31
- const base = `${vendorPrefix()}/node_modules`;
32
- const [scope] = OPENCLI_NPM_PACKAGE.split('/');
33
- return { dir: `${base}/${OPENCLI_NPM_PACKAGE}`, parent: `${base}/${scope}` };
34
- }
35
-
36
- export function npmCacheDir(): string {
37
- return `${isolatedRoot()}/npm-cache`;
38
- }
39
-
40
- export function libsBinDir(): string {
41
- return `${libsPrefix()}/node_modules/.bin`;
42
- }
43
-
44
- /**
45
- * 把隔离运行时的共用依赖 bin 目录放到 PATH 最前面(可执行文件都从这儿找)。
46
- * @param env 改哪份环境(默认当前进程)。
47
- * @returns 真的插进去了就 `true`;已经在里面就 `false`。
48
- */
49
- export function ensureLibsBinOnPath(env: NodeJS.ProcessEnv = process.env): boolean {
50
- const dir = libsBinDir();
51
- const parts = (env.PATH ?? '').split(delimiter).filter((one) => one !== '');
52
- if (parts.includes(dir)) return false;
53
- env.PATH = [dir, ...parts].join(delimiter);
54
- return true;
3
+ export interface RuntimeLayout {
4
+ readonly root: string;
5
+ readonly home: string;
6
+ readonly vendorPrefix: string;
7
+ readonly vendorPluginsDir: string;
8
+ readonly libsPrefix: string;
9
+ readonly libsBinDir: string;
10
+ readonly npmCacheDir: string;
11
+ }
12
+
13
+ export function runtimeRootOf(id: string): string {
14
+ return `${workbenchRoot()}/${id}`;
15
+ }
16
+
17
+ export function runtimeLayoutOf(id: string): RuntimeLayout {
18
+ const root = runtimeRootOf(id);
19
+ const vendorPrefix = `${root}/plugins-npm`;
20
+ const libsPrefix = `${root}/libs`;
21
+ return {
22
+ root,
23
+ home: `${root}/home`,
24
+ vendorPrefix,
25
+ vendorPluginsDir: `${vendorPrefix}/node_modules`,
26
+ libsPrefix,
27
+ libsBinDir: `${libsPrefix}/node_modules/.bin`,
28
+ npmCacheDir: `${root}/npm-cache`,
29
+ };
55
30
  }
@@ -41,7 +41,7 @@ export function hostSurface(input: HostSurfaceInput = {}): HostSurface {
41
41
  restartHint: deployment === 'desktop' ? '退出并重开 Desktop' : '重启一次 dsh',
42
42
  toolchain: {
43
43
  node: { source: deployment === 'desktop' ? 'desktop-shim' : 'path' },
44
- npm: { neededFor: 'opencli-install' },
44
+ npm: { neededFor: 'runtime-install' },
45
45
  pnpm: { neededFor: 'workshop', expectedMajor: 10 },
46
46
  },
47
47
  };