@runloop/rl-cli 0.2.0 → 0.3.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 +0 -10
- package/dist/cli.js +7 -13
- package/dist/commands/auth.js +2 -1
- package/dist/commands/blueprint/list.js +68 -22
- package/dist/commands/blueprint/preview.js +38 -42
- package/dist/commands/config.js +3 -2
- package/dist/commands/devbox/ssh.js +2 -1
- package/dist/commands/devbox/tunnel.js +2 -1
- package/dist/commands/mcp-http.js +6 -5
- package/dist/commands/mcp-install.js +8 -7
- package/dist/commands/mcp.js +5 -4
- package/dist/commands/menu.js +2 -1
- package/dist/components/ActionsPopup.js +18 -17
- package/dist/components/Banner.js +7 -1
- package/dist/components/Breadcrumb.js +10 -9
- package/dist/components/DevboxActionsMenu.js +18 -180
- package/dist/components/InteractiveSpawn.js +24 -14
- package/dist/components/LogsViewer.js +169 -0
- package/dist/components/MainMenu.js +2 -2
- package/dist/components/UpdateNotification.js +56 -0
- package/dist/hooks/useExitOnCtrlC.js +2 -1
- package/dist/mcp/server-http.js +2 -1
- package/dist/mcp/server.js +6 -1
- package/dist/router/Router.js +3 -1
- package/dist/screens/BlueprintLogsScreen.js +74 -0
- package/dist/services/blueprintService.js +18 -22
- package/dist/utils/CommandExecutor.js +24 -53
- package/dist/utils/client.js +4 -0
- package/dist/utils/logFormatter.js +47 -1
- package/dist/utils/output.js +4 -3
- package/dist/utils/process.js +106 -0
- package/dist/utils/processUtils.js +135 -0
- package/dist/utils/screen.js +40 -2
- package/dist/utils/ssh.js +3 -2
- package/dist/utils/terminalDetection.js +120 -32
- package/dist/utils/theme.js +34 -19
- package/dist/utils/versionCheck.js +53 -0
- package/dist/version.js +12 -0
- package/package.json +4 -5
package/README.md
CHANGED
|
@@ -253,16 +253,6 @@ npm run dev
|
|
|
253
253
|
npm start -- <command>
|
|
254
254
|
```
|
|
255
255
|
|
|
256
|
-
## Tech Stack
|
|
257
|
-
|
|
258
|
-
- [Ink](https://github.com/vadimdemedes/ink) - React for CLIs
|
|
259
|
-
- [Ink Gradient](https://github.com/sindresorhus/ink-gradient) - Gradient text
|
|
260
|
-
- [Ink Big Text](https://github.com/sindresorhus/ink-big-text) - ASCII art
|
|
261
|
-
- [Commander.js](https://github.com/tj/commander.js) - CLI framework
|
|
262
|
-
- [@runloop/api-client](https://github.com/runloopai/api-client-ts) - Runloop API client
|
|
263
|
-
- TypeScript - Type safety
|
|
264
|
-
- [Figures](https://github.com/sindresorhus/figures) - Unicode symbols
|
|
265
|
-
|
|
266
256
|
## Publishing
|
|
267
257
|
|
|
268
258
|
To publish a new version to npm:
|
package/dist/cli.js
CHANGED
|
@@ -6,21 +6,15 @@ import { deleteDevbox } from "./commands/devbox/delete.js";
|
|
|
6
6
|
import { execCommand } from "./commands/devbox/exec.js";
|
|
7
7
|
import { uploadFile } from "./commands/devbox/upload.js";
|
|
8
8
|
import { getConfig } from "./utils/config.js";
|
|
9
|
-
import {
|
|
10
|
-
import { fileURLToPath } from "url";
|
|
11
|
-
import { dirname, join } from "path";
|
|
12
|
-
// Get version from package.json
|
|
13
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
14
|
-
const __dirname = dirname(__filename);
|
|
15
|
-
const packageJson = JSON.parse(readFileSync(join(__dirname, "../package.json"), "utf8"));
|
|
16
|
-
export const VERSION = packageJson.version;
|
|
9
|
+
import { VERSION } from "./version.js";
|
|
17
10
|
import { exitAlternateScreenBuffer } from "./utils/screen.js";
|
|
11
|
+
import { processUtils } from "./utils/processUtils.js";
|
|
18
12
|
// Global Ctrl+C handler to ensure it always exits
|
|
19
|
-
|
|
13
|
+
processUtils.on("SIGINT", () => {
|
|
20
14
|
// Force exit immediately, clearing alternate screen buffer
|
|
21
15
|
exitAlternateScreenBuffer();
|
|
22
|
-
|
|
23
|
-
|
|
16
|
+
processUtils.stdout.write("\n");
|
|
17
|
+
processUtils.exit(130); // Standard exit code for SIGINT
|
|
24
18
|
});
|
|
25
19
|
const program = new Command();
|
|
26
20
|
program
|
|
@@ -55,7 +49,7 @@ config
|
|
|
55
49
|
}
|
|
56
50
|
else {
|
|
57
51
|
console.error(`\n❌ Invalid theme mode: ${mode}\nValid options: auto, light, dark\n`);
|
|
58
|
-
|
|
52
|
+
processUtils.exit(1);
|
|
59
53
|
}
|
|
60
54
|
});
|
|
61
55
|
// Devbox commands
|
|
@@ -464,7 +458,7 @@ program
|
|
|
464
458
|
const config = getConfig();
|
|
465
459
|
if (!config.apiKey) {
|
|
466
460
|
console.error("\n❌ API key not configured. Run: rli auth\n");
|
|
467
|
-
|
|
461
|
+
processUtils.exit(1);
|
|
468
462
|
}
|
|
469
463
|
}
|
|
470
464
|
// If no command provided, show main menu
|
package/dist/commands/auth.js
CHANGED
|
@@ -8,6 +8,7 @@ import { Banner } from "../components/Banner.js";
|
|
|
8
8
|
import { SuccessMessage } from "../components/SuccessMessage.js";
|
|
9
9
|
import { getSettingsUrl } from "../utils/url.js";
|
|
10
10
|
import { colors } from "../utils/theme.js";
|
|
11
|
+
import { processUtils } from "../utils/processUtils.js";
|
|
11
12
|
const AuthUI = () => {
|
|
12
13
|
const [apiKey, setApiKeyInput] = React.useState("");
|
|
13
14
|
const [saved, setSaved] = React.useState(false);
|
|
@@ -15,7 +16,7 @@ const AuthUI = () => {
|
|
|
15
16
|
if (key.return && apiKey.trim()) {
|
|
16
17
|
setApiKey(apiKey.trim());
|
|
17
18
|
setSaved(true);
|
|
18
|
-
setTimeout(() =>
|
|
19
|
+
setTimeout(() => processUtils.exit(0), 1000);
|
|
19
20
|
}
|
|
20
21
|
});
|
|
21
22
|
if (saved) {
|
|
@@ -20,6 +20,7 @@ import { DevboxCreatePage } from "../../components/DevboxCreatePage.js";
|
|
|
20
20
|
import { useExitOnCtrlC } from "../../hooks/useExitOnCtrlC.js";
|
|
21
21
|
import { useViewportHeight } from "../../hooks/useViewportHeight.js";
|
|
22
22
|
import { useCursorPagination } from "../../hooks/useCursorPagination.js";
|
|
23
|
+
import { useNavigation } from "../../store/navigationStore.js";
|
|
23
24
|
const DEFAULT_PAGE_SIZE = 10;
|
|
24
25
|
const ListBlueprintsUI = ({ onBack, onExit, }) => {
|
|
25
26
|
const { exit: inkExit } = useApp();
|
|
@@ -34,6 +35,7 @@ const ListBlueprintsUI = ({ onBack, onExit, }) => {
|
|
|
34
35
|
const [showCreateDevbox, setShowCreateDevbox] = React.useState(false);
|
|
35
36
|
const [selectedIndex, setSelectedIndex] = React.useState(0);
|
|
36
37
|
const [showPopup, setShowPopup] = React.useState(false);
|
|
38
|
+
const { navigate } = useNavigation();
|
|
37
39
|
// Calculate overhead for viewport height
|
|
38
40
|
const overhead = 13;
|
|
39
41
|
const { viewportHeight, terminalWidth } = useViewportHeight({
|
|
@@ -154,6 +156,13 @@ const ListBlueprintsUI = ({ onBack, onExit, }) => {
|
|
|
154
156
|
// Helper function to generate operations based on selected blueprint
|
|
155
157
|
const getOperationsForBlueprint = (blueprint) => {
|
|
156
158
|
const operations = [];
|
|
159
|
+
// View Logs is always available
|
|
160
|
+
operations.push({
|
|
161
|
+
key: "view_logs",
|
|
162
|
+
label: "View Logs",
|
|
163
|
+
color: colors.info,
|
|
164
|
+
icon: figures.info,
|
|
165
|
+
});
|
|
157
166
|
if (blueprint &&
|
|
158
167
|
(blueprint.status === "build_complete" ||
|
|
159
168
|
blueprint.status === "building_complete")) {
|
|
@@ -186,14 +195,33 @@ const ListBlueprintsUI = ({ onBack, onExit, }) => {
|
|
|
186
195
|
const totalPages = Math.max(1, Math.ceil(totalCount / PAGE_SIZE));
|
|
187
196
|
const startIndex = currentPage * PAGE_SIZE;
|
|
188
197
|
const endIndex = startIndex + blueprints.length;
|
|
189
|
-
const executeOperation = async () => {
|
|
198
|
+
const executeOperation = async (blueprintOverride, operationOverride) => {
|
|
190
199
|
const client = getClient();
|
|
191
|
-
|
|
192
|
-
|
|
200
|
+
// Use override if provided, otherwise use selectedBlueprint from state
|
|
201
|
+
// If neither is available, use selectedBlueprintItem as fallback
|
|
202
|
+
const blueprint = blueprintOverride || selectedBlueprint || selectedBlueprintItem;
|
|
203
|
+
// Use operation override if provided (to avoid state timing issues)
|
|
204
|
+
const operation = operationOverride || executingOperation;
|
|
205
|
+
if (!blueprint) {
|
|
206
|
+
console.error("No blueprint selected for operation");
|
|
193
207
|
return;
|
|
208
|
+
}
|
|
209
|
+
// Ensure selectedBlueprint is set in state if it wasn't already
|
|
210
|
+
if (!selectedBlueprint && blueprint) {
|
|
211
|
+
setSelectedBlueprint(blueprint);
|
|
212
|
+
}
|
|
194
213
|
try {
|
|
195
214
|
setOperationLoading(true);
|
|
196
|
-
switch (
|
|
215
|
+
switch (operation) {
|
|
216
|
+
case "view_logs":
|
|
217
|
+
// Navigate to the logs screen
|
|
218
|
+
setOperationLoading(false);
|
|
219
|
+
setExecutingOperation(null);
|
|
220
|
+
navigate("blueprint-logs", {
|
|
221
|
+
blueprintId: blueprint.id,
|
|
222
|
+
blueprintName: blueprint.name || blueprint.id,
|
|
223
|
+
});
|
|
224
|
+
return;
|
|
197
225
|
case "create_devbox":
|
|
198
226
|
setShowCreateDevbox(true);
|
|
199
227
|
setExecutingOperation(null);
|
|
@@ -226,15 +254,18 @@ const ListBlueprintsUI = ({ onBack, onExit, }) => {
|
|
|
226
254
|
useInput((input, key) => {
|
|
227
255
|
// Handle operation input mode
|
|
228
256
|
if (executingOperation && !operationResult && !operationError) {
|
|
257
|
+
// Allow escape/q to cancel any operation, even during loading
|
|
258
|
+
if (input === "q" || key.escape) {
|
|
259
|
+
setExecutingOperation(null);
|
|
260
|
+
setOperationInput("");
|
|
261
|
+
setOperationLoading(false);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
229
264
|
const currentOp = allOperations.find((op) => op.key === executingOperation);
|
|
230
265
|
if (currentOp?.needsInput) {
|
|
231
266
|
if (key.return) {
|
|
232
267
|
executeOperation();
|
|
233
268
|
}
|
|
234
|
-
else if (input === "q" || key.escape) {
|
|
235
|
-
setExecutingOperation(null);
|
|
236
|
-
setOperationInput("");
|
|
237
|
-
}
|
|
238
269
|
}
|
|
239
270
|
return;
|
|
240
271
|
}
|
|
@@ -271,7 +302,7 @@ const ListBlueprintsUI = ({ onBack, onExit, }) => {
|
|
|
271
302
|
else {
|
|
272
303
|
setSelectedBlueprint(selectedBlueprintItem);
|
|
273
304
|
setExecutingOperation(operationKey);
|
|
274
|
-
executeOperation();
|
|
305
|
+
executeOperation(selectedBlueprintItem, operationKey);
|
|
275
306
|
}
|
|
276
307
|
}
|
|
277
308
|
else if (key.escape || input === "q") {
|
|
@@ -293,7 +324,16 @@ const ListBlueprintsUI = ({ onBack, onExit, }) => {
|
|
|
293
324
|
setShowPopup(false);
|
|
294
325
|
setSelectedBlueprint(selectedBlueprintItem);
|
|
295
326
|
setExecutingOperation("delete");
|
|
296
|
-
executeOperation();
|
|
327
|
+
executeOperation(selectedBlueprintItem, "delete");
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
else if (input === "l") {
|
|
331
|
+
const logsIndex = allOperations.findIndex((op) => op.key === "view_logs");
|
|
332
|
+
if (logsIndex >= 0) {
|
|
333
|
+
setShowPopup(false);
|
|
334
|
+
setSelectedBlueprint(selectedBlueprintItem);
|
|
335
|
+
setExecutingOperation("view_logs");
|
|
336
|
+
executeOperation(selectedBlueprintItem, "view_logs");
|
|
297
337
|
}
|
|
298
338
|
}
|
|
299
339
|
return;
|
|
@@ -324,6 +364,11 @@ const ListBlueprintsUI = ({ onBack, onExit, }) => {
|
|
|
324
364
|
setShowPopup(true);
|
|
325
365
|
setSelectedOperation(0);
|
|
326
366
|
}
|
|
367
|
+
else if (input === "l" && selectedBlueprintItem) {
|
|
368
|
+
setSelectedBlueprint(selectedBlueprintItem);
|
|
369
|
+
setExecutingOperation("view_logs");
|
|
370
|
+
executeOperation(selectedBlueprintItem, "view_logs");
|
|
371
|
+
}
|
|
327
372
|
else if (input === "o" && blueprints[selectedIndex]) {
|
|
328
373
|
const url = getBlueprintUrl(blueprints[selectedIndex].id);
|
|
329
374
|
const openBrowser = async () => {
|
|
@@ -373,27 +418,26 @@ const ListBlueprintsUI = ({ onBack, onExit, }) => {
|
|
|
373
418
|
const needsInput = currentOp?.needsInput;
|
|
374
419
|
const operationLabel = currentOp?.label || "Operation";
|
|
375
420
|
if (operationLoading) {
|
|
421
|
+
const messages = {
|
|
422
|
+
delete: "Deleting blueprint...",
|
|
423
|
+
view_logs: "Fetching logs...",
|
|
424
|
+
};
|
|
376
425
|
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
377
426
|
{ label: "Blueprints" },
|
|
378
427
|
{ label: selectedBlueprint.name || selectedBlueprint.id },
|
|
379
428
|
{ label: operationLabel, active: true },
|
|
380
|
-
] }), _jsx(Header, { title: "Executing Operation" }), _jsx(SpinnerComponent, { message: "Please wait..." })] }));
|
|
429
|
+
] }), _jsx(Header, { title: "Executing Operation" }), _jsx(SpinnerComponent, { message: messages[executingOperation] || "Please wait..." }), _jsx(Box, { marginTop: 1, paddingX: 1, children: _jsx(Text, { color: colors.textDim, dimColor: true, children: "Press [q] or [esc] to cancel" }) })] }));
|
|
381
430
|
}
|
|
382
|
-
if
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
};
|
|
431
|
+
// Only show input screen if operation needs input
|
|
432
|
+
// Operations like view_logs navigate away and don't need this screen
|
|
433
|
+
if (needsInput) {
|
|
386
434
|
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
387
435
|
{ label: "Blueprints" },
|
|
388
436
|
{ label: selectedBlueprint.name || selectedBlueprint.id },
|
|
389
437
|
{ label: operationLabel, active: true },
|
|
390
|
-
] }), _jsx(Header, { title:
|
|
438
|
+
] }), _jsx(Header, { title: operationLabel }), _jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: colors.primary, bold: true, children: selectedBlueprint.name || selectedBlueprint.id }) }), _jsx(Box, { children: _jsxs(Text, { color: colors.textDim, children: [currentOp?.inputPrompt || "", " "] }) }), _jsx(Box, { marginTop: 1, children: _jsx(TextInput, { value: operationInput, onChange: setOperationInput, placeholder: currentOp?.inputPlaceholder || "" }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.textDim, dimColor: true, children: "Press [Enter] to execute \u2022 [q or esc] Cancel" }) })] })] }));
|
|
391
439
|
}
|
|
392
|
-
|
|
393
|
-
{ label: "Blueprints" },
|
|
394
|
-
{ label: selectedBlueprint.name || selectedBlueprint.id },
|
|
395
|
-
{ label: operationLabel, active: true },
|
|
396
|
-
] }), _jsx(Header, { title: operationLabel }), _jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: colors.primary, bold: true, children: selectedBlueprint.name || selectedBlueprint.id }) }), _jsx(Box, { children: _jsxs(Text, { color: colors.textDim, children: [currentOp.inputPrompt, " "] }) }), _jsx(Box, { marginTop: 1, children: _jsx(TextInput, { value: operationInput, onChange: setOperationInput, placeholder: currentOp.inputPlaceholder || "" }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.textDim, dimColor: true, children: "Press [Enter] to execute \u2022 [q or esc] Cancel" }) })] })] }));
|
|
440
|
+
// For operations that don't need input (like view_logs), fall through to list view
|
|
397
441
|
}
|
|
398
442
|
// Create devbox screen
|
|
399
443
|
if (showCreateDevbox && selectedBlueprint) {
|
|
@@ -427,7 +471,9 @@ const ListBlueprintsUI = ({ onBack, onExit, }) => {
|
|
|
427
471
|
? "c"
|
|
428
472
|
: op.key === "delete"
|
|
429
473
|
? "d"
|
|
430
|
-
: ""
|
|
474
|
+
: op.key === "view_logs"
|
|
475
|
+
? "l"
|
|
476
|
+
: "",
|
|
431
477
|
})), 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 [o] Browser"] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022 [Esc] Back"] })] })] }));
|
|
432
478
|
};
|
|
433
479
|
// Export the UI component for use in the main menu
|
|
@@ -1,49 +1,45 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
*/
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React from "react";
|
|
4
3
|
import { getClient } from "../../utils/client.js";
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
4
|
+
import { Banner } from "../../components/Banner.js";
|
|
5
|
+
import { SpinnerComponent } from "../../components/Spinner.js";
|
|
6
|
+
import { SuccessMessage } from "../../components/SuccessMessage.js";
|
|
7
|
+
import { ErrorMessage } from "../../components/ErrorMessage.js";
|
|
8
|
+
import { createExecutor } from "../../utils/CommandExecutor.js";
|
|
9
|
+
const PreviewBlueprintUI = ({ name, dockerfile, systemSetupCommands }) => {
|
|
10
|
+
const [loading, setLoading] = React.useState(true);
|
|
11
|
+
const [result, setResult] = React.useState(null);
|
|
12
|
+
const [error, setError] = React.useState(null);
|
|
13
|
+
React.useEffect(() => {
|
|
14
|
+
const previewBlueprint = async () => {
|
|
15
|
+
try {
|
|
16
|
+
const client = getClient();
|
|
17
|
+
const blueprint = await client.blueprints.preview({
|
|
18
|
+
name,
|
|
19
|
+
dockerfile,
|
|
20
|
+
system_setup_commands: systemSetupCommands,
|
|
21
|
+
});
|
|
22
|
+
setResult(blueprint);
|
|
23
|
+
}
|
|
24
|
+
catch (err) {
|
|
25
|
+
setError(err);
|
|
18
26
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
if (options.availablePorts) {
|
|
33
|
-
launchParameters.available_ports = options.availablePorts.map((port) => parseInt(port, 10));
|
|
34
|
-
}
|
|
35
|
-
if (userParameters) {
|
|
36
|
-
launchParameters.user_parameters = userParameters;
|
|
37
|
-
}
|
|
38
|
-
const preview = await client.blueprints.preview({
|
|
27
|
+
finally {
|
|
28
|
+
setLoading(false);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
previewBlueprint();
|
|
32
|
+
}, [name, dockerfile, systemSetupCommands]);
|
|
33
|
+
return (_jsxs(_Fragment, { children: [_jsx(Banner, {}), loading && _jsx(SpinnerComponent, { message: "Previewing blueprint..." }), result && (_jsx(SuccessMessage, { message: "Blueprint preview generated", details: `Name: ${result.name}\nDockerfile: ${result.dockerfile ? "Present" : "Not provided"}\nSetup Commands: ${result.systemSetupCommands?.length || 0}` })), error && (_jsx(ErrorMessage, { message: "Failed to preview blueprint", error: error }))] }));
|
|
34
|
+
};
|
|
35
|
+
export async function previewBlueprint(options) {
|
|
36
|
+
const executor = createExecutor({ output: options.output });
|
|
37
|
+
await executor.executeAction(async () => {
|
|
38
|
+
const client = executor.getClient();
|
|
39
|
+
return client.blueprints.preview({
|
|
39
40
|
name: options.name,
|
|
40
41
|
dockerfile: options.dockerfile,
|
|
41
42
|
system_setup_commands: options.systemSetupCommands,
|
|
42
|
-
launch_parameters: launchParameters,
|
|
43
43
|
});
|
|
44
|
-
|
|
45
|
-
}
|
|
46
|
-
catch (error) {
|
|
47
|
-
outputError("Failed to preview blueprint", error);
|
|
48
|
-
}
|
|
44
|
+
}, () => (_jsx(PreviewBlueprintUI, { name: options.name, dockerfile: options.dockerfile, systemSetupCommands: options.systemSetupCommands })));
|
|
49
45
|
}
|
package/dist/commands/config.js
CHANGED
|
@@ -6,6 +6,7 @@ import { setThemePreference, getThemePreference, clearDetectedTheme, } from "../
|
|
|
6
6
|
import { Header } from "../components/Header.js";
|
|
7
7
|
import { SuccessMessage } from "../components/SuccessMessage.js";
|
|
8
8
|
import { colors, getCurrentTheme, setThemeMode } from "../utils/theme.js";
|
|
9
|
+
import { processUtils } from "../utils/processUtils.js";
|
|
9
10
|
const themeOptions = [
|
|
10
11
|
{
|
|
11
12
|
value: "auto",
|
|
@@ -95,10 +96,10 @@ const StaticConfigUI = ({ action, value }) => {
|
|
|
95
96
|
clearDetectedTheme();
|
|
96
97
|
}
|
|
97
98
|
setSaved(true);
|
|
98
|
-
setTimeout(() =>
|
|
99
|
+
setTimeout(() => processUtils.exit(0), 1500);
|
|
99
100
|
}
|
|
100
101
|
else if (action === "get" || !action) {
|
|
101
|
-
setTimeout(() =>
|
|
102
|
+
setTimeout(() => processUtils.exit(0), 2000);
|
|
102
103
|
}
|
|
103
104
|
}, [action, value]);
|
|
104
105
|
const currentPreference = getThemePreference();
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { spawn } from "child_process";
|
|
5
5
|
import { getClient } from "../../utils/client.js";
|
|
6
6
|
import { output, outputError } from "../../utils/output.js";
|
|
7
|
+
import { processUtils } from "../../utils/processUtils.js";
|
|
7
8
|
import { getSSHKey, waitForReady, generateSSHConfig, checkSSHTools, getProxyCommand, } from "../../utils/ssh.js";
|
|
8
9
|
export async function sshDevbox(devboxId, options = {}) {
|
|
9
10
|
try {
|
|
@@ -59,7 +60,7 @@ export async function sshDevbox(devboxId, options = {}) {
|
|
|
59
60
|
stdio: "inherit",
|
|
60
61
|
});
|
|
61
62
|
sshProcess.on("close", (code) => {
|
|
62
|
-
|
|
63
|
+
processUtils.exit(code || 0);
|
|
63
64
|
});
|
|
64
65
|
sshProcess.on("error", (err) => {
|
|
65
66
|
outputError("SSH connection failed", err);
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { spawn } from "child_process";
|
|
5
5
|
import { getClient } from "../../utils/client.js";
|
|
6
6
|
import { output, outputError } from "../../utils/output.js";
|
|
7
|
+
import { processUtils } from "../../utils/processUtils.js";
|
|
7
8
|
import { getSSHKey, getProxyCommand, checkSSHTools } from "../../utils/ssh.js";
|
|
8
9
|
export async function createTunnel(devboxId, options) {
|
|
9
10
|
try {
|
|
@@ -56,7 +57,7 @@ export async function createTunnel(devboxId, options) {
|
|
|
56
57
|
});
|
|
57
58
|
tunnelProcess.on("close", (code) => {
|
|
58
59
|
console.log("\nTunnel closed.");
|
|
59
|
-
|
|
60
|
+
processUtils.exit(code || 0);
|
|
60
61
|
});
|
|
61
62
|
tunnelProcess.on("error", (err) => {
|
|
62
63
|
outputError("Tunnel creation failed", err);
|
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
import { spawn } from "child_process";
|
|
3
3
|
import { fileURLToPath } from "url";
|
|
4
4
|
import { dirname, join } from "path";
|
|
5
|
+
import { processUtils } from "../utils/processUtils.js";
|
|
5
6
|
const __filename = fileURLToPath(import.meta.url);
|
|
6
7
|
const __dirname = dirname(__filename);
|
|
7
8
|
export async function startMcpHttpServer(port) {
|
|
8
9
|
// Get the path to the compiled MCP HTTP server
|
|
9
10
|
const serverPath = join(__dirname, "../mcp/server-http.js");
|
|
10
|
-
const env = { ...
|
|
11
|
+
const env = { ...processUtils.env };
|
|
11
12
|
if (port) {
|
|
12
13
|
env.PORT = port.toString();
|
|
13
14
|
}
|
|
@@ -20,18 +21,18 @@ export async function startMcpHttpServer(port) {
|
|
|
20
21
|
});
|
|
21
22
|
serverProcess.on("error", (error) => {
|
|
22
23
|
console.error("Failed to start MCP HTTP server:", error);
|
|
23
|
-
|
|
24
|
+
processUtils.exit(1);
|
|
24
25
|
});
|
|
25
26
|
serverProcess.on("exit", (code) => {
|
|
26
27
|
if (code !== 0) {
|
|
27
28
|
console.error(`MCP HTTP server exited with code ${code}`);
|
|
28
|
-
|
|
29
|
+
processUtils.exit(code || 1);
|
|
29
30
|
}
|
|
30
31
|
});
|
|
31
32
|
// Handle Ctrl+C
|
|
32
|
-
|
|
33
|
+
processUtils.on("SIGINT", () => {
|
|
33
34
|
console.log("\nShutting down MCP HTTP server...");
|
|
34
35
|
serverProcess.kill("SIGINT");
|
|
35
|
-
|
|
36
|
+
processUtils.exit(0);
|
|
36
37
|
});
|
|
37
38
|
}
|
|
@@ -3,13 +3,14 @@ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
|
|
|
3
3
|
import { homedir, platform } from "os";
|
|
4
4
|
import { join } from "path";
|
|
5
5
|
import { execSync } from "child_process";
|
|
6
|
+
import { processUtils } from "../utils/processUtils.js";
|
|
6
7
|
function getClaudeConfigPath() {
|
|
7
8
|
const plat = platform();
|
|
8
9
|
if (plat === "darwin") {
|
|
9
10
|
return join(homedir(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
|
|
10
11
|
}
|
|
11
12
|
else if (plat === "win32") {
|
|
12
|
-
const appData =
|
|
13
|
+
const appData = processUtils.env.APPDATA;
|
|
13
14
|
if (!appData) {
|
|
14
15
|
throw new Error("APPDATA environment variable not found");
|
|
15
16
|
}
|
|
@@ -51,7 +52,7 @@ export async function installMcpConfig() {
|
|
|
51
52
|
catch {
|
|
52
53
|
console.error("❌ Error: Claude config file exists but is not valid JSON");
|
|
53
54
|
console.error("Please fix the file manually or delete it to create a new one");
|
|
54
|
-
|
|
55
|
+
processUtils.exit(1);
|
|
55
56
|
}
|
|
56
57
|
}
|
|
57
58
|
else {
|
|
@@ -71,20 +72,20 @@ export async function installMcpConfig() {
|
|
|
71
72
|
// Ask if they want to overwrite
|
|
72
73
|
console.log("\n❓ Do you want to overwrite it? (y/N): ");
|
|
73
74
|
// For non-interactive mode, just exit
|
|
74
|
-
if (
|
|
75
|
+
if (processUtils.stdin.isTTY) {
|
|
75
76
|
const response = await new Promise((resolve) => {
|
|
76
|
-
|
|
77
|
+
processUtils.stdin.on("data", (data) => {
|
|
77
78
|
resolve(data.toString().trim().toLowerCase());
|
|
78
79
|
});
|
|
79
80
|
});
|
|
80
81
|
if (response !== "y" && response !== "yes") {
|
|
81
82
|
console.log("\n✓ Keeping existing configuration");
|
|
82
|
-
|
|
83
|
+
processUtils.exit(0);
|
|
83
84
|
}
|
|
84
85
|
}
|
|
85
86
|
else {
|
|
86
87
|
console.log("\n✓ Keeping existing configuration (non-interactive mode)");
|
|
87
|
-
|
|
88
|
+
processUtils.exit(0);
|
|
88
89
|
}
|
|
89
90
|
}
|
|
90
91
|
// Add runloop MCP server config
|
|
@@ -116,6 +117,6 @@ export async function installMcpConfig() {
|
|
|
116
117
|
},
|
|
117
118
|
},
|
|
118
119
|
}, null, 2));
|
|
119
|
-
|
|
120
|
+
processUtils.exit(1);
|
|
120
121
|
}
|
|
121
122
|
}
|
package/dist/commands/mcp.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { spawn } from "child_process";
|
|
3
3
|
import { fileURLToPath } from "url";
|
|
4
4
|
import { dirname, join } from "path";
|
|
5
|
+
import { processUtils } from "../utils/processUtils.js";
|
|
5
6
|
const __filename = fileURLToPath(import.meta.url);
|
|
6
7
|
const __dirname = dirname(__filename);
|
|
7
8
|
export async function startMcpServer() {
|
|
@@ -14,17 +15,17 @@ export async function startMcpServer() {
|
|
|
14
15
|
});
|
|
15
16
|
serverProcess.on("error", (error) => {
|
|
16
17
|
console.error("Failed to start MCP server:", error);
|
|
17
|
-
|
|
18
|
+
processUtils.exit(1);
|
|
18
19
|
});
|
|
19
20
|
serverProcess.on("exit", (code) => {
|
|
20
21
|
if (code !== 0) {
|
|
21
22
|
console.error(`MCP server exited with code ${code}`);
|
|
22
|
-
|
|
23
|
+
processUtils.exit(code || 1);
|
|
23
24
|
}
|
|
24
25
|
});
|
|
25
26
|
// Handle Ctrl+C
|
|
26
|
-
|
|
27
|
+
processUtils.on("SIGINT", () => {
|
|
27
28
|
serverProcess.kill("SIGINT");
|
|
28
|
-
|
|
29
|
+
processUtils.exit(0);
|
|
29
30
|
});
|
|
30
31
|
}
|
package/dist/commands/menu.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { render } from "ink";
|
|
3
3
|
import { enterAlternateScreenBuffer, exitAlternateScreenBuffer, } from "../utils/screen.js";
|
|
4
|
+
import { processUtils } from "../utils/processUtils.js";
|
|
4
5
|
import { Router } from "../router/Router.js";
|
|
5
6
|
import { NavigationProvider } from "../store/navigationStore.js";
|
|
6
7
|
function AppInner() {
|
|
@@ -24,5 +25,5 @@ export async function runMainMenu(initialScreen = "menu", focusDevboxId) {
|
|
|
24
25
|
console.error("Error in menu:", error);
|
|
25
26
|
}
|
|
26
27
|
exitAlternateScreenBuffer();
|
|
27
|
-
|
|
28
|
+
processUtils.exit(0);
|
|
28
29
|
}
|
|
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
3
|
import figures from "figures";
|
|
4
4
|
import chalk from "chalk";
|
|
5
|
-
import {
|
|
5
|
+
import { getChalkTextColor, getChalkColor } from "../utils/theme.js";
|
|
6
6
|
export const ActionsPopup = ({ devbox: _devbox, operations, selectedOperation, onClose: _onClose, }) => {
|
|
7
7
|
// Calculate max width needed for content (visible characters only)
|
|
8
8
|
// CRITICAL: Ensure all values are valid numbers to prevent Yoga crashes
|
|
@@ -15,11 +15,12 @@ export const ActionsPopup = ({ devbox: _devbox, operations, selectedOperation, o
|
|
|
15
15
|
// Plus 2 for border characters = 6 total extra
|
|
16
16
|
// CRITICAL: Validate all computed widths are positive integers
|
|
17
17
|
const contentWidth = Math.max(10, maxContentWidth + 4);
|
|
18
|
-
// Get background color chalk function -
|
|
19
|
-
// In light mode
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
const
|
|
18
|
+
// Get background color chalk function - use theme colors to match the theme mode
|
|
19
|
+
// In light mode, use light background; in dark mode, use dark background
|
|
20
|
+
const popupBgHex = getChalkColor("background");
|
|
21
|
+
const popupTextHex = getChalkColor("text");
|
|
22
|
+
const bgColorFn = chalk.bgHex(popupBgHex);
|
|
23
|
+
const textColorFn = chalk.hex(popupTextHex);
|
|
23
24
|
// Helper to create background lines with proper padding including left/right margins
|
|
24
25
|
const createBgLine = (styledContent, plainContent) => {
|
|
25
26
|
const visibleLength = plainContent.length;
|
|
@@ -27,11 +28,11 @@ export const ActionsPopup = ({ devbox: _devbox, operations, selectedOperation, o
|
|
|
27
28
|
const repeatCount = Math.max(0, Math.floor(maxContentWidth - visibleLength));
|
|
28
29
|
const rightPadding = " ".repeat(repeatCount);
|
|
29
30
|
// Apply background to left padding + content + right padding
|
|
30
|
-
return
|
|
31
|
+
return bgColorFn(" " + styledContent + rightPadding + " ");
|
|
31
32
|
};
|
|
32
33
|
// Create empty line with full background
|
|
33
34
|
// CRITICAL: Validate repeat count is positive integer
|
|
34
|
-
const emptyLine =
|
|
35
|
+
const emptyLine = bgColorFn(" ".repeat(Math.max(1, Math.floor(contentWidth))));
|
|
35
36
|
// Create border lines with background and integrated title
|
|
36
37
|
const title = `${figures.play} Quick Actions`;
|
|
37
38
|
// The content between ╭ and ╮ should be exactly contentWidth
|
|
@@ -41,12 +42,12 @@ export const ActionsPopup = ({ devbox: _devbox, operations, selectedOperation, o
|
|
|
41
42
|
// CRITICAL: Validate repeat counts are non-negative integers
|
|
42
43
|
const remainingDashes = Math.max(0, Math.floor(contentWidth - titleTotalLength));
|
|
43
44
|
// Use theme primary color for borders to match theme
|
|
44
|
-
const borderColorFn =
|
|
45
|
-
const borderTop =
|
|
45
|
+
const borderColorFn = getChalkTextColor("primary");
|
|
46
|
+
const borderTop = bgColorFn(borderColorFn("╭─" + titleWithSpaces + "─".repeat(remainingDashes) + "╮"));
|
|
46
47
|
// CRITICAL: Validate contentWidth is a positive integer
|
|
47
|
-
const borderBottom =
|
|
48
|
+
const borderBottom = bgColorFn(borderColorFn("╰" + "─".repeat(Math.max(1, Math.floor(contentWidth))) + "╯"));
|
|
48
49
|
const borderSide = (content) => {
|
|
49
|
-
return
|
|
50
|
+
return bgColorFn(borderColorFn("│") + content + borderColorFn("│"));
|
|
50
51
|
};
|
|
51
52
|
return (_jsx(Box, { flexDirection: "column", alignItems: "center", children: _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: borderTop }), _jsx(Text, { children: borderSide(emptyLine) }), operations.map((op, index) => {
|
|
52
53
|
const isSelected = index === selectedOperation;
|
|
@@ -56,14 +57,14 @@ export const ActionsPopup = ({ devbox: _devbox, operations, selectedOperation, o
|
|
|
56
57
|
if (isSelected) {
|
|
57
58
|
// Selected: use operation-specific color for icon and label
|
|
58
59
|
const opColor = op.color;
|
|
59
|
-
const colorFn = chalk[opColor] ||
|
|
60
|
-
styledLine = `${
|
|
60
|
+
const colorFn = chalk[opColor] || textColorFn;
|
|
61
|
+
styledLine = `${textColorFn(pointer)} ${colorFn(op.icon)} ${colorFn.bold(op.label)} ${textColorFn(`[${op.shortcut}]`)}`;
|
|
61
62
|
}
|
|
62
63
|
else {
|
|
63
|
-
// Unselected:
|
|
64
|
-
const dimFn =
|
|
64
|
+
// Unselected: use theme's textDim color for dimmed text
|
|
65
|
+
const dimFn = getChalkTextColor("textDim");
|
|
65
66
|
styledLine = `${dimFn(pointer)} ${dimFn(op.icon)} ${dimFn(op.label)} ${dimFn(`[${op.shortcut}]`)}`;
|
|
66
67
|
}
|
|
67
68
|
return (_jsx(Text, { children: borderSide(createBgLine(styledLine, lineText)) }, op.key));
|
|
68
|
-
}), _jsx(Text, { children: borderSide(emptyLine) }), _jsx(Text, { children: borderSide(createBgLine(
|
|
69
|
+
}), _jsx(Text, { children: borderSide(emptyLine) }), _jsx(Text, { children: borderSide(createBgLine(textColorFn(`${figures.arrowUp}${figures.arrowDown} Nav • [Enter] • [Esc] Close`), `${figures.arrowUp}${figures.arrowDown} Nav • [Enter] • [Esc] Close`)) }), _jsx(Text, { children: borderSide(emptyLine) }), _jsx(Text, { children: borderBottom })] }) }));
|
|
69
70
|
};
|
|
@@ -3,6 +3,12 @@ import React from "react";
|
|
|
3
3
|
import { Box } from "ink";
|
|
4
4
|
import BigText from "ink-big-text";
|
|
5
5
|
import Gradient from "ink-gradient";
|
|
6
|
+
import { isLightMode } from "../utils/theme.js";
|
|
6
7
|
export const Banner = React.memo(() => {
|
|
7
|
-
|
|
8
|
+
// Use theme-aware gradient colors
|
|
9
|
+
// In light mode, use darker/deeper colors for better contrast on light backgrounds
|
|
10
|
+
// "teen" has darker colors (blue/purple) that work well on light backgrounds
|
|
11
|
+
// In dark mode, use the vibrant "vice" gradient (pink/cyan) that works well on dark backgrounds
|
|
12
|
+
const gradientName = isLightMode() ? "teen" : "vice";
|
|
13
|
+
return (_jsx(Box, { flexDirection: "column", alignItems: "flex-start", paddingX: 1, children: _jsx(Gradient, { name: gradientName, children: _jsx(BigText, { text: "RUNLOOP.ai", font: "simple3d" }) }) }));
|
|
8
14
|
});
|