@dsh-workbench/shared 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (206) hide show
  1. package/dist/api/blocker-table.d.ts +9 -0
  2. package/dist/api/blocker-table.js +130 -0
  3. package/dist/api/blocker.d.ts +30 -0
  4. package/dist/api/blocker.js +1 -0
  5. package/dist/api/context-ref.d.ts +10 -0
  6. package/dist/api/context-ref.js +40 -0
  7. package/dist/api/dsh-client.d.ts +170 -0
  8. package/dist/api/dsh-client.js +1 -0
  9. package/dist/api/dsh.d.ts +153 -0
  10. package/dist/api/dsh.js +1 -0
  11. package/dist/api/facts.d.ts +27 -0
  12. package/dist/api/facts.js +146 -0
  13. package/dist/api/host-write.d.ts +12 -0
  14. package/dist/api/host-write.js +17 -0
  15. package/dist/api/long-task.d.ts +18 -0
  16. package/dist/api/long-task.js +22 -0
  17. package/dist/api/unit-board.d.ts +14 -0
  18. package/dist/api/unit-board.js +29 -0
  19. package/dist/api/unit.d.ts +54 -0
  20. package/dist/api/unit.js +1 -0
  21. package/dist/api/workbench-client.d.ts +19 -0
  22. package/dist/api/workbench-client.js +33 -0
  23. package/dist/api/workbench-contract.d.ts +32 -0
  24. package/dist/api/workbench-contract.js +1 -0
  25. package/dist/components/ApprovalDetail.d.ts +27 -0
  26. package/dist/components/ApprovalDetail.js +33 -0
  27. package/dist/components/CardHead.d.ts +16 -0
  28. package/dist/components/CardHead.js +14 -0
  29. package/dist/components/ChoiceGroup.d.ts +14 -0
  30. package/dist/components/ChoiceGroup.js +7 -0
  31. package/dist/components/LongTaskRow.d.ts +16 -0
  32. package/dist/components/LongTaskRow.js +28 -0
  33. package/dist/components/MockStrip.d.ts +14 -0
  34. package/dist/components/MockStrip.js +7 -0
  35. package/dist/components/ResourceStates.d.ts +18 -0
  36. package/dist/components/ResourceStates.js +17 -0
  37. package/dist/components/SaveBar.d.ts +12 -0
  38. package/dist/components/SaveBar.js +6 -0
  39. package/dist/components/StateDot.d.ts +5 -0
  40. package/dist/components/StateDot.js +10 -0
  41. package/dist/components/board-module.d.ts +67 -0
  42. package/dist/components/board-module.js +1 -0
  43. package/dist/components/icons.d.ts +18 -0
  44. package/dist/components/icons.js +81 -0
  45. package/dist/config/constants.d.ts +27 -0
  46. package/dist/config/constants.js +27 -0
  47. package/dist/config/storage.d.ts +4 -0
  48. package/dist/config/storage.js +27 -0
  49. package/dist/host/__tests__/fakes.d.ts +41 -0
  50. package/dist/host/__tests__/fakes.js +76 -0
  51. package/dist/host/__tests__/preflight-fixtures.d.ts +19 -0
  52. package/dist/host/__tests__/preflight-fixtures.js +47 -0
  53. package/dist/host/cli-error.d.ts +8 -0
  54. package/dist/host/cli-error.js +70 -0
  55. package/dist/host/context-inject.d.ts +24 -0
  56. package/dist/host/context-inject.js +74 -0
  57. package/dist/host/data-root.d.ts +3 -0
  58. package/dist/host/data-root.js +12 -0
  59. package/dist/host/events.d.ts +9 -0
  60. package/dist/host/events.js +70 -0
  61. package/dist/host/facts.d.ts +24 -0
  62. package/dist/host/facts.js +44 -0
  63. package/dist/host/git.d.ts +55 -0
  64. package/dist/host/git.js +64 -0
  65. package/dist/host/http.d.ts +36 -0
  66. package/dist/host/http.js +242 -0
  67. package/dist/host/long-task.d.ts +58 -0
  68. package/dist/host/long-task.js +148 -0
  69. package/dist/host/mount-units.d.ts +52 -0
  70. package/dist/host/mount-units.js +186 -0
  71. package/dist/host/opencli-runtime.d.ts +22 -0
  72. package/dist/host/opencli-runtime.js +46 -0
  73. package/dist/host/opencli.d.ts +37 -0
  74. package/dist/host/opencli.js +152 -0
  75. package/dist/host/plugin-source.d.ts +31 -0
  76. package/dist/host/plugin-source.js +81 -0
  77. package/dist/host/preflight.d.ts +58 -0
  78. package/dist/host/preflight.js +233 -0
  79. package/dist/host/probe-cache.d.ts +6 -0
  80. package/dist/host/probe-cache.js +19 -0
  81. package/dist/host/probe-libs.d.ts +45 -0
  82. package/dist/host/probe-libs.js +61 -0
  83. package/dist/host/probe-units.d.ts +17 -0
  84. package/dist/host/probe-units.js +102 -0
  85. package/dist/host/probe-verdict.d.ts +7 -0
  86. package/dist/host/probe-verdict.js +143 -0
  87. package/dist/host/provision.d.ts +14 -0
  88. package/dist/host/provision.js +12 -0
  89. package/dist/host/shell.d.ts +14 -0
  90. package/dist/host/shell.js +23 -0
  91. package/dist/host/tool-approval.d.ts +29 -0
  92. package/dist/host/tool-approval.js +190 -0
  93. package/dist/host/tool-kit.d.ts +19 -0
  94. package/dist/host/tool-kit.js +37 -0
  95. package/dist/host/tool-scope.d.ts +32 -0
  96. package/dist/host/tool-scope.js +84 -0
  97. package/dist/host/unit-face.d.ts +66 -0
  98. package/dist/host/unit-face.js +1 -0
  99. package/dist/host/unit-libs.d.ts +37 -0
  100. package/dist/host/unit-libs.js +1 -0
  101. package/dist/host/unit-readiness.d.ts +7 -0
  102. package/dist/host/unit-readiness.js +5 -0
  103. package/dist/host/unit-registry.d.ts +9 -0
  104. package/dist/host/unit-registry.js +33 -0
  105. package/dist/host/value-handoff.d.ts +16 -0
  106. package/dist/host/value-handoff.js +21 -0
  107. package/dist/host/version-range.d.ts +2 -0
  108. package/dist/host/version-range.js +73 -0
  109. package/dist/host/workspace-store.d.ts +19 -0
  110. package/dist/host/workspace-store.js +133 -0
  111. package/dist/host/write-result.d.ts +1 -0
  112. package/dist/host/write-result.js +18 -0
  113. package/dist/host.d.ts +19 -0
  114. package/dist/host.js +19 -0
  115. package/dist/i18n/en.d.ts +2 -0
  116. package/dist/i18n/en.js +162 -0
  117. package/dist/i18n/index.d.ts +172 -0
  118. package/dist/i18n/index.js +3 -0
  119. package/dist/i18n/merge.d.ts +6 -0
  120. package/dist/i18n/merge.js +40 -0
  121. package/dist/i18n/zh.d.ts +163 -0
  122. package/dist/i18n/zh.js +162 -0
  123. package/dist/index.d.ts +33 -0
  124. package/dist/index.js +33 -0
  125. package/dist/lib/blocker-action.d.ts +13 -0
  126. package/dist/lib/blocker-action.js +12 -0
  127. package/dist/lib/busy.d.ts +5 -0
  128. package/dist/lib/busy.js +7 -0
  129. package/dist/lib/react.d.ts +10 -0
  130. package/dist/lib/react.js +9 -0
  131. package/dist/lib/text.d.ts +4 -0
  132. package/dist/lib/text.js +48 -0
  133. package/dist/lib/ui.d.ts +11 -0
  134. package/dist/lib/ui.js +37 -0
  135. package/package.json +44 -0
  136. package/src/api/__tests__/blocker-table.test.ts +253 -0
  137. package/src/api/__tests__/facts.test.ts +274 -0
  138. package/src/api/__tests__/host-write.test.ts +55 -0
  139. package/src/api/__tests__/long-task.test.ts +52 -0
  140. package/src/api/__tests__/unit-board.test.ts +76 -0
  141. package/src/api/blocker-table.ts +155 -0
  142. package/src/api/blocker.ts +25 -0
  143. package/src/api/context-ref.ts +47 -0
  144. package/src/api/dsh-client.ts +176 -0
  145. package/src/api/dsh.ts +156 -0
  146. package/src/api/facts.ts +166 -0
  147. package/src/api/host-write.ts +27 -0
  148. package/src/api/long-task.ts +38 -0
  149. package/src/api/unit-board.ts +44 -0
  150. package/src/api/unit.ts +59 -0
  151. package/src/api/workbench-client.test.ts +92 -0
  152. package/src/api/workbench-client.ts +59 -0
  153. package/src/api/workbench-contract.ts +40 -0
  154. package/src/components/ApprovalDetail.tsx +57 -0
  155. package/src/components/CardHead.tsx +74 -0
  156. package/src/components/ChoiceGroup.tsx +38 -0
  157. package/src/components/LongTaskRow.tsx +65 -0
  158. package/src/components/MockStrip.tsx +40 -0
  159. package/src/components/ResourceStates.tsx +44 -0
  160. package/src/components/SaveBar.tsx +31 -0
  161. package/src/components/StateDot.tsx +31 -0
  162. package/src/components/board-module.ts +73 -0
  163. package/src/components/icons.tsx +232 -0
  164. package/src/config/constants.test.ts +41 -0
  165. package/src/config/constants.ts +52 -0
  166. package/src/config/storage.test.ts +82 -0
  167. package/src/config/storage.ts +25 -0
  168. package/src/host/__tests__/fakes.ts +116 -0
  169. package/src/host/__tests__/opencli-runtime.test.ts +46 -0
  170. package/src/host/cli-error.ts +77 -0
  171. package/src/host/data-root.ts +13 -0
  172. package/src/host/facts.ts +69 -0
  173. package/src/host/git.ts +99 -0
  174. package/src/host/http.ts +311 -0
  175. package/src/host/long-task.ts +235 -0
  176. package/src/host/opencli-runtime.ts +66 -0
  177. package/src/host/opencli.ts +210 -0
  178. package/src/host/plugin-source.ts +145 -0
  179. package/src/host/provision.ts +34 -0
  180. package/src/host/shell.ts +42 -0
  181. package/src/host/tool-approval.ts +243 -0
  182. package/src/host/tool-kit.ts +56 -0
  183. package/src/host/unit-face.ts +65 -0
  184. package/src/host/unit-libs.ts +41 -0
  185. package/src/host/value-handoff.ts +38 -0
  186. package/src/host/workspace-store.ts +161 -0
  187. package/src/host/write-result.ts +16 -0
  188. package/src/host.ts +19 -0
  189. package/src/i18n/en.ts +170 -0
  190. package/src/i18n/index.test.ts +15 -0
  191. package/src/i18n/index.ts +12 -0
  192. package/src/i18n/merge.test.ts +60 -0
  193. package/src/i18n/merge.ts +54 -0
  194. package/src/i18n/zh.test.ts +40 -0
  195. package/src/i18n/zh.ts +170 -0
  196. package/src/index.ts +33 -0
  197. package/src/lib/blocker-action.test.ts +40 -0
  198. package/src/lib/blocker-action.ts +28 -0
  199. package/src/lib/busy.test.ts +27 -0
  200. package/src/lib/busy.ts +11 -0
  201. package/src/lib/react.test.ts +26 -0
  202. package/src/lib/react.ts +16 -0
  203. package/src/lib/text.test.ts +92 -0
  204. package/src/lib/text.ts +46 -0
  205. package/src/lib/ui.test.ts +93 -0
  206. package/src/lib/ui.ts +38 -0
