@rigkit/engine 0.0.0-canary-20260518T014918-c5bc0c2
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.
- package/README.md +5 -0
- package/package.json +34 -0
- package/src/authoring.ts +530 -0
- package/src/authoring.typecheck.ts +114 -0
- package/src/console-intercept.test.ts +121 -0
- package/src/console-intercept.ts +75 -0
- package/src/db/index.ts +157 -0
- package/src/db/schema/core.ts +71 -0
- package/src/db/schema/index.ts +7 -0
- package/src/engine.test.ts +1244 -0
- package/src/engine.ts +2604 -0
- package/src/env-file.ts +52 -0
- package/src/hash.ts +21 -0
- package/src/host-storage.ts +128 -0
- package/src/index.ts +46 -0
- package/src/provider/types.ts +113 -0
- package/src/state.ts +386 -0
- package/src/types.ts +873 -0
- package/src/version.ts +1 -0
package/README.md
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# @rigkit/engine
|
|
2
|
+
|
|
3
|
+
Runtime engine for Rigkit workflows.
|
|
4
|
+
|
|
5
|
+
This package loads `rig.config.ts`, evaluates workflow nodes, manages graph-based `.rigkit/state.sqlite` node-run cache, runs workspace lifecycle hooks, talks to registered workflow providers, presents provider-owned interaction URLs, and exposes APIs used by the CLI and future app.
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rigkit/engine",
|
|
3
|
+
"version": "0.0.0-canary-20260518T014918-c5bc0c2",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/freestyle-sh/rigkit.git",
|
|
8
|
+
"directory": "packages/engine"
|
|
9
|
+
},
|
|
10
|
+
"exports": {
|
|
11
|
+
".": "./src/index.ts",
|
|
12
|
+
"./package.json": "./package.json"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"src",
|
|
16
|
+
"README.md"
|
|
17
|
+
],
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"drizzle-kit": "^0.31.10",
|
|
20
|
+
"drizzle-orm": "^0.45.2"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@types/bun": "latest",
|
|
24
|
+
"typescript": "latest"
|
|
25
|
+
},
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
28
|
+
},
|
|
29
|
+
"scripts": {
|
|
30
|
+
"build": "tsc --noEmit",
|
|
31
|
+
"typecheck": "tsc --noEmit",
|
|
32
|
+
"test": "bun test"
|
|
33
|
+
}
|
|
34
|
+
}
|
package/src/authoring.ts
ADDED
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
import { RESERVED_WORKFLOW_OPERATION_IDS } from "./types.ts";
|
|
2
|
+
import type {
|
|
3
|
+
EnvResolver,
|
|
4
|
+
RigkitConfigDefinition,
|
|
5
|
+
JsonObject,
|
|
6
|
+
OutputSchema,
|
|
7
|
+
OutputSchemaValue,
|
|
8
|
+
WorkflowDefinition,
|
|
9
|
+
WorkflowInputFieldDefinition,
|
|
10
|
+
WorkflowInputShape,
|
|
11
|
+
WorkflowOperationDefinition,
|
|
12
|
+
WorkflowOperationInputBuilder,
|
|
13
|
+
WorkflowOperationInputHelpers,
|
|
14
|
+
WorkflowOperationOptions,
|
|
15
|
+
WorkflowWorkspaceOperationDefinition,
|
|
16
|
+
WorkflowWorkspaceOperationOptions,
|
|
17
|
+
WorkflowNodeDefinition,
|
|
18
|
+
WorkflowProviderDefinition,
|
|
19
|
+
WorkflowProviderMap,
|
|
20
|
+
WorkflowCacheScope,
|
|
21
|
+
WorkflowSequenceBuilder,
|
|
22
|
+
WorkflowTaskHandler,
|
|
23
|
+
WorkflowTaskNode,
|
|
24
|
+
WorkflowTaskOptions,
|
|
25
|
+
WorkflowTaskResult,
|
|
26
|
+
WorkflowWorkspaceDefinition,
|
|
27
|
+
} from "./types.ts";
|
|
28
|
+
|
|
29
|
+
const reservedTaskContextKeys = new Set(["config", "ctx", "runtime", "providers", "step"]);
|
|
30
|
+
const reservedHostOperationIds = new Set<string>(RESERVED_WORKFLOW_OPERATION_IDS);
|
|
31
|
+
|
|
32
|
+
type WorkflowNodeAuthoringOptions = {
|
|
33
|
+
cacheScope?: WorkflowCacheScope;
|
|
34
|
+
config?: JsonObject;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const readEnv = (name: string, fallback?: string): string => {
|
|
38
|
+
const value = process.env[name];
|
|
39
|
+
if (value !== undefined && value !== "") return value;
|
|
40
|
+
if (fallback !== undefined) return fallback;
|
|
41
|
+
throw new Error(`Missing required environment variable ${name}`);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const env: EnvResolver = Object.assign(readEnv, {
|
|
45
|
+
secret: readEnv,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
export function workflow<const Name extends string, const Providers extends WorkflowProviderMap>(
|
|
49
|
+
name: Name,
|
|
50
|
+
options: { providers: Providers },
|
|
51
|
+
): WorkflowDefinition<Name, Providers> {
|
|
52
|
+
validateProviders(options.providers);
|
|
53
|
+
|
|
54
|
+
const app = {
|
|
55
|
+
kind: "rigkit.workflow" as const,
|
|
56
|
+
name,
|
|
57
|
+
providers: options.providers,
|
|
58
|
+
sequence: <InputContext extends JsonObject = {}>(sequenceName: string) =>
|
|
59
|
+
createSequence(app as unknown as WorkflowDefinition<string, Providers>, sequenceName, []),
|
|
60
|
+
task: (
|
|
61
|
+
taskName: string,
|
|
62
|
+
optionsOrHandler: WorkflowTaskOptions | WorkflowTaskHandler<Providers, {}, never, any>,
|
|
63
|
+
maybeHandler?: WorkflowTaskHandler<Providers, {}, never, any>,
|
|
64
|
+
) =>
|
|
65
|
+
createTask(app as unknown as WorkflowDefinition<string, Providers>, taskName, optionsOrHandler as any, maybeHandler as any),
|
|
66
|
+
step: (
|
|
67
|
+
taskName: string,
|
|
68
|
+
optionsOrHandler: WorkflowTaskOptions | WorkflowTaskHandler<Providers, {}, never, any>,
|
|
69
|
+
maybeHandler?: WorkflowTaskHandler<Providers, {}, never, any>,
|
|
70
|
+
) =>
|
|
71
|
+
createTask(app as unknown as WorkflowDefinition<string, Providers>, taskName, optionsOrHandler as any, maybeHandler as any),
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
return app as unknown as WorkflowDefinition<Name, Providers>;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function sequence<
|
|
78
|
+
Providers extends WorkflowProviderMap = WorkflowProviderMap,
|
|
79
|
+
InputContext extends JsonObject = {},
|
|
80
|
+
>(name: string): WorkflowSequenceBuilder<Providers, InputContext, InputContext> {
|
|
81
|
+
return workflow(name, { providers: {} as Providers }).sequence<InputContext>(name);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function defineConfig<
|
|
85
|
+
const Providers extends WorkflowProviderMap,
|
|
86
|
+
const Workflows extends Record<string, WorkflowNodeDefinition<any, any, any>>,
|
|
87
|
+
>(options: {
|
|
88
|
+
providers: Providers;
|
|
89
|
+
workflows: Workflows;
|
|
90
|
+
}): RigkitConfigDefinition<Providers, Workflows> {
|
|
91
|
+
validateProviders(options.providers);
|
|
92
|
+
for (const [name, node] of Object.entries(options.workflows)) {
|
|
93
|
+
if (!isWorkflowNode(node)) {
|
|
94
|
+
throw new Error(`Workflow ${name} is not a valid Rigkit workflow node`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
kind: "rigkit.config",
|
|
99
|
+
providers: options.providers,
|
|
100
|
+
workflows: options.workflows,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function defineProvider<
|
|
105
|
+
const ProviderId extends string,
|
|
106
|
+
const Config extends object,
|
|
107
|
+
Runtime = unknown,
|
|
108
|
+
>(
|
|
109
|
+
providerId: ProviderId,
|
|
110
|
+
config: WorkflowProviderDefinition<ProviderId, Config, Runtime>["config"],
|
|
111
|
+
plugin?: unknown,
|
|
112
|
+
): WorkflowProviderDefinition<ProviderId, Config, Runtime> {
|
|
113
|
+
return {
|
|
114
|
+
kind: "rigkit.provider",
|
|
115
|
+
providerId,
|
|
116
|
+
config,
|
|
117
|
+
plugin,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function isWorkflow(value: unknown): value is WorkflowDefinition {
|
|
122
|
+
return Boolean(value && typeof value === "object" && getKind(value) === "rigkit.workflow");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function isWorkflowNode(value: unknown): value is WorkflowNodeDefinition<any, any, any> {
|
|
126
|
+
return Boolean(value && typeof value === "object" && getKind(value) === "rigkit.workflow-node");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function isRigkitConfig(value: unknown): value is RigkitConfigDefinition {
|
|
130
|
+
return Boolean(value && typeof value === "object" && getKind(value) === "rigkit.config");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function isProviderDefinition(value: unknown): value is WorkflowProviderDefinition {
|
|
134
|
+
return Boolean(value && typeof value === "object" && getKind(value) === "rigkit.provider");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function createSequence<
|
|
138
|
+
Providers extends WorkflowProviderMap,
|
|
139
|
+
InputContext extends JsonObject,
|
|
140
|
+
OutputContext extends JsonObject,
|
|
141
|
+
WorkspaceData extends object = JsonObject,
|
|
142
|
+
OperationIds extends string = never,
|
|
143
|
+
WorkspaceOperationIds extends string = never,
|
|
144
|
+
PreviousTaskIds extends string = never,
|
|
145
|
+
Config extends JsonObject = {},
|
|
146
|
+
>(
|
|
147
|
+
app: WorkflowDefinition<string, Providers>,
|
|
148
|
+
name: string,
|
|
149
|
+
children: readonly WorkflowNodeDefinition<Providers, any, any>[],
|
|
150
|
+
workspace?: WorkflowWorkspaceDefinition<Providers, OutputContext, any>,
|
|
151
|
+
operations: readonly WorkflowOperationDefinition<Providers, any>[] = [],
|
|
152
|
+
workspaceOperations: readonly WorkflowWorkspaceOperationDefinition<Providers, OutputContext, WorkspaceData, any>[] = [],
|
|
153
|
+
nodeOptions: WorkflowNodeAuthoringOptions = {},
|
|
154
|
+
): WorkflowSequenceBuilder<
|
|
155
|
+
Providers,
|
|
156
|
+
InputContext,
|
|
157
|
+
OutputContext,
|
|
158
|
+
WorkspaceData,
|
|
159
|
+
OperationIds,
|
|
160
|
+
WorkspaceOperationIds,
|
|
161
|
+
PreviousTaskIds,
|
|
162
|
+
Config
|
|
163
|
+
> {
|
|
164
|
+
const node = {
|
|
165
|
+
kind: "rigkit.workflow-node" as const,
|
|
166
|
+
nodeKind: "sequence" as const,
|
|
167
|
+
name,
|
|
168
|
+
workflow: app,
|
|
169
|
+
cacheScope: nodeOptions.cacheScope,
|
|
170
|
+
config: nodeOptions.config,
|
|
171
|
+
children,
|
|
172
|
+
workspaceDefinition: workspace,
|
|
173
|
+
operations,
|
|
174
|
+
workspaceOperations,
|
|
175
|
+
task: (
|
|
176
|
+
taskName: string,
|
|
177
|
+
optionsOrHandler: WorkflowTaskOptions | WorkflowTaskHandler<Providers, OutputContext, PreviousTaskIds, any>,
|
|
178
|
+
maybeHandler?: WorkflowTaskHandler<Providers, OutputContext, PreviousTaskIds, any>,
|
|
179
|
+
) => {
|
|
180
|
+
const task = createTask(app, taskName, optionsOrHandler as any, maybeHandler as any);
|
|
181
|
+
return createSequence(app, name, [...children, task], workspace, operations, workspaceOperations, nodeOptions);
|
|
182
|
+
},
|
|
183
|
+
step: (
|
|
184
|
+
taskName: string,
|
|
185
|
+
optionsOrHandler: WorkflowTaskOptions | WorkflowTaskHandler<Providers, OutputContext, PreviousTaskIds, any>,
|
|
186
|
+
maybeHandler?: WorkflowTaskHandler<Providers, OutputContext, PreviousTaskIds, any>,
|
|
187
|
+
) => {
|
|
188
|
+
const task = createTask(app, taskName, optionsOrHandler as any, maybeHandler as any);
|
|
189
|
+
return createSequence(app, name, [...children, task], workspace, operations, workspaceOperations, nodeOptions);
|
|
190
|
+
},
|
|
191
|
+
add: (child: WorkflowNodeDefinition<Providers, any, any>) => {
|
|
192
|
+
return createSequence(
|
|
193
|
+
app,
|
|
194
|
+
name,
|
|
195
|
+
[...children, attachWorkflowForAuthoring(app, child)],
|
|
196
|
+
workspace,
|
|
197
|
+
operations,
|
|
198
|
+
workspaceOperations,
|
|
199
|
+
nodeOptions,
|
|
200
|
+
);
|
|
201
|
+
},
|
|
202
|
+
parallel: (branches: Record<string, WorkflowNodeDefinition<Providers, any, any>>) => {
|
|
203
|
+
const attachedBranches: Record<string, WorkflowNodeDefinition<Providers, any, any>> = {};
|
|
204
|
+
for (const [branchName, branch] of Object.entries(branches)) {
|
|
205
|
+
if (!branchName) throw new Error(`Parallel branch names must be non-empty`);
|
|
206
|
+
attachedBranches[branchName] = attachWorkflowForAuthoring(app, branch);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const parallelNode = createParallel(app, "parallel", attachedBranches);
|
|
210
|
+
return createSequence(app, name, [...children, parallelNode], workspace, operations, workspaceOperations, nodeOptions);
|
|
211
|
+
},
|
|
212
|
+
workspace: (definition: WorkflowWorkspaceDefinition<Providers, OutputContext, any>) =>
|
|
213
|
+
createSequence(app, name, children, definition, operations, workspaceOperations, nodeOptions),
|
|
214
|
+
operation: (id: string, options: WorkflowOperationOptions<Providers, any>) => {
|
|
215
|
+
const operation = createOperation(id, options);
|
|
216
|
+
assertUniqueOperationId(operations, operation.id, "Operation");
|
|
217
|
+
return createSequence(app, name, children, workspace, [...operations, operation], workspaceOperations, nodeOptions);
|
|
218
|
+
},
|
|
219
|
+
workspaceOperation: (id: string, options: WorkflowWorkspaceOperationOptions<Providers, OutputContext, any, any>) => {
|
|
220
|
+
const operation = createWorkspaceOperation(id, options);
|
|
221
|
+
assertUniqueOperationId(workspaceOperations, operation.id, "Workspace operation");
|
|
222
|
+
return createSequence(app, name, children, workspace, operations, [...workspaceOperations, operation], nodeOptions);
|
|
223
|
+
},
|
|
224
|
+
global: () =>
|
|
225
|
+
createSequence(app, name, children, workspace, operations, workspaceOperations, {
|
|
226
|
+
...nodeOptions,
|
|
227
|
+
cacheScope: "global",
|
|
228
|
+
}),
|
|
229
|
+
local: () =>
|
|
230
|
+
createSequence(app, name, children, workspace, operations, workspaceOperations, {
|
|
231
|
+
...nodeOptions,
|
|
232
|
+
cacheScope: "local",
|
|
233
|
+
}),
|
|
234
|
+
configure: (config: JsonObject) =>
|
|
235
|
+
createSequence(app, name, children, workspace, operations, workspaceOperations, {
|
|
236
|
+
...nodeOptions,
|
|
237
|
+
config: mergeConfig(nodeOptions.config, config),
|
|
238
|
+
}),
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
return node as unknown as WorkflowSequenceBuilder<
|
|
242
|
+
Providers,
|
|
243
|
+
InputContext,
|
|
244
|
+
OutputContext,
|
|
245
|
+
WorkspaceData,
|
|
246
|
+
OperationIds,
|
|
247
|
+
WorkspaceOperationIds,
|
|
248
|
+
PreviousTaskIds,
|
|
249
|
+
Config
|
|
250
|
+
>;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function assertUniqueOperationId(
|
|
254
|
+
operations: readonly { id: string }[],
|
|
255
|
+
id: string,
|
|
256
|
+
label: string,
|
|
257
|
+
): void {
|
|
258
|
+
if (!operations.some((operation) => operation.id === id)) return;
|
|
259
|
+
throw new Error(`${label} id ${id} is already defined`);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function createOperation<Providers extends WorkflowProviderMap, Input extends object>(
|
|
263
|
+
id: string,
|
|
264
|
+
options: WorkflowOperationOptions<Providers, Input>,
|
|
265
|
+
): WorkflowOperationDefinition<Providers, Input> {
|
|
266
|
+
const normalized = id.trim();
|
|
267
|
+
if (!normalized) throw new Error(`Operation ids must be non-empty`);
|
|
268
|
+
if (reservedHostOperationIds.has(normalized)) {
|
|
269
|
+
throw new Error(`Operation id ${normalized} is reserved by the Rigkit host`);
|
|
270
|
+
}
|
|
271
|
+
return {
|
|
272
|
+
id: normalized,
|
|
273
|
+
title: options.title,
|
|
274
|
+
description: options.description,
|
|
275
|
+
input: typeof options.input === "function"
|
|
276
|
+
? options.input(createOperationInputHelpers())
|
|
277
|
+
: options.input,
|
|
278
|
+
run: options.run,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function createWorkspaceOperation<
|
|
283
|
+
Providers extends WorkflowProviderMap,
|
|
284
|
+
Context extends JsonObject,
|
|
285
|
+
Data extends JsonObject,
|
|
286
|
+
Input extends object,
|
|
287
|
+
>(
|
|
288
|
+
id: string,
|
|
289
|
+
options: WorkflowWorkspaceOperationOptions<Providers, Context, Data, Input>,
|
|
290
|
+
): WorkflowWorkspaceOperationDefinition<Providers, Context, Data, Input> {
|
|
291
|
+
const normalized = id.trim();
|
|
292
|
+
if (!normalized) throw new Error(`Workspace operation ids must be non-empty`);
|
|
293
|
+
if (normalized.includes("/")) throw new Error(`Workspace operation ids cannot contain "/"`);
|
|
294
|
+
if (reservedHostOperationIds.has(normalized)) {
|
|
295
|
+
throw new Error(`Workspace operation id ${normalized} is reserved by the Rigkit host`);
|
|
296
|
+
}
|
|
297
|
+
return {
|
|
298
|
+
id: normalized,
|
|
299
|
+
title: options.title,
|
|
300
|
+
description: options.description,
|
|
301
|
+
input: typeof options.input === "function"
|
|
302
|
+
? options.input(createOperationInputHelpers())
|
|
303
|
+
: options.input,
|
|
304
|
+
run: options.run,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function createOperationInputHelpers(): WorkflowOperationInputHelpers {
|
|
309
|
+
return {
|
|
310
|
+
workspaceInput: (options) => createOperationInputBuilder([{
|
|
311
|
+
kind: "workspace",
|
|
312
|
+
name: options.name,
|
|
313
|
+
description: options.description,
|
|
314
|
+
position: options.position,
|
|
315
|
+
required: options.required ?? true,
|
|
316
|
+
}]),
|
|
317
|
+
string: (options) => ({
|
|
318
|
+
kind: "string",
|
|
319
|
+
name: options.name ?? "",
|
|
320
|
+
description: options.description,
|
|
321
|
+
position: options.position,
|
|
322
|
+
required: options.required ?? options.defaultValue === undefined,
|
|
323
|
+
defaultValue: options.defaultValue,
|
|
324
|
+
}),
|
|
325
|
+
boolean: (options) => ({
|
|
326
|
+
kind: "boolean",
|
|
327
|
+
name: options.name ?? "",
|
|
328
|
+
description: options.description,
|
|
329
|
+
position: options.position,
|
|
330
|
+
required: options.required ?? false,
|
|
331
|
+
defaultValue: options.defaultValue,
|
|
332
|
+
}),
|
|
333
|
+
number: (options) => ({
|
|
334
|
+
kind: "number",
|
|
335
|
+
name: options.name ?? "",
|
|
336
|
+
description: options.description,
|
|
337
|
+
position: options.position,
|
|
338
|
+
required: options.required ?? options.defaultValue === undefined,
|
|
339
|
+
defaultValue: options.defaultValue,
|
|
340
|
+
}),
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function createOperationInputBuilder<Input extends object>(
|
|
345
|
+
fields: readonly WorkflowInputFieldDefinition[],
|
|
346
|
+
): WorkflowOperationInputBuilder<Input> {
|
|
347
|
+
return {
|
|
348
|
+
fields,
|
|
349
|
+
extend(shape: WorkflowInputShape) {
|
|
350
|
+
const nextFields = Object.entries(shape).map(([name, field]) => ({
|
|
351
|
+
...field,
|
|
352
|
+
name: field.name || name,
|
|
353
|
+
}));
|
|
354
|
+
return createOperationInputBuilder([...fields, ...nextFields]);
|
|
355
|
+
},
|
|
356
|
+
} as WorkflowOperationInputBuilder<Input>;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function createTask<
|
|
360
|
+
Providers extends WorkflowProviderMap,
|
|
361
|
+
InputContext extends JsonObject,
|
|
362
|
+
PreviousTaskIds extends string = string,
|
|
363
|
+
>(
|
|
364
|
+
app: WorkflowDefinition<string, Providers>,
|
|
365
|
+
name: string,
|
|
366
|
+
optionsOrHandler: WorkflowTaskOptions | WorkflowTaskHandler<Providers, InputContext, PreviousTaskIds, any>,
|
|
367
|
+
maybeHandler?: WorkflowTaskHandler<Providers, InputContext, PreviousTaskIds, any>,
|
|
368
|
+
nodeOptions: WorkflowNodeAuthoringOptions = {},
|
|
369
|
+
): WorkflowTaskNode<Providers, InputContext, any> {
|
|
370
|
+
const options = typeof optionsOrHandler === "function" ? undefined : optionsOrHandler;
|
|
371
|
+
const handler = (typeof optionsOrHandler === "function" ? optionsOrHandler : maybeHandler) as
|
|
372
|
+
| WorkflowTaskHandler<Providers, InputContext, PreviousTaskIds, any>
|
|
373
|
+
| undefined;
|
|
374
|
+
if (!handler) throw new Error(`Task ${name} is missing a handler`);
|
|
375
|
+
|
|
376
|
+
return {
|
|
377
|
+
kind: "rigkit.workflow-node",
|
|
378
|
+
nodeKind: "task",
|
|
379
|
+
name,
|
|
380
|
+
workflow: app,
|
|
381
|
+
cacheScope: nodeOptions.cacheScope,
|
|
382
|
+
config: nodeOptions.config,
|
|
383
|
+
options,
|
|
384
|
+
handler,
|
|
385
|
+
global: (() => createTask(app, name, options ?? handler, options ? handler : undefined, {
|
|
386
|
+
...nodeOptions,
|
|
387
|
+
cacheScope: "global",
|
|
388
|
+
})) as WorkflowTaskNode<Providers, InputContext, any>["global"],
|
|
389
|
+
local: (() => createTask(app, name, options ?? handler, options ? handler : undefined, {
|
|
390
|
+
...nodeOptions,
|
|
391
|
+
cacheScope: "local",
|
|
392
|
+
})) as WorkflowTaskNode<Providers, InputContext, any>["local"],
|
|
393
|
+
configure: ((config: JsonObject) => createTask(app, name, options ?? handler, options ? handler : undefined, {
|
|
394
|
+
...nodeOptions,
|
|
395
|
+
config: mergeConfig(nodeOptions.config, config),
|
|
396
|
+
})) as WorkflowTaskNode<Providers, InputContext, any>["configure"],
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function createParallel<Providers extends WorkflowProviderMap, InputContext extends JsonObject>(
|
|
401
|
+
app: WorkflowDefinition<string, Providers>,
|
|
402
|
+
name: string,
|
|
403
|
+
branches: Record<string, WorkflowNodeDefinition<Providers, any, any>>,
|
|
404
|
+
nodeOptions: WorkflowNodeAuthoringOptions = {},
|
|
405
|
+
): WorkflowNodeDefinition<Providers, InputContext, any> & {
|
|
406
|
+
nodeKind: "parallel";
|
|
407
|
+
branches: Record<string, WorkflowNodeDefinition<Providers, any, any>>;
|
|
408
|
+
} {
|
|
409
|
+
const node = {
|
|
410
|
+
kind: "rigkit.workflow-node" as const,
|
|
411
|
+
nodeKind: "parallel" as const,
|
|
412
|
+
name,
|
|
413
|
+
workflow: app,
|
|
414
|
+
cacheScope: nodeOptions.cacheScope,
|
|
415
|
+
config: nodeOptions.config,
|
|
416
|
+
branches,
|
|
417
|
+
global: () => createParallel(app, name, branches, {
|
|
418
|
+
...nodeOptions,
|
|
419
|
+
cacheScope: "global",
|
|
420
|
+
}),
|
|
421
|
+
local: () => createParallel(app, name, branches, {
|
|
422
|
+
...nodeOptions,
|
|
423
|
+
cacheScope: "local",
|
|
424
|
+
}),
|
|
425
|
+
configure: (config: JsonObject) => createParallel(app, name, branches, {
|
|
426
|
+
...nodeOptions,
|
|
427
|
+
config: mergeConfig(nodeOptions.config, config),
|
|
428
|
+
}),
|
|
429
|
+
};
|
|
430
|
+
return node as unknown as WorkflowNodeDefinition<Providers, InputContext, any> & {
|
|
431
|
+
nodeKind: "parallel";
|
|
432
|
+
branches: Record<string, WorkflowNodeDefinition<Providers, any, any>>;
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function mergeConfig(previous: JsonObject | undefined, next: JsonObject): JsonObject {
|
|
437
|
+
assertJsonObject(next, "configure input");
|
|
438
|
+
return previous ? { ...previous, ...next } : { ...next };
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function assertJsonObject(value: JsonObject, label: string): void {
|
|
442
|
+
try {
|
|
443
|
+
JSON.stringify(value);
|
|
444
|
+
} catch (cause) {
|
|
445
|
+
throw new Error(`${label} must be JSON-serializable`, { cause });
|
|
446
|
+
}
|
|
447
|
+
assertJsonValue(value, label);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function assertJsonValue(value: unknown, label: string): void {
|
|
451
|
+
if (
|
|
452
|
+
value === null ||
|
|
453
|
+
typeof value === "string" ||
|
|
454
|
+
typeof value === "boolean" ||
|
|
455
|
+
typeof value === "number"
|
|
456
|
+
) {
|
|
457
|
+
if (typeof value === "number" && !Number.isFinite(value)) {
|
|
458
|
+
throw new Error(`${label} must be JSON-serializable`);
|
|
459
|
+
}
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
if (Array.isArray(value)) {
|
|
463
|
+
value.forEach((item, index) => assertJsonValue(item, `${label}[${index}]`));
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
if (value && typeof value === "object") {
|
|
467
|
+
for (const [key, item] of Object.entries(value)) {
|
|
468
|
+
assertJsonValue(item, `${label}.${key}`);
|
|
469
|
+
}
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
throw new Error(`${label} must be JSON-serializable`);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function validateProviders(providers: WorkflowProviderMap): void {
|
|
476
|
+
for (const [name, provider] of Object.entries(providers)) {
|
|
477
|
+
if (reservedTaskContextKeys.has(name)) {
|
|
478
|
+
throw new Error(`Provider name ${name} is reserved by the task context`);
|
|
479
|
+
}
|
|
480
|
+
if (!isProviderDefinition(provider)) {
|
|
481
|
+
throw new Error(`Provider ${name} is not a valid Rigkit provider`);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function attachWorkflowForAuthoring<Providers extends WorkflowProviderMap>(
|
|
487
|
+
app: WorkflowDefinition<string, Providers>,
|
|
488
|
+
node: WorkflowNodeDefinition<Providers, any, any>,
|
|
489
|
+
): WorkflowNodeDefinition<Providers, any, any> {
|
|
490
|
+
if (node.workflow === app) return node;
|
|
491
|
+
if (node.nodeKind === "parallel") {
|
|
492
|
+
return {
|
|
493
|
+
...node,
|
|
494
|
+
workflow: app,
|
|
495
|
+
branches: Object.fromEntries(
|
|
496
|
+
Object.entries(parallelBranchesForAuthoring(node)).map(([name, branch]) => [
|
|
497
|
+
name,
|
|
498
|
+
attachWorkflowForAuthoring(app, branch),
|
|
499
|
+
]),
|
|
500
|
+
),
|
|
501
|
+
} as WorkflowNodeDefinition<Providers, any, any>;
|
|
502
|
+
}
|
|
503
|
+
if (node.nodeKind === "sequence") {
|
|
504
|
+
return {
|
|
505
|
+
...node,
|
|
506
|
+
workflow: app,
|
|
507
|
+
children: sequenceChildrenForAuthoring(node).map((child) => attachWorkflowForAuthoring(app, child)),
|
|
508
|
+
} as WorkflowNodeDefinition<Providers, any, any>;
|
|
509
|
+
}
|
|
510
|
+
return {
|
|
511
|
+
...node,
|
|
512
|
+
workflow: app,
|
|
513
|
+
} as WorkflowNodeDefinition<Providers, any, any>;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function sequenceChildrenForAuthoring(
|
|
517
|
+
node: WorkflowNodeDefinition<any, any, any>,
|
|
518
|
+
): readonly WorkflowNodeDefinition<any, any, any>[] {
|
|
519
|
+
return (node as { children?: readonly WorkflowNodeDefinition<any, any, any>[] }).children ?? [];
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function parallelBranchesForAuthoring(
|
|
523
|
+
node: WorkflowNodeDefinition<any, any, any>,
|
|
524
|
+
): Record<string, WorkflowNodeDefinition<any, any, any>> {
|
|
525
|
+
return (node as { branches?: Record<string, WorkflowNodeDefinition<any, any, any>> }).branches ?? {};
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function getKind(value: object): unknown {
|
|
529
|
+
return (value as { kind?: unknown }).kind;
|
|
530
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { sequence } from "./authoring.ts";
|
|
2
|
+
|
|
3
|
+
sequence("normal-operation-ids")
|
|
4
|
+
.operation("open" as const, {
|
|
5
|
+
run: async () => null,
|
|
6
|
+
})
|
|
7
|
+
// @ts-expect-error duplicate operation ids are rejected for literal ids
|
|
8
|
+
.operation("open" as const, {
|
|
9
|
+
run: async () => null,
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
sequence("workspace-operation-ids")
|
|
13
|
+
.step("prepare", async () => ({ ctx: { snapshotId: "snap-1" } }))
|
|
14
|
+
.workspace({
|
|
15
|
+
create: async ({ workflow, workspace }) => {
|
|
16
|
+
const snapshotId: string = workflow.ctx.snapshotId;
|
|
17
|
+
const name: string = workspace.name;
|
|
18
|
+
void snapshotId;
|
|
19
|
+
void name;
|
|
20
|
+
return { vmId: "vm-1", test: "ok" };
|
|
21
|
+
},
|
|
22
|
+
remove: async ({ workspace }) => {
|
|
23
|
+
const vmId: string = workspace.ctx.vmId;
|
|
24
|
+
const test: string = workspace.ctx.test;
|
|
25
|
+
void vmId;
|
|
26
|
+
void test;
|
|
27
|
+
// @ts-expect-error workspace context is read-only
|
|
28
|
+
workspace.ctx.vmId = "next";
|
|
29
|
+
// @ts-expect-error provider resources are not part of the workspace authoring API
|
|
30
|
+
workspace.resources;
|
|
31
|
+
},
|
|
32
|
+
})
|
|
33
|
+
.workspaceOperation("open-cmux" as const, {
|
|
34
|
+
run: async ({ workspace }) => {
|
|
35
|
+
const vmId: string = workspace.ctx.vmId;
|
|
36
|
+
void vmId;
|
|
37
|
+
// @ts-expect-error missing data properties are rejected
|
|
38
|
+
workspace.ctx.missing;
|
|
39
|
+
return null;
|
|
40
|
+
},
|
|
41
|
+
})
|
|
42
|
+
// @ts-expect-error duplicate workspace operation ids are rejected for literal ids
|
|
43
|
+
.workspaceOperation("open-cmux" as const, {
|
|
44
|
+
run: async () => null,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
sequence("typed-step-invalidation")
|
|
48
|
+
.step("github-auth", async () => ({ ctx: { token: "ok" } }))
|
|
49
|
+
.step("check-auth", async ({ step }) => {
|
|
50
|
+
const token: string = step.ctx.token;
|
|
51
|
+
void token;
|
|
52
|
+
return step.invalidate("github-auth");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
sequence("typed-step-invalidation-targets")
|
|
56
|
+
.step("github-auth", async () => ({ ctx: { token: "ok" } }))
|
|
57
|
+
.step("check-auth", async ({ step }) => {
|
|
58
|
+
// @ts-expect-error invalidation target must be a previous task id
|
|
59
|
+
return step.invalidate("missing-auth");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
sequence("typed-config")
|
|
63
|
+
.configure({
|
|
64
|
+
nodeMajor: 22,
|
|
65
|
+
tools: {
|
|
66
|
+
codex: true,
|
|
67
|
+
claude: false,
|
|
68
|
+
},
|
|
69
|
+
})
|
|
70
|
+
.step("read-config", async ({ config }) => {
|
|
71
|
+
const nodeMajor: number = config.nodeMajor;
|
|
72
|
+
const codex: boolean = config.tools.codex;
|
|
73
|
+
void nodeMajor;
|
|
74
|
+
void codex;
|
|
75
|
+
// @ts-expect-error missing config keys are rejected
|
|
76
|
+
config.missing;
|
|
77
|
+
return { ctx: { ready: true } };
|
|
78
|
+
})
|
|
79
|
+
.configure({
|
|
80
|
+
tools: {
|
|
81
|
+
claude: true,
|
|
82
|
+
},
|
|
83
|
+
})
|
|
84
|
+
.step("merged-config", async ({ config }) => {
|
|
85
|
+
const claude: boolean = config.tools.claude;
|
|
86
|
+
void claude;
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
sequence("duplicate-step-id")
|
|
90
|
+
.step("prepare" as const, async () => ({ ctx: { snapshotId: "snap-1" } }))
|
|
91
|
+
// @ts-expect-error duplicate task ids are rejected for literal ids
|
|
92
|
+
.step("prepare" as const, async ({ step }) => ({ ctx: step.ctx }));
|
|
93
|
+
|
|
94
|
+
sequence("reserved-operation-id")
|
|
95
|
+
// @ts-expect-error reserved operation ids are rejected for literal ids
|
|
96
|
+
.operation("create" as const, {
|
|
97
|
+
run: async () => null,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
sequence("reserved-workspace-operation-id")
|
|
101
|
+
.workspace({
|
|
102
|
+
create: async () => ({}),
|
|
103
|
+
remove: async () => {},
|
|
104
|
+
})
|
|
105
|
+
// @ts-expect-error reserved workspace operation ids are rejected for literal ids
|
|
106
|
+
.workspaceOperation("remove" as const, {
|
|
107
|
+
run: async () => null,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
sequence("slash-operation-id")
|
|
111
|
+
// @ts-expect-error operation ids cannot contain slashes
|
|
112
|
+
.operation("workspace/open" as const, {
|
|
113
|
+
run: async () => null,
|
|
114
|
+
});
|