@dsh-workbench/shared 1.1.7 → 1.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (239) hide show
  1. package/dist/api/blocker-table.d.ts +1 -0
  2. package/dist/api/blocker-table.js +1 -1
  3. package/dist/components/SelectField.css +0 -6
  4. package/dist/components/SelectField.d.ts +2 -3
  5. package/dist/components/SelectField.js +9 -30
  6. package/dist/components/SelectFieldNodes.d.ts +3 -1
  7. package/dist/components/SelectFieldNodes.js +2 -1
  8. package/dist/components/SelectFieldWheel.d.ts +9 -0
  9. package/dist/components/SelectFieldWheel.js +35 -0
  10. package/dist/config/constants.d.ts +4 -4
  11. package/dist/config/constants.js +4 -4
  12. package/dist/contract/capabilities.d.ts +29 -0
  13. package/dist/contract/capabilities.js +1 -0
  14. package/dist/contract/needs.d.ts +3 -3
  15. package/dist/contract/needs.js +1 -1
  16. package/dist/contract/plugin-source.d.ts +19 -0
  17. package/dist/contract/protocol/bundle-install.d.ts +3 -1
  18. package/dist/contract/protocol/bundle-install.js +3 -0
  19. package/dist/contract/protocol/market-index.d.ts +1 -0
  20. package/dist/contract/protocol/platform.d.ts +1 -1
  21. package/dist/contract/runner.d.ts +6 -0
  22. package/dist/contract/runner.js +1 -0
  23. package/dist/contract/unit.d.ts +4 -4
  24. package/dist/host/adapter-contract.d.ts +3 -0
  25. package/dist/host/adapter-contract.js +93 -0
  26. package/dist/host/adapter-set.js +6 -4
  27. package/dist/host/adapter-types.d.ts +1 -1
  28. package/dist/host/bundle-manager.d.ts +3 -0
  29. package/dist/host/bundle-manager.js +181 -0
  30. package/dist/host/cli.js +3 -1
  31. package/dist/host/config-sync.d.ts +48 -0
  32. package/dist/host/config-sync.js +342 -0
  33. package/dist/host/dsh-plugins.d.ts +5 -0
  34. package/dist/host/dsh-plugins.js +51 -0
  35. package/dist/host/extension.js +1 -1
  36. package/dist/host/flow/check.d.ts +9 -0
  37. package/dist/host/flow/check.js +179 -0
  38. package/dist/host/flow/expr.d.ts +30 -0
  39. package/dist/host/flow/expr.js +99 -0
  40. package/dist/host/flow/kit.d.ts +14 -0
  41. package/dist/host/flow/kit.js +13 -0
  42. package/dist/host/flow/run.d.ts +13 -0
  43. package/dist/host/flow/run.js +287 -0
  44. package/dist/host/flow/types.d.ts +188 -0
  45. package/dist/host/flow/types.js +1 -0
  46. package/dist/host/host-actions.d.ts +2 -0
  47. package/dist/host/host-actions.js +5 -0
  48. package/dist/host/host-kit.d.ts +19 -0
  49. package/dist/host/host-kit.js +14 -0
  50. package/dist/host/http.d.ts +2 -6
  51. package/dist/host/http.js +2 -2
  52. package/dist/host/installed-module.d.ts +23 -0
  53. package/dist/host/installed-module.js +264 -0
  54. package/dist/host/lib-install.d.ts +7 -9
  55. package/dist/host/lib-install.js +9 -12
  56. package/dist/host/lib-runners.d.ts +3 -5
  57. package/dist/host/lib-runners.js +2 -2
  58. package/dist/host/local-declaration.d.ts +7 -0
  59. package/dist/host/local-declaration.js +33 -0
  60. package/dist/host/market-index-claim.d.ts +0 -1
  61. package/dist/host/market-index-claim.js +0 -3
  62. package/dist/host/market-index-doc.d.ts +1 -45
  63. package/dist/host/market-index-parse.d.ts +9 -4
  64. package/dist/host/market-index-parse.js +17 -92
  65. package/dist/host/market-index.d.ts +5 -6
  66. package/dist/host/market-index.js +29 -87
  67. package/dist/host/module-root.d.ts +1 -0
  68. package/dist/host/module-root.js +5 -0
  69. package/dist/host/npm-registry.d.ts +10 -0
  70. package/dist/host/npm-registry.js +16 -0
  71. package/dist/host/opencli-runner.d.ts +2 -1
  72. package/dist/host/package-content.d.ts +7 -0
  73. package/dist/host/package-content.js +69 -0
  74. package/dist/host/package-kind.d.ts +13 -0
  75. package/dist/host/package-kind.js +51 -0
  76. package/dist/host/package-route.d.ts +31 -0
  77. package/dist/host/package-route.js +57 -0
  78. package/dist/host/paths.d.ts +11 -19
  79. package/dist/host/paths.js +16 -43
  80. package/dist/host/platform.js +1 -1
  81. package/dist/host/plugin-ledger.d.ts +8 -8
  82. package/dist/host/plugin-ledger.js +23 -16
  83. package/dist/host/plugin.d.ts +1 -2
  84. package/dist/host/plugin.js +4 -5
  85. package/dist/host/port-types.d.ts +42 -9
  86. package/dist/host/preflight.d.ts +3 -2
  87. package/dist/host/profile-create.d.ts +18 -0
  88. package/dist/host/profile-create.js +118 -0
  89. package/dist/host/profile-registries.d.ts +10 -0
  90. package/dist/host/profile-registries.js +150 -0
  91. package/dist/host/registry-declaration.d.ts +21 -0
  92. package/dist/host/registry-declaration.js +33 -0
  93. package/dist/host/release-probe.d.ts +34 -0
  94. package/dist/host/release-probe.js +88 -0
  95. package/dist/host/runner.d.ts +26 -0
  96. package/dist/host/runner.js +50 -0
  97. package/dist/host/shell.d.ts +0 -2
  98. package/dist/host/shell.js +0 -7
  99. package/dist/host/spec.js +1 -1
  100. package/dist/host/supply-index.d.ts +1 -0
  101. package/dist/host/supply-index.js +8 -7
  102. package/dist/host/supply-port.js +13 -3
  103. package/dist/host/unit-face.d.ts +2 -1
  104. package/dist/host/version.js +11 -6
  105. package/dist/host.d.ts +24 -6
  106. package/dist/host.js +23 -6
  107. package/dist/i18n/en.js +19 -20
  108. package/dist/i18n/index.d.ts +20 -21
  109. package/dist/i18n/runtime-terms.d.ts +15 -0
  110. package/dist/i18n/runtime-terms.js +22 -0
  111. package/dist/i18n/zh.d.ts +19 -20
  112. package/dist/i18n/zh.js +19 -20
  113. package/dist/index.d.ts +4 -0
  114. package/dist/index.js +4 -0
  115. package/dist/testing/fake-adapter-set.d.ts +5 -0
  116. package/dist/testing/fake-adapter-set.js +150 -0
  117. package/dist/testing/needs.js +1 -1
  118. package/dist/testing.d.ts +1 -0
  119. package/dist/testing.js +1 -0
  120. package/package.json +1 -1
  121. package/src/api/__tests__/blocker-table.test.ts +25 -0
  122. package/src/api/{workbench-client.test.ts → __tests__/workbench-client.test.ts} +2 -2
  123. package/src/api/blocker-table.ts +6 -2
  124. package/src/components/SelectField.css +0 -6
  125. package/src/components/SelectField.tsx +7 -36
  126. package/src/components/SelectFieldNodes.tsx +4 -1
  127. package/src/components/{Card.test.tsx → __tests__/Card.test.tsx} +2 -2
  128. package/src/components/{CardAnchor.test.tsx → __tests__/CardAnchor.test.tsx} +3 -3
  129. package/src/components/{CardHead.test.tsx → __tests__/CardHead.test.tsx} +1 -1
  130. package/src/components/{ChoiceGroup.test.tsx → __tests__/ChoiceGroup.test.tsx} +2 -2
  131. package/src/components/{ConfirmBar.test.tsx → __tests__/ConfirmBar.test.tsx} +5 -5
  132. package/src/components/{Field.test.tsx → __tests__/Field.test.tsx} +1 -1
  133. package/src/components/{Form.test.tsx → __tests__/Form.test.tsx} +5 -5
  134. package/src/components/{FormActions.test.tsx → __tests__/FormActions.test.tsx} +3 -3
  135. package/src/components/{FormRows.test.tsx → __tests__/FormRows.test.tsx} +2 -2
  136. package/src/components/{FormTypes.test.ts → __tests__/FormTypes.test.ts} +1 -1
  137. package/src/components/{FormWiring.test.tsx → __tests__/FormWiring.test.tsx} +4 -4
  138. package/src/components/{Link.test.tsx → __tests__/Link.test.tsx} +1 -1
  139. package/src/components/{Rows.test.tsx → __tests__/Rows.test.tsx} +2 -2
  140. package/src/components/{SelectField.test.tsx → __tests__/SelectField.test.tsx} +7 -37
  141. package/src/components/{Spinner.test.tsx → __tests__/Spinner.test.tsx} +4 -4
  142. package/src/config/{constants.test.ts → __tests__/constants.test.ts} +1 -1
  143. package/src/config/{storage.test.ts → __tests__/storage.test.ts} +1 -1
  144. package/src/config/constants.ts +4 -4
  145. package/src/contract/capabilities.ts +30 -0
  146. package/src/contract/needs.ts +4 -4
  147. package/src/contract/plugin-source.ts +24 -0
  148. package/src/contract/protocol/bundle-install.ts +7 -1
  149. package/src/contract/protocol/market-index.ts +1 -0
  150. package/src/contract/protocol/platform.ts +1 -1
  151. package/src/contract/runner.ts +6 -0
  152. package/src/contract/unit.ts +4 -4
  153. package/src/host/{extension.test.ts → __tests__/extension.test.ts} +4 -4
  154. package/src/host/{install-failure.test.ts → __tests__/install-failure.test.ts} +1 -1
  155. package/src/host/{installed-sources.test.ts → __tests__/installed-sources.test.ts} +1 -1
  156. package/src/host/__tests__/lib-runners.test.ts +11 -2
  157. package/src/host/__tests__/market-index-assemble.test.ts +26 -0
  158. package/src/host/__tests__/market-index.test.ts +41 -51
  159. package/src/host/__tests__/plugin-ledger.test.ts +19 -6
  160. package/src/host/{recipe.test.ts → __tests__/recipe.test.ts} +3 -3
  161. package/src/host/__tests__/runtime-layout.test.ts +14 -0
  162. package/src/host/__tests__/supply-index.test.ts +2 -2
  163. package/src/host/__tests__/supply-port.test.ts +32 -0
  164. package/src/host/adapter-contract.ts +93 -0
  165. package/src/host/adapter-set.ts +6 -4
  166. package/src/host/adapter-types.ts +1 -1
  167. package/src/host/bundle-manager.ts +195 -0
  168. package/src/host/cli.ts +3 -1
  169. package/src/host/config-sync.ts +386 -0
  170. package/src/host/dsh-plugins.ts +60 -0
  171. package/src/host/extension.ts +1 -1
  172. package/src/host/flow/__tests__/advanced.test.ts +302 -0
  173. package/src/host/flow/__tests__/check.test.ts +169 -0
  174. package/src/host/flow/__tests__/run.test.ts +301 -0
  175. package/src/host/flow/check.ts +216 -0
  176. package/src/host/flow/expr.ts +94 -0
  177. package/src/host/flow/kit.ts +30 -0
  178. package/src/host/flow/run.ts +325 -0
  179. package/src/host/flow/types.ts +199 -0
  180. package/src/host/host-actions.ts +7 -0
  181. package/src/host/host-kit.ts +26 -0
  182. package/src/host/http.ts +6 -8
  183. package/src/host/installed-module.ts +272 -0
  184. package/src/host/lib-install.ts +14 -12
  185. package/src/host/lib-runners.ts +6 -8
  186. package/src/host/local-declaration.ts +37 -0
  187. package/src/host/market-index-claim.ts +0 -4
  188. package/src/host/market-index-doc.ts +1 -53
  189. package/src/host/market-index-parse.ts +21 -85
  190. package/src/host/market-index.ts +33 -127
  191. package/src/host/module-root.ts +6 -0
  192. package/src/host/npm-registry.ts +21 -0
  193. package/src/host/package-content.ts +76 -0
  194. package/src/host/package-kind.ts +63 -0
  195. package/src/host/package-route.ts +93 -0
  196. package/src/host/paths.ts +27 -52
  197. package/src/host/platform.ts +1 -1
  198. package/src/host/plugin-ledger.ts +25 -18
  199. package/src/host/plugin.ts +4 -5
  200. package/src/host/port-types.ts +43 -9
  201. package/src/host/preflight.ts +3 -2
  202. package/src/host/profile-create.ts +133 -0
  203. package/src/host/profile-registries.ts +145 -0
  204. package/src/host/registry-declaration.ts +70 -0
  205. package/src/host/release-probe.ts +202 -0
  206. package/src/host/shell.ts +0 -9
  207. package/src/host/spec.ts +1 -1
  208. package/src/host/supply-index.ts +9 -7
  209. package/src/host/supply-port.ts +13 -3
  210. package/src/host/unit-face.ts +2 -1
  211. package/src/host/version.ts +12 -7
  212. package/src/host.ts +34 -6
  213. package/src/i18n/{index.test.ts → __tests__/index.test.ts} +3 -3
  214. package/src/i18n/{merge.test.ts → __tests__/merge.test.ts} +1 -1
  215. package/src/i18n/{zh.test.ts → __tests__/zh.test.ts} +2 -2
  216. package/src/i18n/en.ts +19 -21
  217. package/src/i18n/runtime-terms.ts +26 -0
  218. package/src/i18n/zh.ts +19 -21
  219. package/src/index.ts +4 -0
  220. package/src/lib/{board-fact.test.ts → __tests__/board-fact.test.ts} +1 -1
  221. package/src/lib/{busy.test.ts → __tests__/busy.test.ts} +1 -1
  222. package/src/lib/{form-problem.test.ts → __tests__/form-problem.test.ts} +1 -1
  223. package/src/lib/{git-address.test.ts → __tests__/git-address.test.ts} +1 -1
  224. package/src/lib/{react.test.ts → __tests__/react.test.ts} +1 -1
  225. package/src/lib/{text.test.ts → __tests__/text.test.ts} +1 -1
  226. package/src/lib/{ui.test.ts → __tests__/ui.test.ts} +1 -1
  227. package/src/testing/__tests__/fake-adapter-set.test.ts +28 -0
  228. package/src/testing/fake-adapter-set.ts +173 -0
  229. package/src/testing/needs.ts +1 -1
  230. package/src/testing.ts +1 -0
  231. package/src/host/__tests__/command-name.test.ts +0 -16
  232. package/src/host/__tests__/opencli-runner.test.ts +0 -120
  233. package/src/host/channel-plugin.ts +0 -55
  234. package/src/host/command-name.ts +0 -15
  235. package/src/host/opencli-runner.ts +0 -76
  236. package/src/host/paths.test.ts +0 -26
  237. package/src/host/plugin-source.ts +0 -137
  238. package/src/host/runtime-links.test.ts +0 -31
  239. package/src/host/runtime-links.ts +0 -23
