@runloop/rl-cli 0.4.0 → 0.9.0

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/dist/cli.js CHANGED
@@ -1,13 +1,8 @@
1
1
  #!/usr/bin/env node
2
- import { Command } from "commander";
3
- import { createDevbox } from "./commands/devbox/create.js";
4
- import { listDevboxes } from "./commands/devbox/list.js";
5
- import { deleteDevbox } from "./commands/devbox/delete.js";
6
- import { execCommand } from "./commands/devbox/exec.js";
7
- import { uploadFile } from "./commands/devbox/upload.js";
8
- import { VERSION } from "./version.js";
9
2
  import { exitAlternateScreenBuffer } from "./utils/screen.js";
10
3
  import { processUtils } from "./utils/processUtils.js";
4
+ import { createProgram } from "./utils/commands.js";
5
+ import { getApiKeyErrorMessage } from "./utils/config.js";
11
6
  // Global Ctrl+C handler to ensure it always exits
12
7
  processUtils.on("SIGINT", () => {
13
8
  // Force exit immediately, clearing alternate screen buffer
@@ -15,400 +10,7 @@ processUtils.on("SIGINT", () => {
15
10
  processUtils.stdout.write("\n");
16
11
  processUtils.exit(130); // Standard exit code for SIGINT
17
12
  });
18
- const program = new Command();
19
- program
20
- .name("rli")
21
- .description("Beautiful CLI for Runloop devbox management")
22
- .version(VERSION);
23
- // Devbox commands
24
- const devbox = program
25
- .command("devbox")
26
- .description("Manage devboxes")
27
- .alias("d");
28
- devbox
29
- .command("create")
30
- .description("Create a new devbox")
31
- .option("-n, --name <name>", "Devbox name")
32
- .option("-t, --template <template>", "Snapshot ID to use (alias: --snapshot)")
33
- .option("-s, --snapshot <snapshot>", "Snapshot ID to use")
34
- .option("--blueprint <blueprint>", "Blueprint name or ID to use")
35
- .option("--resources <size>", "Resource size (X_SMALL, SMALL, MEDIUM, LARGE, X_LARGE, XX_LARGE)")
36
- .option("--architecture <arch>", "Architecture (arm64, x86_64)")
37
- .option("--entrypoint <command>", "Entrypoint command to run")
38
- .option("--launch-commands <commands...>", "Initialization commands to run on startup")
39
- .option("--env-vars <vars...>", "Environment variables (format: KEY=value)")
40
- .option("--code-mounts <mounts...>", "Code mount configurations (JSON format)")
41
- .option("--idle-time <seconds>", "Idle time in seconds before idle action")
42
- .option("--idle-action <action>", "Action on idle (shutdown, suspend)")
43
- .option("--available-ports <ports...>", "Available ports")
44
- .option("--root", "Run as root")
45
- .option("--user <user:uid>", "Run as this user (format: username:uid)")
46
- .option("-o, --output [format]", "Output format: text|json|yaml (default: text)")
47
- .action(createDevbox);
48
- devbox
49
- .command("list")
50
- .description("List all devboxes")
51
- .option("-s, --status <status>", "Filter by status (initializing, running, suspending, suspended, resuming, failure, shutdown)")
52
- .option("-l, --limit <n>", "Max results", "20")
53
- .option("-o, --output [format]", "Output format: text|json|yaml (default: json)")
54
- .action(async (options) => {
55
- await listDevboxes(options);
56
- });
57
- devbox
58
- .command("delete <id>")
59
- .description("Shutdown a devbox")
60
- .alias("rm")
61
- .option("-o, --output [format]", "Output format: text|json|yaml (default: text)")
62
- .action(deleteDevbox);
63
- devbox
64
- .command("exec <id> <command...>")
65
- .description("Execute a command in a devbox")
66
- .option("--shell-name <name>", "Shell name to use (optional)")
67
- .option("-o, --output [format]", "Output format: text|json|yaml (default: text)")
68
- .action(async (id, command, options) => {
69
- await execCommand(id, command, options);
70
- });
71
- devbox
72
- .command("upload <id> <file>")
73
- .description("Upload a file to a devbox")
74
- .option("-p, --path <path>", "Target path in devbox")
75
- .option("-o, --output [format]", "Output format: text|json|yaml (default: text)")
76
- .action(uploadFile);
77
- // Additional devbox commands
78
- devbox
79
- .command("get <id>")
80
- .description("Get devbox details")
81
- .option("-o, --output [format]", "Output format: text|json|yaml (default: json)")
82
- .action(async (id, options) => {
83
- const { getDevbox } = await import("./commands/devbox/get.js");
84
- await getDevbox(id, options);
85
- });
86
- devbox
87
- .command("suspend <id>")
88
- .description("Suspend a devbox")
89
- .option("-o, --output [format]", "Output format: text|json|yaml (default: text)")
90
- .action(async (id, options) => {
91
- const { suspendDevbox } = await import("./commands/devbox/suspend.js");
92
- await suspendDevbox(id, options);
93
- });
94
- devbox
95
- .command("resume <id>")
96
- .description("Resume a suspended devbox")
97
- .option("-o, --output [format]", "Output format: text|json|yaml (default: text)")
98
- .action(async (id, options) => {
99
- const { resumeDevbox } = await import("./commands/devbox/resume.js");
100
- await resumeDevbox(id, options);
101
- });
102
- devbox
103
- .command("shutdown <id>")
104
- .description("Shutdown a devbox")
105
- .option("-o, --output [format]", "Output format: text|json|yaml (default: text)")
106
- .action(async (id, options) => {
107
- const { shutdownDevbox } = await import("./commands/devbox/shutdown.js");
108
- await shutdownDevbox(id, options);
109
- });
110
- devbox
111
- .command("ssh <id>")
112
- .description("SSH into a devbox")
113
- .option("--config-only", "Print SSH config only")
114
- .option("--no-wait", "Do not wait for devbox to be ready")
115
- .option("--timeout <seconds>", "Timeout in seconds to wait for readiness", "180")
116
- .option("--poll-interval <seconds>", "Polling interval in seconds while waiting", "3")
117
- .option("-o, --output [format]", "Output format: text|json|yaml (default: text)")
118
- .action(async (id, options) => {
119
- const { sshDevbox } = await import("./commands/devbox/ssh.js");
120
- await sshDevbox(id, options);
121
- });
122
- devbox
123
- .command("scp <id> <src> <dst>")
124
- .description("Copy files to/from a devbox using scp")
125
- .option("--scp-options <options>", "Additional scp options (quoted)")
126
- .option("-o, --output [format]", "Output format: text|json|yaml (default: text)")
127
- .action(async (id, src, dst, options) => {
128
- const { scpFiles } = await import("./commands/devbox/scp.js");
129
- await scpFiles(id, { src, dst, ...options });
130
- });
131
- devbox
132
- .command("rsync <id> <src> <dst>")
133
- .description("Sync files to/from a devbox using rsync")
134
- .option("--rsync-options <options>", "Additional rsync options (quoted)")
135
- .option("-o, --output [format]", "Output format: text|json|yaml (default: text)")
136
- .action(async (id, src, dst, options) => {
137
- const { rsyncFiles } = await import("./commands/devbox/rsync.js");
138
- await rsyncFiles(id, { src, dst, ...options });
139
- });
140
- devbox
141
- .command("tunnel <id> <ports>")
142
- .description("Create a port-forwarding tunnel to a devbox")
143
- .option("-o, --output [format]", "Output format: text|json|yaml (default: text)")
144
- .action(async (id, ports, options) => {
145
- const { createTunnel } = await import("./commands/devbox/tunnel.js");
146
- await createTunnel(id, { ports, ...options });
147
- });
148
- devbox
149
- .command("read <id>")
150
- .description("Read a file from a devbox using the API")
151
- .option("--remote <path>", "Remote file path to read from the devbox")
152
- .option("--output-path <path>", "Local file path to write the contents to")
153
- .option("-o, --output [format]", "Output format: text|json|yaml (default: text)")
154
- .action(async (id, options) => {
155
- const { readFile } = await import("./commands/devbox/read.js");
156
- await readFile(id, options);
157
- });
158
- devbox
159
- .command("write <id>")
160
- .description("Write a file to a devbox using the API")
161
- .option("--input <path>", "Local file path to read contents from")
162
- .option("--remote <path>", "Remote file path to write to on the devbox")
163
- .option("-o, --output [format]", "Output format: text|json|yaml (default: text)")
164
- .action(async (id, options) => {
165
- const { writeFile } = await import("./commands/devbox/write.js");
166
- await writeFile(id, options);
167
- });
168
- devbox
169
- .command("download <id>")
170
- .description("Download a file from a devbox")
171
- .option("--file-path <path>", "Path to the file in the devbox")
172
- .option("--output-path <path>", "Local path where to save the downloaded file")
173
- .option("-o, --output-format [format]", "Output format: text|json|yaml (default: interactive)")
174
- .action(async (id, options) => {
175
- const { downloadFile } = await import("./commands/devbox/download.js");
176
- await downloadFile(id, options);
177
- });
178
- devbox
179
- .command("exec-async <id> <command...>")
180
- .description("Execute a command asynchronously on a devbox")
181
- .option("--shell-name <name>", "Shell name to use (optional)")
182
- .option("-o, --output [format]", "Output format: text|json|yaml (default: text)")
183
- .action(async (id, command, options) => {
184
- const { execAsync } = await import("./commands/devbox/execAsync.js");
185
- await execAsync(id, { command: command.join(" "), ...options });
186
- });
187
- devbox
188
- .command("get-async <id> <execution-id>")
189
- .description("Get status of an async execution")
190
- .option("-o, --output [format]", "Output format: text|json|yaml (default: text)")
191
- .action(async (id, executionId, options) => {
192
- const { getAsync } = await import("./commands/devbox/getAsync.js");
193
- await getAsync(id, { executionId, ...options });
194
- });
195
- devbox
196
- .command("send-stdin <id> <execution-id>")
197
- .description("Send stdin to a running async execution")
198
- .option("--text <text>", "Text content to send to stdin")
199
- .option("--signal <signal>", "Signal to send (EOF, INTERRUPT)")
200
- .option("-o, --output [format]", "Output format: text|json|yaml (default: text)")
201
- .action(async (id, executionId, options) => {
202
- const { sendStdin } = await import("./commands/devbox/sendStdin.js");
203
- await sendStdin(id, executionId, options);
204
- });
205
- devbox
206
- .command("logs <id>")
207
- .description("View devbox logs")
208
- .option("-o, --output [format]", "Output format: text|json|yaml (default: text)")
209
- .action(async (id, options) => {
210
- const { getLogs } = await import("./commands/devbox/logs.js");
211
- await getLogs(id, options);
212
- });
213
- // Snapshot commands
214
- const snapshot = program
215
- .command("snapshot")
216
- .description("Manage devbox snapshots")
217
- .alias("snap");
218
- snapshot
219
- .command("list")
220
- .description("List all snapshots")
221
- .option("-d, --devbox <id>", "Filter by devbox ID")
222
- .option("-o, --output [format]", "Output format: text|json|yaml (default: json)")
223
- .action(async (options) => {
224
- const { listSnapshots } = await import("./commands/snapshot/list.js");
225
- await listSnapshots(options);
226
- });
227
- snapshot
228
- .command("create <devbox-id>")
229
- .description("Create a snapshot of a devbox")
230
- .option("-n, --name <name>", "Snapshot name")
231
- .option("-o, --output [format]", "Output format: text|json|yaml (default: text)")
232
- .action(async (devboxId, options) => {
233
- const { createSnapshot } = await import("./commands/snapshot/create.js");
234
- createSnapshot(devboxId, options);
235
- });
236
- snapshot
237
- .command("delete <id>")
238
- .description("Delete a snapshot")
239
- .alias("rm")
240
- .option("-o, --output [format]", "Output format: text|json|yaml (default: text)")
241
- .action(async (id, options) => {
242
- const { deleteSnapshot } = await import("./commands/snapshot/delete.js");
243
- deleteSnapshot(id, options);
244
- });
245
- snapshot
246
- .command("get <id>")
247
- .description("Get snapshot details")
248
- .option("-o, --output [format]", "Output format: text|json|yaml (default: json)")
249
- .action(async (id, options) => {
250
- const { getSnapshot } = await import("./commands/snapshot/get.js");
251
- await getSnapshot({ id, ...options });
252
- });
253
- snapshot
254
- .command("status <snapshot-id>")
255
- .description("Get snapshot operation status")
256
- .option("-o, --output [format]", "Output format: text|json|yaml (default: text)")
257
- .action(async (snapshotId, options) => {
258
- const { getSnapshotStatus } = await import("./commands/snapshot/status.js");
259
- await getSnapshotStatus({ snapshotId, ...options });
260
- });
261
- // Blueprint commands
262
- const blueprint = program
263
- .command("blueprint")
264
- .description("Manage blueprints")
265
- .alias("bp");
266
- blueprint
267
- .command("list")
268
- .description("List all blueprints")
269
- .option("-n, --name <name>", "Filter by blueprint name")
270
- .option("-o, --output [format]", "Output format: text|json|yaml (default: json)")
271
- .action(async (options) => {
272
- const { listBlueprints } = await import("./commands/blueprint/list.js");
273
- await listBlueprints(options);
274
- });
275
- blueprint
276
- .command("create")
277
- .description("Create a new blueprint")
278
- .requiredOption("--name <name>", "Blueprint name (required)")
279
- .option("--dockerfile <content>", "Dockerfile contents")
280
- .option("--dockerfile-path <path>", "Dockerfile path")
281
- .option("--system-setup-commands <commands...>", "System setup commands")
282
- .option("--resources <size>", "Resource size (X_SMALL, SMALL, MEDIUM, LARGE, X_LARGE, XX_LARGE)")
283
- .option("--architecture <arch>", "Architecture (arm64, x86_64)")
284
- .option("--available-ports <ports...>", "Available ports")
285
- .option("--root", "Run as root")
286
- .option("--user <user:uid>", "Run as this user (format: username:uid)")
287
- .option("-o, --output [format]", "Output format: text|json|yaml (default: json)")
288
- .action(async (options) => {
289
- const { createBlueprint } = await import("./commands/blueprint/create.js");
290
- await createBlueprint(options);
291
- });
292
- blueprint
293
- .command("get <name-or-id>")
294
- .description("Get blueprint details by name or ID (IDs start with bpt_)")
295
- .option("-o, --output [format]", "Output format: text|json|yaml (default: json)")
296
- .action(async (id, options) => {
297
- const { getBlueprint } = await import("./commands/blueprint/get.js");
298
- await getBlueprint({ id, ...options });
299
- });
300
- blueprint
301
- .command("logs <name-or-id>")
302
- .description("Get blueprint build logs by name or ID (IDs start with bpt_)")
303
- .option("-o, --output [format]", "Output format: text|json|yaml (default: text)")
304
- .action(async (id, options) => {
305
- const { getBlueprintLogs } = await import("./commands/blueprint/logs.js");
306
- await getBlueprintLogs({ id, ...options });
307
- });
308
- // Object storage commands
309
- const object = program
310
- .command("object")
311
- .description("Manage object storage")
312
- .alias("obj");
313
- object
314
- .command("list")
315
- .description("List objects")
316
- .option("--limit <n>", "Max results", "20")
317
- .option("--starting-after <id>", "Starting point for pagination")
318
- .option("--name <name>", "Filter by name (partial match supported)")
319
- .option("--content-type <type>", "Filter by content type")
320
- .option("--state <state>", "Filter by state (UPLOADING, READ_ONLY, DELETED)")
321
- .option("--search <query>", "Search by object ID or name")
322
- .option("--public", "List public objects only")
323
- .option("-o, --output [format]", "Output format: text|json|yaml (default: json)")
324
- .action(async (options) => {
325
- const { listObjects } = await import("./commands/object/list.js");
326
- await listObjects(options);
327
- });
328
- object
329
- .command("get <id>")
330
- .description("Get object details")
331
- .option("-o, --output [format]", "Output format: text|json|yaml (default: text)")
332
- .action(async (id, options) => {
333
- const { getObject } = await import("./commands/object/get.js");
334
- await getObject({ id, ...options });
335
- });
336
- object
337
- .command("download <id> <path>")
338
- .description("Download object to local file")
339
- .option("--extract", "Extract downloaded archive after download")
340
- .option("--duration-seconds <seconds>", "Duration in seconds for the presigned URL validity", "3600")
341
- .option("-o, --output [format]", "Output format: text|json|yaml (default: text)")
342
- .action(async (id, path, options) => {
343
- const { downloadObject } = await import("./commands/object/download.js");
344
- await downloadObject({ id, path, ...options });
345
- });
346
- object
347
- .command("upload <path>")
348
- .description("Upload a file as an object")
349
- .option("--name <name>", "Object name (required)")
350
- .option("--content-type <type>", "Content type: unspecified|text|binary|gzip|tar|tgz")
351
- .option("--public", "Make object publicly accessible")
352
- .option("-o, --output [format]", "Output format: text|json|yaml (default: text)")
353
- .action(async (path, options) => {
354
- const { uploadObject } = await import("./commands/object/upload.js");
355
- if (!options.output) {
356
- const { runInteractiveCommand } = await import("./utils/interactiveCommand.js");
357
- await runInteractiveCommand(() => uploadObject({ path, ...options }));
358
- }
359
- else {
360
- await uploadObject({ path, ...options });
361
- }
362
- });
363
- object
364
- .command("delete <id>")
365
- .description("Delete an object (irreversible)")
366
- .option("-o, --output [format]", "Output format: text|json|yaml (default: text)")
367
- .action(async (id, options) => {
368
- const { deleteObject } = await import("./commands/object/delete.js");
369
- await deleteObject({ id, ...options });
370
- });
371
- // MCP server commands
372
- const mcp = program
373
- .command("mcp")
374
- .description("Model Context Protocol (MCP) server commands");
375
- mcp
376
- .command("start")
377
- .description("Start the MCP server")
378
- .option("--http", "Use HTTP/SSE transport instead of stdio")
379
- .option("-p, --port <port>", "Port to listen on for HTTP mode (default: 3000)", parseInt)
380
- .action(async (options) => {
381
- if (options.http) {
382
- const { startMcpHttpServer } = await import("./commands/mcp-http.js");
383
- await startMcpHttpServer(options.port);
384
- }
385
- else {
386
- const { startMcpServer } = await import("./commands/mcp.js");
387
- await startMcpServer();
388
- }
389
- });
390
- mcp
391
- .command("install")
392
- .description("Install Runloop MCP server configuration in Claude Desktop")
393
- .action(async () => {
394
- const { installMcpConfig } = await import("./commands/mcp-install.js");
395
- await installMcpConfig();
396
- });
397
- // Hidden command: 'rli mcp' without subcommand starts the server (for Claude Desktop config compatibility)
398
- program
399
- .command("mcp-server", { hidden: true })
400
- .option("--http", "Use HTTP/SSE transport instead of stdio")
401
- .option("-p, --port <port>", "Port to listen on for HTTP mode (default: 3000)", parseInt)
402
- .action(async (options) => {
403
- if (options.http) {
404
- const { startMcpHttpServer } = await import("./commands/mcp-http.js");
405
- await startMcpHttpServer(options.port);
406
- }
407
- else {
408
- const { startMcpServer } = await import("./commands/mcp.js");
409
- await startMcpServer();
410
- }
411
- });
13
+ const program = createProgram();
412
14
  // Main CLI entry point
413
15
  (async () => {
414
16
  // Initialize theme system early (before any UI rendering)
@@ -417,21 +19,7 @@ program
417
19
  // Check if API key is configured (except for mcp commands)
418
20
  const args = process.argv.slice(2);
419
21
  if (!process.env.RUNLOOP_API_KEY) {
420
- console.error(`
421
- ❌ API key not configured.
422
-
423
- To get started:
424
- 1. Go to https://platform.runloop.ai/settings and create an API key
425
- 2. Set the environment variable:
426
-
427
- export RUNLOOP_API_KEY=your_api_key_here
428
-
429
- To make it permanent, add this line to your shell config:
430
- • For zsh: echo 'export RUNLOOP_API_KEY=your_api_key_here' >> ~/.zshrc
431
- • For bash: echo 'export RUNLOOP_API_KEY=your_api_key_here' >> ~/.bashrc
432
-
433
- Then restart your terminal or run: source ~/.zshrc (or ~/.bashrc)
434
- `);
22
+ console.error(getApiKeyErrorMessage());
435
23
  processUtils.exit(1);
436
24
  return; // Ensure execution stops
437
25
  }
@@ -444,9 +444,10 @@ const ListBlueprintsUI = ({ onBack, onExit, }) => {
444
444
  return (_jsx(DevboxCreatePage, { onBack: () => {
445
445
  setShowCreateDevbox(false);
446
446
  setSelectedBlueprint(null);
447
- }, onCreate: () => {
447
+ }, onCreate: (devbox) => {
448
448
  setShowCreateDevbox(false);
449
449
  setSelectedBlueprint(null);
450
+ navigate("devbox-detail", { devboxId: devbox.id });
450
451
  }, initialBlueprintId: selectedBlueprint.id }));
451
452
  }
452
453
  // Loading state
@@ -452,8 +452,12 @@ const ListDevboxesUI = ({ status, onBack, onExit, onNavigateToDetail, }) => {
452
452
  if (showCreate) {
453
453
  return (_jsx(DevboxCreatePage, { onBack: () => {
454
454
  setShowCreate(false);
455
- }, onCreate: () => {
455
+ }, onCreate: (devbox) => {
456
456
  setShowCreate(false);
457
+ // Navigate to the newly created devbox's detail page
458
+ if (onNavigateToDetail) {
459
+ onNavigateToDetail(devbox.id);
460
+ }
457
461
  } }));
458
462
  }
459
463
  // Actions view
@@ -16,9 +16,12 @@ import { colors } from "../../utils/theme.js";
16
16
  import { useViewportHeight } from "../../hooks/useViewportHeight.js";
17
17
  import { useExitOnCtrlC } from "../../hooks/useExitOnCtrlC.js";
18
18
  import { useCursorPagination } from "../../hooks/useCursorPagination.js";
19
+ import { DevboxCreatePage } from "../../components/DevboxCreatePage.js";
20
+ import { useNavigation } from "../../store/navigationStore.js";
19
21
  const DEFAULT_PAGE_SIZE = 10;
20
22
  const ListSnapshotsUI = ({ devboxId, onBack, onExit, }) => {
21
23
  const { exit: inkExit } = useApp();
24
+ const { navigate } = useNavigation();
22
25
  const [selectedIndex, setSelectedIndex] = React.useState(0);
23
26
  const [showPopup, setShowPopup] = React.useState(false);
24
27
  const [selectedOperation, setSelectedOperation] = React.useState(0);
@@ -28,6 +31,7 @@ const ListSnapshotsUI = ({ devboxId, onBack, onExit, }) => {
28
31
  const [operationResult, setOperationResult] = React.useState(null);
29
32
  const [operationError, setOperationError] = React.useState(null);
30
33
  const [operationLoading, setOperationLoading] = React.useState(false);
34
+ const [showCreateDevbox, setShowCreateDevbox] = React.useState(false);
31
35
  // Calculate overhead for viewport height
32
36
  const overhead = 13;
33
37
  const { viewportHeight, terminalWidth } = useViewportHeight({
@@ -82,11 +86,17 @@ const ListSnapshotsUI = ({ devboxId, onBack, onExit, }) => {
82
86
  pageSize: PAGE_SIZE,
83
87
  getItemId: (snapshot) => snapshot.id,
84
88
  pollInterval: 2000,
85
- pollingEnabled: !showPopup && !executingOperation,
89
+ pollingEnabled: !showPopup && !executingOperation && !showCreateDevbox,
86
90
  deps: [devboxId, PAGE_SIZE],
87
91
  });
88
92
  // Operations for snapshots
89
93
  const operations = React.useMemo(() => [
94
+ {
95
+ key: "create_devbox",
96
+ label: "Create Devbox from Snapshot",
97
+ color: colors.success,
98
+ icon: figures.play,
99
+ },
90
100
  {
91
101
  key: "delete",
92
102
  label: "Delete Snapshot",
@@ -164,6 +174,10 @@ const ListSnapshotsUI = ({ devboxId, onBack, onExit, }) => {
164
174
  }
165
175
  return;
166
176
  }
177
+ // Handle create devbox view
178
+ if (showCreateDevbox) {
179
+ return;
180
+ }
167
181
  // Handle popup navigation
168
182
  if (showPopup) {
169
183
  if (key.upArrow && selectedOperation > 0) {
@@ -175,15 +189,27 @@ const ListSnapshotsUI = ({ devboxId, onBack, onExit, }) => {
175
189
  else if (key.return) {
176
190
  setShowPopup(false);
177
191
  const operationKey = operations[selectedOperation].key;
178
- setSelectedSnapshot(selectedSnapshotItem);
179
- setExecutingOperation(operationKey);
180
- // Execute immediately after state update
181
- setTimeout(() => executeOperation(), 0);
192
+ if (operationKey === "create_devbox") {
193
+ setSelectedSnapshot(selectedSnapshotItem);
194
+ setShowCreateDevbox(true);
195
+ }
196
+ else {
197
+ setSelectedSnapshot(selectedSnapshotItem);
198
+ setExecutingOperation(operationKey);
199
+ // Execute immediately after state update
200
+ setTimeout(() => executeOperation(), 0);
201
+ }
182
202
  }
183
203
  else if (key.escape || input === "q") {
184
204
  setShowPopup(false);
185
205
  setSelectedOperation(0);
186
206
  }
207
+ else if (input === "c") {
208
+ // Create devbox hotkey
209
+ setShowPopup(false);
210
+ setSelectedSnapshot(selectedSnapshotItem);
211
+ setShowCreateDevbox(true);
212
+ }
187
213
  else if (input === "d") {
188
214
  // Delete hotkey
189
215
  setShowPopup(false);
@@ -256,6 +282,17 @@ const ListSnapshotsUI = ({ devboxId, onBack, onExit, }) => {
256
282
  { label: operationLabel, active: true },
257
283
  ] }), _jsx(Header, { title: "Executing Operation" }), _jsx(SpinnerComponent, { message: messages[executingOperation] || "Please wait..." })] }));
258
284
  }
285
+ // Create devbox screen
286
+ if (showCreateDevbox && selectedSnapshot) {
287
+ return (_jsx(DevboxCreatePage, { onBack: () => {
288
+ setShowCreateDevbox(false);
289
+ setSelectedSnapshot(null);
290
+ }, onCreate: (devbox) => {
291
+ setShowCreateDevbox(false);
292
+ setSelectedSnapshot(null);
293
+ navigate("devbox-detail", { devboxId: devbox.id });
294
+ }, initialSnapshotId: selectedSnapshot.id }));
295
+ }
259
296
  // Loading state
260
297
  if (loading && snapshots.length === 0) {
261
298
  return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
@@ -292,7 +329,11 @@ const ListSnapshotsUI = ({ devboxId, onBack, onExit, }) => {
292
329
  label: op.label,
293
330
  color: op.color,
294
331
  icon: op.icon,
295
- shortcut: op.key === "delete" ? "d" : "",
332
+ shortcut: op.key === "create_devbox"
333
+ ? "c"
334
+ : op.key === "delete"
335
+ ? "d"
336
+ : "",
296
337
  })), selectedOperation: selectedOperation, onClose: () => setShowPopup(false) }) })), _jsxs(Box, { marginTop: 1, paddingX: 1, children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [figures.arrowUp, figures.arrowDown, " Navigate"] }), (hasMore || hasPrev) && (_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022 ", figures.arrowLeft, figures.arrowRight, " Page"] })), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022 [a] Actions"] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022 [Esc] Back"] })] })] }));
297
338
  };
