@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/dist/mcp/server.js
CHANGED
|
@@ -3,7 +3,9 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
4
|
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
5
5
|
import Runloop from "@runloop/api-client";
|
|
6
|
+
import { VERSION } from "@runloop/api-client/version.js";
|
|
6
7
|
import Conf from "conf";
|
|
8
|
+
import { processUtils } from "../utils/processUtils.js";
|
|
7
9
|
let configInstance = null;
|
|
8
10
|
function getConfigInstance() {
|
|
9
11
|
if (!configInstance) {
|
|
@@ -51,6 +53,9 @@ function getClient() {
|
|
|
51
53
|
baseURL,
|
|
52
54
|
timeout: 10000, // 10 seconds instead of default 30 seconds
|
|
53
55
|
maxRetries: 2, // 2 retries instead of default 5 (only for retryable errors)
|
|
56
|
+
defaultHeaders: {
|
|
57
|
+
"User-Agent": `Runloop/JS ${VERSION} - CLI MCP`,
|
|
58
|
+
},
|
|
54
59
|
});
|
|
55
60
|
}
|
|
56
61
|
// Define available tools for the MCP server
|
|
@@ -452,5 +457,5 @@ async function main() {
|
|
|
452
457
|
main().catch((error) => {
|
|
453
458
|
console.error("[MCP] Fatal error in main():", error);
|
|
454
459
|
console.error("[MCP] Stack trace:", error instanceof Error ? error.stack : "N/A");
|
|
455
|
-
|
|
460
|
+
processUtils.exit(1);
|
|
456
461
|
});
|
package/dist/router/Router.js
CHANGED
|
@@ -16,6 +16,7 @@ import { DevboxDetailScreen } from "../screens/DevboxDetailScreen.js";
|
|
|
16
16
|
import { DevboxActionsScreen } from "../screens/DevboxActionsScreen.js";
|
|
17
17
|
import { DevboxCreateScreen } from "../screens/DevboxCreateScreen.js";
|
|
18
18
|
import { BlueprintListScreen } from "../screens/BlueprintListScreen.js";
|
|
19
|
+
import { BlueprintLogsScreen } from "../screens/BlueprintLogsScreen.js";
|
|
19
20
|
import { SnapshotListScreen } from "../screens/SnapshotListScreen.js";
|
|
20
21
|
import { SSHSessionScreen } from "../screens/SSHSessionScreen.js";
|
|
21
22
|
/**
|
|
@@ -46,6 +47,7 @@ export function Router() {
|
|
|
46
47
|
break;
|
|
47
48
|
case "blueprint-list":
|
|
48
49
|
case "blueprint-detail":
|
|
50
|
+
case "blueprint-logs":
|
|
49
51
|
if (!currentScreen.startsWith("blueprint")) {
|
|
50
52
|
useBlueprintStore.getState().clearAll();
|
|
51
53
|
}
|
|
@@ -64,5 +66,5 @@ export function Router() {
|
|
|
64
66
|
// and mount new component, preventing race conditions during screen transitions.
|
|
65
67
|
// The key ensures React treats this as a completely new component tree.
|
|
66
68
|
// Wrap in ErrorBoundary to catch any Yoga WASM errors gracefully.
|
|
67
|
-
return (_jsxs(ErrorBoundary, { children: [currentScreen === "menu" && (_jsx(MenuScreen, { ...params }, currentScreen)), currentScreen === "devbox-list" && (_jsx(DevboxListScreen, { ...params }, currentScreen)), currentScreen === "devbox-detail" && (_jsx(DevboxDetailScreen, { ...params }, currentScreen)), currentScreen === "devbox-actions" && (_jsx(DevboxActionsScreen, { ...params }, currentScreen)), currentScreen === "devbox-create" && (_jsx(DevboxCreateScreen, { ...params }, currentScreen)), currentScreen === "blueprint-list" && (_jsx(BlueprintListScreen, { ...params }, currentScreen)), currentScreen === "blueprint-detail" && (_jsx(BlueprintListScreen, { ...params }, currentScreen)), currentScreen === "snapshot-list" && (_jsx(SnapshotListScreen, { ...params }, currentScreen)), currentScreen === "snapshot-detail" && (_jsx(SnapshotListScreen, { ...params }, currentScreen)), currentScreen === "ssh-session" && (_jsx(SSHSessionScreen, { ...params }, currentScreen))] }, `boundary-${currentScreen}`));
|
|
69
|
+
return (_jsxs(ErrorBoundary, { children: [currentScreen === "menu" && (_jsx(MenuScreen, { ...params }, currentScreen)), currentScreen === "devbox-list" && (_jsx(DevboxListScreen, { ...params }, currentScreen)), currentScreen === "devbox-detail" && (_jsx(DevboxDetailScreen, { ...params }, currentScreen)), currentScreen === "devbox-actions" && (_jsx(DevboxActionsScreen, { ...params }, currentScreen)), currentScreen === "devbox-create" && (_jsx(DevboxCreateScreen, { ...params }, currentScreen)), currentScreen === "blueprint-list" && (_jsx(BlueprintListScreen, { ...params }, currentScreen)), currentScreen === "blueprint-detail" && (_jsx(BlueprintListScreen, { ...params }, currentScreen)), currentScreen === "blueprint-logs" && (_jsx(BlueprintLogsScreen, { ...params }, currentScreen)), currentScreen === "snapshot-list" && (_jsx(SnapshotListScreen, { ...params }, currentScreen)), currentScreen === "snapshot-detail" && (_jsx(SnapshotListScreen, { ...params }, currentScreen)), currentScreen === "ssh-session" && (_jsx(SSHSessionScreen, { ...params }, currentScreen))] }, `boundary-${currentScreen}`));
|
|
68
70
|
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* BlueprintLogsScreen - Screen for viewing blueprint build logs
|
|
4
|
+
*/
|
|
5
|
+
import React from "react";
|
|
6
|
+
import { Box, Text } from "ink";
|
|
7
|
+
import { useNavigation } from "../store/navigationStore.js";
|
|
8
|
+
import { LogsViewer } from "../components/LogsViewer.js";
|
|
9
|
+
import { Header } from "../components/Header.js";
|
|
10
|
+
import { SpinnerComponent } from "../components/Spinner.js";
|
|
11
|
+
import { Breadcrumb } from "../components/Breadcrumb.js";
|
|
12
|
+
import { ErrorMessage } from "../components/ErrorMessage.js";
|
|
13
|
+
import { getBlueprintLogs } from "../services/blueprintService.js";
|
|
14
|
+
import { colors } from "../utils/theme.js";
|
|
15
|
+
export function BlueprintLogsScreen({ blueprintId }) {
|
|
16
|
+
const { goBack, params } = useNavigation();
|
|
17
|
+
const [logs, setLogs] = React.useState([]);
|
|
18
|
+
const [loading, setLoading] = React.useState(true);
|
|
19
|
+
const [error, setError] = React.useState(null);
|
|
20
|
+
// Use blueprintId from props or params
|
|
21
|
+
const id = blueprintId || params.blueprintId;
|
|
22
|
+
React.useEffect(() => {
|
|
23
|
+
if (!id) {
|
|
24
|
+
goBack();
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
let cancelled = false;
|
|
28
|
+
const fetchLogs = async () => {
|
|
29
|
+
try {
|
|
30
|
+
setLoading(true);
|
|
31
|
+
setError(null);
|
|
32
|
+
const blueprintLogs = await getBlueprintLogs(id);
|
|
33
|
+
if (!cancelled) {
|
|
34
|
+
setLogs(Array.isArray(blueprintLogs) ? blueprintLogs : []);
|
|
35
|
+
setLoading(false);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
if (!cancelled) {
|
|
40
|
+
setError(err);
|
|
41
|
+
setLoading(false);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
fetchLogs();
|
|
46
|
+
return () => {
|
|
47
|
+
cancelled = true;
|
|
48
|
+
};
|
|
49
|
+
}, [id, goBack]);
|
|
50
|
+
if (!id) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
// Get blueprint name from params if available (for breadcrumb)
|
|
54
|
+
const blueprintName = params.blueprintName || id;
|
|
55
|
+
if (loading) {
|
|
56
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
57
|
+
{ label: "Blueprints" },
|
|
58
|
+
{ label: blueprintName },
|
|
59
|
+
{ label: "Logs", active: true },
|
|
60
|
+
] }), _jsx(Header, { title: "Loading Logs" }), _jsx(SpinnerComponent, { message: "Fetching blueprint logs..." })] }));
|
|
61
|
+
}
|
|
62
|
+
if (error) {
|
|
63
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
64
|
+
{ label: "Blueprints" },
|
|
65
|
+
{ label: blueprintName },
|
|
66
|
+
{ label: "Logs", active: true },
|
|
67
|
+
] }), _jsx(Header, { title: "Error" }), _jsx(ErrorMessage, { message: "Failed to load blueprint logs", error: error }), _jsx(Box, { marginTop: 1, paddingX: 1, children: _jsx(Text, { color: colors.textDim, dimColor: true, children: "Press [q] or [esc] to go back" }) })] }));
|
|
68
|
+
}
|
|
69
|
+
return (_jsx(LogsViewer, { logs: logs, breadcrumbItems: [
|
|
70
|
+
{ label: "Blueprints" },
|
|
71
|
+
{ label: blueprintName },
|
|
72
|
+
{ label: "Logs", active: true },
|
|
73
|
+
], onBack: goBack, title: "Blueprint Build Logs" }));
|
|
74
|
+
}
|
|
@@ -72,34 +72,30 @@ export async function getBlueprint(id) {
|
|
|
72
72
|
}
|
|
73
73
|
/**
|
|
74
74
|
* Get blueprint logs
|
|
75
|
+
* Returns the raw logs array from the API response
|
|
76
|
+
* Similar to getDevboxLogs - formatting is handled by logFormatter
|
|
75
77
|
*/
|
|
76
78
|
export async function getBlueprintLogs(id) {
|
|
77
79
|
const client = getClient();
|
|
78
80
|
const response = await client.blueprints.logs(id);
|
|
79
|
-
//
|
|
80
|
-
|
|
81
|
-
const MAX_LEVEL_LENGTH = 20;
|
|
82
|
-
const logs = [];
|
|
81
|
+
// Return the logs array directly - formatting is handled by logFormatter
|
|
82
|
+
// Ensure timestamp_ms is present (API may return timestamp or timestamp_ms)
|
|
83
83
|
if (response.logs && Array.isArray(response.logs)) {
|
|
84
|
-
response.logs.
|
|
85
|
-
//
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
84
|
+
return response.logs.map((log) => {
|
|
85
|
+
// Normalize timestamp field to timestamp_ms if needed
|
|
86
|
+
// Create a new object to avoid mutating the original
|
|
87
|
+
const normalizedLog = { ...log };
|
|
88
|
+
if (normalizedLog.timestamp && !normalizedLog.timestamp_ms) {
|
|
89
|
+
// If timestamp is a number, use it directly; if it's a string, parse it
|
|
90
|
+
if (typeof normalizedLog.timestamp === "number") {
|
|
91
|
+
normalizedLog.timestamp_ms = normalizedLog.timestamp;
|
|
92
|
+
}
|
|
93
|
+
else if (typeof normalizedLog.timestamp === "string") {
|
|
94
|
+
normalizedLog.timestamp_ms = new Date(normalizedLog.timestamp).getTime();
|
|
95
|
+
}
|
|
89
96
|
}
|
|
90
|
-
|
|
91
|
-
.replace(/\r\n/g, "\\n")
|
|
92
|
-
.replace(/\n/g, "\\n")
|
|
93
|
-
.replace(/\r/g, "\\r")
|
|
94
|
-
.replace(/\t/g, "\\t");
|
|
95
|
-
logs.push({
|
|
96
|
-
timestamp: log.timestamp,
|
|
97
|
-
message,
|
|
98
|
-
level: log.level
|
|
99
|
-
? String(log.level).substring(0, MAX_LEVEL_LENGTH)
|
|
100
|
-
: undefined,
|
|
101
|
-
});
|
|
97
|
+
return normalizedLog;
|
|
102
98
|
});
|
|
103
99
|
}
|
|
104
|
-
return
|
|
100
|
+
return [];
|
|
105
101
|
}
|
|
@@ -5,8 +5,6 @@
|
|
|
5
5
|
import { render } from "ink";
|
|
6
6
|
import { getClient } from "./client.js";
|
|
7
7
|
import { shouldUseNonInteractiveOutput, outputList, outputResult, } from "./output.js";
|
|
8
|
-
import { enableSynchronousUpdates, disableSynchronousUpdates, } from "./terminalSync.js";
|
|
9
|
-
import { exitAlternateScreenBuffer, enterAlternateScreenBuffer, } from "./screen.js";
|
|
10
8
|
import YAML from "yaml";
|
|
11
9
|
export class CommandExecutor {
|
|
12
10
|
options;
|
|
@@ -34,16 +32,13 @@ export class CommandExecutor {
|
|
|
34
32
|
return;
|
|
35
33
|
}
|
|
36
34
|
// Interactive mode
|
|
37
|
-
// Enter alternate screen buffer
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
exitOnCtrlC: false,
|
|
42
|
-
});
|
|
35
|
+
// Enter alternate screen buffer
|
|
36
|
+
process.stdout.write("\x1b[?1049h");
|
|
37
|
+
console.clear();
|
|
38
|
+
const { waitUntilExit } = render(renderUI());
|
|
43
39
|
await waitUntilExit();
|
|
44
40
|
// Exit alternate screen buffer
|
|
45
|
-
|
|
46
|
-
exitAlternateScreenBuffer();
|
|
41
|
+
process.stdout.write("\x1b[?1049l");
|
|
47
42
|
}
|
|
48
43
|
/**
|
|
49
44
|
* Execute a create/action command with automatic format handling
|
|
@@ -60,17 +55,13 @@ export class CommandExecutor {
|
|
|
60
55
|
return;
|
|
61
56
|
}
|
|
62
57
|
// Interactive mode
|
|
63
|
-
// Enter alternate screen buffer
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const { waitUntilExit } = render(renderUI()
|
|
67
|
-
patchConsole: false,
|
|
68
|
-
exitOnCtrlC: false,
|
|
69
|
-
});
|
|
58
|
+
// Enter alternate screen buffer
|
|
59
|
+
process.stdout.write("\x1b[?1049h");
|
|
60
|
+
console.clear();
|
|
61
|
+
const { waitUntilExit } = render(renderUI());
|
|
70
62
|
await waitUntilExit();
|
|
71
63
|
// Exit alternate screen buffer
|
|
72
|
-
|
|
73
|
-
exitAlternateScreenBuffer();
|
|
64
|
+
process.stdout.write("\x1b[?1049l");
|
|
74
65
|
}
|
|
75
66
|
/**
|
|
76
67
|
* Execute a delete command with automatic format handling
|
|
@@ -88,50 +79,30 @@ export class CommandExecutor {
|
|
|
88
79
|
}
|
|
89
80
|
// Interactive mode
|
|
90
81
|
// Enter alternate screen buffer
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const { waitUntilExit } = render(renderUI(), {
|
|
94
|
-
patchConsole: false,
|
|
95
|
-
exitOnCtrlC: false,
|
|
96
|
-
});
|
|
82
|
+
process.stdout.write("\x1b[?1049h");
|
|
83
|
+
const { waitUntilExit } = render(renderUI());
|
|
97
84
|
await waitUntilExit();
|
|
98
85
|
// Exit alternate screen buffer
|
|
99
|
-
|
|
100
|
-
exitAlternateScreenBuffer();
|
|
86
|
+
process.stdout.write("\x1b[?1049l");
|
|
101
87
|
}
|
|
102
88
|
/**
|
|
103
89
|
* Fetch items from an async iterator with optional filtering and limits
|
|
104
|
-
* IMPORTANT: This method tries to access the page data directly first to avoid
|
|
105
|
-
* auto-pagination issues that can cause memory errors with large datasets.
|
|
106
90
|
*/
|
|
107
91
|
async fetchFromIterator(iterator, options = {}) {
|
|
108
92
|
const { filter, limit = 100 } = options;
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
if (filter && !filter(item)) {
|
|
120
|
-
continue;
|
|
121
|
-
}
|
|
122
|
-
items.push(item);
|
|
123
|
-
count++;
|
|
124
|
-
if (count >= limit) {
|
|
125
|
-
break;
|
|
126
|
-
}
|
|
93
|
+
const items = [];
|
|
94
|
+
let count = 0;
|
|
95
|
+
for await (const item of iterator) {
|
|
96
|
+
if (filter && !filter(item)) {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
items.push(item);
|
|
100
|
+
count++;
|
|
101
|
+
if (count >= limit) {
|
|
102
|
+
break;
|
|
127
103
|
}
|
|
128
104
|
}
|
|
129
|
-
|
|
130
|
-
if (filter) {
|
|
131
|
-
items = items.filter(filter);
|
|
132
|
-
}
|
|
133
|
-
// Apply limit
|
|
134
|
-
return items.slice(0, limit);
|
|
105
|
+
return items;
|
|
135
106
|
}
|
|
136
107
|
/**
|
|
137
108
|
* Handle errors consistently across all commands
|
package/dist/utils/client.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import Runloop from "@runloop/api-client";
|
|
2
|
+
import { VERSION } from "@runloop/api-client/version.js";
|
|
2
3
|
import { getConfig } from "./config.js";
|
|
3
4
|
/**
|
|
4
5
|
* Get the base URL based on RUNLOOP_ENV environment variable
|
|
@@ -24,5 +25,8 @@ export function getClient() {
|
|
|
24
25
|
return new Runloop({
|
|
25
26
|
bearerToken: config.apiKey,
|
|
26
27
|
baseURL,
|
|
28
|
+
defaultHeaders: {
|
|
29
|
+
"User-Agent": `Runloop/JS ${VERSION} - CLI`,
|
|
30
|
+
},
|
|
27
31
|
});
|
|
28
32
|
}
|
|
@@ -83,7 +83,7 @@ export function getSourceInfo(source) {
|
|
|
83
83
|
return { abbrev, color: "white" };
|
|
84
84
|
}
|
|
85
85
|
/**
|
|
86
|
-
* Parse a log entry into formatted parts (for use in Ink UI)
|
|
86
|
+
* Parse a devbox log entry into formatted parts (for use in Ink UI)
|
|
87
87
|
*/
|
|
88
88
|
export function parseLogEntry(log) {
|
|
89
89
|
const levelInfo = getLogLevelInfo(log.level);
|
|
@@ -101,6 +101,52 @@ export function parseLogEntry(log) {
|
|
|
101
101
|
exitCodeColor: log.exit_code === 0 ? "green" : "red",
|
|
102
102
|
};
|
|
103
103
|
}
|
|
104
|
+
/**
|
|
105
|
+
* Parse a blueprint log entry into formatted parts (for use in Ink UI)
|
|
106
|
+
* Blueprint logs have a simpler structure (no source, shell_name, cmd, exit_code)
|
|
107
|
+
*/
|
|
108
|
+
export function parseBlueprintLogEntry(log) {
|
|
109
|
+
const levelInfo = getLogLevelInfo(log.level);
|
|
110
|
+
// Blueprint logs don't have a source, use "build" as default
|
|
111
|
+
const sourceInfo = getSourceInfo("build");
|
|
112
|
+
// Handle timestamp - may be timestamp_ms or timestamp
|
|
113
|
+
let timestampMs;
|
|
114
|
+
if (log.timestamp_ms !== undefined) {
|
|
115
|
+
timestampMs = log.timestamp_ms;
|
|
116
|
+
}
|
|
117
|
+
else if (log.timestamp !== undefined) {
|
|
118
|
+
const ts = log.timestamp;
|
|
119
|
+
timestampMs = typeof ts === "number" ? ts : new Date(ts).getTime();
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
// Fallback to current time if no timestamp
|
|
123
|
+
timestampMs = Date.now();
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
timestamp: formatTimestamp(timestampMs),
|
|
127
|
+
level: levelInfo.name,
|
|
128
|
+
levelColor: levelInfo.color,
|
|
129
|
+
source: sourceInfo.abbrev,
|
|
130
|
+
sourceColor: sourceInfo.color,
|
|
131
|
+
shellName: null,
|
|
132
|
+
cmd: null,
|
|
133
|
+
message: log.message || "",
|
|
134
|
+
exitCode: null,
|
|
135
|
+
exitCodeColor: "gray",
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Parse any log entry (devbox or blueprint) into formatted parts
|
|
140
|
+
*/
|
|
141
|
+
export function parseAnyLogEntry(log) {
|
|
142
|
+
// Check if it's a devbox log by looking for source field
|
|
143
|
+
if ("source" in log || "shell_name" in log || "cmd" in log) {
|
|
144
|
+
return parseLogEntry(log);
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
return parseBlueprintLogEntry(log);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
104
150
|
/**
|
|
105
151
|
* Format a log entry as a string with chalk colors (for CLI output)
|
|
106
152
|
*/
|
package/dist/utils/output.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* - outputError(message, error) - outputs error and exits
|
|
7
7
|
*/
|
|
8
8
|
import YAML from "yaml";
|
|
9
|
+
import { processUtils } from "./processUtils.js";
|
|
9
10
|
/**
|
|
10
11
|
* Resolve the output format from options
|
|
11
12
|
*/
|
|
@@ -15,7 +16,7 @@ function resolveFormat(options) {
|
|
|
15
16
|
return format;
|
|
16
17
|
}
|
|
17
18
|
console.error(`Unknown output format: ${format}. Valid options: text, json, yaml`);
|
|
18
|
-
|
|
19
|
+
processUtils.exit(1);
|
|
19
20
|
}
|
|
20
21
|
/**
|
|
21
22
|
* Format a value for text output (key-value pairs)
|
|
@@ -119,7 +120,7 @@ export function outputError(message, error) {
|
|
|
119
120
|
if (error && errorMessage !== message) {
|
|
120
121
|
console.error(` ${errorMessage}`);
|
|
121
122
|
}
|
|
122
|
-
|
|
123
|
+
processUtils.exit(1);
|
|
123
124
|
}
|
|
124
125
|
/**
|
|
125
126
|
* Output a success message for action commands
|
|
@@ -202,5 +203,5 @@ export function validateOutputFormat(format) {
|
|
|
202
203
|
return "yaml";
|
|
203
204
|
}
|
|
204
205
|
console.error(`Unknown output format: ${format}. Valid options: text, json, yaml`);
|
|
205
|
-
|
|
206
|
+
processUtils.exit(1);
|
|
206
207
|
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process utilities wrapper for testability.
|
|
3
|
+
*
|
|
4
|
+
* This module provides wrappers around Node.js process functions
|
|
5
|
+
* that can be easily mocked in tests.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Default implementation using real process
|
|
9
|
+
*/
|
|
10
|
+
const defaultProcess = {
|
|
11
|
+
exit: (code) => {
|
|
12
|
+
process.exit(code);
|
|
13
|
+
},
|
|
14
|
+
stdout: {
|
|
15
|
+
write: (data) => process.stdout.write(data),
|
|
16
|
+
get isTTY() {
|
|
17
|
+
return process.stdout.isTTY;
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
stderr: {
|
|
21
|
+
write: (data) => process.stderr.write(data),
|
|
22
|
+
},
|
|
23
|
+
get env() {
|
|
24
|
+
return process.env;
|
|
25
|
+
},
|
|
26
|
+
cwd: () => process.cwd(),
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Current process implementation - can be swapped for testing
|
|
30
|
+
*/
|
|
31
|
+
let currentProcess = defaultProcess;
|
|
32
|
+
/**
|
|
33
|
+
* Get the current process wrapper
|
|
34
|
+
*/
|
|
35
|
+
export function getProcess() {
|
|
36
|
+
return currentProcess;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Set a custom process wrapper (for testing)
|
|
40
|
+
*/
|
|
41
|
+
export function setProcess(proc) {
|
|
42
|
+
currentProcess = proc;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Reset to the default process wrapper
|
|
46
|
+
*/
|
|
47
|
+
export function resetProcess() {
|
|
48
|
+
currentProcess = defaultProcess;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Exit the process with an optional exit code
|
|
52
|
+
*/
|
|
53
|
+
export function exitProcess(code) {
|
|
54
|
+
return currentProcess.exit(code);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Write to stdout
|
|
58
|
+
*/
|
|
59
|
+
export function writeStdout(data) {
|
|
60
|
+
return currentProcess.stdout.write(data);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Write to stderr
|
|
64
|
+
*/
|
|
65
|
+
export function writeStderr(data) {
|
|
66
|
+
return currentProcess.stderr.write(data);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Check if stdout is a TTY
|
|
70
|
+
*/
|
|
71
|
+
export function isStdoutTTY() {
|
|
72
|
+
return !!currentProcess.stdout.isTTY;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Get environment variable
|
|
76
|
+
*/
|
|
77
|
+
export function getEnv(key) {
|
|
78
|
+
return currentProcess.env[key];
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Get current working directory
|
|
82
|
+
*/
|
|
83
|
+
export function getCwd() {
|
|
84
|
+
return currentProcess.cwd();
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Create a mock process for testing
|
|
88
|
+
*/
|
|
89
|
+
export function createMockProcess(overrides = {}) {
|
|
90
|
+
const exitFn = jest.fn();
|
|
91
|
+
const stdoutWriteFn = jest.fn().mockReturnValue(true);
|
|
92
|
+
const stderrWriteFn = jest.fn().mockReturnValue(true);
|
|
93
|
+
const cwdFn = jest.fn().mockReturnValue("/mock/cwd");
|
|
94
|
+
return {
|
|
95
|
+
exit: overrides.exit ?? exitFn,
|
|
96
|
+
stdout: overrides.stdout ?? {
|
|
97
|
+
write: stdoutWriteFn,
|
|
98
|
+
isTTY: true,
|
|
99
|
+
},
|
|
100
|
+
stderr: overrides.stderr ?? {
|
|
101
|
+
write: stderrWriteFn,
|
|
102
|
+
},
|
|
103
|
+
env: overrides.env ?? {},
|
|
104
|
+
cwd: overrides.cwd ?? cwdFn,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process utilities wrapper for testability.
|
|
3
|
+
*
|
|
4
|
+
* This module provides a mockable interface for process-related operations
|
|
5
|
+
* like exit, stdout/stderr writes, and terminal detection. In tests, you can
|
|
6
|
+
* replace these functions with mocks to avoid actual process termination
|
|
7
|
+
* and capture output.
|
|
8
|
+
*
|
|
9
|
+
* Usage in code:
|
|
10
|
+
* import { processUtils } from '../utils/processUtils.js';
|
|
11
|
+
* processUtils.exit(1);
|
|
12
|
+
* processUtils.stdout.write('Hello');
|
|
13
|
+
*
|
|
14
|
+
* Usage in tests:
|
|
15
|
+
* import { processUtils, resetProcessUtils } from '../utils/processUtils.js';
|
|
16
|
+
* processUtils.exit = jest.fn();
|
|
17
|
+
* // ... run tests ...
|
|
18
|
+
* resetProcessUtils(); // restore original behavior
|
|
19
|
+
*/
|
|
20
|
+
// Store original references for reset
|
|
21
|
+
const originalExit = process.exit.bind(process);
|
|
22
|
+
const originalStdoutWrite = process.stdout.write.bind(process.stdout);
|
|
23
|
+
const originalStderrWrite = process.stderr.write.bind(process.stderr);
|
|
24
|
+
const originalCwd = process.cwd.bind(process);
|
|
25
|
+
const originalOn = process.on.bind(process);
|
|
26
|
+
const originalOff = process.off.bind(process);
|
|
27
|
+
/**
|
|
28
|
+
* The main process utilities object.
|
|
29
|
+
* All properties are mutable for testing purposes.
|
|
30
|
+
*/
|
|
31
|
+
export const processUtils = {
|
|
32
|
+
exit: originalExit,
|
|
33
|
+
stdout: {
|
|
34
|
+
write: (data) => originalStdoutWrite(data),
|
|
35
|
+
get isTTY() {
|
|
36
|
+
return process.stdout.isTTY ?? false;
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
stderr: {
|
|
40
|
+
write: (data) => originalStderrWrite(data),
|
|
41
|
+
get isTTY() {
|
|
42
|
+
return process.stderr.isTTY ?? false;
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
stdin: {
|
|
46
|
+
get isTTY() {
|
|
47
|
+
return process.stdin.isTTY ?? false;
|
|
48
|
+
},
|
|
49
|
+
setRawMode: process.stdin.setRawMode?.bind(process.stdin),
|
|
50
|
+
on: process.stdin.on.bind(process.stdin),
|
|
51
|
+
removeListener: process.stdin.removeListener.bind(process.stdin),
|
|
52
|
+
},
|
|
53
|
+
cwd: originalCwd,
|
|
54
|
+
on: originalOn,
|
|
55
|
+
off: originalOff,
|
|
56
|
+
get env() {
|
|
57
|
+
return process.env;
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* Reset all process utilities to their original implementations.
|
|
62
|
+
* Call this in test teardown to restore normal behavior.
|
|
63
|
+
*/
|
|
64
|
+
export function resetProcessUtils() {
|
|
65
|
+
processUtils.exit = originalExit;
|
|
66
|
+
processUtils.stdout.write = (data) => originalStdoutWrite(data);
|
|
67
|
+
processUtils.stderr.write = (data) => originalStderrWrite(data);
|
|
68
|
+
processUtils.cwd = originalCwd;
|
|
69
|
+
processUtils.on = originalOn;
|
|
70
|
+
processUtils.off = originalOff;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Create a mock process utils for testing.
|
|
74
|
+
* Returns an object with jest mock functions.
|
|
75
|
+
*/
|
|
76
|
+
export function createMockProcessUtils() {
|
|
77
|
+
const exitMock = (() => {
|
|
78
|
+
throw new Error("process.exit called");
|
|
79
|
+
});
|
|
80
|
+
return {
|
|
81
|
+
exit: exitMock,
|
|
82
|
+
stdout: {
|
|
83
|
+
write: () => true,
|
|
84
|
+
isTTY: false,
|
|
85
|
+
},
|
|
86
|
+
stderr: {
|
|
87
|
+
write: () => true,
|
|
88
|
+
isTTY: false,
|
|
89
|
+
},
|
|
90
|
+
stdin: {
|
|
91
|
+
isTTY: false,
|
|
92
|
+
setRawMode: () => { },
|
|
93
|
+
on: () => { },
|
|
94
|
+
removeListener: () => { },
|
|
95
|
+
},
|
|
96
|
+
cwd: () => "/mock/cwd",
|
|
97
|
+
on: () => { },
|
|
98
|
+
off: () => { },
|
|
99
|
+
env: {},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Install mock process utils for testing.
|
|
104
|
+
* Returns a cleanup function to restore originals.
|
|
105
|
+
*/
|
|
106
|
+
export function installMockProcessUtils(mock) {
|
|
107
|
+
const backup = {
|
|
108
|
+
exit: processUtils.exit,
|
|
109
|
+
stdoutWrite: processUtils.stdout.write,
|
|
110
|
+
stderrWrite: processUtils.stderr.write,
|
|
111
|
+
cwd: processUtils.cwd,
|
|
112
|
+
on: processUtils.on,
|
|
113
|
+
off: processUtils.off,
|
|
114
|
+
};
|
|
115
|
+
if (mock.exit)
|
|
116
|
+
processUtils.exit = mock.exit;
|
|
117
|
+
if (mock.stdout?.write)
|
|
118
|
+
processUtils.stdout.write = mock.stdout.write;
|
|
119
|
+
if (mock.stderr?.write)
|
|
120
|
+
processUtils.stderr.write = mock.stderr.write;
|
|
121
|
+
if (mock.cwd)
|
|
122
|
+
processUtils.cwd = mock.cwd;
|
|
123
|
+
if (mock.on)
|
|
124
|
+
processUtils.on = mock.on;
|
|
125
|
+
if (mock.off)
|
|
126
|
+
processUtils.off = mock.off;
|
|
127
|
+
return () => {
|
|
128
|
+
processUtils.exit = backup.exit;
|
|
129
|
+
processUtils.stdout.write = backup.stdoutWrite;
|
|
130
|
+
processUtils.stderr.write = backup.stderrWrite;
|
|
131
|
+
processUtils.cwd = backup.cwd;
|
|
132
|
+
processUtils.on = backup.on;
|
|
133
|
+
processUtils.off = backup.off;
|
|
134
|
+
};
|
|
135
|
+
}
|