@runloop/rl-cli 1.7.1 → 1.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.
Files changed (73) hide show
  1. package/README.md +19 -5
  2. package/dist/cli.js +0 -0
  3. package/dist/commands/blueprint/delete.js +21 -0
  4. package/dist/commands/blueprint/list.js +226 -174
  5. package/dist/commands/blueprint/prune.js +13 -28
  6. package/dist/commands/devbox/create.js +41 -0
  7. package/dist/commands/devbox/list.js +125 -109
  8. package/dist/commands/devbox/tunnel.js +4 -19
  9. package/dist/commands/gateway-config/create.js +44 -0
  10. package/dist/commands/gateway-config/delete.js +21 -0
  11. package/dist/commands/gateway-config/get.js +15 -0
  12. package/dist/commands/gateway-config/list.js +493 -0
  13. package/dist/commands/gateway-config/update.js +60 -0
  14. package/dist/commands/menu.js +2 -1
  15. package/dist/commands/secret/list.js +379 -4
  16. package/dist/commands/snapshot/list.js +11 -2
  17. package/dist/commands/snapshot/prune.js +265 -0
  18. package/dist/components/BenchmarkMenu.js +108 -0
  19. package/dist/components/DetailedInfoView.js +20 -0
  20. package/dist/components/DevboxActionsMenu.js +9 -61
  21. package/dist/components/DevboxCreatePage.js +531 -14
  22. package/dist/components/DevboxDetailPage.js +27 -22
  23. package/dist/components/GatewayConfigCreatePage.js +265 -0
  24. package/dist/components/LogsViewer.js +6 -40
  25. package/dist/components/MainMenu.js +63 -22
  26. package/dist/components/ResourceDetailPage.js +143 -160
  27. package/dist/components/ResourceListView.js +3 -33
  28. package/dist/components/ResourcePicker.js +220 -0
  29. package/dist/components/SecretCreatePage.js +183 -0
  30. package/dist/components/SettingsMenu.js +95 -0
  31. package/dist/components/StateHistory.js +1 -20
  32. package/dist/components/StatusBadge.js +80 -0
  33. package/dist/components/StreamingLogsViewer.js +8 -42
  34. package/dist/components/form/FormTextInput.js +4 -2
  35. package/dist/components/resourceDetailTypes.js +18 -0
  36. package/dist/hooks/useInputHandler.js +103 -0
  37. package/dist/router/Router.js +99 -2
  38. package/dist/screens/BenchmarkDetailScreen.js +163 -0
  39. package/dist/screens/BenchmarkJobCreateScreen.js +524 -0
  40. package/dist/screens/BenchmarkJobDetailScreen.js +614 -0
  41. package/dist/screens/BenchmarkJobListScreen.js +479 -0
  42. package/dist/screens/BenchmarkListScreen.js +266 -0
  43. package/dist/screens/BenchmarkMenuScreen.js +29 -0
  44. package/dist/screens/BenchmarkRunDetailScreen.js +425 -0
  45. package/dist/screens/BenchmarkRunListScreen.js +275 -0
  46. package/dist/screens/BlueprintDetailScreen.js +5 -1
  47. package/dist/screens/DevboxCreateScreen.js +2 -2
  48. package/dist/screens/GatewayConfigDetailScreen.js +236 -0
  49. package/dist/screens/GatewayConfigListScreen.js +7 -0
  50. package/dist/screens/MenuScreen.js +5 -2
  51. package/dist/screens/ScenarioRunDetailScreen.js +226 -0
  52. package/dist/screens/ScenarioRunListScreen.js +245 -0
  53. package/dist/screens/SecretCreateScreen.js +7 -0
  54. package/dist/screens/SecretDetailScreen.js +198 -0
  55. package/dist/screens/SecretListScreen.js +7 -0
  56. package/dist/screens/SettingsMenuScreen.js +26 -0
  57. package/dist/screens/SnapshotDetailScreen.js +6 -0
  58. package/dist/services/agentService.js +42 -0
  59. package/dist/services/benchmarkJobService.js +122 -0
  60. package/dist/services/benchmarkService.js +120 -0
  61. package/dist/services/gatewayConfigService.js +114 -0
  62. package/dist/services/scenarioService.js +34 -0
  63. package/dist/store/benchmarkJobStore.js +66 -0
  64. package/dist/store/benchmarkStore.js +183 -0
  65. package/dist/store/betaFeatureStore.js +47 -0
  66. package/dist/store/gatewayConfigStore.js +83 -0
  67. package/dist/store/index.js +1 -0
  68. package/dist/utils/browser.js +22 -0
  69. package/dist/utils/clipboard.js +41 -0
  70. package/dist/utils/commands.js +80 -0
  71. package/dist/utils/config.js +8 -0
  72. package/dist/utils/time.js +121 -0
  73. package/package.json +42 -43