298
339
  // Export the UI component for use in the main menu
@@ -6,12 +6,12 @@ import { UpdateNotification } from "./UpdateNotification.js";
6
6
  export const Breadcrumb = ({ items, showVersionCheck = false, }) => {
7
7
  const env = process.env.RUNLOOP_ENV?.toLowerCase();
8
8
  const isDevEnvironment = env === "dev";
9
- return (_jsxs(Box, { marginBottom: 1, paddingX: 0, paddingY: 0, children: [_jsxs(Box, { borderStyle: "round", borderColor: colors.primary, paddingX: 2, paddingY: 0, children: [_jsx(Text, { color: colors.primary, bold: true, children: "rl" }), isDevEnvironment && (_jsxs(Text, { color: colors.error, bold: true, children: [" ", "(dev)"] })), _jsx(Text, { color: colors.textDim, children: " \u203A " }), items.map((item, index) => {
10
- // Limit label length to prevent Yoga layout engine errors
11
- const MAX_LABEL_LENGTH = 80;
12
- const truncatedLabel = item.label.length > MAX_LABEL_LENGTH
13
- ? item.label.substring(0, MAX_LABEL_LENGTH) + "..."
14
- : item.label;
15
- return (_jsxs(React.Fragment, { children: [_jsx(Text, { color: item.active ? colors.primary : colors.textDim, children: truncatedLabel }), index < items.length - 1 && (_jsx(Text, { color: colors.textDim, children: " \u203A " }))] }, index));
16
- })] }), showVersionCheck && (_jsx(Box, { paddingX: 2, marginTop: 0, children: _jsx(UpdateNotification, {}) }))] }));
9
+ return (_jsxs(Box, { justifyContent: "space-between", marginBottom: 1, paddingX: 0, paddingY: 0, children: [_jsx(Box, { flexShrink: 0, children: _jsxs(Box, { borderStyle: "round", borderColor: colors.primary, paddingX: 2, paddingY: 0, children: [_jsx(Text, { color: colors.primary, bold: true, children: "rl" }), isDevEnvironment && (_jsxs(Text, { color: colors.error, bold: true, children: [" ", "(dev)"] })), _jsx(Text, { color: colors.textDim, children: " \u203A " }), items.map((item, index) => {
10
+ // Limit label length to prevent Yoga layout engine errors
11
+ const MAX_LABEL_LENGTH = 80;
12
+ const truncatedLabel = item.label.length > MAX_LABEL_LENGTH
13
+ ? item.label.substring(0, MAX_LABEL_LENGTH) + "..."
14
+ : item.label;
15
+ return (_jsxs(React.Fragment, { children: [_jsx(Text, { color: item.active ? colors.primary : colors.textDim, children: truncatedLabel }), index < items.length - 1 && (_jsx(Text, { color: colors.textDim, children: " \u203A " }))] }, index));
16
+ })] }) }), showVersionCheck && _jsx(UpdateNotification, {})] }));
17
17
  };
