@rigkit/cli 0.2.3 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rigkit/cli",
3
- "version": "0.2.3",
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/engine": "0.2.3",
29
- "@rigkit/runtime-client": "0.2.3",
30
- "@rigkit/provider-cmux": "0.2.3"
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.ts CHANGED
@@ -405,29 +405,29 @@ async function runInit(invocation: CliInvocation, options: InitOptions): Promise
405
405
  async function resolveInitAnswers(
406
406
  options: InitOptions,
407
407
  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`);
408
+ ): Promise<{ name: string; apiKey?: string; packageManager: PackageManager }> {
409
+ if (jsonMode && options.name === undefined) {
410
+ throw new Error(`rig init --json requires --name`);
411
411
  }
412
412
 
413
413
  if (jsonMode && options.packageManager && options.packageManager !== "skip") {
414
414
  throw new Error(`rig init --json only supports --package-manager skip`);
415
415
  }
416
416
 
417
- if (options.name === undefined || !options.apiKey) {
417
+ if (options.name === undefined) {
418
418
  assertInteractiveInit();
419
419
  }
420
420
 
421
421
  if (!jsonMode) {
422
422
  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."));
423
+ console.log(chalk.dim("This creates a project folder with rig.config.ts, package.json, and local ignore rules."));
424
424
  console.log("");
425
425
  }
426
426
 
427
427
  const name = options.name !== undefined
428
428
  ? normalizeMachineName(options.name)
429
429
  : await promptName();
430
- const apiKey = options.apiKey?.trim() || await promptRequiredSecret("Freestyle API key");
430
+ const apiKey = options.apiKey?.trim();
431
431
  const packageManager = options.packageManager ?? (jsonMode || !canPrompt() ? "skip" : await promptPackageManager("skip"));
432
432
 
433
433
  return {
@@ -439,7 +439,7 @@ async function resolveInitAnswers(
439
439
 
440
440
  function assertInteractiveInit(): void {
441
441
  if (canPrompt()) return;
442
- throw new Error(`rig init needs --name and --api-key when not running in an interactive terminal`);
442
+ throw new Error(`rig init needs --name when not running in an interactive terminal`);
443
443
  }
444
444
 
445
445
  function canPrompt(): boolean {
@@ -478,14 +478,6 @@ async function promptName(): Promise<string> {
478
478
  return answers.name;
479
479
  }
480
480
 
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
481
  async function promptPackageManager(defaultValue: PackageManager): Promise<PackageManager> {
490
482
  const choices: Array<{ value: PackageManager; label: string; hint: string }> = [
491
483
  { value: "npm", label: "npm", hint: "npm install" },
@@ -507,16 +499,6 @@ async function promptPackageManager(defaultValue: PackageManager): Promise<Packa
507
499
  return answers.packageManager;
508
500
  }
509
501
 
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: "*",
516
- }]);
517
- return answers.value;
518
- }
519
-
520
502
  async function runPackageManagerInstall(
521
503
  projectDir: string,
522
504
  packageManager: PackageManager,
@@ -1214,10 +1196,12 @@ async function runRuntimeOperation<T>(
1214
1196
  return;
1215
1197
  }
1216
1198
  if (isHostCapabilityRequestEvent(event)) {
1217
- presenter?.pause();
1199
+ const suspendPresenter = hostCapabilityNeedsTerminal(event);
1200
+ const logger = createHostCapabilityLogger(event, presenter);
1201
+ if (suspendPresenter) presenter?.pause();
1218
1202
  try {
1219
1203
  if (sendSession) {
1220
- await answerHostCapabilityRequestOverSession(sendSession, event);
1204
+ await answerHostCapabilityRequestOverSession(sendSession, event, { logger });
1221
1205
  } else if (respond) {
1222
1206
  await answerHostCapabilityRequestOverSession((message) => {
1223
1207
  if (isRecord(message) && message.type === "response") {
@@ -1225,12 +1209,12 @@ async function runRuntimeOperation<T>(
1225
1209
  if (id) return respond(id, "error" in message ? { error: message.error } : { result: message.result });
1226
1210
  }
1227
1211
  throw new Error(`Session response channel cannot send ${String(isRecord(message) ? message.type : typeof message)}`);
1228
- }, event);
1212
+ }, event, { logger });
1229
1213
  } else {
1230
- await answerHostCapabilityRequest(runtime, event);
1214
+ await answerHostCapabilityRequest(runtime, event, { logger });
1231
1215
  }
1232
1216
  } finally {
1233
- presenter?.resume();
1217
+ if (suspendPresenter) presenter?.resume();
1234
1218
  }
1235
1219
  return;
1236
1220
  }
@@ -1352,6 +1336,7 @@ type HostCapabilityRequestEvent = {
1352
1336
  type: "host.capability.request";
1353
1337
  requestId?: string;
1354
1338
  id?: string;
1339
+ nodePath?: string;
1355
1340
  capability: string;
1356
1341
  params: unknown;
1357
1342
  };
@@ -1360,6 +1345,15 @@ type HostRequestHandlingOptions = {
1360
1345
  quietOpen?: boolean;
1361
1346
  };
1362
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
+
1363
1357
  class UnsupportedHostCapabilityError extends Error {
1364
1358
  constructor(capability: string) {
1365
1359
  super(
@@ -1407,11 +1401,15 @@ async function answerHostRequestOverSession(
1407
1401
  }
1408
1402
  }
1409
1403
 
1410
- async function answerHostCapabilityRequest(runtime: RuntimeClient, event: HostCapabilityRequestEvent): Promise<void> {
1404
+ async function answerHostCapabilityRequest(
1405
+ runtime: RuntimeClient,
1406
+ event: HostCapabilityRequestEvent,
1407
+ options: HostCapabilityRequestHandlingOptions = {},
1408
+ ): Promise<void> {
1411
1409
  const requestId = event.requestId ?? event.id;
1412
1410
  if (!requestId) throw new Error(`Host capability request is missing requestId`);
1413
1411
  try {
1414
- const handled = await handleHostCapabilityRequest(event.capability, event.params);
1412
+ const handled = await handleHostCapabilityRequest(event.capability, event.params, options);
1415
1413
  await runtime.control.hostResponse(requestId, { result: handled.result });
1416
1414
  } catch (error) {
1417
1415
  await runtime.control.hostResponse(requestId, {
@@ -1423,11 +1421,12 @@ async function answerHostCapabilityRequest(runtime: RuntimeClient, event: HostCa
1423
1421
  async function answerHostCapabilityRequestOverSession(
1424
1422
  send: (message: unknown) => void | Promise<void>,
1425
1423
  event: HostCapabilityRequestEvent,
1424
+ options: HostCapabilityRequestHandlingOptions = {},
1426
1425
  ): Promise<void> {
1427
1426
  const id = event.id ?? event.requestId;
1428
1427
  if (!id) throw new Error(`Host capability request is missing id`);
1429
1428
  try {
1430
- const handled = await handleHostCapabilityRequest(event.capability, event.params);
1429
+ const handled = await handleHostCapabilityRequest(event.capability, event.params, options);
1431
1430
  await send({ type: "response", id, result: handled.result });
1432
1431
  if (handled.closed) reportHostCapabilityClosed(send, id, handled.closed);
1433
1432
  } catch (error) {
@@ -1473,6 +1472,34 @@ function hostRequestNeedsTerminal(event: HostRequestEvent): boolean {
1473
1472
  }
1474
1473
  }
1475
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
+
1476
1503
  function isTrustedCaptureHostCommand(params: unknown): boolean {
1477
1504
  return process.env.RIGKIT_TRUST_HOST_COMMANDS === "1" &&
1478
1505
  isRecord(params) &&
@@ -1484,12 +1511,18 @@ type HandledHostCapability = {
1484
1511
  closed?: Promise<void>;
1485
1512
  };
1486
1513
 
1487
- async function handleHostCapabilityRequest(capability: string, params: unknown): Promise<HandledHostCapability> {
1514
+ async function handleHostCapabilityRequest(
1515
+ capability: string,
1516
+ params: unknown,
1517
+ options: HostCapabilityRequestHandlingOptions = {},
1518
+ ): Promise<HandledHostCapability> {
1488
1519
  const handler = CLI_HOST_CAPABILITY_HANDLERS.get(capability);
1489
1520
  if (!handler) {
1490
1521
  throw new UnsupportedHostCapabilityError(capability);
1491
1522
  }
1492
- return normalizeHostCapabilityResult(await handler.handle(params));
1523
+ return normalizeHostCapabilityResult(await handler.handle(params, {
1524
+ log: (data, logOptions) => options.logger?.(data, logOptions),
1525
+ }));
1493
1526
  }
1494
1527
 
1495
1528
  function normalizeHostCapabilityResult(value: unknown): HandledHostCapability {
@@ -1685,6 +1718,7 @@ function isHostCapabilityRequestEvent(value: unknown): value is HostCapabilityRe
1685
1718
  return isRecord(value) &&
1686
1719
  value.type === "host.capability.request" &&
1687
1720
  (typeof value.requestId === "string" || typeof value.id === "string") &&
1721
+ (value.nodePath === undefined || typeof value.nodePath === "string") &&
1688
1722
  typeof value.capability === "string";
1689
1723
  }
1690
1724
 
@@ -1760,15 +1794,35 @@ function printWorkspaces(
1760
1794
  }
1761
1795
 
1762
1796
  printTable(
1763
- ["name", "workflow", "created"],
1797
+ ["name", "workflow", "created", "age"],
1764
1798
  workspaces.map((workspace) => [
1765
1799
  workspace.name,
1766
1800
  workspace.workflow,
1767
1801
  workspace.createdAt,
1802
+ formatWorkspaceAge(workspace.createdAt),
1768
1803
  ]),
1769
1804
  );
1770
1805
  }
1771
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
+
1772
1826
  function printSnapshots(snapshots: SnapshotRecord[]): void {
1773
1827
  if (snapshots.length === 0) {
1774
1828
  console.log("No snapshots.");
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,16 +26,18 @@ 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"));
@@ -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 { 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
 
@@ -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.4";