@rigkit/engine 0.2.2 → 0.2.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rigkit/engine",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
package/src/authoring.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { RESERVED_WORKFLOW_OPERATION_IDS } from "./types.ts";
1
2
  import type {
2
3
  EnvResolver,
3
4
  RigkitConfigDefinition,
@@ -5,14 +6,14 @@ import type {
5
6
  OutputSchema,
6
7
  OutputSchemaValue,
7
8
  WorkflowDefinition,
8
- WorkflowCreateDefinition,
9
- WorkflowCreateHandler,
10
9
  WorkflowInputFieldDefinition,
11
10
  WorkflowInputShape,
12
11
  WorkflowOperationDefinition,
13
12
  WorkflowOperationInputBuilder,
14
13
  WorkflowOperationInputHelpers,
15
14
  WorkflowOperationOptions,
15
+ WorkflowWorkspaceOperationDefinition,
16
+ WorkflowWorkspaceOperationOptions,
16
17
  WorkflowNodeDefinition,
17
18
  WorkflowProviderDefinition,
18
19
  WorkflowProviderMap,
@@ -24,8 +25,8 @@ import type {
24
25
  WorkflowWorkspaceDefinition,
25
26
  } from "./types.ts";
26
27
 
27
- const reservedTaskContextKeys = new Set(["ctx", "runtime", "providers"]);
28
- const reservedHostOperationIds = new Set(["init", "doctor", "projects", "run", "ls", "help", "version", "completion"]);
28
+ const reservedTaskContextKeys = new Set(["ctx", "runtime", "providers", "step"]);
29
+ const reservedHostOperationIds = new Set<string>(RESERVED_WORKFLOW_OPERATION_IDS);
29
30
 
30
31
  const readEnv = (name: string, fallback?: string): string => {
31
32
  const value = process.env[name];
@@ -50,9 +51,17 @@ export function workflow<const Name extends string, const Providers extends Work
50
51
  providers: options.providers,
51
52
  sequence: <InputContext extends JsonObject = {}>(sequenceName: string) =>
52
53
  createSequence(app as unknown as WorkflowDefinition<string, Providers>, sequenceName, []),
53
- task: (taskName: string, optionsOrHandler: WorkflowTaskOptions | WorkflowTaskHandler<Providers, {}, any>, maybeHandler?: WorkflowTaskHandler<Providers, {}, any>) =>
54
+ task: (
55
+ taskName: string,
56
+ optionsOrHandler: WorkflowTaskOptions | WorkflowTaskHandler<Providers, {}, never, any>,
57
+ maybeHandler?: WorkflowTaskHandler<Providers, {}, never, any>,
58
+ ) =>
54
59
  createTask(app as unknown as WorkflowDefinition<string, Providers>, taskName, optionsOrHandler as any, maybeHandler as any),
55
- step: (taskName: string, optionsOrHandler: WorkflowTaskOptions | WorkflowTaskHandler<Providers, {}, any>, maybeHandler?: WorkflowTaskHandler<Providers, {}, any>) =>
60
+ step: (
61
+ taskName: string,
62
+ optionsOrHandler: WorkflowTaskOptions | WorkflowTaskHandler<Providers, {}, never, any>,
63
+ maybeHandler?: WorkflowTaskHandler<Providers, {}, never, any>,
64
+ ) =>
56
65
  createTask(app as unknown as WorkflowDefinition<string, Providers>, taskName, optionsOrHandler as any, maybeHandler as any),
57
66
  };
58
67
 
@@ -90,12 +99,11 @@ export function defineProvider<
90
99
  const ProviderId extends string,
91
100
  const Config extends object,
92
101
  Runtime = unknown,
93
- WorkspaceContext extends object = object,
94
102
  >(
95
103
  providerId: ProviderId,
96
- config: WorkflowProviderDefinition<ProviderId, Config, Runtime, WorkspaceContext>["config"],
104
+ config: WorkflowProviderDefinition<ProviderId, Config, Runtime>["config"],
97
105
  plugin?: unknown,
98
- ): WorkflowProviderDefinition<ProviderId, Config, Runtime, WorkspaceContext> {
106
+ ): WorkflowProviderDefinition<ProviderId, Config, Runtime> {
99
107
  return {
100
108
  kind: "rigkit.provider",
101
109
  providerId,
@@ -120,14 +128,30 @@ export function isProviderDefinition(value: unknown): value is WorkflowProviderD
120
128
  return Boolean(value && typeof value === "object" && getKind(value) === "rigkit.provider");
121
129
  }
122
130
 
123
- function createSequence<Providers extends WorkflowProviderMap, InputContext extends JsonObject, OutputContext extends JsonObject>(
131
+ function createSequence<
132
+ Providers extends WorkflowProviderMap,
133
+ InputContext extends JsonObject,
134
+ OutputContext extends JsonObject,
135
+ WorkspaceData extends object = JsonObject,
136
+ OperationIds extends string = never,
137
+ WorkspaceOperationIds extends string = never,
138
+ PreviousTaskIds extends string = never,
139
+ >(
124
140
  app: WorkflowDefinition<string, Providers>,
125
141
  name: string,
126
142
  children: readonly WorkflowNodeDefinition<Providers, any, any>[],
127
- workspace?: WorkflowWorkspaceDefinition<Providers, OutputContext>,
128
- create?: WorkflowCreateDefinition<Providers, OutputContext>,
143
+ workspace?: WorkflowWorkspaceDefinition<Providers, OutputContext, any>,
129
144
  operations: readonly WorkflowOperationDefinition<Providers, any>[] = [],
130
- ): WorkflowSequenceBuilder<Providers, InputContext, OutputContext> {
145
+ workspaceOperations: readonly WorkflowWorkspaceOperationDefinition<Providers, OutputContext, WorkspaceData, any>[] = [],
146
+ ): WorkflowSequenceBuilder<
147
+ Providers,
148
+ InputContext,
149
+ OutputContext,
150
+ WorkspaceData,
151
+ OperationIds,
152
+ WorkspaceOperationIds,
153
+ PreviousTaskIds
154
+ > {
131
155
  const node = {
132
156
  kind: "rigkit.workflow-node" as const,
133
157
  nodeKind: "sequence" as const,
@@ -135,27 +159,27 @@ function createSequence<Providers extends WorkflowProviderMap, InputContext exte
135
159
  workflow: app,
136
160
  children,
137
161
  workspaceDefinition: workspace,
138
- createDefinition: create,
139
162
  operations,
163
+ workspaceOperations,
140
164
  task: (
141
165
  taskName: string,
142
- optionsOrHandler: WorkflowTaskOptions | WorkflowTaskHandler<Providers, OutputContext, any>,
143
- maybeHandler?: WorkflowTaskHandler<Providers, OutputContext, any>,
166
+ optionsOrHandler: WorkflowTaskOptions | WorkflowTaskHandler<Providers, OutputContext, PreviousTaskIds, any>,
167
+ maybeHandler?: WorkflowTaskHandler<Providers, OutputContext, PreviousTaskIds, any>,
144
168
  ) => {
145
169
  const task = createTask(app, taskName, optionsOrHandler as any, maybeHandler as any);
146
- return createSequence(app, name, [...children, task], workspace, create, operations);
170
+ return createSequence(app, name, [...children, task], workspace, operations, workspaceOperations);
147
171
  },
148
172
  step: (
149
173
  taskName: string,
150
- optionsOrHandler: WorkflowTaskOptions | WorkflowTaskHandler<Providers, OutputContext, any>,
151
- maybeHandler?: WorkflowTaskHandler<Providers, OutputContext, any>,
174
+ optionsOrHandler: WorkflowTaskOptions | WorkflowTaskHandler<Providers, OutputContext, PreviousTaskIds, any>,
175
+ maybeHandler?: WorkflowTaskHandler<Providers, OutputContext, PreviousTaskIds, any>,
152
176
  ) => {
153
177
  const task = createTask(app, taskName, optionsOrHandler as any, maybeHandler as any);
154
- return createSequence(app, name, [...children, task], workspace, create, operations);
178
+ return createSequence(app, name, [...children, task], workspace, operations, workspaceOperations);
155
179
  },
156
180
  add: (child: WorkflowNodeDefinition<Providers, any, any>) => {
157
181
  assertSameWorkflow(app, child);
158
- return createSequence(app, name, [...children, child], workspace, create, operations);
182
+ return createSequence(app, name, [...children, child], workspace, operations, workspaceOperations);
159
183
  },
160
184
  parallel: (branches: Record<string, WorkflowNodeDefinition<Providers, any, any>>) => {
161
185
  for (const [branchName, branch] of Object.entries(branches)) {
@@ -173,17 +197,40 @@ function createSequence<Providers extends WorkflowProviderMap, InputContext exte
173
197
  workflow: app,
174
198
  branches,
175
199
  };
176
- return createSequence(app, name, [...children, parallelNode], workspace, create, operations);
200
+ return createSequence(app, name, [...children, parallelNode], workspace, operations, workspaceOperations);
201
+ },
202
+ workspace: (definition: WorkflowWorkspaceDefinition<Providers, OutputContext, any>) =>
203
+ createSequence(app, name, children, definition, operations, workspaceOperations),
204
+ operation: (id: string, options: WorkflowOperationOptions<Providers, any>) => {
205
+ const operation = createOperation(id, options);
206
+ assertUniqueOperationId(operations, operation.id, "Operation");
207
+ return createSequence(app, name, children, workspace, [...operations, operation], workspaceOperations);
208
+ },
209
+ workspaceOperation: (id: string, options: WorkflowWorkspaceOperationOptions<Providers, OutputContext, any, any>) => {
210
+ const operation = createWorkspaceOperation(id, options);
211
+ assertUniqueOperationId(workspaceOperations, operation.id, "Workspace operation");
212
+ return createSequence(app, name, children, workspace, operations, [...workspaceOperations, operation]);
177
213
  },
178
- workspace: (definition: WorkflowWorkspaceDefinition<Providers, OutputContext>) =>
179
- createSequence(app, name, children, definition, create, operations),
180
- create: (handler: WorkflowCreateHandler<Providers, OutputContext, any>) =>
181
- createSequence(app, name, children, workspace, { handler }, operations),
182
- operation: (id: string, options: WorkflowOperationOptions<Providers, any>) =>
183
- createSequence(app, name, children, workspace, create, [...operations, createOperation(id, options)]),
184
214
  };
185
215
 
186
- return node as unknown as WorkflowSequenceBuilder<Providers, InputContext, OutputContext>;
216
+ return node as unknown as WorkflowSequenceBuilder<
217
+ Providers,
218
+ InputContext,
219
+ OutputContext,
220
+ WorkspaceData,
221
+ OperationIds,
222
+ WorkspaceOperationIds,
223
+ PreviousTaskIds
224
+ >;
225
+ }
226
+
227
+ function assertUniqueOperationId(
228
+ operations: readonly { id: string }[],
229
+ id: string,
230
+ label: string,
231
+ ): void {
232
+ if (!operations.some((operation) => operation.id === id)) return;
233
+ throw new Error(`${label} id ${id} is already defined`);
187
234
  }
188
235
 
189
236
  function createOperation<Providers extends WorkflowProviderMap, Input extends object>(
@@ -199,9 +246,6 @@ function createOperation<Providers extends WorkflowProviderMap, Input extends ob
199
246
  id: normalized,
200
247
  title: options.title,
201
248
  description: options.description,
202
- createsWorkspace: options.createsWorkspace,
203
- requiredHostMethods: normalizeHostMethodRequirements(options.requiredHostMethods),
204
- requiredHostCapabilities: normalizeHostCapabilityRequirements(options.requiredHostCapabilities),
205
249
  input: typeof options.input === "function"
206
250
  ? options.input(createOperationInputHelpers())
207
251
  : options.input,
@@ -209,31 +253,30 @@ function createOperation<Providers extends WorkflowProviderMap, Input extends ob
209
253
  };
210
254
  }
211
255
 
212
- function normalizeHostMethodRequirements(
213
- methods: WorkflowOperationOptions<any, any>["requiredHostMethods"],
214
- ) {
215
- return methods?.map((method) => {
216
- const id = method.id.trim();
217
- if (!id) throw new Error(`Host method requirements must have non-empty ids`);
218
- return {
219
- id,
220
- ...(method.modes?.length ? { modes: [...method.modes] } : {}),
221
- };
222
- });
223
- }
224
-
225
- function normalizeHostCapabilityRequirements(
226
- capabilities: WorkflowOperationOptions<any, any>["requiredHostCapabilities"],
227
- ) {
228
- return capabilities?.map((capability) => {
229
- const id = capability.id.trim();
230
- if (!id) throw new Error(`Host capability requirements must have non-empty ids`);
231
- const schemaHash = capability.schemaHash?.trim();
232
- return {
233
- id,
234
- ...(schemaHash ? { schemaHash } : {}),
235
- };
236
- });
256
+ function createWorkspaceOperation<
257
+ Providers extends WorkflowProviderMap,
258
+ Context extends JsonObject,
259
+ Data extends JsonObject,
260
+ Input extends object,
261
+ >(
262
+ id: string,
263
+ options: WorkflowWorkspaceOperationOptions<Providers, Context, Data, Input>,
264
+ ): WorkflowWorkspaceOperationDefinition<Providers, Context, Data, Input> {
265
+ const normalized = id.trim();
266
+ if (!normalized) throw new Error(`Workspace operation ids must be non-empty`);
267
+ if (normalized.includes("/")) throw new Error(`Workspace operation ids cannot contain "/"`);
268
+ if (reservedHostOperationIds.has(normalized)) {
269
+ throw new Error(`Workspace 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
+ };
237
280
  }
238
281
 
239
282
  function createOperationInputHelpers(): WorkflowOperationInputHelpers {
@@ -287,15 +330,19 @@ function createOperationInputBuilder<Input extends object>(
287
330
  } as WorkflowOperationInputBuilder<Input>;
288
331
  }
289
332
 
290
- function createTask<Providers extends WorkflowProviderMap, InputContext extends JsonObject>(
333
+ function createTask<
334
+ Providers extends WorkflowProviderMap,
335
+ InputContext extends JsonObject,
336
+ PreviousTaskIds extends string = string,
337
+ >(
291
338
  app: WorkflowDefinition<string, Providers>,
292
339
  name: string,
293
- optionsOrHandler: WorkflowTaskOptions | WorkflowTaskHandler<Providers, InputContext, any>,
294
- maybeHandler?: WorkflowTaskHandler<Providers, InputContext, any>,
340
+ optionsOrHandler: WorkflowTaskOptions | WorkflowTaskHandler<Providers, InputContext, PreviousTaskIds, any>,
341
+ maybeHandler?: WorkflowTaskHandler<Providers, InputContext, PreviousTaskIds, any>,
295
342
  ): WorkflowTaskNode<Providers, InputContext, any> {
296
343
  const options = typeof optionsOrHandler === "function" ? undefined : optionsOrHandler;
297
344
  const handler = (typeof optionsOrHandler === "function" ? optionsOrHandler : maybeHandler) as
298
- | WorkflowTaskHandler<Providers, InputContext, any>
345
+ | WorkflowTaskHandler<Providers, InputContext, PreviousTaskIds, any>
299
346
  | undefined;
300
347
  if (!handler) throw new Error(`Task ${name} is missing a handler`);
301
348
 
@@ -0,0 +1,87 @@
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("duplicate-step-id")
63
+ .step("prepare" as const, async () => ({ ctx: { snapshotId: "snap-1" } }))
64
+ // @ts-expect-error duplicate task ids are rejected for literal ids
65
+ .step("prepare" as const, async ({ step }) => ({ ctx: step.ctx }));
66
+
67
+ sequence("reserved-operation-id")
68
+ // @ts-expect-error reserved operation ids are rejected for literal ids
69
+ .operation("create" as const, {
70
+ run: async () => null,
71
+ });
72
+
73
+ sequence("reserved-workspace-operation-id")
74
+ .workspace({
75
+ create: async () => ({}),
76
+ remove: async () => {},
77
+ })
78
+ // @ts-expect-error reserved workspace operation ids are rejected for literal ids
79
+ .workspaceOperation("remove" as const, {
80
+ run: async () => null,
81
+ });
82
+
83
+ sequence("slash-operation-id")
84
+ // @ts-expect-error operation ids cannot contain slashes
85
+ .operation("workspace/open" as const, {
86
+ run: async () => null,
87
+ });
@@ -6,19 +6,15 @@ export const workspaces = sqliteTable(
6
6
  {
7
7
  id: text("id").primaryKey(),
8
8
  name: text("name").notNull(),
9
- providerId: text("provider_id").notNull(),
10
9
  workflow: text("workflow").notNull(),
11
- resourceId: text("resource_id").notNull(),
12
- snapshotId: text("snapshot_id"),
13
- sourceRef: text("source_ref_json", { mode: "json" }).$type<JsonValue>().notNull(),
14
- context: text("context_json", { mode: "json" }).$type<Record<string, JsonValue>>().notNull(),
10
+ workflowCtx: text("workflow_ctx_json", { mode: "json" }).$type<Record<string, JsonValue>>().notNull(),
15
11
  createdAt: text("created_at").notNull(),
16
12
  updatedAt: text("updated_at").notNull(),
17
- metadata: text("metadata_json", { mode: "json" }).$type<Record<string, JsonValue>>().notNull(),
13
+ ctx: text("ctx_json", { mode: "json" }).$type<Record<string, JsonValue>>().notNull(),
18
14
  },
19
15
  (table) => [
20
16
  uniqueIndex("workspaces_name_idx").on(table.name),
21
- index("workspaces_provider_resource_idx").on(table.providerId, table.resourceId),
17
+ index("workspaces_workflow_idx").on(table.workflow),
22
18
  ],
23
19
  );
24
20