@@ -11,7 +11,7 @@ import { Breadcrumb } from "./Breadcrumb.js";
11
11
  import { MetadataDisplay } from "./MetadataDisplay.js";
12
12
  import { colors } from "../utils/theme.js";
13
13
  import { useExitOnCtrlC } from "../hooks/useExitOnCtrlC.js";
14
- export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, }) => {
14
+ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initialSnapshotId, }) => {
15
15
  const [currentField, setCurrentField] = React.useState("create");
16
16
  const [formData, setFormData] = React.useState({
17
17
  name: "",
@@ -23,7 +23,7 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, }) => {
23
23
  keep_alive: "3600",
24
24
  metadata: {},
25
25
  blueprint_id: initialBlueprintId || "",
26
- snapshot_id: "",
26
+ snapshot_id: initialSnapshotId || "",
27
27
  });
28
28
  const [metadataKey, setMetadataKey] = React.useState("");
29
29
  const [metadataValue, setMetadataValue] = React.useState("");
@@ -7,6 +7,7 @@ import { StatusBadge } from "./StatusBadge.js";
7
7
  import { MetadataDisplay } from "./MetadataDisplay.js";
8
8
  import { Breadcrumb } from "./Breadcrumb.js";
9
9
  import { DevboxActionsMenu } from "./DevboxActionsMenu.js";
