@rigkit/cli 0.2.3 → 0.2.5

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 CHANGED
@@ -5,9 +5,9 @@ Global `rig` CLI.
5
5
  ```bash
6
6
  npm i -g @rigkit/cli
7
7
  rig init
8
- rig plan
8
+ rig run plan
9
9
  rig ls
10
- rig plan github:owner/repo
10
+ rig run plan github:owner/repo
11
11
  ```
12
12
 
13
13
  `rig init` asks for a project name, Freestyle API key, and package manager. It creates a project folder containing a workflow-based `rig.config.ts`, `.env`, `.env.example`, `package.json`, and local ignore rules.
@@ -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
- Workspace-specific operations run as `rig <workspace>/<operation>`, for example `rig website-workspace/open-cmux` or `rig website-workspace/remove -y`.
19
+ Workspace-specific operations run as `rig run <workspace>/<operation>`, for example `rig run website-workspace/open-cmux` or `rig run 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",
3
+ "version": "0.2.5",
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/engine": "0.2.3",
29
- "@rigkit/runtime-client": "0.2.3",
30
- "@rigkit/provider-cmux": "0.2.3"
28
+ "@rigkit/engine": "0.2.5",
29
+ "@rigkit/runtime-client": "0.2.5",
30
+ "@rigkit/provider-cmux": "0.2.5"
31
31
  },
32
32
  "devDependencies": {
33
33
  "@types/bun": "latest",
package/src/cli.test.ts CHANGED
@@ -23,7 +23,7 @@ describe("CLI entrypoint", () => {
23
23
  expect(rootHelp.exitCode).toBe(0);
24
24
  expect(rootHelp.stderr).toBe("");
25
25
  expect(rootHelp.stdout).toContain("rig ");
26
- expect(rootHelp.stdout).toContain("<operation> Run a project operation exposed by the runtime");
26
+ expect(rootHelp.stdout).toContain("run Run a project operation exposed by the runtime");
27
27
 
28
28
  const version = await runCli(["version"]);
29
29
  expect(version.exitCode).toBe(0);
@@ -34,15 +34,15 @@ describe("CLI entrypoint", () => {
34
34
  expect(help.exitCode).toBe(0);
35
35
  expect(help.stderr).toBe("");
36
36
  expect(help.stdout).toContain("rig ");
37
- expect(help.stdout).toContain("<operation> Run a project operation exposed by the runtime");
37
+ expect(help.stdout).toContain("run Run a project operation exposed by the runtime");
38
38
  });
39
39
 
40
- test("treats unknown root tokens as project operations", async () => {
40
+ test("rejects operation shorthand at the root", async () => {
41
41
  const result = await runCli(["unknown"]);
42
42
 
43
43
  expect(result.exitCode).toBe(1);
44
44
  expect(result.stdout).toBe("");
45
- expect(result.stderr).toContain("No Rigkit config found");
45
+ expect(result.stderr).toContain("unknown command 'unknown'");
46
46
  });
47
47
 
48
48
  test("serves dynamic shell completion endpoint", async () => {
@@ -106,11 +106,23 @@ describe("CLI entrypoint", () => {
106
106
  });
107
107
  });
108
108
 
109
+ test("rejects workspace create names that are not shell-safe", async () => {
110
+ const projectDir = mkdtempSync(join(tmpdir(), "rigkit-cli-create-name-"));
111
+
112
+ await withWorkspaceRuntime({ projectDir }, async ({ env }) => {
113
+ const result = await runCli(["-C", projectDir, "run", "create", "--name", "some workspace", "--json"], { env });
114
+
115
+ expect(result.exitCode).toBe(1);
116
+ expect(result.stdout).toBe("");
117
+ expect(result.stderr).toContain('Invalid workspace name "some workspace"');
118
+ });
119
+ });
120
+
109
121
  test("requires discovered projects for operation --all", async () => {
110
122
  const cwd = mkdtempSync(join(tmpdir(), "rigkit-cli-run-all-"));
111
123
 
112
124
  try {
113
- const result = await runCli(["plan", "--all", "--json"], { cwd });
125
+ const result = await runCli(["run", "plan", "--all", "--json"], { cwd });
114
126
 
115
127
  expect(result.exitCode).toBe(1);
116
128
  expect(result.stdout).toBe("");
@@ -128,7 +140,7 @@ describe("CLI entrypoint", () => {
128
140
  writeFileSync(join(cwd, "web", "rig.config.ts"), "export default {}\n");
129
141
 
130
142
  try {
131
- const result = await runCli(["plan", "--discover", "--json"], { cwd });
143
+ const result = await runCli(["run", "plan", "--discover", "--json"], { cwd });
132
144
 
133
145
  expect(result.exitCode).toBe(1);
134
146
  expect(result.stdout).toBe("");
@@ -216,6 +228,30 @@ async function withWorkspaceRuntime(
216
228
  }],
217
229
  });
218
230
  }
231
+ if (pathname === "/operations") {
232
+ return runtimeJson({
233
+ operations: [{
234
+ id: "create",
235
+ kind: "command",
236
+ source: "core",
237
+ title: "Create",
238
+ description: "Create a workspace",
239
+ createsWorkspace: true,
240
+ cli: {
241
+ options: [{ name: "name", flag: "--name", required: true, type: "string" }],
242
+ },
243
+ inputSchema: {
244
+ type: "object",
245
+ additionalProperties: false,
246
+ properties: {
247
+ name: { type: "string", minLength: 1 },
248
+ },
249
+ required: ["name"],
250
+ },
251
+ }],
252
+ workspaceOperations: [],
253
+ });
254
+ }
219
255
  return runtimeJson({ error: { message: "Not found" } }, { status: 404 });
220
256
  },
221
257
  });
package/src/cli.ts CHANGED
@@ -148,41 +148,10 @@ const CLI_HOST_CAPABILITIES: Array<{ id: string; schemaHash?: string }> = [
148
148
  ...(capability.schemaHash ? { schemaHash: capability.schemaHash } : {}),
149
149
  }));
150
150
 
151
- const MANAGEMENT_COMMANDS = new Set([
152
- "completion",
153
- "doctor",
154
- "help",
155
- "init",
156
- "ls",
157
- "projects",
158
- "run",
159
- "version",
160
- ]);
161
-
162
- const GLOBAL_OPTIONS_WITH_VALUES = new Set(["-C", "--project", "--config", "--state"]);
163
-
164
151
  if (process.argv[2] === "__complete") {
165
152
  runCompletionEndpoint(process.argv.slice(3)).catch(handleCliError);
166
153
  } else {
167
- runCli(normalizeOperationArgv(process.argv)).catch(handleCliError);
168
- }
169
-
170
- function normalizeOperationArgv(argv: string[]): string[] {
171
- const args = argv.slice(2);
172
- for (let index = 0; index < args.length; index += 1) {
173
- const arg = args[index]!;
174
- if (arg === "--") return argv;
175
- if (GLOBAL_OPTIONS_WITH_VALUES.has(arg)) {
176
- index += 1;
177
- continue;
178
- }
179
- if ([...GLOBAL_OPTIONS_WITH_VALUES].some((option) => arg.startsWith(`${option}=`))) continue;
180
- if (arg === "--json") continue;
181
- if (arg.startsWith("-")) return argv;
182
- if (MANAGEMENT_COMMANDS.has(arg)) return argv;
183
- return [...argv.slice(0, 2), ...args.slice(0, index), "run", ...args.slice(index)];
184
- }
185
- return argv;
154
+ runCli(process.argv).catch(handleCliError);
186
155
  }
187
156
 
188
157
  async function runCli(argv: string[]): Promise<void> {
@@ -190,7 +159,7 @@ async function runCli(argv: string[]): Promise<void> {
190
159
  program
191
160
  .name("rig")
192
161
  .description("Rigkit workflow CLI")
193
- .usage("[options] <command|operation>")
162
+ .usage("[options] <command>")
194
163
  .version(RIGKIT_CLI_VERSION, "-v, --version", "Show Rigkit CLI version")
195
164
  .showHelpAfterError()
196
165
  .exitOverride()
@@ -228,7 +197,7 @@ async function runCli(argv: string[]): Promise<void> {
228
197
  });
229
198
 
230
199
  program
231
- .command("run <operation> [args...]", { hidden: true })
200
+ .command("run <operation> [args...]")
232
201
  .description("Run a project operation exposed by the runtime")
233
202
  .allowUnknownOption(true)
234
203
  .option("--all", "Run against every discovered project")
@@ -405,29 +374,29 @@ async function runInit(invocation: CliInvocation, options: InitOptions): Promise
405
374
  async function resolveInitAnswers(
406
375
  options: InitOptions,
407
376
  jsonMode: boolean,
408
- ): Promise<{ name: string; apiKey: string; packageManager: PackageManager }> {
409
- if (jsonMode && (options.name === undefined || !options.apiKey?.trim())) {
410
- throw new Error(`rig init --json requires --name and --api-key`);
377
+ ): Promise<{ name: string; apiKey?: string; packageManager: PackageManager }> {
378
+ if (jsonMode && options.name === undefined) {
379
+ throw new Error(`rig init --json requires --name`);
411
380
  }
412
381
 
413
382
  if (jsonMode && options.packageManager && options.packageManager !== "skip") {
414
383
  throw new Error(`rig init --json only supports --package-manager skip`);
415
384
  }
416
385
 
417
- if (options.name === undefined || !options.apiKey) {
386
+ if (options.name === undefined) {
418
387
  assertInteractiveInit();
419
388
  }
420
389
 
421
390
  if (!jsonMode) {
422
391
  console.log(chalk.bold("Initialize Rigkit"));
423
- console.log(chalk.dim("This creates a project folder with rig.config.ts, .env, package.json, and local ignore rules."));
392
+ console.log(chalk.dim("This creates a project folder with rig.config.ts, package.json, and local ignore rules."));
424
393
  console.log("");
425
394
  }
426
395
 
427
396
  const name = options.name !== undefined
428
397
  ? normalizeMachineName(options.name)
429
398
  : await promptName();
430
- const apiKey = options.apiKey?.trim() || await promptRequiredSecret("Freestyle API key");
399
+ const apiKey = options.apiKey?.trim();
431
400
  const packageManager = options.packageManager ?? (jsonMode || !canPrompt() ? "skip" : await promptPackageManager("skip"));
432
401
 
433
402
  return {
@@ -439,7 +408,7 @@ async function resolveInitAnswers(
439
408
 
440
409
  function assertInteractiveInit(): void {
441
410
  if (canPrompt()) return;
442
- throw new Error(`rig init needs --name and --api-key when not running in an interactive terminal`);
411
+ throw new Error(`rig init needs --name when not running in an interactive terminal`);
443
412
  }
444
413
 
445
414
  function canPrompt(): boolean {
@@ -478,14 +447,6 @@ async function promptName(): Promise<string> {
478
447
  return answers.name;
479
448
  }
480
449
 
481
- async function promptRequiredSecret(label: string): Promise<string> {
482
- for (;;) {
483
- const value = (await promptSecret(label)).trim();
484
- if (value) return value;
485
- console.log(chalk.red(`${label} is required.`));
486
- }
487
- }
488
-
489
450
  async function promptPackageManager(defaultValue: PackageManager): Promise<PackageManager> {
490
451
  const choices: Array<{ value: PackageManager; label: string; hint: string }> = [
491
452
  { value: "npm", label: "npm", hint: "npm install" },
@@ -507,14 +468,50 @@ async function promptPackageManager(defaultValue: PackageManager): Promise<Packa
507
468
  return answers.packageManager;
508
469
  }
509
470
 
510
- async function promptSecret(label: string): Promise<string> {
511
- const answers = await inquirer.prompt<{ value: string }>([{
512
- type: "password",
513
- name: "value",
514
- message: `${label}:`,
515
- mask: "*",
471
+ async function promptWorkspaceName(): Promise<string> {
472
+ const answers = await inquirer.prompt<{ name: string }>([{
473
+ type: "input",
474
+ name: "name",
475
+ message: "Workspace name:",
476
+ validate(value: string) {
477
+ return validateWorkspaceName(value.trim());
478
+ },
479
+ filter: (value: string) => value.trim(),
516
480
  }]);
517
- return answers.value;
481
+ return answers.name;
482
+ }
483
+
484
+ const workspaceNamePattern = /^(?!-)[A-Za-z0-9._-]+$/;
485
+
486
+ function validateWorkspaceName(value: string): true | string {
487
+ if (!value) return "Workspace name is required.";
488
+ if (!workspaceNamePattern.test(value)) {
489
+ return 'Use only letters, numbers, ".", "_", and "-", and do not start with "-".';
490
+ }
491
+ return true;
492
+ }
493
+
494
+ function assertValidWorkspaceName(value: unknown): void {
495
+ if (typeof value !== "string") throw new Error(`Workspace name must be a string`);
496
+ const valid = validateWorkspaceName(value);
497
+ if (valid !== true) throw new Error(`Invalid workspace name "${value}". ${valid}`);
498
+ }
499
+
500
+ async function promptWorkspaceOperation(
501
+ workspace: string,
502
+ operations: RuntimeOperationDefinition[],
503
+ ): Promise<string> {
504
+ const answers = await inquirer.prompt<{ operation: string }>([{
505
+ type: "select",
506
+ name: "operation",
507
+ message: `Operation for ${workspace}:`,
508
+ choices: operations.map((operation) => ({
509
+ name: operation.id,
510
+ value: operation.id,
511
+ description: operation.description || operation.title || "workspace operation",
512
+ })),
513
+ }]);
514
+ return answers.operation;
518
515
  }
519
516
 
520
517
  async function runPackageManagerInstall(
@@ -598,7 +595,7 @@ function printInitResult(result: InitProjectResult, install: InitInstallResult):
598
595
  if (install.skipped) {
599
596
  console.log(` ${detectInstallCommand(result.packageJsonPath)}`);
600
597
  }
601
- console.log(" rig plan");
598
+ console.log(" rig run plan");
602
599
  }
603
600
 
604
601
  function displayProjectDir(projectDir: string): string {
@@ -667,6 +664,7 @@ async function runProjectOperation(
667
664
  }
668
665
 
669
666
  await renderOperationResult(operation, result, parsed.hostOptions);
667
+ printInteractiveOutputGap(invocation);
670
668
  }
671
669
 
672
670
  async function runDiscoveredProjectOperation(
@@ -722,6 +720,7 @@ async function runDiscoveredProjectOperation(
722
720
  console.log(chalk.bold(displayProjectDir(project.projectDir)));
723
721
  }
724
722
  await renderOperationResult(operation, result, parsed.hostOptions);
723
+ printInteractiveOutputGap(invocation);
725
724
  }
726
725
  }
727
726
 
@@ -792,13 +791,13 @@ async function executeRuntimeOperation(
792
791
  result: unknown;
793
792
  }> {
794
793
  const manifest = await readRuntimeOperations(runtime);
795
- const resolved = findRuntimeOperation(manifest, requestedOperation);
794
+ const resolved = await resolveRequestedRuntimeOperation(invocation, runtime, manifest, requestedOperation);
796
795
  if (!resolved) {
797
796
  throw new Error(`This project does not define a Rigkit operation named "${requestedOperation}".`);
798
797
  }
799
798
  const { operation, runOperation } = resolved;
800
799
 
801
- const parsed = parseOperationArgs(operation, args);
800
+ const parsed = await parseOperationArgsWithPrompts(invocation, operation, args);
802
801
  enforceHostOnlyBooleanGuards(operation, parsed);
803
802
 
804
803
  const result = await runRuntimeOperation<unknown>(
@@ -811,6 +810,28 @@ async function executeRuntimeOperation(
811
810
  return { operation, parsed, result };
812
811
  }
813
812
 
813
+ async function resolveRequestedRuntimeOperation(
814
+ invocation: CliInvocation,
815
+ runtime: RuntimeClient,
816
+ manifest: RuntimeOperationManifest,
817
+ requestedOperation: string,
818
+ ): Promise<{ operation: RuntimeOperationDefinition; runOperation: string } | undefined> {
819
+ const resolved = findRuntimeOperation(manifest, requestedOperation);
820
+ if (resolved) return resolved;
821
+
822
+ if (requestedOperation.includes("/") || wantsJson(invocation) || !canPrompt()) return undefined;
823
+
824
+ const workspaces = await runtime.control.workspaces()
825
+ .then((response) => response.workspaces as WorkspaceRecord[])
826
+ .catch(() => []);
827
+ const workspace = workspaces.find((item) => item.name === requestedOperation);
828
+ if (!workspace || (manifest.workspaceOperations ?? []).length === 0) return undefined;
829
+
830
+ const operationId = await promptWorkspaceOperation(workspace.name, manifest.workspaceOperations ?? []);
831
+ const operation = (manifest.workspaceOperations ?? []).find((item) => item.id === operationId);
832
+ return operation ? { operation, runOperation: `${workspace.name}/${operation.id}` } : undefined;
833
+ }
834
+
814
835
  function findRuntimeOperation(
815
836
  manifest: RuntimeOperationManifest,
816
837
  requestedOperation: string,
@@ -836,7 +857,36 @@ function parseWorkspaceOperationId(value: string): { workspace: string; operatio
836
857
  };
837
858
  }
838
859
 
839
- function parseOperationArgs(operation: RuntimeOperationDefinition, args: string[]): ParsedOperationInput {
860
+ async function parseOperationArgsWithPrompts(
861
+ invocation: CliInvocation,
862
+ operation: RuntimeOperationDefinition,
863
+ args: string[],
864
+ ): Promise<ParsedOperationInput> {
865
+ const parsed = parseOperationArgs(operation, args, {
866
+ allowMissingRequired: !wantsJson(invocation) && canPrompt(),
867
+ });
868
+
869
+ if (
870
+ operation.createsWorkspace &&
871
+ parsed.input.name === undefined &&
872
+ !wantsJson(invocation) &&
873
+ canPrompt()
874
+ ) {
875
+ parsed.input.name = await promptWorkspaceName();
876
+ }
877
+ if (operation.createsWorkspace && parsed.input.name !== undefined) {
878
+ assertValidWorkspaceName(parsed.input.name);
879
+ }
880
+
881
+ enforceRequiredOperationInputs(operation, parsed);
882
+ return parsed;
883
+ }
884
+
885
+ function parseOperationArgs(
886
+ operation: RuntimeOperationDefinition,
887
+ args: string[],
888
+ options: { allowMissingRequired?: boolean } = {},
889
+ ): ParsedOperationInput {
840
890
  const cli = inferCliMetadata(operation);
841
891
  const input: Record<string, unknown> = {};
842
892
  const hostOptions: Record<string, unknown> = {};
@@ -875,19 +925,27 @@ function parseOperationArgs(operation: RuntimeOperationDefinition, args: string[
875
925
  assignPositional(positionals, positionalIndex++, arg, input);
876
926
  }
877
927
 
928
+ if (!options.allowMissingRequired) enforceRequiredOperationInputs(operation, { input, hostOptions });
929
+
930
+ return { input, hostOptions };
931
+ }
932
+
933
+ function enforceRequiredOperationInputs(
934
+ operation: RuntimeOperationDefinition,
935
+ parsed: ParsedOperationInput,
936
+ ): void {
937
+ const cli = inferCliMetadata(operation);
878
938
  for (const option of cli.options ?? []) {
879
- if (option.required && input[option.name] === undefined && hostOptions[option.name] === undefined) {
939
+ if (option.required && parsed.input[option.name] === undefined && parsed.hostOptions[option.name] === undefined) {
880
940
  throw new Error(`Operation ${operation.id} requires ${option.flag}`);
881
941
  }
882
942
  }
883
943
 
884
944
  for (const name of operation.inputSchema?.required ?? []) {
885
- if (input[name] === undefined) {
945
+ if (parsed.input[name] === undefined) {
886
946
  throw new Error(`Operation ${operation.id} requires ${name}`);
887
947
  }
888
948
  }
889
-
890
- return { input, hostOptions };
891
949
  }
892
950
 
893
951
  function enforceHostOnlyBooleanGuards(
@@ -1090,7 +1148,7 @@ async function runHelp(invocation: CliInvocation): Promise<void> {
1090
1148
  commands: [
1091
1149
  { name: "help", description: "Show Rigkit CLI help" },
1092
1150
  { name: "init", description: "Initialize a Rigkit project" },
1093
- { name: "<operation>", description: "Run a project operation exposed by the runtime" },
1151
+ { name: "run", description: "Run a project operation exposed by the runtime" },
1094
1152
  { name: "ls", description: "List project workspaces" },
1095
1153
  { name: "projects", description: "Discover Rigkit projects below the current directory" },
1096
1154
  { name: "doctor", description: "Show Rigkit runtime diagnostics" },
@@ -1104,12 +1162,12 @@ async function runHelp(invocation: CliInvocation): Promise<void> {
1104
1162
  `rig ${RIGKIT_CLI_VERSION}`,
1105
1163
  "",
1106
1164
  "Usage:",
1107
- " rig [options] <command|operation>",
1165
+ " rig [options] <command>",
1108
1166
  "",
1109
1167
  "Commands:",
1110
1168
  " help Show Rigkit CLI help",
1111
1169
  " init Initialize a Rigkit project",
1112
- " <operation> Run a project operation exposed by the runtime",
1170
+ " run Run a project operation exposed by the runtime",
1113
1171
  " ls List project workspaces",
1114
1172
  " projects Discover Rigkit projects below the current directory",
1115
1173
  " doctor Show Rigkit runtime diagnostics",
@@ -1214,10 +1272,12 @@ async function runRuntimeOperation<T>(
1214
1272
  return;
1215
1273
  }
1216
1274
  if (isHostCapabilityRequestEvent(event)) {
1217
- presenter?.pause();
1275
+ const suspendPresenter = hostCapabilityNeedsTerminal(event);
1276
+ const logger = createHostCapabilityLogger(event, presenter);
1277
+ if (suspendPresenter) presenter?.pause();
1218
1278
  try {
1219
1279
  if (sendSession) {
1220
- await answerHostCapabilityRequestOverSession(sendSession, event);
1280
+ await answerHostCapabilityRequestOverSession(sendSession, event, { logger });
1221
1281
  } else if (respond) {
1222
1282
  await answerHostCapabilityRequestOverSession((message) => {
1223
1283
  if (isRecord(message) && message.type === "response") {
@@ -1225,12 +1285,12 @@ async function runRuntimeOperation<T>(
1225
1285
  if (id) return respond(id, "error" in message ? { error: message.error } : { result: message.result });
1226
1286
  }
1227
1287
  throw new Error(`Session response channel cannot send ${String(isRecord(message) ? message.type : typeof message)}`);
1228
- }, event);
1288
+ }, event, { logger });
1229
1289
  } else {
1230
- await answerHostCapabilityRequest(runtime, event);
1290
+ await answerHostCapabilityRequest(runtime, event, { logger });
1231
1291
  }
1232
1292
  } finally {
1233
- presenter?.resume();
1293
+ if (suspendPresenter) presenter?.resume();
1234
1294
  }
1235
1295
  return;
1236
1296
  }
@@ -1352,6 +1412,7 @@ type HostCapabilityRequestEvent = {
1352
1412
  type: "host.capability.request";
1353
1413
  requestId?: string;
1354
1414
  id?: string;
1415
+ nodePath?: string;
1355
1416
  capability: string;
1356
1417
  params: unknown;
1357
1418
  };
@@ -1360,6 +1421,15 @@ type HostRequestHandlingOptions = {
1360
1421
  quietOpen?: boolean;
1361
1422
  };
1362
1423
 
1424
+ type HostCapabilityLogOptions = {
1425
+ stream?: "stdout" | "stderr" | "info";
1426
+ label?: string;
1427
+ };
1428
+
1429
+ type HostCapabilityRequestHandlingOptions = {
1430
+ logger?: (data: string, options?: HostCapabilityLogOptions) => void;
1431
+ };
1432
+
1363
1433
  class UnsupportedHostCapabilityError extends Error {
1364
1434
  constructor(capability: string) {
1365
1435
  super(
@@ -1407,11 +1477,15 @@ async function answerHostRequestOverSession(
1407
1477
  }
1408
1478
  }
1409
1479
 
1410
- async function answerHostCapabilityRequest(runtime: RuntimeClient, event: HostCapabilityRequestEvent): Promise<void> {
1480
+ async function answerHostCapabilityRequest(
1481
+ runtime: RuntimeClient,
1482
+ event: HostCapabilityRequestEvent,
1483
+ options: HostCapabilityRequestHandlingOptions = {},
1484
+ ): Promise<void> {
1411
1485
  const requestId = event.requestId ?? event.id;
1412
1486
  if (!requestId) throw new Error(`Host capability request is missing requestId`);
1413
1487
  try {
1414
- const handled = await handleHostCapabilityRequest(event.capability, event.params);
1488
+ const handled = await handleHostCapabilityRequest(event.capability, event.params, options);
1415
1489
  await runtime.control.hostResponse(requestId, { result: handled.result });
1416
1490
  } catch (error) {
1417
1491
  await runtime.control.hostResponse(requestId, {
@@ -1423,11 +1497,12 @@ async function answerHostCapabilityRequest(runtime: RuntimeClient, event: HostCa
1423
1497
  async function answerHostCapabilityRequestOverSession(
1424
1498
  send: (message: unknown) => void | Promise<void>,
1425
1499
  event: HostCapabilityRequestEvent,
1500
+ options: HostCapabilityRequestHandlingOptions = {},
1426
1501
  ): Promise<void> {
1427
1502
  const id = event.id ?? event.requestId;
1428
1503
  if (!id) throw new Error(`Host capability request is missing id`);
1429
1504
  try {
1430
- const handled = await handleHostCapabilityRequest(event.capability, event.params);
1505
+ const handled = await handleHostCapabilityRequest(event.capability, event.params, options);
1431
1506
  await send({ type: "response", id, result: handled.result });
1432
1507
  if (handled.closed) reportHostCapabilityClosed(send, id, handled.closed);
1433
1508
  } catch (error) {
@@ -1473,6 +1548,34 @@ function hostRequestNeedsTerminal(event: HostRequestEvent): boolean {
1473
1548
  }
1474
1549
  }
1475
1550
 
1551
+ function hostCapabilityNeedsTerminal(event: HostCapabilityRequestEvent): boolean {
1552
+ switch (event.capability) {
1553
+ case "cmux.open":
1554
+ return false;
1555
+ default:
1556
+ return true;
1557
+ }
1558
+ }
1559
+
1560
+ function createHostCapabilityLogger(
1561
+ event: HostCapabilityRequestEvent,
1562
+ presenter: RunPresenter | undefined,
1563
+ ): (data: string, options?: HostCapabilityLogOptions) => void {
1564
+ return (data, options = {}) => {
1565
+ if (presenter) {
1566
+ presenter.render({
1567
+ type: "log.output",
1568
+ nodePath: event.nodePath ?? "runtime",
1569
+ stream: options.stream ?? "info",
1570
+ label: options.label ?? event.capability,
1571
+ data,
1572
+ });
1573
+ return;
1574
+ }
1575
+ console.error(data);
1576
+ };
1577
+ }
1578
+
1476
1579
  function isTrustedCaptureHostCommand(params: unknown): boolean {
1477
1580
  return process.env.RIGKIT_TRUST_HOST_COMMANDS === "1" &&
1478
1581
  isRecord(params) &&
@@ -1484,12 +1587,18 @@ type HandledHostCapability = {
1484
1587
  closed?: Promise<void>;
1485
1588
  };
1486
1589
 
1487
- async function handleHostCapabilityRequest(capability: string, params: unknown): Promise<HandledHostCapability> {
1590
+ async function handleHostCapabilityRequest(
1591
+ capability: string,
1592
+ params: unknown,
1593
+ options: HostCapabilityRequestHandlingOptions = {},
1594
+ ): Promise<HandledHostCapability> {
1488
1595
  const handler = CLI_HOST_CAPABILITY_HANDLERS.get(capability);
1489
1596
  if (!handler) {
1490
1597
  throw new UnsupportedHostCapabilityError(capability);
1491
1598
  }
1492
- return normalizeHostCapabilityResult(await handler.handle(params));
1599
+ return normalizeHostCapabilityResult(await handler.handle(params, {
1600
+ log: (data, logOptions) => options.logger?.(data, logOptions),
1601
+ }));
1493
1602
  }
1494
1603
 
1495
1604
  function normalizeHostCapabilityResult(value: unknown): HandledHostCapability {
@@ -1685,6 +1794,7 @@ function isHostCapabilityRequestEvent(value: unknown): value is HostCapabilityRe
1685
1794
  return isRecord(value) &&
1686
1795
  value.type === "host.capability.request" &&
1687
1796
  (typeof value.requestId === "string" || typeof value.id === "string") &&
1797
+ (value.nodePath === undefined || typeof value.nodePath === "string") &&
1688
1798
  typeof value.capability === "string";
1689
1799
  }
1690
1800
 
@@ -1739,6 +1849,11 @@ function printJson(value: unknown): void {
1739
1849
  console.log(JSON.stringify(value, null, 2));
1740
1850
  }
1741
1851
 
1852
+ function printInteractiveOutputGap(invocation: CliInvocation): void {
1853
+ if (wantsJson(invocation) || !process.stdout.isTTY) return;
1854
+ console.log("");
1855
+ }
1856
+
1742
1857
  function printPlan(plan: WorkflowPlan): void {
1743
1858
  console.log(`${plan.workflow}: ${plan.cachedNodeCount}/${plan.nodeCount} nodes cached`);
1744
1859
 
@@ -1760,15 +1875,35 @@ function printWorkspaces(
1760
1875
  }
1761
1876
 
1762
1877
  printTable(
1763
- ["name", "workflow", "created"],
1878
+ ["name", "workflow", "created", "age"],
1764
1879
  workspaces.map((workspace) => [
1765
1880
  workspace.name,
1766
1881
  workspace.workflow,
1767
1882
  workspace.createdAt,
1883
+ formatWorkspaceAge(workspace.createdAt),
1768
1884
  ]),
1769
1885
  );
1770
1886
  }
1771
1887
 
1888
+ function formatWorkspaceAge(createdAt: string): string {
1889
+ const createdTime = Date.parse(createdAt);
1890
+ if (Number.isNaN(createdTime)) return chalk.dim("unknown");
1891
+
1892
+ const ageMs = Math.max(0, Date.now() - createdTime);
1893
+ const minute = 60 * 1000;
1894
+ const hour = 60 * minute;
1895
+ const day = 24 * hour;
1896
+ const label = ageMs < hour
1897
+ ? `${Math.max(1, Math.floor(ageMs / minute))}m`
1898
+ : ageMs < day
1899
+ ? `${Math.floor(ageMs / hour)}h`
1900
+ : `${Math.floor(ageMs / day)}d`;
1901
+
1902
+ if (ageMs < day) return chalk.green(label);
1903
+ if (ageMs <= 3 * day) return chalk.yellow(label);
1904
+ return chalk.red(label);
1905
+ }
1906
+
1772
1907
  function printSnapshots(snapshots: SnapshotRecord[]): void {
1773
1908
  if (snapshots.length === 0) {
1774
1909
  console.log("No snapshots.");
@@ -11,8 +11,8 @@ describe("CLI completion", () => {
11
11
  await withWorkspaceRuntime({ projectDir }, async () => {
12
12
  const items = await completeRig({
13
13
  cwd: projectDir,
14
- words: ["rig", "ssh", ""],
15
- currentIndex: 2,
14
+ words: ["rig", "run", "ssh", ""],
15
+ currentIndex: 3,
16
16
  });
17
17
 
18
18
  expect(items.map((item) => item.value)).toEqual(["api", "web"]);
@@ -25,8 +25,8 @@ describe("CLI completion", () => {
25
25
  await withWorkspaceRuntime({ projectDir }, async () => {
26
26
  const items = await completeRig({
27
27
  cwd: projectDir,
28
- words: ["rig", "ssh", "vm-"],
29
- currentIndex: 2,
28
+ words: ["rig", "run", "ssh", "vm-"],
29
+ currentIndex: 3,
30
30
  });
31
31
 
32
32
  expect(items).toEqual([]);
@@ -39,8 +39,8 @@ describe("CLI completion", () => {
39
39
  await withWorkspaceRuntime({ projectDir, cleanupDir: parentDir }, async () => {
40
40
  const items = await completeRig({
41
41
  cwd: parentDir,
42
- words: ["rig", "-C", "project", "ssh", ""],
43
- currentIndex: 4,
42
+ words: ["rig", "-C", "project", "run", "ssh", ""],
43
+ currentIndex: 5,
44
44
  });
45
45
 
46
46
  expect(items.map((item) => item.value)).toEqual(["api", "web"]);
@@ -55,15 +55,42 @@ describe("CLI completion", () => {
55
55
  words: ["rig", "run", ""],
56
56
  currentIndex: 2,
57
57
  });
58
- expect(roots.map((item) => item.value)).toContain("api/");
58
+ expect(roots.map((item) => item.value)).toContain("api");
59
59
  expect(roots.map((item) => item.value)).toContain("ssh");
60
60
 
61
- const operations = await completeRig({
61
+ const exactWorkspace = await completeRig({
62
+ cwd: projectDir,
63
+ words: ["rig", "run", "api"],
64
+ currentIndex: 2,
65
+ });
66
+ expect(exactWorkspace.map((item) => item.value)).toEqual(["api/remove", "api/open-cmux"]);
67
+
68
+ const workspaceAfterSpace = await completeRig({
69
+ cwd: projectDir,
70
+ words: ["rig", "run", "api", ""],
71
+ currentIndex: 3,
72
+ });
73
+ expect(workspaceAfterSpace.map((item) => item.value)).toEqual(["api/remove", "api/open-cmux"]);
74
+
75
+ const slashWorkspace = await completeRig({
62
76
  cwd: projectDir,
63
77
  words: ["rig", "run", "api/"],
64
78
  currentIndex: 2,
65
79
  });
66
- expect(operations.map((item) => item.value)).toEqual(["api/remove", "api/open-cmux"]);
80
+ expect(slashWorkspace.map((item) => item.value)).toEqual(["api/remove", "api/open-cmux"]);
81
+ });
82
+ });
83
+
84
+ test("does not complete runtime operations at the root command position", async () => {
85
+ const projectDir = mkdtempSync(join(tmpdir(), "rigkit-completion-"));
86
+ await withWorkspaceRuntime({ projectDir }, async () => {
87
+ const items = await completeRig({
88
+ cwd: projectDir,
89
+ words: ["rig", "s"],
90
+ currentIndex: 1,
91
+ });
92
+
93
+ expect(items.map((item) => item.value)).not.toContain("ssh");
67
94
  });
68
95
  });
69
96
 
@@ -72,7 +99,10 @@ describe("CLI completion", () => {
72
99
 
73
100
  expect(formatCompletionItems(items, "bash")).toBe("api");
74
101
  expect(formatCompletionItems(items, "zsh")).toBe("api\tvm-api");
102
+ expect(formatCompletionItems([{ value: "api", description: "workspace smoke", noSpace: true }], "zsh"))
103
+ .toBe("api\tworkspace smoke\tnospace");
75
104
  expect(renderCompletionScript("zsh")).toContain("rig __complete");
105
+ expect(renderCompletionScript("zsh")).toContain("compadd -S ''");
76
106
  });
77
107
 
78
108
  test("completes ls targets", async () => {
package/src/completion.ts CHANGED
@@ -6,6 +6,7 @@ export type CompletionShell = "bash" | "fish" | "zsh";
6
6
  export type CompletionItem = {
7
7
  value: string;
8
8
  description?: string;
9
+ noSpace?: boolean;
9
10
  };
10
11
 
11
12
  type CompleteRigInput = {
@@ -102,31 +103,10 @@ export async function completeRig(input: CompleteRigInput): Promise<CompletionIt
102
103
  if (expectsOptionValue(before)) return [];
103
104
 
104
105
  if (!command) {
105
- const rootOperation = parseRootOperation(before);
106
- if (rootOperation.operation) {
107
- if (current.startsWith("-")) {
108
- const operation = await safeResolveRuntimeOperation(resolveProjectDir(words, cwd), rootOperation.operation);
109
- return filterItems([
110
- ...(operation?.cli?.options ?? []).flatMap((option) => [
111
- { value: option.flag, description: option.name },
112
- ...(option.aliases ?? []).map((alias) => ({ value: alias, description: option.name })),
113
- ]),
114
- ...COMMAND_OPTIONS.operation,
115
- ...GLOBAL_OPTIONS,
116
- ], current);
117
- }
118
- const operation = await safeResolveRuntimeOperation(resolveProjectDir(words, cwd), rootOperation.operation);
119
- const operationPositionalCount = countRunOperationPositionals(rootOperation.args);
120
- const positional = operation?.cli?.positionals?.find((item) => item.index === operationPositionalCount);
121
- if (positional && /workspace|vm/i.test(positional.name)) {
122
- return filterItems(await workspaceTargets(resolveProjectDir(words, cwd), current, /vm/i.test(positional.name)), current);
123
- }
124
- return [];
125
- }
126
106
  return filterItems(
127
107
  current.startsWith("-")
128
108
  ? GLOBAL_OPTIONS
129
- : [...COMMANDS, ...await safeOperationTargets(resolveProjectDir(words, cwd), current), ...GLOBAL_OPTIONS],
109
+ : [...COMMANDS, ...GLOBAL_OPTIONS],
130
110
  current,
131
111
  );
132
112
  }
@@ -157,6 +137,9 @@ export async function completeRig(input: CompleteRigInput): Promise<CompletionIt
157
137
  return filterItems(await safeOperationTargets(resolveProjectDir(words, cwd), current), current);
158
138
  }
159
139
  const operation = await safeResolveRuntimeOperation(resolveProjectDir(words, cwd), run.operation);
140
+ if (!operation && run.args.length === 0) {
141
+ return filterItems(await safeWorkspaceOperationTargets(resolveProjectDir(words, cwd), run.operation), current);
142
+ }
160
143
  const operationPositionalCount = countRunOperationPositionals(run.args);
161
144
  const positional = operation?.cli?.positionals?.find((item) => item.index === operationPositionalCount);
162
145
  if (positional && /workspace|vm/i.test(positional.name)) {
@@ -178,6 +161,9 @@ export async function completeRig(input: CompleteRigInput): Promise<CompletionIt
178
161
  export function formatCompletionItems(items: CompletionItem[], shell: CompletionShell): string {
179
162
  const lines = items.map((item) => {
180
163
  if (shell === "bash") return item.value;
164
+ if (shell === "zsh" && item.noSpace) {
165
+ return `${item.value}\t${item.description ?? ""}\tnospace`;
166
+ }
181
167
  return item.description ? `${item.value}\t${item.description}` : item.value;
182
168
  });
183
169
  return lines.join("\n");
@@ -217,22 +203,33 @@ complete -c rig -f -a "(__rig_complete)"
217
203
  `;