package/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  A **TUI + CLI** for the [Runloop.ai](https://runloop.ai) platform. Use it as an **interactive TUI** (Terminal User Interface) with rich UI components, or as a **traditional CLI** for scripting and automation.
8
8
 
9
- 📖 **[Full Documentation](https://docs.runloop.ai/docs/tools/cli)**
9
+ 📖 **[Full Documentation](https://docs.runloop.ai/docs/tools/rl-cli)**
10
10
 
11
11
  <p align="center">
12
12
  <img src="https://raw.githubusercontent.com/runloopai/rl-cli/main/misc/demo.gif" alt="Runloop CLI Demo" width="800">
@@ -36,10 +36,12 @@ rli devbox delete <devbox-id>
36
36
 
37
37
  ## Installation
38
38
 
39
- Install globally via npm:
39
+ Install globally via npm or pnpm:
40
40
 
41
41
  ```bash
42
42
  npm install -g @runloop/rl-cli
43
+ # or
44
+ pnpm add -g @runloop/rl-cli
43
45
  ```
44
46
 
45
47
  ## Setup
@@ -107,6 +109,7 @@ rli snapshot list # List all snapshots
107
109
  rli snapshot create <devbox-id> # Create a snapshot of a devbox
108
110
  rli snapshot delete <id> # Delete a snapshot
109
111
  rli snapshot get <id> # Get snapshot details
112
+ rli snapshot prune <devbox-id> # Delete old snapshots for a devbox, ke...
110
113
  rli snapshot status <snapshot-id> # Get snapshot operation status
111
114
  ```
112
115
 
@@ -117,6 +120,7 @@ rli blueprint list # List all blueprints
117
120
  rli blueprint create # Create a new blueprint
118
121
  rli blueprint get <name-or-id> # Get blueprint details by name or ID (...
119
122
  rli blueprint logs <name-or-id> # Get blueprint build logs by name or I...
123
+ rli blueprint delete <id> # Delete a blueprint by ID
120
124
  rli blueprint prune <name> # Delete old blueprint builds, keeping ...
121
125
  rli blueprint from-dockerfile # Create a blueprint from a Dockerfile ...
122
126
  ```
@@ -150,6 +154,16 @@ rli secret update <name> # Update a secret value (value from std
150
154
  rli secret delete <name> # Delete a secret
151
155
  ```
152
156
 
157
+ ### Gateway-config Commands (alias: `gwc`)
158
+
159
+ ```bash
160
+ rli gateway-config list # List gateway configurations
161
+ rli gateway-config create # Create a new gateway configuration
162
+ rli gateway-config get <id> # Get gateway configuration details
163
+ rli gateway-config update <id> # Update a gateway configuration
164
+ rli gateway-config delete <id> # Delete a gateway configuration
165
+ ```
166
+
153
167
  ### Mcp Commands
154
168
 
155
169
  ```bash
@@ -197,13 +211,13 @@ The TUI supports both light and dark terminal themes and will automatically sele
197
211
 
198
212
  ```bash
199
213
  # Install dependencies
200
- npm install
214
+ pnpm install
201
215
 
202
216
  # Build
203
- npm run build
217
+ pnpm run build
204
218
 
205
219
  # Watch mode
206
- npm run dev
220
+ pnpm run dev
207
221
 
208
222
  ## Contributing
209
223
 
package/dist/cli.js CHANGED
File without changes
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Delete blueprint command
3
+ */
4
+ import { getClient } from "../../utils/client.js";
5
+ import { output, outputError } from "../../utils/output.js";
6
+ export async function deleteBlueprint(id, options = {}) {
7
+ try {
8
+ const client = getClient();
9
+ await client.blueprints.delete(id);
10
+ // Default: just output the ID for easy scripting
11
+ if (!options.output || options.output === "text") {
12
+ console.log(id);
13
+ }
14
+ else {
15
+ output({ id, status: "deleted" }, { format: options.output, defaultFormat: "json" });
16
+ }
17
+ }
18
+ catch (error) {
19
+ outputError("Failed to delete blueprint", error);
20
+ }
21
+ }
@@ -1,6 +1,6 @@
1
1
  import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import React from "react";
3
- import { Box, Text, useInput, useApp } from "ink";
3
+ import { Box, Text, useApp } from "ink";
4
4
  import TextInput from "ink-text-input";
5
5
  import figures from "figures";
6
6
  import { getClient } from "../../utils/client.js";
@@ -23,6 +23,8 @@ import { useExitOnCtrlC } from "../../hooks/useExitOnCtrlC.js";
23
23
  import { useViewportHeight } from "../../hooks/useViewportHeight.js";
24
24
  import { useCursorPagination } from "../../hooks/useCursorPagination.js";
25
25
  import { useListSearch } from "../../hooks/useListSearch.js";
26
+ import { openInBrowser } from "../../utils/browser.js";
27
+ import { useInputHandler, } from "../../hooks/useInputHandler.js";
26
28
  import { useNavigation } from "../../store/navigationStore.js";
27
29
  import { ConfirmationPrompt } from "../../components/ConfirmationPrompt.js";
28
30
  const DEFAULT_PAGE_SIZE = 10;
@@ -281,192 +283,242 @@ const ListBlueprintsUI = ({ onBack, onExit, }) => {
281
283
  return true;
282
284
  })
283
285
  : allOperations;
284
- // Handle input for all views
285
- useInput((input, key) => {
286
- // Handle search mode input
287
- if (search.searchMode) {
288
- if (key.escape) {
289
- search.cancelSearch();
290
- }
291
- return;
292
- }
293
- // Handle operation input mode
294
- if (executingOperation && !operationResult && !operationError) {
295
- // Allow escape/q to cancel any operation, even during loading
296
- if (input === "q" || key.escape) {
297
- setExecutingOperation(null);
298
- setOperationInput("");
299
- setOperationLoading(false);
300
- return;
301
- }
302
- const currentOp = allOperations.find((op) => op.key === executingOperation);
303
- if (currentOp?.needsInput) {
304
- if (key.return) {
305
- executeOperation();
306
- }
307
- }
308
- return;
309
- }
310
- // Handle operation result display
311
- if (operationResult || operationError) {
312
- if (input === "q" || key.escape || key.return) {
313
- setOperationResult(null);
314
- setOperationError(null);
315
- setExecutingOperation(null);
316
- setOperationInput("");
317
- }
318
- return;
319
- }
320
- // Handle create devbox view
321
- if (showCreateDevbox) {
322
- return;
286
+ // --- Callbacks for input modes ---
287
+ const cancelOperation = React.useCallback(() => {
288
+ setExecutingOperation(null);
289
+ setOperationInput("");
290
+ setOperationLoading(false);
291
+ }, []);
292
+ const dismissResult = React.useCallback(() => {
293
+ setOperationResult(null);
294
+ setOperationError(null);
295
+ setExecutingOperation(null);
296
+ setOperationInput("");
297
+ }, []);
298
+ const closePopup = React.useCallback(() => {
299
+ setShowPopup(false);
300
+ setSelectedOperation(0);
301
+ }, []);
302
+ const executePopupSelection = React.useCallback(() => {
303
+ setShowPopup(false);
304
+ const operationKey = allOperations[selectedOperation].key;
305
+ if (operationKey === "view_details") {
306
+ navigate("blueprint-detail", {
307
+ blueprintId: selectedBlueprintItem.id,
308
+ });
323
309
  }
324
- // Handle actions popup overlay
325
- if (showPopup) {
326
- if (key.upArrow && selectedOperation > 0) {
327
- setSelectedOperation(selectedOperation - 1);
328
- }
329
- else if (key.downArrow &&
330
- selectedOperation < allOperations.length - 1) {
331
- setSelectedOperation(selectedOperation + 1);
332
- }
333
- else if (key.return) {
334
- setShowPopup(false);
335
- const operationKey = allOperations[selectedOperation].key;
336
- if (operationKey === "view_details") {
337
- navigate("blueprint-detail", {
338
- blueprintId: selectedBlueprintItem.id,
339
- });
340
- }
341
- else if (operationKey === "create_devbox") {
342
- setSelectedBlueprint(selectedBlueprintItem);
343
- setShowCreateDevbox(true);
344
- }
345
- else if (operationKey === "delete") {
346
- // Show delete confirmation
347
- setSelectedBlueprint(selectedBlueprintItem);
348
- setShowDeleteConfirm(true);
349
- }
350
- else {
351
- setSelectedBlueprint(selectedBlueprintItem);
352
- setExecutingOperation(operationKey);
353
- executeOperation(selectedBlueprintItem, operationKey);
354
- }
355
- }
356
- else if (input === "v" && selectedBlueprintItem) {
357
- // View details hotkey
358
- setShowPopup(false);
359
- navigate("blueprint-detail", {
360
- blueprintId: selectedBlueprintItem.id,
361
- });
362
- }
363
- else if (key.escape || input === "q") {
364
- setShowPopup(false);
365
- setSelectedOperation(0);
366
- }
367
- else if (input === "c") {
368
- if (selectedBlueprintItem &&
369
- (selectedBlueprintItem.status === "build_complete" ||
370
- selectedBlueprintItem.status === "building_complete")) {
371
- setShowPopup(false);
372
- setSelectedBlueprint(selectedBlueprintItem);
373
- setShowCreateDevbox(true);
374
- }
375
- }
376
- else if (input === "d") {
377
- const deleteIndex = allOperations.findIndex((op) => op.key === "delete");
378
- if (deleteIndex >= 0) {
379
- // Show delete confirmation
380
- setShowPopup(false);
381
- setSelectedBlueprint(selectedBlueprintItem);
382
- setShowDeleteConfirm(true);
383
- }
384
- }
385
- else if (input === "l") {
386
- const logsIndex = allOperations.findIndex((op) => op.key === "view_logs");
387
- if (logsIndex >= 0) {
388
- setShowPopup(false);
389
- setSelectedBlueprint(selectedBlueprintItem);
390
- setExecutingOperation("view_logs");
391
- executeOperation(selectedBlueprintItem, "view_logs");
392
- }
393
- }
394
- return;
310
+ else if (operationKey === "create_devbox") {
311
+ setSelectedBlueprint(selectedBlueprintItem);
312
+ setShowCreateDevbox(true);
395
313
  }
396
- // Handle list navigation
397
- const pageBlueprints = blueprints.length;
398
- if (key.upArrow && selectedIndex > 0) {
399
- setSelectedIndex(selectedIndex - 1);
314
+ else if (operationKey === "delete") {
315
+ setSelectedBlueprint(selectedBlueprintItem);
316
+ setShowDeleteConfirm(true);
400
317
  }
401
- else if (key.downArrow && selectedIndex < pageBlueprints - 1) {
402
- setSelectedIndex(selectedIndex + 1);
318
+ else {
319
+ setSelectedBlueprint(selectedBlueprintItem);
320
+ setExecutingOperation(operationKey);
321
+ executeOperation(selectedBlueprintItem, operationKey);
403
322
  }
404
- else if ((input === "n" || key.rightArrow) &&
405
- !loading &&
406
- !navigating &&
407
- hasMore) {
323
+ }, [
324
+ allOperations,
325
+ selectedOperation,
326
+ selectedBlueprintItem,
327
+ navigate,
328
+ executeOperation,
329
+ ]);
330
+ const goToNextPage = React.useCallback(() => {
331
+ if (!loading && !navigating && hasMore) {
408
332
  nextPage();
409
333
  setSelectedIndex(0);
410
334
  }
411
- else if ((input === "p" || key.leftArrow) &&
412
- !loading &&
413
- !navigating &&
414
- hasPrev) {
335
+ }, [loading, navigating, hasMore, nextPage]);
336
+ const goToPrevPage = React.useCallback(() => {
337
+ if (!loading && !navigating && hasPrev) {
415
338
  prevPage();
416
339
  setSelectedIndex(0);
417
340
  }
418
- else if (key.return && selectedBlueprintItem) {
419
- // Enter key navigates to detail view
420
- navigate("blueprint-detail", {
421
- blueprintId: selectedBlueprintItem.id,
422
- });
423
- }
424
- else if (input === "a") {
425
- setShowPopup(true);
426
- setSelectedOperation(0);
427
- }
428
- else if (input === "l" && selectedBlueprintItem) {
429
- setSelectedBlueprint(selectedBlueprintItem);
430
- setExecutingOperation("view_logs");
431
- executeOperation(selectedBlueprintItem, "view_logs");
341
+ }, [loading, navigating, hasPrev, prevPage]);
342
+ const handleListEscape = React.useCallback(() => {
343
+ if (search.handleEscape())
344
+ return;
345
+ if (onBack) {
346
+ onBack();
432
347
  }
433
- else if (input === "o" && blueprints[selectedIndex]) {
434
- const url = getBlueprintUrl(blueprints[selectedIndex].id);
435
- const openBrowser = async () => {
436
- const { exec } = await import("child_process");
437
- const platform = process.platform;
438
- let openCommand;
439
- if (platform === "darwin") {
440
- openCommand = `open "${url}"`;
441
- }
442
- else if (platform === "win32") {
443
- openCommand = `start "${url}"`;
444
- }
445
- else {
446
- openCommand = `xdg-open "${url}"`;
447
- }
448
- exec(openCommand);
449
- };
450
- openBrowser();
348
+ else if (onExit) {
349
+ onExit();
451
350
  }
452
- else if (input === "/") {
453
- search.enterSearchMode();
351
+ else {
352
+ inkExit();
454
353
  }
455
- else if (key.escape) {
456
- if (search.handleEscape()) {
457
- return;
458
- }
459
- if (onBack) {
460
- onBack();
461
- }
462
- else if (onExit) {
463
- onExit();
464
- }
465
- else {
466
- inkExit();
467
- }
468
- }
469
- });
354
+ }, [search, onBack, onExit, inkExit]);
355
+ const handleOpenInBrowser = React.useCallback(() => {
356
+ const bp = blueprints[selectedIndex];
357
+ if (!bp)
358
+ return;
359
+ openInBrowser(getBlueprintUrl(bp.id));
360
+ }, [blueprints, selectedIndex]);
361
+ // --- Declarative input modes ---
362
+ const inputModes = React.useMemo(() => [
363
+ // Search mode: only escape to cancel, swallow everything else
364
+ {
365
+ name: "search",
366
+ active: () => search.searchMode,
367
+ bindings: {
368
+ escape: () => search.cancelSearch(),
369
+ },
370
+ captureAll: true,
371
+ },
372
+ // Operation input mode: escape/q to cancel, enter to submit
373
+ {
374
+ name: "operationInput",
375
+ active: () => !!executingOperation && !operationResult && !operationError,
376
+ bindings: {
377
+ q: cancelOperation,
378
+ escape: cancelOperation,
379
+ enter: () => {
380
+ const currentOp = allOperations.find((op) => op.key === executingOperation);
381
+ if (currentOp?.needsInput) {
382
+ executeOperation();
383
+ }
384
+ },
385
+ },
386
+ captureAll: true,
387
+ },
388
+ // Operation result display: any dismiss key
389
+ {
390
+ name: "operationResult",
391
+ active: () => !!operationResult || !!operationError,
392
+ bindings: {
393
+ q: dismissResult,
394
+ escape: dismissResult,
395
+ enter: dismissResult,
396
+ },
397
+ captureAll: true,
398
+ },
399
+ // Create devbox subview: swallow all input
400
+ {
401
+ name: "createDevbox",
402
+ active: () => showCreateDevbox,
403
+ bindings: {},
404
+ captureAll: true,
405
+ },
406
+ // Actions popup overlay
407
+ {
408
+ name: "popup",
409
+ active: () => showPopup,
410
+ bindings: {
411
+ up: () => {
412
+ if (selectedOperation > 0)
413
+ setSelectedOperation(selectedOperation - 1);
414
+ },
415
+ down: () => {
416
+ if (selectedOperation < allOperations.length - 1)
417
+ setSelectedOperation(selectedOperation + 1);
418
+ },
419
+ enter: executePopupSelection,
420
+ escape: closePopup,
421
+ q: closePopup,
422
+ v: () => {
423
+ if (selectedBlueprintItem) {
424
+ setShowPopup(false);
425
+ navigate("blueprint-detail", {
426
+ blueprintId: selectedBlueprintItem.id,
427
+ });
428
+ }
429
+ },
430
+ c: () => {
431
+ if (selectedBlueprintItem &&
432
+ (selectedBlueprintItem.status === "build_complete" ||
433
+ selectedBlueprintItem.status === "building_complete")) {
434
+ setShowPopup(false);
435
+ setSelectedBlueprint(selectedBlueprintItem);
436
+ setShowCreateDevbox(true);
437
+ }
438
+ },
439
+ d: () => {
440
+ const deleteIndex = allOperations.findIndex((op) => op.key === "delete");
441
+ if (deleteIndex >= 0) {
442
+ setShowPopup(false);
443
+ setSelectedBlueprint(selectedBlueprintItem);
444
+ setShowDeleteConfirm(true);
445
+ }
446
+ },
447
+ l: () => {
448
+ const logsIndex = allOperations.findIndex((op) => op.key === "view_logs");
449
+ if (logsIndex >= 0) {
450
+ setShowPopup(false);
451
+ setSelectedBlueprint(selectedBlueprintItem);
452
+ setExecutingOperation("view_logs");
453
+ executeOperation(selectedBlueprintItem, "view_logs");
454
+ }
455
+ },
456
+ },
457
+ },
458
+ // List navigation (default mode)
459
+ {
460
+ name: "list",
461
+ active: () => true,
462
+ bindings: {
463
+ up: () => {
464
+ if (selectedIndex > 0)
465
+ setSelectedIndex(selectedIndex - 1);
466
+ },
467
+ down: () => {
468
+ if (selectedIndex < blueprints.length - 1)
469
+ setSelectedIndex(selectedIndex + 1);
470
+ },
471
+ n: goToNextPage,
472
+ right: goToNextPage,
473
+ p: goToPrevPage,
474
+ left: goToPrevPage,
475
+ enter: () => {
476
+ if (selectedBlueprintItem) {
477
+ navigate("blueprint-detail", {
478
+ blueprintId: selectedBlueprintItem.id,
479
+ });
480
+ }
481
+ },
482
+ a: () => {
483
+ setShowPopup(true);
484
+ setSelectedOperation(0);
485
+ },
486
+ l: () => {
487
+ if (selectedBlueprintItem) {
488
+ setSelectedBlueprint(selectedBlueprintItem);
489
+ setExecutingOperation("view_logs");
490
+ executeOperation(selectedBlueprintItem, "view_logs");
491
+ }
492
+ },
493
+ o: handleOpenInBrowser,
494
+ "/": () => search.enterSearchMode(),
495
+ escape: handleListEscape,
496
+ },
497
+ },
498
+ ], [
499
+ search,
500
+ executingOperation,
501
+ operationResult,
502
+ operationError,
503
+ cancelOperation,
504
+ allOperations,
505
+ executeOperation,
506
+ dismissResult,
507
+ showCreateDevbox,
508
+ showPopup,
509
+ selectedOperation,
510
+ executePopupSelection,
511
+ closePopup,
512
+ selectedBlueprintItem,
513
+ navigate,
514
+ selectedIndex,
515
+ blueprints.length,
516
+ goToNextPage,
517
+ goToPrevPage,
518
+ handleOpenInBrowser,
519
+ handleListEscape,
520
+ ]);
521
+ useInputHandler(inputModes);
470
522
  // Delete confirmation
471
523
  if (showDeleteConfirm && selectedBlueprint) {
472
524
  return (_jsx(ConfirmationPrompt, { title: "Delete Blueprint", message: `Are you sure you want to delete "${selectedBlueprint.name || selectedBlueprint.id}"?`, details: "This action cannot be undone.", breadcrumbItems: [
@@ -4,6 +4,7 @@
4
4
  import * as readline from "readline";
5
5
  import { getClient } from "../../utils/client.js";
6
6
  import { output, outputError } from "../../utils/output.js";
7
+ import { formatRelativeTime } from "../../utils/time.js";
7
8
  /**
8
9
  * Fetch all blueprints with a given name (handles pagination)
9
10
  */
@@ -48,6 +49,7 @@ function categorizeBlueprints(blueprints, keepCount) {
48
49
  // Sort successful by create_time_ms descending (newest first)
49
50
  successful.sort((a, b) => (b.create_time_ms || 0) - (a.create_time_ms || 0));
50
51
  // Determine what to keep and delete
52
+ // keepCount of 0 means delete all (including successful builds)
51
53
  const toKeep = successful.slice(0, keepCount);
52
54
  const toDelete = [...successful.slice(keepCount), ...failed];
53
55
  return {
@@ -57,28 +59,6 @@ function categorizeBlueprints(blueprints, keepCount) {
57
59
  failed,
58
60
  };
59
61
  }
60
- /**
61
- * Format a timestamp for display
62
- */
63
- function formatTimestamp(createTimeMs) {
64
- if (!createTimeMs) {
65
- return "unknown time";
66
- }
67
- const now = Date.now();
68
- const diffMs = now - createTimeMs;
69
- const diffMinutes = Math.floor(diffMs / 60000);
70
- const diffHours = Math.floor(diffMs / 3600000);
71
- const diffDays = Math.floor(diffMs / 86400000);
72
- if (diffMinutes < 60) {
73
- return `${diffMinutes} minute${diffMinutes !== 1 ? "s" : ""} ago`;
74
- }
75
- else if (diffHours < 24) {
76
- return `${diffHours} hour${diffHours !== 1 ? "s" : ""} ago`;
77
- }
78
- else {
79
- return `${diffDays} day${diffDays !== 1 ? "s" : ""} ago`;
80
- }
81
- }
82
62
  /**
83
63
  * Display a summary of what will be kept and deleted
84
64
  */
@@ -91,11 +71,16 @@ function displaySummary(name, result, isDryRun) {
91
71
  // Show what will be kept
92
72
  console.log(`\nKeeping (${result.toKeep.length} most recent successful):`);
93
73
  if (result.toKeep.length === 0) {
94
- console.log(" (none - no successful builds found)");
74
+ if (result.successful.length === 0) {
75
+ console.log(" (none - no successful builds found)");
76
+ }
77
+ else {
78
+ console.log(" (none)");
79
+ }
95
80
  }
96
81
  else {
97
82
  for (const blueprint of result.toKeep) {
98
- console.log(` ✓ ${blueprint.id} - Created ${formatTimestamp(blueprint.create_time_ms)}`);
83
+ console.log(` ✓ ${blueprint.id} - Created ${formatRelativeTime(blueprint.create_time_ms)}`);
99
84
  }
100
85
  }
101
86
  // Show what will be deleted
@@ -108,7 +93,7 @@ function displaySummary(name, result, isDryRun) {
108
93
  for (const blueprint of result.toDelete) {
109
94
  const icon = blueprint.status === "build_complete" ? "✓" : "⚠";
110
95
  const statusLabel = blueprint.status === "build_complete" ? "successful" : "failed";
111
- console.log(` ${icon} ${blueprint.id} - Created ${formatTimestamp(blueprint.create_time_ms)} (${statusLabel})`);
96
+ console.log(` ${icon} ${blueprint.id} - Created ${formatRelativeTime(blueprint.create_time_ms)} (${statusLabel})`);
112
97
  }
113
98
  }
114
99
  }
@@ -123,7 +108,7 @@ function displayDeletedBlueprints(deleted) {
123
108
  for (const blueprint of deleted) {
124
109
  const icon = blueprint.status === "build_complete" ? "✓" : "⚠";
125
110
  const statusLabel = blueprint.status === "build_complete" ? "successful" : "failed";
126
- console.log(` ${icon} ${blueprint.id} - Created ${formatTimestamp(blueprint.create_time_ms)} (${statusLabel})`);
111
+ console.log(` ${icon} ${blueprint.id} - Created ${formatRelativeTime(blueprint.create_time_ms)} (${statusLabel})`);
127
112
  }
128
113
  }
129
114
  /**
@@ -173,8 +158,8 @@ export async function pruneBlueprints(name, options = {}) {
173
158
  const isDryRun = options.dryRun || false;
174
159
  const autoConfirm = options.yes || false;
175
160
  const keepCount = parseInt(options.keep || "1", 10);
176
- if (isNaN(keepCount) || keepCount < 1) {
177
- outputError("--keep must be a positive integer");
161
+ if (isNaN(keepCount) || keepCount < 0) {
162
+ outputError("--keep must be a non-negative integer");
178
163
  }
179
164
  // Fetch all blueprints with the given name
180
165
  console.log(`Fetching blueprints named "${name}"...`);