10
+ import { StateHistory } from "./StateHistory.js";
10
11
  import { getDevboxUrl } from "../utils/url.js";
11
12
  import { colors } from "../utils/theme.js";
12
13
  import { useViewportHeight } from "../hooks/useViewportHeight.js";
@@ -433,7 +434,7 @@ export const DevboxDetailPage = ({ devbox: initialDevbox, onBack, }) => {
433
434
  lp?.architecture) && (_jsxs(Box, { flexDirection: "column", paddingX: 1, flexGrow: 1, children: [_jsxs(Text, { color: colors.warning, bold: true, children: [figures.squareSmallFilled, " Resources"] }), _jsxs(Text, { dimColor: true, children: [lp?.resource_size_request && `${lp.resource_size_request}`, lp?.architecture && ` • ${lp.architecture}`, lp?.custom_cpu_cores && ` • ${lp.custom_cpu_cores}VCPU`, lp?.custom_gb_memory && ` • ${lp.custom_gb_memory}GB RAM`, lp?.custom_disk_size && ` • ${lp.custom_disk_size}GB DISC`] })] })), hasCapabilities && (_jsxs(Box, { flexDirection: "column", paddingX: 1, flexGrow: 1, children: [_jsxs(Text, { color: colors.info, bold: true, children: [figures.tick, " Capabilities"] }), _jsx(Text, { dimColor: true, children: selectedDevbox.capabilities
434
435
  .filter((c) => c !== "unknown")
435
436
  .join(", ") })] })), (selectedDevbox.blueprint_id || selectedDevbox.snapshot_id) && (_jsxs(Box, { flexDirection: "column", paddingX: 1, flexGrow: 1, children: [_jsxs(Text, { color: colors.secondary, bold: true, children: [figures.circleFilled, " Source"] }), selectedDevbox.blueprint_id && (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: "BP: " }), _jsx(Text, { color: colors.idColor, children: selectedDevbox.blueprint_id })] })), selectedDevbox.snapshot_id && (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: "Snap: " }), _jsx(Text, { color: colors.idColor, children: selectedDevbox.snapshot_id })] }))] }))] }), selectedDevbox.metadata &&
