@dsh-workbench/shared 0.3.6 → 0.3.7

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 (136) hide show
  1. package/dist/api/context-ref.d.ts +0 -12
  2. package/dist/api/context-ref.js +0 -40
  3. package/dist/api/facts.d.ts +0 -6
  4. package/dist/api/facts.js +0 -8
  5. package/dist/components/ConfirmBar.css +26 -0
  6. package/dist/components/ConfirmBar.d.ts +36 -0
  7. package/dist/components/ConfirmBar.js +26 -0
  8. package/dist/components/Form.d.ts +7 -5
  9. package/dist/components/Form.js +5 -3
  10. package/dist/components/FormNodes.d.ts +17 -13
  11. package/dist/components/FormNodes.js +13 -5
  12. package/dist/components/FormTypes.d.ts +39 -14
  13. package/dist/components/MockStrip.css +11 -1
  14. package/dist/components/StateDot.d.ts +5 -0
  15. package/dist/components/StateDot.js +10 -0
  16. package/dist/config/constants.d.ts +2 -0
  17. package/dist/config/constants.js +2 -0
  18. package/dist/host/__tests__/fakes.d.ts +41 -0
  19. package/dist/host/__tests__/fakes.js +76 -0
  20. package/dist/host/__tests__/preflight-fixtures.d.ts +19 -0
  21. package/dist/host/__tests__/preflight-fixtures.js +47 -0
  22. package/dist/host/board-fact.d.ts +8 -0
  23. package/dist/host/board-fact.js +16 -0
  24. package/dist/host/browser-read.js +2 -1
  25. package/dist/host/context-inject.d.ts +24 -0
  26. package/dist/host/context-inject.js +74 -0
  27. package/dist/host/context-ref.d.ts +13 -0
  28. package/dist/host/context-ref.js +41 -0
  29. package/dist/host/diy-debug.d.ts +80 -0
  30. package/dist/host/diy-debug.js +88 -0
  31. package/dist/host/diy-record.d.ts +135 -0
  32. package/dist/host/diy-record.js +162 -0
  33. package/dist/host/events.d.ts +9 -0
  34. package/dist/host/events.js +70 -0
  35. package/dist/host/facts.d.ts +6 -0
  36. package/dist/host/facts.js +9 -0
  37. package/dist/host/git.js +1 -1
  38. package/dist/host/http.d.ts +2 -2
  39. package/dist/host/http.js +3 -3
  40. package/dist/host/long-task.d.ts +5 -0
  41. package/dist/host/long-task.js +3 -0
  42. package/dist/host/mount-units.d.ts +52 -0
  43. package/dist/host/mount-units.js +186 -0
  44. package/dist/host/opencli-runtime.js +1 -1
  45. package/dist/host/opencli-window.js +1 -1
  46. package/dist/host/opencli.js +2 -1
  47. package/dist/host/panel-state.d.ts +120 -0
  48. package/dist/host/panel-state.js +176 -0
  49. package/dist/host/plugin-source.js +1 -1
  50. package/dist/host/preflight.d.ts +58 -0
  51. package/dist/host/preflight.js +233 -0
  52. package/dist/host/probe-cache.d.ts +6 -0
  53. package/dist/host/probe-cache.js +19 -0
  54. package/dist/host/probe-libs.d.ts +45 -0
  55. package/dist/host/probe-libs.js +61 -0
  56. package/dist/host/probe-units.d.ts +17 -0
  57. package/dist/host/probe-units.js +102 -0
  58. package/dist/host/probe-verdict.d.ts +7 -0
  59. package/dist/host/probe-verdict.js +143 -0
  60. package/dist/host/shell-text.d.ts +13 -0
  61. package/dist/host/shell-text.js +20 -0
  62. package/dist/host/tool-scope.d.ts +32 -0
  63. package/dist/host/tool-scope.js +84 -0
  64. package/dist/host/unit-declaration.d.ts +65 -0
  65. package/dist/host/unit-declaration.js +213 -0
  66. package/dist/host/unit-readiness.d.ts +7 -0
  67. package/dist/host/unit-readiness.js +5 -0
  68. package/dist/host/unit-registry.d.ts +9 -0
  69. package/dist/host/unit-registry.js +33 -0
  70. package/dist/host/version-range.d.ts +2 -0
  71. package/dist/host/version-range.js +73 -0
  72. package/dist/host.d.ts +7 -4
  73. package/dist/host.js +7 -4
  74. package/dist/i18n/en.js +4 -0
  75. package/dist/i18n/index.d.ts +5 -1
  76. package/dist/i18n/zh.d.ts +4 -0
  77. package/dist/i18n/zh.js +4 -0
  78. package/dist/index.d.ts +1 -4
  79. package/dist/index.js +1 -4
  80. package/dist/lib/board-fact.d.ts +0 -7
  81. package/dist/lib/board-fact.js +0 -15
  82. package/dist/lib/diy-record.d.ts +38 -1
  83. package/dist/lib/diy-record.js +66 -0
  84. package/dist/lib/text.d.ts +0 -13
  85. package/dist/lib/text.js +0 -20
  86. package/dist/testing/declaration.js +1 -1
  87. package/package.json +1 -1
  88. package/src/api/__tests__/facts.test.ts +0 -13
  89. package/src/api/context-ref.ts +0 -39
  90. package/src/api/facts.ts +0 -9
  91. package/src/components/ConfirmBar.css +26 -0
  92. package/src/components/ConfirmBar.test.tsx +130 -0
  93. package/src/components/ConfirmBar.tsx +63 -0
  94. package/src/components/Form.test.tsx +15 -0
  95. package/src/components/Form.tsx +7 -5
  96. package/src/components/FormNodes.tsx +50 -26
  97. package/src/components/FormTypes.test.ts +31 -0
  98. package/src/components/FormTypes.ts +46 -15
  99. package/src/components/FormWiring.test.tsx +1 -0
  100. package/src/config/constants.ts +3 -0
  101. package/src/host/__tests__/board-fact.test.ts +47 -0
  102. package/src/{lib → host/__tests__}/diy-debug.test.ts +1 -1
  103. package/src/{lib → host/__tests__}/diy-record.test.ts +137 -1
  104. package/src/host/__tests__/facts.test.ts +13 -0
  105. package/src/host/__tests__/long-task.test.ts +42 -0
  106. package/src/{lib → host/__tests__}/panel-state.test.ts +1 -1
  107. package/src/host/__tests__/preflight-route.test.ts +33 -0
  108. package/src/host/__tests__/shell-text.test.ts +44 -0
  109. package/src/host/board-fact.ts +14 -0
  110. package/src/host/browser-read.ts +2 -1
  111. package/src/host/context-ref.ts +40 -0
  112. package/src/{lib → host}/diy-record.ts +95 -1
  113. package/src/host/facts.ts +10 -0
  114. package/src/host/git.ts +1 -1
  115. package/src/host/http.ts +4 -4
  116. package/src/host/long-task.ts +9 -0
  117. package/src/host/opencli-runtime.ts +1 -1
  118. package/src/host/opencli-window.ts +1 -1
  119. package/src/host/opencli.ts +2 -1
  120. package/src/host/plugin-source.ts +1 -1
  121. package/src/host/shell-text.ts +21 -0
  122. package/src/{api → host}/unit-declaration.ts +1 -1
  123. package/src/host.ts +7 -4
  124. package/src/i18n/en.ts +4 -0
  125. package/src/i18n/zh.ts +4 -0
  126. package/src/index.ts +1 -4
  127. package/src/lib/board-fact.test.ts +1 -33
  128. package/src/lib/board-fact.ts +0 -13
  129. package/src/lib/text.test.ts +1 -43
  130. package/src/lib/text.ts +0 -22
  131. package/src/testing/declaration.ts +1 -1
  132. package/dist/lib/lint-probe.d.ts +0 -1
  133. package/dist/lib/lint-probe.js +0 -6
  134. /package/src/{api → host}/__tests__/unit-declaration.test.ts +0 -0
  135. /package/src/{lib → host}/diy-debug.ts +0 -0
  136. /package/src/{lib → host}/panel-state.ts +0 -0
