@dsh-workbench/shared 1.1.8 → 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 (224) 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/platform.d.ts +1 -1
  20. package/dist/contract/runner.d.ts +6 -0
  21. package/dist/contract/runner.js +1 -0
  22. package/dist/contract/unit.d.ts +4 -4
  23. package/dist/host/adapter-contract.d.ts +3 -0
  24. package/dist/host/adapter-contract.js +93 -0
  25. package/dist/host/adapter-set.js +6 -4
  26. package/dist/host/adapter-types.d.ts +1 -1
  27. package/dist/host/bundle-manager.d.ts +3 -0
  28. package/dist/host/bundle-manager.js +181 -0
  29. package/dist/host/cli.js +3 -1
  30. package/dist/host/config-sync.d.ts +48 -0
  31. package/dist/host/config-sync.js +342 -0
  32. package/dist/host/dsh-plugins.d.ts +5 -0
  33. package/dist/host/dsh-plugins.js +51 -0
  34. package/dist/host/extension.js +1 -1
  35. package/dist/host/flow/check.d.ts +9 -0
  36. package/dist/host/flow/check.js +179 -0
  37. package/dist/host/flow/expr.d.ts +30 -0
  38. package/dist/host/flow/expr.js +99 -0
  39. package/dist/host/flow/kit.d.ts +14 -0
  40. package/dist/host/flow/kit.js +13 -0
  41. package/dist/host/flow/run.d.ts +13 -0
  42. package/dist/host/flow/run.js +287 -0
  43. package/dist/host/flow/types.d.ts +188 -0
  44. package/dist/host/flow/types.js +1 -0
  45. package/dist/host/host-actions.d.ts +2 -0
  46. package/dist/host/host-actions.js +5 -0
  47. package/dist/host/host-kit.d.ts +19 -0
  48. package/dist/host/host-kit.js +14 -0
  49. package/dist/host/http.d.ts +2 -6
  50. package/dist/host/http.js +2 -2
  51. package/dist/host/installed-module.d.ts +23 -0
  52. package/dist/host/installed-module.js +264 -0
  53. package/dist/host/lib-install.d.ts +7 -9
  54. package/dist/host/lib-install.js +9 -12
  55. package/dist/host/lib-runners.d.ts +3 -5
  56. package/dist/host/lib-runners.js +2 -2
  57. package/dist/host/local-declaration.d.ts +7 -0
  58. package/dist/host/local-declaration.js +33 -0
  59. package/dist/host/module-root.d.ts +1 -0
  60. package/dist/host/module-root.js +5 -0
  61. package/dist/host/npm-registry.d.ts +10 -0
  62. package/dist/host/npm-registry.js +16 -0
  63. package/dist/host/opencli-runner.d.ts +2 -1
  64. package/dist/host/package-content.d.ts +7 -0
  65. package/dist/host/package-content.js +69 -0
  66. package/dist/host/package-kind.d.ts +13 -0
  67. package/dist/host/package-kind.js +51 -0
  68. package/dist/host/package-route.d.ts +31 -0
  69. package/dist/host/package-route.js +57 -0
  70. package/dist/host/paths.d.ts +11 -19
  71. package/dist/host/paths.js +16 -43
  72. package/dist/host/platform.js +1 -1
  73. package/dist/host/plugin-ledger.d.ts +8 -8
  74. package/dist/host/plugin-ledger.js +23 -16
  75. package/dist/host/plugin.d.ts +1 -2
  76. package/dist/host/plugin.js +4 -5
  77. package/dist/host/port-types.d.ts +42 -9
  78. package/dist/host/preflight.d.ts +3 -2
  79. package/dist/host/profile-create.d.ts +18 -0
  80. package/dist/host/profile-create.js +118 -0
  81. package/dist/host/profile-registries.d.ts +10 -0
  82. package/dist/host/profile-registries.js +150 -0
  83. package/dist/host/registry-declaration.d.ts +21 -0
  84. package/dist/host/registry-declaration.js +33 -0
  85. package/dist/host/release-probe.d.ts +34 -0
  86. package/dist/host/release-probe.js +88 -0
  87. package/dist/host/runner.d.ts +26 -0
  88. package/dist/host/runner.js +50 -0
  89. package/dist/host/shell.d.ts +0 -2
  90. package/dist/host/shell.js +0 -7
  91. package/dist/host/spec.js +1 -1
  92. package/dist/host/supply-index.d.ts +1 -0
  93. package/dist/host/supply-index.js +8 -7
  94. package/dist/host/supply-port.js +13 -3
  95. package/dist/host/unit-face.d.ts +2 -1
  96. package/dist/host/version.js +11 -6
  97. package/dist/host.d.ts +24 -6
  98. package/dist/host.js +23 -6
  99. package/dist/i18n/en.js +16 -19
  100. package/dist/i18n/index.d.ts +17 -20
  101. package/dist/i18n/runtime-terms.d.ts +15 -0
  102. package/dist/i18n/runtime-terms.js +22 -0
  103. package/dist/i18n/zh.d.ts +16 -19
  104. package/dist/i18n/zh.js +16 -19
  105. package/dist/index.d.ts +4 -0
  106. package/dist/index.js +4 -0
  107. package/dist/testing/fake-adapter-set.d.ts +5 -0
  108. package/dist/testing/fake-adapter-set.js +150 -0
  109. package/dist/testing/needs.js +1 -1
  110. package/dist/testing.d.ts +1 -0
  111. package/dist/testing.js +1 -0
  112. package/package.json +1 -1
  113. package/src/api/__tests__/blocker-table.test.ts +25 -0
  114. package/src/api/{workbench-client.test.ts → __tests__/workbench-client.test.ts} +2 -2
  115. package/src/api/blocker-table.ts +6 -2
  116. package/src/components/SelectField.css +0 -6
  117. package/src/components/SelectField.tsx +7 -36
  118. package/src/components/SelectFieldNodes.tsx +4 -1
  119. package/src/components/{Card.test.tsx → __tests__/Card.test.tsx} +2 -2
  120. package/src/components/{CardAnchor.test.tsx → __tests__/CardAnchor.test.tsx} +3 -3
  121. package/src/components/{CardHead.test.tsx → __tests__/CardHead.test.tsx} +1 -1
  122. package/src/components/{ChoiceGroup.test.tsx → __tests__/ChoiceGroup.test.tsx} +2 -2
  123. package/src/components/{ConfirmBar.test.tsx → __tests__/ConfirmBar.test.tsx} +5 -5
  124. package/src/components/{Field.test.tsx → __tests__/Field.test.tsx} +1 -1
  125. package/src/components/{Form.test.tsx → __tests__/Form.test.tsx} +5 -5
  126. package/src/components/{FormActions.test.tsx → __tests__/FormActions.test.tsx} +3 -3
  127. package/src/components/{FormRows.test.tsx → __tests__/FormRows.test.tsx} +2 -2
  128. package/src/components/{FormTypes.test.ts → __tests__/FormTypes.test.ts} +1 -1
  129. package/src/components/{FormWiring.test.tsx → __tests__/FormWiring.test.tsx} +4 -4
  130. package/src/components/{Link.test.tsx → __tests__/Link.test.tsx} +1 -1
  131. package/src/components/{Rows.test.tsx → __tests__/Rows.test.tsx} +2 -2
  132. package/src/components/{SelectField.test.tsx → __tests__/SelectField.test.tsx} +7 -37
  133. package/src/components/{Spinner.test.tsx → __tests__/Spinner.test.tsx} +4 -4
  134. package/src/config/{constants.test.ts → __tests__/constants.test.ts} +1 -1
  135. package/src/config/{storage.test.ts → __tests__/storage.test.ts} +1 -1
  136. package/src/config/constants.ts +4 -4
  137. package/src/contract/capabilities.ts +30 -0
  138. package/src/contract/needs.ts +4 -4
  139. package/src/contract/plugin-source.ts +24 -0
  140. package/src/contract/protocol/bundle-install.ts +7 -1
  141. package/src/contract/protocol/platform.ts +1 -1
  142. package/src/contract/runner.ts +6 -0
  143. package/src/contract/unit.ts +4 -4
  144. package/src/host/{extension.test.ts → __tests__/extension.test.ts} +4 -4
  145. package/src/host/{install-failure.test.ts → __tests__/install-failure.test.ts} +1 -1
  146. package/src/host/{installed-sources.test.ts → __tests__/installed-sources.test.ts} +1 -1
  147. package/src/host/__tests__/lib-runners.test.ts +11 -2
  148. package/src/host/__tests__/plugin-ledger.test.ts +19 -6
  149. package/src/host/{recipe.test.ts → __tests__/recipe.test.ts} +3 -3
  150. package/src/host/__tests__/runtime-layout.test.ts +14 -0
  151. package/src/host/__tests__/supply-index.test.ts +2 -2
  152. package/src/host/__tests__/supply-port.test.ts +32 -0
  153. package/src/host/adapter-contract.ts +93 -0
  154. package/src/host/adapter-set.ts +6 -4
  155. package/src/host/adapter-types.ts +1 -1
  156. package/src/host/bundle-manager.ts +195 -0
  157. package/src/host/cli.ts +3 -1
  158. package/src/host/config-sync.ts +386 -0
  159. package/src/host/dsh-plugins.ts +60 -0
  160. package/src/host/extension.ts +1 -1
  161. package/src/host/flow/__tests__/advanced.test.ts +302 -0
  162. package/src/host/flow/__tests__/check.test.ts +169 -0
  163. package/src/host/flow/__tests__/run.test.ts +301 -0
  164. package/src/host/flow/check.ts +216 -0
  165. package/src/host/flow/expr.ts +94 -0
  166. package/src/host/flow/kit.ts +30 -0
  167. package/src/host/flow/run.ts +325 -0
  168. package/src/host/flow/types.ts +199 -0
  169. package/src/host/host-actions.ts +7 -0
  170. package/src/host/host-kit.ts +26 -0
  171. package/src/host/http.ts +6 -8
  172. package/src/host/installed-module.ts +272 -0
  173. package/src/host/lib-install.ts +14 -12
  174. package/src/host/lib-runners.ts +6 -8
  175. package/src/host/local-declaration.ts +37 -0
  176. package/src/host/module-root.ts +6 -0
  177. package/src/host/npm-registry.ts +21 -0
  178. package/src/host/package-content.ts +76 -0
  179. package/src/host/package-kind.ts +63 -0
  180. package/src/host/package-route.ts +93 -0
  181. package/src/host/paths.ts +27 -52
  182. package/src/host/platform.ts +1 -1
  183. package/src/host/plugin-ledger.ts +25 -18
  184. package/src/host/plugin.ts +4 -5
  185. package/src/host/port-types.ts +43 -9
  186. package/src/host/preflight.ts +3 -2
  187. package/src/host/profile-create.ts +133 -0
  188. package/src/host/profile-registries.ts +145 -0
  189. package/src/host/registry-declaration.ts +70 -0
  190. package/src/host/release-probe.ts +202 -0
  191. package/src/host/shell.ts +0 -9
  192. package/src/host/spec.ts +1 -1
  193. package/src/host/supply-index.ts +9 -7
  194. package/src/host/supply-port.ts +13 -3
  195. package/src/host/unit-face.ts +2 -1
  196. package/src/host/version.ts +12 -7
  197. package/src/host.ts +34 -6
  198. package/src/i18n/{index.test.ts → __tests__/index.test.ts} +3 -3
  199. package/src/i18n/{merge.test.ts → __tests__/merge.test.ts} +1 -1
  200. package/src/i18n/{zh.test.ts → __tests__/zh.test.ts} +2 -2
  201. package/src/i18n/en.ts +16 -20
  202. package/src/i18n/runtime-terms.ts +26 -0
  203. package/src/i18n/zh.ts +16 -20
  204. package/src/index.ts +4 -0
  205. package/src/lib/{board-fact.test.ts → __tests__/board-fact.test.ts} +1 -1
  206. package/src/lib/{busy.test.ts → __tests__/busy.test.ts} +1 -1
  207. package/src/lib/{form-problem.test.ts → __tests__/form-problem.test.ts} +1 -1
  208. package/src/lib/{git-address.test.ts → __tests__/git-address.test.ts} +1 -1
  209. package/src/lib/{react.test.ts → __tests__/react.test.ts} +1 -1
  210. package/src/lib/{text.test.ts → __tests__/text.test.ts} +1 -1
  211. package/src/lib/{ui.test.ts → __tests__/ui.test.ts} +1 -1
  212. package/src/testing/__tests__/fake-adapter-set.test.ts +28 -0
  213. package/src/testing/fake-adapter-set.ts +173 -0
  214. package/src/testing/needs.ts +1 -1
  215. package/src/testing.ts +1 -0
  216. package/src/host/__tests__/command-name.test.ts +0 -16
  217. package/src/host/__tests__/opencli-runner.test.ts +0 -120
  218. package/src/host/channel-plugin.ts +0 -55
  219. package/src/host/command-name.ts +0 -15
  220. package/src/host/opencli-runner.ts +0 -76
  221. package/src/host/paths.test.ts +0 -26
  222. package/src/host/plugin-source.ts +0 -137
  223. package/src/host/runtime-links.test.ts +0 -31
  224. package/src/host/runtime-links.ts +0 -23
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
  };
