@rigkit/engine 0.2.1 → 0.2.3
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 +1 -1
- package/src/authoring.ts +82 -50
- package/src/authoring.typecheck.ts +67 -0
- package/src/db/schema/core.ts +3 -7
- package/src/engine.test.ts +127 -103
- package/src/engine.ts +309 -475
- package/src/provider/types.ts +2 -30
- package/src/state.ts +6 -14
- package/src/types.ts +223 -109
- package/src/version.ts +1 -1
package/src/engine.ts
CHANGED
|
@@ -3,16 +3,15 @@ import { dirname, join, resolve } from "node:path";
|
|
|
3
3
|
import { pathToFileURL } from "node:url";
|
|
4
4
|
import { isRigkitConfig, isProviderDefinition, isWorkflowNode } from "./authoring.ts";
|
|
5
5
|
import { loadDotEnv } from "./env-file.ts";
|
|
6
|
-
import { hash
|
|
6
|
+
import { hash } from "./hash.ts";
|
|
7
|
+
import { RESERVED_WORKFLOW_OPERATION_IDS } from "./types.ts";
|
|
7
8
|
import type {
|
|
8
9
|
BaseProviderPlugin,
|
|
9
10
|
InteractionPresenter,
|
|
10
11
|
InteractionPresentationRequest,
|
|
11
12
|
ProviderFactory,
|
|
12
13
|
ProviderRuntimeContext,
|
|
13
|
-
SshConnection,
|
|
14
14
|
WorkflowProviderController,
|
|
15
|
-
WorkflowWorkspaceProvider,
|
|
16
15
|
} from "./provider/types.ts";
|
|
17
16
|
import {
|
|
18
17
|
createStateStore,
|
|
@@ -30,20 +29,18 @@ import type {
|
|
|
30
29
|
LocalWorkspaceRuntime,
|
|
31
30
|
OutputSchema,
|
|
32
31
|
ProviderRuntimeMap,
|
|
33
|
-
ProviderWorkspaceContext,
|
|
34
32
|
WorkflowInputFieldDefinition,
|
|
35
33
|
WorkflowDefinition,
|
|
36
34
|
WorkflowEvent,
|
|
37
35
|
WorkflowNodeDefinition,
|
|
38
36
|
WorkflowOperationDefinition,
|
|
39
|
-
WorkflowHostCapabilityRequirement,
|
|
40
|
-
WorkflowHostMethodRequirement,
|
|
41
37
|
WorkflowPlan,
|
|
42
38
|
WorkflowPlanNode,
|
|
43
39
|
WorkflowProviderMap,
|
|
44
40
|
WorkflowTaskNode,
|
|
45
41
|
WorkspaceRecord,
|
|
46
42
|
WorkspaceRuntimeRecord,
|
|
43
|
+
WorkflowWorkspaceOperationDefinition,
|
|
47
44
|
} from "./types.ts";
|
|
48
45
|
|
|
49
46
|
export type CreateDevMachineEngineOptions = {
|
|
@@ -119,8 +116,6 @@ export type EngineOperationSummary = {
|
|
|
119
116
|
title?: string;
|
|
120
117
|
description?: string;
|
|
121
118
|
createsWorkspace?: boolean;
|
|
122
|
-
requiredHostMethods?: readonly WorkflowHostMethodRequirement[];
|
|
123
|
-
requiredHostCapabilities?: readonly WorkflowHostCapabilityRequirement[];
|
|
124
119
|
inputFields: readonly WorkflowInputFieldDefinition[];
|
|
125
120
|
cli?: EngineOperationCli;
|
|
126
121
|
};
|
|
@@ -179,6 +174,13 @@ type RuntimeOperationEntry = {
|
|
|
179
174
|
readonly run: (input: { workflow?: string; input?: unknown }) => Promise<unknown>;
|
|
180
175
|
};
|
|
181
176
|
|
|
177
|
+
type RuntimeWorkspaceOperationEntry = {
|
|
178
|
+
readonly summary: EngineOperationSummary;
|
|
179
|
+
readonly run: (input: { workspace: string; workflow?: string; input?: unknown }) => Promise<unknown>;
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
let configImportCounter = 0;
|
|
183
|
+
|
|
182
184
|
export class DevMachineEngine {
|
|
183
185
|
private readonly projectDir: string;
|
|
184
186
|
private readonly configPath: string;
|
|
@@ -226,7 +228,7 @@ export class DevMachineEngine {
|
|
|
226
228
|
}
|
|
227
229
|
|
|
228
230
|
const moduleUrl = pathToFileURL(this.configPath);
|
|
229
|
-
moduleUrl.searchParams.set("t",
|
|
231
|
+
moduleUrl.searchParams.set("t", `${Date.now()}-${configImportCounter++}`);
|
|
230
232
|
const mod = await import(moduleUrl.href);
|
|
231
233
|
const roots = normalizeDefinitions(mod.default ?? mod.workflow);
|
|
232
234
|
const loaded = await Promise.all(roots.map((root) => this.resolveWorkflow(root)));
|
|
@@ -304,6 +306,10 @@ export class DevMachineEngine {
|
|
|
304
306
|
return this.listRuntimeOperationEntries().map((entry) => entry.summary);
|
|
305
307
|
}
|
|
306
308
|
|
|
309
|
+
listRuntimeWorkspaceOperations(): EngineOperationSummary[] {
|
|
310
|
+
return this.listRuntimeWorkspaceOperationEntries().map((entry) => entry.summary);
|
|
311
|
+
}
|
|
312
|
+
|
|
307
313
|
private listRuntimeOperationEntries(): RuntimeOperationEntry[] {
|
|
308
314
|
const configOperations = this.listConfigOperationEntries();
|
|
309
315
|
const configOperationIds = new Set(configOperations.map((entry) => entry.summary.id));
|
|
@@ -317,6 +323,16 @@ export class DevMachineEngine {
|
|
|
317
323
|
];
|
|
318
324
|
}
|
|
319
325
|
|
|
326
|
+
private listRuntimeWorkspaceOperationEntries(): RuntimeWorkspaceOperationEntry[] {
|
|
327
|
+
const configOperations = this.listConfigWorkspaceOperationEntries();
|
|
328
|
+
const configOperationIds = new Set(configOperations.map((entry) => entry.summary.id));
|
|
329
|
+
const coreOperations = this.listCoreWorkspaceOperationEntries();
|
|
330
|
+
return [
|
|
331
|
+
...coreOperations.filter((entry) => !configOperationIds.has(entry.summary.id)),
|
|
332
|
+
...configOperations,
|
|
333
|
+
];
|
|
334
|
+
}
|
|
335
|
+
|
|
320
336
|
private listConfigOperationEntries(): RuntimeOperationEntry[] {
|
|
321
337
|
return this.listConfigOperationSummaries().map((summary) => ({
|
|
322
338
|
summary,
|
|
@@ -339,18 +355,65 @@ export class DevMachineEngine {
|
|
|
339
355
|
source: "config" as const,
|
|
340
356
|
title: operation.title,
|
|
341
357
|
description: operation.description,
|
|
342
|
-
createsWorkspace: operation.createsWorkspace,
|
|
343
|
-
requiredHostMethods: operation.requiredHostMethods,
|
|
344
|
-
requiredHostCapabilities: operation.requiredHostCapabilities,
|
|
345
358
|
inputFields: operation.input?.fields ?? [],
|
|
346
359
|
};
|
|
347
360
|
}),
|
|
348
361
|
);
|
|
349
362
|
}
|
|
350
363
|
|
|
364
|
+
private listConfigWorkspaceOperationEntries(): RuntimeWorkspaceOperationEntry[] {
|
|
365
|
+
return this.listWorkflows().flatMap((workflow) =>
|
|
366
|
+
workflow.workspaceOperations.map((operation) => ({
|
|
367
|
+
summary: this.workspaceOperationSummary(workflow, operation),
|
|
368
|
+
run: async (input) => {
|
|
369
|
+
const workspace = this.getStateService().getWorkspace(input.workspace);
|
|
370
|
+
if (!workspace) throw new Error(`Unknown workspace ${input.workspace}`);
|
|
371
|
+
if (workspace.workflow !== workflow.name) {
|
|
372
|
+
throw new EngineOperationValidationError({
|
|
373
|
+
operation: operation.id,
|
|
374
|
+
message: `Workspace ${workspace.name} belongs to workflow ${workspace.workflow}, not ${workflow.name}`,
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
const providers = await this.createProviders(workflow);
|
|
378
|
+
return await this.runConfigWorkspaceOperation({
|
|
379
|
+
workflow,
|
|
380
|
+
providers,
|
|
381
|
+
workspace,
|
|
382
|
+
operation,
|
|
383
|
+
rawInput: input.input,
|
|
384
|
+
});
|
|
385
|
+
},
|
|
386
|
+
}))
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
private listConfigWorkspaceOperationSummaries(): EngineOperationSummary[] {
|
|
391
|
+
return this.listWorkflows().flatMap((workflow) =>
|
|
392
|
+
workflow.workspaceOperations.map((operation) => {
|
|
393
|
+
assertAllowedConfigOperationId(operation.id);
|
|
394
|
+
return this.workspaceOperationSummary(workflow, operation);
|
|
395
|
+
}),
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
private workspaceOperationSummary(
|
|
400
|
+
workflow: LoadedWorkflow,
|
|
401
|
+
operation: WorkflowWorkspaceOperationDefinition<any, any, any, any>,
|
|
402
|
+
): EngineOperationSummary {
|
|
403
|
+
return {
|
|
404
|
+
workflow: workflow.name,
|
|
405
|
+
id: operation.id,
|
|
406
|
+
source: "config" as const,
|
|
407
|
+
kind: "workspace-action" as const,
|
|
408
|
+
title: operation.title,
|
|
409
|
+
description: operation.description,
|
|
410
|
+
inputFields: operation.input?.fields ?? [],
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
|
|
351
414
|
private listCoreOperationEntries(): RuntimeOperationEntry[] {
|
|
352
415
|
const workflows = this.listWorkflows();
|
|
353
|
-
const hasWorkspaceCreator = workflows.some((workflow) => workflow.
|
|
416
|
+
const hasWorkspaceCreator = workflows.some((workflow) => workflow.workspace);
|
|
354
417
|
const workflowField = stringField({
|
|
355
418
|
name: "workflow",
|
|
356
419
|
required: false,
|
|
@@ -419,7 +482,7 @@ export class DevMachineEngine {
|
|
|
419
482
|
source: "core",
|
|
420
483
|
kind: "command",
|
|
421
484
|
title: "Create",
|
|
422
|
-
description: "Create a workspace
|
|
485
|
+
description: "Create a workspace",
|
|
423
486
|
createsWorkspace: true,
|
|
424
487
|
inputFields: [
|
|
425
488
|
workflowField,
|
|
@@ -434,7 +497,7 @@ export class DevMachineEngine {
|
|
|
434
497
|
},
|
|
435
498
|
async (input) => {
|
|
436
499
|
const parsed = parseCoreOperationInput("create", input.input);
|
|
437
|
-
return await this.
|
|
500
|
+
return await this.createWorkspace({
|
|
438
501
|
workflow: optionalStringInput("create", parsed, "workflow"),
|
|
439
502
|
name: requiredStringInput("create", parsed, "name"),
|
|
440
503
|
});
|
|
@@ -442,117 +505,40 @@ export class DevMachineEngine {
|
|
|
442
505
|
),
|
|
443
506
|
]
|
|
444
507
|
: []),
|
|
508
|
+
];
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
private listCoreWorkspaceOperationEntries(): RuntimeWorkspaceOperationEntry[] {
|
|
512
|
+
const workflows = this.listWorkflows().filter((workflow) => workflow.workspace);
|
|
513
|
+
if (workflows.length === 0) return [];
|
|
514
|
+
const coreOperation = (
|
|
515
|
+
summary: EngineOperationSummary,
|
|
516
|
+
run: RuntimeWorkspaceOperationEntry["run"],
|
|
517
|
+
): RuntimeWorkspaceOperationEntry => ({ summary, run });
|
|
518
|
+
|
|
519
|
+
return workflows.map((workflow) =>
|
|
445
520
|
coreOperation(
|
|
446
521
|
{
|
|
447
|
-
workflow:
|
|
448
|
-
id: "
|
|
449
|
-
source: "core",
|
|
450
|
-
kind: "command",
|
|
451
|
-
title: "SSH",
|
|
452
|
-
description: "Get an SSH command for a workspace or VM",
|
|
453
|
-
requiredHostMethods: [{ id: "host.command.run", modes: ["interactive"] }],
|
|
454
|
-
inputFields: [
|
|
455
|
-
workflowField,
|
|
456
|
-
stringField({ name: "workspaceOrVmId", position: 0, required: true }),
|
|
457
|
-
stringField({ name: "user", required: false }),
|
|
458
|
-
booleanField({ name: "print", required: false, defaultValue: false }),
|
|
459
|
-
],
|
|
460
|
-
cli: {
|
|
461
|
-
positionals: [
|
|
462
|
-
{ name: "workspaceOrVmId", index: 0 },
|
|
463
|
-
],
|
|
464
|
-
options: [
|
|
465
|
-
{ name: "workflow", flag: "--workflow" },
|
|
466
|
-
{ name: "user", flag: "--user" },
|
|
467
|
-
{ name: "print", flag: "--print", type: "boolean" },
|
|
468
|
-
],
|
|
469
|
-
},
|
|
470
|
-
},
|
|
471
|
-
async (input) => {
|
|
472
|
-
const parsed = parseCoreOperationInput("ssh", input.input);
|
|
473
|
-
const workspaceOrVmId = requiredStringInput("ssh", parsed, "workspaceOrVmId");
|
|
474
|
-
const terminal = await this.attachTerminal({
|
|
475
|
-
workflow: optionalStringInput("ssh", parsed, "workflow"),
|
|
476
|
-
workspaceOrVmId,
|
|
477
|
-
printOnly: true,
|
|
478
|
-
user: optionalStringInput("ssh", parsed, "user"),
|
|
479
|
-
});
|
|
480
|
-
if (optionalBooleanInput("ssh", parsed, "print", false)) return terminal;
|
|
481
|
-
const commandResult = await (this.local.command ?? runLocalCommand)({
|
|
482
|
-
argv: ["sh", "-lc", terminal.command],
|
|
483
|
-
cwd: this.projectDir,
|
|
484
|
-
mode: "interactive",
|
|
485
|
-
reason: `Open an SSH session to ${workspaceOrVmId}`,
|
|
486
|
-
presentation: {
|
|
487
|
-
visible: true,
|
|
488
|
-
label: "SSH into workspace",
|
|
489
|
-
},
|
|
490
|
-
});
|
|
491
|
-
return { ...terminal, commandResult };
|
|
492
|
-
},
|
|
493
|
-
),
|
|
494
|
-
coreOperation(
|
|
495
|
-
{
|
|
496
|
-
workflow: "",
|
|
497
|
-
id: "snapshot",
|
|
498
|
-
source: "core",
|
|
499
|
-
kind: "command",
|
|
500
|
-
title: "Snapshot",
|
|
501
|
-
description: "Capture a snapshot from a workspace VM",
|
|
502
|
-
inputFields: [
|
|
503
|
-
workflowField,
|
|
504
|
-
stringField({ name: "workspace", position: 0, required: true }),
|
|
505
|
-
stringField({ name: "label", required: false }),
|
|
506
|
-
],
|
|
507
|
-
cli: {
|
|
508
|
-
positionals: [
|
|
509
|
-
{ name: "workspace", index: 0 },
|
|
510
|
-
],
|
|
511
|
-
options: [
|
|
512
|
-
{ name: "workflow", flag: "--workflow" },
|
|
513
|
-
{ name: "label", flag: "--label" },
|
|
514
|
-
],
|
|
515
|
-
},
|
|
516
|
-
},
|
|
517
|
-
async (input) => {
|
|
518
|
-
const parsed = parseCoreOperationInput("snapshot", input.input);
|
|
519
|
-
return await this.snapshotWorkspace({
|
|
520
|
-
workflow: optionalStringInput("snapshot", parsed, "workflow"),
|
|
521
|
-
workspace: requiredStringInput("snapshot", parsed, "workspace"),
|
|
522
|
-
label: optionalStringInput("snapshot", parsed, "label"),
|
|
523
|
-
});
|
|
524
|
-
},
|
|
525
|
-
),
|
|
526
|
-
coreOperation(
|
|
527
|
-
{
|
|
528
|
-
workflow: "",
|
|
529
|
-
id: "delete",
|
|
530
|
-
aliases: ["rm"],
|
|
522
|
+
workflow: workflow.name,
|
|
523
|
+
id: "remove",
|
|
531
524
|
source: "core",
|
|
532
|
-
kind: "
|
|
533
|
-
title: "
|
|
534
|
-
description: "
|
|
535
|
-
inputFields: [
|
|
536
|
-
workflowField,
|
|
537
|
-
stringField({ name: "workspace", position: 0, required: true }),
|
|
538
|
-
],
|
|
525
|
+
kind: "workspace-action",
|
|
526
|
+
title: "Remove",
|
|
527
|
+
description: "Remove a workspace",
|
|
528
|
+
inputFields: [],
|
|
539
529
|
cli: {
|
|
540
|
-
positionals: [{ name: "workspace", index: 0 }],
|
|
541
530
|
options: [
|
|
542
|
-
{ name: "
|
|
543
|
-
{ name: "yes", flag: "--yes", aliases: ["-y"], required: true, type: "boolean", runtime: false },
|
|
531
|
+
{ name: "yes", flag: "--yes", aliases: ["-y"], type: "boolean", runtime: false },
|
|
544
532
|
],
|
|
545
533
|
},
|
|
546
534
|
},
|
|
547
|
-
async (input) =>
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
),
|
|
555
|
-
];
|
|
535
|
+
async (input) =>
|
|
536
|
+
await this.removeWorkspace({
|
|
537
|
+
workflow: input.workflow,
|
|
538
|
+
workspace: input.workspace,
|
|
539
|
+
}),
|
|
540
|
+
)
|
|
541
|
+
);
|
|
556
542
|
}
|
|
557
543
|
|
|
558
544
|
listNodeRuns(): WorkflowNodeRunRecord[] {
|
|
@@ -564,6 +550,15 @@ export class DevMachineEngine {
|
|
|
564
550
|
}
|
|
565
551
|
|
|
566
552
|
async runRuntimeOperation(input: { operation: string; workflow?: string; input?: unknown }): Promise<unknown> {
|
|
553
|
+
const workspaceTarget = parseWorkspaceOperationId(input.operation);
|
|
554
|
+
if (workspaceTarget) {
|
|
555
|
+
return await this.runWorkspaceOperation({
|
|
556
|
+
workspace: workspaceTarget.workspace,
|
|
557
|
+
operation: workspaceTarget.operation,
|
|
558
|
+
workflow: input.workflow,
|
|
559
|
+
input: input.input,
|
|
560
|
+
});
|
|
561
|
+
}
|
|
567
562
|
const operation = this.findRuntimeOperationEntry(input.operation);
|
|
568
563
|
if (!operation) throw new EngineOperationNotFoundError(input.operation);
|
|
569
564
|
return await operation.run({ workflow: input.workflow, input: input.input });
|
|
@@ -588,10 +583,44 @@ export class DevMachineEngine {
|
|
|
588
583
|
workflow: workflow.name,
|
|
589
584
|
});
|
|
590
585
|
if (result !== undefined) assertJsonValue(result, `Operation ${operation.id} result`);
|
|
591
|
-
if (operation.createsWorkspace) return this.saveOperationWorkspace(workflow, operation, input.input, result);
|
|
592
586
|
return result ?? null;
|
|
593
587
|
}
|
|
594
588
|
|
|
589
|
+
async runWorkspaceOperation(input: {
|
|
590
|
+
workspace: string;
|
|
591
|
+
operation: string;
|
|
592
|
+
workflow?: string;
|
|
593
|
+
input?: unknown;
|
|
594
|
+
}): Promise<unknown> {
|
|
595
|
+
const workspace = this.getStateService().getWorkspace(input.workspace);
|
|
596
|
+
if (!workspace) throw new Error(`Unknown workspace ${input.workspace}`);
|
|
597
|
+
const workflow = this.getWorkflow(input.workflow ?? workspace.workflow);
|
|
598
|
+
if (workspace.workflow !== workflow.name) {
|
|
599
|
+
throw new EngineOperationValidationError({
|
|
600
|
+
operation: input.operation,
|
|
601
|
+
message: `Workspace ${workspace.name} belongs to workflow ${workspace.workflow}, not ${workflow.name}`,
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
const core = this.listCoreWorkspaceOperationEntries().find((entry) =>
|
|
606
|
+
entry.summary.workflow === workflow.name && entry.summary.id === input.operation
|
|
607
|
+
);
|
|
608
|
+
if (core) {
|
|
609
|
+
return await core.run({ workspace: workspace.name, workflow: workflow.name, input: input.input });
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
const operation = workflow.workspaceOperations.find((item) => item.id === input.operation);
|
|
613
|
+
if (!operation) throw new EngineOperationNotFoundError(`${workspace.name}/${input.operation}`);
|
|
614
|
+
const providers = await this.createProviders(workflow);
|
|
615
|
+
return await this.runConfigWorkspaceOperation({
|
|
616
|
+
workflow,
|
|
617
|
+
providers,
|
|
618
|
+
workspace,
|
|
619
|
+
operation,
|
|
620
|
+
rawInput: input.input,
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
|
|
595
624
|
async plan(input: { workflow?: string; machine?: string } = {}): Promise<WorkflowPlan> {
|
|
596
625
|
const workflow = this.getWorkflow(input.workflow ?? input.machine);
|
|
597
626
|
const providers = await this.createProviders(workflow);
|
|
@@ -624,158 +653,106 @@ export class DevMachineEngine {
|
|
|
624
653
|
providers,
|
|
625
654
|
mode: "apply",
|
|
626
655
|
});
|
|
627
|
-
const workspaceSource = this.resolveWorkspaceSource(workflow, result.context, providers, { required: false });
|
|
628
656
|
|
|
629
657
|
return {
|
|
630
658
|
context: result.context,
|
|
631
|
-
snapshotId: snapshotIdOf(workspaceSource),
|
|
632
|
-
workspaceSource,
|
|
633
659
|
plan: result.plan,
|
|
634
660
|
};
|
|
635
661
|
}
|
|
636
662
|
|
|
637
663
|
async fork(input: { workflow?: string; machine?: string; name: string }): Promise<WorkspaceRecord> {
|
|
638
|
-
|
|
664
|
+
return await this.createWorkspace(input);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
async createWorkspace(input: { workflow?: string; machine?: string; name: string }): Promise<WorkspaceRecord> {
|
|
668
|
+
if (!input.name) throw new Error(`create requires a workspace name`);
|
|
669
|
+
if (this.getStateService().getWorkspace(input.name)) {
|
|
670
|
+
throw new Error(`Workspace ${input.name} already exists`);
|
|
671
|
+
}
|
|
639
672
|
|
|
640
673
|
const applied = await this.apply({ workflow: input.workflow ?? input.machine });
|
|
641
674
|
const workflow = this.getWorkflow(input.workflow ?? input.machine);
|
|
642
675
|
const providers = await this.createProviders(workflow);
|
|
643
|
-
if (workflow.
|
|
644
|
-
|
|
645
|
-
workflow,
|
|
646
|
-
providers,
|
|
647
|
-
context: applied.context,
|
|
648
|
-
name: input.name,
|
|
649
|
-
});
|
|
676
|
+
if (!workflow.workspace) {
|
|
677
|
+
throw new Error(`Workflow ${workflow.name} does not define a workspace`);
|
|
650
678
|
}
|
|
651
679
|
|
|
652
|
-
const sourceRef = this.resolveWorkspaceSource(workflow, applied.context, providers, { required: true })!;
|
|
653
|
-
const workspaceProvider = this.findWorkspaceProvider(providers, sourceRef);
|
|
654
|
-
const created = await workspaceProvider.createWorkspace(sourceRef, { name: input.name });
|
|
655
|
-
const providerId = created.providerId ?? providerIdOf(sourceRef) ?? this.providerIdForWorkspaceProvider(providers, workspaceProvider);
|
|
656
680
|
const now = new Date().toISOString();
|
|
657
|
-
|
|
658
681
|
const workspace: WorkspaceRecord = {
|
|
659
682
|
id: crypto.randomUUID(),
|
|
660
683
|
name: input.name,
|
|
661
|
-
providerId,
|
|
662
684
|
workflow: workflow.name,
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
sourceRef: created.sourceRef ?? sourceRef,
|
|
666
|
-
context: { ...applied.context },
|
|
685
|
+
workflowCtx: { ...applied.context },
|
|
686
|
+
ctx: {},
|
|
667
687
|
createdAt: now,
|
|
668
688
|
updatedAt: now,
|
|
669
|
-
metadata: created.metadata ?? {},
|
|
670
689
|
};
|
|
671
690
|
|
|
672
691
|
this.getStateService().saveWorkspace(workspace);
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
providers,
|
|
676
|
-
workspaceProvider,
|
|
677
|
-
workspace,
|
|
678
|
-
context: applied.context,
|
|
679
|
-
});
|
|
680
|
-
|
|
681
|
-
this.emit({
|
|
682
|
-
type: "workspace.ready",
|
|
683
|
-
workspaceId: input.name,
|
|
684
|
-
providerId: workspace.providerId,
|
|
685
|
-
resourceId: workspace.resourceId,
|
|
686
|
-
snapshotId: workspace.snapshotId,
|
|
687
|
-
});
|
|
688
|
-
|
|
689
|
-
return workspace;
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
async attachTerminal(input: {
|
|
693
|
-
workspaceOrVmId: string;
|
|
694
|
-
workflow?: string;
|
|
695
|
-
machine?: string;
|
|
696
|
-
printOnly?: boolean;
|
|
697
|
-
user?: string;
|
|
698
|
-
}): Promise<{ command: string }> {
|
|
699
|
-
const workspace = this.getStateService().findWorkspace(input.workspaceOrVmId);
|
|
700
|
-
const workflow = this.getWorkflow(input.workflow ?? input.machine ?? workspace?.workflow);
|
|
701
|
-
const providers = await this.createProviders(workflow);
|
|
702
|
-
const workspaceProvider = workspace
|
|
703
|
-
? this.workspaceProviderById(providers, workspace.providerId)
|
|
704
|
-
: this.singleWorkspaceProvider(providers);
|
|
705
|
-
if (workspace) {
|
|
706
|
-
await this.runWorkspaceOpenHook({
|
|
692
|
+
try {
|
|
693
|
+
await this.runWorkspaceCreate({
|
|
707
694
|
workflow,
|
|
708
695
|
providers,
|
|
709
|
-
workspaceProvider,
|
|
710
696
|
workspace,
|
|
711
|
-
context:
|
|
697
|
+
context: applied.context,
|
|
698
|
+
name: input.name,
|
|
712
699
|
});
|
|
700
|
+
} catch (error) {
|
|
701
|
+
this.getStateService().deleteWorkspace(workspace.name);
|
|
702
|
+
throw error;
|
|
713
703
|
}
|
|
714
|
-
const
|
|
704
|
+
const ready = this.getStateService().getWorkspace(input.name) ?? workspace;
|
|
715
705
|
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
stderr: "inherit",
|
|
721
|
-
});
|
|
722
|
-
await proc.exited;
|
|
723
|
-
}
|
|
706
|
+
this.emit({
|
|
707
|
+
type: "workspace.ready",
|
|
708
|
+
workspaceId: ready.name,
|
|
709
|
+
});
|
|
724
710
|
|
|
725
|
-
return
|
|
711
|
+
return ready;
|
|
726
712
|
}
|
|
727
713
|
|
|
728
714
|
async deleteWorkspace(input: { workspace: string; workflow?: string; machine?: string }): Promise<WorkspaceRecord> {
|
|
729
|
-
|
|
730
|
-
if (!workspace) throw new Error(`Unknown workspace ${input.workspace}`);
|
|
731
|
-
|
|
732
|
-
const workflow = this.getWorkflow(input.workflow ?? input.machine ?? workspace.workflow);
|
|
733
|
-
const providers = await this.createProviders(workflow);
|
|
734
|
-
const workspaceProvider = this.workspaceProviderById(providers, workspace.providerId);
|
|
735
|
-
await workspaceProvider.deleteWorkspace(workspace);
|
|
736
|
-
|
|
737
|
-
this.getStateService().deleteWorkspace(input.workspace);
|
|
738
|
-
|
|
739
|
-
return workspace;
|
|
715
|
+
return await this.removeWorkspace(input);
|
|
740
716
|
}
|
|
741
717
|
|
|
742
|
-
async
|
|
718
|
+
async removeWorkspace(input: { workspace: string; workflow?: string; machine?: string }): Promise<WorkspaceRecord> {
|
|
743
719
|
const workspace = this.getStateService().getWorkspace(input.workspace);
|
|
744
720
|
if (!workspace) throw new Error(`Unknown workspace ${input.workspace}`);
|
|
745
721
|
|
|
746
722
|
const workflow = this.getWorkflow(input.workflow ?? input.machine ?? workspace.workflow);
|
|
723
|
+
if (workspace.workflow !== workflow.name) {
|
|
724
|
+
throw new EngineOperationValidationError({
|
|
725
|
+
operation: "remove",
|
|
726
|
+
message: `Workspace ${workspace.name} belongs to workflow ${workspace.workflow}, not ${workflow.name}`,
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
if (!workflow.workspace) {
|
|
730
|
+
throw new Error(`Workflow ${workflow.name} does not define a workspace`);
|
|
731
|
+
}
|
|
747
732
|
const providers = await this.createProviders(workflow);
|
|
748
|
-
const
|
|
749
|
-
const
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
}),
|
|
764
|
-
providerFingerprint,
|
|
765
|
-
upstreamRunIds: [],
|
|
766
|
-
output: { sourceRef },
|
|
767
|
-
artifacts: collectArtifacts(sourceRef),
|
|
768
|
-
invalidated: false,
|
|
769
|
-
createdAt: now,
|
|
770
|
-
metadata: {
|
|
771
|
-
workspace: workspace.name,
|
|
772
|
-
label: input.label ?? null,
|
|
773
|
-
snapshotId: snapshot.snapshotId ?? null,
|
|
733
|
+
const metadata: JsonObject = {};
|
|
734
|
+
const runtime = await this.createTaskRuntime({
|
|
735
|
+
workflow,
|
|
736
|
+
providers,
|
|
737
|
+
nodePath: `workspace.${workspace.name}.remove`,
|
|
738
|
+
metadata,
|
|
739
|
+
});
|
|
740
|
+
const draft = cloneWorkspace(workspace);
|
|
741
|
+
const workspaceRuntime = this.createWorkspaceRuntime(draft);
|
|
742
|
+
|
|
743
|
+
await workflow.workspace.remove({
|
|
744
|
+
...runtime,
|
|
745
|
+
workflow: {
|
|
746
|
+
name: workflow.name,
|
|
747
|
+
ctx: Object.freeze({ ...workspace.workflowCtx }),
|
|
774
748
|
},
|
|
775
|
-
|
|
749
|
+
workspace: workspaceRuntime,
|
|
750
|
+
providers: runtime,
|
|
751
|
+
local: this.local,
|
|
752
|
+
});
|
|
776
753
|
|
|
777
|
-
this.getStateService().
|
|
778
|
-
return
|
|
754
|
+
this.getStateService().deleteWorkspace(input.workspace);
|
|
755
|
+
return workspace;
|
|
779
756
|
}
|
|
780
757
|
|
|
781
758
|
private async evaluate(input: {
|
|
@@ -898,10 +875,13 @@ export class DevMachineEngine {
|
|
|
898
875
|
const nodePath = [...input.prefix, input.node.name].join(".");
|
|
899
876
|
const upstreamRunIds = [...input.state.upstreamRunIds];
|
|
900
877
|
const nodeKey = hash({
|
|
878
|
+
cache: "task-v2",
|
|
901
879
|
kind: "task",
|
|
902
880
|
path: nodePath,
|
|
903
881
|
name: input.node.name,
|
|
904
882
|
version: input.node.options?.version ?? null,
|
|
883
|
+
handler: functionFingerprintFor(input.node.handler),
|
|
884
|
+
output: input.node.options?.output ?? null,
|
|
905
885
|
});
|
|
906
886
|
const planIndex = input.index.value++;
|
|
907
887
|
|
|
@@ -1129,65 +1109,87 @@ export class DevMachineEngine {
|
|
|
1129
1109
|
return Object.fromEntries(entries) as ProviderRuntimeMap<WorkflowProviderMap>;
|
|
1130
1110
|
}
|
|
1131
1111
|
|
|
1132
|
-
private async
|
|
1112
|
+
private async runWorkspaceCreate(input: {
|
|
1133
1113
|
workflow: LoadedWorkflow;
|
|
1134
1114
|
providers: ProviderControllers;
|
|
1135
|
-
workspaceProvider: WorkflowWorkspaceProvider;
|
|
1136
1115
|
workspace: WorkspaceRecord;
|
|
1137
1116
|
context: Record<string, JsonValue>;
|
|
1117
|
+
name: string;
|
|
1138
1118
|
}): Promise<void> {
|
|
1139
|
-
|
|
1119
|
+
if (!input.workflow.workspace) {
|
|
1120
|
+
throw new Error(`Workflow ${input.workflow.name} does not define a workspace`);
|
|
1121
|
+
}
|
|
1122
|
+
const draft = cloneWorkspace(input.workspace);
|
|
1123
|
+
const metadata: JsonObject = {};
|
|
1124
|
+
const providers = await this.createTaskRuntime({
|
|
1125
|
+
workflow: input.workflow,
|
|
1126
|
+
providers: input.providers,
|
|
1127
|
+
nodePath: `workspace.${input.name}.create`,
|
|
1128
|
+
metadata,
|
|
1129
|
+
});
|
|
1130
|
+
|
|
1131
|
+
try {
|
|
1132
|
+
const data = await input.workflow.workspace.create({
|
|
1133
|
+
...providers,
|
|
1134
|
+
workflow: {
|
|
1135
|
+
name: input.workflow.name,
|
|
1136
|
+
ctx: Object.freeze({ ...input.context }),
|
|
1137
|
+
},
|
|
1138
|
+
workspace: {
|
|
1139
|
+
name: input.name,
|
|
1140
|
+
},
|
|
1141
|
+
providers,
|
|
1142
|
+
local: this.local,
|
|
1143
|
+
});
|
|
1144
|
+
assertJsonValue(data, `Workflow ${input.workflow.name} workspace create result`);
|
|
1145
|
+
if (!isPlainObject(data)) {
|
|
1146
|
+
throw new Error(`Workflow ${input.workflow.name} workspace create result must be an object`);
|
|
1147
|
+
}
|
|
1148
|
+
draft.ctx = { ...data };
|
|
1149
|
+
} finally {
|
|
1150
|
+
draft.updatedAt = new Date().toISOString();
|
|
1151
|
+
this.getStateService().saveWorkspace(draft);
|
|
1152
|
+
}
|
|
1140
1153
|
}
|
|
1141
1154
|
|
|
1142
|
-
private async
|
|
1155
|
+
private async runConfigWorkspaceOperation(input: {
|
|
1143
1156
|
workflow: LoadedWorkflow;
|
|
1144
1157
|
providers: ProviderControllers;
|
|
1145
|
-
workspaceProvider: WorkflowWorkspaceProvider;
|
|
1146
1158
|
workspace: WorkspaceRecord;
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
}
|
|
1151
|
-
|
|
1152
|
-
private async runWorkspaceHook(
|
|
1153
|
-
lifecycle: "created" | "open",
|
|
1154
|
-
hook: ((context: {
|
|
1155
|
-
workspace: WorkspaceRuntimeRecord;
|
|
1156
|
-
ctx: Readonly<Record<string, JsonValue>>;
|
|
1157
|
-
providers: ProviderRuntimeMap<WorkflowProviderMap>;
|
|
1158
|
-
providerContext: ProviderWorkspaceContext;
|
|
1159
|
-
local: LocalWorkspaceRuntime;
|
|
1160
|
-
}) => Promise<void> | void) | undefined,
|
|
1161
|
-
input: {
|
|
1162
|
-
workflow: LoadedWorkflow;
|
|
1163
|
-
providers: ProviderControllers;
|
|
1164
|
-
workspaceProvider: WorkflowWorkspaceProvider;
|
|
1165
|
-
workspace: WorkspaceRecord;
|
|
1166
|
-
context: Record<string, JsonValue>;
|
|
1167
|
-
},
|
|
1168
|
-
): Promise<void> {
|
|
1169
|
-
if (!hook) return;
|
|
1170
|
-
|
|
1171
|
-
const providerContext = await input.workspaceProvider.workspaceContext?.(input.workspace) ?? {};
|
|
1172
|
-
const workspace: WorkspaceRuntimeRecord = {
|
|
1173
|
-
...input.workspace,
|
|
1174
|
-
cwd: resolveWorkspaceCwd(input.workflow, input.context),
|
|
1175
|
-
};
|
|
1159
|
+
operation: WorkflowWorkspaceOperationDefinition<any, any, any, any>;
|
|
1160
|
+
rawInput: unknown;
|
|
1161
|
+
}): Promise<unknown> {
|
|
1176
1162
|
const metadata: JsonObject = {};
|
|
1177
1163
|
const providers = await this.createTaskRuntime({
|
|
1178
1164
|
workflow: input.workflow,
|
|
1179
1165
|
providers: input.providers,
|
|
1180
|
-
nodePath: `workspace.${input.workspace.name}.${
|
|
1166
|
+
nodePath: `workspace.${input.workspace.name}.${input.operation.id}`,
|
|
1181
1167
|
metadata,
|
|
1182
1168
|
});
|
|
1183
|
-
|
|
1184
|
-
|
|
1169
|
+
const draft = cloneWorkspace(input.workspace);
|
|
1170
|
+
const workspace = this.createWorkspaceRuntime(draft);
|
|
1171
|
+
const operationInput = this.resolveOperationInput(input.workflow, input.operation, input.rawInput ?? {});
|
|
1172
|
+
|
|
1173
|
+
const result = await input.operation.run({
|
|
1174
|
+
...providers,
|
|
1175
|
+
workflow: {
|
|
1176
|
+
name: input.workflow.name,
|
|
1177
|
+
ctx: Object.freeze({ ...input.workspace.workflowCtx }),
|
|
1178
|
+
},
|
|
1179
|
+
input: Object.freeze(operationInput),
|
|
1185
1180
|
workspace,
|
|
1186
|
-
ctx: Object.freeze({ ...input.context }),
|
|
1187
1181
|
providers,
|
|
1188
|
-
providerContext: normalizeProviderWorkspaceContext(providerContext),
|
|
1189
1182
|
local: this.local,
|
|
1190
1183
|
});
|
|
1184
|
+
if (result !== undefined) assertJsonValue(result, `Workspace operation ${input.operation.id} result`);
|
|
1185
|
+
return result ?? null;
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
private createWorkspaceRuntime<Data extends object>(draft: WorkspaceRecord): WorkspaceRuntimeRecord<Data> {
|
|
1189
|
+
return Object.freeze({
|
|
1190
|
+
name: draft.name,
|
|
1191
|
+
ctx: Object.freeze({ ...draft.ctx }) as Data,
|
|
1192
|
+
}) as WorkspaceRuntimeRecord<Data>;
|
|
1191
1193
|
}
|
|
1192
1194
|
|
|
1193
1195
|
private getWorkflow(name: string | undefined): LoadedWorkflow {
|
|
@@ -1229,7 +1231,7 @@ export class DevMachineEngine {
|
|
|
1229
1231
|
|
|
1230
1232
|
private resolveOperationInput(
|
|
1231
1233
|
workflow: LoadedWorkflow,
|
|
1232
|
-
operation:
|
|
1234
|
+
operation: { id: string; input?: { fields: readonly WorkflowInputFieldDefinition[] } },
|
|
1233
1235
|
value: unknown,
|
|
1234
1236
|
): Record<string, unknown> {
|
|
1235
1237
|
const raw = isPlainObject(value) ? value : {};
|
|
@@ -1269,11 +1271,7 @@ export class DevMachineEngine {
|
|
|
1269
1271
|
message: `Workspace ${workspace.name} belongs to workflow ${workspace.workflow}, not ${workflow.name}`,
|
|
1270
1272
|
});
|
|
1271
1273
|
}
|
|
1272
|
-
resolved[field.name] =
|
|
1273
|
-
...workspace,
|
|
1274
|
-
cwd: resolveWorkspaceCwd(workflow, workspace.context),
|
|
1275
|
-
data: workspace.metadata,
|
|
1276
|
-
};
|
|
1274
|
+
resolved[field.name] = this.createWorkspaceRuntime(cloneWorkspace(workspace));
|
|
1277
1275
|
continue;
|
|
1278
1276
|
}
|
|
1279
1277
|
|
|
@@ -1313,115 +1311,6 @@ export class DevMachineEngine {
|
|
|
1313
1311
|
return resolved;
|
|
1314
1312
|
}
|
|
1315
1313
|
|
|
1316
|
-
private saveOperationWorkspace(
|
|
1317
|
-
workflow: LoadedWorkflow,
|
|
1318
|
-
operation: WorkflowOperationDefinition<any, any>,
|
|
1319
|
-
rawInput: unknown,
|
|
1320
|
-
result: unknown,
|
|
1321
|
-
): WorkspaceRecord {
|
|
1322
|
-
if (!isPlainObject(result)) {
|
|
1323
|
-
throw new Error(`Operation ${operation.id} must return an object when createsWorkspace is true`);
|
|
1324
|
-
}
|
|
1325
|
-
const data = result as Record<string, JsonValue>;
|
|
1326
|
-
const raw = isPlainObject(rawInput) ? rawInput : {};
|
|
1327
|
-
const name = typeof data.name === "string"
|
|
1328
|
-
? data.name
|
|
1329
|
-
: typeof raw.name === "string"
|
|
1330
|
-
? raw.name
|
|
1331
|
-
: undefined;
|
|
1332
|
-
if (!name) throw new Error(`Operation ${operation.id} must return or receive a workspace name`);
|
|
1333
|
-
const resourceId = typeof data.resourceId === "string"
|
|
1334
|
-
? data.resourceId
|
|
1335
|
-
: typeof data.vmId === "string"
|
|
1336
|
-
? data.vmId
|
|
1337
|
-
: name;
|
|
1338
|
-
const now = new Date().toISOString();
|
|
1339
|
-
const workspace: WorkspaceRecord = {
|
|
1340
|
-
id: crypto.randomUUID(),
|
|
1341
|
-
name,
|
|
1342
|
-
providerId: typeof data.providerId === "string" ? data.providerId : "config",
|
|
1343
|
-
workflow: workflow.name,
|
|
1344
|
-
resourceId,
|
|
1345
|
-
snapshotId: typeof data.snapshotId === "string" ? data.snapshotId : undefined,
|
|
1346
|
-
sourceRef: isJsonValue(data.sourceRef) ? data.sourceRef : data,
|
|
1347
|
-
context: {},
|
|
1348
|
-
createdAt: now,
|
|
1349
|
-
updatedAt: now,
|
|
1350
|
-
metadata: data,
|
|
1351
|
-
};
|
|
1352
|
-
this.getStateService().saveWorkspace(workspace);
|
|
1353
|
-
this.emit({
|
|
1354
|
-
type: "workspace.ready",
|
|
1355
|
-
workspaceId: workspace.name,
|
|
1356
|
-
providerId: workspace.providerId,
|
|
1357
|
-
resourceId: workspace.resourceId,
|
|
1358
|
-
snapshotId: workspace.snapshotId,
|
|
1359
|
-
});
|
|
1360
|
-
return workspace;
|
|
1361
|
-
}
|
|
1362
|
-
|
|
1363
|
-
private async createWorkspaceFromCallback(input: {
|
|
1364
|
-
workflow: LoadedWorkflow;
|
|
1365
|
-
providers: ProviderControllers;
|
|
1366
|
-
context: Record<string, JsonValue>;
|
|
1367
|
-
name: string;
|
|
1368
|
-
}): Promise<WorkspaceRecord> {
|
|
1369
|
-
if (!input.workflow.create) {
|
|
1370
|
-
throw new Error(`Workflow ${input.workflow.name} does not define a create callback`);
|
|
1371
|
-
}
|
|
1372
|
-
const metadata: JsonObject = {};
|
|
1373
|
-
const runtime = await this.createTaskRuntime({
|
|
1374
|
-
workflow: input.workflow,
|
|
1375
|
-
providers: input.providers,
|
|
1376
|
-
nodePath: `create.${input.name}`,
|
|
1377
|
-
metadata,
|
|
1378
|
-
});
|
|
1379
|
-
const result = await input.workflow.create.handler({
|
|
1380
|
-
...runtime,
|
|
1381
|
-
ctx: Object.freeze({ ...input.context }),
|
|
1382
|
-
name: input.name,
|
|
1383
|
-
providers: runtime,
|
|
1384
|
-
local: this.local,
|
|
1385
|
-
workflow: input.workflow.name,
|
|
1386
|
-
});
|
|
1387
|
-
assertJsonValue(result, `Workflow ${input.workflow.name} create result`);
|
|
1388
|
-
if (!isPlainObject(result)) {
|
|
1389
|
-
throw new Error(`Workflow ${input.workflow.name} create result must be an object`);
|
|
1390
|
-
}
|
|
1391
|
-
|
|
1392
|
-
const data = result as Record<string, JsonValue>;
|
|
1393
|
-
const name = typeof data.name === "string" ? data.name : input.name;
|
|
1394
|
-
const resourceId = typeof data.resourceId === "string"
|
|
1395
|
-
? data.resourceId
|
|
1396
|
-
: typeof data.vmId === "string"
|
|
1397
|
-
? data.vmId
|
|
1398
|
-
: name;
|
|
1399
|
-
const now = new Date().toISOString();
|
|
1400
|
-
const workspace: WorkspaceRecord = {
|
|
1401
|
-
id: crypto.randomUUID(),
|
|
1402
|
-
name,
|
|
1403
|
-
providerId: typeof data.providerId === "string" ? data.providerId : "config",
|
|
1404
|
-
workflow: input.workflow.name,
|
|
1405
|
-
resourceId,
|
|
1406
|
-
snapshotId: typeof data.snapshotId === "string" ? data.snapshotId : undefined,
|
|
1407
|
-
sourceRef: isJsonValue(data.sourceRef) ? data.sourceRef : data,
|
|
1408
|
-
context: { ...input.context },
|
|
1409
|
-
createdAt: now,
|
|
1410
|
-
updatedAt: now,
|
|
1411
|
-
metadata: data,
|
|
1412
|
-
};
|
|
1413
|
-
|
|
1414
|
-
this.getStateService().saveWorkspace(workspace);
|
|
1415
|
-
this.emit({
|
|
1416
|
-
type: "workspace.ready",
|
|
1417
|
-
workspaceId: workspace.name,
|
|
1418
|
-
providerId: workspace.providerId,
|
|
1419
|
-
resourceId: workspace.resourceId,
|
|
1420
|
-
snapshotId: workspace.snapshotId,
|
|
1421
|
-
});
|
|
1422
|
-
return workspace;
|
|
1423
|
-
}
|
|
1424
|
-
|
|
1425
1314
|
private getStateService(): StateService {
|
|
1426
1315
|
if (!this.state) {
|
|
1427
1316
|
throw new Error(`No state database loaded. Call engine.load() first.`);
|
|
@@ -1467,75 +1356,11 @@ export class DevMachineEngine {
|
|
|
1467
1356
|
providers,
|
|
1468
1357
|
root,
|
|
1469
1358
|
workspace: root.workspaceDefinition,
|
|
1470
|
-
create: root.createDefinition,
|
|
1471
1359
|
operations: root.operations ?? [],
|
|
1360
|
+
workspaceOperations: root.workspaceOperations ?? [],
|
|
1472
1361
|
};
|
|
1473
1362
|
}
|
|
1474
1363
|
|
|
1475
|
-
private resolveWorkspaceSource(
|
|
1476
|
-
workflow: LoadedWorkflow,
|
|
1477
|
-
context: Record<string, JsonValue>,
|
|
1478
|
-
providers: ProviderControllers,
|
|
1479
|
-
options: { required: boolean },
|
|
1480
|
-
): JsonValue | undefined {
|
|
1481
|
-
const source = workflow.workspace?.source?.(context);
|
|
1482
|
-
if (source !== undefined) {
|
|
1483
|
-
assertJsonValue(source, `Workflow ${workflow.name} workspace source`);
|
|
1484
|
-
return source;
|
|
1485
|
-
}
|
|
1486
|
-
|
|
1487
|
-
const candidates = collectArtifacts(context).filter((artifact) =>
|
|
1488
|
-
Object.values(providers).some((provider) => provider.workspace?.canUse(artifact)),
|
|
1489
|
-
);
|
|
1490
|
-
|
|
1491
|
-
if (candidates.length === 1) return candidates[0];
|
|
1492
|
-
if (!options.required && candidates.length === 0) return undefined;
|
|
1493
|
-
if (candidates.length === 0) {
|
|
1494
|
-
throw new Error(`Workflow ${workflow.name} did not produce a provider artifact that can be forked`);
|
|
1495
|
-
}
|
|
1496
|
-
throw new Error(`Workflow ${workflow.name} produced multiple forkable artifacts; configure workspace.source`);
|
|
1497
|
-
}
|
|
1498
|
-
|
|
1499
|
-
private findWorkspaceProvider(
|
|
1500
|
-
providers: ProviderControllers,
|
|
1501
|
-
sourceRef: JsonValue,
|
|
1502
|
-
): WorkflowWorkspaceProvider {
|
|
1503
|
-
const provider = Object.values(providers).find((controller) => controller.workspace?.canUse(sourceRef));
|
|
1504
|
-
if (!provider?.workspace) {
|
|
1505
|
-
throw new Error(`No workflow provider can create a workspace from ${stableJson(sourceRef)}`);
|
|
1506
|
-
}
|
|
1507
|
-
return provider.workspace;
|
|
1508
|
-
}
|
|
1509
|
-
|
|
1510
|
-
private workspaceProviderById(
|
|
1511
|
-
providers: ProviderControllers,
|
|
1512
|
-
providerId: string,
|
|
1513
|
-
): WorkflowWorkspaceProvider {
|
|
1514
|
-
const provider = Object.values(providers).find((controller) => controller.providerId === providerId);
|
|
1515
|
-
if (!provider?.workspace) {
|
|
1516
|
-
throw new Error(`Provider ${providerId} does not support workspaces`);
|
|
1517
|
-
}
|
|
1518
|
-
return provider.workspace;
|
|
1519
|
-
}
|
|
1520
|
-
|
|
1521
|
-
private singleWorkspaceProvider(providers: ProviderControllers): WorkflowWorkspaceProvider {
|
|
1522
|
-
const workspaceProviders = Object.values(providers).filter((provider) => provider.workspace);
|
|
1523
|
-
if (workspaceProviders.length !== 1 || !workspaceProviders[0]?.workspace) {
|
|
1524
|
-
throw new Error(`Expected exactly one workspace-capable provider`);
|
|
1525
|
-
}
|
|
1526
|
-
return workspaceProviders[0].workspace;
|
|
1527
|
-
}
|
|
1528
|
-
|
|
1529
|
-
private providerIdForWorkspaceProvider(
|
|
1530
|
-
providers: ProviderControllers,
|
|
1531
|
-
workspaceProvider: WorkflowWorkspaceProvider,
|
|
1532
|
-
): string {
|
|
1533
|
-
for (const provider of Object.values(providers)) {
|
|
1534
|
-
if (provider.workspace === workspaceProvider) return provider.providerId;
|
|
1535
|
-
}
|
|
1536
|
-
return "unknown";
|
|
1537
|
-
}
|
|
1538
|
-
|
|
1539
1364
|
private emit(event: WorkflowEvent): void {
|
|
1540
1365
|
for (const handler of this.handlers) handler(event);
|
|
1541
1366
|
}
|
|
@@ -1547,6 +1372,23 @@ export async function createDevMachineEngine(
|
|
|
1547
1372
|
return new DevMachineEngine(options);
|
|
1548
1373
|
}
|
|
1549
1374
|
|
|
1375
|
+
function cloneWorkspace(workspace: WorkspaceRecord): WorkspaceRecord {
|
|
1376
|
+
return {
|
|
1377
|
+
...workspace,
|
|
1378
|
+
workflowCtx: { ...workspace.workflowCtx },
|
|
1379
|
+
ctx: { ...workspace.ctx },
|
|
1380
|
+
};
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
function parseWorkspaceOperationId(value: string): { workspace: string; operation: string } | undefined {
|
|
1384
|
+
const slash = value.indexOf("/");
|
|
1385
|
+
if (slash <= 0 || slash === value.length - 1) return undefined;
|
|
1386
|
+
return {
|
|
1387
|
+
workspace: value.slice(0, slash),
|
|
1388
|
+
operation: value.slice(slash + 1),
|
|
1389
|
+
};
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1550
1392
|
async function resolveProviderDefinition(
|
|
1551
1393
|
definition: WorkflowDefinition<any, any>["providers"][string],
|
|
1552
1394
|
): Promise<LoadedProviderDefinition> {
|
|
@@ -1645,7 +1487,7 @@ function summarizeWorkflow(workflow: LoadedWorkflow): WorkflowSummary {
|
|
|
1645
1487
|
providers: Object.entries(workflow.providers).map(([name, provider]) => `${name}:${provider.providerId}`),
|
|
1646
1488
|
nodes: collectNodePaths(workflow.root),
|
|
1647
1489
|
operations: workflow.operations.map((operation) => operation.id),
|
|
1648
|
-
createsWorkspace: Boolean(workflow.
|
|
1490
|
+
createsWorkspace: Boolean(workflow.workspace),
|
|
1649
1491
|
workspace: workflow.workspace,
|
|
1650
1492
|
};
|
|
1651
1493
|
}
|
|
@@ -1673,18 +1515,36 @@ function collectNodePaths(root: WorkflowNodeDefinition<any, any, any>): string[]
|
|
|
1673
1515
|
|
|
1674
1516
|
function providerFingerprintFor(workflow: LoadedWorkflow): string {
|
|
1675
1517
|
return hash({
|
|
1518
|
+
cache: "provider-v2",
|
|
1676
1519
|
providers: Object.fromEntries(
|
|
1677
1520
|
Object.entries(workflow.providers).map(([name, provider]) => [
|
|
1678
1521
|
name,
|
|
1679
1522
|
{
|
|
1680
1523
|
providerId: provider.providerId,
|
|
1681
1524
|
config: provider.config,
|
|
1525
|
+
plugin: providerPluginFingerprint(provider.plugin),
|
|
1682
1526
|
},
|
|
1683
1527
|
]),
|
|
1684
1528
|
),
|
|
1685
1529
|
});
|
|
1686
1530
|
}
|
|
1687
1531
|
|
|
1532
|
+
function providerPluginFingerprint(plugin: unknown): unknown {
|
|
1533
|
+
if (!isBaseProviderPlugin(plugin)) return null;
|
|
1534
|
+
return {
|
|
1535
|
+
providerId: plugin.providerId,
|
|
1536
|
+
createProvider: functionFingerprintFor(plugin.createProvider),
|
|
1537
|
+
};
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
function functionFingerprintFor(fn: Function): { name: string; length: number; source: string } {
|
|
1541
|
+
return {
|
|
1542
|
+
name: fn.name,
|
|
1543
|
+
length: fn.length,
|
|
1544
|
+
source: Function.prototype.toString.call(fn),
|
|
1545
|
+
};
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1688
1548
|
function sequenceChildren(node: WorkflowNodeDefinition<any, any, any>): readonly WorkflowNodeDefinition<any, any, any>[] {
|
|
1689
1549
|
return (node as { children?: readonly WorkflowNodeDefinition<any, any, any>[] }).children ?? [];
|
|
1690
1550
|
}
|
|
@@ -1763,23 +1623,6 @@ function kindOf(value: unknown): string | undefined {
|
|
|
1763
1623
|
return isPlainObject(value) && typeof value.kind === "string" ? value.kind : undefined;
|
|
1764
1624
|
}
|
|
1765
1625
|
|
|
1766
|
-
function snapshotIdOf(value: unknown): string | undefined {
|
|
1767
|
-
return isPlainObject(value) && typeof value.snapshotId === "string" ? value.snapshotId : undefined;
|
|
1768
|
-
}
|
|
1769
|
-
|
|
1770
|
-
function resolveWorkspaceCwd(workflow: LoadedWorkflow, context: Record<string, JsonValue>): string | undefined {
|
|
1771
|
-
const cwd = workflow.workspace?.cwd;
|
|
1772
|
-
return typeof cwd === "function" ? cwd(context) : cwd;
|
|
1773
|
-
}
|
|
1774
|
-
|
|
1775
|
-
function normalizeProviderWorkspaceContext(value: unknown): ProviderWorkspaceContext {
|
|
1776
|
-
if (value === undefined) return {};
|
|
1777
|
-
if (!isPlainObject(value)) {
|
|
1778
|
-
throw new Error(`Provider workspace context must be an object`);
|
|
1779
|
-
}
|
|
1780
|
-
return value as ProviderWorkspaceContext;
|
|
1781
|
-
}
|
|
1782
|
-
|
|
1783
1626
|
function assertJsonValue(value: unknown, label: string): asserts value is JsonValue {
|
|
1784
1627
|
if (
|
|
1785
1628
|
value === null ||
|
|
@@ -1814,15 +1657,7 @@ function isJsonValue(value: unknown): value is JsonValue {
|
|
|
1814
1657
|
}
|
|
1815
1658
|
}
|
|
1816
1659
|
|
|
1817
|
-
const RESERVED_HOST_OPERATION_IDS = new Set(
|
|
1818
|
-
"completion",
|
|
1819
|
-
"doctor",
|
|
1820
|
-
"help",
|
|
1821
|
-
"init",
|
|
1822
|
-
"projects",
|
|
1823
|
-
"run",
|
|
1824
|
-
"version",
|
|
1825
|
-
]);
|
|
1660
|
+
const RESERVED_HOST_OPERATION_IDS = new Set<string>(RESERVED_WORKFLOW_OPERATION_IDS);
|
|
1826
1661
|
|
|
1827
1662
|
const CORE_OPERATION_INPUT_FIELDS: Record<string, readonly string[]> = {
|
|
1828
1663
|
plan: ["workflow"],
|
|
@@ -1830,7 +1665,6 @@ const CORE_OPERATION_INPUT_FIELDS: Record<string, readonly string[]> = {
|
|
|
1830
1665
|
create: ["workflow", "name"],
|
|
1831
1666
|
ssh: ["workflow", "workspaceOrVmId", "user", "print"],
|
|
1832
1667
|
snapshot: ["workflow", "workspace", "label"],
|
|
1833
|
-
delete: ["workflow", "workspace"],
|
|
1834
1668
|
};
|
|
1835
1669
|
|
|
1836
1670
|
function assertAllowedConfigOperationId(operationId: string): void {
|