@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
|
@@ -2,15 +2,16 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import React from "react";
|
|
3
3
|
import { Box, Text } from "ink";
|
|
4
4
|
import { colors } from "../utils/theme.js";
|
|
5
|
-
|
|
5
|
+
import { UpdateNotification } from "./UpdateNotification.js";
|
|
6
|
+
export const Breadcrumb = ({ items, showVersionCheck = false, }) => {
|
|
6
7
|
const env = process.env.RUNLOOP_ENV?.toLowerCase();
|
|
7
8
|
const isDevEnvironment = env === "dev";
|
|
8
|
-
return (
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
+
// 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(Box, { paddingX: 2, marginTop: 0, children: _jsx(UpdateNotification, {}) }))] }));
|
|
16
17
|
};
|
|
@@ -13,7 +13,7 @@ import { useViewportHeight } from "../hooks/useViewportHeight.js";
|
|
|
13
13
|
import { useNavigation } from "../store/navigationStore.js";
|
|
14
14
|
import { useExitOnCtrlC } from "../hooks/useExitOnCtrlC.js";
|
|
15
15
|
import { getDevboxLogs, execCommand, suspendDevbox, resumeDevbox, shutdownDevbox, uploadFile, createSnapshot as createDevboxSnapshot, createTunnel, createSSHKey, } from "../services/devboxService.js";
|
|
16
|
-
import {
|
|
16
|
+
import { LogsViewer } from "./LogsViewer.js";
|
|
17
17
|
export const DevboxActionsMenu = ({ devbox, onBack, breadcrumbItems = [
|
|
18
18
|
{ label: "Devboxes" },
|
|
19
19
|
{ label: devbox.name || devbox.id, active: true },
|
|
@@ -25,8 +25,6 @@ export const DevboxActionsMenu = ({ devbox, onBack, breadcrumbItems = [
|
|
|
25
25
|
const [operationInput, setOperationInput] = React.useState("");
|
|
26
26
|
const [operationResult, setOperationResult] = React.useState(null);
|
|
27
27
|
const [operationError, setOperationError] = React.useState(null);
|
|
28
|
-
const [logsWrapMode, setLogsWrapMode] = React.useState(false);
|
|
29
|
-
const [logsScroll, setLogsScroll] = React.useState(0);
|
|
30
28
|
const [execScroll, setExecScroll] = React.useState(0);
|
|
31
29
|
const [copyStatus, setCopyStatus] = React.useState(null);
|
|
32
30
|
// Calculate viewport for exec output:
|
|
@@ -38,14 +36,6 @@ export const DevboxActionsMenu = ({ devbox, onBack, breadcrumbItems = [
|
|
|
38
36
|
// - Safety buffer: 1 line
|
|
39
37
|
// Total: 16 lines
|
|
40
38
|
const execViewport = useViewportHeight({ overhead: 16, minHeight: 10 });
|
|
41
|
-
// Calculate viewport for logs output:
|
|
42
|
-
// - Breadcrumb (3 lines + marginBottom): 4 lines
|
|
43
|
-
// - Log box borders: 2 lines
|
|
44
|
-
// - Stats bar (marginTop + content): 2 lines
|
|
45
|
-
// - Help bar (marginTop + content): 2 lines
|
|
46
|
-
// - Safety buffer: 1 line
|
|
47
|
-
// Total: 11 lines
|
|
48
|
-
const logsViewport = useViewportHeight({ overhead: 11, minHeight: 10 });
|
|
49
39
|
// CRITICAL: Aggressive memory cleanup to prevent heap exhaustion
|
|
50
40
|
React.useEffect(() => {
|
|
51
41
|
// Clear large data immediately when results are shown to free memory faster
|
|
@@ -185,8 +175,6 @@ export const DevboxActionsMenu = ({ devbox, onBack, breadcrumbItems = [
|
|
|
185
175
|
setOperationResult(null);
|
|
186
176
|
setOperationError(null);
|
|
187
177
|
setOperationInput("");
|
|
188
|
-
setLogsWrapMode(true);
|
|
189
|
-
setLogsScroll(0);
|
|
190
178
|
setExecScroll(0);
|
|
191
179
|
setCopyStatus(null);
|
|
192
180
|
// If skipOperationsMenu is true, go back to parent instead of operations menu
|
|
@@ -283,102 +271,6 @@ export const DevboxActionsMenu = ({ devbox, onBack, breadcrumbItems = [
|
|
|
283
271
|
};
|
|
284
272
|
copyToClipboard(output);
|
|
285
273
|
}
|
|
286
|
-
else if ((key.upArrow || input === "k") &&
|
|
287
|
-
operationResult &&
|
|
288
|
-
typeof operationResult === "object" &&
|
|
289
|
-
operationResult.__customRender === "logs") {
|
|
290
|
-
setLogsScroll(Math.max(0, logsScroll - 1));
|
|
291
|
-
}
|
|
292
|
-
else if ((key.downArrow || input === "j") &&
|
|
293
|
-
operationResult &&
|
|
294
|
-
typeof operationResult === "object" &&
|
|
295
|
-
operationResult.__customRender === "logs") {
|
|
296
|
-
setLogsScroll(logsScroll + 1);
|
|
297
|
-
}
|
|
298
|
-
else if (key.pageUp &&
|
|
299
|
-
operationResult &&
|
|
300
|
-
typeof operationResult === "object" &&
|
|
301
|
-
operationResult.__customRender === "logs") {
|
|
302
|
-
setLogsScroll(Math.max(0, logsScroll - 10));
|
|
303
|
-
}
|
|
304
|
-
else if (key.pageDown &&
|
|
305
|
-
operationResult &&
|
|
306
|
-
typeof operationResult === "object" &&
|
|
307
|
-
operationResult.__customRender === "logs") {
|
|
308
|
-
setLogsScroll(logsScroll + 10);
|
|
309
|
-
}
|
|
310
|
-
else if (input === "g" &&
|
|
311
|
-
operationResult &&
|
|
312
|
-
typeof operationResult === "object" &&
|
|
313
|
-
operationResult.__customRender === "logs") {
|
|
314
|
-
setLogsScroll(0);
|
|
315
|
-
}
|
|
316
|
-
else if (input === "G" &&
|
|
317
|
-
operationResult &&
|
|
318
|
-
typeof operationResult === "object" &&
|
|
319
|
-
operationResult.__customRender === "logs") {
|
|
320
|
-
const logs = operationResult.__logs || [];
|
|
321
|
-
const maxScroll = Math.max(0, logs.length - logsViewport.viewportHeight);
|
|
322
|
-
setLogsScroll(maxScroll);
|
|
323
|
-
}
|
|
324
|
-
else if (input === "w" &&
|
|
325
|
-
operationResult &&
|
|
326
|
-
typeof operationResult === "object" &&
|
|
327
|
-
operationResult.__customRender === "logs") {
|
|
328
|
-
setLogsWrapMode(!logsWrapMode);
|
|
329
|
-
}
|
|
330
|
-
else if (input === "c" &&
|
|
331
|
-
operationResult &&
|
|
332
|
-
typeof operationResult === "object" &&
|
|
333
|
-
operationResult.__customRender === "logs") {
|
|
334
|
-
// Copy logs to clipboard using shared formatter
|
|
335
|
-
const logs = operationResult.__logs || [];
|
|
336
|
-
const logsText = logs
|
|
337
|
-
.map((log) => {
|
|
338
|
-
const parts = parseLogEntry(log);
|
|
339
|
-
const cmd = parts.cmd ? `$ ${parts.cmd} ` : "";
|
|
340
|
-
const exitCode = parts.exitCode !== null ? `exit=${parts.exitCode} ` : "";
|
|
341
|
-
const shell = parts.shellName ? `(${parts.shellName}) ` : "";
|
|
342
|
-
return `${parts.timestamp} ${parts.level} [${parts.source}] ${shell}${cmd}${parts.message} ${exitCode}`.trim();
|
|
343
|
-
})
|
|
344
|
-
.join("\n");
|
|
345
|
-
const copyToClipboard = async (text) => {
|
|
346
|
-
const { spawn } = await import("child_process");
|
|
347
|
-
const platform = process.platform;
|
|
348
|
-
let command;
|
|
349
|
-
let args;
|
|
350
|
-
if (platform === "darwin") {
|
|
351
|
-
command = "pbcopy";
|
|
352
|
-
args = [];
|
|
353
|
-
}
|
|
354
|
-
else if (platform === "win32") {
|
|
355
|
-
command = "clip";
|
|
356
|
-
args = [];
|
|
357
|
-
}
|
|
358
|
-
else {
|
|
359
|
-
command = "xclip";
|
|
360
|
-
args = ["-selection", "clipboard"];
|
|
361
|
-
}
|
|
362
|
-
const proc = spawn(command, args);
|
|
363
|
-
proc.stdin.write(text);
|
|
364
|
-
proc.stdin.end();
|
|
365
|
-
proc.on("exit", (code) => {
|
|
366
|
-
if (code === 0) {
|
|
367
|
-
setCopyStatus("Copied to clipboard!");
|
|
368
|
-
setTimeout(() => setCopyStatus(null), 2000);
|
|
369
|
-
}
|
|
370
|
-
else {
|
|
371
|
-
setCopyStatus("Failed to copy");
|
|
372
|
-
setTimeout(() => setCopyStatus(null), 2000);
|
|
373
|
-
}
|
|
374
|
-
});
|
|
375
|
-
proc.on("error", () => {
|
|
376
|
-
setCopyStatus("Copy not supported");
|
|
377
|
-
setTimeout(() => setCopyStatus(null), 2000);
|
|
378
|
-
});
|
|
379
|
-
};
|
|
380
|
-
copyToClipboard(logsText);
|
|
381
|
-
}
|
|
382
274
|
return;
|
|
383
275
|
}
|
|
384
276
|
// Operations selection mode
|
|
@@ -560,77 +452,23 @@ export const DevboxActionsMenu = ({ devbox, onBack, breadcrumbItems = [
|
|
|
560
452
|
typeof operationResult === "object" &&
|
|
561
453
|
operationResult.__customRender === "logs") {
|
|
562
454
|
const logs = operationResult.__logs || [];
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
const MAX_MESSAGE_LENGTH = 1000;
|
|
581
|
-
const fullMessage = escapedMessage.length > MAX_MESSAGE_LENGTH
|
|
582
|
-
? escapedMessage.substring(0, MAX_MESSAGE_LENGTH) + "..."
|
|
583
|
-
: escapedMessage;
|
|
584
|
-
const cmd = parts.cmd
|
|
585
|
-
? `$ ${parts.cmd.substring(0, 40)}${parts.cmd.length > 40 ? "..." : ""} `
|
|
586
|
-
: "";
|
|
587
|
-
const exitCode = parts.exitCode !== null ? `exit=${parts.exitCode} ` : "";
|
|
588
|
-
// Map color names to theme colors
|
|
589
|
-
const levelColorMap = {
|
|
590
|
-
red: colors.error,
|
|
591
|
-
yellow: colors.warning,
|
|
592
|
-
blue: colors.primary,
|
|
593
|
-
gray: colors.textDim,
|
|
594
|
-
};
|
|
595
|
-
const sourceColorMap = {
|
|
596
|
-
magenta: "#d33682",
|
|
597
|
-
cyan: colors.info,
|
|
598
|
-
green: colors.success,
|
|
599
|
-
yellow: colors.warning,
|
|
600
|
-
gray: colors.textDim,
|
|
601
|
-
white: colors.text,
|
|
602
|
-
};
|
|
603
|
-
const levelColor = levelColorMap[parts.levelColor] || colors.textDim;
|
|
604
|
-
const sourceColor = sourceColorMap[parts.sourceColor] || colors.textDim;
|
|
605
|
-
if (logsWrapMode) {
|
|
606
|
-
return (_jsxs(Box, { children: [_jsx(Text, { color: colors.textDim, dimColor: true, children: parts.timestamp }), _jsx(Text, { children: " " }), _jsx(Text, { color: levelColor, bold: parts.levelColor === "red", children: parts.level }), _jsx(Text, { children: " " }), _jsxs(Text, { color: sourceColor, children: ["[", parts.source, "]"] }), _jsx(Text, { children: " " }), parts.shellName && (_jsxs(Text, { color: colors.textDim, dimColor: true, children: ["(", parts.shellName, ")", " "] })), cmd && _jsx(Text, { color: colors.info, children: cmd }), _jsx(Text, { children: fullMessage }), exitCode && (_jsxs(Text, { color: parts.exitCode === 0 ? colors.success : colors.error, children: [" ", exitCode] }))] }, index));
|
|
607
|
-
}
|
|
608
|
-
else {
|
|
609
|
-
// Calculate available width for message truncation
|
|
610
|
-
const timestampLen = parts.timestamp.length;
|
|
611
|
-
const levelLen = parts.level.length;
|
|
612
|
-
const sourceLen = parts.source.length + 2; // brackets
|
|
613
|
-
const shellLen = parts.shellName
|
|
614
|
-
? parts.shellName.length + 3
|
|
615
|
-
: 0;
|
|
616
|
-
const cmdLen = cmd.length;
|
|
617
|
-
const exitLen = exitCode.length;
|
|
618
|
-
const spacesLen = 5; // spaces between elements
|
|
619
|
-
const metadataWidth = timestampLen +
|
|
620
|
-
levelLen +
|
|
621
|
-
sourceLen +
|
|
622
|
-
shellLen +
|
|
623
|
-
cmdLen +
|
|
624
|
-
exitLen +
|
|
625
|
-
spacesLen;
|
|
626
|
-
const safeTerminalWidth = Math.max(80, terminalWidth);
|
|
627
|
-
const availableMessageWidth = Math.max(20, safeTerminalWidth - metadataWidth);
|
|
628
|
-
const truncatedMessage = fullMessage.length > availableMessageWidth
|
|
629
|
-
? fullMessage.substring(0, Math.max(1, availableMessageWidth - 3)) + "..."
|
|
630
|
-
: fullMessage;
|
|
631
|
-
return (_jsxs(Box, { children: [_jsx(Text, { color: colors.textDim, dimColor: true, children: parts.timestamp }), _jsx(Text, { children: " " }), _jsx(Text, { color: levelColor, bold: parts.levelColor === "red", children: parts.level }), _jsx(Text, { children: " " }), _jsxs(Text, { color: sourceColor, children: ["[", parts.source, "]"] }), _jsx(Text, { children: " " }), parts.shellName && (_jsxs(Text, { color: colors.textDim, dimColor: true, children: ["(", parts.shellName, ")", " "] })), cmd && _jsx(Text, { color: colors.info, children: cmd }), _jsx(Text, { children: truncatedMessage }), exitCode && (_jsxs(Text, { color: parts.exitCode === 0 ? colors.success : colors.error, children: [" ", exitCode] }))] }, index));
|
|
632
|
-
}
|
|
633
|
-
}) }), _jsxs(Box, { marginTop: 1, paddingX: 1, children: [_jsxs(Text, { color: colors.primary, bold: true, children: [figures.hamburger, " ", totalCount] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "total logs"] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Viewing ", actualScroll + 1, "-", Math.min(actualScroll + viewportHeight, logs.length), " of", " ", logs.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", " "] }), _jsx(Text, { color: logsWrapMode ? colors.success : colors.textDim, bold: logsWrapMode, children: logsWrapMode ? "Wrap: ON" : "Wrap: OFF" }), copyStatus && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsx(Text, { color: colors.success, bold: true, children: copyStatus })] }))] }), _jsx(Box, { marginTop: 1, paddingX: 1, children: _jsxs(Text, { color: colors.textDim, dimColor: true, children: [figures.arrowUp, figures.arrowDown, " Navigate \u2022 [g] Top \u2022 [G] Bottom \u2022 [w] Toggle Wrap \u2022 [c] Copy \u2022 [Enter], [q], or [esc] Back"] }) })] }));
|
|
455
|
+
return (_jsx(LogsViewer, { logs: logs, breadcrumbItems: [
|
|
456
|
+
...breadcrumbItems,
|
|
457
|
+
{ label: "Logs", active: true },
|
|
458
|
+
], onBack: () => {
|
|
459
|
+
// Clear large data structures immediately to prevent memory leaks
|
|
460
|
+
setOperationResult(null);
|
|
461
|
+
setOperationError(null);
|
|
462
|
+
setOperationInput("");
|
|
463
|
+
// If skipOperationsMenu is true, go back to parent instead of operations menu
|
|
464
|
+
if (skipOperationsMenu) {
|
|
465
|
+
setExecutingOperation(null);
|
|
466
|
+
onBack();
|
|
467
|
+
}
|
|
468
|
+
else {
|
|
469
|
+
setExecutingOperation(null);
|
|
470
|
+
}
|
|
471
|
+
}, title: "Logs" }));
|
|
634
472
|
}
|
|
635
473
|
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [...breadcrumbItems, { label: operationLabel, active: true }] }), _jsx(Header, { title: "Operation Result" }), operationResult && _jsx(SuccessMessage, { message: operationResult }), operationError && (_jsx(ErrorMessage, { message: "Operation failed", error: operationError })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.textDim, dimColor: true, children: "Press [Enter], [q], or [esc] to continue" }) })] }));
|
|
636
474
|
}
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import React from "react";
|
|
7
7
|
import { spawn } from "child_process";
|
|
8
|
-
import {
|
|
8
|
+
import { showCursor, clearScreen, enterAlternateScreenBuffer, } from "../utils/screen.js";
|
|
9
|
+
import { processUtils } from "../utils/processUtils.js";
|
|
9
10
|
/**
|
|
10
11
|
* Releases terminal control from Ink so a subprocess can take over.
|
|
11
12
|
* This directly manipulates stdin to bypass Ink's input handling.
|
|
@@ -15,17 +16,31 @@ function releaseTerminal() {
|
|
|
15
16
|
process.stdin.pause();
|
|
16
17
|
// Disable raw mode so the subprocess can control terminal echo and line buffering
|
|
17
18
|
// SSH needs to set its own terminal modes
|
|
18
|
-
if (
|
|
19
|
-
|
|
19
|
+
if (processUtils.stdin.isTTY && processUtils.stdin.setRawMode) {
|
|
20
|
+
processUtils.stdin.setRawMode(false);
|
|
21
|
+
}
|
|
22
|
+
// Reset terminal attributes (SGR reset) - clears any colors/styles Ink may have set
|
|
23
|
+
if (processUtils.stdout.isTTY) {
|
|
24
|
+
processUtils.stdout.write("\x1b[0m");
|
|
25
|
+
}
|
|
26
|
+
// Show cursor - Ink may have hidden it, and subprocesses expect it to be visible
|
|
27
|
+
showCursor();
|
|
28
|
+
// Flush stdout to ensure all pending writes are complete before handoff
|
|
29
|
+
if (processUtils.stdout.isTTY) {
|
|
30
|
+
processUtils.stdout.write("");
|
|
20
31
|
}
|
|
21
32
|
}
|
|
22
33
|
/**
|
|
23
34
|
* Restores terminal control to Ink after subprocess exits.
|
|
24
35
|
*/
|
|
25
36
|
function restoreTerminal() {
|
|
37
|
+
// Clear the screen to remove subprocess output before Ink renders
|
|
38
|
+
clearScreen();
|
|
39
|
+
// Re-enter alternate screen buffer for Ink's fullscreen UI
|
|
40
|
+
enterAlternateScreenBuffer();
|
|
26
41
|
// Re-enable raw mode for Ink's input handling
|
|
27
|
-
if (
|
|
28
|
-
|
|
42
|
+
if (processUtils.stdin.isTTY && processUtils.stdin.setRawMode) {
|
|
43
|
+
processUtils.stdin.setRawMode(true);
|
|
29
44
|
}
|
|
30
45
|
// Resume stdin so Ink can read input again
|
|
31
46
|
process.stdin.resume();
|
|
@@ -41,12 +56,11 @@ export const InteractiveSpawn = ({ command, args, onExit, onError, }) => {
|
|
|
41
56
|
return;
|
|
42
57
|
}
|
|
43
58
|
hasSpawnedRef.current = true;
|
|
44
|
-
// Exit alternate screen so subprocess gets a clean terminal
|
|
45
|
-
exitAlternateScreenBuffer();
|
|
46
59
|
// Release terminal from Ink's control
|
|
47
60
|
releaseTerminal();
|
|
48
|
-
//
|
|
49
|
-
setTimeout
|
|
61
|
+
// Use setImmediate to ensure terminal state is released without noticeable delay
|
|
62
|
+
// This is faster than setTimeout and ensures the event loop has processed the release
|
|
63
|
+
setImmediate(() => {
|
|
50
64
|
// Spawn the process with inherited stdio for proper TTY allocation
|
|
51
65
|
const child = spawn(command, args, {
|
|
52
66
|
stdio: "inherit", // This allows the process to use the terminal directly
|
|
@@ -59,8 +73,6 @@ export const InteractiveSpawn = ({ command, args, onExit, onError, }) => {
|
|
|
59
73
|
hasSpawnedRef.current = false;
|
|
60
74
|
// Restore terminal control to Ink
|
|
61
75
|
restoreTerminal();
|
|
62
|
-
// Re-enter alternate screen after process exits
|
|
63
|
-
enterAlternateScreenBuffer();
|
|
64
76
|
if (onExit) {
|
|
65
77
|
onExit(code);
|
|
66
78
|
}
|
|
@@ -71,13 +83,11 @@ export const InteractiveSpawn = ({ command, args, onExit, onError, }) => {
|
|
|
71
83
|
hasSpawnedRef.current = false;
|
|
72
84
|
// Restore terminal control to Ink
|
|
73
85
|
restoreTerminal();
|
|
74
|
-
// Re-enter alternate screen on error
|
|
75
|
-
enterAlternateScreenBuffer();
|
|
76
86
|
if (onError) {
|
|
77
87
|
onError(error);
|
|
78
88
|
}
|
|
79
89
|
});
|
|
80
|
-
}
|
|
90
|
+
});
|
|
81
91
|
// Cleanup function - kill the process if component unmounts
|
|
82
92
|
return () => {
|
|
83
93
|
if (processRef.current && !processRef.current.killed) {
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* LogsViewer - Shared component for viewing logs (devbox or blueprint)
|
|
4
|
+
* Extracted from DevboxActionsMenu for reuse
|
|
5
|
+
*/
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { Box, Text, useInput } from "ink";
|
|
8
|
+
import figures from "figures";
|
|
9
|
+
import { Breadcrumb } from "./Breadcrumb.js";
|
|
10
|
+
import { colors } from "../utils/theme.js";
|
|
11
|
+
import { useViewportHeight } from "../hooks/useViewportHeight.js";
|
|
12
|
+
import { parseAnyLogEntry } from "../utils/logFormatter.js";
|
|
13
|
+
export const LogsViewer = ({ logs, breadcrumbItems = [{ label: "Logs", active: true }], onBack, title = "Logs", }) => {
|
|
14
|
+
const [logsWrapMode, setLogsWrapMode] = React.useState(false);
|
|
15
|
+
const [logsScroll, setLogsScroll] = React.useState(0);
|
|
16
|
+
const [copyStatus, setCopyStatus] = React.useState(null);
|
|
17
|
+
// Calculate viewport for logs output:
|
|
18
|
+
// - Breadcrumb (3 lines + marginBottom): 4 lines
|
|
19
|
+
// - Log box borders: 2 lines
|
|
20
|
+
// - Stats bar (marginTop + content): 2 lines
|
|
21
|
+
// - Help bar (marginTop + content): 2 lines
|
|
22
|
+
// - Safety buffer: 1 line
|
|
23
|
+
// Total: 11 lines
|
|
24
|
+
const logsViewport = useViewportHeight({ overhead: 11, minHeight: 10 });
|
|
25
|
+
// Handle input for logs navigation
|
|
26
|
+
useInput((input, key) => {
|
|
27
|
+
if (key.upArrow || input === "k") {
|
|
28
|
+
setLogsScroll(Math.max(0, logsScroll - 1));
|
|
29
|
+
}
|
|
30
|
+
else if (key.downArrow || input === "j") {
|
|
31
|
+
setLogsScroll(logsScroll + 1);
|
|
32
|
+
}
|
|
33
|
+
else if (key.pageUp) {
|
|
34
|
+
setLogsScroll(Math.max(0, logsScroll - 10));
|
|
35
|
+
}
|
|
36
|
+
else if (key.pageDown) {
|
|
37
|
+
setLogsScroll(logsScroll + 10);
|
|
38
|
+
}
|
|
39
|
+
else if (input === "g") {
|
|
40
|
+
setLogsScroll(0);
|
|
41
|
+
}
|
|
42
|
+
else if (input === "G") {
|
|
43
|
+
const maxScroll = Math.max(0, logs.length - logsViewport.viewportHeight);
|
|
44
|
+
setLogsScroll(maxScroll);
|
|
45
|
+
}
|
|
46
|
+
else if (input === "w") {
|
|
47
|
+
setLogsWrapMode(!logsWrapMode);
|
|
48
|
+
}
|
|
49
|
+
else if (input === "c") {
|
|
50
|
+
// Copy logs to clipboard using shared formatter
|
|
51
|
+
const logsText = logs
|
|
52
|
+
.map((log) => {
|
|
53
|
+
const parts = parseAnyLogEntry(log);
|
|
54
|
+
const cmd = parts.cmd ? `$ ${parts.cmd} ` : "";
|
|
55
|
+
const exitCode = parts.exitCode !== null ? `exit=${parts.exitCode} ` : "";
|
|
56
|
+
const shell = parts.shellName ? `(${parts.shellName}) ` : "";
|
|
57
|
+
return `${parts.timestamp} ${parts.level} [${parts.source}] ${shell}${cmd}${parts.message} ${exitCode}`.trim();
|
|
58
|
+
})
|
|
59
|
+
.join("\n");
|
|
60
|
+
const copyToClipboard = async (text) => {
|
|
61
|
+
const { spawn } = await import("child_process");
|
|
62
|
+
const platform = process.platform;
|
|
63
|
+
let command;
|
|
64
|
+
let args;
|
|
65
|
+
if (platform === "darwin") {
|
|
66
|
+
command = "pbcopy";
|
|
67
|
+
args = [];
|
|
68
|
+
}
|
|
69
|
+
else if (platform === "win32") {
|
|
70
|
+
command = "clip";
|
|
71
|
+
args = [];
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
command = "xclip";
|
|
75
|
+
args = ["-selection", "clipboard"];
|
|
76
|
+
}
|
|
77
|
+
const proc = spawn(command, args);
|
|
78
|
+
proc.stdin.write(text);
|
|
79
|
+
proc.stdin.end();
|
|
80
|
+
proc.on("exit", (code) => {
|
|
81
|
+
if (code === 0) {
|
|
82
|
+
setCopyStatus("Copied to clipboard!");
|
|
83
|
+
setTimeout(() => setCopyStatus(null), 2000);
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
setCopyStatus("Failed to copy");
|
|
87
|
+
setTimeout(() => setCopyStatus(null), 2000);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
proc.on("error", () => {
|
|
91
|
+
setCopyStatus("Copy not supported");
|
|
92
|
+
setTimeout(() => setCopyStatus(null), 2000);
|
|
93
|
+
});
|
|
94
|
+
};
|
|
95
|
+
copyToClipboard(logsText);
|
|
96
|
+
}
|
|
97
|
+
else if (input === "q" || key.escape || key.return) {
|
|
98
|
+
onBack();
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
const viewportHeight = Math.max(1, logsViewport.viewportHeight);
|
|
102
|
+
const terminalWidth = logsViewport.terminalWidth;
|
|
103
|
+
const maxScroll = Math.max(0, logs.length - viewportHeight);
|
|
104
|
+
const actualScroll = Math.min(logsScroll, maxScroll);
|
|
105
|
+
const visibleLogs = logs.slice(actualScroll, actualScroll + viewportHeight);
|
|
106
|
+
const hasMore = actualScroll + viewportHeight < logs.length;
|
|
107
|
+
const hasLess = actualScroll > 0;
|
|
108
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: breadcrumbItems }), _jsx(Box, { flexDirection: "column", borderStyle: "round", borderColor: colors.border, paddingX: 1, children: logs.length === 0 ? (_jsx(Text, { color: colors.textDim, dimColor: true, children: "No logs available" })) : (visibleLogs.map((log, index) => {
|
|
109
|
+
const parts = parseAnyLogEntry(log);
|
|
110
|
+
// Sanitize message: escape special chars to prevent layout breaks
|
|
111
|
+
const escapedMessage = parts.message
|
|
112
|
+
.replace(/\r\n/g, "\\n")
|
|
113
|
+
.replace(/\n/g, "\\n")
|
|
114
|
+
.replace(/\r/g, "\\r")
|
|
115
|
+
.replace(/\t/g, "\\t");
|
|
116
|
+
// Limit message length to prevent Yoga layout engine errors
|
|
117
|
+
const MAX_MESSAGE_LENGTH = 1000;
|
|
118
|
+
const fullMessage = escapedMessage.length > MAX_MESSAGE_LENGTH
|
|
119
|
+
? escapedMessage.substring(0, MAX_MESSAGE_LENGTH) + "..."
|
|
120
|
+
: escapedMessage;
|
|
121
|
+
const cmd = parts.cmd
|
|
122
|
+
? `$ ${parts.cmd.substring(0, 40)}${parts.cmd.length > 40 ? "..." : ""} `
|
|
123
|
+
: "";
|
|
124
|
+
const exitCode = parts.exitCode !== null ? `exit=${parts.exitCode} ` : "";
|
|
125
|
+
// Map color names to theme colors
|
|
126
|
+
const levelColorMap = {
|
|
127
|
+
red: colors.error,
|
|
128
|
+
yellow: colors.warning,
|
|
129
|
+
blue: colors.primary,
|
|
130
|
+
gray: colors.textDim,
|
|
131
|
+
};
|
|
132
|
+
const sourceColorMap = {
|
|
133
|
+
magenta: "#d33682",
|
|
134
|
+
cyan: colors.info,
|
|
135
|
+
green: colors.success,
|
|
136
|
+
yellow: colors.warning,
|
|
137
|
+
gray: colors.textDim,
|
|
138
|
+
white: colors.text,
|
|
139
|
+
};
|
|
140
|
+
const levelColor = levelColorMap[parts.levelColor] || colors.textDim;
|
|
141
|
+
const sourceColor = sourceColorMap[parts.sourceColor] || colors.textDim;
|
|
142
|
+
if (logsWrapMode) {
|
|
143
|
+
return (_jsxs(Box, { children: [_jsx(Text, { color: colors.textDim, dimColor: true, children: parts.timestamp }), _jsx(Text, { children: " " }), _jsx(Text, { color: levelColor, bold: parts.levelColor === "red", children: parts.level }), _jsx(Text, { children: " " }), _jsxs(Text, { color: sourceColor, children: ["[", parts.source, "]"] }), _jsx(Text, { children: " " }), parts.shellName && (_jsxs(Text, { color: colors.textDim, dimColor: true, children: ["(", parts.shellName, ")", " "] })), cmd && _jsx(Text, { color: colors.info, children: cmd }), _jsx(Text, { children: fullMessage }), exitCode && (_jsxs(Text, { color: parts.exitCode === 0 ? colors.success : colors.error, children: [" ", exitCode] }))] }, index));
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
// Calculate available width for message truncation
|
|
147
|
+
const timestampLen = parts.timestamp.length;
|
|
148
|
+
const levelLen = parts.level.length;
|
|
149
|
+
const sourceLen = parts.source.length + 2; // brackets
|
|
150
|
+
const shellLen = parts.shellName ? parts.shellName.length + 3 : 0;
|
|
151
|
+
const cmdLen = cmd.length;
|
|
152
|
+
const exitLen = exitCode.length;
|
|
153
|
+
const spacesLen = 5; // spaces between elements
|
|
154
|
+
const metadataWidth = timestampLen +
|
|
155
|
+
levelLen +
|
|
156
|
+
sourceLen +
|
|
157
|
+
shellLen +
|
|
158
|
+
cmdLen +
|
|
159
|
+
exitLen +
|
|
160
|
+
spacesLen;
|
|
161
|
+
const safeTerminalWidth = Math.max(80, terminalWidth);
|
|
162
|
+
const availableMessageWidth = Math.max(20, safeTerminalWidth - metadataWidth);
|
|
163
|
+
const truncatedMessage = fullMessage.length > availableMessageWidth
|
|
164
|
+
? fullMessage.substring(0, Math.max(1, availableMessageWidth - 3)) + "..."
|
|
165
|
+
: fullMessage;
|
|
166
|
+
return (_jsxs(Box, { children: [_jsx(Text, { color: colors.textDim, dimColor: true, children: parts.timestamp }), _jsx(Text, { children: " " }), _jsx(Text, { color: levelColor, bold: parts.levelColor === "red", children: parts.level }), _jsx(Text, { children: " " }), _jsxs(Text, { color: sourceColor, children: ["[", parts.source, "]"] }), _jsx(Text, { children: " " }), parts.shellName && (_jsxs(Text, { color: colors.textDim, dimColor: true, children: ["(", parts.shellName, ")", " "] })), cmd && _jsx(Text, { color: colors.info, children: cmd }), _jsx(Text, { children: truncatedMessage }), exitCode && (_jsxs(Text, { color: parts.exitCode === 0 ? colors.success : colors.error, children: [" ", exitCode] }))] }, index));
|
|
167
|
+
}
|
|
168
|
+
})) }), _jsxs(Box, { marginTop: 1, paddingX: 1, children: [_jsxs(Text, { color: colors.primary, bold: true, children: [figures.hamburger, " ", logs.length] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "total logs"] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Viewing ", actualScroll + 1, "-", Math.min(actualScroll + viewportHeight, logs.length), " of", " ", logs.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", " "] }), _jsx(Text, { color: logsWrapMode ? colors.success : colors.textDim, bold: logsWrapMode, children: logsWrapMode ? "Wrap: ON" : "Wrap: OFF" }), copyStatus && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsx(Text, { color: colors.success, bold: true, children: copyStatus })] }))] }), _jsx(Box, { marginTop: 1, paddingX: 1, children: _jsxs(Text, { color: colors.textDim, dimColor: true, children: [figures.arrowUp, figures.arrowDown, " Navigate \u2022 [g] Top \u2022 [G] Bottom \u2022 [w] Toggle Wrap \u2022 [c] Copy \u2022 [Enter], [q], or [esc] Back"] }) })] }));
|
|
169
|
+
};
|
|
@@ -4,7 +4,7 @@ import { Box, Text, useInput, useApp } from "ink";
|
|
|
4
4
|
import figures from "figures";
|
|
5
5
|
import { Banner } from "./Banner.js";
|
|
6
6
|
import { Breadcrumb } from "./Breadcrumb.js";
|
|
7
|
-
import { VERSION } from "../
|
|
7
|
+
import { VERSION } from "../version.js";
|
|
8
8
|
import { colors } from "../utils/theme.js";
|
|
9
9
|
import { useViewportHeight } from "../hooks/useViewportHeight.js";
|
|
10
10
|
import { useExitOnCtrlC } from "../hooks/useExitOnCtrlC.js";
|
|
@@ -69,7 +69,7 @@ export const MainMenu = ({ onSelect }) => {
|
|
|
69
69
|
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 }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "- ", item.description] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "[", index + 1, "]"] })] }, item.key));
|
|
70
70
|
}) }), _jsx(Box, { paddingX: 2, marginTop: 1, children: _jsxs(Text, { color: colors.textDim, dimColor: true, children: [figures.arrowUp, figures.arrowDown, " Navigate \u2022 [1-3] Quick select \u2022 [Enter] Select \u2022 [Esc] Quit"] }) })] }));
|
|
71
71
|
}
|
|
72
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Breadcrumb, { items: [{ label: "Home", active: true }] }), _jsx(Banner, {}), _jsx(Box, { flexDirection: "column", paddingX: 2, flexShrink: 0, children: _jsx(Box, { paddingX: 1, children: _jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Cloud development environments for your team \u2022 v", VERSION] }) }) }), _jsxs(Box, { flexDirection: "column", paddingX: 2, marginTop: 1, flexGrow: 1, children: [_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(Text, { color: colors.text, bold: true, children: "Select a resource:" }) }), menuItems.map((item, index) => {
|
|
72
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Breadcrumb, { items: [{ label: "Home", active: true }], showVersionCheck: true }), _jsx(Banner, {}), _jsx(Box, { flexDirection: "column", paddingX: 2, flexShrink: 0, children: _jsx(Box, { paddingX: 1, children: _jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Cloud development environments for your team \u2022 v", VERSION] }) }) }), _jsxs(Box, { flexDirection: "column", paddingX: 2, marginTop: 1, flexGrow: 1, children: [_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(Text, { color: colors.text, bold: true, children: "Select a resource:" }) }), menuItems.map((item, index) => {
|
|
73
73
|
const isSelected = index === selectedIndex;
|
|
74
74
|
return (_jsxs(Box, { paddingX: 2, paddingY: 0, borderStyle: "single", borderColor: isSelected ? item.color : colors.border, marginTop: index === 0 ? 1 : 0, flexShrink: 0, children: [isSelected && (_jsxs(_Fragment, { children: [_jsx(Text, { color: item.color, bold: true, children: 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 }), _jsx(Text, { color: colors.textDim, children: " " }), _jsx(Text, { color: colors.textDim, dimColor: true, children: item.description }), _jsx(Text, { children: " " }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: ["[", index + 1, "]"] })] }, item.key));
|
|
75
75
|
})] }), _jsx(Box, { paddingX: 2, flexShrink: 0, children: _jsx(Box, { paddingX: 1, children: _jsxs(Text, { color: colors.textDim, dimColor: true, children: [figures.arrowUp, figures.arrowDown, " Navigate \u2022 [1-3] Quick select \u2022 [Enter] Select \u2022 [Esc] Quit"] }) }) })] }));
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { Box, Text } from "ink";
|
|
4
|
+
import { colors } from "../utils/theme.js";
|
|
5
|
+
import { VERSION } from "../version.js";
|
|
6
|
+
/**
|
|
7
|
+
* Version check component that checks npm for updates and displays a notification
|
|
8
|
+
* Restored from git history and enhanced with better visual styling
|
|
9
|
+
*/
|
|
10
|
+
export const UpdateNotification = () => {
|
|
11
|
+
const [updateAvailable, setUpdateAvailable] = React.useState(null);
|
|
12
|
+
const [isChecking, setIsChecking] = React.useState(true);
|
|
13
|
+
React.useEffect(() => {
|
|
14
|
+
const checkForUpdates = async () => {
|
|
15
|
+
try {
|
|
16
|
+
const currentVersion = VERSION;
|
|
17
|
+
const response = await fetch("https://registry.npmjs.org/@runloop/rl-cli/latest");
|
|
18
|
+
if (response.ok) {
|
|
19
|
+
const data = (await response.json());
|
|
20
|
+
const latestVersion = data.version;
|
|
21
|
+
if (latestVersion && latestVersion !== currentVersion) {
|
|
22
|
+
// Check if current version is older than latest
|
|
23
|
+
const compareVersions = (version1, version2) => {
|
|
24
|
+
const v1parts = version1.split(".").map(Number);
|
|
25
|
+
const v2parts = version2.split(".").map(Number);
|
|
26
|
+
for (let i = 0; i < Math.max(v1parts.length, v2parts.length); i++) {
|
|
27
|
+
const v1part = v1parts[i] || 0;
|
|
28
|
+
const v2part = v2parts[i] || 0;
|
|
29
|
+
if (v1part > v2part)
|
|
30
|
+
return 1;
|
|
31
|
+
if (v1part < v2part)
|
|
32
|
+
return -1;
|
|
33
|
+
}
|
|
34
|
+
return 0;
|
|
35
|
+
};
|
|
36
|
+
const isUpdateAvailable = compareVersions(latestVersion, currentVersion) > 0;
|
|
37
|
+
if (isUpdateAvailable) {
|
|
38
|
+
setUpdateAvailable(latestVersion);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// Silently fail
|
|
45
|
+
}
|
|
46
|
+
finally {
|
|
47
|
+
setIsChecking(false);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
checkForUpdates();
|
|
51
|
+
}, []);
|
|
52
|
+
if (isChecking || !updateAvailable) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
return (_jsxs(Box, { borderStyle: "round", borderColor: colors.warning, paddingX: 1, paddingY: 0, marginTop: 0, children: [_jsx(Text, { color: colors.warning, bold: true, children: "\u2728" }), _jsxs(Text, { color: colors.text, bold: true, children: [" ", "Update available:", " "] }), _jsx(Text, { color: colors.warning, bold: true, children: VERSION }), _jsxs(Text, { color: colors.primary, bold: true, children: [" ", "\u2192", " "] }), _jsx(Text, { color: colors.success, bold: true, children: updateAvailable }), _jsxs(Text, { color: colors.text, bold: true, children: [" ", "\u2022 Run:", " "] }), _jsx(Text, { color: colors.primary, bold: true, children: "npm install -g @runloop/rl-cli@latest" })] }));
|
|
56
|
+
};
|
|
@@ -4,11 +4,12 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { useInput } from "ink";
|
|
6
6
|
import { exitAlternateScreenBuffer } from "../utils/screen.js";
|
|
7
|
+
import { processUtils } from "../utils/processUtils.js";
|
|
7
8
|
export function useExitOnCtrlC() {
|
|
8
9
|
useInput((input, key) => {
|
|
9
10
|
if (key.ctrl && input === "c") {
|
|
10
11
|
exitAlternateScreenBuffer();
|
|
11
|
-
|
|
12
|
+
processUtils.exit(130); // Standard exit code for SIGINT
|
|
12
13
|
}
|
|
13
14
|
});
|
|
14
15
|
}
|
package/dist/mcp/server-http.js
CHANGED
|
@@ -4,6 +4,7 @@ import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
|
|
4
4
|
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
5
5
|
import { getClient } from "../utils/client.js";
|
|
6
6
|
import express from "express";
|
|
7
|
+
import { processUtils } from "../utils/processUtils.js";
|
|
7
8
|
// Define available tools for the MCP server
|
|
8
9
|
const TOOLS = [
|
|
9
10
|
{
|
|
@@ -412,5 +413,5 @@ async function main() {
|
|
|
412
413
|
}
|
|
413
414
|
main().catch((error) => {
|
|
414
415
|
console.error("Fatal error in main():", error);
|
|
415
|
-
|
|
416
|
+
processUtils.exit(1);
|
|
416
417
|
});
|