@rigkit/engine 0.2.2 → 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 +77 -103
- package/src/engine.ts +285 -474
- 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,11 @@ 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
182
|
let configImportCounter = 0;
|
|
183
183
|
|
|
184
184
|
export class DevMachineEngine {
|
|
@@ -306,6 +306,10 @@ export class DevMachineEngine {
|
|
|
306
306
|
return this.listRuntimeOperationEntries().map((entry) => entry.summary);
|
|
307
307
|
}
|
|
308
308
|
|
|
309
|
+
listRuntimeWorkspaceOperations(): EngineOperationSummary[] {
|
|
310
|
+
return this.listRuntimeWorkspaceOperationEntries().map((entry) => entry.summary);
|
|
311
|
+
}
|
|
312
|
+
|
|
309
313
|
private listRuntimeOperationEntries(): RuntimeOperationEntry[] {
|
|
310
314
|
const configOperations = this.listConfigOperationEntries();
|
|
311
315
|
const configOperationIds = new Set(configOperations.map((entry) => entry.summary.id));
|
|
@@ -319,6 +323,16 @@ export class DevMachineEngine {
|
|
|
319
323
|
];
|
|
320
324
|
}
|
|
321
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
|
+
|
|
322
336
|
private listConfigOperationEntries(): RuntimeOperationEntry[] {
|
|
323
337
|
return this.listConfigOperationSummaries().map((summary) => ({
|
|
324
338
|
summary,
|
|
@@ -341,18 +355,65 @@ export class DevMachineEngine {
|
|
|
341
355
|
source: "config" as const,
|
|
342
356
|
title: operation.title,
|
|
343
357
|
description: operation.description,
|
|
344
|
-
createsWorkspace: operation.createsWorkspace,
|
|
345
|
-
requiredHostMethods: operation.requiredHostMethods,
|
|
346
|
-
requiredHostCapabilities: operation.requiredHostCapabilities,
|
|
347
358
|
inputFields: operation.input?.fields ?? [],
|
|
348
359
|
};
|
|
349
360
|
}),
|
|
350
361
|
);
|
|
351
362
|
}
|
|
352
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
|
+
|
|
353
414
|
private listCoreOperationEntries(): RuntimeOperationEntry[] {
|
|
354
415
|
const workflows = this.listWorkflows();
|
|
355
|
-
const hasWorkspaceCreator = workflows.some((workflow) => workflow.
|
|
416
|
+
const hasWorkspaceCreator = workflows.some((workflow) => workflow.workspace);
|
|
356
417
|
const workflowField = stringField({
|
|
357
418
|
name: "workflow",
|
|
358
419
|
required: false,
|
|
@@ -421,7 +482,7 @@ export class DevMachineEngine {
|
|
|
421
482
|
source: "core",
|
|
422
483
|
kind: "command",
|
|
423
484
|
title: "Create",
|
|
424
|
-
description: "Create a workspace
|
|
485
|
+
description: "Create a workspace",
|
|
425
486
|
createsWorkspace: true,
|
|
426
487
|
inputFields: [
|
|
427
488
|
workflowField,
|
|
@@ -436,7 +497,7 @@ export class DevMachineEngine {
|
|
|
436
497
|
},
|
|
437
498
|
async (input) => {
|
|
438
499
|
const parsed = parseCoreOperationInput("create", input.input);
|
|
439
|
-
return await this.
|
|
500
|
+
return await this.createWorkspace({
|
|
440
501
|
workflow: optionalStringInput("create", parsed, "workflow"),
|
|
441
502
|
name: requiredStringInput("create", parsed, "name"),
|
|
442
503
|
});
|
|
@@ -444,117 +505,40 @@ export class DevMachineEngine {
|
|
|
444
505
|
),
|
|
445
506
|
]
|
|
446
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) =>
|
|
447
520
|
coreOperation(
|
|
448
521
|
{
|
|
449
|
-
workflow:
|
|
450
|
-
id: "
|
|
451
|
-
source: "core",
|
|
452
|
-
kind: "command",
|
|
453
|
-
title: "SSH",
|
|
454
|
-
description: "Get an SSH command for a workspace or VM",
|
|
455
|
-
requiredHostMethods: [{ id: "host.command.run", modes: ["interactive"] }],
|
|
456
|
-
inputFields: [
|
|
457
|
-
workflowField,
|
|
458
|
-
stringField({ name: "workspaceOrVmId", position: 0, required: true }),
|
|
459
|
-
stringField({ name: "user", required: false }),
|
|
460
|
-
booleanField({ name: "print", required: false, defaultValue: false }),
|
|
461
|
-
],
|
|
462
|
-
cli: {
|
|
463
|
-
positionals: [
|
|
464
|
-
{ name: "workspaceOrVmId", index: 0 },
|
|
465
|
-
],
|
|
466
|
-
options: [
|
|
467
|
-
{ name: "workflow", flag: "--workflow" },
|
|
468
|
-
{ name: "user", flag: "--user" },
|
|
469
|
-
{ name: "print", flag: "--print", type: "boolean" },
|
|
470
|
-
],
|
|
471
|
-
},
|
|
472
|
-
},
|
|
473
|
-
async (input) => {
|
|
474
|
-
const parsed = parseCoreOperationInput("ssh", input.input);
|
|
475
|
-
const workspaceOrVmId = requiredStringInput("ssh", parsed, "workspaceOrVmId");
|
|
476
|
-
const terminal = await this.attachTerminal({
|
|
477
|
-
workflow: optionalStringInput("ssh", parsed, "workflow"),
|
|
478
|
-
workspaceOrVmId,
|
|
479
|
-
printOnly: true,
|
|
480
|
-
user: optionalStringInput("ssh", parsed, "user"),
|
|
481
|
-
});
|
|
482
|
-
if (optionalBooleanInput("ssh", parsed, "print", false)) return terminal;
|
|
483
|
-
const commandResult = await (this.local.command ?? runLocalCommand)({
|
|
484
|
-
argv: ["sh", "-lc", terminal.command],
|
|
485
|
-
cwd: this.projectDir,
|
|
486
|
-
mode: "interactive",
|
|
487
|
-
reason: `Open an SSH session to ${workspaceOrVmId}`,
|
|
488
|
-
presentation: {
|
|
489
|
-
visible: true,
|
|
490
|
-
label: "SSH into workspace",
|
|
491
|
-
},
|
|
492
|
-
});
|
|
493
|
-
return { ...terminal, commandResult };
|
|
494
|
-
},
|
|
495
|
-
),
|
|
496
|
-
coreOperation(
|
|
497
|
-
{
|
|
498
|
-
workflow: "",
|
|
499
|
-
id: "snapshot",
|
|
500
|
-
source: "core",
|
|
501
|
-
kind: "command",
|
|
502
|
-
title: "Snapshot",
|
|
503
|
-
description: "Capture a snapshot from a workspace VM",
|
|
504
|
-
inputFields: [
|
|
505
|
-
workflowField,
|
|
506
|
-
stringField({ name: "workspace", position: 0, required: true }),
|
|
507
|
-
stringField({ name: "label", required: false }),
|
|
508
|
-
],
|
|
509
|
-
cli: {
|
|
510
|
-
positionals: [
|
|
511
|
-
{ name: "workspace", index: 0 },
|
|
512
|
-
],
|
|
513
|
-
options: [
|
|
514
|
-
{ name: "workflow", flag: "--workflow" },
|
|
515
|
-
{ name: "label", flag: "--label" },
|
|
516
|
-
],
|
|
517
|
-
},
|
|
518
|
-
},
|
|
519
|
-
async (input) => {
|
|
520
|
-
const parsed = parseCoreOperationInput("snapshot", input.input);
|
|
521
|
-
return await this.snapshotWorkspace({
|
|
522
|
-
workflow: optionalStringInput("snapshot", parsed, "workflow"),
|
|
523
|
-
workspace: requiredStringInput("snapshot", parsed, "workspace"),
|
|
524
|
-
label: optionalStringInput("snapshot", parsed, "label"),
|
|
525
|
-
});
|
|
526
|
-
},
|
|
527
|
-
),
|
|
528
|
-
coreOperation(
|
|
529
|
-
{
|
|
530
|
-
workflow: "",
|
|
531
|
-
id: "delete",
|
|
532
|
-
aliases: ["rm"],
|
|
522
|
+
workflow: workflow.name,
|
|
523
|
+
id: "remove",
|
|
533
524
|
source: "core",
|
|
534
|
-
kind: "
|
|
535
|
-
title: "
|
|
536
|
-
description: "
|
|
537
|
-
inputFields: [
|
|
538
|
-
workflowField,
|
|
539
|
-
stringField({ name: "workspace", position: 0, required: true }),
|
|
540
|
-
],
|
|
525
|
+
kind: "workspace-action",
|
|
526
|
+
title: "Remove",
|
|
527
|
+
description: "Remove a workspace",
|
|
528
|
+
inputFields: [],
|
|
541
529
|
cli: {
|
|
542
|
-
positionals: [{ name: "workspace", index: 0 }],
|
|
543
530
|
options: [
|
|
544
|
-
{ name: "
|
|
545
|
-
{ name: "yes", flag: "--yes", aliases: ["-y"], required: true, type: "boolean", runtime: false },
|
|
531
|
+
{ name: "yes", flag: "--yes", aliases: ["-y"], type: "boolean", runtime: false },
|
|
546
532
|
],
|
|
547
533
|
},
|
|
548
534
|
},
|
|
549
|
-
async (input) =>
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
),
|
|
557
|
-
];
|
|
535
|
+
async (input) =>
|
|
536
|
+
await this.removeWorkspace({
|
|
537
|
+
workflow: input.workflow,
|
|
538
|
+
workspace: input.workspace,
|
|
539
|
+
}),
|
|
540
|
+
)
|
|
541
|
+
);
|
|
558
542
|
}
|
|
559
543
|
|
|
560
544
|
listNodeRuns(): WorkflowNodeRunRecord[] {
|
|
@@ -566,6 +550,15 @@ export class DevMachineEngine {
|
|
|
566
550
|
}
|
|
567
551
|
|
|
568
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
|
+
}
|
|
569
562
|
const operation = this.findRuntimeOperationEntry(input.operation);
|
|
570
563
|
if (!operation) throw new EngineOperationNotFoundError(input.operation);
|
|
571
564
|
return await operation.run({ workflow: input.workflow, input: input.input });
|
|
@@ -590,10 +583,44 @@ export class DevMachineEngine {
|
|
|
590
583
|
workflow: workflow.name,
|
|
591
584
|
});
|
|
592
585
|
if (result !== undefined) assertJsonValue(result, `Operation ${operation.id} result`);
|
|
593
|
-
if (operation.createsWorkspace) return this.saveOperationWorkspace(workflow, operation, input.input, result);
|
|
594
586
|
return result ?? null;
|
|
595
587
|
}
|
|
596
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
|
+
|
|
597
624
|
async plan(input: { workflow?: string; machine?: string } = {}): Promise<WorkflowPlan> {
|
|
598
625
|
const workflow = this.getWorkflow(input.workflow ?? input.machine);
|
|
599
626
|
const providers = await this.createProviders(workflow);
|
|
@@ -626,158 +653,106 @@ export class DevMachineEngine {
|
|
|
626
653
|
providers,
|
|
627
654
|
mode: "apply",
|
|
628
655
|
});
|
|
629
|
-
const workspaceSource = this.resolveWorkspaceSource(workflow, result.context, providers, { required: false });
|
|
630
656
|
|
|
631
657
|
return {
|
|
632
658
|
context: result.context,
|
|
633
|
-
snapshotId: snapshotIdOf(workspaceSource),
|
|
634
|
-
workspaceSource,
|
|
635
659
|
plan: result.plan,
|
|
636
660
|
};
|
|
637
661
|
}
|
|
638
662
|
|
|
639
663
|
async fork(input: { workflow?: string; machine?: string; name: string }): Promise<WorkspaceRecord> {
|
|
640
|
-
|
|
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
|
+
}
|
|
641
672
|
|
|
642
673
|
const applied = await this.apply({ workflow: input.workflow ?? input.machine });
|
|
643
674
|
const workflow = this.getWorkflow(input.workflow ?? input.machine);
|
|
644
675
|
const providers = await this.createProviders(workflow);
|
|
645
|
-
if (workflow.
|
|
646
|
-
|
|
647
|
-
workflow,
|
|
648
|
-
providers,
|
|
649
|
-
context: applied.context,
|
|
650
|
-
name: input.name,
|
|
651
|
-
});
|
|
676
|
+
if (!workflow.workspace) {
|
|
677
|
+
throw new Error(`Workflow ${workflow.name} does not define a workspace`);
|
|
652
678
|
}
|
|
653
679
|
|
|
654
|
-
const sourceRef = this.resolveWorkspaceSource(workflow, applied.context, providers, { required: true })!;
|
|
655
|
-
const workspaceProvider = this.findWorkspaceProvider(providers, sourceRef);
|
|
656
|
-
const created = await workspaceProvider.createWorkspace(sourceRef, { name: input.name });
|
|
657
|
-
const providerId = created.providerId ?? providerIdOf(sourceRef) ?? this.providerIdForWorkspaceProvider(providers, workspaceProvider);
|
|
658
680
|
const now = new Date().toISOString();
|
|
659
|
-
|
|
660
681
|
const workspace: WorkspaceRecord = {
|
|
661
682
|
id: crypto.randomUUID(),
|
|
662
683
|
name: input.name,
|
|
663
|
-
providerId,
|
|
664
684
|
workflow: workflow.name,
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
sourceRef: created.sourceRef ?? sourceRef,
|
|
668
|
-
context: { ...applied.context },
|
|
685
|
+
workflowCtx: { ...applied.context },
|
|
686
|
+
ctx: {},
|
|
669
687
|
createdAt: now,
|
|
670
688
|
updatedAt: now,
|
|
671
|
-
metadata: created.metadata ?? {},
|
|
672
689
|
};
|
|
673
690
|
|
|
674
691
|
this.getStateService().saveWorkspace(workspace);
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
providers,
|
|
678
|
-
workspaceProvider,
|
|
679
|
-
workspace,
|
|
680
|
-
context: applied.context,
|
|
681
|
-
});
|
|
682
|
-
|
|
683
|
-
this.emit({
|
|
684
|
-
type: "workspace.ready",
|
|
685
|
-
workspaceId: input.name,
|
|
686
|
-
providerId: workspace.providerId,
|
|
687
|
-
resourceId: workspace.resourceId,
|
|
688
|
-
snapshotId: workspace.snapshotId,
|
|
689
|
-
});
|
|
690
|
-
|
|
691
|
-
return workspace;
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
async attachTerminal(input: {
|
|
695
|
-
workspaceOrVmId: string;
|
|
696
|
-
workflow?: string;
|
|
697
|
-
machine?: string;
|
|
698
|
-
printOnly?: boolean;
|
|
699
|
-
user?: string;
|
|
700
|
-
}): Promise<{ command: string }> {
|
|
701
|
-
const workspace = this.getStateService().findWorkspace(input.workspaceOrVmId);
|
|
702
|
-
const workflow = this.getWorkflow(input.workflow ?? input.machine ?? workspace?.workflow);
|
|
703
|
-
const providers = await this.createProviders(workflow);
|
|
704
|
-
const workspaceProvider = workspace
|
|
705
|
-
? this.workspaceProviderById(providers, workspace.providerId)
|
|
706
|
-
: this.singleWorkspaceProvider(providers);
|
|
707
|
-
if (workspace) {
|
|
708
|
-
await this.runWorkspaceOpenHook({
|
|
692
|
+
try {
|
|
693
|
+
await this.runWorkspaceCreate({
|
|
709
694
|
workflow,
|
|
710
695
|
providers,
|
|
711
|
-
workspaceProvider,
|
|
712
696
|
workspace,
|
|
713
|
-
context:
|
|
697
|
+
context: applied.context,
|
|
698
|
+
name: input.name,
|
|
714
699
|
});
|
|
700
|
+
} catch (error) {
|
|
701
|
+
this.getStateService().deleteWorkspace(workspace.name);
|
|
702
|
+
throw error;
|
|
715
703
|
}
|
|
716
|
-
const
|
|
704
|
+
const ready = this.getStateService().getWorkspace(input.name) ?? workspace;
|
|
717
705
|
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
stderr: "inherit",
|
|
723
|
-
});
|
|
724
|
-
await proc.exited;
|
|
725
|
-
}
|
|
706
|
+
this.emit({
|
|
707
|
+
type: "workspace.ready",
|
|
708
|
+
workspaceId: ready.name,
|
|
709
|
+
});
|
|
726
710
|
|
|
727
|
-
return
|
|
711
|
+
return ready;
|
|
728
712
|
}
|
|
729
713
|
|
|
730
714
|
async deleteWorkspace(input: { workspace: string; workflow?: string; machine?: string }): Promise<WorkspaceRecord> {
|
|
731
|
-
|
|
732
|
-
if (!workspace) throw new Error(`Unknown workspace ${input.workspace}`);
|
|
733
|
-
|
|
734
|
-
const workflow = this.getWorkflow(input.workflow ?? input.machine ?? workspace.workflow);
|
|
735
|
-
const providers = await this.createProviders(workflow);
|
|
736
|
-
const workspaceProvider = this.workspaceProviderById(providers, workspace.providerId);
|
|
737
|
-
await workspaceProvider.deleteWorkspace(workspace);
|
|
738
|
-
|
|
739
|
-
this.getStateService().deleteWorkspace(input.workspace);
|
|
740
|
-
|
|
741
|
-
return workspace;
|
|
715
|
+
return await this.removeWorkspace(input);
|
|
742
716
|
}
|
|
743
717
|
|
|
744
|
-
async
|
|
718
|
+
async removeWorkspace(input: { workspace: string; workflow?: string; machine?: string }): Promise<WorkspaceRecord> {
|
|
745
719
|
const workspace = this.getStateService().getWorkspace(input.workspace);
|
|
746
720
|
if (!workspace) throw new Error(`Unknown workspace ${input.workspace}`);
|
|
747
721
|
|
|
748
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
|
+
}
|
|
749
732
|
const providers = await this.createProviders(workflow);
|
|
750
|
-
const
|
|
751
|
-
const
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
}),
|
|
766
|
-
providerFingerprint,
|
|
767
|
-
upstreamRunIds: [],
|
|
768
|
-
output: { sourceRef },
|
|
769
|
-
artifacts: collectArtifacts(sourceRef),
|
|
770
|
-
invalidated: false,
|
|
771
|
-
createdAt: now,
|
|
772
|
-
metadata: {
|
|
773
|
-
workspace: workspace.name,
|
|
774
|
-
label: input.label ?? null,
|
|
775
|
-
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 }),
|
|
776
748
|
},
|
|
777
|
-
|
|
749
|
+
workspace: workspaceRuntime,
|
|
750
|
+
providers: runtime,
|
|
751
|
+
local: this.local,
|
|
752
|
+
});
|
|
778
753
|
|
|
779
|
-
this.getStateService().
|
|
780
|
-
return
|
|
754
|
+
this.getStateService().deleteWorkspace(input.workspace);
|
|
755
|
+
return workspace;
|
|
781
756
|
}
|
|
782
757
|
|
|
783
758
|
private async evaluate(input: {
|
|
@@ -1134,65 +1109,87 @@ export class DevMachineEngine {
|
|
|
1134
1109
|
return Object.fromEntries(entries) as ProviderRuntimeMap<WorkflowProviderMap>;
|
|
1135
1110
|
}
|
|
1136
1111
|
|
|
1137
|
-
private async
|
|
1112
|
+
private async runWorkspaceCreate(input: {
|
|
1138
1113
|
workflow: LoadedWorkflow;
|
|
1139
1114
|
providers: ProviderControllers;
|
|
1140
|
-
workspaceProvider: WorkflowWorkspaceProvider;
|
|
1141
1115
|
workspace: WorkspaceRecord;
|
|
1142
1116
|
context: Record<string, JsonValue>;
|
|
1117
|
+
name: string;
|
|
1143
1118
|
}): Promise<void> {
|
|
1144
|
-
|
|
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
|
+
}
|
|
1145
1153
|
}
|
|
1146
1154
|
|
|
1147
|
-
private async
|
|
1155
|
+
private async runConfigWorkspaceOperation(input: {
|
|
1148
1156
|
workflow: LoadedWorkflow;
|
|
1149
1157
|
providers: ProviderControllers;
|
|
1150
|
-
workspaceProvider: WorkflowWorkspaceProvider;
|
|
1151
1158
|
workspace: WorkspaceRecord;
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
}
|
|
1156
|
-
|
|
1157
|
-
private async runWorkspaceHook(
|
|
1158
|
-
lifecycle: "created" | "open",
|
|
1159
|
-
hook: ((context: {
|
|
1160
|
-
workspace: WorkspaceRuntimeRecord;
|
|
1161
|
-
ctx: Readonly<Record<string, JsonValue>>;
|
|
1162
|
-
providers: ProviderRuntimeMap<WorkflowProviderMap>;
|
|
1163
|
-
providerContext: ProviderWorkspaceContext;
|
|
1164
|
-
local: LocalWorkspaceRuntime;
|
|
1165
|
-
}) => Promise<void> | void) | undefined,
|
|
1166
|
-
input: {
|
|
1167
|
-
workflow: LoadedWorkflow;
|
|
1168
|
-
providers: ProviderControllers;
|
|
1169
|
-
workspaceProvider: WorkflowWorkspaceProvider;
|
|
1170
|
-
workspace: WorkspaceRecord;
|
|
1171
|
-
context: Record<string, JsonValue>;
|
|
1172
|
-
},
|
|
1173
|
-
): Promise<void> {
|
|
1174
|
-
if (!hook) return;
|
|
1175
|
-
|
|
1176
|
-
const providerContext = await input.workspaceProvider.workspaceContext?.(input.workspace) ?? {};
|
|
1177
|
-
const workspace: WorkspaceRuntimeRecord = {
|
|
1178
|
-
...input.workspace,
|
|
1179
|
-
cwd: resolveWorkspaceCwd(input.workflow, input.context),
|
|
1180
|
-
};
|
|
1159
|
+
operation: WorkflowWorkspaceOperationDefinition<any, any, any, any>;
|
|
1160
|
+
rawInput: unknown;
|
|
1161
|
+
}): Promise<unknown> {
|
|
1181
1162
|
const metadata: JsonObject = {};
|
|
1182
1163
|
const providers = await this.createTaskRuntime({
|
|
1183
1164
|
workflow: input.workflow,
|
|
1184
1165
|
providers: input.providers,
|
|
1185
|
-
nodePath: `workspace.${input.workspace.name}.${
|
|
1166
|
+
nodePath: `workspace.${input.workspace.name}.${input.operation.id}`,
|
|
1186
1167
|
metadata,
|
|
1187
1168
|
});
|
|
1188
|
-
|
|
1189
|
-
|
|
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),
|
|
1190
1180
|
workspace,
|
|
1191
|
-
ctx: Object.freeze({ ...input.context }),
|
|
1192
1181
|
providers,
|
|
1193
|
-
providerContext: normalizeProviderWorkspaceContext(providerContext),
|
|
1194
1182
|
local: this.local,
|
|
1195
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>;
|
|
1196
1193
|
}
|
|
1197
1194
|
|
|
1198
1195
|
private getWorkflow(name: string | undefined): LoadedWorkflow {
|
|
@@ -1234,7 +1231,7 @@ export class DevMachineEngine {
|
|
|
1234
1231
|
|
|
1235
1232
|
private resolveOperationInput(
|
|
1236
1233
|
workflow: LoadedWorkflow,
|
|
1237
|
-
operation:
|
|
1234
|
+
operation: { id: string; input?: { fields: readonly WorkflowInputFieldDefinition[] } },
|
|
1238
1235
|
value: unknown,
|
|
1239
1236
|
): Record<string, unknown> {
|
|
1240
1237
|
const raw = isPlainObject(value) ? value : {};
|
|
@@ -1274,11 +1271,7 @@ export class DevMachineEngine {
|
|
|
1274
1271
|
message: `Workspace ${workspace.name} belongs to workflow ${workspace.workflow}, not ${workflow.name}`,
|
|
1275
1272
|
});
|
|
1276
1273
|
}
|
|
1277
|
-
resolved[field.name] =
|
|
1278
|
-
...workspace,
|
|
1279
|
-
cwd: resolveWorkspaceCwd(workflow, workspace.context),
|
|
1280
|
-
data: workspace.metadata,
|
|
1281
|
-
};
|
|
1274
|
+
resolved[field.name] = this.createWorkspaceRuntime(cloneWorkspace(workspace));
|
|
1282
1275
|
continue;
|
|
1283
1276
|
}
|
|
1284
1277
|
|
|
@@ -1318,115 +1311,6 @@ export class DevMachineEngine {
|
|
|
1318
1311
|
return resolved;
|
|
1319
1312
|
}
|
|
1320
1313
|
|
|
1321
|
-
private saveOperationWorkspace(
|
|
1322
|
-
workflow: LoadedWorkflow,
|
|
1323
|
-
operation: WorkflowOperationDefinition<any, any>,
|
|
1324
|
-
rawInput: unknown,
|
|
1325
|
-
result: unknown,
|
|
1326
|
-
): WorkspaceRecord {
|
|
1327
|
-
if (!isPlainObject(result)) {
|
|
1328
|
-
throw new Error(`Operation ${operation.id} must return an object when createsWorkspace is true`);
|
|
1329
|
-
}
|
|
1330
|
-
const data = result as Record<string, JsonValue>;
|
|
1331
|
-
const raw = isPlainObject(rawInput) ? rawInput : {};
|
|
1332
|
-
const name = typeof data.name === "string"
|
|
1333
|
-
? data.name
|
|
1334
|
-
: typeof raw.name === "string"
|
|
1335
|
-
? raw.name
|
|
1336
|
-
: undefined;
|
|
1337
|
-
if (!name) throw new Error(`Operation ${operation.id} must return or receive a workspace name`);
|
|
1338
|
-
const resourceId = typeof data.resourceId === "string"
|
|
1339
|
-
? data.resourceId
|
|
1340
|
-
: typeof data.vmId === "string"
|
|
1341
|
-
? data.vmId
|
|
1342
|
-
: name;
|
|
1343
|
-
const now = new Date().toISOString();
|
|
1344
|
-
const workspace: WorkspaceRecord = {
|
|
1345
|
-
id: crypto.randomUUID(),
|
|
1346
|
-
name,
|
|
1347
|
-
providerId: typeof data.providerId === "string" ? data.providerId : "config",
|
|
1348
|
-
workflow: workflow.name,
|
|
1349
|
-
resourceId,
|
|
1350
|
-
snapshotId: typeof data.snapshotId === "string" ? data.snapshotId : undefined,
|
|
1351
|
-
sourceRef: isJsonValue(data.sourceRef) ? data.sourceRef : data,
|
|
1352
|
-
context: {},
|
|
1353
|
-
createdAt: now,
|
|
1354
|
-
updatedAt: now,
|
|
1355
|
-
metadata: data,
|
|
1356
|
-
};
|
|
1357
|
-
this.getStateService().saveWorkspace(workspace);
|
|
1358
|
-
this.emit({
|
|
1359
|
-
type: "workspace.ready",
|
|
1360
|
-
workspaceId: workspace.name,
|
|
1361
|
-
providerId: workspace.providerId,
|
|
1362
|
-
resourceId: workspace.resourceId,
|
|
1363
|
-
snapshotId: workspace.snapshotId,
|
|
1364
|
-
});
|
|
1365
|
-
return workspace;
|
|
1366
|
-
}
|
|
1367
|
-
|
|
1368
|
-
private async createWorkspaceFromCallback(input: {
|
|
1369
|
-
workflow: LoadedWorkflow;
|
|
1370
|
-
providers: ProviderControllers;
|
|
1371
|
-
context: Record<string, JsonValue>;
|
|
1372
|
-
name: string;
|
|
1373
|
-
}): Promise<WorkspaceRecord> {
|
|
1374
|
-
if (!input.workflow.create) {
|
|
1375
|
-
throw new Error(`Workflow ${input.workflow.name} does not define a create callback`);
|
|
1376
|
-
}
|
|
1377
|
-
const metadata: JsonObject = {};
|
|
1378
|
-
const runtime = await this.createTaskRuntime({
|
|
1379
|
-
workflow: input.workflow,
|
|
1380
|
-
providers: input.providers,
|
|
1381
|
-
nodePath: `create.${input.name}`,
|
|
1382
|
-
metadata,
|
|
1383
|
-
});
|
|
1384
|
-
const result = await input.workflow.create.handler({
|
|
1385
|
-
...runtime,
|
|
1386
|
-
ctx: Object.freeze({ ...input.context }),
|
|
1387
|
-
name: input.name,
|
|
1388
|
-
providers: runtime,
|
|
1389
|
-
local: this.local,
|
|
1390
|
-
workflow: input.workflow.name,
|
|
1391
|
-
});
|
|
1392
|
-
assertJsonValue(result, `Workflow ${input.workflow.name} create result`);
|
|
1393
|
-
if (!isPlainObject(result)) {
|
|
1394
|
-
throw new Error(`Workflow ${input.workflow.name} create result must be an object`);
|
|
1395
|
-
}
|
|
1396
|
-
|
|
1397
|
-
const data = result as Record<string, JsonValue>;
|
|
1398
|
-
const name = typeof data.name === "string" ? data.name : input.name;
|
|
1399
|
-
const resourceId = typeof data.resourceId === "string"
|
|
1400
|
-
? data.resourceId
|
|
1401
|
-
: typeof data.vmId === "string"
|
|
1402
|
-
? data.vmId
|
|
1403
|
-
: name;
|
|
1404
|
-
const now = new Date().toISOString();
|
|
1405
|
-
const workspace: WorkspaceRecord = {
|
|
1406
|
-
id: crypto.randomUUID(),
|
|
1407
|
-
name,
|
|
1408
|
-
providerId: typeof data.providerId === "string" ? data.providerId : "config",
|
|
1409
|
-
workflow: input.workflow.name,
|
|
1410
|
-
resourceId,
|
|
1411
|
-
snapshotId: typeof data.snapshotId === "string" ? data.snapshotId : undefined,
|
|
1412
|
-
sourceRef: isJsonValue(data.sourceRef) ? data.sourceRef : data,
|
|
1413
|
-
context: { ...input.context },
|
|
1414
|
-
createdAt: now,
|
|
1415
|
-
updatedAt: now,
|
|
1416
|
-
metadata: data,
|
|
1417
|
-
};
|
|
1418
|
-
|
|
1419
|
-
this.getStateService().saveWorkspace(workspace);
|
|
1420
|
-
this.emit({
|
|
1421
|
-
type: "workspace.ready",
|
|
1422
|
-
workspaceId: workspace.name,
|
|
1423
|
-
providerId: workspace.providerId,
|
|
1424
|
-
resourceId: workspace.resourceId,
|
|
1425
|
-
snapshotId: workspace.snapshotId,
|
|
1426
|
-
});
|
|
1427
|
-
return workspace;
|
|
1428
|
-
}
|
|
1429
|
-
|
|
1430
1314
|
private getStateService(): StateService {
|
|
1431
1315
|
if (!this.state) {
|
|
1432
1316
|
throw new Error(`No state database loaded. Call engine.load() first.`);
|
|
@@ -1472,75 +1356,11 @@ export class DevMachineEngine {
|
|
|
1472
1356
|
providers,
|
|
1473
1357
|
root,
|
|
1474
1358
|
workspace: root.workspaceDefinition,
|
|
1475
|
-
create: root.createDefinition,
|
|
1476
1359
|
operations: root.operations ?? [],
|
|
1360
|
+
workspaceOperations: root.workspaceOperations ?? [],
|
|
1477
1361
|
};
|
|
1478
1362
|
}
|
|
1479
1363
|
|
|
1480
|
-
private resolveWorkspaceSource(
|
|
1481
|
-
workflow: LoadedWorkflow,
|
|
1482
|
-
context: Record<string, JsonValue>,
|
|
1483
|
-
providers: ProviderControllers,
|
|
1484
|
-
options: { required: boolean },
|
|
1485
|
-
): JsonValue | undefined {
|
|
1486
|
-
const source = workflow.workspace?.source?.(context);
|
|
1487
|
-
if (source !== undefined) {
|
|
1488
|
-
assertJsonValue(source, `Workflow ${workflow.name} workspace source`);
|
|
1489
|
-
return source;
|
|
1490
|
-
}
|
|
1491
|
-
|
|
1492
|
-
const candidates = collectArtifacts(context).filter((artifact) =>
|
|
1493
|
-
Object.values(providers).some((provider) => provider.workspace?.canUse(artifact)),
|
|
1494
|
-
);
|
|
1495
|
-
|
|
1496
|
-
if (candidates.length === 1) return candidates[0];
|
|
1497
|
-
if (!options.required && candidates.length === 0) return undefined;
|
|
1498
|
-
if (candidates.length === 0) {
|
|
1499
|
-
throw new Error(`Workflow ${workflow.name} did not produce a provider artifact that can be forked`);
|
|
1500
|
-
}
|
|
1501
|
-
throw new Error(`Workflow ${workflow.name} produced multiple forkable artifacts; configure workspace.source`);
|
|
1502
|
-
}
|
|
1503
|
-
|
|
1504
|
-
private findWorkspaceProvider(
|
|
1505
|
-
providers: ProviderControllers,
|
|
1506
|
-
sourceRef: JsonValue,
|
|
1507
|
-
): WorkflowWorkspaceProvider {
|
|
1508
|
-
const provider = Object.values(providers).find((controller) => controller.workspace?.canUse(sourceRef));
|
|
1509
|
-
if (!provider?.workspace) {
|
|
1510
|
-
throw new Error(`No workflow provider can create a workspace from ${stableJson(sourceRef)}`);
|
|
1511
|
-
}
|
|
1512
|
-
return provider.workspace;
|
|
1513
|
-
}
|
|
1514
|
-
|
|
1515
|
-
private workspaceProviderById(
|
|
1516
|
-
providers: ProviderControllers,
|
|
1517
|
-
providerId: string,
|
|
1518
|
-
): WorkflowWorkspaceProvider {
|
|
1519
|
-
const provider = Object.values(providers).find((controller) => controller.providerId === providerId);
|
|
1520
|
-
if (!provider?.workspace) {
|
|
1521
|
-
throw new Error(`Provider ${providerId} does not support workspaces`);
|
|
1522
|
-
}
|
|
1523
|
-
return provider.workspace;
|
|
1524
|
-
}
|
|
1525
|
-
|
|
1526
|
-
private singleWorkspaceProvider(providers: ProviderControllers): WorkflowWorkspaceProvider {
|
|
1527
|
-
const workspaceProviders = Object.values(providers).filter((provider) => provider.workspace);
|
|
1528
|
-
if (workspaceProviders.length !== 1 || !workspaceProviders[0]?.workspace) {
|
|
1529
|
-
throw new Error(`Expected exactly one workspace-capable provider`);
|
|
1530
|
-
}
|
|
1531
|
-
return workspaceProviders[0].workspace;
|
|
1532
|
-
}
|
|
1533
|
-
|
|
1534
|
-
private providerIdForWorkspaceProvider(
|
|
1535
|
-
providers: ProviderControllers,
|
|
1536
|
-
workspaceProvider: WorkflowWorkspaceProvider,
|
|
1537
|
-
): string {
|
|
1538
|
-
for (const provider of Object.values(providers)) {
|
|
1539
|
-
if (provider.workspace === workspaceProvider) return provider.providerId;
|
|
1540
|
-
}
|
|
1541
|
-
return "unknown";
|
|
1542
|
-
}
|
|
1543
|
-
|
|
1544
1364
|
private emit(event: WorkflowEvent): void {
|
|
1545
1365
|
for (const handler of this.handlers) handler(event);
|
|
1546
1366
|
}
|
|
@@ -1552,6 +1372,23 @@ export async function createDevMachineEngine(
|
|
|
1552
1372
|
return new DevMachineEngine(options);
|
|
1553
1373
|
}
|
|
1554
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
|
+
|
|
1555
1392
|
async function resolveProviderDefinition(
|
|
1556
1393
|
definition: WorkflowDefinition<any, any>["providers"][string],
|
|
1557
1394
|
): Promise<LoadedProviderDefinition> {
|
|
@@ -1650,7 +1487,7 @@ function summarizeWorkflow(workflow: LoadedWorkflow): WorkflowSummary {
|
|
|
1650
1487
|
providers: Object.entries(workflow.providers).map(([name, provider]) => `${name}:${provider.providerId}`),
|
|
1651
1488
|
nodes: collectNodePaths(workflow.root),
|
|
1652
1489
|
operations: workflow.operations.map((operation) => operation.id),
|
|
1653
|
-
createsWorkspace: Boolean(workflow.
|
|
1490
|
+
createsWorkspace: Boolean(workflow.workspace),
|
|
1654
1491
|
workspace: workflow.workspace,
|
|
1655
1492
|
};
|
|
1656
1493
|
}
|
|
@@ -1786,23 +1623,6 @@ function kindOf(value: unknown): string | undefined {
|
|
|
1786
1623
|
return isPlainObject(value) && typeof value.kind === "string" ? value.kind : undefined;
|
|
1787
1624
|
}
|
|
1788
1625
|
|
|
1789
|
-
function snapshotIdOf(value: unknown): string | undefined {
|
|
1790
|
-
return isPlainObject(value) && typeof value.snapshotId === "string" ? value.snapshotId : undefined;
|
|
1791
|
-
}
|
|
1792
|
-
|
|
1793
|
-
function resolveWorkspaceCwd(workflow: LoadedWorkflow, context: Record<string, JsonValue>): string | undefined {
|
|
1794
|
-
const cwd = workflow.workspace?.cwd;
|
|
1795
|
-
return typeof cwd === "function" ? cwd(context) : cwd;
|
|
1796
|
-
}
|
|
1797
|
-
|
|
1798
|
-
function normalizeProviderWorkspaceContext(value: unknown): ProviderWorkspaceContext {
|
|
1799
|
-
if (value === undefined) return {};
|
|
1800
|
-
if (!isPlainObject(value)) {
|
|
1801
|
-
throw new Error(`Provider workspace context must be an object`);
|
|
1802
|
-
}
|
|
1803
|
-
return value as ProviderWorkspaceContext;
|
|
1804
|
-
}
|
|
1805
|
-
|
|
1806
1626
|
function assertJsonValue(value: unknown, label: string): asserts value is JsonValue {
|
|
1807
1627
|
if (
|
|
1808
1628
|
value === null ||
|
|
@@ -1837,15 +1657,7 @@ function isJsonValue(value: unknown): value is JsonValue {
|
|
|
1837
1657
|
}
|
|
1838
1658
|
}
|
|
1839
1659
|
|
|
1840
|
-
const RESERVED_HOST_OPERATION_IDS = new Set(
|
|
1841
|
-
"completion",
|
|
1842
|
-
"doctor",
|
|
1843
|
-
"help",
|
|
1844
|
-
"init",
|
|
1845
|
-
"projects",
|
|
1846
|
-
"run",
|
|
1847
|
-
"version",
|
|
1848
|
-
]);
|
|
1660
|
+
const RESERVED_HOST_OPERATION_IDS = new Set<string>(RESERVED_WORKFLOW_OPERATION_IDS);
|
|
1849
1661
|
|
|
1850
1662
|
const CORE_OPERATION_INPUT_FIELDS: Record<string, readonly string[]> = {
|
|
1851
1663
|
plan: ["workflow"],
|
|
@@ -1853,7 +1665,6 @@ const CORE_OPERATION_INPUT_FIELDS: Record<string, readonly string[]> = {
|
|
|
1853
1665
|
create: ["workflow", "name"],
|
|
1854
1666
|
ssh: ["workflow", "workspaceOrVmId", "user", "print"],
|
|
1855
1667
|
snapshot: ["workflow", "workspace", "label"],
|
|
1856
|
-
delete: ["workflow", "workspace"],
|
|
1857
1668
|
};
|
|
1858
1669
|
|
|
1859
1670
|
function assertAllowedConfigOperationId(operationId: string): void {
|