@@ -0,0 +1,325 @@
1
+ import { evalPredicate, resolveInput, resolveValue } from './expr.js';
2
+ import type {
3
+ FlowAction,
4
+ FlowActionContext,
5
+ FlowActionRegistry,
6
+ FlowActionStep,
7
+ FlowDefinition,
8
+ FlowResult,
9
+ FlowRunOptions,
10
+ FlowRetry,
11
+ FlowScope,
12
+ FlowStepEvent,
13
+ RegisteredFlowAction,
14
+ } from './types.js';
15
+
16
+ type Sleep = (ms: number, signal: AbortSignal) => Promise<void>;
17
+
18
+ class StepFailure extends Error {
19
+ readonly stepId: string;
20
+
21
+ constructor(stepId: string, cause: unknown) {
22
+ super(cause instanceof Error ? cause.message : String(cause));
23
+ this.name = 'StepFailure';
24
+ this.stepId = stepId;
25
+ this.cause = cause;
26
+ }
27
+ }
28
+
29
+ function abortError(): Error {
30
+ const error = new Error('流程已停止');
31
+ error.name = 'AbortError';
32
+ return error;
33
+ }
34
+
35
+ function isAbortError(error: unknown): boolean {
36
+ return error instanceof Error && error.name === 'AbortError';
37
+ }
38
+
39
+ function defaultSleep(ms: number, signal: AbortSignal): Promise<void> {
40
+ return new Promise((resolve) => {
41
+ if (signal.aborted) {
42
+ resolve();
43
+ return;
44
+ }
45
+ const timer = setTimeout(() => {
46
+ signal.removeEventListener('abort', onAbort);
47
+ resolve();
48
+ }, ms);
49
+ function onAbort(): void {
50
+ clearTimeout(timer);
51
+ resolve();
52
+ }
53
+ signal.addEventListener('abort', onAbort, { once: true });
54
+ });
55
+ }
56
+
57
+ function targetOf(flow: FlowDefinition, scope: FlowScope): string {
58
+ if (flow.target === undefined) return flow.id;
59
+ const value = resolveValue(flow.target, scope);
60
+ return typeof value === 'string' && value.trim() !== '' ? value.trim() : flow.id;
61
+ }
62
+
63
+ function stopped(steps: Readonly<Record<string, unknown>>, target: string): FlowResult {
64
+ return { status: 'stopped', target, steps };
65
+ }
66
+
67
+ /**
68
+ * 建一个内存里的动作注册表;每个流程运行时各有一份,没有模块级状态。
69
+ * @returns 注册表。
70
+ */
71
+ export function createFlowActionRegistry(): FlowActionRegistry {
72
+ const entries = new Map<string, RegisteredFlowAction>();
73
+ return {
74
+ register(name, action, meta) {
75
+ const trimmed = name.trim();
76
+ if (trimmed === '') throw new Error('动作名不能为空');
77
+ entries.set(trimmed, { name: trimmed, action, meta: meta ?? {} });
78
+ },
79
+ get(name) {
80
+ return entries.get(name);
81
+ },
82
+ has(name) {
83
+ return entries.has(name);
84
+ },
85
+ list() {
86
+ return [...entries.values()];
87
+ },
88
+ };
89
+ }
90
+
91
+ async function callWithRetry(
92
+ action: FlowAction,
93
+ input: unknown,
94
+ context: FlowActionContext,
95
+ retry: FlowRetry | undefined,
96
+ sleep: Sleep,
97
+ signal: AbortSignal,
98
+ onAttempt?: (attempt: number) => void,
99
+ ): Promise<unknown> {
100
+ const attempts = Math.max(0, Math.min(5, Math.trunc(retry?.attempts ?? 0)));
101
+ const delayMs = Math.max(0, Math.min(60_000, Math.trunc(retry?.delayMs ?? 0)));
102
+ let last: unknown;
103
+ for (let attempt = 1; attempt <= attempts + 1; attempt += 1) {
104
+ if (signal.aborted) throw abortError();
105
+ onAttempt?.(attempt);
106
+ try {
107
+ return await action(input, context);
108
+ } catch (error) {
109
+ last = error;
110
+ if (signal.aborted) throw abortError();
111
+ if (attempt > attempts) break;
112
+ if (delayMs > 0) await sleep(delayMs, signal);
113
+ }
114
+ }
115
+ throw last;
116
+ }
117
+
118
+ interface RunOneOptions {
119
+ readonly step: FlowActionStep;
120
+ readonly title: string;
121
+ readonly stepId: string;
122
+ readonly scope: FlowScope;
123
+ readonly actions: FlowActionRegistry;
124
+ readonly signal: AbortSignal;
125
+ readonly runId: string;
126
+ readonly workspace: string;
127
+ readonly sleep: Sleep;
128
+ readonly now: () => number;
129
+ readonly emit: (event: FlowStepEvent) => void;
130
+ }
131
+
132
+ async function runOneInner(options: RunOneOptions): Promise<unknown> {
133
+ const { step, title, stepId, scope, actions, signal, runId, workspace, sleep, now, emit } =
134
+ options;
135
+ const entry = actions.get(step.action);
136
+ if (entry === undefined) throw new Error(`注册表里没有这个动作:${step.action}`);
137
+ const input = resolveInput(step.with, scope);
138
+ const context: FlowActionContext = {
139
+ signal,
140
+ runId,
141
+ workspace,
142
+ stepId,
143
+ log: (text) => {
144
+ emit({ stepId, title, status: 'running', message: text });
145
+ },
146
+ login: (url) => {
147
+ emit({ stepId, title, status: 'needs-login', loginUrl: url });
148
+ },
149
+ };
150
+
151
+ if (step.poll !== undefined) {
152
+ const everyMs = Math.max(100, Math.min(60_000, Math.trunc(step.poll.everyMs)));
153
+ const rawMax = Math.trunc(step.poll.maxMs);
154
+ const maxMs = rawMax === 0 ? 0 : Math.max(everyMs, Math.min(1_800_000, rawMax));
155
+ const startedAt = now();
156
+ let pollAttempt = 0;
157
+ for (;;) {
158
+ if (signal.aborted) throw abortError();
159
+ pollAttempt += 1;
160
+ emit({
161
+ stepId,
162
+ title,
163
+ status: 'running',
164
+ attempt: pollAttempt,
165
+ message: `第 ${pollAttempt} 次`,
166
+ });
167
+ const value = await callWithRetry(entry.action, input, context, step.retry, sleep, signal);
168
+ if (evalPredicate(step.poll.until, { ...scope, value })) return value;
169
+ if (maxMs > 0 && now() - startedAt >= maxMs) {
170
+ if (step.poll.onTimeout === 'continue') return undefined;
171
+ throw new Error(`步骤 ${step.id} 轮询超时(${maxMs}ms)`);
172
+ }
173
+ await sleep(everyMs, signal);
174
+ }
175
+ }
176
+
177
+ return callWithRetry(entry.action, input, context, step.retry, sleep, signal, (attempt) => {
178
+ emit({ stepId, title, status: 'running', attempt });
179
+ });
180
+ }
181
+
182
+ async function runOne(options: RunOneOptions): Promise<unknown> {
183
+ try {
184
+ return await runOneInner(options);
185
+ } catch (error) {
186
+ if (error instanceof StepFailure || isAbortError(error)) throw error;
187
+ throw new StepFailure(options.stepId, error);
188
+ }
189
+ }
190
+
191
+ /**
192
+ * 顺序跑一条流程:条件跳过、重试、轮询、逐项展开、子流程、取消;失败停在具体步骤。
193
+ * 不碰平台、不落盘;动作怎么实现由注册表决定。
194
+ * @param options 流程、输入、动作表、取消信号、身份与进度回调。
195
+ * @returns 成功 / 失败 / 停止;失败带 `failedStep`。
196
+ */
197
+ export async function runFlow(options: FlowRunOptions): Promise<FlowResult> {
198
+ const { flow, input, actions, signal, runId, workspace, onStep } = options;
199
+ const sleep = options.sleep ?? defaultSleep;
200
+ const now = options.now ?? (() => Date.now());
201
+ const steps: Record<string, unknown> = {};
202
+ const emit = (event: FlowStepEvent): void => {
203
+ try {
204
+ onStep(event);
205
+ } catch {
206
+ }
207
+ };
208
+ const target = targetOf(flow, { in: input, steps });
209
+ const shouldStop = (): boolean =>
210
+ flow.stopWhen !== undefined && evalPredicate(flow.stopWhen, { in: input, steps });
211
+
212
+ if (shouldStop()) return { status: 'ok', target, steps };
213
+
214
+ for (const step of flow.steps) {
215
+ if (signal.aborted) return stopped(steps, target);
216
+ const title = step.title ?? step.id;
217
+ try {
218
+ if (shouldStop()) return { status: 'ok', target, steps };
219
+
220
+ if (step.when !== undefined && !evalPredicate(step.when, { in: input, steps })) {
221
+ emit({ stepId: step.id, title, status: 'skipped' });
222
+ if (shouldStop()) return { status: 'ok', target, steps };
223
+ continue;
224
+ }
225
+
226
+ if ('flow' in step) {
227
+ emit({ stepId: step.id, title, status: 'running' });
228
+ const subInput = resolveInput(step.with, { in: input, steps });
229
+ const nested = await runFlow({
230
+ flow: step.flow,
231
+ input: subInput,
232
+ actions,
233
+ signal,
234
+ runId: `${runId}/${step.id}`,
235
+ workspace,
236
+ onStep: (event) => {
237
+ emit({
238
+ ...event,
239
+ stepId: `${step.id}.${event.stepId}`,
240
+ title: `${title} / ${event.title}`,
241
+ });
242
+ },
243
+ sleep,
244
+ now,
245
+ });
246
+ if (nested.status === 'failed') {
247
+ const failed =
248
+ nested.failedStep === undefined ? step.id : `${step.id}.${nested.failedStep}`;
249
+ throw new StepFailure(failed, nested.error ?? new Error(`子流程失败在 ${failed}`));
250
+ }
251
+ steps[step.id] = nested.steps;
252
+ if (nested.status === 'stopped') return stopped(steps, target);
253
+ emit({ stepId: step.id, title, status: 'ok' });
254
+ if (shouldStop()) return { status: 'ok', target, steps };
255
+ continue;
256
+ }
257
+
258
+ if (step.forEach !== undefined) {
259
+ const over = resolveValue(step.forEach.over, { in: input, steps });
260
+ if (!Array.isArray(over)) {
261
+ throw new Error(`步骤 ${step.id} 的 forEach.over 不是数组`);
262
+ }
263
+ const max = Math.max(1, Math.min(50, Math.trunc(step.forEach.max)));
264
+ if (over.length > max) {
265
+ throw new Error(`步骤 ${step.id} 的 forEach 有 ${over.length} 项,超过上限 ${max}`);
266
+ }
267
+ const output: unknown[] = [];
268
+ for (const [index, item] of over.entries()) {
269
+ if (signal.aborted) return stopped(steps, target);
270
+ output.push(
271
+ await runOne({
272
+ step,
273
+ title,
274
+ stepId: `${step.id}[${index}]`,
275
+ scope: { in: input, steps, item },
276
+ actions,
277
+ signal,
278
+ runId,
279
+ workspace,
280
+ sleep,
281
+ now,
282
+ emit,
283
+ }),
284
+ );
285
+ }
286
+ steps[step.id] = output;
287
+ emit({ stepId: step.id, title, status: 'ok' });
288
+ if (shouldStop()) return { status: 'ok', target, steps };
289
+ continue;
290
+ }
291
+
292
+ steps[step.id] = await runOne({
293
+ step,
294
+ title,
295
+ stepId: step.id,
296
+ scope: { in: input, steps },
297
+ actions,
298
+ signal,
299
+ runId,
300
+ workspace,
301
+ sleep,
302
+ now,
303
+ emit,
304
+ });
305
+ emit({ stepId: step.id, title, status: 'ok' });
306
+ if (shouldStop()) return { status: 'ok', target, steps };
307
+ } catch (error) {
308
+ if (signal.aborted || isAbortError(error)) return stopped(steps, target);
309
+ if (error instanceof StepFailure) {
310
+ emit({ stepId: error.stepId, title, status: 'failed', message: error.message });
311
+ return {
312
+ status: 'failed',
313
+ target,
314
+ steps,
315
+ failedStep: error.stepId,
316
+ error: error.cause ?? error,
317
+ };
318
+ }
319
+ emit({ stepId: step.id, title, status: 'failed', message: String(error) });
320
+ return { status: 'failed', target, steps, failedStep: step.id, error };
321
+ }
322
+ }
323
+
324
+ return { status: 'ok', target, steps };
325
+ }
@@ -0,0 +1,199 @@
1
+ /**
2
+ * 一条流程:按顺序跑的步骤清单;步骤里的动作按名字从注册表取。
3
+ */
4
+ export interface FlowDefinition {
5
+ readonly id: string;
6
+ readonly title: string;
7
+ readonly target?: FlowValue | undefined;
8
+ readonly stopWhen?: FlowPredicate | undefined;
9
+ readonly steps: readonly FlowStep[];
10
+ }
11
+
12
+ /**
13
+ * 重试:`attempts` 是额外次数,总共最多 6 次。
14
+ */
15
+ export interface FlowRetry {
16
+ readonly attempts: number;
17
+ readonly delayMs?: number | undefined;
18
+ }
19
+
20
+ /**
21
+ * 轮询:每次调动作,`until` 判停;`maxMs: 0` 表示不限时。
22
+ * `onTimeout: 'continue'` 把 `undefined` 写进步骤结果,交给后面的 `when` 决定。
23
+ */
24
+ export interface FlowPoll {
25
+ readonly everyMs: number;
26
+ readonly maxMs: number;
27
+ readonly until: FlowPredicate;
28
+ readonly onTimeout?: 'fail' | 'continue' | undefined;
29
+ }
30
+
31
+ /**
32
+ * 逐项展开:`over` 求成数组,每项用 `item` 作用域跑一次,最多 `max` 项。
33
+ */
34
+ export interface FlowForEach {
35
+ readonly over: FlowValue;
36
+ readonly as: string;
37
+ readonly max: number;
38
+ }
39
+
40
+ /**
41
+ * 一步:调用一个注册表里的动作,可选重试、轮询、逐项展开。
42
+ */
43
+ export interface FlowActionStep {
44
+ readonly id: string;
45
+ readonly title?: string | undefined;
46
+ readonly action: string;
47
+ readonly with?: Readonly<Record<string, FlowValue>> | undefined;
48
+ readonly when?: FlowPredicate | undefined;
49
+ readonly retry?: FlowRetry | undefined;
50
+ readonly poll?: FlowPoll | undefined;
51
+ readonly forEach?: FlowForEach | undefined;
52
+ }
53
+
54
+ /**
55
+ * 一步:跑另一条流程;`with` 求出来的对象就是子流程的 `in`,结果挂在 `steps.<id>` 下。
56
+ */
57
+ export interface FlowSubFlowStep {
58
+ readonly id: string;
59
+ readonly title?: string | undefined;
60
+ readonly flow: FlowDefinition;
61
+ readonly with?: Readonly<Record<string, FlowValue>> | undefined;
62
+ readonly when?: FlowPredicate | undefined;
63
+ }
64
+
65
+ /**
66
+ * 一步要么调动作,要么跑子流程。
67
+ */
68
+ export type FlowStep = FlowActionStep | FlowSubFlowStep;
69
+
70
+ /**
71
+ * 步骤入参里的一个值:字面量、引用,或二选一。
72
+ * 引用只从 `in` / `steps` / `item` / `value` 起头;`value` 只在 `poll.until` 里。
73
+ */
74
+ export type FlowValue =
75
+ | { readonly $ref: string }
76
+ | { readonly $lit: unknown }
77
+ | { readonly $default: readonly [FlowValue, FlowValue] };
78
+
79
+ /**
80
+ * 条件表达式:相等、包含、存在、与、非;没有算术与函数调用。
81
+ */
82
+ export type FlowPredicate =
83
+ | { readonly $eq: readonly [FlowValue, FlowValue] }
84
+ | { readonly $in: readonly [FlowValue, readonly unknown[]] }
85
+ | { readonly $exists: FlowValue }
86
+ | { readonly $and: readonly FlowPredicate[] }
87
+ | { readonly $not: FlowPredicate };
88
+
89
+ /**
90
+ * 求值现场:流程输入、已经跑完的步骤输出、当前 `forEach` 项、当前 `poll` 返回值。
91
+ */
92
+ export interface FlowScope {
93
+ readonly in: unknown;
94
+ readonly steps: Readonly<Record<string, unknown>>;
95
+ readonly item?: unknown;
96
+ readonly value?: unknown;
97
+ }
98
+
99
+ /**
100
+ * 动作执行时拿到的现场:取消信号、这次跑的 id 与工作区、当前步骤 id、一行进度、登录提示。
101
+ */
102
+ export interface FlowActionContext {
103
+ readonly signal: AbortSignal;
104
+ readonly runId: string;
105
+ readonly workspace: string;
106
+ readonly stepId: string;
107
+ log(text: string): void;
108
+ login(url: string): void;
109
+ }
110
+
111
+ /**
112
+ * 动作的可选元数据:给人看的标题、入参形状、以及“会不会改平台”。
113
+ * `writes` 只给外层审批用,不参与执行。
114
+ */
115
+ export interface FlowActionMeta {
116
+ readonly title?: string | undefined;
117
+ readonly description?: string | undefined;
118
+ readonly input?: Readonly<Record<string, unknown>> | undefined;
119
+ readonly writes?: boolean | undefined;
120
+ }
121
+
122
+ /**
123
+ * 一个动作:实现完全自由,引擎只按名字调它。
124
+ */
125
+ export type FlowAction = (input: unknown, context: FlowActionContext) => Promise<unknown>;
126
+
127
+ /**
128
+ * 注册表里的一格:名字、实现、元数据。
129
+ */
130
+ export interface RegisteredFlowAction {
131
+ readonly name: string;
132
+ readonly action: FlowAction;
133
+ readonly meta: FlowActionMeta;
134
+ }
135
+
136
+ /**
137
+ * 动作注册表:流程只能引用这里登记过的名字。
138
+ */
139
+ export interface FlowActionRegistry {
140
+ register(name: string, action: FlowAction, meta?: FlowActionMeta): void;
141
+ get(name: string): RegisteredFlowAction | undefined;
142
+ has(name: string): boolean;
143
+ list(): readonly RegisteredFlowAction[];
144
+ }
145
+
146
+ /**
147
+ * 步骤事件:开始、成功、跳过、失败、需要登录;重试时带第几次。
148
+ */
149
+ export interface FlowStepEvent {
150
+ readonly stepId: string;
151
+ readonly title: string;
152
+ readonly status: 'running' | 'ok' | 'skipped' | 'failed' | 'needs-login';
153
+ readonly attempt?: number | undefined;
154
+ readonly message?: string | undefined;
155
+ readonly loginUrl?: string | undefined;
156
+ }
157
+
158
+ /**
159
+ * 跑一条流程要的那几件:流程、输入、动作表、取消信号、身份与进度回调。
160
+ * `sleep` / `now` 可注入,测试里用假时钟。
161
+ */
162
+ export interface FlowRunOptions {
163
+ readonly flow: FlowDefinition;
164
+ readonly input: unknown;
165
+ readonly actions: FlowActionRegistry;
166
+ readonly signal: AbortSignal;
167
+ readonly runId: string;
168
+ readonly workspace: string;
169
+ readonly onStep: (event: FlowStepEvent) => void;
170
+ readonly sleep?: ((ms: number, signal: AbortSignal) => Promise<void>) | undefined;
171
+ readonly now?: (() => number) | undefined;
172
+ }
173
+
174
+ /**
175
+ * 一次跑的结果:成功、失败或停止;停止不是失败。
176
+ */
177
+ export interface FlowResult {
178
+ readonly status: 'ok' | 'failed' | 'stopped';
179
+ readonly target: string;
180
+ readonly steps: Readonly<Record<string, unknown>>;
181
+ readonly failedStep?: string | undefined;
182
+ readonly error?: unknown;
183
+ }
184
+
185
+ /**
186
+ * 一条校验问题:指到具体路径,给 agent 或人直接改。
187
+ */
188
+ export interface FlowProblem {
189
+ readonly path: string;
190
+ readonly problem: string;
191
+ }
192
+
193
+ /**
194
+ * 校验结果:`ok` 为真表示没有 problems。
195
+ */
196
+ export interface FlowCheck {
197
+ readonly ok: boolean;
198
+ readonly problems: readonly FlowProblem[];
199
+ }
@@ -0,0 +1,7 @@
1
+ import type { HostActions } from './port-types.js';
2
+ import { configSyncAction, configSyncHandler } from './config-sync.js';
3
+ import { profileCreateAction, profileCreateHandler } from './profile-create.js';
4
+
5
+ export function createHostActions(): HostActions {
6
+ return { configSyncAction, configSyncHandler, profileCreateAction, profileCreateHandler };
7
+ }
@@ -0,0 +1,26 @@
1
+ import type { EnvSupplyPort } from './port-types.js';
2
+ import { createFlowHost, type FlowHost } from './flow/kit.js';
3
+ import { sessionIdOf, sessionWorkspace } from './tool-kit.js';
4
+
5
+ /**
6
+ * 宿主 kit 的公共部分:flow、工具侧 session 读法、外部供给装法。
7
+ */
8
+ export interface HostKit {
9
+ readonly flow: FlowHost;
10
+ readonly sessionIdOf: typeof sessionIdOf;
11
+ readonly sessionWorkspace: typeof sessionWorkspace;
12
+ readonly libInstallCommand: EnvSupplyPort['libInstallCommand'];
13
+ }
14
+
15
+ /**
16
+ * 建一份宿主 kit;每个挂载点一份,没有模块级状态。
17
+ * @returns flow / sessionIdOf / sessionWorkspace / libInstallCommand。
18
+ */
19
+ export function createHostKit(parts: { readonly libInstallCommand: EnvSupplyPort['libInstallCommand'] }): HostKit {
20
+ return {
21
+ flow: createFlowHost(),
22
+ sessionIdOf,
23
+ sessionWorkspace,
24
+ libInstallCommand: parts.libInstallCommand,
25
+ };
26
+ }
package/src/host/http.ts CHANGED
@@ -7,6 +7,7 @@ import {
7
7
  type ProvisionHandler,
8
8
  type ProvisionRequest,
9
9
  } from './provision.js';
