@runloop/rl-cli 1.8.0 → 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/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 +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/ResourceDetailPage.js +143 -160
- package/dist/components/ResourceListView.js +3 -33
- package/dist/components/ResourcePicker.js +220 -0
- package/dist/components/SecretCreatePage.js +2 -4
- 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/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 +114 -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 +80 -0
- package/dist/utils/time.js +121 -0
- package/package.json +42 -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";
|
|
@@ -393,31 +395,9 @@ export const DevboxActionsMenu = ({ devbox, onBack, breadcrumbItems = [
|
|
|
393
395
|
// Open tunnel URL in browser
|
|
394
396
|
const tunnelUrl = operationResult.__tunnelUrl;
|
|
395
397
|
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();
|
|
398
|
+
openInBrowser(tunnelUrl);
|
|
399
|
+
setCopyStatus("Opened in browser!");
|
|
400
|
+
setTimeout(() => setCopyStatus(null), 2000);
|
|
421
401
|
}
|
|
422
402
|
}
|
|
423
403
|
else if ((key.upArrow || input === "k") &&
|
|
@@ -469,42 +449,10 @@ export const DevboxActionsMenu = ({ devbox, onBack, breadcrumbItems = [
|
|
|
469
449
|
// Copy exec output to clipboard
|
|
470
450
|
const output = (operationResult.stdout || "") +
|
|
471
451
|
(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);
|
|
452
|
+
copyToClipboard(output).then((status) => {
|
|
453
|
+
setCopyStatus(status);
|
|
454
|
+
setTimeout(() => setCopyStatus(null), 2000);
|
|
455
|
+
});
|
|
508
456
|
}
|
|
509
457
|
return;
|
|
510
458
|
}
|