@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,7 +1,7 @@
1
1
  import { describe, expect, it } from 'vitest';
2
- import type { DataReader } from '../contract/data.js';
3
- import type { ExtensionNeed } from '../contract/needs.js';
4
- import { extensionCallerOf } from './extension.js';
2
+ import type { DataReader } from '../../contract/data.js';
3
+ import type { ExtensionNeed } from '../../contract/needs.js';
4
+ import { extensionCallerOf } from '../extension.js';
5
5
 
6
6
  const NEED: ExtensionNeed = {
7
7
  kind: 'extension',
@@ -32,7 +32,7 @@ describe('extensionCallerOf', () => {
32
32
  await expect(caller.json(['issue-statuses'])).rejects.toMatchObject({
33
33
  code: 'cli-unclassified',
34
34
  message: expect.stringContaining('workbench_modules_update(id=coding-issues)'),
35
- hint: expect.stringContaining('opencli coding issue-statuses --help'),
35
+ hint: expect.stringContaining('coding issue-statuses --help'),
36
36
  detail: "error: unknown command 'issue-statuses'",
37
37
  });
38
38
  });
@@ -4,7 +4,7 @@ import {
4
4
  installFailureOf,
5
5
  installTargets,
6
6
  installWriteProblem,
7
- } from './install-failure.js';
7
+ } from '../install-failure.js';
8
8
 
9
9
  const CURRENT = { uid: 502, gid: 20 };
10
10
 
@@ -8,7 +8,7 @@ import {
8
8
  packageSourceOf,
9
9
  readInstalledSources,
10
10
  rememberPackageSource,
11
- } from './installed-sources.js';
11
+ } from '../installed-sources.js';
12
12
 
13
13
  const dirs: string[] = [];
14
14
 
@@ -1,10 +1,10 @@
1
1
  import { describe, expect, it, vi } from 'vitest';
2
2
  import type { LibNeed } from '../../contract/needs.js';
3
3
  import type { LibReader } from '../../contract/lib.js';
4
- import type { OpencliRunner } from '../opencli-runner.js';
4
+ import type { LibRunner } from '../../contract/runner.js';
5
5
  import { withLibRunners } from '../lib-runners.js';
6
6
 
7
- const runner: OpencliRunner = async () => ({ stdout: '[]' });
7
+ const runner: LibRunner = async () => ({ stdout: '[]' });
8
8
 
