@rigkit/cli 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/README.md +1 -1
- package/package.json +4 -4
- package/src/cli.test.ts +4 -11
- package/src/cli.ts +119 -106
- package/src/completion.test.ts +55 -24
- package/src/completion.ts +59 -20
- package/src/init.test.ts +13 -6
- package/src/init.ts +51 -26
- package/src/project.ts +2 -0
- package/src/run-presenter.ts +1 -1
- package/src/version.ts +1 -1
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ Interactive terminals use Inquirer prompts and a chalk/log-update run timeline.
|
|
|
16
16
|
|
|
17
17
|
Interactive providers can ask the CLI to open provider-owned URLs. For example, Freestyle terminal sessions are served by the Freestyle provider, while the CLI only opens the presented URL in a browser.
|
|
18
18
|
|
|
19
|
-
`rig
|
|
19
|
+
Workspace-specific operations run as `rig <workspace>/<operation>`, for example `rig website-workspace/open-cmux` or `rig website-workspace/remove -y`.
|
|
20
20
|
|
|
21
21
|
`rig ls` lists workspaces for the selected project. `rig ls snapshots` lists cached snapshot runs, and `rig ls config` shows the resolved project paths.
|
|
22
22
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rigkit/cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -25,9 +25,9 @@
|
|
|
25
25
|
"log-symbols": "^7.0.1",
|
|
26
26
|
"log-update": "^8.0.0",
|
|
27
27
|
"ora": "^9.4.0",
|
|
28
|
-
"@rigkit/
|
|
29
|
-
"@rigkit/
|
|
30
|
-
"@rigkit/
|
|
28
|
+
"@rigkit/provider-cmux": "0.2.4",
|
|
29
|
+
"@rigkit/engine": "0.2.4",
|
|
30
|
+
"@rigkit/runtime-client": "0.2.4"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"@types/bun": "latest",
|
package/src/cli.test.ts
CHANGED
|
@@ -83,8 +83,8 @@ describe("CLI entrypoint", () => {
|
|
|
83
83
|
|
|
84
84
|
expect(result.exitCode).toBe(0);
|
|
85
85
|
expect(result.stderr).toBe("");
|
|
86
|
-
expect(result.stdout).toContain("name
|
|
87
|
-
expect(result.stdout).toContain("api
|
|
86
|
+
expect(result.stdout).toContain("name workflow");
|
|
87
|
+
expect(result.stdout).toContain("api smoke");
|
|
88
88
|
});
|
|
89
89
|
});
|
|
90
90
|
|
|
@@ -100,8 +100,7 @@ describe("CLI entrypoint", () => {
|
|
|
100
100
|
workspaces: [{
|
|
101
101
|
name: "api",
|
|
102
102
|
workflow: "smoke",
|
|
103
|
-
|
|
104
|
-
snapshotId: "snap-api",
|
|
103
|
+
ctx: {},
|
|
105
104
|
}],
|
|
106
105
|
});
|
|
107
106
|
});
|
|
@@ -210,14 +209,8 @@ async function withWorkspaceRuntime(
|
|
|
210
209
|
workspaces: [{
|
|
211
210
|
id: "workspace-api",
|
|
212
211
|
name: "api",
|
|
213
|
-
providerId: "freestyle",
|
|
214
212
|
workflow: "smoke",
|
|
215
|
-
|
|
216
|
-
snapshotId: "snap-api",
|
|
217
|
-
sourceRef: null,
|
|
218
|
-
context: {},
|
|
219
|
-
metadata: {},
|
|
220
|
-
data: {},
|
|
213
|
+
ctx: {},
|
|
221
214
|
createdAt: now,
|
|
222
215
|
updatedAt: now,
|
|
223
216
|
}],
|
package/src/cli.ts
CHANGED
|
@@ -94,15 +94,8 @@ type EngineProjectInfo = {
|
|
|
94
94
|
};
|
|
95
95
|
|
|
96
96
|
type RuntimeOperationManifest = {
|
|
97
|
-
hostMethods?: {
|
|
98
|
-
known?: Array<{ id: string; modes?: string[] }>;
|
|
99
|
-
requiredByOperations?: Record<string, string[]>;
|
|
100
|
-
};
|
|
101
|
-
hostCapabilities?: {
|
|
102
|
-
optional?: Array<{ id: string; schemaHash?: string }>;
|
|
103
|
-
requiredByOperations?: Record<string, string[]>;
|
|
104
|
-
};
|
|
105
97
|
operations: RuntimeOperationDefinition[];
|
|
98
|
+
workspaceOperations?: RuntimeOperationDefinition[];
|
|
106
99
|
};
|
|
107
100
|
|
|
108
101
|
type RuntimeOperationDefinition = {
|
|
@@ -111,8 +104,6 @@ type RuntimeOperationDefinition = {
|
|
|
111
104
|
title?: string;
|
|
112
105
|
description?: string;
|
|
113
106
|
createsWorkspace?: boolean;
|
|
114
|
-
requiredHostMethods?: Array<{ id: string; modes?: string[] }>;
|
|
115
|
-
requiredHostCapabilities?: Array<{ id: string; schemaHash?: string }>;
|
|
116
107
|
cli?: {
|
|
117
108
|
positionals?: Array<{ name: string; index: number }>;
|
|
118
109
|
options?: Array<{
|
|
@@ -414,29 +405,29 @@ async function runInit(invocation: CliInvocation, options: InitOptions): Promise
|
|
|
414
405
|
async function resolveInitAnswers(
|
|
415
406
|
options: InitOptions,
|
|
416
407
|
jsonMode: boolean,
|
|
417
|
-
): Promise<{ name: string; apiKey
|
|
418
|
-
if (jsonMode &&
|
|
419
|
-
throw new Error(`rig init --json requires --name
|
|
408
|
+
): Promise<{ name: string; apiKey?: string; packageManager: PackageManager }> {
|
|
409
|
+
if (jsonMode && options.name === undefined) {
|
|
410
|
+
throw new Error(`rig init --json requires --name`);
|
|
420
411
|
}
|
|
421
412
|
|
|
422
413
|
if (jsonMode && options.packageManager && options.packageManager !== "skip") {
|
|
423
414
|
throw new Error(`rig init --json only supports --package-manager skip`);
|
|
424
415
|
}
|
|
425
416
|
|
|
426
|
-
if (options.name === undefined
|
|
417
|
+
if (options.name === undefined) {
|
|
427
418
|
assertInteractiveInit();
|
|
428
419
|
}
|
|
429
420
|
|
|
430
421
|
if (!jsonMode) {
|
|
431
422
|
console.log(chalk.bold("Initialize Rigkit"));
|
|
432
|
-
console.log(chalk.dim("This creates a project folder with rig.config.ts,
|
|
423
|
+
console.log(chalk.dim("This creates a project folder with rig.config.ts, package.json, and local ignore rules."));
|
|
433
424
|
console.log("");
|
|
434
425
|
}
|
|
435
426
|
|
|
436
427
|
const name = options.name !== undefined
|
|
437
428
|
? normalizeMachineName(options.name)
|
|
438
429
|
: await promptName();
|
|
439
|
-
const apiKey = options.apiKey?.trim()
|
|
430
|
+
const apiKey = options.apiKey?.trim();
|
|
440
431
|
const packageManager = options.packageManager ?? (jsonMode || !canPrompt() ? "skip" : await promptPackageManager("skip"));
|
|
441
432
|
|
|
442
433
|
return {
|
|
@@ -448,7 +439,7 @@ async function resolveInitAnswers(
|
|
|
448
439
|
|
|
449
440
|
function assertInteractiveInit(): void {
|
|
450
441
|
if (canPrompt()) return;
|
|
451
|
-
throw new Error(`rig init needs --name
|
|
442
|
+
throw new Error(`rig init needs --name when not running in an interactive terminal`);
|
|
452
443
|
}
|
|
453
444
|
|
|
454
445
|
function canPrompt(): boolean {
|
|
@@ -487,14 +478,6 @@ async function promptName(): Promise<string> {
|
|
|
487
478
|
return answers.name;
|
|
488
479
|
}
|
|
489
480
|
|
|
490
|
-
async function promptRequiredSecret(label: string): Promise<string> {
|
|
491
|
-
for (;;) {
|
|
492
|
-
const value = (await promptSecret(label)).trim();
|
|
493
|
-
if (value) return value;
|
|
494
|
-
console.log(chalk.red(`${label} is required.`));
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
|
|
498
481
|
async function promptPackageManager(defaultValue: PackageManager): Promise<PackageManager> {
|
|
499
482
|
const choices: Array<{ value: PackageManager; label: string; hint: string }> = [
|
|
500
483
|
{ value: "npm", label: "npm", hint: "npm install" },
|
|
@@ -516,16 +499,6 @@ async function promptPackageManager(defaultValue: PackageManager): Promise<Packa
|
|
|
516
499
|
return answers.packageManager;
|
|
517
500
|
}
|
|
518
501
|
|
|
519
|
-
async function promptSecret(label: string): Promise<string> {
|
|
520
|
-
const answers = await inquirer.prompt<{ value: string }>([{
|
|
521
|
-
type: "password",
|
|
522
|
-
name: "value",
|
|
523
|
-
message: `${label}:`,
|
|
524
|
-
mask: "*",
|
|
525
|
-
}]);
|
|
526
|
-
return answers.value;
|
|
527
|
-
}
|
|
528
|
-
|
|
529
502
|
async function runPackageManagerInstall(
|
|
530
503
|
projectDir: string,
|
|
531
504
|
packageManager: PackageManager,
|
|
@@ -801,18 +774,18 @@ async function executeRuntimeOperation(
|
|
|
801
774
|
result: unknown;
|
|
802
775
|
}> {
|
|
803
776
|
const manifest = await readRuntimeOperations(runtime);
|
|
804
|
-
const
|
|
805
|
-
if (!
|
|
777
|
+
const resolved = findRuntimeOperation(manifest, requestedOperation);
|
|
778
|
+
if (!resolved) {
|
|
806
779
|
throw new Error(`This project does not define a Rigkit operation named "${requestedOperation}".`);
|
|
807
780
|
}
|
|
781
|
+
const { operation, runOperation } = resolved;
|
|
808
782
|
|
|
809
|
-
preflightHostSupport(operation);
|
|
810
783
|
const parsed = parseOperationArgs(operation, args);
|
|
811
784
|
enforceHostOnlyBooleanGuards(operation, parsed);
|
|
812
785
|
|
|
813
786
|
const result = await runRuntimeOperation<unknown>(
|
|
814
787
|
runtime,
|
|
815
|
-
|
|
788
|
+
runOperation,
|
|
816
789
|
parsed.input,
|
|
817
790
|
{ renderEvents: !wantsJson(invocation) },
|
|
818
791
|
);
|
|
@@ -821,50 +794,28 @@ async function executeRuntimeOperation(
|
|
|
821
794
|
}
|
|
822
795
|
|
|
823
796
|
function findRuntimeOperation(
|
|
824
|
-
|
|
797
|
+
manifest: RuntimeOperationManifest,
|
|
825
798
|
requestedOperation: string,
|
|
826
|
-
): RuntimeOperationDefinition | undefined {
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
function preflightHostSupport(operation: RuntimeOperationDefinition): void {
|
|
833
|
-
const unsupportedMethod = operation.requiredHostMethods?.find((method) => {
|
|
834
|
-
const supported = CLI_HOST_METHODS.find((item) => item.id === method.id);
|
|
835
|
-
return !supported || !supportsModes(supported.modes, method.modes);
|
|
836
|
-
});
|
|
837
|
-
if (unsupportedMethod) {
|
|
838
|
-
throw new Error(
|
|
839
|
-
`Operation "${operation.id}" requires host method "${formatMethodRequirement(unsupportedMethod)}". ` +
|
|
840
|
-
`Upgrade Rigkit or use a host that supports this method.`,
|
|
841
|
-
);
|
|
842
|
-
}
|
|
843
|
-
|
|
844
|
-
const unsupportedCapability = operation.requiredHostCapabilities?.find((capability) => {
|
|
845
|
-
const supported = CLI_HOST_CAPABILITIES.find((item) => item.id === capability.id);
|
|
846
|
-
return !supported || (capability.schemaHash && supported.schemaHash !== capability.schemaHash);
|
|
847
|
-
});
|
|
848
|
-
if (unsupportedCapability) {
|
|
849
|
-
throw new Error(
|
|
850
|
-
`Operation "${operation.id}" requires host capability "${formatCapabilityRequirement(unsupportedCapability)}". ` +
|
|
851
|
-
`Install or enable a local host capability handler to use it from this host.`,
|
|
852
|
-
);
|
|
799
|
+
): { operation: RuntimeOperationDefinition; runOperation: string } | undefined {
|
|
800
|
+
const workspaceOperation = parseWorkspaceOperationId(requestedOperation);
|
|
801
|
+
if (workspaceOperation) {
|
|
802
|
+
const operation = (manifest.workspaceOperations ?? []).find((item) => item.id === workspaceOperation.operation);
|
|
803
|
+
return operation ? { operation, runOperation: requestedOperation } : undefined;
|
|
853
804
|
}
|
|
854
|
-
}
|
|
855
|
-
|
|
856
|
-
function supportsModes(hostModes: string[] | undefined, requiredModes: string[] | undefined): boolean {
|
|
857
|
-
if (!requiredModes?.length) return true;
|
|
858
|
-
const supported = new Set(hostModes ?? []);
|
|
859
|
-
return requiredModes.every((mode) => supported.has(mode));
|
|
860
|
-
}
|
|
861
805
|
|
|
862
|
-
|
|
863
|
-
|
|
806
|
+
const operation = manifest.operations.find((operation) =>
|
|
807
|
+
operation.id === requestedOperation || operation.aliases?.includes(requestedOperation)
|
|
808
|
+
);
|
|
809
|
+
return operation ? { operation, runOperation: operation.id } : undefined;
|
|
864
810
|
}
|
|
865
811
|
|
|
866
|
-
function
|
|
867
|
-
|
|
812
|
+
function parseWorkspaceOperationId(value: string): { workspace: string; operation: string } | undefined {
|
|
813
|
+
const slash = value.indexOf("/");
|
|
814
|
+
if (slash <= 0 || slash === value.length - 1) return undefined;
|
|
815
|
+
return {
|
|
816
|
+
workspace: value.slice(0, slash),
|
|
817
|
+
operation: value.slice(slash + 1),
|
|
818
|
+
};
|
|
868
819
|
}
|
|
869
820
|
|
|
870
821
|
function parseOperationArgs(operation: RuntimeOperationDefinition, args: string[]): ParsedOperationInput {
|
|
@@ -1016,12 +967,12 @@ async function renderOperationResult(
|
|
|
1016
967
|
console.log("No changes applied.");
|
|
1017
968
|
return;
|
|
1018
969
|
}
|
|
1019
|
-
console.log(`resolved ${result.plan.workflow}
|
|
970
|
+
console.log(`resolved ${result.plan.workflow}`);
|
|
1020
971
|
return;
|
|
1021
972
|
}
|
|
1022
973
|
|
|
1023
974
|
if (operation.createsWorkspace && isWorkspaceRecord(result)) {
|
|
1024
|
-
console.log(
|
|
975
|
+
console.log(result.name);
|
|
1025
976
|
return;
|
|
1026
977
|
}
|
|
1027
978
|
|
|
@@ -1036,16 +987,8 @@ async function renderOperationResult(
|
|
|
1036
987
|
return;
|
|
1037
988
|
}
|
|
1038
989
|
|
|
1039
|
-
if (isRecord(result)) {
|
|
1040
|
-
const metadata = isRecord(result.metadata) ? result.metadata : {};
|
|
1041
|
-
if (typeof metadata.snapshotId === "string") {
|
|
1042
|
-
console.log(metadata.snapshotId);
|
|
1043
|
-
return;
|
|
1044
|
-
}
|
|
1045
|
-
}
|
|
1046
|
-
|
|
1047
990
|
if (isWorkspaceRecord(result)) {
|
|
1048
|
-
console.log(
|
|
991
|
+
console.log(result.name);
|
|
1049
992
|
return;
|
|
1050
993
|
}
|
|
1051
994
|
|
|
@@ -1253,10 +1196,12 @@ async function runRuntimeOperation<T>(
|
|
|
1253
1196
|
return;
|
|
1254
1197
|
}
|
|
1255
1198
|
if (isHostCapabilityRequestEvent(event)) {
|
|
1256
|
-
|
|
1199
|
+
const suspendPresenter = hostCapabilityNeedsTerminal(event);
|
|
1200
|
+
const logger = createHostCapabilityLogger(event, presenter);
|
|
1201
|
+
if (suspendPresenter) presenter?.pause();
|
|
1257
1202
|
try {
|
|
1258
1203
|
if (sendSession) {
|
|
1259
|
-
await answerHostCapabilityRequestOverSession(sendSession, event);
|
|
1204
|
+
await answerHostCapabilityRequestOverSession(sendSession, event, { logger });
|
|
1260
1205
|
} else if (respond) {
|
|
1261
1206
|
await answerHostCapabilityRequestOverSession((message) => {
|
|
1262
1207
|
if (isRecord(message) && message.type === "response") {
|
|
@@ -1264,12 +1209,12 @@ async function runRuntimeOperation<T>(
|
|
|
1264
1209
|
if (id) return respond(id, "error" in message ? { error: message.error } : { result: message.result });
|
|
1265
1210
|
}
|
|
1266
1211
|
throw new Error(`Session response channel cannot send ${String(isRecord(message) ? message.type : typeof message)}`);
|
|
1267
|
-
}, event);
|
|
1212
|
+
}, event, { logger });
|
|
1268
1213
|
} else {
|
|
1269
|
-
await answerHostCapabilityRequest(runtime, event);
|
|
1214
|
+
await answerHostCapabilityRequest(runtime, event, { logger });
|
|
1270
1215
|
}
|
|
1271
1216
|
} finally {
|
|
1272
|
-
presenter?.resume();
|
|
1217
|
+
if (suspendPresenter) presenter?.resume();
|
|
1273
1218
|
}
|
|
1274
1219
|
return;
|
|
1275
1220
|
}
|
|
@@ -1391,6 +1336,7 @@ type HostCapabilityRequestEvent = {
|
|
|
1391
1336
|
type: "host.capability.request";
|
|
1392
1337
|
requestId?: string;
|
|
1393
1338
|
id?: string;
|
|
1339
|
+
nodePath?: string;
|
|
1394
1340
|
capability: string;
|
|
1395
1341
|
params: unknown;
|
|
1396
1342
|
};
|
|
@@ -1399,6 +1345,15 @@ type HostRequestHandlingOptions = {
|
|
|
1399
1345
|
quietOpen?: boolean;
|
|
1400
1346
|
};
|
|
1401
1347
|
|
|
1348
|
+
type HostCapabilityLogOptions = {
|
|
1349
|
+
stream?: "stdout" | "stderr" | "info";
|
|
1350
|
+
label?: string;
|
|
1351
|
+
};
|
|
1352
|
+
|
|
1353
|
+
type HostCapabilityRequestHandlingOptions = {
|
|
1354
|
+
logger?: (data: string, options?: HostCapabilityLogOptions) => void;
|
|
1355
|
+
};
|
|
1356
|
+
|
|
1402
1357
|
class UnsupportedHostCapabilityError extends Error {
|
|
1403
1358
|
constructor(capability: string) {
|
|
1404
1359
|
super(
|
|
@@ -1446,11 +1401,15 @@ async function answerHostRequestOverSession(
|
|
|
1446
1401
|
}
|
|
1447
1402
|
}
|
|
1448
1403
|
|
|
1449
|
-
async function answerHostCapabilityRequest(
|
|
1404
|
+
async function answerHostCapabilityRequest(
|
|
1405
|
+
runtime: RuntimeClient,
|
|
1406
|
+
event: HostCapabilityRequestEvent,
|
|
1407
|
+
options: HostCapabilityRequestHandlingOptions = {},
|
|
1408
|
+
): Promise<void> {
|
|
1450
1409
|
const requestId = event.requestId ?? event.id;
|
|
1451
1410
|
if (!requestId) throw new Error(`Host capability request is missing requestId`);
|
|
1452
1411
|
try {
|
|
1453
|
-
const handled = await handleHostCapabilityRequest(event.capability, event.params);
|
|
1412
|
+
const handled = await handleHostCapabilityRequest(event.capability, event.params, options);
|
|
1454
1413
|
await runtime.control.hostResponse(requestId, { result: handled.result });
|
|
1455
1414
|
} catch (error) {
|
|
1456
1415
|
await runtime.control.hostResponse(requestId, {
|
|
@@ -1462,11 +1421,12 @@ async function answerHostCapabilityRequest(runtime: RuntimeClient, event: HostCa
|
|
|
1462
1421
|
async function answerHostCapabilityRequestOverSession(
|
|
1463
1422
|
send: (message: unknown) => void | Promise<void>,
|
|
1464
1423
|
event: HostCapabilityRequestEvent,
|
|
1424
|
+
options: HostCapabilityRequestHandlingOptions = {},
|
|
1465
1425
|
): Promise<void> {
|
|
1466
1426
|
const id = event.id ?? event.requestId;
|
|
1467
1427
|
if (!id) throw new Error(`Host capability request is missing id`);
|
|
1468
1428
|
try {
|
|
1469
|
-
const handled = await handleHostCapabilityRequest(event.capability, event.params);
|
|
1429
|
+
const handled = await handleHostCapabilityRequest(event.capability, event.params, options);
|
|
1470
1430
|
await send({ type: "response", id, result: handled.result });
|
|
1471
1431
|
if (handled.closed) reportHostCapabilityClosed(send, id, handled.closed);
|
|
1472
1432
|
} catch (error) {
|
|
@@ -1512,6 +1472,34 @@ function hostRequestNeedsTerminal(event: HostRequestEvent): boolean {
|
|
|
1512
1472
|
}
|
|
1513
1473
|
}
|
|
1514
1474
|
|
|
1475
|
+
function hostCapabilityNeedsTerminal(event: HostCapabilityRequestEvent): boolean {
|
|
1476
|
+
switch (event.capability) {
|
|
1477
|
+
case "cmux.open":
|
|
1478
|
+
return false;
|
|
1479
|
+
default:
|
|
1480
|
+
return true;
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
function createHostCapabilityLogger(
|
|
1485
|
+
event: HostCapabilityRequestEvent,
|
|
1486
|
+
presenter: RunPresenter | undefined,
|
|
1487
|
+
): (data: string, options?: HostCapabilityLogOptions) => void {
|
|
1488
|
+
return (data, options = {}) => {
|
|
1489
|
+
if (presenter) {
|
|
1490
|
+
presenter.render({
|
|
1491
|
+
type: "log.output",
|
|
1492
|
+
nodePath: event.nodePath ?? "runtime",
|
|
1493
|
+
stream: options.stream ?? "info",
|
|
1494
|
+
label: options.label ?? event.capability,
|
|
1495
|
+
data,
|
|
1496
|
+
});
|
|
1497
|
+
return;
|
|
1498
|
+
}
|
|
1499
|
+
console.error(data);
|
|
1500
|
+
};
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1515
1503
|
function isTrustedCaptureHostCommand(params: unknown): boolean {
|
|
1516
1504
|
return process.env.RIGKIT_TRUST_HOST_COMMANDS === "1" &&
|
|
1517
1505
|
isRecord(params) &&
|
|
@@ -1523,12 +1511,18 @@ type HandledHostCapability = {
|
|
|
1523
1511
|
closed?: Promise<void>;
|
|
1524
1512
|
};
|
|
1525
1513
|
|
|
1526
|
-
async function handleHostCapabilityRequest(
|
|
1514
|
+
async function handleHostCapabilityRequest(
|
|
1515
|
+
capability: string,
|
|
1516
|
+
params: unknown,
|
|
1517
|
+
options: HostCapabilityRequestHandlingOptions = {},
|
|
1518
|
+
): Promise<HandledHostCapability> {
|
|
1527
1519
|
const handler = CLI_HOST_CAPABILITY_HANDLERS.get(capability);
|
|
1528
1520
|
if (!handler) {
|
|
1529
1521
|
throw new UnsupportedHostCapabilityError(capability);
|
|
1530
1522
|
}
|
|
1531
|
-
return normalizeHostCapabilityResult(await handler.handle(params
|
|
1523
|
+
return normalizeHostCapabilityResult(await handler.handle(params, {
|
|
1524
|
+
log: (data, logOptions) => options.logger?.(data, logOptions),
|
|
1525
|
+
}));
|
|
1532
1526
|
}
|
|
1533
1527
|
|
|
1534
1528
|
function normalizeHostCapabilityResult(value: unknown): HandledHostCapability {
|
|
@@ -1724,6 +1718,7 @@ function isHostCapabilityRequestEvent(value: unknown): value is HostCapabilityRe
|
|
|
1724
1718
|
return isRecord(value) &&
|
|
1725
1719
|
value.type === "host.capability.request" &&
|
|
1726
1720
|
(typeof value.requestId === "string" || typeof value.id === "string") &&
|
|
1721
|
+
(value.nodePath === undefined || typeof value.nodePath === "string") &&
|
|
1727
1722
|
typeof value.capability === "string";
|
|
1728
1723
|
}
|
|
1729
1724
|
|
|
@@ -1738,8 +1733,8 @@ function isWorkflowPlan(value: unknown): value is WorkflowPlan {
|
|
|
1738
1733
|
function isWorkspaceRecord(value: unknown): value is WorkspaceRecord {
|
|
1739
1734
|
return isRecord(value) &&
|
|
1740
1735
|
typeof value.name === "string" &&
|
|
1741
|
-
typeof value.
|
|
1742
|
-
|
|
1736
|
+
typeof value.workflow === "string" &&
|
|
1737
|
+
isRecord(value.ctx);
|
|
1743
1738
|
}
|
|
1744
1739
|
|
|
1745
1740
|
function isDevMachineEvent(value: unknown): value is DevMachineEvent {
|
|
@@ -1791,7 +1786,7 @@ function printPlan(plan: WorkflowPlan): void {
|
|
|
1791
1786
|
}
|
|
1792
1787
|
|
|
1793
1788
|
function printWorkspaces(
|
|
1794
|
-
workspaces: ReadonlyArray<Pick<WorkspaceRecord, "name" | "workflow" | "
|
|
1789
|
+
workspaces: ReadonlyArray<Pick<WorkspaceRecord, "name" | "workflow" | "createdAt">>,
|
|
1795
1790
|
): void {
|
|
1796
1791
|
if (workspaces.length === 0) {
|
|
1797
1792
|
console.log("No workspaces.");
|
|
@@ -1799,17 +1794,35 @@ function printWorkspaces(
|
|
|
1799
1794
|
}
|
|
1800
1795
|
|
|
1801
1796
|
printTable(
|
|
1802
|
-
["name", "
|
|
1797
|
+
["name", "workflow", "created", "age"],
|
|
1803
1798
|
workspaces.map((workspace) => [
|
|
1804
1799
|
workspace.name,
|
|
1805
|
-
workspace.resourceId ?? "",
|
|
1806
|
-
workspace.snapshotId ?? "",
|
|
1807
1800
|
workspace.workflow,
|
|
1808
1801
|
workspace.createdAt,
|
|
1802
|
+
formatWorkspaceAge(workspace.createdAt),
|
|
1809
1803
|
]),
|
|
1810
1804
|
);
|
|
1811
1805
|
}
|
|
1812
1806
|
|
|
1807
|
+
function formatWorkspaceAge(createdAt: string): string {
|
|
1808
|
+
const createdTime = Date.parse(createdAt);
|
|
1809
|
+
if (Number.isNaN(createdTime)) return chalk.dim("unknown");
|
|
1810
|
+
|
|
1811
|
+
const ageMs = Math.max(0, Date.now() - createdTime);
|
|
1812
|
+
const minute = 60 * 1000;
|
|
1813
|
+
const hour = 60 * minute;
|
|
1814
|
+
const day = 24 * hour;
|
|
1815
|
+
const label = ageMs < hour
|
|
1816
|
+
? `${Math.max(1, Math.floor(ageMs / minute))}m`
|
|
1817
|
+
: ageMs < day
|
|
1818
|
+
? `${Math.floor(ageMs / hour)}h`
|
|
1819
|
+
: `${Math.floor(ageMs / day)}d`;
|
|
1820
|
+
|
|
1821
|
+
if (ageMs < day) return chalk.green(label);
|
|
1822
|
+
if (ageMs <= 3 * day) return chalk.yellow(label);
|
|
1823
|
+
return chalk.red(label);
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1813
1826
|
function printSnapshots(snapshots: SnapshotRecord[]): void {
|
|
1814
1827
|
if (snapshots.length === 0) {
|
|
1815
1828
|
console.log("No snapshots.");
|
|
@@ -1903,7 +1916,7 @@ function renderEvent(event: DevMachineEvent): void {
|
|
|
1903
1916
|
console.error(`artifact ${event.providerId}:${event.kind}`);
|
|
1904
1917
|
return;
|
|
1905
1918
|
case "workspace.ready":
|
|
1906
|
-
console.error(`workspace ${event.workspaceId}
|
|
1919
|
+
console.error(`workspace ${event.workspaceId} ready`);
|
|
1907
1920
|
return;
|
|
1908
1921
|
default:
|
|
1909
1922
|
return;
|
package/src/completion.test.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { projectIdFor, runtimeFingerprintFor, runtimePaths, SUPPORTED_RUNTIME_AP
|
|
|
6
6
|
import { completeRig, formatCompletionItems, renderCompletionScript } from "./completion.ts";
|
|
7
7
|
|
|
8
8
|
describe("CLI completion", () => {
|
|
9
|
-
test("completes
|
|
9
|
+
test("completes workspace targets from the runtime", async () => {
|
|
10
10
|
const projectDir = mkdtempSync(join(tmpdir(), "rigkit-completion-"));
|
|
11
11
|
await withWorkspaceRuntime({ projectDir }, async () => {
|
|
12
12
|
const items = await completeRig({
|
|
@@ -16,11 +16,11 @@ describe("CLI completion", () => {
|
|
|
16
16
|
});
|
|
17
17
|
|
|
18
18
|
expect(items.map((item) => item.value)).toEqual(["api", "web"]);
|
|
19
|
-
expect(items[0]?.description).toBe("
|
|
19
|
+
expect(items[0]?.description).toBe("smoke");
|
|
20
20
|
});
|
|
21
21
|
});
|
|
22
22
|
|
|
23
|
-
test("
|
|
23
|
+
test("does not complete provider resource ids as workspace targets", async () => {
|
|
24
24
|
const projectDir = mkdtempSync(join(tmpdir(), "rigkit-completion-"));
|
|
25
25
|
await withWorkspaceRuntime({ projectDir }, async () => {
|
|
26
26
|
const items = await completeRig({
|
|
@@ -29,7 +29,7 @@ describe("CLI completion", () => {
|
|
|
29
29
|
currentIndex: 2,
|
|
30
30
|
});
|
|
31
31
|
|
|
32
|
-
expect(items
|
|
32
|
+
expect(items).toEqual([]);
|
|
33
33
|
});
|
|
34
34
|
});
|
|
35
35
|
|
|
@@ -47,6 +47,26 @@ describe("CLI completion", () => {
|
|
|
47
47
|
});
|
|
48
48
|
});
|
|
49
49
|
|
|
50
|
+
test("completes workspace operation targets", async () => {
|
|
51
|
+
const projectDir = mkdtempSync(join(tmpdir(), "rigkit-completion-"));
|
|
52
|
+
await withWorkspaceRuntime({ projectDir }, async () => {
|
|
53
|
+
const roots = await completeRig({
|
|
54
|
+
cwd: projectDir,
|
|
55
|
+
words: ["rig", "run", ""],
|
|
56
|
+
currentIndex: 2,
|
|
57
|
+
});
|
|
58
|
+
expect(roots.map((item) => item.value)).toContain("api/");
|
|
59
|
+
expect(roots.map((item) => item.value)).toContain("ssh");
|
|
60
|
+
|
|
61
|
+
const operations = await completeRig({
|
|
62
|
+
cwd: projectDir,
|
|
63
|
+
words: ["rig", "run", "api/"],
|
|
64
|
+
currentIndex: 2,
|
|
65
|
+
});
|
|
66
|
+
expect(operations.map((item) => item.value)).toEqual(["api/remove", "api/open-cmux"]);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
50
70
|
test("formats shell completion items", () => {
|
|
51
71
|
const items = [{ value: "api", description: "vm-api" }];
|
|
52
72
|
|
|
@@ -110,26 +130,16 @@ async function withWorkspaceRuntime(
|
|
|
110
130
|
{
|
|
111
131
|
id: "workspace-api",
|
|
112
132
|
name: "api",
|
|
113
|
-
providerId: "freestyle",
|
|
114
133
|
workflow: "smoke",
|
|
115
|
-
|
|
116
|
-
sourceRef: null,
|
|
117
|
-
context: {},
|
|
118
|
-
metadata: {},
|
|
119
|
-
data: {},
|
|
134
|
+
ctx: {},
|
|
120
135
|
createdAt: now,
|
|
121
136
|
updatedAt: now,
|
|
122
137
|
},
|
|
123
138
|
{
|
|
124
139
|
id: "workspace-web",
|
|
125
140
|
name: "web",
|
|
126
|
-
providerId: "freestyle",
|
|
127
141
|
workflow: "smoke",
|
|
128
|
-
|
|
129
|
-
sourceRef: null,
|
|
130
|
-
context: {},
|
|
131
|
-
metadata: {},
|
|
132
|
-
data: {},
|
|
142
|
+
ctx: {},
|
|
133
143
|
createdAt: now,
|
|
134
144
|
updatedAt: now,
|
|
135
145
|
},
|
|
@@ -138,14 +148,6 @@ async function withWorkspaceRuntime(
|
|
|
138
148
|
}
|
|
139
149
|
if (pathname === "/operations") {
|
|
140
150
|
return runtimeJson({
|
|
141
|
-
hostMethods: {
|
|
142
|
-
known: [],
|
|
143
|
-
requiredByOperations: {},
|
|
144
|
-
},
|
|
145
|
-
hostCapabilities: {
|
|
146
|
-
optional: [],
|
|
147
|
-
requiredByOperations: {},
|
|
148
|
-
},
|
|
149
151
|
operations: [
|
|
150
152
|
{
|
|
151
153
|
id: "ssh",
|
|
@@ -166,6 +168,35 @@ async function withWorkspaceRuntime(
|
|
|
166
168
|
},
|
|
167
169
|
},
|
|
168
170
|
],
|
|
171
|
+
workspaceOperations: [
|
|
172
|
+
{
|
|
173
|
+
id: "remove",
|
|
174
|
+
kind: "workspace-action",
|
|
175
|
+
source: "core",
|
|
176
|
+
title: "Remove",
|
|
177
|
+
description: "remove workspace",
|
|
178
|
+
cli: {
|
|
179
|
+
options: [{ name: "yes", flag: "--yes", aliases: ["-y"], type: "boolean", runtime: false }],
|
|
180
|
+
},
|
|
181
|
+
inputSchema: {
|
|
182
|
+
type: "object",
|
|
183
|
+
additionalProperties: false,
|
|
184
|
+
properties: {},
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
id: "open-cmux",
|
|
189
|
+
kind: "workspace-action",
|
|
190
|
+
source: "config",
|
|
191
|
+
title: "Open cmux",
|
|
192
|
+
description: "open cmux",
|
|
193
|
+
inputSchema: {
|
|
194
|
+
type: "object",
|
|
195
|
+
additionalProperties: false,
|
|
196
|
+
properties: {},
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
],
|
|
169
200
|
});
|
|
170
201
|
}
|
|
171
202
|
return runtimeJson({ error: { message: "Not found" } }, { status: 404 });
|
package/src/completion.ts
CHANGED
|
@@ -17,6 +17,7 @@ type CompleteRigInput = {
|
|
|
17
17
|
const COMMANDS: CompletionItem[] = [
|
|
18
18
|
{ value: "help", description: "show CLI help" },
|
|
19
19
|
{ value: "init", description: "initialize a Rigkit project" },
|
|
20
|
+
{ value: "run", description: "run a project operation" },
|
|
20
21
|
{ value: "ls", description: "list project workspaces" },
|
|
21
22
|
{ value: "projects", description: "discover Rigkit projects" },
|
|
22
23
|
{ value: "doctor", description: "show runtime diagnostics" },
|
|
@@ -77,6 +78,7 @@ const OPTIONS_WITH_VALUES = new Set([
|
|
|
77
78
|
|
|
78
79
|
type RuntimeOperationManifest = {
|
|
79
80
|
operations: RuntimeOperationDefinition[];
|
|
81
|
+
workspaceOperations?: RuntimeOperationDefinition[];
|
|
80
82
|
};
|
|
81
83
|
|
|
82
84
|
type RuntimeOperationDefinition = {
|
|
@@ -121,7 +123,12 @@ export async function completeRig(input: CompleteRigInput): Promise<CompletionIt
|
|
|
121
123
|
}
|
|
122
124
|
return [];
|
|
123
125
|
}
|
|
124
|
-
return filterItems(
|
|
126
|
+
return filterItems(
|
|
127
|
+
current.startsWith("-")
|
|
128
|
+
? GLOBAL_OPTIONS
|
|
129
|
+
: [...COMMANDS, ...await safeOperationTargets(resolveProjectDir(words, cwd), current), ...GLOBAL_OPTIONS],
|
|
130
|
+
current,
|
|
131
|
+
);
|
|
125
132
|
}
|
|
126
133
|
|
|
127
134
|
if (current.startsWith("-")) {
|
|
@@ -147,7 +154,7 @@ export async function completeRig(input: CompleteRigInput): Promise<CompletionIt
|
|
|
147
154
|
if (command === "run") {
|
|
148
155
|
const run = parseRunCommand(before);
|
|
149
156
|
if (!run.operation) {
|
|
150
|
-
return filterItems(await safeOperationTargets(resolveProjectDir(words, cwd)), current);
|
|
157
|
+
return filterItems(await safeOperationTargets(resolveProjectDir(words, cwd), current), current);
|
|
151
158
|
}
|
|
152
159
|
const operation = await safeResolveRuntimeOperation(resolveProjectDir(words, cwd), run.operation);
|
|
153
160
|
const operationPositionalCount = countRunOperationPositionals(run.args);
|
|
@@ -355,48 +362,65 @@ function projectPaths(projectDir: string): { projectDir: string; configPath: str
|
|
|
355
362
|
|
|
356
363
|
async function workspaceTargets(
|
|
357
364
|
paths: { projectDir: string; configPath: string },
|
|
358
|
-
|
|
359
|
-
|
|
365
|
+
_current: string,
|
|
366
|
+
_includeVmIds: boolean,
|
|
360
367
|
): Promise<CompletionItem[]> {
|
|
361
368
|
const workspaces = await readWorkspaces(paths);
|
|
362
369
|
const items = workspaces.map((workspace) => ({
|
|
363
370
|
value: workspace.name,
|
|
364
|
-
description: workspace.
|
|
371
|
+
description: workspace.workflow,
|
|
365
372
|
}));
|
|
366
373
|
|
|
367
|
-
if (includeVmIds && current.length > 0) {
|
|
368
|
-
for (const workspace of workspaces) {
|
|
369
|
-
if (!workspace.resourceId) continue;
|
|
370
|
-
items.push({
|
|
371
|
-
value: workspace.resourceId,
|
|
372
|
-
description: workspace.name,
|
|
373
|
-
});
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
|
|
377
374
|
return dedupeItems(items);
|
|
378
375
|
}
|
|
379
376
|
|
|
380
|
-
async function readWorkspaces(paths: { projectDir: string; configPath: string }): Promise<Array<{ name: string;
|
|
377
|
+
async function readWorkspaces(paths: { projectDir: string; configPath: string }): Promise<Array<{ name: string; workflow: string }>> {
|
|
381
378
|
const runtime = await getOrStartRuntime(paths);
|
|
382
379
|
const { workspaces } = await runtime.control.workspaces();
|
|
383
380
|
return workspaces.map((workspace) => ({
|
|
384
381
|
name: workspace.name,
|
|
385
|
-
|
|
382
|
+
workflow: workspace.workflow,
|
|
386
383
|
}));
|
|
387
384
|
}
|
|
388
385
|
|
|
389
|
-
async function operationTargets(
|
|
386
|
+
async function operationTargets(
|
|
387
|
+
paths: { projectDir: string; configPath: string },
|
|
388
|
+
current: string,
|
|
389
|
+
): Promise<CompletionItem[]> {
|
|
390
390
|
const manifest = await readOperations(paths);
|
|
391
|
+
if (current.includes("/")) {
|
|
392
|
+
return workspaceOperationTargets(manifest, current);
|
|
393
|
+
}
|
|
394
|
+
const workspaces = await readWorkspaces(paths).catch(() => []);
|
|
391
395
|
return manifest.operations.flatMap((operation) => [
|
|
392
396
|
{ value: operation.id, description: operation.description },
|
|
393
397
|
...(operation.aliases ?? []).map((alias) => ({ value: alias, description: operation.description })),
|
|
398
|
+
]).concat(workspaces.map((workspace) => ({
|
|
399
|
+
value: `${workspace.name}/`,
|
|
400
|
+
description: `workspace ${workspace.workflow}`,
|
|
401
|
+
})));
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function workspaceOperationTargets(manifest: RuntimeOperationManifest, current: string): CompletionItem[] {
|
|
405
|
+
const slash = current.indexOf("/");
|
|
406
|
+
if (slash < 0) return [];
|
|
407
|
+
const workspace = current.slice(0, slash);
|
|
408
|
+
if (!workspace) return [];
|
|
409
|
+
return (manifest.workspaceOperations ?? []).flatMap((operation) => [
|
|
410
|
+
{ value: `${workspace}/${operation.id}`, description: operation.description ?? "workspace operation" },
|
|
411
|
+
...(operation.aliases ?? []).map((alias) => ({
|
|
412
|
+
value: `${workspace}/${alias}`,
|
|
413
|
+
description: operation.description ?? "workspace operation",
|
|
414
|
+
})),
|
|
394
415
|
]);
|
|
395
416
|
}
|
|
396
417
|
|
|
397
|
-
async function safeOperationTargets(
|
|
418
|
+
async function safeOperationTargets(
|
|
419
|
+
paths: { projectDir: string; configPath: string },
|
|
420
|
+
current: string,
|
|
421
|
+
): Promise<CompletionItem[]> {
|
|
398
422
|
try {
|
|
399
|
-
return await operationTargets(paths);
|
|
423
|
+
return await operationTargets(paths, current);
|
|
400
424
|
} catch {
|
|
401
425
|
return [];
|
|
402
426
|
}
|
|
@@ -407,6 +431,12 @@ async function resolveRuntimeOperation(
|
|
|
407
431
|
operationId: string,
|
|
408
432
|
): Promise<RuntimeOperationDefinition | undefined> {
|
|
409
433
|
const manifest = await readOperations(paths);
|
|
434
|
+
const workspaceOperation = parseWorkspaceOperationId(operationId);
|
|
435
|
+
if (workspaceOperation) {
|
|
436
|
+
return (manifest.workspaceOperations ?? []).find((operation) =>
|
|
437
|
+
operation.id === workspaceOperation.operation || operation.aliases?.includes(workspaceOperation.operation)
|
|
438
|
+
);
|
|
439
|
+
}
|
|
410
440
|
return manifest.operations.find((operation) =>
|
|
411
441
|
operation.id === operationId || operation.aliases?.includes(operationId)
|
|
412
442
|
);
|
|
@@ -428,6 +458,15 @@ async function readOperations(paths: { projectDir: string; configPath: string })
|
|
|
428
458
|
return await runtime.control.operations() as unknown as RuntimeOperationManifest;
|
|
429
459
|
}
|
|
430
460
|
|
|
461
|
+
function parseWorkspaceOperationId(value: string): { workspace: string; operation: string } | undefined {
|
|
462
|
+
const slash = value.indexOf("/");
|
|
463
|
+
if (slash <= 0 || slash === value.length - 1) return undefined;
|
|
464
|
+
return {
|
|
465
|
+
workspace: value.slice(0, slash),
|
|
466
|
+
operation: value.slice(slash + 1),
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
|
|
431
470
|
function filterItems(items: CompletionItem[], current: string): CompletionItem[] {
|
|
432
471
|
return dedupeItems(items).filter((item) => item.value.startsWith(current));
|
|
433
472
|
}
|
package/src/init.test.ts
CHANGED
|
@@ -3,7 +3,12 @@ import { existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from
|
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { initProject, normalizeMachineName } from "./init.ts";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
FREESTYLE_PROVIDER_PACKAGE_NAME,
|
|
8
|
+
FREESTYLE_SDK_PACKAGE_NAME,
|
|
9
|
+
FREESTYLE_SDK_PACKAGE_VERSION,
|
|
10
|
+
PROJECT_PACKAGE_NAME,
|
|
11
|
+
} from "./project.ts";
|
|
7
12
|
import { RIGKIT_CLI_VERSION } from "./version.ts";
|
|
8
13
|
|
|
9
14
|
describe("initProject", () => {
|
|
@@ -14,7 +19,6 @@ describe("initProject", () => {
|
|
|
14
19
|
projectDir,
|
|
15
20
|
configPath: join(projectDir, "rig.config.ts"),
|
|
16
21
|
name: "Platform API",
|
|
17
|
-
apiKey: "fs_test_123",
|
|
18
22
|
});
|
|
19
23
|
|
|
20
24
|
expect(result.name).toBe("platform-api");
|
|
@@ -22,16 +26,18 @@ describe("initProject", () => {
|
|
|
22
26
|
expect(existsSync(projectDir)).toBe(true);
|
|
23
27
|
expect(result.created).toEqual({
|
|
24
28
|
config: true,
|
|
25
|
-
env:
|
|
26
|
-
envExample:
|
|
29
|
+
env: false,
|
|
30
|
+
envExample: false,
|
|
27
31
|
gitignore: true,
|
|
28
32
|
packageJson: true,
|
|
29
33
|
});
|
|
30
34
|
|
|
31
35
|
expect(readFileSync(join(projectDir, "rig.config.ts"), "utf8")).toContain('sequence("platform-api"');
|
|
32
36
|
expect(readFileSync(join(projectDir, "rig.config.ts"), "utf8")).toContain("defineConfig({");
|
|
33
|
-
expect(readFileSync(join(projectDir, ".
|
|
34
|
-
expect(readFileSync(join(projectDir, ".
|
|
37
|
+
expect(readFileSync(join(projectDir, "rig.config.ts"), "utf8")).toContain("new VmSpec()");
|
|
38
|
+
expect(readFileSync(join(projectDir, "rig.config.ts"), "utf8")).not.toContain("FREESTYLE_API_KEY");
|
|
39
|
+
expect(existsSync(join(projectDir, ".env"))).toBe(false);
|
|
40
|
+
expect(existsSync(join(projectDir, ".env.example"))).toBe(false);
|
|
35
41
|
expect(readFileSync(join(projectDir, ".gitignore"), "utf8")).toContain(".env\n.rigkit/\n");
|
|
36
42
|
|
|
37
43
|
const pkg = JSON.parse(readFileSync(join(projectDir, "package.json"), "utf8"));
|
|
@@ -40,6 +46,7 @@ describe("initProject", () => {
|
|
|
40
46
|
expect(pkg.scripts.apply).toBe("rig apply");
|
|
41
47
|
expect(pkg.devDependencies[PROJECT_PACKAGE_NAME]).toBe(RIGKIT_CLI_VERSION);
|
|
42
48
|
expect(pkg.devDependencies[FREESTYLE_PROVIDER_PACKAGE_NAME]).toBe(RIGKIT_CLI_VERSION);
|
|
49
|
+
expect(pkg.devDependencies[FREESTYLE_SDK_PACKAGE_NAME]).toBe(FREESTYLE_SDK_PACKAGE_VERSION);
|
|
43
50
|
});
|
|
44
51
|
|
|
45
52
|
test("updates existing project files without replacing package metadata", () => {
|
package/src/init.ts
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { RIGKIT_CLI_VERSION } from "./version.ts";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
FREESTYLE_PROVIDER_PACKAGE_NAME,
|
|
6
|
+
FREESTYLE_SDK_PACKAGE_NAME,
|
|
7
|
+
FREESTYLE_SDK_PACKAGE_VERSION,
|
|
8
|
+
PROJECT_PACKAGE_NAME,
|
|
9
|
+
} from "./project.ts";
|
|
5
10
|
|
|
6
11
|
export type InitProjectInput = {
|
|
7
12
|
projectDir: string;
|
|
8
13
|
configPath: string;
|
|
9
14
|
name: string;
|
|
10
|
-
apiKey
|
|
15
|
+
apiKey?: string;
|
|
11
16
|
force?: boolean;
|
|
12
17
|
};
|
|
13
18
|
|
|
@@ -47,11 +52,14 @@ export function initProject(input: InitProjectInput): InitProjectResult {
|
|
|
47
52
|
writeFileSync(input.configPath, starterConfig(name));
|
|
48
53
|
}
|
|
49
54
|
|
|
55
|
+
const apiKey = input.apiKey?.trim();
|
|
50
56
|
const envPath = join(input.projectDir, ".env");
|
|
51
|
-
const env =
|
|
57
|
+
const env = apiKey
|
|
58
|
+
? writeEnvFile(envPath, apiKey)
|
|
59
|
+
: { created: false, updated: false };
|
|
52
60
|
|
|
53
61
|
const envExamplePath = join(input.projectDir, ".env.example");
|
|
54
|
-
const wroteEnvExample = !existsSync(envExamplePath);
|
|
62
|
+
const wroteEnvExample = Boolean(apiKey) && !existsSync(envExamplePath);
|
|
55
63
|
if (wroteEnvExample) {
|
|
56
64
|
writeFileSync(envExamplePath, "FREESTYLE_API_KEY=\n");
|
|
57
65
|
}
|
|
@@ -94,32 +102,47 @@ export function normalizeMachineName(value: string): string {
|
|
|
94
102
|
export function starterConfig(name: string): string {
|
|
95
103
|
const workflowName = JSON.stringify(normalizeMachineName(name));
|
|
96
104
|
|
|
97
|
-
return `import { defineConfig,
|
|
98
|
-
import { freestyle } from "@rigkit/provider-freestyle";
|
|
105
|
+
return `import { defineConfig, sequence } from "@rigkit/sdk";
|
|
106
|
+
import { freestyle, VmBaseImage, VmSpec } from "@rigkit/provider-freestyle";
|
|
99
107
|
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
108
|
+
const vmIdleTimeoutSeconds = 3600;
|
|
109
|
+
const vmSpec = new VmSpec()
|
|
110
|
+
.baseImage(new VmBaseImage("FROM node:22"))
|
|
111
|
+
.idleTimeoutSeconds(vmIdleTimeoutSeconds);
|
|
112
|
+
|
|
113
|
+
const freestyleProvider = freestyle.provider();
|
|
104
114
|
|
|
105
115
|
const dev = sequence(${workflowName})
|
|
106
|
-
.step("verify-node-22", async ({ providers }) => {
|
|
107
|
-
const vm = await providers.freestyle.vms.create(
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
116
|
+
.step("verify-node-22", async ({ providers, step }) => {
|
|
117
|
+
const { vm, vmId } = await providers.freestyle.client.vms.create({
|
|
118
|
+
spec: vmSpec,
|
|
119
|
+
logger: step.log,
|
|
120
|
+
});
|
|
121
|
+
try {
|
|
122
|
+
const result = await vm.exec("node --version");
|
|
123
|
+
if ((result.statusCode ?? 0) !== 0 || !result.stdout.trim().startsWith("v22.")) {
|
|
124
|
+
throw new Error(\`Expected Node.js v22, got: \${result.stdout}\${result.stderr}\`);
|
|
125
|
+
}
|
|
126
|
+
const snapshot = await vm.snapshot();
|
|
127
|
+
return { ctx: { snapshotId: snapshot.snapshotId } };
|
|
128
|
+
} finally {
|
|
129
|
+
await providers.freestyle.client.vms.delete({ vmId });
|
|
111
130
|
}
|
|
112
|
-
return { vm: await vm.snapshotRef() };
|
|
113
131
|
})
|
|
114
|
-
.
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
132
|
+
.workspace({
|
|
133
|
+
create: async ({ workflow, providers, step }) => {
|
|
134
|
+
const { vmId } = await providers.freestyle.client.vms.create({
|
|
135
|
+
snapshotId: workflow.ctx.snapshotId,
|
|
136
|
+
idleTimeoutSeconds: vmIdleTimeoutSeconds,
|
|
137
|
+
logger: step.log,
|
|
138
|
+
});
|
|
139
|
+
return {
|
|
140
|
+
vmId,
|
|
141
|
+
};
|
|
142
|
+
},
|
|
143
|
+
remove: async ({ providers, workspace }) => {
|
|
144
|
+
await providers.freestyle.client.vms.delete({ vmId: workspace.ctx.vmId });
|
|
145
|
+
},
|
|
123
146
|
});
|
|
124
147
|
|
|
125
148
|
export default defineConfig({
|
|
@@ -217,11 +240,13 @@ function ensureProjectPackageJson(
|
|
|
217
240
|
const devDependencies = isRecord(pkg.devDependencies) ? pkg.devDependencies : {};
|
|
218
241
|
const sdkDependencyChanged =
|
|
219
242
|
devDependencies[PROJECT_PACKAGE_NAME] !== RIGKIT_CLI_VERSION ||
|
|
220
|
-
devDependencies[FREESTYLE_PROVIDER_PACKAGE_NAME] !== RIGKIT_CLI_VERSION
|
|
243
|
+
devDependencies[FREESTYLE_PROVIDER_PACKAGE_NAME] !== RIGKIT_CLI_VERSION ||
|
|
244
|
+
devDependencies[FREESTYLE_SDK_PACKAGE_NAME] !== FREESTYLE_SDK_PACKAGE_VERSION;
|
|
221
245
|
if (sdkDependencyChanged) {
|
|
222
246
|
delete devDependencies["@rigkit/runtime"];
|
|
223
247
|
devDependencies[PROJECT_PACKAGE_NAME] = RIGKIT_CLI_VERSION;
|
|
224
248
|
devDependencies[FREESTYLE_PROVIDER_PACKAGE_NAME] = RIGKIT_CLI_VERSION;
|
|
249
|
+
devDependencies[FREESTYLE_SDK_PACKAGE_NAME] = FREESTYLE_SDK_PACKAGE_VERSION;
|
|
225
250
|
updated = true;
|
|
226
251
|
}
|
|
227
252
|
pkg.devDependencies = sortObject(devDependencies);
|
package/src/project.ts
CHANGED
|
@@ -4,6 +4,8 @@ import { existsSync, readdirSync } from "node:fs";
|
|
|
4
4
|
export const DEFAULT_CONFIG_FILE = "rig.config.ts";
|
|
5
5
|
export const PROJECT_PACKAGE_NAME = "@rigkit/sdk";
|
|
6
6
|
export const FREESTYLE_PROVIDER_PACKAGE_NAME = "@rigkit/provider-freestyle";
|
|
7
|
+
export const FREESTYLE_SDK_PACKAGE_NAME = "freestyle";
|
|
8
|
+
export const FREESTYLE_SDK_PACKAGE_VERSION = "^0.1.51";
|
|
7
9
|
|
|
8
10
|
export type ConfigPathOptions = {
|
|
9
11
|
project?: string;
|
package/src/run-presenter.ts
CHANGED
|
@@ -146,7 +146,7 @@ export function createRunPresenter(operation: string): RunPresenter | undefined
|
|
|
146
146
|
);
|
|
147
147
|
break;
|
|
148
148
|
case "workspace.ready":
|
|
149
|
-
phase = `Workspace ${String(event.
|
|
149
|
+
phase = `Workspace ${String(event.workspaceId ?? "ready")} ready`;
|
|
150
150
|
break;
|
|
151
151
|
case "run.completed":
|
|
152
152
|
finalStatus = "completed";
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const RIGKIT_CLI_VERSION = "0.2.
|
|
1
|
+
export const RIGKIT_CLI_VERSION = "0.2.4";
|