@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.
- package/README.md +19 -5
- package/dist/cli.js +0 -0
- package/dist/commands/blueprint/delete.js +21 -0
- package/dist/commands/blueprint/list.js +226 -174
- package/dist/commands/blueprint/prune.js +13 -28
- package/dist/commands/devbox/create.js +41 -0
- package/dist/commands/devbox/list.js +125 -109
- package/dist/commands/devbox/tunnel.js +4 -19
- package/dist/commands/gateway-config/create.js +44 -0
- package/dist/commands/gateway-config/delete.js +21 -0
- package/dist/commands/gateway-config/get.js +15 -0
- package/dist/commands/gateway-config/list.js +493 -0
- package/dist/commands/gateway-config/update.js +60 -0
- package/dist/commands/menu.js +2 -1
- package/dist/commands/secret/list.js +379 -4
- package/dist/commands/snapshot/list.js +11 -2
- package/dist/commands/snapshot/prune.js +265 -0
- package/dist/components/BenchmarkMenu.js +108 -0
- package/dist/components/DetailedInfoView.js +20 -0
- package/dist/components/DevboxActionsMenu.js +9 -61
- package/dist/components/DevboxCreatePage.js +531 -14
- package/dist/components/DevboxDetailPage.js +27 -22
- package/dist/components/GatewayConfigCreatePage.js +265 -0
- package/dist/components/LogsViewer.js +6 -40
- package/dist/components/MainMenu.js +63 -22
- package/dist/components/ResourceDetailPage.js +143 -160
- package/dist/components/ResourceListView.js +3 -33
- package/dist/components/ResourcePicker.js +220 -0
- package/dist/components/SecretCreatePage.js +183 -0
- package/dist/components/SettingsMenu.js +95 -0
- package/dist/components/StateHistory.js +1 -20
- package/dist/components/StatusBadge.js +80 -0
- package/dist/components/StreamingLogsViewer.js +8 -42
- package/dist/components/form/FormTextInput.js +4 -2
- package/dist/components/resourceDetailTypes.js +18 -0
- package/dist/hooks/useInputHandler.js +103 -0
- package/dist/router/Router.js +99 -2
- package/dist/screens/BenchmarkDetailScreen.js +163 -0
- package/dist/screens/BenchmarkJobCreateScreen.js +524 -0
- package/dist/screens/BenchmarkJobDetailScreen.js +614 -0
- package/dist/screens/BenchmarkJobListScreen.js +479 -0
- package/dist/screens/BenchmarkListScreen.js +266 -0
- package/dist/screens/BenchmarkMenuScreen.js +29 -0
- package/dist/screens/BenchmarkRunDetailScreen.js +425 -0
- package/dist/screens/BenchmarkRunListScreen.js +275 -0
- package/dist/screens/BlueprintDetailScreen.js +5 -1
- package/dist/screens/DevboxCreateScreen.js +2 -2
- package/dist/screens/GatewayConfigDetailScreen.js +236 -0
- package/dist/screens/GatewayConfigListScreen.js +7 -0
- package/dist/screens/MenuScreen.js +5 -2
- package/dist/screens/ScenarioRunDetailScreen.js +226 -0
- package/dist/screens/ScenarioRunListScreen.js +245 -0
- package/dist/screens/SecretCreateScreen.js +7 -0
- package/dist/screens/SecretDetailScreen.js +198 -0
- package/dist/screens/SecretListScreen.js +7 -0
- package/dist/screens/SettingsMenuScreen.js +26 -0
- package/dist/screens/SnapshotDetailScreen.js +6 -0
- package/dist/services/agentService.js +42 -0
- package/dist/services/benchmarkJobService.js +122 -0
- package/dist/services/benchmarkService.js +120 -0
- package/dist/services/gatewayConfigService.js +114 -0
- package/dist/services/scenarioService.js +34 -0
- package/dist/store/benchmarkJobStore.js +66 -0
- package/dist/store/benchmarkStore.js +183 -0
- package/dist/store/betaFeatureStore.js +47 -0
- package/dist/store/gatewayConfigStore.js +83 -0
- package/dist/store/index.js +1 -0
- package/dist/utils/browser.js +22 -0
- package/dist/utils/clipboard.js +41 -0
- package/dist/utils/commands.js +80 -0
- package/dist/utils/config.js +8 -0
- package/dist/utils/time.js +121 -0
- 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
|
-
|
|
214
|
+
pnpm install
|
|
201
215
|
|
|
202
216
|
# Build
|
|
203
|
-
|
|
217
|
+
pnpm run build
|
|
204
218
|
|
|
205
219
|
# Watch mode
|
|
206
|
-
|
|
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,
|
|
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
|
-
//
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
setSelectedIndex(selectedIndex - 1);
|
|
314
|
+
else if (operationKey === "delete") {
|
|
315
|
+
setSelectedBlueprint(selectedBlueprintItem);
|
|
316
|
+
setShowDeleteConfirm(true);
|
|
400
317
|
}
|
|
401
|
-
else
|
|
402
|
-
|
|
318
|
+
else {
|
|
319
|
+
setSelectedBlueprint(selectedBlueprintItem);
|
|
320
|
+
setExecutingOperation(operationKey);
|
|
321
|
+
executeOperation(selectedBlueprintItem, operationKey);
|
|
403
322
|
}
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
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
|
-
|
|
412
|
-
|
|
413
|
-
|
|
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
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
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 (
|
|
434
|
-
|
|
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
|
|
453
|
-
|
|
351
|
+
else {
|
|
352
|
+
inkExit();
|
|
454
353
|
}
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
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
|
-
|
|
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 ${
|
|
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 ${
|
|
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 ${
|
|
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 <
|
|
177
|
-
outputError("--keep must be a
|
|
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}"...`);
|