@runloop/rl-cli 1.8.0 → 1.10.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 +21 -7
- 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 +142 -110
- package/dist/commands/devbox/rsync.js +69 -41
- package/dist/commands/devbox/scp.js +180 -39
- package/dist/commands/devbox/tunnel.js +4 -19
- package/dist/commands/gateway-config/create.js +53 -0
- package/dist/commands/gateway-config/delete.js +21 -0
- package/dist/commands/gateway-config/get.js +18 -0
- package/dist/commands/gateway-config/list.js +493 -0
- package/dist/commands/gateway-config/update.js +70 -0
- package/dist/commands/snapshot/list.js +11 -2
- package/dist/commands/snapshot/prune.js +265 -0
- package/dist/components/BenchmarkMenu.js +23 -3
- package/dist/components/DetailedInfoView.js +20 -0
- package/dist/components/DevboxActionsMenu.js +26 -62
- package/dist/components/DevboxCreatePage.js +763 -15
- package/dist/components/DevboxDetailPage.js +73 -24
- package/dist/components/GatewayConfigCreatePage.js +272 -0
- package/dist/components/LogsViewer.js +6 -40
- package/dist/components/ResourceDetailPage.js +143 -160
- package/dist/components/ResourceListView.js +3 -33
- package/dist/components/ResourcePicker.js +234 -0
- package/dist/components/SecretCreatePage.js +71 -27
- package/dist/components/SettingsMenu.js +12 -2
- package/dist/components/StateHistory.js +1 -20
- package/dist/components/StatusBadge.js +9 -2
- 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 +79 -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 +6 -0
- package/dist/screens/BenchmarkRunDetailScreen.js +258 -22
- package/dist/screens/BenchmarkRunListScreen.js +21 -1
- 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/ScenarioRunDetailScreen.js +6 -0
- package/dist/screens/SecretDetailScreen.js +26 -2
- package/dist/screens/SettingsMenuScreen.js +3 -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 +47 -0
- package/dist/services/gatewayConfigService.js +153 -0
- package/dist/services/scenarioService.js +34 -0
- package/dist/store/benchmarkJobStore.js +66 -0
- package/dist/store/benchmarkStore.js +63 -0
- package/dist/store/gatewayConfigStore.js +83 -0
- package/dist/utils/browser.js +22 -0
- package/dist/utils/clipboard.js +41 -0
- package/dist/utils/commands.js +105 -9
- package/dist/utils/gatewayConfigValidation.js +58 -0
- package/dist/utils/time.js +121 -0
- package/package.json +43 -43
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Snapshot prune command - Delete old snapshots for a given source devbox
|
|
3
|
+
*/
|
|
4
|
+
import * as readline from "readline";
|
|
5
|
+
import { getClient } from "../../utils/client.js";
|
|
6
|
+
import { output, outputError } from "../../utils/output.js";
|
|
7
|
+
import { formatRelativeTime } from "../../utils/time.js";
|
|
8
|
+
/**
|
|
9
|
+
* Query the async status for a snapshot and return a normalized status string.
|
|
10
|
+
* Maps API statuses: "complete" → "ready", others passed through.
|
|
11
|
+
*/
|
|
12
|
+
async function querySnapshotStatus(snapshotId) {
|
|
13
|
+
const client = getClient();
|
|
14
|
+
try {
|
|
15
|
+
const statusResponse = await client.devboxes.diskSnapshots.queryStatus(snapshotId);
|
|
16
|
+
const operationStatus = statusResponse.status;
|
|
17
|
+
return operationStatus === "complete" ? "ready" : operationStatus;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return "unknown";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Fetch all snapshots for a given source devbox (handles pagination)
|
|
25
|
+
* and enrich each snapshot with its async operation status.
|
|
26
|
+
*/
|
|
27
|
+
async function fetchAllSnapshotsForDevbox(devboxId) {
|
|
28
|
+
const client = getClient();
|
|
29
|
+
const allSnapshots = [];
|
|
30
|
+
let hasMore = true;
|
|
31
|
+
let startingAfter = undefined;
|
|
32
|
+
while (hasMore) {
|
|
33
|
+
const params = {
|
|
34
|
+
devbox_id: devboxId,
|
|
35
|
+
limit: 100,
|
|
36
|
+
};
|
|
37
|
+
if (startingAfter) {
|
|
38
|
+
params.starting_after = startingAfter;
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
const page = await client.devboxes.listDiskSnapshots(params);
|
|
42
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
43
|
+
const snapshots = (page.snapshots || []);
|
|
44
|
+
allSnapshots.push(...snapshots);
|
|
45
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
46
|
+
hasMore = page.has_more || false;
|
|
47
|
+
if (hasMore && snapshots.length > 0) {
|
|
48
|
+
startingAfter = snapshots[snapshots.length - 1].id;
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
hasMore = false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
console.error("Warning: Error fetching snapshots:", error);
|
|
56
|
+
// Continue with partial results
|
|
57
|
+
hasMore = false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// The listDiskSnapshots endpoint does not include status — query it for each snapshot
|
|
61
|
+
const enriched = await Promise.all(allSnapshots.map(async (snapshot) => ({
|
|
62
|
+
...snapshot,
|
|
63
|
+
status: await querySnapshotStatus(snapshot.id),
|
|
64
|
+
})));
|
|
65
|
+
return enriched;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Categorize snapshots into successful and failed, and determine what to keep/delete
|
|
69
|
+
*/
|
|
70
|
+
function categorizeSnapshots(snapshots, keepCount) {
|
|
71
|
+
// Filter successful snapshots (status "ready" means completed successfully)
|
|
72
|
+
const successful = snapshots.filter((s) => s.status === "ready");
|
|
73
|
+
// Filter failed/incomplete snapshots
|
|
74
|
+
const failed = snapshots.filter((s) => s.status !== "ready");
|
|
75
|
+
// Sort successful by create_time_ms descending (newest first)
|
|
76
|
+
successful.sort((a, b) => (b.create_time_ms || 0) - (a.create_time_ms || 0));
|
|
77
|
+
// Determine what to keep and delete
|
|
78
|
+
const toKeep = successful.slice(0, keepCount);
|
|
79
|
+
const toDelete = [...successful.slice(keepCount), ...failed];
|
|
80
|
+
return {
|
|
81
|
+
toKeep,
|
|
82
|
+
toDelete,
|
|
83
|
+
successful,
|
|
84
|
+
failed,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Display a summary of what will be kept and deleted
|
|
89
|
+
*/
|
|
90
|
+
function displaySummary(devboxId, result, isDryRun) {
|
|
91
|
+
const total = result.successful.length + result.failed.length;
|
|
92
|
+
console.log(`\nAnalyzing snapshots for devbox "${devboxId}"...`);
|
|
93
|
+
console.log(`\nFound ${total} snapshot${total !== 1 ? "s" : ""}:`);
|
|
94
|
+
console.log(` ✓ ${result.successful.length} ready snapshot${result.successful.length !== 1 ? "s" : ""}`);
|
|
95
|
+
console.log(` ✗ ${result.failed.length} failed/incomplete snapshot${result.failed.length !== 1 ? "s" : ""}`);
|
|
96
|
+
// Show what will be kept
|
|
97
|
+
console.log(`\nKeeping (${result.toKeep.length} most recent ready):`);
|
|
98
|
+
if (result.toKeep.length === 0) {
|
|
99
|
+
console.log(" (none - no ready snapshots found)");
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
for (const snapshot of result.toKeep) {
|
|
103
|
+
const label = snapshot.name ? ` "${snapshot.name}"` : "";
|
|
104
|
+
console.log(` ✓ ${snapshot.id}${label} - Created ${formatRelativeTime(snapshot.create_time_ms)}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// Show what will be deleted
|
|
108
|
+
console.log(`\n${isDryRun ? "Would delete" : "To be deleted"} (${result.toDelete.length} snapshot${result.toDelete.length !== 1 ? "s" : ""}):`);
|
|
109
|
+
if (result.toDelete.length === 0) {
|
|
110
|
+
console.log(" (none)");
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
for (const snapshot of result.toDelete) {
|
|
114
|
+
const icon = snapshot.status === "ready" ? "✓" : "⚠";
|
|
115
|
+
const statusLabel = snapshot.status === "ready" ? "ready" : snapshot.status || "unknown";
|
|
116
|
+
const label = snapshot.name ? ` "${snapshot.name}"` : "";
|
|
117
|
+
console.log(` ${icon} ${snapshot.id}${label} - Created ${formatRelativeTime(snapshot.create_time_ms)} (${statusLabel})`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Display all deleted snapshots
|
|
123
|
+
*/
|
|
124
|
+
function displayDeletedSnapshots(deleted) {
|
|
125
|
+
if (deleted.length === 0) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
console.log("\nDeleted snapshots:");
|
|
129
|
+
for (const snapshot of deleted) {
|
|
130
|
+
const icon = snapshot.status === "ready" ? "✓" : "⚠";
|
|
131
|
+
const statusLabel = snapshot.status === "ready" ? "ready" : snapshot.status || "unknown";
|
|
132
|
+
const label = snapshot.name ? ` "${snapshot.name}"` : "";
|
|
133
|
+
console.log(` ${icon} ${snapshot.id}${label} - Created ${formatRelativeTime(snapshot.create_time_ms)} (${statusLabel})`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Prompt user for confirmation
|
|
138
|
+
*/
|
|
139
|
+
async function confirmDeletion(count) {
|
|
140
|
+
const rl = readline.createInterface({
|
|
141
|
+
input: process.stdin,
|
|
142
|
+
output: process.stdout,
|
|
143
|
+
});
|
|
144
|
+
return new Promise((resolve) => {
|
|
145
|
+
rl.question(`\nDelete ${count} snapshot${count !== 1 ? "s" : ""}? (y/N): `, (answer) => {
|
|
146
|
+
rl.close();
|
|
147
|
+
resolve(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Delete snapshots with error tracking
|
|
153
|
+
*/
|
|
154
|
+
async function deleteSnapshotsWithTracking(snapshots) {
|
|
155
|
+
const client = getClient();
|
|
156
|
+
const results = {
|
|
157
|
+
deleted: [],
|
|
158
|
+
failed: [],
|
|
159
|
+
};
|
|
160
|
+
for (const snapshot of snapshots) {
|
|
161
|
+
try {
|
|
162
|
+
await client.devboxes.diskSnapshots.delete(snapshot.id);
|
|
163
|
+
results.deleted.push(snapshot);
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
results.failed.push({
|
|
167
|
+
id: snapshot.id,
|
|
168
|
+
error: error instanceof Error ? error.message : String(error),
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return results;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Main prune function
|
|
176
|
+
*/
|
|
177
|
+
export async function pruneSnapshots(devboxId, options = {}) {
|
|
178
|
+
try {
|
|
179
|
+
// Parse and validate options
|
|
180
|
+
const isDryRun = !!options.dryRun;
|
|
181
|
+
const autoConfirm = !!options.yes;
|
|
182
|
+
const keepCount = parseInt(options.keep || "1", 10);
|
|
183
|
+
if (isNaN(keepCount) || keepCount < 0) {
|
|
184
|
+
outputError("--keep must be a non-negative integer");
|
|
185
|
+
}
|
|
186
|
+
// Fetch all snapshots for the given devbox
|
|
187
|
+
console.log(`Fetching snapshots for devbox "${devboxId}"...`);
|
|
188
|
+
const snapshots = await fetchAllSnapshotsForDevbox(devboxId);
|
|
189
|
+
// Handle no snapshots found
|
|
190
|
+
if (snapshots.length === 0) {
|
|
191
|
+
console.log(`No snapshots found for devbox: ${devboxId}`);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
// Categorize snapshots
|
|
195
|
+
const categorized = categorizeSnapshots(snapshots, keepCount);
|
|
196
|
+
// Display summary
|
|
197
|
+
displaySummary(devboxId, categorized, isDryRun);
|
|
198
|
+
// Handle dry-run mode
|
|
199
|
+
if (isDryRun) {
|
|
200
|
+
console.log("\n(Dry run - no changes made)");
|
|
201
|
+
const result = {
|
|
202
|
+
sourceDevboxId: devboxId,
|
|
203
|
+
totalFound: snapshots.length,
|
|
204
|
+
successfulSnapshots: categorized.successful.length,
|
|
205
|
+
failedSnapshots: categorized.failed.length,
|
|
206
|
+
kept: categorized.toKeep,
|
|
207
|
+
deleted: [],
|
|
208
|
+
failed: [],
|
|
209
|
+
dryRun: true,
|
|
210
|
+
};
|
|
211
|
+
if (options.output && options.output !== "text") {
|
|
212
|
+
output(result, { format: options.output, defaultFormat: "json" });
|
|
213
|
+
}
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
// Handle nothing to delete
|
|
217
|
+
if (categorized.toDelete.length === 0) {
|
|
218
|
+
console.log("\nNothing to delete.");
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
// Warn if no successful snapshots
|
|
222
|
+
if (categorized.successful.length === 0) {
|
|
223
|
+
console.log("\nWarning: No ready snapshots found. Only deleting failed/incomplete snapshots.");
|
|
224
|
+
}
|
|
225
|
+
// Get confirmation unless --yes flag is set
|
|
226
|
+
if (!autoConfirm) {
|
|
227
|
+
const confirmed = await confirmDeletion(categorized.toDelete.length);
|
|
228
|
+
if (!confirmed) {
|
|
229
|
+
console.log("\nOperation cancelled.");
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
// Perform deletions
|
|
234
|
+
console.log(`\nDeleting ${categorized.toDelete.length} snapshot${categorized.toDelete.length !== 1 ? "s" : ""}...`);
|
|
235
|
+
const deletionResults = await deleteSnapshotsWithTracking(categorized.toDelete);
|
|
236
|
+
// Display results
|
|
237
|
+
console.log("\nResults:");
|
|
238
|
+
console.log(` ✓ Successfully deleted: ${deletionResults.deleted.length} snapshot${deletionResults.deleted.length !== 1 ? "s" : ""}`);
|
|
239
|
+
// Show all deleted snapshots
|
|
240
|
+
displayDeletedSnapshots(deletionResults.deleted);
|
|
241
|
+
if (deletionResults.failed.length > 0) {
|
|
242
|
+
console.log(`\n ✗ Failed to delete: ${deletionResults.failed.length} snapshot${deletionResults.failed.length !== 1 ? "s" : ""}`);
|
|
243
|
+
for (const failure of deletionResults.failed) {
|
|
244
|
+
console.log(` - ${failure.id}: ${failure.error}`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
// Output structured data if requested
|
|
248
|
+
if (options.output && options.output !== "text") {
|
|
249
|
+
const result = {
|
|
250
|
+
sourceDevboxId: devboxId,
|
|
251
|
+
totalFound: snapshots.length,
|
|
252
|
+
successfulSnapshots: categorized.successful.length,
|
|
253
|
+
failedSnapshots: categorized.failed.length,
|
|
254
|
+
kept: categorized.toKeep,
|
|
255
|
+
deleted: deletionResults.deleted,
|
|
256
|
+
failed: deletionResults.failed,
|
|
257
|
+
dryRun: false,
|
|
258
|
+
};
|
|
259
|
+
output(result, { format: options.output, defaultFormat: "json" });
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
catch (error) {
|
|
263
|
+
outputError("Failed to prune snapshots", error);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
@@ -10,6 +10,13 @@ import { NavigationTips } from "./NavigationTips.js";
|
|
|
10
10
|
import { colors } from "../utils/theme.js";
|
|
11
11
|
import { useExitOnCtrlC } from "../hooks/useExitOnCtrlC.js";
|
|
12
12
|
const benchmarkMenuItems = [
|
|
13
|
+
{
|
|
14
|
+
key: "benchmarks",
|
|
15
|
+
label: "Benchmarks",
|
|
16
|
+
description: "View benchmark definitions",
|
|
17
|
+
icon: "◉",
|
|
18
|
+
color: colors.primary,
|
|
19
|
+
},
|
|
13
20
|
{
|
|
14
21
|
key: "benchmark-runs",
|
|
15
22
|
label: "Benchmark Runs",
|
|
@@ -17,6 +24,13 @@ const benchmarkMenuItems = [
|
|
|
17
24
|
icon: "▶",
|
|
18
25
|
color: colors.success,
|
|
19
26
|
},
|
|
27
|
+
{
|
|
28
|
+
key: "benchmark-jobs",
|
|
29
|
+
label: "Benchmark Jobs",
|
|
30
|
+
description: "Run and manage benchmark jobs",
|
|
31
|
+
icon: "▣",
|
|
32
|
+
color: colors.warning,
|
|
33
|
+
},
|
|
20
34
|
{
|
|
21
35
|
key: "scenario-runs",
|
|
22
36
|
label: "Scenario Runs",
|
|
@@ -66,10 +80,16 @@ export const BenchmarkMenu = ({ onSelect, onBack }) => {
|
|
|
66
80
|
else if (key.escape) {
|
|
67
81
|
onBack();
|
|
68
82
|
}
|
|
69
|
-
else if (input === "
|
|
83
|
+
else if (input === "1") {
|
|
84
|
+
onSelect("benchmarks");
|
|
85
|
+
}
|
|
86
|
+
else if (input === "2") {
|
|
70
87
|
onSelect("benchmark-runs");
|
|
71
88
|
}
|
|
72
|
-
else if (input === "
|
|
89
|
+
else if (input === "3") {
|
|
90
|
+
onSelect("benchmark-jobs");
|
|
91
|
+
}
|
|
92
|
+
else if (input === "4") {
|
|
73
93
|
onSelect("scenario-runs");
|
|
74
94
|
}
|
|
75
95
|
else if (input === "q") {
|
|
@@ -80,7 +100,7 @@ export const BenchmarkMenu = ({ onSelect, onBack }) => {
|
|
|
80
100
|
const isSelected = index === selectedIndex;
|
|
81
101
|
return (_jsxs(Box, { marginBottom: 0, children: [_jsx(Text, { color: isSelected ? item.color : colors.textDim, children: isSelected ? figures.pointer : " " }), _jsx(Text, { children: " " }), _jsx(Text, { color: item.color, bold: true, children: item.icon }), _jsx(Text, { children: " " }), _jsx(Text, { color: isSelected ? item.color : colors.text, bold: isSelected, children: item.label }), !isNarrow && (_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "- ", item.description] })), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "[", index + 1, "]"] })] }, item.key));
|
|
82
102
|
}) }), _jsx(NavigationTips, { showArrows: true, paddingX: 2, tips: [
|
|
83
|
-
{ key: "1-
|
|
103
|
+
{ key: "1-4", label: "Quick select" },
|
|
84
104
|
{ key: "Enter", label: "Select" },
|
|
85
105
|
{ key: "Esc", label: "Back" },
|
|
86
106
|
{ key: "q", label: "Quit" },
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import figures from "figures";
|
|
4
|
+
import { Header } from "./Header.js";
|
|
5
|
+
import { StatusBadge } from "./StatusBadge.js";
|
|
6
|
+
import { Breadcrumb } from "./Breadcrumb.js";
|
|
7
|
+
import { colors } from "../utils/theme.js";
|
|
8
|
+
export function DetailedInfoView({ detailLines, scrollOffset, viewportHeight, displayName, resourceId, status, resourceType, breadcrumbPrefix = [], }) {
|
|
9
|
+
const maxScroll = Math.max(0, detailLines.length - viewportHeight);
|
|
10
|
+
const actualScroll = Math.min(scrollOffset, maxScroll);
|
|
11
|
+
const visibleLines = detailLines.slice(actualScroll, actualScroll + viewportHeight);
|
|
12
|
+
const hasMore = actualScroll + viewportHeight < detailLines.length;
|
|
13
|
+
const hasLess = actualScroll > 0;
|
|
14
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
15
|
+
...breadcrumbPrefix,
|
|
16
|
+
{ label: resourceType },
|
|
17
|
+
{ label: displayName },
|
|
18
|
+
{ label: "Full Details", active: true },
|
|
19
|
+
] }), _jsx(Header, { title: `${displayName} - Complete Information` }), _jsx(Box, { flexDirection: "column", marginBottom: 1, children: _jsxs(Box, { marginBottom: 1, children: [_jsx(StatusBadge, { status: status }), _jsx(Text, { children: " " }), _jsx(Text, { color: colors.idColor, children: resourceId })] }) }), _jsx(Box, { flexDirection: "column", marginTop: 1, marginBottom: 1, borderStyle: "round", borderColor: colors.border, paddingX: 2, paddingY: 1, children: _jsx(Box, { flexDirection: "column", children: visibleLines }) }), _jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [figures.arrowUp, figures.arrowDown, " Scroll \u2022 Line ", actualScroll + 1, "-", Math.min(actualScroll + viewportHeight, detailLines.length), " of", " ", detailLines.length] }), hasLess && _jsxs(Text, { color: colors.primary, children: [" ", figures.arrowUp] }), hasMore && _jsxs(Text, { color: colors.primary, children: [" ", figures.arrowDown] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022 [q or esc] Back to Details"] })] })] }));
|
|
20
|
+
}
|
|
@@ -11,6 +11,8 @@ import { Breadcrumb } from "./Breadcrumb.js";
|
|
|
11
11
|
import { NavigationTips } from "./NavigationTips.js";
|
|
12
12
|
import { ConfirmationPrompt } from "./ConfirmationPrompt.js";
|
|
13
13
|
import { colors } from "../utils/theme.js";
|
|
14
|
+
import { openInBrowser } from "../utils/browser.js";
|
|
15
|
+
import { copyToClipboard } from "../utils/clipboard.js";
|
|
14
16
|
import { useViewportHeight } from "../hooks/useViewportHeight.js";
|
|
15
17
|
import { useNavigation } from "../store/navigationStore.js";
|
|
16
18
|
import { useExitOnCtrlC } from "../hooks/useExitOnCtrlC.js";
|
|
@@ -137,8 +139,10 @@ export const DevboxActionsMenu = ({ devbox, onBack, breadcrumbItems = [
|
|
|
137
139
|
},
|
|
138
140
|
];
|
|
139
141
|
// Filter operations based on devbox status
|
|
142
|
+
const hasTunnel = !!(devbox?.tunnel && devbox.tunnel.tunnel_key);
|
|
140
143
|
const operations = devbox
|
|
141
|
-
? allOperations
|
|
144
|
+
? allOperations
|
|
145
|
+
.filter((op) => {
|
|
142
146
|
const status = devbox.status;
|
|
143
147
|
// When suspended: logs and resume
|
|
144
148
|
if (status === "suspended") {
|
|
@@ -156,6 +160,20 @@ export const DevboxActionsMenu = ({ devbox, onBack, breadcrumbItems = [
|
|
|
156
160
|
}
|
|
157
161
|
// Default for transitional states (provisioning, initializing)
|
|
158
162
|
return op.key === "logs" || op.key === "delete";
|
|
163
|
+
})
|
|
164
|
+
.map((op) => {
|
|
165
|
+
// Dynamic tunnel label based on whether tunnel is active
|
|
166
|
+
if (op.key === "tunnel") {
|
|
167
|
+
return hasTunnel
|
|
168
|
+
? {
|
|
169
|
+
...op,
|
|
170
|
+
label: "Tunnel (Active)",
|
|
171
|
+
color: colors.success,
|
|
172
|
+
icon: figures.tick,
|
|
173
|
+
}
|
|
174
|
+
: op;
|
|
175
|
+
}
|
|
176
|
+
return op;
|
|
159
177
|
})
|
|
160
178
|
: allOperations;
|
|
161
179
|
// Auto-execute operations that don't need input (except delete which needs confirmation)
|
|
@@ -393,31 +411,9 @@ export const DevboxActionsMenu = ({ devbox, onBack, breadcrumbItems = [
|
|
|
393
411
|
// Open tunnel URL in browser
|
|
394
412
|
const tunnelUrl = operationResult.__tunnelUrl;
|
|
395
413
|
if (tunnelUrl) {
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
let openCommand;
|
|
400
|
-
if (platform === "darwin") {
|
|
401
|
-
openCommand = `open "${tunnelUrl}"`;
|
|
402
|
-
}
|
|
403
|
-
else if (platform === "win32") {
|
|
404
|
-
openCommand = `start "${tunnelUrl}"`;
|
|
405
|
-
}
|
|
406
|
-
else {
|
|
407
|
-
openCommand = `xdg-open "${tunnelUrl}"`;
|
|
408
|
-
}
|
|
409
|
-
exec(openCommand, (error) => {
|
|
410
|
-
if (error) {
|
|
411
|
-
setCopyStatus("Could not open browser");
|
|
412
|
-
setTimeout(() => setCopyStatus(null), 2000);
|
|
413
|
-
}
|
|
414
|
-
else {
|
|
415
|
-
setCopyStatus("Opened in browser!");
|
|
416
|
-
setTimeout(() => setCopyStatus(null), 2000);
|
|
417
|
-
}
|
|
418
|
-
});
|
|
419
|
-
};
|
|
420
|
-
openBrowser();
|
|
414
|
+
openInBrowser(tunnelUrl);
|
|
415
|
+
setCopyStatus("Opened in browser!");
|
|
416
|
+
setTimeout(() => setCopyStatus(null), 2000);
|
|
421
417
|
}
|
|
422
418
|
}
|
|
423
419
|
else if ((key.upArrow || input === "k") &&
|
|
@@ -469,42 +465,10 @@ export const DevboxActionsMenu = ({ devbox, onBack, breadcrumbItems = [
|
|
|
469
465
|
// Copy exec output to clipboard
|
|
470
466
|
const output = (operationResult.stdout || "") +
|
|
471
467
|
(operationResult.stderr || "");
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
let args;
|
|
477
|
-
if (platform === "darwin") {
|
|
478
|
-
command = "pbcopy";
|
|
479
|
-
args = [];
|
|
480
|
-
}
|
|
481
|
-
else if (platform === "win32") {
|
|
482
|
-
command = "clip";
|
|
483
|
-
args = [];
|
|
484
|
-
}
|
|
485
|
-
else {
|
|
486
|
-
command = "xclip";
|
|
487
|
-
args = ["-selection", "clipboard"];
|
|
488
|
-
}
|
|
489
|
-
const proc = spawn(command, args);
|
|
490
|
-
proc.stdin.write(text);
|
|
491
|
-
proc.stdin.end();
|
|
492
|
-
proc.on("exit", (code) => {
|
|
493
|
-
if (code === 0) {
|
|
494
|
-
setCopyStatus("Copied to clipboard!");
|
|
495
|
-
setTimeout(() => setCopyStatus(null), 2000);
|
|
496
|
-
}
|
|
497
|
-
else {
|
|
498
|
-
setCopyStatus("Failed to copy");
|
|
499
|
-
setTimeout(() => setCopyStatus(null), 2000);
|
|
500
|
-
}
|
|
501
|
-
});
|
|
502
|
-
proc.on("error", () => {
|
|
503
|
-
setCopyStatus("Copy not supported");
|
|
504
|
-
setTimeout(() => setCopyStatus(null), 2000);
|
|
505
|
-
});
|
|
506
|
-
};
|
|
507
|
-
copyToClipboard(output);
|
|
468
|
+
copyToClipboard(output).then((status) => {
|
|
469
|
+
setCopyStatus(status);
|
|
470
|
+
setTimeout(() => setCopyStatus(null), 2000);
|
|
471
|
+
});
|
|
508
472
|
}
|
|
509
473
|
return;
|
|
510
474
|
}
|