@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.
Files changed (66) hide show
  1. package/README.md +21 -7
  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 +142 -110
  8. package/dist/commands/devbox/rsync.js +69 -41
  9. package/dist/commands/devbox/scp.js +180 -39
  10. package/dist/commands/devbox/tunnel.js +4 -19
  11. package/dist/commands/gateway-config/create.js +53 -0
  12. package/dist/commands/gateway-config/delete.js +21 -0
  13. package/dist/commands/gateway-config/get.js +18 -0
  14. package/dist/commands/gateway-config/list.js +493 -0
  15. package/dist/commands/gateway-config/update.js +70 -0
  16. package/dist/commands/snapshot/list.js +11 -2
  17. package/dist/commands/snapshot/prune.js +265 -0
  18. package/dist/components/BenchmarkMenu.js +23 -3
  19. package/dist/components/DetailedInfoView.js +20 -0
  20. package/dist/components/DevboxActionsMenu.js +26 -62
  21. package/dist/components/DevboxCreatePage.js +763 -15
  22. package/dist/components/DevboxDetailPage.js +73 -24
  23. package/dist/components/GatewayConfigCreatePage.js +272 -0
  24. package/dist/components/LogsViewer.js +6 -40
  25. package/dist/components/ResourceDetailPage.js +143 -160
  26. package/dist/components/ResourceListView.js +3 -33
  27. package/dist/components/ResourcePicker.js +234 -0
  28. package/dist/components/SecretCreatePage.js +71 -27
  29. package/dist/components/SettingsMenu.js +12 -2
  30. package/dist/components/StateHistory.js +1 -20
  31. package/dist/components/StatusBadge.js +9 -2
  32. package/dist/components/StreamingLogsViewer.js +8 -42
  33. package/dist/components/form/FormTextInput.js +4 -2
  34. package/dist/components/resourceDetailTypes.js +18 -0
  35. package/dist/hooks/useInputHandler.js +103 -0
  36. package/dist/router/Router.js +79 -2
  37. package/dist/screens/BenchmarkDetailScreen.js +163 -0
  38. package/dist/screens/BenchmarkJobCreateScreen.js +524 -0
  39. package/dist/screens/BenchmarkJobDetailScreen.js +614 -0
  40. package/dist/screens/BenchmarkJobListScreen.js +479 -0
  41. package/dist/screens/BenchmarkListScreen.js +266 -0
  42. package/dist/screens/BenchmarkMenuScreen.js +6 -0
  43. package/dist/screens/BenchmarkRunDetailScreen.js +258 -22
  44. package/dist/screens/BenchmarkRunListScreen.js +21 -1
  45. package/dist/screens/BlueprintDetailScreen.js +5 -1
  46. package/dist/screens/DevboxCreateScreen.js +2 -2
  47. package/dist/screens/GatewayConfigDetailScreen.js +236 -0
  48. package/dist/screens/GatewayConfigListScreen.js +7 -0
  49. package/dist/screens/ScenarioRunDetailScreen.js +6 -0
  50. package/dist/screens/SecretDetailScreen.js +26 -2
  51. package/dist/screens/SettingsMenuScreen.js +3 -0
  52. package/dist/screens/SnapshotDetailScreen.js +6 -0
  53. package/dist/services/agentService.js +42 -0
  54. package/dist/services/benchmarkJobService.js +122 -0
  55. package/dist/services/benchmarkService.js +47 -0
  56. package/dist/services/gatewayConfigService.js +153 -0
  57. package/dist/services/scenarioService.js +34 -0
  58. package/dist/store/benchmarkJobStore.js +66 -0
  59. package/dist/store/benchmarkStore.js +63 -0
  60. package/dist/store/gatewayConfigStore.js +83 -0
  61. package/dist/utils/browser.js +22 -0
  62. package/dist/utils/clipboard.js +41 -0
  63. package/dist/utils/commands.js +105 -9
  64. package/dist/utils/gatewayConfigValidation.js +58 -0
  65. package/dist/utils/time.js +121 -0
  66. 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 === "b" || input === "1") {
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 === "s" || input === "2") {
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-2", label: "Quick select" },
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.filter((op) => {
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
- const openBrowser = async () => {
397
- const { exec } = await import("child_process");
398
- const platform = process.platform;
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
- const copyToClipboard = async (text) => {
473
- const { spawn } = await import("child_process");
474
- const platform = process.platform;
475
- let command;
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
  }