@@ -3,7 +3,7 @@ import type { ModuleStore } from './workspace-store.js';
3
3
  export const PLUGIN_LEDGER_FILE = 'plugins.json';
4
4
 
5
5
  export interface PluginLedgerEntry {
6
- readonly opencliHome: string;
6
+ readonly home: string;
7
7
  readonly package: string;
8
8
  readonly name?: string;
9
9
  readonly registry?: string;
@@ -13,10 +13,10 @@ export interface PluginLedgerEntry {
13
13
  }
14
14
 
15
15
  export function ledgerKeyOf(entry: {
16
- readonly opencliHome: string;
16
+ readonly home: string;
17
17
  readonly package: string;
18
18
  }): string {
19
- return `${entry.opencliHome}\u0000${entry.package}`;
19
+ return `${entry.home}\u0000${entry.package}`;
20
20
  }
21
21
 
22
22
  function textOf(value: unknown): string | undefined {
@@ -25,18 +25,25 @@ function textOf(value: unknown): string | undefined {
25
25
  return trimmed === '' ? undefined : trimmed;
26
26
  }
27
27
 
28
+ function legacyHomeOf(box: Readonly<Record<string, unknown>>): unknown {
29
+ for (const [key, value] of Object.entries(box)) {
30
+ if (key !== 'home' && key.toLowerCase().endsWith('home')) return value;
31
+ }
32
+ return undefined;
33
+ }
34
+
28
35
  function entryOf(value: unknown): PluginLedgerEntry | undefined {
29
36
  if (typeof value !== 'object' || value === null) return undefined;
30
37
  const box = value as Readonly<Record<string, unknown>>;
31
- const opencliHome = textOf(box['opencliHome']);
38
+ const home = textOf(box['home']) ?? textOf(legacyHomeOf(box));
32
39
  const pkg = textOf(box['package']);
33
40
  const installedAt = textOf(box['installedAt']);
34
- if (opencliHome === undefined || pkg === undefined || installedAt === undefined) return undefined;
41
+ if (home === undefined || pkg === undefined || installedAt === undefined) return undefined;
35
42
  const name = textOf(box['name']);
36
43
  const registry = textOf(box['registry']);
37
44
  const releasedAt = textOf(box['releasedAt']);
38
45
  return {
39
- opencliHome,
46
+ home,
40
47
  package: pkg,
41
48
  installedAt,
42
49
  ...(name === undefined ? {} : { name }),
@@ -74,9 +81,9 @@ export function writePluginLedger(store: ModuleStore, entries: readonly PluginLe
74
81
 
75
82
  export function pluginEntriesOfHome(
76
83
  entries: readonly PluginLedgerEntry[],
77
- opencliHome: string,
84
+ home: string,
78
85
  ): PluginLedgerEntry[] {
79
- return entries.filter((entry) => entry.opencliHome === opencliHome);
86
+ return entries.filter((entry) => entry.home === home);
80
87
  }
81
88
 
82
89
  export function isHeldEntry(entry: PluginLedgerEntry): boolean {
@@ -85,30 +92,30 @@ export function isHeldEntry(entry: PluginLedgerEntry): boolean {
85
92
 
86
93
  export function heldEntriesOfHome(
87
94
  entries: readonly PluginLedgerEntry[],
88
- opencliHome: string,
95
+ home: string,
89
96
  ): PluginLedgerEntry[] {
90
- return pluginEntriesOfHome(entries, opencliHome).filter(isHeldEntry);
97
+ return pluginEntriesOfHome(entries, home).filter(isHeldEntry);
91
98
  }
92
99
 
93
100
  export function heldEntryOfPlugin(
94
101
  entries: readonly PluginLedgerEntry[],
95
- opencliHome: string,
102
+ home: string,
96
103
  plugin: string,
97
104
  ): PluginLedgerEntry | undefined {
98
105
  const key = plugin.trim();
99
106
  if (key === '') return undefined;
100
- return heldEntriesOfHome(entries, opencliHome).find(
107
+ return heldEntriesOfHome(entries, home).find(
101
108
  (entry) => entry.name === key || entry.package === key,
102
109
  );
103
110
  }
104
111
 
105
112
  export function withReleasedEntry(
106
113
  entries: readonly PluginLedgerEntry[],
107
- opencliHome: string,
114
+ home: string,
108
115
  pkg: string,
109
116
  releasedAt: string,
110
117
  ): PluginLedgerEntry[] {
111
- const entry = pluginEntryOf(entries, opencliHome, pkg);
118
+ const entry = pluginEntryOf(entries, home, pkg);
112
119
  if (entry === undefined || entry.releasedAt !== undefined) return [...entries];
113
120
  return withPluginEntry(entries, { ...entry, releasedAt });
114
121
  }
@@ -127,10 +134,10 @@ export function pluginRetentionOf(options: {
127
134
 
128
135
  export function pluginEntryOf(
129
136
  entries: readonly PluginLedgerEntry[],
130
- opencliHome: string,
137
+ home: string,
131
138
  pkg: string,
132
139
  ): PluginLedgerEntry | undefined {
133
- const key = ledgerKeyOf({ opencliHome, package: pkg });
140
+ const key = ledgerKeyOf({ home, package: pkg });
134
141
  return entries.find((entry) => ledgerKeyOf(entry) === key);
135
142
  }
136
143
 
@@ -144,9 +151,9 @@ export function withPluginEntry(
144
151
 
145
152
  export function withoutPluginEntry(
146
153
  entries: readonly PluginLedgerEntry[],
147
- opencliHome: string,
154
+ home: string,
148
155
  pkg: string,
149
156
  ): PluginLedgerEntry[] {
150
- const key = ledgerKeyOf({ opencliHome, package: pkg });
157
+ const key = ledgerKeyOf({ home, package: pkg });
151
158
  return entries.filter((entry) => ledgerKeyOf(entry) !== key);
152
159
  }
@@ -1,7 +1,6 @@
1
1
  import { readFileSync, readdirSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
 
4
- export const OPENCLI_MANIFEST_FILE = 'opencli-plugin.json';
5
4
 
6
5
  function asRecord(value: unknown): Readonly<Record<string, unknown>> | undefined {
7
6
  return typeof value === 'object' && value !== null ? (value as Readonly<Record<string, unknown>>) : undefined;
@@ -13,9 +12,9 @@ function isCommandFile(name: string): boolean {
13
12
  return name.endsWith('.ts') || name.endsWith('.js');
14
13
  }
15
14
 
16
- function pluginManifestOf(dir: string): Readonly<Record<string, unknown>> | undefined {
15
+ function pluginManifestOf(dir: string, manifestFile: string): Readonly<Record<string, unknown>> | undefined {
17
16
  try {
18
- const parsed: unknown = JSON.parse(readFileSync(join(dir, OPENCLI_MANIFEST_FILE), 'utf8'));
17
+ const parsed: unknown = JSON.parse(readFileSync(join(dir, manifestFile), 'utf8'));
19
18
  if (Array.isArray(parsed)) return undefined;
20
19
  return asRecord(parsed);
21
20
  } catch {
@@ -31,8 +30,8 @@ function hasCommandFile(dir: string): boolean {
31
30
  }
32
31
  }
33
32
 
34
- export function pluginAt(dir: string): { readonly name?: string } | undefined {
35
- const manifest = pluginManifestOf(dir);
33
+ export function pluginAt(dir: string, manifestFile: string): { readonly name?: string } | undefined {
34
+ const manifest = pluginManifestOf(dir, manifestFile);
36
35
  if (manifest === undefined || !hasCommandFile(dir)) return undefined;
37
36
  const name = manifest.name;
38
37
  return typeof name === 'string' && name.trim() !== '' ? { name: name.trim() } : {};
@@ -1,19 +1,23 @@
1
1
  import type { DshHostContext, DshShell } from '../api/dsh.js';
2
+ import type { RuntimeCapabilities, RuntimeProfile } from '../contract/capabilities.js';
3
+ import type { PluginPackageKind } from '../contract/protocol/bundle-install.js';
2
4
  import type { LibNeed, SupplySource } from '../contract/needs.js';
3
5
  import type { NeedProbeInput, UnitCapability } from '../contract/unit-capability.js';
6
+ import type { DataReader } from '../contract/data.js';
7
+ import type { LibRunner } from '../contract/runner.js';
4
8
  import type { WorkbenchSelfUpdate } from '../contract/protocol/bundle-install.js';
5
9
  import type {
6
10
  BundleManager,
7
11
  InstalledModule,
8
- OpencliResolution,
9
12
  ProfileModule,
10
13
  ProfilePackage,
11
14
  RegistryNotebook,
15
+ RuntimeStatus,
12
16
  SelfUpdateRequest,
13
17
  SelfUpdateStart,
14
18
  } from './adapter-types.js';
15
19
  import type { PostResource, RouteHandler, RouteTable } from './http.js';
16
- import type { PluginSourceOptions, ResolvedPluginSource } from './plugin-source.js';
20
+ import type { PluginSourceOptions, ResolvedPluginSource } from '../contract/plugin-source.js';
17
21
 
18
22
  export interface LoggerLike {
19
23
  info?(message: string): void;
@@ -29,7 +33,7 @@ export interface ProfilePort {
29
33
  export type SupplyPluginSource = ResolvedPluginSource;
30
34
 
31
35
  export interface SupplyPackageRoute {
32
- readonly kind: 'opencli-plugin' | 'dsh-bundle' | 'neither';
36
+ readonly kind: PluginPackageKind | 'dsh-bundle' | 'neither';
33
37
  readonly pkg?: string;
34
38
  readonly pluginName?: string;
35
39
  }
@@ -85,6 +89,7 @@ export interface EnvSupplyPort {
85
89
  readonly source: Extract<SupplySource, { kind: 'npm' }>;
86
90
  readonly version?: string | undefined;
87
91
  }): string;
92
+ libUninstallCommand(options: { readonly need: LibNeed }): string;
88
93
  pluginInstallPlan(options: SupplyPluginInstallOptions): ResolvedPluginSource;
89
94
  latestVersion(options: SupplyLatestOptions): Promise<string | undefined>;
90
95
  missingReleaseParts(options: SupplyMissingPartsOptions): Promise<readonly string[]>;
@@ -93,10 +98,14 @@ export interface EnvSupplyPort {
93
98
  }
94
99
 
95
100
  export interface ModulePort {
101
+ isolatedRoot(): string;
96
102
  isolatedHome(): string;
97
- cliCatalogPath(): string;
98
- vendorPluginsDir(): string;
99
- opencliInstallCommand(): string;
103
+ pluginPackagePrefix?(): string;
104
+ pluginManifestFile?(): string;
105
+ cliCatalogPath?(): string;
106
+ vendorPluginsDir?(): string;
107
+ runtimeInstallCommand?(): string;
108
+ afterLibInstall(): void;
100
109
  profileDirOf(ctx: DshHostContext): string | null;
101
110
  profileModules(dir: string | null): readonly ProfileModule[];
102
111
  readInstalledModule(dir: string | null, pkg: string): InstalledModule;
@@ -140,8 +149,10 @@ export interface ProbePreflightInput {
140
149
  readonly routes: RouteTable;
141
150
  readonly ctx: DshHostContext;
142
151
  readonly shell: DshShell;
143
- readonly opencliCommand: string;
144
- readonly installCommand?: string | undefined;
152
+ readonly runtimeCommands: Readonly<Record<string, string>>;
153
+ readonly runtimeCapabilities: RuntimeCapabilities;
154
+ readonly runtimeCheck?: (() => Promise<RuntimeCheck>) | undefined;
155
+ readonly runtimeLabel?: string | undefined;
145
156
  readonly warmReads: () => readonly (readonly string[])[];
146
157
  }
147
158
 
@@ -156,9 +167,32 @@ export interface RuntimeSeedOptions {
156
167
  readonly log: (message: string) => void;
157
168
  }
158
169
 
170
+ export type { RuntimeCapabilities, RuntimeProfile, RuntimeTerms } from '../contract/capabilities.js';
171
+
172
+ export interface RuntimeCheck {
173
+ readonly ok: boolean;
174
+ readonly detail: string;
175
+ }
176
+
177
+ export interface RuntimeCommandSurface {
178
+ readonly name: string;
179
+ readonly pluginPrefix: string;
180
+ invoke(site: string, command?: string): string;
181
+ help(site: string): string;
182
+ browser?(verb: readonly string[]): string;
183
+ background?(command: string, argv: readonly string[]): string;
184
+ readonly authorHint?: string;
185
+ }
186
+
159
187
  export interface RuntimePort {
160
188
  packageRoot(moduleUrl: string): string;
161
- opencliCommand(ctx: DshHostContext): OpencliResolution;
189
+ capabilities(ctx: DshHostContext): RuntimeCapabilities;
190
+ profile?(ctx: DshHostContext): RuntimeProfile;
191
+ commandSurface?(ctx: DshHostContext): RuntimeCommandSurface | undefined;
192
+ check?(ctx: DshHostContext): Promise<RuntimeCheck>;
193
+ commands(ctx: DshHostContext): Readonly<Record<string, string>>;
194
+ runners(ctx: DshHostContext, data: DataReader): Readonly<Record<string, LibRunner>>;
195
+ health(ctx: DshHostContext): Readonly<Record<string, RuntimeStatus>>;
162
196
  seed(ctx: DshHostContext, options: RuntimeSeedOptions): void;
163
197
  warnUnwritableRoot(ctx: DshHostContext): void;
164
198
  }
@@ -1,11 +1,12 @@
1
- export type PreflightStepId = 'toolchain' | 'opencli' | 'bridge' | 'diy' | 'warm';
1
+ export type PreflightStepId = 'toolchain' | 'cli' | 'bridge' | 'runtime' | 'diy' | 'warm';
2
2
  export type PreflightStepStatus = 'pending' | 'running' | 'ok' | 'warn' | 'failed';
3
3
  export type PreflightFailure = 'permission' | 'network' | 'missing' | 'unknown';
4
4
  export type PreflightGuidance = 'install' | 'path' | 'shell';
5
- export type PreflightBridgeIssue = 'daemon' | 'extension' | 'profile' | 'stale' | 'unknown';
5
+ export type PreflightBridgeIssue = string;
6
6
 
7
7
  export interface PreflightStep {
8
8
  readonly id: PreflightStepId;
9
+ readonly label?: string;
9
10
  readonly status: PreflightStepStatus;
10
11
  readonly detail?: string;
11
12
  readonly action?: string;
@@ -0,0 +1,133 @@
1
+ import { existsSync, readFileSync, readdirSync, rmSync } from 'node:fs';
2
+ import { join, resolve, sep } from 'node:path';
3
+ import type { PostResource, RouteHandler } from './http.js';
4
+ import { quoteShellArg } from './shell-text.js';
5
+ import type {
6
+ ProfileCreateApplied,
7
+ ProfileCreateProfile,
8
+ ProfileCreateSnapshot,
9
+ } from '../contract/protocol/profile-create.js';
10
+ import { PROFILE_CREATE_RESOURCE } from '../contract/protocol/profile-create.js';
11
+
12
+ const PROFILE_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
13
+ const BUNDLE = '@dsh-workbench/core';
14
+
15
+ export interface ProfileCreateDeps {
16
+ readonly home: string;
17
+ readonly self: string;
18
+ readonly packageRoot: string;
19
+ readonly runSetup?: ((env: Readonly<Record<string, string>>) => Promise<{ ok: boolean; log: string }>) | undefined;
20
+ }
21
+
22
+ function textOf(value: unknown): string {
23
+ return typeof value === 'string' ? value : '';
24
+ }
25
+
26
+ function profileNames(profilesDir: string): readonly string[] {
27
+ try {
28
+ return readdirSync(profilesDir, { withFileTypes: true })
29
+ .filter((entry) => entry.isDirectory() || entry.isSymbolicLink())
30
+ .map((entry) => entry.name)
31
+ .filter((name) => existsSync(join(profilesDir, name, 'package.json')))
32
+ .sort((a, b) => a.localeCompare(b));
33
+ } catch {
34
+ return [];
35
+ }
36
+ }
37
+
38
+ export function profileCreateSnapshot(deps: ProfileCreateDeps): ProfileCreateSnapshot {
39
+ const profiles: ProfileCreateProfile[] = profileNames(join(deps.home, 'profiles')).map((name) => ({
40
+ name,
41
+ current: name === deps.self,
42
+ readable: true,
43
+ }));
44
+ return { current: deps.self, profiles };
45
+ }
46
+
47
+ export function profileCreateHandler(deps: ProfileCreateDeps): RouteHandler {
48
+ return () => ({ ok: true, ...profileCreateSnapshot(deps) });
49
+ }
50
+
51
+ function inside(base: string, path: string): boolean {
52
+ const root = resolve(base);
53
+ const target = resolve(path);
54
+ return target === root ? false : target.startsWith(`${root}${sep}`);
55
+ }
56
+
57
+ export async function profileCreateApply(
58
+ deps: ProfileCreateDeps,
59
+ payload: unknown,
60
+ ): Promise<ProfileCreateApplied | { ok: false; text: string }> {
61
+ const body = typeof payload === 'object' && payload !== null ? (payload as Record<string, unknown>) : {};
62
+ const profile = textOf(body['profile']).trim();
63
+ const source = textOf(body['source']).trim();
64
+ const copy = typeof body['copy'] === 'object' && body['copy'] !== null ? (body['copy'] as Record<string, unknown>) : {};
65
+ const flags = {
66
+ settings: copy['settings'] === true,
67
+ };
68
+ if (!PROFILE_NAME.test(profile)) return { ok: false, text: '新工作台的名字不合法:只用字母、数字、点、下划线或短横线' };
69
+ if (source === '') return { ok: false, text: '先选一个来源 profile' };
70
+ if (source === profile) return { ok: false, text: '来源不能是新工作台自己' };
71
+ const profilesDir = join(deps.home, 'profiles');
72
+ if (!profileNames(profilesDir).includes(source)) return { ok: false, text: `没有这个 profile:${source}` };
73
+ if (existsSync(join(profilesDir, profile))) return { ok: false, text: `已经有一个叫 ${profile} 的 profile` };
74
+ const targetHome = join(deps.home, profile);
75
+ if (existsSync(targetHome)) return { ok: false, text: `运行时目录已经存在:${targetHome}` };
76
+ const env = {
77
+ DSH_HOME: deps.home,
78
+ DSH_WORKBENCH_HOME: targetHome,
79
+ WORKBENCH_PROFILE: profile,
80
+ WORKBENCH_SOURCE_PROFILE: source,
81
+ WB_CLONE: '1',
82
+ WB_COPY_SETTINGS: flags.settings ? '1' : '0',
83
+ };
84
+ const setup = deps.runSetup;
85
+ if (setup === undefined) {
86
+ return { ok: false, text: '这台机器上没有装工作台的脚本入口,建不了新的工作台。' };
87
+ }
88
+ const cleanup = (): void => {
89
+ try {
90
+ if (inside(profilesDir, join(profilesDir, profile))) rmSync(join(profilesDir, profile), { recursive: true, force: true });
91
+ if (inside(deps.home, targetHome)) rmSync(targetHome, { recursive: true, force: true });
92
+ } catch {
93
+ return;
94
+ }
95
+ };
96
+ const result = await setup(env);
97
+ if (!result.ok) {
98
+ cleanup();
99
+ return { ok: false, text: `创建失败:${result.log.trim().slice(-1200) || 'setup 没给出原因'}` };
100
+ }
101
+ try {
102
+ const manifest = JSON.parse(readFileSync(join(profilesDir, profile, 'package.json'), 'utf8')) as {
103
+ dsh?: { profile?: { bundles?: unknown } };
104
+ };
105
+ const bundles = manifest.dsh?.profile?.bundles;
106
+ if (!Array.isArray(bundles) || !bundles.includes(BUNDLE)) {
107
+ cleanup();
108
+ return { ok: false, text: 'profile 建好了,但工作台 bundle 没被选中;先跑一次 setup 修好它' };
109
+ }
110
+ } catch (error) {
111
+ cleanup();
112
+ return { ok: false, text: `profile 建好了,但读不了清单:${error instanceof Error ? error.message : String(error)}` };
113
+ }
114
+ const copied = Object.entries(flags)
115
+ .filter(([, on]) => on)
116
+ .map(([name]) => name);
117
+ const bin = join(deps.packageRoot, 'bin', 'dsh-workbench');
118
+ return {
119
+ ok: true,
120
+ profile,
121
+ source,
122
+ home: targetHome,
123
+ command: `WORKBENCH_PROFILE=${quoteShellArg(profile)} ${quoteShellArg(bin)}`,
124
+ copied,
125
+ };
126
+ }
127
+
128
+ export function profileCreateAction(deps: ProfileCreateDeps): PostResource {
129
+ return {
130
+ resource: PROFILE_CREATE_RESOURCE,
131
+ handle: (payload: unknown) => profileCreateApply(deps, payload),
132
+ };
133
+ }
@@ -0,0 +1,145 @@
1
+ import type { RegistryEntry, RegistryNotebook } from './adapter-types.js';
2
+ import { normalizeRegistry, sameRegistry, scopeOf } from './adapter-types.js';
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ export const REGISTRY_MARKER =
6
+ '# dsh-workbench 写的:这一行的包只在这个源上(dsh 卸载没有源参数,pnpm 得问得到)';
7
+
8
+ function registryRootOf(name: string, tarball: string): string | null {
9
+ const at = tarball.indexOf(`/${name}/-/`);
10
+ if (at > 0) return normalizeRegistry(tarball.slice(0, at));
11
+ try {
12
+ return normalizeRegistry(new URL(tarball).origin);
13
+ } catch {
14
+ return null;
15
+ }
16
+ }
17
+
18
+ const PACKAGE_KEY = /^ {2}'?(\S+)'?:\s*$/;
19
+ const PACKAGE_NAME = /^(?:@[^/\s]+\/)?[^/\s@]+$/;
20
+ const TARBALL = /tarball:\s*(\S+?)\}?\s*$/;
21
+
22
+ export function sourcesFromLockfile(text: string): readonly RegistryEntry[] {
23
+ const found = new Map<string, string | null>();
24
+ let name: string | null = null;
25
+ for (const line of text.split('\n')) {
26
+ const key = PACKAGE_KEY.exec(line);
27
+ if (key !== null) {
28
+ const raw = key[1] ?? '';
29
+ const at = raw.lastIndexOf('@');
30
+ const candidate = at > 0 ? raw.slice(0, at) : '';
31
+ name = PACKAGE_NAME.test(candidate) ? candidate : null;
32
+ continue;
33
+ }
34
+ if (name === null) continue;
35
+ const tarball = TARBALL.exec(line);
36
+ if (tarball === null) continue;
37
+ const scope = scopeOf(name);
38
+ const root = registryRootOf(name, tarball[1] ?? '');
39
+ name = null;
40
+ if (scope === null || root === null) continue;
41
+ const seen = found.get(scope);
42
+ if (seen === undefined) found.set(scope, root);
43
+ else if (!sameRegistry(seen, root)) found.set(scope, null);
44
+ }
45
+ return [...found.entries()]
46
+ .filter((entry): entry is [string, string] => entry[1] !== null)
47
+ .map(([scope, registry]) => ({ scope, registry }));
48
+ }
49
+
50
+ export interface RegistryConfigMerge {
51
+ readonly text: string;
52
+ readonly added: readonly string[];
53
+ }
54
+
55
+ export function mergeRegistryConfig(text: string, entries: readonly RegistryEntry[]): RegistryConfigMerge {
56
+ const lines = text.split('\n');
57
+ const present = (key: string): boolean => lines.some((line) => line.trim().startsWith(`${key}=`));
58
+ const appended: string[] = [];
59
+ const added: string[] = [];
60
+ for (const entry of entries) {
61
+ const key = `${entry.scope}:registry`;
62
+ if (entry.registry.trim() === '' || present(key)) continue;
63
+ lines.push(`${key}=${normalizeRegistry(entry.registry)}`);
64
+ appended.push(`${key}=${normalizeRegistry(entry.registry)}`);
65
+ added.push(key);
66
+ }
67
+ if (appended.length === 0) return { text, added: [] };
68
+ const base = text === '' ? '' : text.endsWith('\n') ? text : `${text}\n`;
69
+ const marker = base.includes(REGISTRY_MARKER) ? '' : `${REGISTRY_MARKER}\n`;
70
+ return { text: `${base}${marker}${appended.join('\n')}\n`, added };
71
+ }
72
+
73
+ export function replaceScopeRegistry(text: string, scope: string, registry: string): string {
74
+ const key = `${scope}:registry`;
75
+ const value = normalizeRegistry(registry);
76
+ let found = false;
77
+ const lines = text.split('\n').map((line) => {
78
+ if (!line.trim().startsWith(`${key}=`)) return line;
79
+ found = true;
80
+ return `${key}=${value}`;
81
+ });
82
+ if (found) return lines.join('\n');
83
+ const base = text === '' ? '' : text.endsWith('\n') ? text : `${text}\n`;
84
+ return `${base}${key}=${value}\n`;
85
+ }
86
+
87
+ function readNpmrc(file: string): string {
88
+ try {
89
+ return fs.readFileSync(file, 'utf8');
90
+ } catch {
91
+ return '';
92
+ }
93
+ }
94
+
95
+ function writeNpmrc(file: string, text: string): void {
96
+ const temp = `${file}.dsh-workbench.tmp`;
97
+ fs.writeFileSync(temp, text, { mode: 0o600 });
98
+ fs.renameSync(temp, file);
99
+ }
100
+
101
+ export function createRegistryNotebook(dir: string | null): RegistryNotebook {
102
+ return {
103
+ sources() {
104
+ if (dir === null) return [];
105
+ try {
106
+ return sourcesFromLockfile(fs.readFileSync(path.join(dir, 'pnpm-lock.yaml'), 'utf8'));
107
+ } catch {
108
+ return [];
109
+ }
110
+ },
111
+ remember(entries) {
112
+ if (dir === null || entries.length === 0) return [];
113
+ const file = path.join(dir, '.npmrc');
114
+ const merged = mergeRegistryConfig(readNpmrc(file), entries);
115
+ if (merged.added.length === 0) return [];
116
+ writeNpmrc(file, merged.text);
117
+ return merged.added;
118
+ },
119
+ swapScope(scope, registry) {
120
+ if (dir === null || scope.trim() === '' || registry.trim() === '') return () => {};
121
+ const file = path.join(dir, '.npmrc');
122
+ const key = `${scope.trim()}:registry`;
123
+ const value = normalizeRegistry(registry);
124
+ const before = readNpmrc(file);
125
+ const previous = before.split('\n').find((line) => line.trim().startsWith(`${key}=`));
126
+ const after = replaceScopeRegistry(before, scope.trim(), registry);
127
+ if (after === before) return () => {};
128
+ writeNpmrc(file, after);
129
+ return () => {
130
+ const now = readNpmrc(file);
131
+ if (now === after) {
132
+ if (before === '') fs.rmSync(file, { force: true });
133
+ else writeNpmrc(file, before);
134
+ return;
135
+ }
136
+ if (!now.includes(`${key}=${value}`)) return;
137
+ const restored =
138
+ previous === undefined
139
+ ? now.split('\n').filter((line) => !line.trim().startsWith(`${key}=`)).join('\n')
140
+ : now.replace(`${key}=${value}`, previous);
141
+ writeNpmrc(file, restored);
142
+ };
143
+ },
144
+ };
145
+ }