@@ -0,0 +1,162 @@
1
+ /**
2
+ * 录制约定的有效期(15 分钟,毫秒):超过它没动静的约定算过期、当场让人。
3
+ */
4
+ export const RECORD_STALE_MS = 15 * 60 * 1000;
5
+ /**
6
+ * 一张图最多试几次(初拍 + 一次重试):失败如实报过就停,不每拍重试到底。
7
+ */
8
+ export const RECORD_SHOT_TRIES = 2;
9
+ /**
10
+ * 把工具入参收成人话动作。
11
+ * @param raw - 任意入参值。
12
+ * @returns `'peek'` / `'halt'` / `'stop'` 原样;别的值(含 `undefined`、多余空白)给 `null`,由调用方拒。
13
+ */
14
+ export function parseRecordIntent(raw) {
15
+ return raw === 'peek' || raw === 'halt' || raw === 'stop' ? raw : null;
16
+ }
17
+ /**
18
+ * 这一拍该不该落图:页面计数涨过 `shotsSeen` 才算;同一个计数已经试满就先放下,等他自己再按。
19
+ * @param state - 当前有效的那份约定。
20
+ * @param pageShots - 页面浮条上那个「已拍」计数。
21
+ * @returns 该落的那个计数;不该落给 `null`(没涨 / 试满了 / 不是个正经数)。
22
+ */
23
+ export function shotDueOf(state, pageShots) {
24
+ if (!Number.isFinite(pageShots))
25
+ return null;
26
+ const n = Math.floor(pageShots);
27
+ if (n <= state.shotsSeen)
28
+ return null;
29
+ if (state.shotTries !== null && state.shotTries.n === n && state.shotTries.tries >= RECORD_SHOT_TRIES)
30
+ return null;
31
+ return n;
32
+ }
33
+ function alive(state, now) {
34
+ if (state === null)
35
+ return null;
36
+ return now - state.at <= RECORD_STALE_MS ? state : null;
37
+ }
38
+ function ownerOf(state, sessionId, now) {
39
+ const held = alive(state, now);
40
+ if (held === null)
41
+ return 'none';
42
+ return held.sessionId === sessionId ? held : 'not-owner';
43
+ }
44
+ /**
45
+ * 造一份新的录制约定存储(进程内存,同一时刻只可能有一份有效约定)。
46
+ * @returns 存取口;动作成不成都在返回的 `RecordMove` 里,不抛异常。
47
+ */
48
+ export function createRecordStore() {
49
+ let held = null;
50
+ const live = (now) => alive(held, now);
51
+ const withShot = (found, shot, now, error) => {
52
+ const row = error === undefined ? shot : { n: shot.n, at: shot.at, path: shot.path, error };
53
+ const kept = found.shots.filter((one) => one.n !== shot.n);
54
+ const tries = found.shotTries !== null && found.shotTries.n === shot.n ? found.shotTries.tries : 0;
55
+ return {
56
+ ...found,
57
+ shots: [...kept, row].sort((one, two) => one.n - two.n),
58
+ shotsSeen: error === undefined ? Math.max(found.shotsSeen, shot.n) : found.shotsSeen,
59
+ shotTries: error === undefined ? null : { n: shot.n, tries: tries + 1 },
60
+ at: now,
61
+ };
62
+ };
63
+ return {
64
+ live,
65
+ begin(pact) {
66
+ const current = alive(held, pact.now);
67
+ if (current !== null) {
68
+ return { ok: false, code: current.sessionId === pact.sessionId ? 'already-held' : 'held-by-other' };
69
+ }
70
+ const state = {
71
+ phase: 'armed',
72
+ sessionId: pact.sessionId,
73
+ workspace: pact.workspace,
74
+ unit: pact.unit,
75
+ name: pact.name,
76
+ url: pact.url,
77
+ title: pact.title,
78
+ action: pact.action,
79
+ expect: pact.expect,
80
+ captureFile: pact.captureFile,
81
+ startedAt: pact.now,
82
+ t0: null,
83
+ t1: null,
84
+ shots: [],
85
+ shotsSeen: 0,
86
+ shotTries: null,
87
+ beats: 0,
88
+ beatMs: 0,
89
+ at: pact.now,
90
+ };
91
+ held = state;
92
+ return { ok: true, state };
93
+ },
94
+ started(sessionId, at, now) {
95
+ const found = ownerOf(held, sessionId, now);
96
+ if (typeof found === 'string')
97
+ return { ok: false, code: found };
98
+ if (found.phase === 'recording')
99
+ return { ok: false, code: 'already-recording' };
100
+ if (found.phase !== 'armed')
101
+ return { ok: false, code: 'not-armed' };
102
+ const state = { ...found, phase: 'recording', t0: at, at: now };
103
+ held = state;
104
+ return { ok: true, state };
105
+ },
106
+ finished(sessionId, at, now) {
107
+ const found = ownerOf(held, sessionId, now);
108
+ if (typeof found === 'string')
109
+ return { ok: false, code: found };
110
+ if (found.phase !== 'recording')
111
+ return { ok: false, code: 'not-recording' };
112
+ const state = { ...found, phase: 'finished', t1: at, at: now };
113
+ held = state;
114
+ return { ok: true, state };
115
+ },
116
+ shotTaken(sessionId, shot, now) {
117
+ const found = ownerOf(held, sessionId, now);
118
+ if (typeof found === 'string')
119
+ return { ok: false, code: found };
120
+ const state = withShot(found, shot, now, undefined);
121
+ held = state;
122
+ return { ok: true, state };
123
+ },
124
+ shotFailed(sessionId, shot, now) {
125
+ const found = ownerOf(held, sessionId, now);
126
+ if (typeof found === 'string')
127
+ return { ok: false, code: found };
128
+ const state = withShot(found, shot, now, shot.error === undefined ? '落图失败' : shot.error);
129
+ held = state;
130
+ return { ok: true, state };
131
+ },
132
+ beat(sessionId, ms, now) {
133
+ const found = ownerOf(held, sessionId, now);
134
+ if (typeof found === 'string')
135
+ return { ok: false, code: found };
136
+ const state = {
137
+ ...found,
138
+ beats: found.beats + 1,
139
+ beatMs: Math.max(0, Math.floor(ms)),
140
+ at: now,
141
+ };
142
+ held = state;
143
+ return { ok: true, state };
144
+ },
145
+ stop(sessionId, now) {
146
+ const found = ownerOf(held, sessionId, now);
147
+ if (typeof found === 'string')
148
+ return { ok: false, code: found };
149
+ held = null;
150
+ return { ok: true, state: found };
151
+ },
152
+ };
153
+ }
154
+ /**
155
+ * 「谁还占着、在录哪个动作」的一句话,用于别人来抢时说清现况。
156
+ * @param state - 当前有效的那份约定。
157
+ * @returns 形如「会话 x(自 HH:mm:ssZ 起在录 y 的「动作」)」的中文串。
158
+ */
159
+ export function recordOwnerText(state) {
160
+ const at = new Date(state.startedAt).toISOString().slice(11, 19);
161
+ return `会话 ${state.sessionId}(自 ${at}Z 起在录 ${state.unit} 的「${state.action}」)`;
162
+ }
@@ -0,0 +1,9 @@
1
+ import type { DshHostContext, DshRequest, DshResponse } from '@dsh-workbench/shared';
2
+ import type { FactLog } from './facts';
3
+ export interface FactStream {
4
+ handler(req: DshRequest, res: DshResponse): void;
5
+ closeAll(): void;
6
+ size(): number;
7
+ }
8
+ export declare function createFactStream(log: FactLog): FactStream;
9
+ export declare function registerFactStream(ctx: DshHostContext, log: FactLog): () => void;
@@ -0,0 +1,70 @@
1
+ import { EVENTS_PATH } from '@dsh-workbench/shared';
2
+ const HEARTBEAT_MS = 25_000;
3
+ function frame(payload) {
4
+ return `data: ${JSON.stringify(payload)}\n\n`;
5
+ }
6
+ export function createFactStream(log) {
7
+ const connections = new Set();
8
+ const cleanups = new Map();
9
+ const drop = (res) => {
10
+ cleanups.get(res)?.();
11
+ cleanups.delete(res);
12
+ connections.delete(res);
13
+ };
14
+ return {
15
+ size: () => connections.size,
16
+ closeAll() {
17
+ for (const res of [...connections]) {
18
+ drop(res);
19
+ res.end?.();
20
+ }
21
+ },
22
+ handler(req, res) {
23
+ if (req.method !== undefined && req.method !== 'GET') {
24
+ res.statusCode = 405;
25
+ res.end();
26
+ return;
27
+ }
28
+ const url = req.url ?? '/';
29
+ const cut = url.indexOf('?');
30
+ const since = Number(new URLSearchParams(cut === -1 ? '' : url.slice(cut + 1)).get('since') ?? 0) || 0;
31
+ const opening = log.since(since);
32
+ if (typeof res.write !== 'function') {
33
+ res.statusCode = 200;
34
+ res.setHeader('content-type', 'application/json; charset=utf-8');
35
+ res.end(JSON.stringify({ ok: true, ...opening }));
36
+ return;
37
+ }
38
+ const headers = {
39
+ 'content-type': 'text/event-stream; charset=utf-8',
40
+ 'cache-control': 'no-store, no-transform',
41
+ connection: 'keep-alive',
42
+ };
43
+ if (typeof res.writeHead === 'function')
44
+ res.writeHead(200, headers);
45
+ else {
46
+ res.statusCode = 200;
47
+ for (const [name, value] of Object.entries(headers))
48
+ res.setHeader(name, value);
49
+ }
50
+ res.write(': connected\n\n');
51
+ res.write(frame({ type: 'sync', ...opening }));
52
+ connections.add(res);
53
+ const timer = setInterval(() => res.write?.(': ping\n\n'), HEARTBEAT_MS);
54
+ const unsubscribe = log.subscribe((change) => res.write?.(frame({ type: 'bump', ...change })));
55
+ cleanups.set(res, () => {
56
+ clearInterval(timer);
57
+ unsubscribe();
58
+ });
59
+ res.on?.('close', () => drop(res));
60
+ },
61
+ };
62
+ }
63
+ export function registerFactStream(ctx, log) {
64
+ const stream = createFactStream(log);
65
+ const unregister = ctx.webServer.register({ kind: 'exact', path: EVENTS_PATH, handler: stream.handler });
66
+ return () => {
67
+ stream.closeAll();
68
+ unregister();
69
+ };
70
+ }
@@ -22,3 +22,9 @@ export interface ToolUnit {
22
22
  }