9
9
  function needOf(runs?: readonly string[]): LibNeed {
10
10
  return {
@@ -28,6 +28,15 @@ function logger(): { readonly warn: ReturnType<typeof vi.fn<(message: string) =>
28
28
  }
29
29
 
30
30
  describe('withLibRunners', () => {
31
+ it('★ 换一个运行时名字(mcp)照样注入:外壳不认具体名字,只看清单的 runs', async () => {
32
+ const module = { setMcpRunner: vi.fn() };
33
+ const libs = withLibRunners(libsOf(module), { mcp: runner }, logger());
34
+
35
+ await libs.load(needOf(['mcp']), 'src/run.js');
36
+
37
+ expect(module.setMcpRunner).toHaveBeenCalledWith(runner);
38
+ });
39
+
31
40
  it('没声明 runs → 原样返回,不注入、不记 warn', async () => {
32
41
  const module = { setOpencliRunner: vi.fn() };
33
42
  const warn = logger();
@@ -0,0 +1,26 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import type { MarketIndex } from '../../contract/protocol/market-index.js';
3
+ import { assembleMarketIndex, marketEntryFilesOf } from '../market-index.js';
4
+
5
+ describe('市场索引的迁移', () => {
6
+ it('迁移时把老产物路径里的 unit 补进元数据', () => {
7
+ const legacy: MarketIndex = {
8
+ format: 1,
9
+ plugins: [
10
+ {
11
+ name: '@scope/module-x',
12
+ versions: [
13
+ {
14
+ version: '0.1.0',
15
+ source: { type: 'git', url: 'https://git.example.com/m.git', commit: 'a'.repeat(40), path: 'packages/x' },
16
+ },
17
+ ],
18
+ },
19
+ ],
20
+ };
21
+ const files = marketEntryFilesOf(legacy, 'market');
22
+ const entry = JSON.parse(files[0]?.text ?? '{}');
23
+ expect(entry.metadata).toEqual({ unit: 'x' });
24
+ expect(assembleMarketIndex(files).ok).toBe(true);
25
+ });
26
+ });
@@ -8,9 +8,8 @@ import {
8
8
  marketEntryFilesOf,
9
9
  marketEntryPath,
10
10
  marketInstallTarget,
11
- marketMetaPath,
12
11
  marketSourceKey,
13
- parseMarketEntryDoc,
12
+ parseMarketEntryJson,
14
13
  parseMarketIndex,
15
14
  parseMarketIndexJson,
16
15
  parseSourceConfig,
@@ -353,75 +352,69 @@ describe('索引源与安装目标', () => {
353
352
  });
354
353
  });
355
354
 
356
- describe('条目文件:一个版本一个文件', () => {
357
- const versionDoc = (version: string, timestamp: number, title: string) => ({
355
+ describe('条目文件:一个包一个文件', () => {
356
+ const entryOf = () => ({
358
357
  name: '@scope/module-x',
359
- version,
360
- source: { type: 'npm' },
361
- publishTimestamp: timestamp,
362
- title: { zh: title },
358
+ title: { zh: '探针' },
359
+ versions: [{ version: '0.1.0', source: { type: 'npm' as const }, publishTimestamp: 1_760_000_000 }],
363
360
  });
364
361
 
365
- it('路径按包名分层,meta 文件带 epoch 与随机后缀', () => {
366
- expect(marketEntryPath('market', '@scope/module-x', '0.1.0')).toBe('market/@scope/module-x/0.1.0.json');
367
- expect(marketMetaPath('market', '@scope/module-x', 3, 'a1b2c3')).toBe(
368
- 'market/@scope/module-x/meta-3-a1b2c3.json',
369
- );
370
- expect(marketEntryPath('market', 'plain', '1.0.0')).toBe('market/plain/1.0.0.json');
362
+ it('路径是 market/<包名>.json', () => {
363
+ expect(marketEntryPath('market', '@scope/module-x')).toBe('market/@scope/module-x.json');
364
+ expect(marketEntryPath('market', 'plain')).toBe('market/plain.json');
371
365
  });
372
366
 
373
- it('包名/版本号不合法就不给路径(防止写出目录外)', () => {
374
- expect(() => marketEntryPath('market', '../escape', '1.0.0')).toThrow();
375
- expect(() => marketEntryPath('market', 'ok', '../1.0.0')).toThrow();
367
+ it('包名不合法就不给路径(防止写出目录外)', () => {
368
+ expect(() => marketEntryPath('market', '../escape')).toThrow();
369
+ expect(() => marketEntryPath('market', 'Bad Name')).toThrow();
376
370
  });
377
371
 
378
372
  it('单条文档能解析,坏文档整条拒', () => {
379
- const good = parseMarketEntryDoc(versionDoc('0.1.0', 1_760_000_000, '探针'));
373
+ const good = parseMarketEntryJson(JSON.stringify(entryOf()));
380
374
  expect(good.ok).toBe(true);
381
375
  if (!good.ok) return;
382
- expect(good.doc.name).toBe('@scope/module-x');
383
- const bad = parseMarketEntryDoc({ name: 'Bad Name', version: '0.1.0', source: { type: 'npm' } });
384
- expect(bad.ok).toBe(false);
385
- const noSource = parseMarketEntryDoc({ name: '@scope/x', version: '0.1.0' });
386
- expect(noSource.ok).toBe(false);
387
- });
388
-
389
- it('组装:版本全收并按 SemVer 降序,元数据取最新那份', () => {
390
- const files = [
391
- { path: 'market/@scope/module-x/0.1.0.json', text: JSON.stringify(versionDoc('0.1.0', 1_760_000_000, '旧标题')) },
392
- { path: 'market/@scope/module-x/0.2.0.json', text: JSON.stringify(versionDoc('0.2.0', 1_760_000_100, '新标题')) },
393
- { path: 'market/@scope/module-x/meta-1760000200000.json', text: JSON.stringify({
394
- name: '@scope/module-x',
395
- publishTimestamp: 1_760_000_200,
396
- title: { zh: '最新标题' },
397
- }) },
398
- ];
399
- const assembled = assembleMarketIndex(files);
376
+ expect(good.entry.name).toBe('@scope/module-x');
377
+ expect(parseMarketEntryJson(JSON.stringify({ name: 'Bad Name', versions: [] })).ok).toBe(false);
378
+ expect(parseMarketEntryJson(JSON.stringify({ name: '@scope/x' })).ok).toBe(false);
379
+ expect(parseMarketEntryJson('{oops').ok).toBe(false);
380
+ });
381
+
382
+ it('组装:版本按 SemVer 降序,一个包一条', () => {
383
+ const assembled = assembleMarketIndex([
384
+ {
385
+ path: 'market/@scope/module-x.json',
386
+ text: JSON.stringify({
387
+ ...entryOf(),
388
+ versions: [
389
+ { version: '0.2.0', source: { type: 'npm' }, publishTimestamp: 1_760_000_100 },
390
+ { version: '0.1.0', source: { type: 'npm' }, publishTimestamp: 1_760_000_000 },
391
+ ],
392
+ }),
393
+ },
394
+ ]);
400
395
  expect(assembled.ok).toBe(true);
401
396
  if (!assembled.ok) return;
402
- const entry = assembled.index.plugins[0];
403
- expect(entry?.versions.map((one) => one.version)).toEqual(['0.2.0', '0.1.0']);
404
- expect(entry?.title?.zh).toBe('最新标题');
397
+ expect(assembled.index.plugins[0]?.versions.map((one) => one.version)).toEqual(['0.2.0', '0.1.0']);
405
398
  });
406
399
 
407
- it('组装:同一个版本出现两次就整份拒(那是真重复发布)', () => {
400
+ it('组装:同一个包出现两次就整份拒', () => {
408
401
  const assembled = assembleMarketIndex([
409
- { path: 'a/0.1.0.json', text: JSON.stringify(versionDoc('0.1.0', 1_760_000_000, '甲')) },
410
- { path: 'b/0.1.0.json', text: JSON.stringify(versionDoc('0.1.0', 1_760_000_100, '乙')) },
402
+ { path: 'a.json', text: JSON.stringify(entryOf()) },
403
+ { path: 'b.json', text: JSON.stringify(entryOf()) },
411
404
  ]);
412
405
  expect(assembled.ok).toBe(false);
413
406
  if (assembled.ok) return;
414
- expect(assembled.problems.some((one) => one.kind === 'duplicate-version')).toBe(true);
407
+ expect(assembled.problems.some((one) => one.kind === 'duplicate-name')).toBe(true);
415
408
  });
416
409
 
417
410
  it('从目录读只收 .json,README 不当条目', () => {
418
411
  const dir = mkdtempSync(join(tmpdir(), 'wb-entry-'));
419
412
  try {
420
- mkdirSync(join(dir, '@scope/module-x'), { recursive: true });
413
+ mkdirSync(dir, { recursive: true });
421
414
  writeFileSync(join(dir, 'README.md'), 'index');
422
- writeFileSync(join(dir, '@scope/module-x', '0.1.0.json'), JSON.stringify(versionDoc('0.1.0', 1_760_000_000, '探针')));
415
+ writeFileSync(join(dir, '@scope-module-x.json'), JSON.stringify(entryOf()));
423
416
  const files = collectMarketEntryFiles(dir);
424
- expect(files.map((one) => one.path)).toEqual(['@scope/module-x/0.1.0.json']);
417
+ expect(files.map((one) => one.path)).toEqual(['@scope-module-x.json']);
425
418
  const assembled = assembleMarketIndex(files);
426
419
  expect(assembled.ok).toBe(true);
427
420
  if (!assembled.ok) return;
@@ -431,7 +424,7 @@ describe('条目文件:一个版本一个文件', () => {
431
424
  }
432
425
  });
433
426
 
434
- it('迁移:一份 format 1 能拆成条目文件,再组装回来一模一样', () => {
427
+ it('迁移:一份 format 1 拆成每包一个文件,再组装回来一模一样', () => {
435
428
  const legacy: MarketIndex = {
436
429
  format: 1,
437
430
  plugins: [
@@ -447,10 +440,7 @@ describe('条目文件:一个版本一个文件', () => {
447
440
  ],
448
441
  };
449
442
  const files = marketEntryFilesOf(legacy, 'market');
450
- expect(files.map((one) => one.path)).toEqual([
451
- 'market/@scope/module-x/0.2.0.json',
452
- 'market/@scope/module-x/0.1.0.json',
453
- ]);
443
+ expect(files.map((one) => one.path)).toEqual(['market/@scope/module-x.json']);
454
444
  const assembled = assembleMarketIndex(files);
455
445
  expect(assembled.ok).toBe(true);
456
446
  if (!assembled.ok) return;
@@ -13,7 +13,7 @@ const HOME = '/h/workbench/opencli/home';
13
13
 
14
14
  function entry(over: Partial<PluginLedgerEntry> = {}): PluginLedgerEntry {
15
15
  return {
16
- opencliHome: HOME,
16
+ home: HOME,
17
17
  package: 'opencli-plugin-inventory',
18
18
  installedAt: '2026-10-14T00:00:00.000Z',
19
19
  ...over,
@@ -44,11 +44,11 @@ describe('台账条目的「放手」那一笔', () => {
44
44
  });
45
45
 
46
46
  it('另一个 home 的那一笔不受影响(隔离 wrapper 会换 home)', () => {
47
- const other = entry({ opencliHome: '/h/global/opencli/home' });
47
+ const other = entry({ home: '/h/global/opencli/home' });
48
48
  const released = withReleasedEntry([entry(), other], HOME, 'opencli-plugin-inventory', '2026-10-15T00:00:00.000Z');
49
49
 
50
- expect(released.find((item) => item.opencliHome !== HOME)?.releasedAt).toBeUndefined();
51
- expect(heldEntriesOfHome(released, other.opencliHome)).toHaveLength(1);
50
+ expect(released.find((item) => item.home !== HOME)?.releasedAt).toBeUndefined();
51
+ expect(heldEntriesOfHome(released, other.home)).toHaveLength(1);
52
52
  });
53
53
 
54
54
  it('★ 按插件名找"用户持着的"那一份:插件名与包名两边都认(§3.4 那条映射)', () => {
@@ -67,8 +67,8 @@ describe('台账条目的「放手」那一笔', () => {
67
67
  });
68
68
 
69
69
  it('键还是「home + 包名」(同一份文件里两个 home 不互相撒谎)', () => {
70
- expect(ledgerKeyOf({ opencliHome: '/a', package: 'x' })).not.toBe(
71
- ledgerKeyOf({ opencliHome: '/b', package: 'x' }),
70
+ expect(ledgerKeyOf({ home: '/a', package: 'x' })).not.toBe(
71
+ ledgerKeyOf({ home: '/b', package: 'x' }),
72
72
  );
73
73
  });
74
74
  });
@@ -92,3 +92,16 @@ describe('物理去留:由谁主张说话(动作与结果分层那条)', (
92
92
  expect(pluginRetentionOf({ held: false, claimedBy: [], trustworthy: true })).toBe('remove');
93
93
  });
94
94
  });
95
+
96
+ describe('落盘字段改名后的老台账', () => {
97
+ it('★ 老文件里的 opencliHome 还认(改名不动数据)', () => {
98
+ const entries = parsePluginLedger({
99
+ entries: [
100
+ { opencliHome: '/dsh/home-a', package: 'demo', installedAt: '2026-10-14T00:00:00.000Z' },
101
+ { home: '/dsh/home-b', package: 'other', installedAt: '2026-10-14T00:00:00.000Z' },
102
+ ],
103
+ });
104
+
105
+ expect(entries.map((entry) => entry.home)).toEqual(['/dsh/home-a', '/dsh/home-b']);
106
+ });
107
+ });
@@ -1,7 +1,7 @@
1
1
  import { describe, expect, it } from 'vitest';
2
- import type { CallOverrides, RequestRecipe } from '../contract/data.js';
3
- import { describeRecipe, scriptOf, scriptOfMany } from './recipe.js';
4
- import { callsProblemOf, MAX_RECIPE_CALLS, recipeProblemOf } from './recipe-rules.js';
2
+ import type { CallOverrides, RequestRecipe } from '../../contract/data.js';
3
+ import { describeRecipe, scriptOf, scriptOfMany } from '../recipe.js';
4
+ import { callsProblemOf, MAX_RECIPE_CALLS, recipeProblemOf } from '../recipe-rules.js';
5
5
 
6
6
  const BASE: RequestRecipe = {
7
7
  origin: 'https://x.example.com',
@@ -0,0 +1,14 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { runtimeLayoutOf, runtimeRootOf } from '../paths.js';
3
+
4
+ describe('运行时根的算法(这里不认识任何具体运行时)', () => {
5
+ it('就是 workbench 根加一个 id,别处自己拼下面的布局', () => {
6
+ const layout = runtimeLayoutOf('demo-runtime');
7
+
8
+ expect(layout.root).toBe(runtimeRootOf('demo-runtime'));
9
+ expect(layout.root.endsWith('/workbench/demo-runtime')).toBe(true);
10
+ expect(layout.vendorPluginsDir).toBe(`${layout.root}/plugins-npm/node_modules`);
11
+ expect(layout.libsPrefix).toBe(`${layout.root}/libs`);
12
+ expect(layout.npmCacheDir).toBe(`${layout.root}/npm-cache`);
13
+ });
14
+ });
@@ -51,7 +51,7 @@ describe('供给索引:一个包只有一个来源', () => {
51
51
  const index = buildSupplyIndex([
52
52
  manifest('a', [extension('coding', [npm('opencli-plugin-coding')])]),
53
53
  manifest('b', [extension('coding')]),
54
- ]);
54
+ ], { packagePrefix: 'opencli-plugin-' });
55
55
 
56
56
  expect(index.conflicts.map((item) => item.kind)).toEqual(['source-mismatch']);
57
57
  expect(index.entryOf('extension', 'coding')?.conflicted).toBe(true);
@@ -152,7 +152,7 @@ describe('供给索引:一个包只有一个来源', () => {
152
152
  });
153
153
 
154
154
  it('没写 sources 的 extension 按约定落包名,进索引但不代装', () => {
155
- const index = buildSupplyIndex([manifest('a', [extension('coding')])]);
155
+ const index = buildSupplyIndex([manifest('a', [extension('coding')])], { packagePrefix: 'opencli-plugin-' });
156
156
 
157
157
  expect(index.entryOf('extension', 'coding')?.package).toBe('opencli-plugin-coding');
158
158
  expect(index.sourcesOf('extension', 'coding')).toEqual([]);
@@ -0,0 +1,32 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { supplyPortOf } from '../supply-port.js';
3
+ import { WORKBENCH_ADAPTERS } from '../adapters.js';
4
+ import { fakeAdapterSet } from '../../testing/fake-adapter-set.js';
5
+
6
+ function ctxOf(set: ReturnType<typeof fakeAdapterSet>): never {
7
+ return { get: (name: string) => (name === WORKBENCH_ADAPTERS ? set : undefined) } as never;
8
+ }
9
+
10
+ describe('组合出给外壳的 supply 端口', () => {
11
+ it('给了 CLI 目录就透传(opencli 那类运行时)', () => {
12
+ const port = supplyPortOf(ctxOf(fakeAdapterSet()));
13
+
14
+ expect(port.cliCatalogPath?.()).toBe('/tmp/dsh-workbench-fake/catalog.json');
15
+ expect(port.vendorPluginsDir?.()).toContain('dsh-workbench-fake');
16
+ expect(port.runtimeInstallCommand?.()).toBeTruthy();
17
+ });
18
+
19
+ it('★ 没给就整条不挂(纯 API 的运行时不用瞎编目录)', () => {
20
+ const set = fakeAdapterSet();
21
+ const modules = { ...set.modules } as Record<string, unknown>;
22
+ delete modules['cliCatalogPath'];
23
+ delete modules['vendorPluginsDir'];
24
+ delete modules['runtimeInstallCommand'];
25
+ const port = supplyPortOf(ctxOf({ ...set, modules } as never));
26
+
27
+ expect(port.cliCatalogPath).toBeUndefined();
28
+ expect(port.vendorPluginsDir).toBeUndefined();
29
+ expect(port.runtimeInstallCommand).toBeUndefined();
30
+ expect(port.isolatedRoot()).toBeTruthy();
31
+ });
32
+ });
@@ -0,0 +1,93 @@
1
+ import type { DshHostContext } from '../api/dsh.js';
2
+ import type { AdapterSet } from './adapters.js';
3
+ import { validateAdapterSet } from './adapter-set.js';
4
+
5
+ const CAPABILITY_KEYS = ['console', 'pluginMarket', 'browserBridge'] as const;
6
+ const DATA_METHODS = ['read', 'request', 'requestMany', 'submit'] as const;
7
+
8
+ function isNonEmptyString(value: unknown): boolean {
9
+ return typeof value === 'string' && value.trim() !== '';
10
+ }
11
+
12
+ const CLI_DIR_METHODS = [
13
+ ['cliCatalogPath', 'cliCatalogPath'],
14
+ ['vendorPluginsDir', 'vendorPluginsDir'],
15
+ ] as const;
16
+
17
+ export function adapterContractGaps(set: AdapterSet, ctx: DshHostContext): readonly string[] {
18
+ const gaps: string[] = [];
19
+ const validated = validateAdapterSet(set, '契约');
20
+ if (!validated.ok) {
21
+ gaps.push(`成员不全:${validated.detail}`);
22
+ return gaps;
23
+ }
24
+
25
+ if (!Number.isInteger(set.apiVersion) || set.apiVersion < 1) {
26
+ gaps.push(`apiVersion 不是正整数:${String(set.apiVersion)}`);
27
+ }
28
+
29
+ const capabilities = set.runtime.capabilities(ctx);
30
+ for (const key of CAPABILITY_KEYS) {
31
+ if (typeof capabilities[key] !== 'boolean') gaps.push(`capabilities.${key} 不是布尔值`);
32
+ }
33
+
34
+ if (typeof set.runtime.profile === 'function') {
35
+ const profile = set.runtime.profile(ctx);
36
+ if (!isNonEmptyString(profile?.label)) gaps.push('runtime.profile().label 不是非空字符串');
37
+ for (const key of ['plugin', 'pluginOf', 'pluginThing', 'pluginArea'] as const) {
38
+ const text = profile?.terms?.[key];
39
+ if (!isNonEmptyString(text?.zh) || !isNonEmptyString(text?.en)) {
40
+ gaps.push(`runtime.profile().terms.${key} 要给 zh / en 两份非空文案`);
41
+ }
42
+ }
43
+ if (!isNonEmptyString(profile?.installHint?.zh) || !isNonEmptyString(profile?.installHint?.en)) {
44
+ gaps.push('runtime.profile().installHint 要给 zh / en 两份非空文案');
45
+ }
46
+ }
47
+
48
+ if (typeof set.runtime.commandSurface === 'function') {
49
+ const surface = set.runtime.commandSurface(ctx);
50
+ if (surface !== undefined) {
51
+ if (!isNonEmptyString(surface.name)) gaps.push('runtime.commandSurface().name 不是非空字符串');
52
+ if (!isNonEmptyString(surface.pluginPrefix)) {
53
+ gaps.push('runtime.commandSurface().pluginPrefix 不是非空字符串');
54
+ }
55
+ if (typeof surface.invoke !== 'function') gaps.push('runtime.commandSurface().invoke 不是函数');
56
+ else if (!isNonEmptyString(surface.invoke('site', 'command'))) {
57
+ gaps.push('runtime.commandSurface().invoke(site, command) 拼不出非空命令行');
58
+ }
59
+ if (typeof surface.help !== 'function') gaps.push('runtime.commandSurface().help 不是函数');
60
+ else if (!isNonEmptyString(surface.help('site'))) {
61
+ gaps.push('runtime.commandSurface().help(site) 拼不出非空提示');
62
+ }
63
+ }
64
+ }
65
+
66
+ const health = set.runtime.health(ctx);
67
+ const commands = set.runtime.commands(ctx);
68
+ for (const [name, status] of Object.entries(health)) {
69
+ if (!isNonEmptyString(status?.command)) gaps.push(`health['${name}'].command 不是非空字符串`);
70
+ if (!['env', 'isolated', 'path'].includes(status?.source)) gaps.push(`health['${name}'].source 不是已知来源`);
71
+ }
72
+ for (const name of Object.keys(commands)) {
73
+ if (health[name] === undefined) gaps.push(`commands 里的 ${name} 在 health 里没有对应条目`);
74
+ else if (!isNonEmptyString(commands[name])) gaps.push(`commands['${name}'] 不是非空字符串`);
75
+ }
76
+
77
+ for (const method of DATA_METHODS) {
78
+ if (typeof set.data[method] !== 'function') gaps.push(`data.${method} 不是函数`);
79
+ }
80
+
81
+ if (!isNonEmptyString(set.modules.isolatedRoot())) gaps.push('modules.isolatedRoot() 不是非空字符串');
82
+ if (set.modules.runtimeInstallCommand !== undefined && !isNonEmptyString(set.modules.runtimeInstallCommand())) {
83
+ gaps.push('modules.runtimeInstallCommand() 给了但不是非空字符串');
84
+ }
85
+ for (const [name, method] of CLI_DIR_METHODS) {
86
+ const given = set.modules[method];
87
+ if (given !== undefined && !isNonEmptyString(given())) gaps.push(`modules.${method}() 给了但不是非空字符串(${name})`);
88
+ }
89
+ if (typeof set.probe.probeUnits !== 'function') gaps.push('probe.probeUnits 不是函数');
90
+ if (typeof set.probe.registerPreflight !== 'function') gaps.push('probe.registerPreflight 不是函数');
91
+
92
+ return gaps;
93
+ }
@@ -61,10 +61,9 @@ const CORE_METHODS = [
61
61
  const FULL_MEMBERS = ['modules', 'runtime', 'plugins', 'actions'] as const;
62
62
 
63
63
  const FULL_METHODS = [
64
+ ['modules', 'isolatedRoot'],
64
65
  ['modules', 'isolatedHome'],
65
- ['modules', 'cliCatalogPath'],
66
- ['modules', 'vendorPluginsDir'],
67
- ['modules', 'opencliInstallCommand'],
66
+ ['modules', 'afterLibInstall'],
68
67
  ['modules', 'profileDirOf'],
69
68
  ['modules', 'profileModules'],
70
69
  ['modules', 'readInstalledModule'],
@@ -81,7 +80,10 @@ const FULL_METHODS = [
81
80
  ['modules', 'isNewerVersion'],
82
81
  ['modules', 'startSelfUpdate'],
83
82
  ['runtime', 'packageRoot'],
84
- ['runtime', 'opencliCommand'],
83
+ ['runtime', 'capabilities'],
84
+ ['runtime', 'commands'],
85
+ ['runtime', 'runners'],
86
+ ['runtime', 'health'],
85
87
  ['runtime', 'seed'],
86
88
  ['runtime', 'warnUnwritableRoot'],
87
89
  ['plugins', 'declarationLookup'],
@@ -77,7 +77,7 @@ export interface SelfUpdateStart {
77
77
  readonly forward: string;
78
78
  }
79
79
 
80
- export interface OpencliResolution {
80
+ export interface RuntimeStatus {
81
81
  readonly command: string;
82
82
  readonly source: 'env' | 'isolated' | 'path';
83
83
  readonly isolated: string | null;