@runloop/rl-cli 0.1.1 → 0.2.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 +54 -0
- package/dist/cli.js +73 -60
- package/dist/commands/auth.js +0 -1
- package/dist/commands/blueprint/create.js +31 -83
- package/dist/commands/blueprint/get.js +29 -34
- package/dist/commands/blueprint/list.js +215 -213
- package/dist/commands/blueprint/logs.js +133 -37
- package/dist/commands/blueprint/preview.js +42 -38
- package/dist/commands/config.js +117 -0
- package/dist/commands/devbox/create.js +120 -40
- package/dist/commands/devbox/delete.js +17 -33
- package/dist/commands/devbox/download.js +29 -43
- package/dist/commands/devbox/exec.js +22 -39
- package/dist/commands/devbox/execAsync.js +20 -37
- package/dist/commands/devbox/get.js +13 -35
- package/dist/commands/devbox/getAsync.js +12 -34
- package/dist/commands/devbox/list.js +241 -402
- package/dist/commands/devbox/logs.js +20 -38
- package/dist/commands/devbox/read.js +29 -43
- package/dist/commands/devbox/resume.js +13 -35
- package/dist/commands/devbox/rsync.js +26 -78
- package/dist/commands/devbox/scp.js +25 -79
- package/dist/commands/devbox/sendStdin.js +41 -0
- package/dist/commands/devbox/shutdown.js +13 -35
- package/dist/commands/devbox/ssh.js +45 -78
- package/dist/commands/devbox/suspend.js +13 -35
- package/dist/commands/devbox/tunnel.js +36 -88
- package/dist/commands/devbox/upload.js +28 -36
- package/dist/commands/devbox/write.js +29 -44
- package/dist/commands/mcp-install.js +4 -3
- package/dist/commands/menu.js +24 -66
- package/dist/commands/object/delete.js +12 -34
- package/dist/commands/object/download.js +26 -74
- package/dist/commands/object/get.js +12 -34
- package/dist/commands/object/list.js +15 -93
- package/dist/commands/object/upload.js +35 -96
- package/dist/commands/snapshot/create.js +23 -39
- package/dist/commands/snapshot/delete.js +17 -33
- package/dist/commands/snapshot/get.js +16 -0
- package/dist/commands/snapshot/list.js +309 -80
- package/dist/commands/snapshot/status.js +12 -34
- package/dist/components/ActionsPopup.js +63 -39
- package/dist/components/Breadcrumb.js +10 -52
- package/dist/components/DevboxActionsMenu.js +182 -110
- package/dist/components/DevboxCreatePage.js +12 -7
- package/dist/components/DevboxDetailPage.js +76 -28
- package/dist/components/ErrorBoundary.js +29 -0
- package/dist/components/ErrorMessage.js +10 -2
- package/dist/components/Header.js +12 -4
- package/dist/components/InteractiveSpawn.js +94 -0
- package/dist/components/MainMenu.js +36 -32
- package/dist/components/MetadataDisplay.js +4 -4
- package/dist/components/OperationsMenu.js +1 -1
- package/dist/components/ResourceActionsMenu.js +4 -4
- package/dist/components/ResourceListView.js +46 -34
- package/dist/components/Spinner.js +7 -2
- package/dist/components/StatusBadge.js +1 -1
- package/dist/components/SuccessMessage.js +12 -2
- package/dist/components/Table.js +16 -6
- package/dist/hooks/useCursorPagination.js +125 -85
- package/dist/hooks/useExitOnCtrlC.js +14 -0
- package/dist/hooks/useViewportHeight.js +47 -0
- package/dist/mcp/server.js +65 -6
- package/dist/router/Router.js +68 -0
- package/dist/router/types.js +1 -0
- package/dist/screens/BlueprintListScreen.js +7 -0
- package/dist/screens/DevboxActionsScreen.js +25 -0
- package/dist/screens/DevboxCreateScreen.js +11 -0
- package/dist/screens/DevboxDetailScreen.js +60 -0
- package/dist/screens/DevboxListScreen.js +23 -0
- package/dist/screens/LogsSessionScreen.js +49 -0
- package/dist/screens/MenuScreen.js +23 -0
- package/dist/screens/SSHSessionScreen.js +55 -0
- package/dist/screens/SnapshotListScreen.js +7 -0
- package/dist/services/blueprintService.js +105 -0
- package/dist/services/devboxService.js +215 -0
- package/dist/services/snapshotService.js +81 -0
- package/dist/store/blueprintStore.js +89 -0
- package/dist/store/devboxStore.js +105 -0
- package/dist/store/index.js +7 -0
- package/dist/store/navigationStore.js +101 -0
- package/dist/store/snapshotStore.js +87 -0
- package/dist/utils/CommandExecutor.js +53 -24
- package/dist/utils/client.js +0 -2
- package/dist/utils/config.js +20 -90
- package/dist/utils/interactiveCommand.js +3 -2
- package/dist/utils/logFormatter.js +162 -0
- package/dist/utils/memoryMonitor.js +85 -0
- package/dist/utils/output.js +150 -59
- package/dist/utils/screen.js +23 -0
- package/dist/utils/ssh.js +3 -1
- package/dist/utils/sshSession.js +5 -29
- package/dist/utils/terminalDetection.js +97 -0
- package/dist/utils/terminalSync.js +39 -0
- package/dist/utils/theme.js +147 -13
- package/package.json +16 -13
|
@@ -5,6 +5,8 @@
|
|
|
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";
|
|
8
10
|
import YAML from "yaml";
|
|
9
11
|
export class CommandExecutor {
|
|
10
12
|
options;
|
|
@@ -32,13 +34,16 @@ export class CommandExecutor {
|
|
|
32
34
|
return;
|
|
33
35
|
}
|
|
34
36
|
// Interactive mode
|
|
35
|
-
// Enter alternate screen buffer
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
// Enter alternate screen buffer (this automatically clears the screen)
|
|
38
|
+
enableSynchronousUpdates();
|
|
39
|
+
const { waitUntilExit } = render(renderUI(), {
|
|
40
|
+
patchConsole: false,
|
|
41
|
+
exitOnCtrlC: false,
|
|
42
|
+
});
|
|
39
43
|
await waitUntilExit();
|
|
40
44
|
// Exit alternate screen buffer
|
|
41
|
-
|
|
45
|
+
disableSynchronousUpdates();
|
|
46
|
+
exitAlternateScreenBuffer();
|
|
42
47
|
}
|
|
43
48
|
/**
|
|
44
49
|
* Execute a create/action command with automatic format handling
|
|
@@ -55,13 +60,17 @@ export class CommandExecutor {
|
|
|
55
60
|
return;
|
|
56
61
|
}
|
|
57
62
|
// Interactive mode
|
|
58
|
-
// Enter alternate screen buffer
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const { waitUntilExit } = render(renderUI()
|
|
63
|
+
// Enter alternate screen buffer (this automatically clears the screen)
|
|
64
|
+
enterAlternateScreenBuffer();
|
|
65
|
+
enableSynchronousUpdates();
|
|
66
|
+
const { waitUntilExit } = render(renderUI(), {
|
|
67
|
+
patchConsole: false,
|
|
68
|
+
exitOnCtrlC: false,
|
|
69
|
+
});
|
|
62
70
|
await waitUntilExit();
|
|
63
71
|
// Exit alternate screen buffer
|
|
64
|
-
|
|
72
|
+
disableSynchronousUpdates();
|
|
73
|
+
exitAlternateScreenBuffer();
|
|
65
74
|
}
|
|
66
75
|
/**
|
|
67
76
|
* Execute a delete command with automatic format handling
|
|
@@ -79,30 +88,50 @@ export class CommandExecutor {
|
|
|
79
88
|
}
|
|
80
89
|
// Interactive mode
|
|
81
90
|
// Enter alternate screen buffer
|
|
82
|
-
|
|
83
|
-
|
|
91
|
+
enterAlternateScreenBuffer();
|
|
92
|
+
enableSynchronousUpdates();
|
|
93
|
+
const { waitUntilExit } = render(renderUI(), {
|
|
94
|
+
patchConsole: false,
|
|
95
|
+
exitOnCtrlC: false,
|
|
96
|
+
});
|
|
84
97
|
await waitUntilExit();
|
|
85
98
|
// Exit alternate screen buffer
|
|
86
|
-
|
|
99
|
+
disableSynchronousUpdates();
|
|
100
|
+
exitAlternateScreenBuffer();
|
|
87
101
|
}
|
|
88
102
|
/**
|
|
89
103
|
* 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.
|
|
90
106
|
*/
|
|
91
107
|
async fetchFromIterator(iterator, options = {}) {
|
|
92
108
|
const { filter, limit = 100 } = options;
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
109
|
+
let items = [];
|
|
110
|
+
// Try to access page data directly to avoid auto-pagination
|
|
111
|
+
const pageData = iterator.data || iterator.items;
|
|
112
|
+
if (pageData && Array.isArray(pageData)) {
|
|
113
|
+
items = pageData;
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
// Fall back to iteration with limit
|
|
117
|
+
let count = 0;
|
|
118
|
+
for await (const item of iterator) {
|
|
119
|
+
if (filter && !filter(item)) {
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
items.push(item);
|
|
123
|
+
count++;
|
|
124
|
+
if (count >= limit) {
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
103
127
|
}
|
|
104
128
|
}
|
|
105
|
-
|
|
129
|
+
// Apply filter if provided
|
|
130
|
+
if (filter) {
|
|
131
|
+
items = items.filter(filter);
|
|
132
|
+
}
|
|
133
|
+
// Apply limit
|
|
134
|
+
return items.slice(0, limit);
|
|
106
135
|
}
|
|
107
136
|
/**
|
|
108
137
|
* Handle errors consistently across all commands
|
package/dist/utils/client.js
CHANGED
package/dist/utils/config.js
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import Conf from "conf";
|
|
2
2
|
import { homedir } from "os";
|
|
3
3
|
import { join } from "path";
|
|
4
|
-
import { existsSync, statSync, mkdirSync, writeFileSync
|
|
5
|
-
import { fileURLToPath } from "url";
|
|
6
|
-
import { dirname } from "path";
|
|
4
|
+
import { existsSync, statSync, mkdirSync, writeFileSync } from "fs";
|
|
7
5
|
const config = new Conf({
|
|
8
6
|
projectName: "runloop-cli",
|
|
9
7
|
});
|
|
@@ -33,25 +31,6 @@ export function sshUrl() {
|
|
|
33
31
|
export function getCacheDir() {
|
|
34
32
|
return join(homedir(), ".cache", "rl-cli");
|
|
35
33
|
}
|
|
36
|
-
function getCurrentVersion() {
|
|
37
|
-
try {
|
|
38
|
-
// First try environment variable (when installed via npm)
|
|
39
|
-
if (process.env.npm_package_version) {
|
|
40
|
-
return process.env.npm_package_version;
|
|
41
|
-
}
|
|
42
|
-
// Fall back to reading package.json directly
|
|
43
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
44
|
-
const __dirname = dirname(__filename);
|
|
45
|
-
// When running from dist/, we need to go up two levels to find package.json
|
|
46
|
-
const packageJsonPath = join(__dirname, "../../package.json");
|
|
47
|
-
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
|
48
|
-
return packageJson.version;
|
|
49
|
-
}
|
|
50
|
-
catch (error) {
|
|
51
|
-
// Ultimate fallback
|
|
52
|
-
return "0.1.0";
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
34
|
export function shouldCheckForUpdates() {
|
|
56
35
|
const cacheDir = getCacheDir();
|
|
57
36
|
const cacheFile = join(cacheDir, "last_update_check");
|
|
@@ -59,13 +38,8 @@ export function shouldCheckForUpdates() {
|
|
|
59
38
|
return true;
|
|
60
39
|
}
|
|
61
40
|
const stats = statSync(cacheFile);
|
|
62
|
-
const
|
|
63
|
-
return
|
|
64
|
-
}
|
|
65
|
-
export function hasCachedUpdateInfo() {
|
|
66
|
-
const cacheDir = getCacheDir();
|
|
67
|
-
const cacheFile = join(cacheDir, "last_update_check");
|
|
68
|
-
return existsSync(cacheFile);
|
|
41
|
+
const daysSinceUpdate = (Date.now() - stats.mtime.getTime()) / (1000 * 60 * 60 * 24);
|
|
42
|
+
return daysSinceUpdate >= 1;
|
|
69
43
|
}
|
|
70
44
|
export function updateCheckCache() {
|
|
71
45
|
const cacheDir = getCacheDir();
|
|
@@ -77,67 +51,23 @@ export function updateCheckCache() {
|
|
|
77
51
|
// Touch the cache file
|
|
78
52
|
writeFileSync(cacheFile, "");
|
|
79
53
|
}
|
|
80
|
-
export
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
if (
|
|
84
|
-
|
|
85
|
-
console.error(`\n🔄 Update available: ${currentVersion} → 0.1.0\n` +
|
|
86
|
-
` Run: npm install -g @runloop/rl-cli@latest\n\n`);
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
89
|
-
// Only fetch from npm if cache is expired or forcing
|
|
90
|
-
if (!force && !shouldCheckForUpdates()) {
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
93
|
-
try {
|
|
94
|
-
const response = await fetch("https://registry.npmjs.org/@runloop/rl-cli/latest");
|
|
95
|
-
if (!response.ok) {
|
|
96
|
-
if (force) {
|
|
97
|
-
console.error("❌ Failed to check for updates\n");
|
|
98
|
-
}
|
|
99
|
-
return; // Silently fail if we can't check
|
|
100
|
-
}
|
|
101
|
-
const data = await response.json();
|
|
102
|
-
const latestVersion = data.version;
|
|
103
|
-
if (force) {
|
|
104
|
-
console.error(`Current version: ${currentVersion}\n`);
|
|
105
|
-
console.error(`Latest version: ${latestVersion}\n`);
|
|
106
|
-
}
|
|
107
|
-
if (latestVersion && latestVersion !== currentVersion) {
|
|
108
|
-
// Check if current version is older than latest
|
|
109
|
-
const isUpdateAvailable = compareVersions(latestVersion, currentVersion) > 0;
|
|
110
|
-
if (isUpdateAvailable) {
|
|
111
|
-
console.error(`\n🔄 Update available: ${currentVersion} → ${latestVersion}\n` +
|
|
112
|
-
` Run: npm install -g @runloop/rl-cli@latest\n\n`);
|
|
113
|
-
}
|
|
114
|
-
else if (force) {
|
|
115
|
-
console.error("✅ You're running the latest version!\n");
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
else if (force) {
|
|
119
|
-
console.error("✅ You're running the latest version!\n");
|
|
120
|
-
}
|
|
121
|
-
// Update the cache to indicate we've checked
|
|
122
|
-
updateCheckCache();
|
|
123
|
-
}
|
|
124
|
-
catch (error) {
|
|
125
|
-
if (force) {
|
|
126
|
-
console.error(`❌ Error checking for updates: ${error}\n`);
|
|
127
|
-
}
|
|
128
|
-
// Silently fail - don't interrupt the user's workflow
|
|
54
|
+
export function getThemePreference() {
|
|
55
|
+
// Check environment variable first, then fall back to stored config
|
|
56
|
+
const envTheme = process.env.RUNLOOP_THEME?.toLowerCase();
|
|
57
|
+
if (envTheme === "light" || envTheme === "dark" || envTheme === "auto") {
|
|
58
|
+
return envTheme;
|
|
129
59
|
}
|
|
60
|
+
return config.get("theme") || "auto";
|
|
130
61
|
}
|
|
131
|
-
function
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
return 0;
|
|
62
|
+
export function setThemePreference(theme) {
|
|
63
|
+
config.set("theme", theme);
|
|
64
|
+
}
|
|
65
|
+
export function getDetectedTheme() {
|
|
66
|
+
return config.get("detectedTheme") || null;
|
|
67
|
+
}
|
|
68
|
+
export function setDetectedTheme(theme) {
|
|
69
|
+
config.set("detectedTheme", theme);
|
|
70
|
+
}
|
|
71
|
+
export function clearDetectedTheme() {
|
|
72
|
+
config.delete("detectedTheme");
|
|
143
73
|
}
|
|
@@ -1,14 +1,15 @@
|
|
|
1
|
+
import { enterAlternateScreenBuffer, exitAlternateScreenBuffer, } from "./screen.js";
|
|
1
2
|
/**
|
|
2
3
|
* Wrapper for interactive commands that need alternate screen buffer management
|
|
3
4
|
*/
|
|
4
5
|
export async function runInteractiveCommand(command) {
|
|
5
6
|
// Enter alternate screen buffer
|
|
6
|
-
|
|
7
|
+
enterAlternateScreenBuffer();
|
|
7
8
|
try {
|
|
8
9
|
await command();
|
|
9
10
|
}
|
|
10
11
|
finally {
|
|
11
12
|
// Exit alternate screen buffer
|
|
12
|
-
|
|
13
|
+
exitAlternateScreenBuffer();
|
|
13
14
|
}
|
|
14
15
|
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared log formatting utilities for both CLI and interactive mode
|
|
3
|
+
*/
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
// Source abbreviations for consistent display
|
|
6
|
+
const SOURCE_CONFIG = {
|
|
7
|
+
setup_commands: { abbrev: "setup", color: "magenta" },
|
|
8
|
+
entrypoint: { abbrev: "entry", color: "cyan" },
|
|
9
|
+
exec: { abbrev: "exec", color: "green" },
|
|
10
|
+
files: { abbrev: "files", color: "yellow" },
|
|
11
|
+
stats: { abbrev: "stats", color: "gray" },
|
|
12
|
+
};
|
|
13
|
+
const SOURCE_WIDTH = 5;
|
|
14
|
+
/**
|
|
15
|
+
* Format timestamp based on how recent the log is
|
|
16
|
+
*/
|
|
17
|
+
export function formatTimestamp(timestampMs) {
|
|
18
|
+
const date = new Date(timestampMs);
|
|
19
|
+
const now = new Date();
|
|
20
|
+
const isToday = date.toDateString() === now.toDateString();
|
|
21
|
+
const isThisYear = date.getFullYear() === now.getFullYear();
|
|
22
|
+
const time = date.toLocaleTimeString("en-US", {
|
|
23
|
+
hour12: false,
|
|
24
|
+
hour: "2-digit",
|
|
25
|
+
minute: "2-digit",
|
|
26
|
+
second: "2-digit",
|
|
27
|
+
});
|
|
28
|
+
const ms = date.getMilliseconds().toString().padStart(3, "0");
|
|
29
|
+
if (isToday) {
|
|
30
|
+
return `${time}.${ms}`;
|
|
31
|
+
}
|
|
32
|
+
else if (isThisYear) {
|
|
33
|
+
const monthDay = date.toLocaleDateString("en-US", {
|
|
34
|
+
month: "short",
|
|
35
|
+
day: "numeric",
|
|
36
|
+
});
|
|
37
|
+
return `${monthDay} ${time}`;
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
const fullDate = date.toLocaleDateString("en-US", {
|
|
41
|
+
year: "numeric",
|
|
42
|
+
month: "short",
|
|
43
|
+
day: "numeric",
|
|
44
|
+
});
|
|
45
|
+
return `${fullDate} ${time}`;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Get log level info (normalized name and color)
|
|
50
|
+
*/
|
|
51
|
+
export function getLogLevelInfo(level) {
|
|
52
|
+
const normalized = level.toUpperCase();
|
|
53
|
+
switch (normalized) {
|
|
54
|
+
case "ERROR":
|
|
55
|
+
case "ERR":
|
|
56
|
+
return { name: "ERROR", color: "red" };
|
|
57
|
+
case "WARN":
|
|
58
|
+
case "WARNING":
|
|
59
|
+
return { name: "WARN ", color: "yellow" };
|
|
60
|
+
case "INFO":
|
|
61
|
+
return { name: "INFO ", color: "blue" };
|
|
62
|
+
case "DEBUG":
|
|
63
|
+
return { name: "DEBUG", color: "gray" };
|
|
64
|
+
default:
|
|
65
|
+
return { name: normalized.padEnd(5), color: "gray" };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Get source info (abbreviated name and color)
|
|
70
|
+
*/
|
|
71
|
+
export function getSourceInfo(source) {
|
|
72
|
+
if (!source) {
|
|
73
|
+
return { abbrev: "sys".padEnd(SOURCE_WIDTH), color: "gray" };
|
|
74
|
+
}
|
|
75
|
+
const config = SOURCE_CONFIG[source];
|
|
76
|
+
if (config) {
|
|
77
|
+
return { abbrev: config.abbrev.padEnd(SOURCE_WIDTH), color: config.color };
|
|
78
|
+
}
|
|
79
|
+
// Unknown source: truncate/pad to width
|
|
80
|
+
const abbrev = source.length > SOURCE_WIDTH
|
|
81
|
+
? source.slice(0, SOURCE_WIDTH)
|
|
82
|
+
: source.padEnd(SOURCE_WIDTH);
|
|
83
|
+
return { abbrev, color: "white" };
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Parse a log entry into formatted parts (for use in Ink UI)
|
|
87
|
+
*/
|
|
88
|
+
export function parseLogEntry(log) {
|
|
89
|
+
const levelInfo = getLogLevelInfo(log.level);
|
|
90
|
+
const sourceInfo = getSourceInfo(log.source);
|
|
91
|
+
return {
|
|
92
|
+
timestamp: formatTimestamp(log.timestamp_ms),
|
|
93
|
+
level: levelInfo.name,
|
|
94
|
+
levelColor: levelInfo.color,
|
|
95
|
+
source: sourceInfo.abbrev,
|
|
96
|
+
sourceColor: sourceInfo.color,
|
|
97
|
+
shellName: log.shell_name || null,
|
|
98
|
+
cmd: log.cmd || null,
|
|
99
|
+
message: log.message || "",
|
|
100
|
+
exitCode: log.exit_code ?? null,
|
|
101
|
+
exitCodeColor: log.exit_code === 0 ? "green" : "red",
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Format a log entry as a string with chalk colors (for CLI output)
|
|
106
|
+
*/
|
|
107
|
+
export function formatLogEntryString(log) {
|
|
108
|
+
const parts = parseLogEntry(log);
|
|
109
|
+
const result = [];
|
|
110
|
+
// Timestamp (dim)
|
|
111
|
+
result.push(chalk.dim(parts.timestamp));
|
|
112
|
+
// Level (colored, bold for errors)
|
|
113
|
+
const levelChalk = parts.levelColor === "red"
|
|
114
|
+
? chalk.red.bold
|
|
115
|
+
: parts.levelColor === "yellow"
|
|
116
|
+
? chalk.yellow.bold
|
|
117
|
+
: parts.levelColor === "blue"
|
|
118
|
+
? chalk.blue
|
|
119
|
+
: chalk.gray;
|
|
120
|
+
result.push(levelChalk(parts.level));
|
|
121
|
+
// Source (colored, in brackets)
|
|
122
|
+
const sourceChalk = {
|
|
123
|
+
magenta: chalk.magenta,
|
|
124
|
+
cyan: chalk.cyan,
|
|
125
|
+
green: chalk.green,
|
|
126
|
+
yellow: chalk.yellow,
|
|
127
|
+
gray: chalk.gray,
|
|
128
|
+
white: chalk.white,
|
|
129
|
+
}[parts.sourceColor] || chalk.white;
|
|
130
|
+
result.push(sourceChalk(`[${parts.source}]`));
|
|
131
|
+
// Shell name if present
|
|
132
|
+
if (parts.shellName) {
|
|
133
|
+
result.push(chalk.dim(`(${parts.shellName})`));
|
|
134
|
+
}
|
|
135
|
+
// Command if present
|
|
136
|
+
if (parts.cmd) {
|
|
137
|
+
result.push(chalk.cyan("$") + " " + chalk.white(parts.cmd));
|
|
138
|
+
}
|
|
139
|
+
// Message
|
|
140
|
+
if (parts.message) {
|
|
141
|
+
result.push(parts.message);
|
|
142
|
+
}
|
|
143
|
+
// Exit code if present
|
|
144
|
+
if (parts.exitCode !== null) {
|
|
145
|
+
const exitChalk = parts.exitCode === 0 ? chalk.green : chalk.red;
|
|
146
|
+
result.push(exitChalk(`exit=${parts.exitCode}`));
|
|
147
|
+
}
|
|
148
|
+
return result.join(" ");
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Format logs for CLI output
|
|
152
|
+
*/
|
|
153
|
+
export function formatLogsForCLI(response) {
|
|
154
|
+
const logs = response.logs;
|
|
155
|
+
if (!logs || logs.length === 0) {
|
|
156
|
+
console.log(chalk.dim("No logs available"));
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
for (const log of logs) {
|
|
160
|
+
console.log(formatLogEntryString(log));
|
|
161
|
+
}
|
|
162
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory Monitor - Track memory usage and manage GC
|
|
3
|
+
* Helps prevent heap exhaustion during navigation
|
|
4
|
+
*/
|
|
5
|
+
let lastMemoryUsage = null;
|
|
6
|
+
let gcAttempts = 0;
|
|
7
|
+
const MAX_GC_ATTEMPTS_PER_MINUTE = 5;
|
|
8
|
+
let lastGCReset = Date.now();
|
|
9
|
+
// Memory thresholds (in bytes)
|
|
10
|
+
const HEAP_WARNING_THRESHOLD = 3.5e9; // 3.5 GB
|
|
11
|
+
const HEAP_CRITICAL_THRESHOLD = 4e9; // 4 GB
|
|
12
|
+
export function logMemoryUsage(label) {
|
|
13
|
+
if (process.env.NODE_ENV === "development" || process.env.DEBUG_MEMORY) {
|
|
14
|
+
const current = process.memoryUsage();
|
|
15
|
+
const heapUsedMB = (current.heapUsed / 1024 / 1024).toFixed(2);
|
|
16
|
+
const heapTotalMB = (current.heapTotal / 1024 / 1024).toFixed(2);
|
|
17
|
+
const rssMB = (current.rss / 1024 / 1024).toFixed(2);
|
|
18
|
+
let delta = "";
|
|
19
|
+
if (lastMemoryUsage) {
|
|
20
|
+
const heapDelta = current.heapUsed - lastMemoryUsage.heapUsed;
|
|
21
|
+
const heapDeltaMB = (heapDelta / 1024 / 1024).toFixed(2);
|
|
22
|
+
delta = ` (Δ ${heapDeltaMB}MB)`;
|
|
23
|
+
}
|
|
24
|
+
console.error(`[MEMORY] ${label}: Heap ${heapUsedMB}/${heapTotalMB}MB, RSS ${rssMB}MB${delta}`);
|
|
25
|
+
// Warn if approaching limits
|
|
26
|
+
if (current.heapUsed > HEAP_WARNING_THRESHOLD) {
|
|
27
|
+
console.warn(`[MEMORY WARNING] Heap usage is high: ${heapUsedMB}MB (threshold: 3500MB)`);
|
|
28
|
+
}
|
|
29
|
+
lastMemoryUsage = current;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export function getMemoryPressure() {
|
|
33
|
+
const usage = process.memoryUsage();
|
|
34
|
+
const heapUsedPercent = (usage.heapUsed / usage.heapTotal) * 100;
|
|
35
|
+
if (usage.heapUsed > HEAP_CRITICAL_THRESHOLD || heapUsedPercent > 95)
|
|
36
|
+
return "critical";
|
|
37
|
+
if (usage.heapUsed > HEAP_WARNING_THRESHOLD || heapUsedPercent > 85)
|
|
38
|
+
return "high";
|
|
39
|
+
if (heapUsedPercent > 70)
|
|
40
|
+
return "medium";
|
|
41
|
+
return "low";
|
|
42
|
+
}
|
|
43
|
+
export function shouldTriggerGC() {
|
|
44
|
+
const pressure = getMemoryPressure();
|
|
45
|
+
return pressure === "high" || pressure === "critical";
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Force garbage collection if available and needed
|
|
49
|
+
* Respects rate limiting to avoid GC thrashing
|
|
50
|
+
*/
|
|
51
|
+
export function tryForceGC(reason) {
|
|
52
|
+
// Reset GC attempt counter every minute
|
|
53
|
+
const now = Date.now();
|
|
54
|
+
if (now - lastGCReset > 60000) {
|
|
55
|
+
gcAttempts = 0;
|
|
56
|
+
lastGCReset = now;
|
|
57
|
+
}
|
|
58
|
+
// Rate limit GC attempts
|
|
59
|
+
if (gcAttempts >= MAX_GC_ATTEMPTS_PER_MINUTE) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
// Check if global.gc is available (requires --expose-gc flag)
|
|
63
|
+
if (typeof global.gc === "function") {
|
|
64
|
+
const beforeHeap = process.memoryUsage().heapUsed;
|
|
65
|
+
global.gc();
|
|
66
|
+
gcAttempts++;
|
|
67
|
+
const afterHeap = process.memoryUsage().heapUsed;
|
|
68
|
+
const freedMB = ((beforeHeap - afterHeap) / 1024 / 1024).toFixed(2);
|
|
69
|
+
if (process.env.DEBUG_MEMORY) {
|
|
70
|
+
console.error(`[MEMORY] Forced GC${reason ? ` (${reason})` : ""}: Freed ${freedMB}MB`);
|
|
71
|
+
}
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Monitor memory and trigger GC if needed
|
|
78
|
+
* Call this after major operations like screen transitions
|
|
79
|
+
*/
|
|
80
|
+
export function checkMemoryPressure() {
|
|
81
|
+
const pressure = getMemoryPressure();
|
|
82
|
+
if (pressure === "critical" || pressure === "high") {
|
|
83
|
+
tryForceGC(`Memory pressure: ${pressure}`);
|
|
84
|
+
}
|
|
85
|
+
}
|