@@ -0,0 +1,58 @@
1
+ import { type LongTaskStatus, type LongTaskStep } from '../api/long-task.js';
2
+ export interface LongTaskBase {
3
+ readonly id: string;
4
+ readonly workspace: string;
5
+ readonly target: string;
6
+ readonly status: LongTaskStatus;
7
+ readonly step: string;
8
+ readonly steps: readonly string[];
9
+ readonly loginUrl?: string | undefined;
10
+ readonly why?: string | undefined;
11
+ readonly startedAt: string;
12
+ readonly at: string;
13
+ }
14
+ export interface LongTaskStart {
15
+ readonly workspace: string;
16
+ readonly target: string;
17
+ readonly payload?: unknown;
18
+ }
19
+ export interface LongTaskChange<S extends LongTaskBase> {
20
+ readonly kind: 'start' | 'step' | 'settle';
21
+ readonly snapshot: S;
22
+ readonly step?: LongTaskStep | undefined;
23
+ }
24
+ export interface LongTaskDeps<S extends LongTaskBase> {
25
+ readonly run: (options: {
26
+ readonly payload: unknown;
27
+ readonly onStep: (step: LongTaskStep) => void;
28
+ }) => Promise<unknown>;
29
+ readonly step?: ((step: LongTaskStep, current: S) => Partial<S>) | undefined;
30
+ readonly outcome?: ((result: unknown, current: S) => Partial<S>) | undefined;
31
+ readonly view: (snapshot: S) => Readonly<Record<string, unknown>>;
32
+ readonly now?: (() => string) | undefined;
33
+ readonly nextId?: (() => string) | undefined;
34
+ readonly facts?: ((change: LongTaskChange<S>) => readonly string[] | undefined) | undefined;
35
+ readonly recentMs?: number | undefined;
36
+ }
37
+ export interface LongTaskRegistry<S extends LongTaskBase> {
38
+ start(options: LongTaskStart): S;
39
+ snapshot(id: string): S | null;
40
+ active(workspace: string, target: string): S | null;
41
+ current(workspace: string): readonly Readonly<Record<string, unknown>>[];
42
+ onChange(listener: (change: LongTaskChange<S>) => void): () => void;
43
+ }
44
+ export declare function createLongTaskRegistry<S extends LongTaskBase>(deps: LongTaskDeps<S>): LongTaskRegistry<S>;
45
+ export declare function sharedLongTaskRegistry<S extends LongTaskBase>(scope: string, key: string, deps: LongTaskDeps<S>): LongTaskRegistry<S>;
46
+ export interface LongTaskHost {
47
+ create<S extends LongTaskBase>(options: {
48
+ readonly key: string;
49
+ } & LongTaskDeps<S>): LongTaskRegistry<S>;
50
+ }
51
+ export declare function createLongTaskHost(options: {
52
+ readonly scope: string;
53
+ readonly unit: string;
54
+ readonly bump: (facts: readonly string[]) => void;
55
+ }): {
56
+ readonly host: LongTaskHost;
57
+ dispose(): void;
58
+ };
@@ -0,0 +1,148 @@
1
+ import { runsRouteOf } from '../api/long-task.js';
2
+ const LIVE = new Set(['running', 'needs-login']);
3
+ const RECENT_MS = 10 * 60 * 1000;
4
+ export function createLongTaskRegistry(deps) {
5
+ const runs = new Map();
6
+ const now = deps.now ?? (() => new Date().toISOString());
7
+ let counter = 0;
8
+ const nextId = deps.nextId ?? (() => `run-${Date.now()}-${(counter += 1)}`);
9
+ const recentMs = deps.recentMs ?? RECENT_MS;
10
+ const listeners = new Set();
11
+ const notify = (change) => {
12
+ for (const listener of [...listeners]) {
13
+ try {
14
+ listener(change);
15
+ }
16
+ catch {
17
+ }
18
+ }
19
+ };
20
+ const patch = (id, next, change) => {
21
+ const current = runs.get(id);
22
+ if (current === undefined)
23
+ return;
24
+ const patched = { ...current, ...next, at: now() };
25
+ runs.set(id, patched);
26
+ notify({ ...change, snapshot: patched });
27
+ };
28
+ return {
29
+ active(workspace, target) {
30
+ for (const snapshot of runs.values()) {
31
+ if (snapshot.workspace !== workspace || snapshot.target !== target)
32
+ continue;
33
+ if (snapshot.status === 'running' || snapshot.status === 'needs-login')
34
+ return snapshot;
35
+ }
36
+ return null;
37
+ },
38
+ current(workspace) {
39
+ const floor = Date.parse(now()) - recentMs;
40
+ const newest = new Map();
41
+ for (const snapshot of runs.values()) {
42
+ if (snapshot.workspace !== workspace)
43
+ continue;
44
+ newest.set(snapshot.target, snapshot);
45
+ }
46
+ return [...newest.values()]
47
+ .filter((snapshot) => LIVE.has(snapshot.status) || Date.parse(snapshot.at) >= floor)
48
+ .reverse()
49
+ .map((snapshot) => deps.view(snapshot));
50
+ },
51
+ start(options) {
52
+ const running = this.active(options.workspace, options.target);
53
+ if (running !== null)
54
+ return running;
55
+ const id = nextId();
56
+ const startedAt = now();
57
+ runs.set(id, {
58
+ id,
59
+ workspace: options.workspace,
60
+ target: options.target,
61
+ status: 'running',
62
+ step: '',
63
+ steps: [],
64
+ startedAt,
65
+ at: startedAt,
66
+ });
67
+ notify({ kind: 'start', snapshot: runs.get(id) });
68
+ const onStep = (step) => {
69
+ const current = runs.get(id);
70
+ if (current === undefined)
71
+ return;
72
+ const steps = [...current.steps, `${step.kind === 'fail' ? '✗ ' : ''}${step.text}`].slice(-20);
73
+ const extra = deps.step?.(step, current) ?? {};
74
+ if (step.loginUrl !== undefined) {
75
+ patch(id, { ...extra, status: 'needs-login', step: step.text, steps, loginUrl: step.loginUrl }, { kind: 'step', step });
76
+ return;
77
+ }
78
+ patch(id, {
79
+ ...extra,
80
+ status: current.status === 'needs-login' ? 'running' : current.status,
81
+ step: step.text,
82
+ steps,
83
+ ...(current.status === 'needs-login' ? { loginUrl: undefined } : {}),
84
+ }, { kind: 'step', step });
85
+ };
86
+ void deps
87
+ .run({ payload: options.payload, onStep })
88
+ .then((result) => {
89
+ const current = runs.get(id);
90
+ patch(id, { status: 'ok', ...(deps.outcome?.(result, current) ?? {}), loginUrl: undefined }, { kind: 'settle' });
91
+ })
92
+ .catch((error) => {
93
+ patch(id, {
94
+ status: 'failed',
95
+ why: error instanceof Error ? error.message : String(error),
96
+ loginUrl: undefined,
97
+ }, { kind: 'settle' });
98
+ });
99
+ return runs.get(id);
100
+ },
101
+ snapshot(id) {
102
+ return runs.get(id) ?? null;
103
+ },
104
+ onChange(listener) {
105
+ listeners.add(listener);
106
+ return () => listeners.delete(listener);
107
+ },
108
+ };
109
+ }
110
+ function isRegistry(value) {
111
+ if (typeof value !== 'object' || value === null)
112
+ return false;
113
+ const candidate = value;
114
+ return (typeof candidate.start === 'function' &&
115
+ typeof candidate.current === 'function' &&
116
+ typeof candidate.snapshot === 'function' &&
117
+ typeof candidate.onChange === 'function');
118
+ }
119
+ export function sharedLongTaskRegistry(scope, key, deps) {
120
+ const store = globalThis;
121
+ const slot = Symbol.for(`dsh-workbench:long-task:${scope}:${key}`);
122
+ const existing = store[slot];
123
+ if (existing !== undefined && isRegistry(existing))
124
+ return existing;
125
+ const created = createLongTaskRegistry(deps);
126
+ store[slot] = created;
127
+ return created;
128
+ }
129
+ export function createLongTaskHost(options) {
130
+ const offs = [];
131
+ return {
132
+ host: {
133
+ create(createOptions) {
134
+ const { key, facts, ...deps } = createOptions;
135
+ const registry = sharedLongTaskRegistry(options.scope, `${options.unit}:${key}`, deps);
136
+ offs.push(registry.onChange((change) => {
137
+ const named = [runsRouteOf(options.unit), ...(facts?.(change) ?? [])];
138
+ options.bump([...new Set(named)]);
139
+ }));
140
+ return registry;
141
+ },
142
+ },
143
+ dispose() {
144
+ for (const off of offs.splice(0))
145
+ off();
146
+ },
147
+ };
148
+ }
@@ -0,0 +1,52 @@
1
+ import type { DshHostContext } from '@dsh-workbench/shared';
2
+ import type { UnitManifest } from '@dsh-workbench/shared';
3
+ import { type RouteTable } from './http';
4
+ import type { ContextRenderer, UnitHostFace, UnitMountDeps, UnitMountResult, UnitCall } from './unit-face';
5
+ import { type UnitReadiness } from './unit-readiness';
6
+ export interface MountedUnit {
7
+ readonly face: UnitHostFace;
8
+ readonly result: UnitMountResult;
9
+ readonly readiness: UnitReadiness;
10
+ }
11
+ export interface UnitsPlan {
12
+ readonly mounted: readonly MountedUnit[];
13
+ readonly routes: RouteTable;
14
+ readonly gate: UnitMountResult['gate'];
15
+ readonly prefs: ReadonlyArray<{
16
+ readonly resource: string;
17
+ readonly unit: string;
18
+ handle(payload: unknown): unknown;
19
+ }>;
20
+ readonly actions: ReadonlyArray<{
21
+ readonly resource: string;
22
+ readonly unit: string;
23
+ handle(payload: unknown): unknown;
24
+ }>;
25
+ readonly provision: UnitMountResult['provision'];
26
+ readonly contextRenderers: ReadonlyMap<string, ContextRenderer>;
27
+ readonly owners: ReadonlyMap<string, string>;
28
+ readonly routesOfUnit: ReadonlyMap<string, readonly string[]>;
29
+ readonly factsOf: ReadonlyMap<string, (call: UnitCall) => readonly string[] | undefined>;
30
+ readonly skipped: ReadonlyArray<{
31
+ readonly id: string;
32
+ readonly missing: readonly string[];
33
+ }>;
34
+ readonly warmReads: () => readonly (readonly string[])[];
35
+ readonly disposeLongTasks: () => void;
36
+ }
37
+ export declare function mountUnits(faces: readonly UnitHostFace[], deps: {
38
+ ctx: DshHostContext;
39
+ logger?: {
40
+ info?(m: string): void;
41
+ warn?(m: string): void;
42
+ } | undefined;
43
+ manifests: () => readonly UnitManifest[];
44
+ opencliCommand?: string;
45
+ probe?: UnitMountDeps['probe'];
46
+ store?: UnitMountDeps['store'];
47
+ values?: UnitMountDeps['values'];
48
+ longTasks?: {
49
+ readonly scope: string;
50
+ readonly bump: (facts: readonly string[]) => void;
51
+ } | undefined;
52
+ }): UnitsPlan;
@@ -0,0 +1,186 @@
1
+ import { runsRouteOf } from '@dsh-workbench/shared';
2
+ import { param } from './http';
3
+ import { createLongTaskHost } from './long-task';
4
+ import { unitReadiness } from './unit-readiness';
5
+ import { createWorkspaceStore } from './workspace-store';
6
+ function withRunsRoute(result, unit) {
7
+ const runs = result.longTasks;
8
+ if (runs === undefined)
9
+ return result;
10
+ return {
11
+ ...result,
12
+ routes: {
13
+ ...result.routes,
14
+ [runsRouteOf(unit)]: (search) => {
15
+ const path = (param(search, 'path') ?? '').trim();
16
+ return { ok: true, runs: path === '' ? [] : runs.current(path) };
17
+ },
18
+ },
19
+ };
20
+ }
21
+ function collectRoutes(mounted) {
22
+ const routes = {};
23
+ const owner = new Map();
24
+ const routesOfUnit = new Map();
25
+ for (const item of mounted) {
26
+ const unit = item.face.manifest.id;
27
+ for (const [key, handler] of Object.entries(item.result.routes)) {
28
+ if (key !== unit && !key.startsWith(`${unit}/`)) {
29
+ throw new Error(`路由名要住在自己那一格的命名空间里(${unit} 或 ${unit}/…):${key} 不属于 ${unit} —— ` +
30
+ `装 / 升 / 卸时点名不到它,那个模块的数据会停在旧值`);
31
+ }
32
+ const previous = owner.get(key);
33
+ if (previous !== undefined) {
34
+ throw new Error(`路由名冲突:${key} 由套件 ${previous} 与 ${unit} 同时声明`);
35
+ }
36
+ owner.set(key, unit);
37
+ routes[key] = handler;
38
+ const own = routesOfUnit.get(unit);
39
+ if (own === undefined)
40
+ routesOfUnit.set(unit, [key]);
41
+ else
42
+ own.push(key);
43
+ }
44
+ }
45
+ return { routes, owners: owner, routesOfUnit };
46
+ }
47
+ function pickGate(mounted) {
48
+ const providers = mounted.filter((item) => item.result.gate !== undefined);
49
+ if (providers.length > 1) {
50
+ throw new Error(`启停闸门只能有一个提供者,现在是:${providers.map((item) => item.face.manifest.id).join('、')}`);
51
+ }
52
+ return providers[0]?.result.gate;
53
+ }
54
+ function pickProvision(mounted) {
55
+ const providers = mounted.filter((item) => item.result.provision !== undefined);
56
+ if (providers.length > 1) {
57
+ throw new Error(`机器供给只能有一个提供者,现在是:${providers.map((item) => item.face.manifest.id).join('、')}`);
58
+ }
59
+ return providers[0]?.result.provision;
60
+ }
61
+ function collectPrefs(mounted) {
62
+ const owner = new Map();
63
+ const prefs = [];
64
+ for (const item of mounted) {
65
+ const entry = item.result.prefs;
66
+ if (entry === undefined)
67
+ continue;
68
+ const previous = owner.get(entry.resource);
69
+ if (previous !== undefined) {
70
+ throw new Error(`偏好资源名冲突:${entry.resource} 由套件 ${previous} 与 ${item.face.manifest.id} 同时声明`);
71
+ }
72
+ owner.set(entry.resource, item.face.manifest.id);
73
+ prefs.push({ ...entry, unit: item.face.manifest.id });
74
+ }
75
+ return prefs;
76
+ }
77
+ function collectActions(mounted) {
78
+ const owner = new Map();
79
+ const actions = [];
80
+ for (const item of mounted) {
81
+ for (const entry of item.result.actions ?? []) {
82
+ const previous = owner.get(entry.resource);
83
+ if (previous !== undefined) {
84
+ throw new Error(`动作资源名冲突:${entry.resource} 由套件 ${previous} 与 ${item.face.manifest.id} 同时声明`);
85
+ }
86
+ owner.set(entry.resource, item.face.manifest.id);
87
+ actions.push({ ...entry, unit: item.face.manifest.id });
88
+ }
89
+ }
90
+ return actions;
91
+ }
92
+ function collectWarmReads(mounted) {
93
+ const declarations = mounted
94
+ .map((item) => item.result.warmReads)
95
+ .filter((declare) => declare !== undefined);
96
+ return () => {
97
+ const seen = new Set();
98
+ const reads = [];
99
+ for (const read of declarations.flatMap((declare) => declare())) {
100
+ const key = read.join(' ');
101
+ if (seen.has(key))
102
+ continue;
103
+ seen.add(key);
104
+ reads.push(read);
105
+ }
106
+ return reads;
107
+ };
108
+ }
109
+ function collectContextRenderers(mounted) {
110
+ const table = new Map();
111
+ const owner = new Map();
112
+ for (const item of mounted) {
113
+ const context = item.result.context;
114
+ if (context === undefined)
115
+ continue;
116
+ for (const kind of context.kinds) {
117
+ const previous = owner.get(kind);
118
+ if (previous !== undefined) {
119
+ throw new Error(`对话引用的 kind 冲突:${kind} 由套件 ${previous} 与 ${item.face.manifest.id} 同时认领`);
120
+ }
121
+ owner.set(kind, item.face.manifest.id);
122
+ table.set(kind, (ref) => context.render(ref));
123
+ }
124
+ }
125
+ return table;
126
+ }
127
+ export function mountUnits(faces, deps) {
128
+ const store = deps.store ?? createWorkspaceStore({ root: null });
129
+ const mounted = [];
130
+ const skipped = [];
131
+ const longTaskDisposers = [];
132
+ for (const face of faces) {
133
+ const readiness = unitReadiness(face.manifest, (dep) => deps.ctx.get(dep));
134
+ if (!readiness.canMount) {
135
+ skipped.push({ id: face.manifest.id, missing: readiness.missingHard });
136
+ continue;
137
+ }
138
+ const unit = face.manifest.id;
139
+ const longTasks = deps.longTasks === undefined
140
+ ? undefined
141
+ : createLongTaskHost({ scope: deps.longTasks.scope, unit, bump: deps.longTasks.bump });
142
+ if (longTasks !== undefined)
143
+ longTaskDisposers.push(longTasks.dispose);
144
+ const result = face.mount({
145
+ ctx: deps.ctx,
146
+ logger: deps.logger,
147
+ manifests: deps.manifests,
148
+ ...(deps.opencliCommand === undefined ? {} : { opencliCommand: deps.opencliCommand }),
149
+ ...(deps.probe === undefined ? {} : { probe: deps.probe }),
150
+ ...(deps.values === undefined ? {} : { values: deps.values }),
151
+ ...(longTasks === undefined ? {} : { longTasks: longTasks.host }),
152
+ store,
153
+ });
154
+ mounted.push({ face, readiness, result: withRunsRoute(result, unit) });
155
+ }
156
+ const collected = collectRoutes(mounted);
157
+ const prefs = collectPrefs(mounted);
158
+ const actions = collectActions(mounted);
159
+ const owners = new Map(collected.owners);
160
+ for (const entry of [...prefs, ...actions])
161
+ owners.set(entry.resource, entry.unit);
162
+ const factsOf = new Map();
163
+ for (const item of mounted) {
164
+ const declare = item.result.factsOf;
165
+ if (declare !== undefined)
166
+ factsOf.set(item.face.manifest.id, declare);
167
+ }
168
+ return {
169
+ mounted,
170
+ routes: collected.routes,
171
+ owners,
172
+ routesOfUnit: collected.routesOfUnit,
173
+ factsOf,
174
+ gate: pickGate(mounted),
175
+ prefs,
176
+ actions,
177
+ provision: pickProvision(mounted),
178
+ contextRenderers: collectContextRenderers(mounted),
179
+ skipped,
180
+ warmReads: collectWarmReads(mounted),
181
+ disposeLongTasks: () => {
182
+ for (const dispose of longTaskDisposers.splice(0))
183
+ dispose();
184
+ },
185
+ };
186
+ }
@@ -0,0 +1,22 @@
1
+ import type { UnitLibNeed } from '../api/unit.js';
2
+ import { dshHome } from './data-root';
3
+ export { dshHome };
4
+ export declare function isolatedRoot(): string;
5
+ export declare function isolatedHome(): string;
6
+ export declare const OPENCLI_NPM_PACKAGE = "@jackwener/opencli";
7
+ export declare function toolchainDir(): string;
8
+ export declare function toolchainOpencliDir(): string;
9
+ export declare function vendorPrefix(): string;
10
+ export declare function vendorPeerLink(): {
11
+ readonly dir: string;
12
+ readonly parent: string;
13
+ };
14
+ export declare function npmCacheDir(): string;
15
+ export declare function isolatedCommand(): string;
16
+ export declare function libsPrefix(): string;
17
+ export declare function libDir(name: string): string;
18
+ export declare function libInstallCommand(options: {
19
+ readonly need: UnitLibNeed;
20
+ readonly version?: string | undefined;
21
+ }): string;
22
+ export declare function isolatedInstallCommand(): string;
@@ -0,0 +1,46 @@
1
+ import { OPENCLI_PIN } from '../config/constants.js';
2
+ import { quoteShellArg } from '../lib/text.js';
3
+ import { dshHome } from './data-root';
4
+ export { dshHome };
5
+ export function isolatedRoot() {
6
+ return `${dshHome()}/workbench/opencli`;
7
+ }
8
+ export function isolatedHome() {
9
+ return `${isolatedRoot()}/home`;
10
+ }
11
+ export const OPENCLI_NPM_PACKAGE = '@jackwener/opencli';
12
+ export function toolchainDir() {
13
+ return `${isolatedRoot()}/toolchain`;
14
+ }
15
+ export function toolchainOpencliDir() {
16
+ return `${toolchainDir()}/node_modules/${OPENCLI_NPM_PACKAGE}`;
17
+ }
18
+ export function vendorPrefix() {
19
+ return `${isolatedRoot()}/plugins-npm`;
20
+ }
21
+ export function vendorPeerLink() {
22
+ const base = `${vendorPrefix()}/node_modules`;
23
+ const [scope] = OPENCLI_NPM_PACKAGE.split('/');
24
+ return { dir: `${base}/${OPENCLI_NPM_PACKAGE}`, parent: `${base}/${scope}` };
25
+ }
26
+ export function npmCacheDir() {
27
+ return `${isolatedRoot()}/npm-cache`;
28
+ }
29
+ export function isolatedCommand() {
30
+ return `${isolatedRoot()}/bin/opencli`;
31
+ }
32
+ export function libsPrefix() {
33
+ return `${isolatedRoot()}/libs`;
34
+ }
35
+ export function libDir(name) {
36
+ return `${libsPrefix()}/node_modules/${name}`;
37
+ }
38
+ export function libInstallCommand(options) {
39
+ const { need, version } = options;
40
+ const registry = process.env[need.registryEnv]?.trim() || need.registryDefault;
41
+ const spec = version ?? need.version ?? 'latest';
42
+ return `npm i --prefix '${libsPrefix()}' ${quoteShellArg(`${need.name}@${spec}`)} --registry '${registry}' --cache '${npmCacheDir()}'`;
43
+ }
44
+ export function isolatedInstallCommand() {
45
+ return `npm i --prefix '${toolchainDir()}' ${OPENCLI_NPM_PACKAGE}@${OPENCLI_PIN} --cache '${npmCacheDir()}'`;
46
+ }
@@ -0,0 +1,37 @@
1
+ import type { DshShell } from '../api/dsh.js';
2
+ export declare class WorkbenchDataError extends Error {
3
+ readonly code: string;
4
+ readonly detail?: string;
5
+ readonly hint?: string;
6
+ readonly project?: unknown;
7
+ readonly helpUrl?: string;
8
+ constructor(code: string, message: string, extra?: {
9
+ detail?: string;
10
+ hint?: string;
11
+ project?: unknown;
12
+ helpUrl?: string;
13
+ });
14
+ }
15
+ export interface OpencliResult {
16
+ readonly value: unknown;
17
+ readonly at: number;
18
+ }
19
+ export type OpencliSession = 'persistent' | 'ephemeral';
20
+ export declare function opencliCommandLine(command: string, argv: readonly string[], session?: OpencliSession): string;
21
+ export interface Opencli {
22
+ json(args: readonly string[], options?: {
23
+ key?: string;
24
+ workdir?: string;
25
+ session?: OpencliSession;
26
+ }): Promise<OpencliResult>;
27
+ }
28
+ export declare function createOpencli(options: {
29
+ shell: DshShell;
30
+ plugin: string;
31
+ logger?: {
32
+ warn?(message: string): void;
33
+ } | undefined;
34
+ command?: string;
35
+ timeoutMs?: number;
36
+ maxConcurrent?: number;
37
+ }): Opencli;