436
- Object.keys(selectedDevbox.metadata).length > 0 && (_jsx(Box, { marginBottom: 1, paddingX: 1, children: _jsx(MetadataDisplay, { metadata: selectedDevbox.metadata, showBorder: false }) })), selectedDevbox.failure_reason && (_jsxs(Box, { marginBottom: 1, paddingX: 1, children: [_jsxs(Text, { color: colors.error, bold: true, children: [figures.cross, " "] }), _jsx(Text, { color: colors.error, dimColor: true, children: selectedDevbox.failure_reason })] })), _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { color: colors.primary, bold: true, children: [figures.play, " Actions"] }), _jsx(Box, { flexDirection: "column", children: operations.map((op, index) => {
437
+ Object.keys(selectedDevbox.metadata).length > 0 && (_jsx(Box, { marginBottom: 1, paddingX: 1, children: _jsx(MetadataDisplay, { metadata: selectedDevbox.metadata, showBorder: false }) })), selectedDevbox.failure_reason && (_jsxs(Box, { marginBottom: 1, paddingX: 1, children: [_jsxs(Text, { color: colors.error, bold: true, children: [figures.cross, " "] }), _jsx(Text, { color: colors.error, dimColor: true, children: selectedDevbox.failure_reason })] })), _jsx(StateHistory, { stateTransitions: selectedDevbox.state_transitions }), _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { color: colors.primary, bold: true, children: [figures.play, " Actions"] }), _jsx(Box, { flexDirection: "column", children: operations.map((op, index) => {
437
438
  const isSelected = index === selectedOperation;
438
439
  return (_jsxs(Box, { children: [_jsxs(Text, { color: isSelected ? colors.primary : colors.textDim, children: [isSelected ? figures.pointer : " ", " "] }), _jsxs(Text, { color: isSelected ? op.color : colors.textDim, bold: isSelected, children: [op.icon, " ", op.label] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "[", op.shortcut, "]"] })] }, op.key));
439
440
  }) })] }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: colors.textDim, dimColor: true, children: [figures.arrowUp, figures.arrowDown, " Navigate \u2022 [Enter] Execute \u2022 [i] Full Details \u2022 [o] Browser \u2022 [q] Back"] }) })] }));