10
+ import type { RuntimeStatus } from './adapter-types.js';
10
11
  import { WorkbenchDataError } from './data-error.js';
11
12
  import { writeResultForClient } from './write-result.js';
12
13
 
@@ -251,13 +252,10 @@ export function capabilityHandler(
251
252
  };
252
253
  }
253
254
 
254
- export interface OpencliHealth {
255
- readonly command: string;
256
- readonly source: 'env' | 'isolated' | 'path';
257
- readonly isolated: string | null;
258
- }
259
-
260
- export function healthHandler(ctx: DshHostContext, opencli?: OpencliHealth): RouteHandler {
255
+ export function healthHandler(
256
+ ctx: DshHostContext,
257
+ runtimes?: Readonly<Record<string, RuntimeStatus>>,
258
+ ): RouteHandler {
261
259
  return () => {
262
260
  const profile = ctx.get<{ dir?: string }>('profileContext');
263
261
  const dir = profile?.dir;
@@ -268,7 +266,7 @@ export function healthHandler(ctx: DshHostContext, opencli?: OpencliHealth): Rou
268
266
  profileDir: dir ?? null,
269
267
  pid: process.pid,
270
268
  now: new Date().toISOString(),
271
- ...(opencli === undefined ? {} : { opencli }),
269
+ ...(runtimes === undefined ? {} : { runtimes }),
272
270
  };
273
271
  };
274
272
  }