218
204
  }
219
205
 
220
- return `#compdef rig
206
+ return `#compdef rig
221
207
  # rig zsh completion
222
208
  _rig() {
223
- local -a raw completions
224
- local line value description
209
+ local -a raw values descriptions nospace_values nospace_descriptions
210
+ local line value description rest marker
225
211
  raw=("\${(@f)$(command rig __complete --shell zsh --index $((CURRENT - 1)) -- "\${words[@]}" 2>/dev/null)}")
226
212
  for line in "\${raw[@]}"; do
227
213
  value="\${line%%$'\\t'*}"
214
+ description=""
215
+ marker=""
228
216
  if [[ "$line" == *$'\\t'* ]]; then
229
- description="\${line#*$'\\t'}"
230
- completions+=("\${value}:\${description}")
217
+ rest="\${line#*$'\\t'}"
218
+ description="\${rest%%$'\\t'*}"
219
+ if [[ "$rest" == *$'\\t'* ]]; then
220
+ marker="\${rest#*$'\\t'}"
221
+ fi
222
+ fi
223
+ if [[ "$marker" == "nospace" ]]; then
224
+ nospace_values+=("\${value}")
225
+ nospace_descriptions+=("\${description}")
231
226
  else
232
- completions+=("\${value}")
227
+ values+=("\${value}")
228
+ descriptions+=("\${description}")
233
229
  fi
234
230
  done
235
- _describe 'rig' completions
231
+ (( \${#nospace_values} )) && compadd -S '' -d nospace_descriptions -a nospace_values
232
+ (( \${#values} )) && compadd -d descriptions -a values
236
233
  }
237
234
  compdef _rig rig
238
235
  `;
@@ -298,21 +295,6 @@ function parseRunCommand(words: string[]): { operation?: string; args: string[]
298
295
  return { operation: args[0], args: args.slice(1) };
299
296
  }
300
297
 
301
- function parseRootOperation(words: string[]): { operation?: string; args: string[] } {
302
- const args: string[] = [];
303
- for (let index = 0; index < words.length; index += 1) {
304
- const word = words[index]!;
305
- if (OPTIONS_WITH_VALUES.has(word)) {
306
- index += 1;
307
- continue;
308
- }
309
- if (word.startsWith("--") && word.includes("=")) continue;
310
- if (word.startsWith("-")) continue;
311
- args.push(word);
312
- }
313
- return { operation: args[0], args: args.slice(1) };
314
- }
315
-
316
298
  function countRunOperationPositionals(args: string[]): number {
317
299
  let count = 0;
318
300
  for (let index = 0; index < args.length; index += 1) {
@@ -392,15 +374,35 @@ async function operationTargets(
392
374
  return workspaceOperationTargets(manifest, current);
393
375
  }
394
376
  const workspaces = await readWorkspaces(paths).catch(() => []);
377
+ if (workspaces.some((workspace) => workspace.name === current)) {
378
+ return workspaceOperationTargets(manifest, `${current}/`);
379
+ }
395
380
  return manifest.operations.flatMap((operation) => [
396
381
  { value: operation.id, description: operation.description },
397
382
  ...(operation.aliases ?? []).map((alias) => ({ value: alias, description: operation.description })),
398
383
  ]).concat(workspaces.map((workspace) => ({
399
- value: `${workspace.name}/`,
384
+ value: workspace.name,
400
385
  description: `workspace ${workspace.workflow}`,
386
+ noSpace: true,
401
387
  })));
402
388
  }
403
389
 
390
+ async function safeWorkspaceOperationTargets(
391
+ paths: { projectDir: string; configPath: string },
392
+ workspace: string,
393
+ ): Promise<CompletionItem[]> {
394
+ try {
395
+ const [manifest, workspaces] = await Promise.all([
396
+ readOperations(paths),
397
+ readWorkspaces(paths),
398
+ ]);
399
+ if (!workspaces.some((item) => item.name === workspace)) return [];
400
+ return workspaceOperationTargets(manifest, `${workspace}/`);
401
+ } catch {
402
+ return [];
403
+ }
404
+ }
405
+
404
406
  function workspaceOperationTargets(manifest: RuntimeOperationManifest, current: string): CompletionItem[] {
405
407
  const slash = current.indexOf("/");
406
408
  if (slash < 0) return [];
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 { FREESTYLE_PROVIDER_PACKAGE_NAME, PROJECT_PACKAGE_NAME } from "./project.ts";
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,24 +26,27 @@ describe("initProject", () => {
22
26
  expect(existsSync(projectDir)).toBe(true);
23
27
  expect(result.created).toEqual({
24
28
  config: true,
25
- env: true,
26
- envExample: true,
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, ".env"), "utf8")).toBe("FREESTYLE_API_KEY=fs_test_123\n");
34
- expect(readFileSync(join(projectDir, ".env.example"), "utf8")).toBe("FREESTYLE_API_KEY=\n");
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"));
38
44
  expect(pkg.name).toBe("platform-api");
39
- expect(pkg.scripts.plan).toBe("rig plan");
40
- expect(pkg.scripts.apply).toBe("rig apply");
45
+ expect(pkg.scripts.plan).toBe("rig run plan");
46
+ expect(pkg.scripts.apply).toBe("rig run 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", () => {
@@ -67,7 +74,7 @@ describe("initProject", () => {
67
74
  const pkg = JSON.parse(readFileSync(join(projectDir, "package.json"), "utf8"));
68
75
  expect(pkg.name).toBe("existing");
69
76
  expect(pkg.scripts.test).toBe("bun test");
70
- expect(pkg.scripts.plan).toBe("rig plan");
77
+ expect(pkg.scripts.plan).toBe("rig run plan");
71
78
  });
72
79
  });
73
80
 
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 { FREESTYLE_PROVIDER_PACKAGE_NAME, PROJECT_PACKAGE_NAME } from "./project.ts";
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: string;
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 = writeEnvFile(envPath, input.apiKey);
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,46 @@ 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, env, sequence } from "@rigkit/sdk";
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 freestyleProvider = freestyle.provider({
101
- apiKey: () => env.secret("FREESTYLE_API_KEY"),
102
- image: "node-22",
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
- const result = await vm.probe("node --version", { name: "node is v22" });
109
- if (!result.ok || !result.stdout.trim().startsWith("v22.")) {
110
- throw new Error(\`Expected Node.js v22, got: \${result.stdout}\${result.stderr}\`);
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
132
  .workspace({
115
- create: async ({ workflow, providers }) => {
116
- const vm = await providers.freestyle.vms.fromSnapshot(workflow.ctx.vm);
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
+ });
117
139
  return {
118
- vmId: vm.vmId,
140
+ vmId,
119
141
  };
120
142
  },
121
143
  remove: async ({ providers, workspace }) => {
122
- await providers.freestyle.vms.delete(workspace.ctx.vmId);
144
+ await providers.freestyle.client.vms.delete({ vmId: workspace.ctx.vmId });
123
145
  },
124
146
  });
125
147
 
@@ -207,7 +229,7 @@ function ensureProjectPackageJson(
207
229
  }
208
230
 
209
231
  const scripts = pkg.scripts as Record<string, string>;
210
- for (const [key, value] of Object.entries({ plan: "rig plan", apply: "rig apply" })) {
232
+ for (const [key, value] of Object.entries({ plan: "rig run plan", apply: "rig run apply" })) {
211
233
  if (scripts[key] !== value) {
212
234
  scripts[key] = value;
213
235
  updated = true;
@@ -218,11 +240,13 @@ function ensureProjectPackageJson(
218
240
  const devDependencies = isRecord(pkg.devDependencies) ? pkg.devDependencies : {};
219
241
  const sdkDependencyChanged =
220
242
  devDependencies[PROJECT_PACKAGE_NAME] !== RIGKIT_CLI_VERSION ||
221
- 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;
222
245
  if (sdkDependencyChanged) {
223
246
  delete devDependencies["@rigkit/runtime"];
224
247
  devDependencies[PROJECT_PACKAGE_NAME] = RIGKIT_CLI_VERSION;
225
248
  devDependencies[FREESTYLE_PROVIDER_PACKAGE_NAME] = RIGKIT_CLI_VERSION;
249
+ devDependencies[FREESTYLE_SDK_PACKAGE_NAME] = FREESTYLE_SDK_PACKAGE_VERSION;
226
250
  updated = true;
227
251
  }
228
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/version.ts CHANGED
@@ -1 +1 @@
1
- export const RIGKIT_CLI_VERSION = "0.2.3";
1
+ export const RIGKIT_CLI_VERSION = "0.2.5";