@runloop/rl-cli 0.4.0 → 0.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 +73 -106
- package/dist/cli.js +4 -416
- package/dist/commands/blueprint/list.js +2 -1
- package/dist/commands/devbox/list.js +5 -1
- package/dist/commands/snapshot/list.js +47 -6
- package/dist/components/Breadcrumb.js +8 -8
- package/dist/components/DevboxCreatePage.js +2 -2
- package/dist/components/DevboxDetailPage.js +2 -1
- package/dist/components/MainMenu.js +13 -2
- package/dist/components/StateHistory.js +80 -0
- package/dist/components/UpdateNotification.js +3 -44
- package/dist/hooks/useUpdateCheck.js +54 -0
- package/dist/mcp/server.js +1 -1
- package/dist/screens/DevboxCreateScreen.js +4 -4
- package/dist/utils/client.js +1 -1
- package/dist/utils/commands.js +408 -0
- package/dist/utils/config.js +21 -0
- package/dist/utils/exec.js +22 -0
- package/dist/utils/theme.js +1 -1
- package/package.json +21 -8
- package/dist/commands/auth.js +0 -29
- package/dist/commands/blueprint/preview.js +0 -45
- package/dist/commands/config.js +0 -118
- package/dist/commands/create.js +0 -42
- package/dist/commands/delete.js +0 -34
- package/dist/commands/exec.js +0 -35
- package/dist/commands/list.js +0 -59
- package/dist/commands/upload.js +0 -40
- package/dist/components/Table.example.js +0 -85
- package/dist/screens/LogsSessionScreen.js +0 -49
- package/dist/utils/CommandExecutor.js +0 -131
- package/dist/utils/memoryMonitor.js +0 -85
- package/dist/utils/process.js +0 -106
- package/dist/utils/versionCheck.js +0 -53
package/dist/cli.js
CHANGED
|
@@ -1,13 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { Command } from "commander";
|
|
3
|
-
import { createDevbox } from "./commands/devbox/create.js";
|
|
4
|
-
import { listDevboxes } from "./commands/devbox/list.js";
|
|
5
|
-
import { deleteDevbox } from "./commands/devbox/delete.js";
|
|
6
|
-
import { execCommand } from "./commands/devbox/exec.js";
|
|
7
|
-
import { uploadFile } from "./commands/devbox/upload.js";
|
|
8
|
-
import { VERSION } from "./version.js";
|
|
9
2
|
import { exitAlternateScreenBuffer } from "./utils/screen.js";
|
|
10
3
|
import { processUtils } from "./utils/processUtils.js";
|
|
4
|
+
import { createProgram } from "./utils/commands.js";
|
|
5
|
+
import { getApiKeyErrorMessage } from "./utils/config.js";
|
|
11
6
|
// Global Ctrl+C handler to ensure it always exits
|
|
12
7
|
processUtils.on("SIGINT", () => {
|
|
13
8
|
// Force exit immediately, clearing alternate screen buffer
|
|
@@ -15,400 +10,7 @@ processUtils.on("SIGINT", () => {
|
|
|
15
10
|
processUtils.stdout.write("\n");
|
|
16
11
|
processUtils.exit(130); // Standard exit code for SIGINT
|
|
17
12
|
});
|
|
18
|
-
const program =
|
|
19
|
-
program
|
|
20
|
-
.name("rli")
|
|
21
|
-
.description("Beautiful CLI for Runloop devbox management")
|
|
22
|
-
.version(VERSION);
|
|
23
|
-
// Devbox commands
|
|
24
|
-
const devbox = program
|
|
25
|
-
.command("devbox")
|
|
26
|
-
.description("Manage devboxes")
|
|
27
|
-
.alias("d");
|
|
28
|
-
devbox
|
|
29
|
-
.command("create")
|
|
30
|
-
.description("Create a new devbox")
|
|
31
|
-
.option("-n, --name <name>", "Devbox name")
|
|
32
|
-
.option("-t, --template <template>", "Snapshot ID to use (alias: --snapshot)")
|
|
33
|
-
.option("-s, --snapshot <snapshot>", "Snapshot ID to use")
|
|
34
|
-
.option("--blueprint <blueprint>", "Blueprint name or ID to use")
|
|
35
|
-
.option("--resources <size>", "Resource size (X_SMALL, SMALL, MEDIUM, LARGE, X_LARGE, XX_LARGE)")
|
|
36
|
-
.option("--architecture <arch>", "Architecture (arm64, x86_64)")
|
|
37
|
-
.option("--entrypoint <command>", "Entrypoint command to run")
|
|
38
|
-
.option("--launch-commands <commands...>", "Initialization commands to run on startup")
|
|
39
|
-
.option("--env-vars <vars...>", "Environment variables (format: KEY=value)")
|
|
40
|
-
.option("--code-mounts <mounts...>", "Code mount configurations (JSON format)")
|
|
41
|
-
.option("--idle-time <seconds>", "Idle time in seconds before idle action")
|
|
42
|
-
.option("--idle-action <action>", "Action on idle (shutdown, suspend)")
|
|
43
|
-
.option("--available-ports <ports...>", "Available ports")
|
|
44
|
-
.option("--root", "Run as root")
|
|
45
|
-
.option("--user <user:uid>", "Run as this user (format: username:uid)")
|
|
46
|
-
.option("-o, --output [format]", "Output format: text|json|yaml (default: text)")
|
|
47
|
-
.action(createDevbox);
|
|
48
|
-
devbox
|
|
49
|
-
.command("list")
|
|
50
|
-
.description("List all devboxes")
|
|
51
|
-
.option("-s, --status <status>", "Filter by status (initializing, running, suspending, suspended, resuming, failure, shutdown)")
|
|
52
|
-
.option("-l, --limit <n>", "Max results", "20")
|
|
53
|
-
.option("-o, --output [format]", "Output format: text|json|yaml (default: json)")
|
|
54
|
-
.action(async (options) => {
|
|
55
|
-
await listDevboxes(options);
|
|
56
|
-
});
|
|
57
|
-
devbox
|
|
58
|
-
.command("delete <id>")
|
|
59
|
-
.description("Shutdown a devbox")
|
|
60
|
-
.alias("rm")
|
|
61
|
-
.option("-o, --output [format]", "Output format: text|json|yaml (default: text)")
|
|
62
|
-
.action(deleteDevbox);
|
|
63
|
-
devbox
|
|
64
|
-
.command("exec <id> <command...>")
|
|
65
|
-
.description("Execute a command in a devbox")
|
|
66
|
-
.option("--shell-name <name>", "Shell name to use (optional)")
|
|
67
|
-
.option("-o, --output [format]", "Output format: text|json|yaml (default: text)")
|
|
68
|
-
.action(async (id, command, options) => {
|
|
69
|
-
await execCommand(id, command, options);
|
|
70
|
-
});
|
|
71
|
-
devbox
|
|
72
|
-
.command("upload <id> <file>")
|
|
73
|
-
.description("Upload a file to a devbox")
|
|
74
|
-
.option("-p, --path <path>", "Target path in devbox")
|
|
75
|
-
.option("-o, --output [format]", "Output format: text|json|yaml (default: text)")
|
|
76
|
-
.action(uploadFile);
|
|
77
|
-
// Additional devbox commands
|
|
78
|
-
devbox
|
|
79
|
-
.command("get <id>")
|
|
80
|
-
.description("Get devbox details")
|
|
81
|
-
.option("-o, --output [format]", "Output format: text|json|yaml (default: json)")
|
|
82
|
-
.action(async (id, options) => {
|
|
83
|
-
const { getDevbox } = await import("./commands/devbox/get.js");
|
|
84
|
-
await getDevbox(id, options);
|
|
85
|
-
});
|
|
86
|
-
devbox
|
|
87
|
-
.command("suspend <id>")
|
|
88
|
-
.description("Suspend a devbox")
|
|
89
|
-
.option("-o, --output [format]", "Output format: text|json|yaml (default: text)")
|
|
90
|
-
.action(async (id, options) => {
|
|
91
|
-
const { suspendDevbox } = await import("./commands/devbox/suspend.js");
|
|
92
|
-
await suspendDevbox(id, options);
|
|
93
|
-
});
|
|
94
|
-
devbox
|
|
95
|
-
.command("resume <id>")
|
|
96
|
-
.description("Resume a suspended devbox")
|
|
97
|
-
.option("-o, --output [format]", "Output format: text|json|yaml (default: text)")
|
|
98
|
-
.action(async (id, options) => {
|
|
99
|
-
const { resumeDevbox } = await import("./commands/devbox/resume.js");
|
|
100
|
-
await resumeDevbox(id, options);
|
|
101
|
-
});
|
|
102
|
-
devbox
|
|
103
|
-
.command("shutdown <id>")
|
|
104
|
-
.description("Shutdown a devbox")
|
|
105
|
-
.option("-o, --output [format]", "Output format: text|json|yaml (default: text)")
|
|
106
|
-
.action(async (id, options) => {
|
|
107
|
-
const { shutdownDevbox } = await import("./commands/devbox/shutdown.js");
|
|
108
|
-
await shutdownDevbox(id, options);
|
|
109
|
-
});
|
|
110
|
-
devbox
|
|
111
|
-
.command("ssh <id>")
|
|
112
|
-
.description("SSH into a devbox")
|
|
113
|
-
.option("--config-only", "Print SSH config only")
|
|
114
|
-
.option("--no-wait", "Do not wait for devbox to be ready")
|
|
115
|
-
.option("--timeout <seconds>", "Timeout in seconds to wait for readiness", "180")
|
|
116
|
-
.option("--poll-interval <seconds>", "Polling interval in seconds while waiting", "3")
|
|
117
|
-
.option("-o, --output [format]", "Output format: text|json|yaml (default: text)")
|
|
118
|
-
.action(async (id, options) => {
|
|
119
|
-
const { sshDevbox } = await import("./commands/devbox/ssh.js");
|
|
120
|
-
await sshDevbox(id, options);
|
|
121
|
-
});
|
|
122
|
-
devbox
|
|
123
|
-
.command("scp <id> <src> <dst>")
|
|
124
|
-
.description("Copy files to/from a devbox using scp")
|
|
125
|
-
.option("--scp-options <options>", "Additional scp options (quoted)")
|
|
126
|
-
.option("-o, --output [format]", "Output format: text|json|yaml (default: text)")
|
|
127
|
-
.action(async (id, src, dst, options) => {
|
|
128
|
-
const { scpFiles } = await import("./commands/devbox/scp.js");
|
|
129
|
-
await scpFiles(id, { src, dst, ...options });
|
|
130
|
-
});
|
|
131
|
-
devbox
|
|
132
|
-
.command("rsync <id> <src> <dst>")
|
|
133
|
-
.description("Sync files to/from a devbox using rsync")
|
|
134
|
-
.option("--rsync-options <options>", "Additional rsync options (quoted)")
|
|
135
|
-
.option("-o, --output [format]", "Output format: text|json|yaml (default: text)")
|
|
136
|
-
.action(async (id, src, dst, options) => {
|
|
137
|
-
const { rsyncFiles } = await import("./commands/devbox/rsync.js");
|
|
138
|
-
await rsyncFiles(id, { src, dst, ...options });
|
|
139
|
-
});
|
|
140
|
-
devbox
|
|
141
|
-
.command("tunnel <id> <ports>")
|
|
142
|
-
.description("Create a port-forwarding tunnel to a devbox")
|
|
143
|
-
.option("-o, --output [format]", "Output format: text|json|yaml (default: text)")
|
|
144
|
-
.action(async (id, ports, options) => {
|
|
145
|
-
const { createTunnel } = await import("./commands/devbox/tunnel.js");
|
|
146
|
-
await createTunnel(id, { ports, ...options });
|
|
147
|
-
});
|
|
148
|
-
devbox
|
|
149
|
-
.command("read <id>")
|
|
150
|
-
.description("Read a file from a devbox using the API")
|
|
151
|
-
.option("--remote <path>", "Remote file path to read from the devbox")
|
|
152
|
-
.option("--output-path <path>", "Local file path to write the contents to")
|
|
153
|
-
.option("-o, --output [format]", "Output format: text|json|yaml (default: text)")
|
|
154
|
-
.action(async (id, options) => {
|
|
155
|
-
const { readFile } = await import("./commands/devbox/read.js");
|
|
156
|
-
await readFile(id, options);
|
|
157
|
-
});
|
|
158
|
-
devbox
|
|
159
|
-
.command("write <id>")
|
|
160
|
-
.description("Write a file to a devbox using the API")
|
|
161
|
-
.option("--input <path>", "Local file path to read contents from")
|
|
162
|
-
.option("--remote <path>", "Remote file path to write to on the devbox")
|
|
163
|
-
.option("-o, --output [format]", "Output format: text|json|yaml (default: text)")
|
|
164
|
-
.action(async (id, options) => {
|
|
165
|
-
const { writeFile } = await import("./commands/devbox/write.js");
|
|
166
|
-
await writeFile(id, options);
|
|
167
|
-
});
|
|
168
|
-
devbox
|
|
169
|
-
.command("download <id>")
|
|
170
|
-
.description("Download a file from a devbox")
|
|
171
|
-
.option("--file-path <path>", "Path to the file in the devbox")
|
|
172
|
-
.option("--output-path <path>", "Local path where to save the downloaded file")
|
|
173
|
-
.option("-o, --output-format [format]", "Output format: text|json|yaml (default: interactive)")
|
|
174
|
-
.action(async (id, options) => {
|
|
175
|
-
const { downloadFile } = await import("./commands/devbox/download.js");
|
|
176
|
-
await downloadFile(id, options);
|
|
177
|
-
});
|
|
178
|
-
devbox
|
|
179
|
-
.command("exec-async <id> <command...>")
|
|
180
|
-
.description("Execute a command asynchronously on a devbox")
|
|
181
|
-
.option("--shell-name <name>", "Shell name to use (optional)")
|
|
182
|
-
.option("-o, --output [format]", "Output format: text|json|yaml (default: text)")
|
|
183
|
-
.action(async (id, command, options) => {
|
|
184
|
-
const { execAsync } = await import("./commands/devbox/execAsync.js");
|
|
185
|
-
await execAsync(id, { command: command.join(" "), ...options });
|
|
186
|
-
});
|
|
187
|
-
devbox
|
|
188
|
-
.command("get-async <id> <execution-id>")
|
|
189
|
-
.description("Get status of an async execution")
|
|
190
|
-
.option("-o, --output [format]", "Output format: text|json|yaml (default: text)")
|
|
191
|
-
.action(async (id, executionId, options) => {
|
|
192
|
-
const { getAsync } = await import("./commands/devbox/getAsync.js");
|
|
193
|
-
await getAsync(id, { executionId, ...options });
|
|
194
|
-
});
|
|
195
|
-
devbox
|
|
196
|
-
.command("send-stdin <id> <execution-id>")
|
|
197
|
-
.description("Send stdin to a running async execution")
|
|
198
|
-
.option("--text <text>", "Text content to send to stdin")
|
|
199
|
-
.option("--signal <signal>", "Signal to send (EOF, INTERRUPT)")
|
|
200
|
-
.option("-o, --output [format]", "Output format: text|json|yaml (default: text)")
|
|
201
|
-
.action(async (id, executionId, options) => {
|
|
202
|
-
const { sendStdin } = await import("./commands/devbox/sendStdin.js");
|
|
203
|
-
await sendStdin(id, executionId, options);
|
|
204
|
-
});
|
|
205
|
-
devbox
|
|
206
|
-
.command("logs <id>")
|
|
207
|
-
.description("View devbox logs")
|
|
208
|
-
.option("-o, --output [format]", "Output format: text|json|yaml (default: text)")
|
|
209
|
-
.action(async (id, options) => {
|
|
210
|
-
const { getLogs } = await import("./commands/devbox/logs.js");
|
|
211
|
-
await getLogs(id, options);
|
|
212
|
-
});
|
|
213
|
-
// Snapshot commands
|
|
214
|
-
const snapshot = program
|
|
215
|
-
.command("snapshot")
|
|
216
|
-
.description("Manage devbox snapshots")
|
|
217
|
-
.alias("snap");
|
|
218
|
-
snapshot
|
|
219
|
-
.command("list")
|
|
220
|
-
.description("List all snapshots")
|
|
221
|
-
.option("-d, --devbox <id>", "Filter by devbox ID")
|
|
222
|
-
.option("-o, --output [format]", "Output format: text|json|yaml (default: json)")
|
|
223
|
-
.action(async (options) => {
|
|
224
|
-
const { listSnapshots } = await import("./commands/snapshot/list.js");
|
|
225
|
-
await listSnapshots(options);
|
|
226
|
-
});
|
|
227
|
-
snapshot
|
|
228
|
-
.command("create <devbox-id>")
|
|
229
|
-
.description("Create a snapshot of a devbox")
|
|
230
|
-
.option("-n, --name <name>", "Snapshot name")
|
|
231
|
-
.option("-o, --output [format]", "Output format: text|json|yaml (default: text)")
|
|
232
|
-
.action(async (devboxId, options) => {
|
|
233
|
-
const { createSnapshot } = await import("./commands/snapshot/create.js");
|
|
234
|
-
createSnapshot(devboxId, options);
|
|
235
|
-
});
|
|
236
|
-
snapshot
|
|
237
|
-
.command("delete <id>")
|
|
238
|
-
.description("Delete a snapshot")
|
|
239
|
-
.alias("rm")
|
|
240
|
-
.option("-o, --output [format]", "Output format: text|json|yaml (default: text)")
|
|
241
|
-
.action(async (id, options) => {
|
|
242
|
-
const { deleteSnapshot } = await import("./commands/snapshot/delete.js");
|
|
243
|
-
deleteSnapshot(id, options);
|
|
244
|
-
});
|
|
245
|
-
snapshot
|
|
246
|
-
.command("get <id>")
|
|
247
|
-
.description("Get snapshot details")
|
|
248
|
-
.option("-o, --output [format]", "Output format: text|json|yaml (default: json)")
|
|
249
|
-
.action(async (id, options) => {
|
|
250
|
-
const { getSnapshot } = await import("./commands/snapshot/get.js");
|
|
251
|
-
await getSnapshot({ id, ...options });
|
|
252
|
-
});
|
|
253
|
-
snapshot
|
|
254
|
-
.command("status <snapshot-id>")
|
|
255
|
-
.description("Get snapshot operation status")
|
|
256
|
-
.option("-o, --output [format]", "Output format: text|json|yaml (default: text)")
|
|
257
|
-
.action(async (snapshotId, options) => {
|
|
258
|
-
const { getSnapshotStatus } = await import("./commands/snapshot/status.js");
|
|
259
|
-
await getSnapshotStatus({ snapshotId, ...options });
|
|
260
|
-
});
|
|
261
|
-
// Blueprint commands
|
|
262
|
-
const blueprint = program
|
|
263
|
-
.command("blueprint")
|
|
264
|
-
.description("Manage blueprints")
|
|
265
|
-
.alias("bp");
|
|
266
|
-
blueprint
|
|
267
|
-
.command("list")
|
|
268
|
-
.description("List all blueprints")
|
|
269
|
-
.option("-n, --name <name>", "Filter by blueprint name")
|
|
270
|
-
.option("-o, --output [format]", "Output format: text|json|yaml (default: json)")
|
|
271
|
-
.action(async (options) => {
|
|
272
|
-
const { listBlueprints } = await import("./commands/blueprint/list.js");
|
|
273
|
-
await listBlueprints(options);
|
|
274
|
-
});
|
|
275
|
-
blueprint
|
|
276
|
-
.command("create")
|
|
277
|
-
.description("Create a new blueprint")
|
|
278
|
-
.requiredOption("--name <name>", "Blueprint name (required)")
|
|
279
|
-
.option("--dockerfile <content>", "Dockerfile contents")
|
|
280
|
-
.option("--dockerfile-path <path>", "Dockerfile path")
|
|
281
|
-
.option("--system-setup-commands <commands...>", "System setup commands")
|
|
282
|
-
.option("--resources <size>", "Resource size (X_SMALL, SMALL, MEDIUM, LARGE, X_LARGE, XX_LARGE)")
|
|
283
|
-
.option("--architecture <arch>", "Architecture (arm64, x86_64)")
|
|
284
|
-
.option("--available-ports <ports...>", "Available ports")
|
|
285
|
-
.option("--root", "Run as root")
|
|
286
|
-
.option("--user <user:uid>", "Run as this user (format: username:uid)")
|
|
287
|
-
.option("-o, --output [format]", "Output format: text|json|yaml (default: json)")
|
|
288
|
-
.action(async (options) => {
|
|
289
|
-
const { createBlueprint } = await import("./commands/blueprint/create.js");
|
|
290
|
-
await createBlueprint(options);
|
|
291
|
-
});
|
|
292
|
-
blueprint
|
|
293
|
-
.command("get <name-or-id>")
|
|
294
|
-
.description("Get blueprint details by name or ID (IDs start with bpt_)")
|
|
295
|
-
.option("-o, --output [format]", "Output format: text|json|yaml (default: json)")
|
|
296
|
-
.action(async (id, options) => {
|
|
297
|
-
const { getBlueprint } = await import("./commands/blueprint/get.js");
|
|
298
|
-
await getBlueprint({ id, ...options });
|
|
299
|
-
});
|
|
300
|
-
blueprint
|
|
301
|
-
.command("logs <name-or-id>")
|
|
302
|
-
.description("Get blueprint build logs by name or ID (IDs start with bpt_)")
|
|
303
|
-
.option("-o, --output [format]", "Output format: text|json|yaml (default: text)")
|
|
304
|
-
.action(async (id, options) => {
|
|
305
|
-
const { getBlueprintLogs } = await import("./commands/blueprint/logs.js");
|
|
306
|
-
await getBlueprintLogs({ id, ...options });
|
|
307
|
-
});
|
|
308
|
-
// Object storage commands
|
|
309
|
-
const object = program
|
|
310
|
-
.command("object")
|
|
311
|
-
.description("Manage object storage")
|
|
312
|
-
.alias("obj");
|
|
313
|
-
object
|
|
314
|
-
.command("list")
|
|
315
|
-
.description("List objects")
|
|
316
|
-
.option("--limit <n>", "Max results", "20")
|
|
317
|
-
.option("--starting-after <id>", "Starting point for pagination")
|
|
318
|
-
.option("--name <name>", "Filter by name (partial match supported)")
|
|
319
|
-
.option("--content-type <type>", "Filter by content type")
|
|
320
|
-
.option("--state <state>", "Filter by state (UPLOADING, READ_ONLY, DELETED)")
|
|
321
|
-
.option("--search <query>", "Search by object ID or name")
|
|
322
|
-
.option("--public", "List public objects only")
|
|
323
|
-
.option("-o, --output [format]", "Output format: text|json|yaml (default: json)")
|
|
324
|
-
.action(async (options) => {
|
|
325
|
-
const { listObjects } = await import("./commands/object/list.js");
|
|
326
|
-
await listObjects(options);
|
|
327
|
-
});
|
|
328
|
-
object
|
|
329
|
-
.command("get <id>")
|
|
330
|
-
.description("Get object details")
|
|
331
|
-
.option("-o, --output [format]", "Output format: text|json|yaml (default: text)")
|
|
332
|
-
.action(async (id, options) => {
|
|
333
|
-
const { getObject } = await import("./commands/object/get.js");
|
|
334
|
-
await getObject({ id, ...options });
|
|
335
|
-
});
|
|
336
|
-
object
|
|
337
|
-
.command("download <id> <path>")
|
|
338
|
-
.description("Download object to local file")
|
|
339
|
-
.option("--extract", "Extract downloaded archive after download")
|
|
340
|
-
.option("--duration-seconds <seconds>", "Duration in seconds for the presigned URL validity", "3600")
|
|
341
|
-
.option("-o, --output [format]", "Output format: text|json|yaml (default: text)")
|
|
342
|
-
.action(async (id, path, options) => {
|
|
343
|
-
const { downloadObject } = await import("./commands/object/download.js");
|
|
344
|
-
await downloadObject({ id, path, ...options });
|
|
345
|
-
});
|
|
346
|
-
object
|
|
347
|
-
.command("upload <path>")
|
|
348
|
-
.description("Upload a file as an object")
|
|
349
|
-
.option("--name <name>", "Object name (required)")
|
|
350
|
-
.option("--content-type <type>", "Content type: unspecified|text|binary|gzip|tar|tgz")
|
|
351
|
-
.option("--public", "Make object publicly accessible")
|
|
352
|
-
.option("-o, --output [format]", "Output format: text|json|yaml (default: text)")
|
|
353
|
-
.action(async (path, options) => {
|
|
354
|
-
const { uploadObject } = await import("./commands/object/upload.js");
|
|
355
|
-
if (!options.output) {
|
|
356
|
-
const { runInteractiveCommand } = await import("./utils/interactiveCommand.js");
|
|
357
|
-
await runInteractiveCommand(() => uploadObject({ path, ...options }));
|
|
358
|
-
}
|
|
359
|
-
else {
|
|
360
|
-
await uploadObject({ path, ...options });
|
|
361
|
-
}
|
|
362
|
-
});
|
|
363
|
-
object
|
|
364
|
-
.command("delete <id>")
|
|
365
|
-
.description("Delete an object (irreversible)")
|
|
366
|
-
.option("-o, --output [format]", "Output format: text|json|yaml (default: text)")
|
|
367
|
-
.action(async (id, options) => {
|
|
368
|
-
const { deleteObject } = await import("./commands/object/delete.js");
|
|
369
|
-
await deleteObject({ id, ...options });
|
|
370
|
-
});
|
|
371
|
-
// MCP server commands
|
|
372
|
-
const mcp = program
|
|
373
|
-
.command("mcp")
|
|
374
|
-
.description("Model Context Protocol (MCP) server commands");
|
|
375
|
-
mcp
|
|
376
|
-
.command("start")
|
|
377
|
-
.description("Start the MCP server")
|
|
378
|
-
.option("--http", "Use HTTP/SSE transport instead of stdio")
|
|
379
|
-
.option("-p, --port <port>", "Port to listen on for HTTP mode (default: 3000)", parseInt)
|
|
380
|
-
.action(async (options) => {
|
|
381
|
-
if (options.http) {
|
|
382
|
-
const { startMcpHttpServer } = await import("./commands/mcp-http.js");
|
|
383
|
-
await startMcpHttpServer(options.port);
|
|
384
|
-
}
|
|
385
|
-
else {
|
|
386
|
-
const { startMcpServer } = await import("./commands/mcp.js");
|
|
387
|
-
await startMcpServer();
|
|
388
|
-
}
|
|
389
|
-
});
|
|
390
|
-
mcp
|
|
391
|
-
.command("install")
|
|
392
|
-
.description("Install Runloop MCP server configuration in Claude Desktop")
|
|
393
|
-
.action(async () => {
|
|
394
|
-
const { installMcpConfig } = await import("./commands/mcp-install.js");
|
|
395
|
-
await installMcpConfig();
|
|
396
|
-
});
|
|
397
|
-
// Hidden command: 'rli mcp' without subcommand starts the server (for Claude Desktop config compatibility)
|
|
398
|
-
program
|
|
399
|
-
.command("mcp-server", { hidden: true })
|
|
400
|
-
.option("--http", "Use HTTP/SSE transport instead of stdio")
|
|
401
|
-
.option("-p, --port <port>", "Port to listen on for HTTP mode (default: 3000)", parseInt)
|
|
402
|
-
.action(async (options) => {
|
|
403
|
-
if (options.http) {
|
|
404
|
-
const { startMcpHttpServer } = await import("./commands/mcp-http.js");
|
|
405
|
-
await startMcpHttpServer(options.port);
|
|
406
|
-
}
|
|
407
|
-
else {
|
|
408
|
-
const { startMcpServer } = await import("./commands/mcp.js");
|
|
409
|
-
await startMcpServer();
|
|
410
|
-
}
|
|
411
|
-
});
|
|
13
|
+
const program = createProgram();
|
|
412
14
|
// Main CLI entry point
|
|
413
15
|
(async () => {
|
|
414
16
|
// Initialize theme system early (before any UI rendering)
|
|
@@ -417,21 +19,7 @@ program
|
|
|
417
19
|
// Check if API key is configured (except for mcp commands)
|
|
418
20
|
const args = process.argv.slice(2);
|
|
419
21
|
if (!process.env.RUNLOOP_API_KEY) {
|
|
420
|
-
console.error(
|
|
421
|
-
❌ API key not configured.
|
|
422
|
-
|
|
423
|
-
To get started:
|
|
424
|
-
1. Go to https://platform.runloop.ai/settings and create an API key
|
|
425
|
-
2. Set the environment variable:
|
|
426
|
-
|
|
427
|
-
export RUNLOOP_API_KEY=your_api_key_here
|
|
428
|
-
|
|
429
|
-
To make it permanent, add this line to your shell config:
|
|
430
|
-
• For zsh: echo 'export RUNLOOP_API_KEY=your_api_key_here' >> ~/.zshrc
|
|
431
|
-
• For bash: echo 'export RUNLOOP_API_KEY=your_api_key_here' >> ~/.bashrc
|
|
432
|
-
|
|
433
|
-
Then restart your terminal or run: source ~/.zshrc (or ~/.bashrc)
|
|
434
|
-
`);
|
|
22
|
+
console.error(getApiKeyErrorMessage());
|
|
435
23
|
processUtils.exit(1);
|
|
436
24
|
return; // Ensure execution stops
|
|
437
25
|
}
|
|
@@ -444,9 +444,10 @@ const ListBlueprintsUI = ({ onBack, onExit, }) => {
|
|
|
444
444
|
return (_jsx(DevboxCreatePage, { onBack: () => {
|
|
445
445
|
setShowCreateDevbox(false);
|
|
446
446
|
setSelectedBlueprint(null);
|
|
447
|
-
}, onCreate: () => {
|
|
447
|
+
}, onCreate: (devbox) => {
|
|
448
448
|
setShowCreateDevbox(false);
|
|
449
449
|
setSelectedBlueprint(null);
|
|
450
|
+
navigate("devbox-detail", { devboxId: devbox.id });
|
|
450
451
|
}, initialBlueprintId: selectedBlueprint.id }));
|
|
451
452
|
}
|
|
452
453
|
// Loading state
|
|
@@ -452,8 +452,12 @@ const ListDevboxesUI = ({ status, onBack, onExit, onNavigateToDetail, }) => {
|
|
|
452
452
|
if (showCreate) {
|
|
453
453
|
return (_jsx(DevboxCreatePage, { onBack: () => {
|
|
454
454
|
setShowCreate(false);
|
|
455
|
-
}, onCreate: () => {
|
|
455
|
+
}, onCreate: (devbox) => {
|
|
456
456
|
setShowCreate(false);
|
|
457
|
+
// Navigate to the newly created devbox's detail page
|
|
458
|
+
if (onNavigateToDetail) {
|
|
459
|
+
onNavigateToDetail(devbox.id);
|
|
460
|
+
}
|
|
457
461
|
} }));
|
|
458
462
|
}
|
|
459
463
|
// Actions view
|
|
@@ -16,9 +16,12 @@ import { colors } from "../../utils/theme.js";
|
|
|
16
16
|
import { useViewportHeight } from "../../hooks/useViewportHeight.js";
|
|
17
17
|
import { useExitOnCtrlC } from "../../hooks/useExitOnCtrlC.js";
|
|
18
18
|
import { useCursorPagination } from "../../hooks/useCursorPagination.js";
|
|
19
|
+
import { DevboxCreatePage } from "../../components/DevboxCreatePage.js";
|
|
20
|
+
import { useNavigation } from "../../store/navigationStore.js";
|
|
19
21
|
const DEFAULT_PAGE_SIZE = 10;
|
|
20
22
|
const ListSnapshotsUI = ({ devboxId, onBack, onExit, }) => {
|
|
21
23
|
const { exit: inkExit } = useApp();
|
|
24
|
+
const { navigate } = useNavigation();
|
|
22
25
|
const [selectedIndex, setSelectedIndex] = React.useState(0);
|
|
23
26
|
const [showPopup, setShowPopup] = React.useState(false);
|
|
24
27
|
const [selectedOperation, setSelectedOperation] = React.useState(0);
|
|
@@ -28,6 +31,7 @@ const ListSnapshotsUI = ({ devboxId, onBack, onExit, }) => {
|
|
|
28
31
|
const [operationResult, setOperationResult] = React.useState(null);
|
|
29
32
|
const [operationError, setOperationError] = React.useState(null);
|
|
30
33
|
const [operationLoading, setOperationLoading] = React.useState(false);
|
|
34
|
+
const [showCreateDevbox, setShowCreateDevbox] = React.useState(false);
|
|
31
35
|
// Calculate overhead for viewport height
|
|
32
36
|
const overhead = 13;
|
|
33
37
|
const { viewportHeight, terminalWidth } = useViewportHeight({
|
|
@@ -82,11 +86,17 @@ const ListSnapshotsUI = ({ devboxId, onBack, onExit, }) => {
|
|
|
82
86
|
pageSize: PAGE_SIZE,
|
|
83
87
|
getItemId: (snapshot) => snapshot.id,
|
|
84
88
|
pollInterval: 2000,
|
|
85
|
-
pollingEnabled: !showPopup && !executingOperation,
|
|
89
|
+
pollingEnabled: !showPopup && !executingOperation && !showCreateDevbox,
|
|
86
90
|
deps: [devboxId, PAGE_SIZE],
|
|
87
91
|
});
|
|
88
92
|
// Operations for snapshots
|
|
89
93
|
const operations = React.useMemo(() => [
|
|
94
|
+
{
|
|
95
|
+
key: "create_devbox",
|
|
96
|
+
label: "Create Devbox from Snapshot",
|
|
97
|
+
color: colors.success,
|
|
98
|
+
icon: figures.play,
|
|
99
|
+
},
|
|
90
100
|
{
|
|
91
101
|
key: "delete",
|
|
92
102
|
label: "Delete Snapshot",
|
|
@@ -164,6 +174,10 @@ const ListSnapshotsUI = ({ devboxId, onBack, onExit, }) => {
|
|
|
164
174
|
}
|
|
165
175
|
return;
|
|
166
176
|
}
|
|
177
|
+
// Handle create devbox view
|
|
178
|
+
if (showCreateDevbox) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
167
181
|
// Handle popup navigation
|
|
168
182
|
if (showPopup) {
|
|
169
183
|
if (key.upArrow && selectedOperation > 0) {
|
|
@@ -175,15 +189,27 @@ const ListSnapshotsUI = ({ devboxId, onBack, onExit, }) => {
|
|
|
175
189
|
else if (key.return) {
|
|
176
190
|
setShowPopup(false);
|
|
177
191
|
const operationKey = operations[selectedOperation].key;
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
192
|
+
if (operationKey === "create_devbox") {
|
|
193
|
+
setSelectedSnapshot(selectedSnapshotItem);
|
|
194
|
+
setShowCreateDevbox(true);
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
setSelectedSnapshot(selectedSnapshotItem);
|
|
198
|
+
setExecutingOperation(operationKey);
|
|
199
|
+
// Execute immediately after state update
|
|
200
|
+
setTimeout(() => executeOperation(), 0);
|
|
201
|
+
}
|
|
182
202
|
}
|
|
183
203
|
else if (key.escape || input === "q") {
|
|
184
204
|
setShowPopup(false);
|
|
185
205
|
setSelectedOperation(0);
|
|
186
206
|
}
|
|
207
|
+
else if (input === "c") {
|
|
208
|
+
// Create devbox hotkey
|
|
209
|
+
setShowPopup(false);
|
|
210
|
+
setSelectedSnapshot(selectedSnapshotItem);
|
|
211
|
+
setShowCreateDevbox(true);
|
|
212
|
+
}
|
|
187
213
|
else if (input === "d") {
|
|
188
214
|
// Delete hotkey
|
|
189
215
|
setShowPopup(false);
|
|
@@ -256,6 +282,17 @@ const ListSnapshotsUI = ({ devboxId, onBack, onExit, }) => {
|
|
|
256
282
|
{ label: operationLabel, active: true },
|
|
257
283
|
] }), _jsx(Header, { title: "Executing Operation" }), _jsx(SpinnerComponent, { message: messages[executingOperation] || "Please wait..." })] }));
|
|
258
284
|
}
|
|
285
|
+
// Create devbox screen
|
|
286
|
+
if (showCreateDevbox && selectedSnapshot) {
|
|
287
|
+
return (_jsx(DevboxCreatePage, { onBack: () => {
|
|
288
|
+
setShowCreateDevbox(false);
|
|
289
|
+
setSelectedSnapshot(null);
|
|
290
|
+
}, onCreate: (devbox) => {
|
|
291
|
+
setShowCreateDevbox(false);
|
|
292
|
+
setSelectedSnapshot(null);
|
|
293
|
+
navigate("devbox-detail", { devboxId: devbox.id });
|
|
294
|
+
}, initialSnapshotId: selectedSnapshot.id }));
|
|
295
|
+
}
|
|
259
296
|
// Loading state
|
|
260
297
|
if (loading && snapshots.length === 0) {
|
|
261
298
|
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
@@ -292,7 +329,11 @@ const ListSnapshotsUI = ({ devboxId, onBack, onExit, }) => {
|
|
|
292
329
|
label: op.label,
|
|
293
330
|
color: op.color,
|
|
294
331
|
icon: op.icon,
|
|
295
|
-
shortcut: op.key === "
|
|
332
|
+
shortcut: op.key === "create_devbox"
|
|
333
|
+
? "c"
|
|
334
|
+
: op.key === "delete"
|
|
335
|
+
? "d"
|
|
336
|
+
: "",
|
|
296
337
|
})), selectedOperation: selectedOperation, onClose: () => setShowPopup(false) }) })), _jsxs(Box, { marginTop: 1, paddingX: 1, children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [figures.arrowUp, figures.arrowDown, " Navigate"] }), (hasMore || hasPrev) && (_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022 ", figures.arrowLeft, figures.arrowRight, " Page"] })), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022 [a] Actions"] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022 [Esc] Back"] })] })] }));
|
|
297
338
|
};
|
|
298
339
|
// Export the UI component for use in the main menu
|
|
@@ -6,12 +6,12 @@ import { UpdateNotification } from "./UpdateNotification.js";
|
|
|
6
6
|
export const Breadcrumb = ({ items, showVersionCheck = false, }) => {
|
|
7
7
|
const env = process.env.RUNLOOP_ENV?.toLowerCase();
|
|
8
8
|
const isDevEnvironment = env === "dev";
|
|
9
|
-
return (_jsxs(Box, { marginBottom: 1, paddingX: 0, paddingY: 0, children: [_jsxs(Box, { borderStyle: "round", borderColor: colors.primary, paddingX: 2, paddingY: 0, children: [_jsx(Text, { color: colors.primary, bold: true, children: "rl" }), isDevEnvironment && (_jsxs(Text, { color: colors.error, bold: true, children: [" ", "(dev)"] })), _jsx(Text, { color: colors.textDim, children: " \u203A " }), items.map((item, index) => {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
9
|
+
return (_jsxs(Box, { justifyContent: "space-between", marginBottom: 1, paddingX: 0, paddingY: 0, children: [_jsx(Box, { flexShrink: 0, children: _jsxs(Box, { borderStyle: "round", borderColor: colors.primary, paddingX: 2, paddingY: 0, children: [_jsx(Text, { color: colors.primary, bold: true, children: "rl" }), isDevEnvironment && (_jsxs(Text, { color: colors.error, bold: true, children: [" ", "(dev)"] })), _jsx(Text, { color: colors.textDim, children: " \u203A " }), items.map((item, index) => {
|
|
10
|
+
// Limit label length to prevent Yoga layout engine errors
|
|
11
|
+
const MAX_LABEL_LENGTH = 80;
|
|
12
|
+
const truncatedLabel = item.label.length > MAX_LABEL_LENGTH
|
|
13
|
+
? item.label.substring(0, MAX_LABEL_LENGTH) + "..."
|
|
14
|
+
: item.label;
|
|
15
|
+
return (_jsxs(React.Fragment, { children: [_jsx(Text, { color: item.active ? colors.primary : colors.textDim, children: truncatedLabel }), index < items.length - 1 && (_jsx(Text, { color: colors.textDim, children: " \u203A " }))] }, index));
|
|
16
|
+
})] }) }), showVersionCheck && _jsx(UpdateNotification, {})] }));
|
|
17
17
|
};
|
|
@@ -11,7 +11,7 @@ import { Breadcrumb } from "./Breadcrumb.js";
|
|
|
11
11
|
import { MetadataDisplay } from "./MetadataDisplay.js";
|
|
12
12
|
import { colors } from "../utils/theme.js";
|
|
13
13
|
import { useExitOnCtrlC } from "../hooks/useExitOnCtrlC.js";
|
|
14
|
-
export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, }) => {
|
|
14
|
+
export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initialSnapshotId, }) => {
|
|
15
15
|
const [currentField, setCurrentField] = React.useState("create");
|
|
16
16
|
const [formData, setFormData] = React.useState({
|
|
17
17
|
name: "",
|
|
@@ -23,7 +23,7 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, }) => {
|
|
|
23
23
|
keep_alive: "3600",
|
|
24
24
|
metadata: {},
|
|
25
25
|
blueprint_id: initialBlueprintId || "",
|
|
26
|
-
snapshot_id: "",
|
|
26
|
+
snapshot_id: initialSnapshotId || "",
|
|
27
27
|
});
|
|
28
28
|
const [metadataKey, setMetadataKey] = React.useState("");
|
|
29
29
|
const [metadataValue, setMetadataValue] = React.useState("");
|
|
@@ -7,6 +7,7 @@ import { StatusBadge } from "./StatusBadge.js";
|
|
|
7
7
|
import { MetadataDisplay } from "./MetadataDisplay.js";
|
|
8
8
|
import { Breadcrumb } from "./Breadcrumb.js";
|
|
9
9
|
import { DevboxActionsMenu } from "./DevboxActionsMenu.js";
|
|
10
|
+
import { StateHistory } from "./StateHistory.js";
|
|
10
11
|
import { getDevboxUrl } from "../utils/url.js";
|
|
11
12
|
import { colors } from "../utils/theme.js";
|
|
12
13
|
import { useViewportHeight } from "../hooks/useViewportHeight.js";
|
|
@@ -433,7 +434,7 @@ export const DevboxDetailPage = ({ devbox: initialDevbox, onBack, }) => {
|
|
|
433
434
|
lp?.architecture) && (_jsxs(Box, { flexDirection: "column", paddingX: 1, flexGrow: 1, children: [_jsxs(Text, { color: colors.warning, bold: true, children: [figures.squareSmallFilled, " Resources"] }), _jsxs(Text, { dimColor: true, children: [lp?.resource_size_request && `${lp.resource_size_request}`, lp?.architecture && ` • ${lp.architecture}`, lp?.custom_cpu_cores && ` • ${lp.custom_cpu_cores}VCPU`, lp?.custom_gb_memory && ` • ${lp.custom_gb_memory}GB RAM`, lp?.custom_disk_size && ` • ${lp.custom_disk_size}GB DISC`] })] })), hasCapabilities && (_jsxs(Box, { flexDirection: "column", paddingX: 1, flexGrow: 1, children: [_jsxs(Text, { color: colors.info, bold: true, children: [figures.tick, " Capabilities"] }), _jsx(Text, { dimColor: true, children: selectedDevbox.capabilities
|
|
434
435
|
.filter((c) => c !== "unknown")
|
|
435
436
|
.join(", ") })] })), (selectedDevbox.blueprint_id || selectedDevbox.snapshot_id) && (_jsxs(Box, { flexDirection: "column", paddingX: 1, flexGrow: 1, children: [_jsxs(Text, { color: colors.secondary, bold: true, children: [figures.circleFilled, " Source"] }), selectedDevbox.blueprint_id && (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: "BP: " }), _jsx(Text, { color: colors.idColor, children: selectedDevbox.blueprint_id })] })), selectedDevbox.snapshot_id && (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: "Snap: " }), _jsx(Text, { color: colors.idColor, children: selectedDevbox.snapshot_id })] }))] }))] }), selectedDevbox.metadata &&
|
|
436
|
-
Object.keys(selectedDevbox.metadata).length > 0 && (_jsx(Box, { marginBottom: 1, paddingX: 1, children: _jsx(MetadataDisplay, { metadata: selectedDevbox.metadata, showBorder: false }) })), selectedDevbox.failure_reason && (_jsxs(Box, { marginBottom: 1, paddingX: 1, children: [_jsxs(Text, { color: colors.error, bold: true, children: [figures.cross, " "] }), _jsx(Text, { color: colors.error, dimColor: true, children: selectedDevbox.failure_reason })] })), _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { color: colors.primary, bold: true, children: [figures.play, " Actions"] }), _jsx(Box, { flexDirection: "column", children: operations.map((op, index) => {
|
|
437
|
+
Object.keys(selectedDevbox.metadata).length > 0 && (_jsx(Box, { marginBottom: 1, paddingX: 1, children: _jsx(MetadataDisplay, { metadata: selectedDevbox.metadata, showBorder: false }) })), selectedDevbox.failure_reason && (_jsxs(Box, { marginBottom: 1, paddingX: 1, children: [_jsxs(Text, { color: colors.error, bold: true, children: [figures.cross, " "] }), _jsx(Text, { color: colors.error, dimColor: true, children: selectedDevbox.failure_reason })] })), _jsx(StateHistory, { stateTransitions: selectedDevbox.state_transitions }), _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { color: colors.primary, bold: true, children: [figures.play, " Actions"] }), _jsx(Box, { flexDirection: "column", children: operations.map((op, index) => {
|
|
437
438
|
const isSelected = index === selectedOperation;
|
|
438
439
|
return (_jsxs(Box, { children: [_jsxs(Text, { color: isSelected ? colors.primary : colors.textDim, children: [isSelected ? figures.pointer : " ", " "] }), _jsxs(Text, { color: isSelected ? op.color : colors.textDim, bold: isSelected, children: [op.icon, " ", op.label] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "[", op.shortcut, "]"] })] }, op.key));
|
|
439
440
|
}) })] }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: colors.textDim, dimColor: true, children: [figures.arrowUp, figures.arrowDown, " Navigate \u2022 [Enter] Execute \u2022 [i] Full Details \u2022 [o] Browser \u2022 [q] Back"] }) })] }));
|