23
23
  export declare function toolUnits(manifests: readonly UnitManifest[]): Map<string, ToolUnit>;
24
24
  export declare function wroteNow(result: unknown): boolean | undefined;
25
+ /**
26
+ * 由单元 id 拼出它那一族事实的范围名。
27
+ * @param unit 单元 id,不带后缀。
28
+ * @returns <unit>* —— 永远是范围名,不是某一条事实。
29
+ */
30
+ export declare function unitFactScope(unit: string): string;
@@ -1,3 +1,4 @@
1
+ import { FACT_SCOPE_SUFFIX } from '../api/facts.js';
1
2
  export function createFactLog(options = {}) {
2
3
  const epoch = options.epoch ?? `e${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
3
4
  let rev = 0;
@@ -42,3 +43,11 @@ export function wroteNow(result) {
42
43
  const wrote = value?.wrote;
43
44
  return typeof wrote === 'boolean' ? wrote : undefined;
44
45
  }
46
+ /**
47
+ * 由单元 id 拼出它那一族事实的范围名。
48
+ * @param unit 单元 id,不带后缀。
49
+ * @returns <unit>* —— 永远是范围名,不是某一条事实。
50
+ */
51
+ export function unitFactScope(unit) {
52
+ return `${unit}${FACT_SCOPE_SUFFIX}`;
53
+ }
package/dist/host/git.js CHANGED
@@ -1,4 +1,4 @@
1
- import { quoteShellArg, tailLines } from '../lib/text.js';
1
+ import { quoteShellArg, tailLines } from './shell-text.js';
2
2
  import { runShell } from './shell.js';
3
3
  export function createHeadReader(options) {
4
4
  const { shell, logger } = options;
@@ -1,5 +1,5 @@
1
1
  import type { DshHostContext } from '../api/dsh.js';
2
- import { type PanelStateStore } from '../lib/panel-state.js';
2
+ import { type PanelStateStore } from './panel-state.js';
3
3
  import { type ProvisionHandler } from './provision.js';
4
4
  export type RouteHandler = (search: URLSearchParams) => Promise<unknown> | unknown;
5
5
  export type RouteTable = Record<string, RouteHandler>;
@@ -27,7 +27,7 @@ export declare function changesHandler(log: {
27
27
  }): RouteHandler;
28
28
  export declare function preflightHandler(preflight: {
29
29
  run(options?: {
30
- force?: boolean;
30
+ hard?: boolean;
31
31
  }): Promise<unknown>;
32
32
  snapshot(): unknown;
33
33
  }): RouteHandler;
package/dist/host/http.js CHANGED
@@ -1,5 +1,5 @@
1
- import { unitFactScope } from '../api/facts.js';
2
- import { parseCardsReport, parseCrashReport } from '../lib/panel-state.js';
1
+ import { unitFactScope } from './facts.js';
2
+ import { parseCardsReport, parseCrashReport } from './panel-state.js';
3
3
  import { ACTIONS_PREFIX, API_PREFIX, DIY_DEBUG_PATH, DIY_RECORD_PATH, PANEL_STATE_PREFIX, PLUGIN_ID, PREFS_PREFIX, PROVISION_PREFIX } from '../config/constants.js';
4
4
  import { parseProvisionBody, } from './provision.js';
5
5
  import { WorkbenchDataError } from '../host/opencli.js';
@@ -179,7 +179,7 @@ export function changesHandler(log) {
179
179
  }
180
180
  export function preflightHandler(preflight) {
181
181
  return (search) => {
182
- void preflight.run({ force: flag(search, 'force') });
182
+ void preflight.run({ hard: flag(search, 'hard') });
183
183
  return { ok: true, ...preflight.snapshot() };
184
184
  };
185
185
  }
@@ -39,11 +39,16 @@ export interface LongTaskDeps<S extends LongTaskBase> {
39
39
  readonly facts?: ((change: LongTaskChange<S>) => readonly string[] | undefined) | undefined;
40
40
  readonly recentMs?: number | undefined;
41
41
  }
42
+ /**
43
+ * 一个单元的长任务注册表:`current(workspace)` 是面板那一行要的投影,`live()` 是外壳侧栏那颗
44
+ * loading 点要的(**不分工作区、不分目标**,把现在活着的每一趟交出来),`stop` 只把中断信号转给生产方。
45
+ */
42
46
  export interface LongTaskRegistry<S extends LongTaskBase> {
43
47
  start(options: LongTaskStart): S;
44
48
  snapshot(id: string): S | null;
45
49
  active(workspace: string, target: string): S | null;
46
50
  current(workspace: string): readonly Readonly<Record<string, unknown>>[];
51
+ live(): readonly S[];
47
52
  stop(id: string, by?: string): boolean;
48
53
  onChange(listener: (change: LongTaskChange<S>) => void): () => void;
49
54
  }
@@ -55,6 +55,9 @@ export function createLongTaskRegistry(deps) {
55
55
  .reverse()
56
56
  .map((snapshot) => ({ ...deps.view(snapshot), id: snapshot.id }));
57
57
  },
58
+ live() {
59
+ return [...runs.values()].filter((snapshot) => LIVE.has(snapshot.status));
60
+ },
58
61
  start(options) {
59
62
  const running = this.active(options.workspace, options.target);
60
63
  if (running !== null)
@@ -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
+ }
@@ -1,5 +1,5 @@
1
1
  import { OPENCLI_PIN } from '../config/constants.js';
2
- import { quoteShellArg } from '../lib/text.js';
2
+ import { quoteShellArg } from './shell-text.js';
3
3
  import { dshHome } from './data-root.js';
4
4
  export { dshHome };
5
5
  export function isolatedRoot() {
@@ -1,4 +1,4 @@
1
- import { quoteShellArg } from '../lib/text.js';
1
+ import { quoteShellArg } from './shell-text.js';
2
2
  const OPENCLI_WINDOW_ENV = 'OPENCLI_WINDOW=background';
3
3
  export function backgroundPrefix() {
4
4
  return [quoteShellArg('env'), quoteShellArg(OPENCLI_WINDOW_ENV)];
@@ -1,5 +1,6 @@
1
1
  import { OPENCLI_MAX_CONCURRENT, OPENCLI_TIMEOUT_MS } from '../config/constants.js';
2
- import { parseJsonLoose, quoteShellArg, tailLines } from '../lib/text.js';
2
+ import { parseJsonLoose } from '../lib/text.js';
3
+ import { quoteShellArg, tailLines } from './shell-text.js';
3
4
  import { helpUrlOf, parseCliErrorEnvelope } from './cli-error.js';
4
5
  import { backgroundPrefix } from './opencli-window.js';
5
6
  import { runShell } from './shell.js';