@plumpslabs/fennec-cli 1.13.4 → 1.13.5
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 +204 -95
- package/dist/index.js +2464 -680
- package/dist/index.js.map +1 -1
- package/package.json +5 -2
package/dist/index.js
CHANGED
|
@@ -129,14 +129,14 @@ var require_emoji_regex = __commonJS({
|
|
|
129
129
|
var require_string_width = __commonJS({
|
|
130
130
|
"../../node_modules/.pnpm/string-width@4.2.3/node_modules/string-width/index.js"(exports, module) {
|
|
131
131
|
"use strict";
|
|
132
|
-
var
|
|
132
|
+
var stripAnsi2 = require_strip_ansi();
|
|
133
133
|
var isFullwidthCodePoint = require_is_fullwidth_code_point();
|
|
134
134
|
var emojiRegex = require_emoji_regex();
|
|
135
135
|
var stringWidth = (string) => {
|
|
136
136
|
if (typeof string !== "string" || string.length === 0) {
|
|
137
137
|
return 0;
|
|
138
138
|
}
|
|
139
|
-
string =
|
|
139
|
+
string = stripAnsi2(string);
|
|
140
140
|
if (string.length === 0) {
|
|
141
141
|
return 0;
|
|
142
142
|
}
|
|
@@ -181,23 +181,23 @@ var require_utils = __commonJS({
|
|
|
181
181
|
function repeat(str, times) {
|
|
182
182
|
return Array(times + 1).join(str);
|
|
183
183
|
}
|
|
184
|
-
function
|
|
184
|
+
function pad2(str, len, pad3, dir) {
|
|
185
185
|
let length = strlen(str);
|
|
186
186
|
if (len + 1 >= length) {
|
|
187
187
|
let padlen = len - length;
|
|
188
188
|
switch (dir) {
|
|
189
189
|
case "right": {
|
|
190
|
-
str = repeat(
|
|
190
|
+
str = repeat(pad3, padlen) + str;
|
|
191
191
|
break;
|
|
192
192
|
}
|
|
193
193
|
case "center": {
|
|
194
194
|
let right = Math.ceil(padlen / 2);
|
|
195
195
|
let left = padlen - right;
|
|
196
|
-
str = repeat(
|
|
196
|
+
str = repeat(pad3, left) + str + repeat(pad3, right);
|
|
197
197
|
break;
|
|
198
198
|
}
|
|
199
199
|
default: {
|
|
200
|
-
str = str + repeat(
|
|
200
|
+
str = str + repeat(pad3, padlen);
|
|
201
201
|
break;
|
|
202
202
|
}
|
|
203
203
|
}
|
|
@@ -455,7 +455,7 @@ var require_utils = __commonJS({
|
|
|
455
455
|
module.exports = {
|
|
456
456
|
strlen,
|
|
457
457
|
repeat,
|
|
458
|
-
pad,
|
|
458
|
+
pad: pad2,
|
|
459
459
|
truncate,
|
|
460
460
|
mergeOptions,
|
|
461
461
|
wordWrap: multiLineWordWrap,
|
|
@@ -1902,7 +1902,7 @@ var symbols = {
|
|
|
1902
1902
|
ears: "\u23DC"
|
|
1903
1903
|
};
|
|
1904
1904
|
function renderTable(columns, rows, options) {
|
|
1905
|
-
const
|
|
1905
|
+
const pad2 = options?.padding ?? 1;
|
|
1906
1906
|
if (rows.length === 0) {
|
|
1907
1907
|
return pc.dim(" (no data)");
|
|
1908
1908
|
}
|
|
@@ -1910,8 +1910,8 @@ function renderTable(columns, rows, options) {
|
|
|
1910
1910
|
head: columns.map((c) => pc.bold(c.label)),
|
|
1911
1911
|
colAligns: columns.map((c) => c.align ?? "left"),
|
|
1912
1912
|
style: {
|
|
1913
|
-
"padding-left":
|
|
1914
|
-
"padding-right":
|
|
1913
|
+
"padding-left": pad2,
|
|
1914
|
+
"padding-right": pad2,
|
|
1915
1915
|
head: [],
|
|
1916
1916
|
// reset default styles; our pc.bold is already applied
|
|
1917
1917
|
border: [],
|
|
@@ -1955,8 +1955,8 @@ function renderTable(columns, rows, options) {
|
|
|
1955
1955
|
function renderKV(key, value, options) {
|
|
1956
1956
|
const indent = options?.indent ?? 2;
|
|
1957
1957
|
const sep = options?.separator ?? ":";
|
|
1958
|
-
const
|
|
1959
|
-
return `${
|
|
1958
|
+
const pad2 = " ".repeat(indent);
|
|
1959
|
+
return `${pad2}${pc.dim(key + sep)} ${value}`;
|
|
1960
1960
|
}
|
|
1961
1961
|
function renderKVColor(key, value, color = pc.bold) {
|
|
1962
1962
|
return ` ${pc.dim(key + ":")} ${color(value)}`;
|
|
@@ -2013,10 +2013,10 @@ async function selectPrompt(message, options) {
|
|
|
2013
2013
|
console.log(` ${pc.dim("0) Cancel")}
|
|
2014
2014
|
`);
|
|
2015
2015
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
2016
|
-
const answer = await new Promise((
|
|
2016
|
+
const answer = await new Promise((resolve13) => {
|
|
2017
2017
|
rl.question(` ${pc.bold("Enter number")} ${pc.dim("(0-" + options.length + ")")}: `, (ans) => {
|
|
2018
2018
|
rl.close();
|
|
2019
|
-
|
|
2019
|
+
resolve13(ans.trim());
|
|
2020
2020
|
});
|
|
2021
2021
|
});
|
|
2022
2022
|
const num = parseInt(answer, 10);
|
|
@@ -2031,11 +2031,11 @@ async function selectPrompt(message, options) {
|
|
|
2031
2031
|
async function confirmPrompt(message, defaultValue = false) {
|
|
2032
2032
|
const hint = defaultValue ? pc.bold("Y") + pc.dim("/n") : pc.dim("y/") + pc.bold("N");
|
|
2033
2033
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
2034
|
-
const answer = await new Promise((
|
|
2034
|
+
const answer = await new Promise((resolve13) => {
|
|
2035
2035
|
rl.question(`
|
|
2036
2036
|
${message} ${pc.dim("(" + hint + ")")}: `, (ans) => {
|
|
2037
2037
|
rl.close();
|
|
2038
|
-
|
|
2038
|
+
resolve13(ans.trim().toLowerCase());
|
|
2039
2039
|
});
|
|
2040
2040
|
});
|
|
2041
2041
|
if (!answer) return defaultValue;
|
|
@@ -2091,7 +2091,7 @@ function hexColor(color) {
|
|
|
2091
2091
|
}
|
|
2092
2092
|
var fennecOrange2 = hexColor("#FF6432");
|
|
2093
2093
|
var fennecGold = hexColor("#FFB347");
|
|
2094
|
-
var VERSION = "1.13.
|
|
2094
|
+
var VERSION = "1.13.5";
|
|
2095
2095
|
var cachedBanner = null;
|
|
2096
2096
|
var cachedCompact = null;
|
|
2097
2097
|
function generateBanner() {
|
|
@@ -2135,91 +2135,373 @@ var FOX_COMPACT = generateCompactBanner();
|
|
|
2135
2135
|
// src/utils/help.ts
|
|
2136
2136
|
import pc3 from "picocolors";
|
|
2137
2137
|
var fennecOrange3 = hexColor("#FF6432");
|
|
2138
|
+
var COMMANDS = {
|
|
2139
|
+
"start-server": {
|
|
2140
|
+
name: "start",
|
|
2141
|
+
usage: "start [options]",
|
|
2142
|
+
summary: "Start the Fennec MCP server (default when no app command is given)",
|
|
2143
|
+
description: "Boots the Fennec MCP server so AI agents can connect. Also auto-resurrects tracked apps that died since the last session.",
|
|
2144
|
+
options: [
|
|
2145
|
+
["--config <path>", "Path to a fennec.config.yaml file"],
|
|
2146
|
+
["--sse", "Use SSE (HTTP) transport instead of stdio"]
|
|
2147
|
+
],
|
|
2148
|
+
examples: ["start", "start --sse", "start --config ./fennec.config.yaml"]
|
|
2149
|
+
},
|
|
2150
|
+
start: {
|
|
2151
|
+
name: "start",
|
|
2152
|
+
usage: "start <command> --name <name> [options]",
|
|
2153
|
+
short: "start <command>",
|
|
2154
|
+
summary: "Start an app as a background daemon (like)",
|
|
2155
|
+
description: "Launches <command> as a detached daemon. Its output is written directly to ~/.fennec/logs/<name>.log, so Fennec confirms the app is really running and then exits \u2014 no Ctrl+C needed.",
|
|
2156
|
+
aliases: ["run"],
|
|
2157
|
+
options: [
|
|
2158
|
+
["--name <name>", "App name used for tracking (recommended)"],
|
|
2159
|
+
["--port <port>", "Port the app listens on \u2014 Fennec waits until it accepts connections"],
|
|
2160
|
+
["--cwd <dir>", "Working directory to run the command in"],
|
|
2161
|
+
["--restart", "Auto-restart if it crashes or its port stops listening (detached supervisor, survives terminal close)"]
|
|
2162
|
+
],
|
|
2163
|
+
examples: [
|
|
2164
|
+
'start "npm run dev" --name web --port 3000',
|
|
2165
|
+
"start node server.js --name api --cwd ./backend --restart"
|
|
2166
|
+
]
|
|
2167
|
+
},
|
|
2168
|
+
run: {
|
|
2169
|
+
name: "run",
|
|
2170
|
+
usage: "run <command> --name <name> [options]",
|
|
2171
|
+
short: "run <command>",
|
|
2172
|
+
summary: "Alias of `start <command>` \u2014 run an app under Fennec",
|
|
2173
|
+
aliases: ["start"],
|
|
2174
|
+
options: [
|
|
2175
|
+
["--name <name>", "App name used for tracking (recommended)"],
|
|
2176
|
+
["--port <port>", "Port the app listens on"],
|
|
2177
|
+
["--cwd <dir>", "Working directory"],
|
|
2178
|
+
["--restart", "Auto-restart if it crashes or its port stops listening"]
|
|
2179
|
+
],
|
|
2180
|
+
examples: ["run npm start --name web --port 8080"]
|
|
2181
|
+
},
|
|
2182
|
+
ps: {
|
|
2183
|
+
name: "ps",
|
|
2184
|
+
usage: "ps [options]",
|
|
2185
|
+
summary: "List Fennec-tracked apps with live status",
|
|
2186
|
+
description: "Shows tracked apps and whether they are genuinely running (PID + command identity are verified to avoid false positives from PID reuse).",
|
|
2187
|
+
options: [
|
|
2188
|
+
["--system, -a, --all", "Show all system processes instead of tracked apps"],
|
|
2189
|
+
["--name <name>", "Filter system processes by name"],
|
|
2190
|
+
["--sort <field>", "Sort by cpu | mem | pid | name (default: name)"],
|
|
2191
|
+
["--json", "Output tracked apps as JSON"],
|
|
2192
|
+
["-w, --watch", "Watch mode \u2014 refresh every 3s (with --system)"]
|
|
2193
|
+
],
|
|
2194
|
+
examples: ["ps", "ps --json", "ps --system --sort cpu -w"]
|
|
2195
|
+
},
|
|
2196
|
+
status: {
|
|
2197
|
+
name: "status",
|
|
2198
|
+
usage: "status [name] [options]",
|
|
2199
|
+
summary: "Show system overview & top processes",
|
|
2200
|
+
options: [["-w, --watch", "Watch mode \u2014 refresh every 3s"]],
|
|
2201
|
+
examples: ["status", "status -w"]
|
|
2202
|
+
},
|
|
2203
|
+
log: {
|
|
2204
|
+
name: "log",
|
|
2205
|
+
usage: "log <name|pid> [options]",
|
|
2206
|
+
summary: "Show (and follow) logs for a tracked app",
|
|
2207
|
+
description: "Prints the app's status (running/stopped) then its recent logs. Run without an app name to list all tracked apps to choose from. Secrets (API keys, tokens, connection strings, private keys) are redacted by default \u2014 safe to pipe to an AI assistant. Use --json for a bounded, machine-readable view (AI mode).",
|
|
2208
|
+
aliases: ["logs"],
|
|
2209
|
+
options: [
|
|
2210
|
+
["--lines <n>", "Number of lines to show (default: 30, capped at 500)"],
|
|
2211
|
+
["-f, --follow", "Follow mode \u2014 stream new log lines live (Ctrl+C to stop)"],
|
|
2212
|
+
["--level <level>", "Filter by level: error | warn | info | debug"],
|
|
2213
|
+
["--since <dur>", "Only show lines from the last duration (e.g. 10m, 1h, 30s)"],
|
|
2214
|
+
["--json", "Machine-readable JSON (AI mode) \u2014 bounded, redacted, no ANSI"],
|
|
2215
|
+
["--no-redact", "Disable secret redaction (use with care)"],
|
|
2216
|
+
["--clear", "Delete the log file for this app"]
|
|
2217
|
+
],
|
|
2218
|
+
examples: ["logs", "log web", "log web -f", "log api --level error --lines 100"]
|
|
2219
|
+
},
|
|
2220
|
+
spawn: {
|
|
2221
|
+
name: "spawn",
|
|
2222
|
+
usage: "spawn [name] [--all]",
|
|
2223
|
+
summary: "Re-spawn a stopped tracked app from its saved config",
|
|
2224
|
+
description: "Revives a previously stopped app using its saved command/args/cwd. With no name, shows an interactive list of stopped apps.",
|
|
2225
|
+
options: [["--all, -a", "Re-spawn all stopped apps that have a saved command"]],
|
|
2226
|
+
examples: ["spawn", "spawn web", "spawn --all"]
|
|
2227
|
+
},
|
|
2228
|
+
stop: {
|
|
2229
|
+
name: "stop",
|
|
2230
|
+
usage: "stop <name|--all>",
|
|
2231
|
+
summary: "Stop (pause) a tracked app but keep it in the registry",
|
|
2232
|
+
description: "Sends SIGTERM but keeps the entry in tracked.json so it can be revived later with `fennec spawn`.",
|
|
2233
|
+
options: [["--all, -a", "Stop all running tracked apps"]],
|
|
2234
|
+
examples: ["stop web", "stop --all"]
|
|
2235
|
+
},
|
|
2236
|
+
restart: {
|
|
2237
|
+
name: "restart",
|
|
2238
|
+
usage: "restart <name|pid>",
|
|
2239
|
+
summary: "Stop and re-spawn a tracked app from its saved config",
|
|
2240
|
+
examples: ["restart web"]
|
|
2241
|
+
},
|
|
2242
|
+
kill: {
|
|
2243
|
+
name: "kill",
|
|
2244
|
+
usage: "kill <pid|name|all>",
|
|
2245
|
+
summary: "Kill a process by PID, name, or kill all user processes",
|
|
2246
|
+
description: "Permanently removes a tracked app (unlike `stop`, which keeps it). Prompts before killing.",
|
|
2247
|
+
options: [
|
|
2248
|
+
["--signal <sig>", "Signal to send (default: SIGTERM). e.g. SIGKILL, SIGINT"],
|
|
2249
|
+
["--all, -a", "Kill all user processes (asks for confirmation)"]
|
|
2250
|
+
],
|
|
2251
|
+
examples: ["kill web", "kill 12345 --signal SIGKILL", "kill all"]
|
|
2252
|
+
},
|
|
2253
|
+
supervisor: {
|
|
2254
|
+
name: "supervisor",
|
|
2255
|
+
usage: "supervisor <start|stop|status|restart>",
|
|
2256
|
+
short: "supervisor <action>",
|
|
2257
|
+
summary: "Manage the background daemon that keeps --restart apps alive",
|
|
2258
|
+
description: "The supervisor is a detached daemon that auto-restarts apps started with --restart, even after you close the terminal. It polls every few seconds, health-checks each app's port (restarting ones that are alive but not listening) and has crash-loop backoff. `fennec start --restart` starts it automatically.",
|
|
2259
|
+
options: [
|
|
2260
|
+
["start", "Start the supervisor daemon"],
|
|
2261
|
+
["stop", "Stop the supervisor daemon"],
|
|
2262
|
+
["status", "Show supervisor status and managed apps"],
|
|
2263
|
+
["restart", "Restart the supervisor daemon"]
|
|
2264
|
+
],
|
|
2265
|
+
examples: ["supervisor status", "supervisor start", "supervisor stop"]
|
|
2266
|
+
},
|
|
2267
|
+
persist: {
|
|
2268
|
+
name: "persist",
|
|
2269
|
+
usage: "persist <enable|disable|status>",
|
|
2270
|
+
short: "persist <action>",
|
|
2271
|
+
summary: "Make fennec survive reboots (auto-start apps after login)",
|
|
2272
|
+
description: "Installs a boot unit (systemd user service on Linux, launchd agent on macOS, a startup script on Windows) that starts the supervisor at login. The supervisor then resurrects every app started with --restart, so your whole fleet comes back after a reboot \u2014 like `startup` + `save`. `fennec start --restart` enables this automatically.",
|
|
2273
|
+
options: [
|
|
2274
|
+
["enable", "Install the boot unit for the current user"],
|
|
2275
|
+
["disable", "Remove the boot unit"],
|
|
2276
|
+
["status", "Show whether boot persistence is active"]
|
|
2277
|
+
],
|
|
2278
|
+
examples: ["persist enable", "persist status", "persist disable"]
|
|
2279
|
+
},
|
|
2280
|
+
inspect: {
|
|
2281
|
+
name: "inspect",
|
|
2282
|
+
usage: "inspect <name|pid> [--plain] [--tail N] [--since 10m]",
|
|
2283
|
+
short: "inspect <name|pid>",
|
|
2284
|
+
summary: "Compact, AI-safe snapshot of an app (status + logs + errors)",
|
|
2285
|
+
description: "Returns a single bounded, machine-readable (JSON) view of one app: liveness, port health, uptime, memory, recent log lines and an error scan. Output is capped (token budget), secrets are redacted by default, and the shape is stable \u2014 so an AI assistant can observe a real running app (BE, FE, worker, console, ...) cheaply and predictably. Add --plain for a short human summary.",
|
|
2286
|
+
options: [
|
|
2287
|
+
["--plain", "Short human-readable summary instead of JSON"],
|
|
2288
|
+
["--tail N", "Max recent log lines (default 40, cap 200)"],
|
|
2289
|
+
["--since 10m|1h", "Only consider log lines from the last duration"]
|
|
2290
|
+
],
|
|
2291
|
+
examples: ["inspect web --plain", "inspect web --since 10m", "inspect api --tail 20"]
|
|
2292
|
+
},
|
|
2293
|
+
dev: {
|
|
2294
|
+
name: "dev",
|
|
2295
|
+
usage: "dev <up|down|status> [--config fennec.config.yaml]",
|
|
2296
|
+
short: "dev <action>",
|
|
2297
|
+
summary: "Orchestrate a whole dev stack from fennec.config.yaml",
|
|
2298
|
+
description: "Declarative dev-environment orchestration. `fennec dev up` reads fennec.config.yaml, starts every app (BE/FE/DB/worker) like `fennec start --restart`, respects dependencies (waits for a dependent's port), and brings the whole stack up as one unit. Apps are supervised (auto-restart + port health-check) and boot-persistent. `fennec dev down` stops them (disabling auto-restart). `fennec dev status` shows the stack. This is the AI-friendly way to boot an entire project and then observe it with `fennec observe` / `inspect`.",
|
|
2299
|
+
options: [
|
|
2300
|
+
["up", "Start the whole stack from fennec.config.yaml"],
|
|
2301
|
+
["down", "Stop the whole stack (auto-restart disabled)"],
|
|
2302
|
+
["status", "Show stack status (running/stopped, last restart cause)"],
|
|
2303
|
+
["--config <path>", "Path to the config file (default: ./fennec.config.yaml)"]
|
|
2304
|
+
],
|
|
2305
|
+
examples: ["dev up", "dev up --config ./stack.yaml", "dev down", "dev status"]
|
|
2306
|
+
},
|
|
2307
|
+
info: {
|
|
2308
|
+
name: "info",
|
|
2309
|
+
usage: "info <name>",
|
|
2310
|
+
summary: "Show detailed info for a tracked app",
|
|
2311
|
+
examples: ["info web"]
|
|
2312
|
+
},
|
|
2313
|
+
rename: {
|
|
2314
|
+
name: "rename",
|
|
2315
|
+
usage: "rename <old-name> <new-name>",
|
|
2316
|
+
summary: "Rename a tracked app",
|
|
2317
|
+
examples: ["rename web frontend"]
|
|
2318
|
+
},
|
|
2319
|
+
attach: {
|
|
2320
|
+
name: "attach",
|
|
2321
|
+
usage: "attach <port> --name <name>",
|
|
2322
|
+
short: "attach <port>",
|
|
2323
|
+
summary: "Observe a running process by the port it listens on",
|
|
2324
|
+
options: [["--name <name>", "Name to track the observed process under"]],
|
|
2325
|
+
examples: ["attach 3000 --name web"]
|
|
2326
|
+
},
|
|
2327
|
+
"attach-pid": {
|
|
2328
|
+
name: "attach-pid",
|
|
2329
|
+
usage: "attach-pid <pid>",
|
|
2330
|
+
summary: "Attach to and observe a process by its PID",
|
|
2331
|
+
examples: ["attach-pid 12345"]
|
|
2332
|
+
},
|
|
2333
|
+
"attach-port": {
|
|
2334
|
+
name: "attach-port",
|
|
2335
|
+
usage: "attach-port <port>",
|
|
2336
|
+
summary: "Attach to and observe a process by its port",
|
|
2337
|
+
examples: ["attach-port 8080"]
|
|
2338
|
+
},
|
|
2339
|
+
pipe: {
|
|
2340
|
+
name: "pipe",
|
|
2341
|
+
usage: "pipe --name <name>",
|
|
2342
|
+
summary: "Pipe stdin into a Fennec log watcher",
|
|
2343
|
+
options: [["--name <name>", "Watcher name (required)"]],
|
|
2344
|
+
examples: ["npm run dev | fennec pipe --name web"]
|
|
2345
|
+
},
|
|
2346
|
+
watch: {
|
|
2347
|
+
name: "watch",
|
|
2348
|
+
usage: "watch --file <path> [--name <name>]",
|
|
2349
|
+
short: "watch --file <path>",
|
|
2350
|
+
summary: "Watch an existing log file",
|
|
2351
|
+
options: [
|
|
2352
|
+
["--file <path>", "Path to the log file (required)"],
|
|
2353
|
+
["--name <name>", "Watcher name"]
|
|
2354
|
+
],
|
|
2355
|
+
examples: ["watch --file ./app.log --name web"]
|
|
2356
|
+
},
|
|
2357
|
+
export: {
|
|
2358
|
+
name: "export",
|
|
2359
|
+
usage: "export --file <path>",
|
|
2360
|
+
summary: "Export tracked apps to a file",
|
|
2361
|
+
options: [["--file <path>", "Destination file path (required)"]],
|
|
2362
|
+
examples: ["export --file ./fennec-apps.json"]
|
|
2363
|
+
},
|
|
2364
|
+
import: {
|
|
2365
|
+
name: "import",
|
|
2366
|
+
usage: "import <file>",
|
|
2367
|
+
summary: "Import tracked apps from a file",
|
|
2368
|
+
examples: ["import ./fennec-apps.json"]
|
|
2369
|
+
},
|
|
2370
|
+
cleanup: {
|
|
2371
|
+
name: "cleanup",
|
|
2372
|
+
usage: "cleanup",
|
|
2373
|
+
summary: "Remove dead/stale entries from the tracked registry",
|
|
2374
|
+
examples: ["cleanup"]
|
|
2375
|
+
},
|
|
2376
|
+
init: {
|
|
2377
|
+
name: "init",
|
|
2378
|
+
usage: "init",
|
|
2379
|
+
summary: "Generate a fennec.config.yaml in the current directory",
|
|
2380
|
+
examples: ["init"]
|
|
2381
|
+
},
|
|
2382
|
+
setup: {
|
|
2383
|
+
name: "setup",
|
|
2384
|
+
usage: "setup",
|
|
2385
|
+
summary: "Interactively configure your MCP client for Fennec",
|
|
2386
|
+
examples: ["setup"]
|
|
2387
|
+
},
|
|
2388
|
+
"install-browsers": {
|
|
2389
|
+
name: "install-browsers",
|
|
2390
|
+
usage: "install-browsers",
|
|
2391
|
+
summary: "Install Playwright browser engines",
|
|
2392
|
+
examples: ["install-browsers"]
|
|
2393
|
+
},
|
|
2394
|
+
sessions: {
|
|
2395
|
+
name: "sessions",
|
|
2396
|
+
usage: "sessions",
|
|
2397
|
+
summary: "List saved browser auth sessions",
|
|
2398
|
+
examples: ["sessions"]
|
|
2399
|
+
},
|
|
2400
|
+
health: {
|
|
2401
|
+
name: "health",
|
|
2402
|
+
usage: "health",
|
|
2403
|
+
summary: "Run a health check of the Fennec environment",
|
|
2404
|
+
examples: ["health"]
|
|
2405
|
+
},
|
|
2406
|
+
help: {
|
|
2407
|
+
name: "help",
|
|
2408
|
+
usage: "help [command]",
|
|
2409
|
+
summary: "Show this help, or detailed help for a command",
|
|
2410
|
+
examples: ["help", "help start", "start --help"]
|
|
2411
|
+
},
|
|
2412
|
+
version: {
|
|
2413
|
+
name: "version",
|
|
2414
|
+
usage: "version",
|
|
2415
|
+
summary: "Show the installed Fennec version",
|
|
2416
|
+
examples: ["version"]
|
|
2417
|
+
}
|
|
2418
|
+
};
|
|
2419
|
+
var LOOKUP = {
|
|
2420
|
+
logs: "log",
|
|
2421
|
+
"-v": "version",
|
|
2422
|
+
"--version": "version",
|
|
2423
|
+
"-h": "help",
|
|
2424
|
+
"--help": "help"
|
|
2425
|
+
};
|
|
2426
|
+
function findCommandDoc(cmd) {
|
|
2427
|
+
if (COMMANDS[cmd]) return COMMANDS[cmd];
|
|
2428
|
+
const alias = LOOKUP[cmd];
|
|
2429
|
+
return alias ? COMMANDS[alias] : void 0;
|
|
2430
|
+
}
|
|
2431
|
+
var GROUPS = [
|
|
2432
|
+
{ title: "Server", keys: ["start-server"] },
|
|
2433
|
+
{ title: "Apps & Processes", keys: ["start", "run", "ps", "status", "log", "spawn", "stop", "restart", "kill", "supervisor", "persist", "dev", "inspect", "info", "rename"] },
|
|
2434
|
+
{ title: "Observation", keys: ["attach", "attach-pid", "attach-port", "pipe", "watch"] },
|
|
2435
|
+
{ title: "Data", keys: ["export", "import", "cleanup"] },
|
|
2436
|
+
{ title: "Configuration", keys: ["init", "setup", "install-browsers", "sessions"] },
|
|
2437
|
+
{ title: "Other", keys: ["health", "help", "version"] }
|
|
2438
|
+
];
|
|
2439
|
+
function pad(s, width) {
|
|
2440
|
+
return s.length >= width ? s : s + " ".repeat(width - s.length);
|
|
2441
|
+
}
|
|
2138
2442
|
function showHelp() {
|
|
2139
|
-
const sep = fennecOrange3("\u2500".repeat(
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
${pc3.bold("Usage:")} fennec ${pc3.dim("<command>")} ${pc3.dim("[options]")}
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
${sep}
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
${
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
${pc3.
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
${
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
${pc3.
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
${pc3.cyan("attach-port")} ${pc3.dim("<port>")} ${pc3.dim("Attach to process by port")}
|
|
2202
|
-
|
|
2203
|
-
${sep}
|
|
2204
|
-
${pc3.bold("Configuration")}
|
|
2205
|
-
${sep}
|
|
2206
|
-
|
|
2207
|
-
${pc3.cyan("init")} ${pc3.dim("Generate fennec.config.yaml")}
|
|
2208
|
-
${pc3.cyan("setup")} ${pc3.dim("Configure MCP client")}
|
|
2209
|
-
${pc3.cyan("install-browsers")} ${pc3.dim("Install Playwright browser engines")}
|
|
2210
|
-
${pc3.cyan("sessions")} ${pc3.dim("List saved auth sessions")}
|
|
2211
|
-
|
|
2212
|
-
${sep}
|
|
2213
|
-
${pc3.bold("Other")}
|
|
2214
|
-
${sep}
|
|
2215
|
-
|
|
2216
|
-
${pc3.cyan("help")} ${pc3.dim("Show this help message")}
|
|
2217
|
-
${pc3.cyan("version")} ${pc3.dim("Show version")}
|
|
2218
|
-
|
|
2219
|
-
${pc3.dim("\u2500".repeat(50))}
|
|
2220
|
-
|
|
2221
|
-
${pc3.dim("Learn more:")} ${pc3.cyan("https://github.com/plumpslabs/fennec")}
|
|
2222
|
-
`);
|
|
2443
|
+
const sep = fennecOrange3("\u2500".repeat(56));
|
|
2444
|
+
const lines = [];
|
|
2445
|
+
lines.push("");
|
|
2446
|
+
lines.push(` ${pc3.bold("Usage:")} fennec ${pc3.dim("<command>")} ${pc3.dim("[options]")}`);
|
|
2447
|
+
lines.push(` ${pc3.dim("Run")} ${pc3.cyan("fennec <command> --help")} ${pc3.dim("for details on any command.")}`);
|
|
2448
|
+
for (const group of GROUPS) {
|
|
2449
|
+
lines.push("");
|
|
2450
|
+
lines.push(` ${sep}`);
|
|
2451
|
+
lines.push(` ${pc3.bold(group.title)}`);
|
|
2452
|
+
lines.push(` ${sep}`);
|
|
2453
|
+
lines.push("");
|
|
2454
|
+
for (const key of group.keys) {
|
|
2455
|
+
const doc = COMMANDS[key];
|
|
2456
|
+
if (!doc) continue;
|
|
2457
|
+
const usageLabel = key === "start-server" ? "start" : doc.short ?? doc.usage;
|
|
2458
|
+
lines.push(` ${pc3.cyan(pad(usageLabel, 24))} ${pc3.dim(doc.summary)}`);
|
|
2459
|
+
}
|
|
2460
|
+
}
|
|
2461
|
+
lines.push("");
|
|
2462
|
+
lines.push(` ${pc3.dim("\u2500".repeat(56))}`);
|
|
2463
|
+
lines.push(` ${pc3.dim("Learn more:")} ${pc3.cyan("https://github.com/plumpslabs/fennec")}`);
|
|
2464
|
+
lines.push("");
|
|
2465
|
+
console.error(lines.join("\n"));
|
|
2466
|
+
}
|
|
2467
|
+
function showCommandHelp(cmd) {
|
|
2468
|
+
const doc = findCommandDoc(cmd);
|
|
2469
|
+
if (!doc) {
|
|
2470
|
+
console.error(`
|
|
2471
|
+
${pc3.yellow("\u26A0")} Unknown command: ${pc3.bold(cmd)}`);
|
|
2472
|
+
console.error(` ${pc3.dim("Run")} ${pc3.cyan("fennec help")} ${pc3.dim("to see all commands.")}
|
|
2473
|
+
`);
|
|
2474
|
+
return;
|
|
2475
|
+
}
|
|
2476
|
+
const lines = [];
|
|
2477
|
+
lines.push("");
|
|
2478
|
+
lines.push(` ${pc3.bold(fennecOrange3(doc.name))} ${pc3.dim("\u2014")} ${doc.summary}`);
|
|
2479
|
+
lines.push("");
|
|
2480
|
+
lines.push(` ${pc3.bold("Usage:")} ${pc3.cyan(`fennec ${doc.usage}`)}`);
|
|
2481
|
+
if (doc.aliases?.length) {
|
|
2482
|
+
lines.push(` ${pc3.bold("Aliases:")} ${doc.aliases.map((a) => pc3.cyan(a)).join(", ")}`);
|
|
2483
|
+
}
|
|
2484
|
+
if (doc.description) {
|
|
2485
|
+
lines.push("");
|
|
2486
|
+
lines.push(` ${doc.description}`);
|
|
2487
|
+
}
|
|
2488
|
+
if (doc.options?.length) {
|
|
2489
|
+
lines.push("");
|
|
2490
|
+
lines.push(` ${pc3.bold("Options:")}`);
|
|
2491
|
+
const width = Math.max(...doc.options.map(([f]) => f.length));
|
|
2492
|
+
for (const [flag, desc] of doc.options) {
|
|
2493
|
+
lines.push(` ${pc3.cyan(pad(flag, width))} ${pc3.dim(desc)}`);
|
|
2494
|
+
}
|
|
2495
|
+
}
|
|
2496
|
+
if (doc.examples?.length) {
|
|
2497
|
+
lines.push("");
|
|
2498
|
+
lines.push(` ${pc3.bold("Examples:")}`);
|
|
2499
|
+
for (const ex of doc.examples) {
|
|
2500
|
+
lines.push(` ${pc3.dim("$")} ${fennecOrange3(`fennec ${ex}`)}`);
|
|
2501
|
+
}
|
|
2502
|
+
}
|
|
2503
|
+
lines.push("");
|
|
2504
|
+
console.error(lines.join("\n"));
|
|
2223
2505
|
}
|
|
2224
2506
|
|
|
2225
2507
|
// src/commands/pipe.ts
|
|
@@ -2330,105 +2612,22 @@ async function watchCommand(args2) {
|
|
|
2330
2612
|
}
|
|
2331
2613
|
|
|
2332
2614
|
// src/commands/start.ts
|
|
2333
|
-
import {
|
|
2334
|
-
import { resolve as
|
|
2335
|
-
import { homedir as homedir2 } from "os";
|
|
2336
|
-
import { spawn } from "child_process";
|
|
2615
|
+
import { mkdirSync as mkdirSync4 } from "fs";
|
|
2616
|
+
import { resolve as resolve4, dirname as dirname4 } from "path";
|
|
2337
2617
|
import { FennecServer } from "@plumpslabs/fennec-core";
|
|
2338
|
-
import
|
|
2618
|
+
import pc7 from "picocolors";
|
|
2339
2619
|
|
|
2340
2620
|
// src/commands/tracker.ts
|
|
2341
|
-
import { existsSync as
|
|
2621
|
+
import { existsSync as existsSync3, writeFileSync, readFileSync as readFileSync2, mkdirSync, renameSync, statSync, rmSync, openSync, closeSync, writeSync } from "fs";
|
|
2622
|
+
import { spawn } from "child_process";
|
|
2342
2623
|
import { resolve as resolve2, dirname, basename } from "path";
|
|
2343
2624
|
import { homedir } from "os";
|
|
2344
|
-
function getTrackedPath() {
|
|
2345
|
-
const dir = process.env.FENNEC_DATA_DIR ? resolve2(process.env.FENNEC_DATA_DIR) : resolve2(homedir(), ".fennec");
|
|
2346
|
-
return resolve2(dir, "tracked.json");
|
|
2347
|
-
}
|
|
2348
|
-
function readTracked() {
|
|
2349
|
-
try {
|
|
2350
|
-
const path = getTrackedPath();
|
|
2351
|
-
if (!existsSync2(path)) return [];
|
|
2352
|
-
return JSON.parse(readFileSync(path, "utf-8"));
|
|
2353
|
-
} catch {
|
|
2354
|
-
return [];
|
|
2355
|
-
}
|
|
2356
|
-
}
|
|
2357
|
-
function saveTracked(processes) {
|
|
2358
|
-
try {
|
|
2359
|
-
const path = getTrackedPath();
|
|
2360
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
2361
|
-
writeFileSync(path, JSON.stringify(processes, null, 2), "utf-8");
|
|
2362
|
-
} catch (err) {
|
|
2363
|
-
console.error("[fennec] Failed to save tracked processes:", err instanceof Error ? err.message : String(err));
|
|
2364
|
-
}
|
|
2365
|
-
}
|
|
2366
|
-
function addTracked(proc) {
|
|
2367
|
-
const tracked = readTracked();
|
|
2368
|
-
const filtered = tracked.filter((t) => t.name !== proc.name);
|
|
2369
|
-
filtered.push(proc);
|
|
2370
|
-
saveTracked(filtered);
|
|
2371
|
-
}
|
|
2372
|
-
function removeTracked(name) {
|
|
2373
|
-
const tracked = readTracked();
|
|
2374
|
-
saveTracked(tracked.filter((t) => t.name !== name));
|
|
2375
|
-
}
|
|
2376
|
-
function removeTrackedByPid(pid) {
|
|
2377
|
-
const tracked = readTracked();
|
|
2378
|
-
saveTracked(tracked.filter((t) => t.pid !== pid));
|
|
2379
|
-
}
|
|
2380
|
-
function formatUptime(seconds) {
|
|
2381
|
-
if (seconds < 60) return `${seconds}s`;
|
|
2382
|
-
if (seconds < 3600) return `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
|
|
2383
|
-
const h = Math.floor(seconds / 3600);
|
|
2384
|
-
const m = Math.floor(seconds % 3600 / 60);
|
|
2385
|
-
if (h >= 24) {
|
|
2386
|
-
const d = Math.floor(h / 24);
|
|
2387
|
-
return `${d}d ${h % 24}h ${m}m`;
|
|
2388
|
-
}
|
|
2389
|
-
return `${h}h ${m}m`;
|
|
2390
|
-
}
|
|
2391
|
-
var DEFAULT_MAX_LOG_SIZE = 10 * 1024 * 1024;
|
|
2392
|
-
var DEFAULT_MAX_LOG_FILES = 3;
|
|
2393
|
-
function rotateLogFile(filePath, maxSize = DEFAULT_MAX_LOG_SIZE, maxFiles = DEFAULT_MAX_LOG_FILES) {
|
|
2394
|
-
try {
|
|
2395
|
-
if (!existsSync2(filePath)) return false;
|
|
2396
|
-
const stats = statSync(filePath);
|
|
2397
|
-
if (stats.size < maxSize) return false;
|
|
2398
|
-
const dir = dirname(filePath);
|
|
2399
|
-
const base = basename(filePath);
|
|
2400
|
-
const oldestPath = resolve2(dir, `${base}.${maxFiles}`);
|
|
2401
|
-
if (existsSync2(oldestPath)) {
|
|
2402
|
-
try {
|
|
2403
|
-
rmSync(oldestPath);
|
|
2404
|
-
} catch {
|
|
2405
|
-
}
|
|
2406
|
-
}
|
|
2407
|
-
for (let i = maxFiles - 1; i >= 1; i--) {
|
|
2408
|
-
const src = resolve2(dir, `${base}.${i}`);
|
|
2409
|
-
const dst = resolve2(dir, `${base}.${i + 1}`);
|
|
2410
|
-
if (existsSync2(src)) {
|
|
2411
|
-
try {
|
|
2412
|
-
renameSync(src, dst);
|
|
2413
|
-
} catch {
|
|
2414
|
-
}
|
|
2415
|
-
}
|
|
2416
|
-
}
|
|
2417
|
-
try {
|
|
2418
|
-
renameSync(filePath, resolve2(dir, `${base}.1`));
|
|
2419
|
-
} catch {
|
|
2420
|
-
}
|
|
2421
|
-
return true;
|
|
2422
|
-
} catch (err) {
|
|
2423
|
-
console.error("[fennec] Log rotation failed:", err instanceof Error ? err.message : String(err));
|
|
2424
|
-
return false;
|
|
2425
|
-
}
|
|
2426
|
-
}
|
|
2427
2625
|
|
|
2428
2626
|
// src/utils/system-process.ts
|
|
2429
|
-
import { readdirSync, readFileSync
|
|
2627
|
+
import { readdirSync, readFileSync, existsSync as existsSync2, readlinkSync } from "fs";
|
|
2430
2628
|
import { execSync } from "child_process";
|
|
2431
2629
|
import { cpus } from "os";
|
|
2630
|
+
import net from "net";
|
|
2432
2631
|
function readProcProcesses() {
|
|
2433
2632
|
const processes = [];
|
|
2434
2633
|
const currentUid = process.getuid?.();
|
|
@@ -2453,7 +2652,7 @@ function readSingleProcProcess(pid, currentUid, totalMemKb) {
|
|
|
2453
2652
|
const statPath = `/proc/${pid}/stat`;
|
|
2454
2653
|
const statusPath = `/proc/${pid}/status`;
|
|
2455
2654
|
const cmdlinePath = `/proc/${pid}/cmdline`;
|
|
2456
|
-
if (!
|
|
2655
|
+
if (!existsSync2(statPath)) return null;
|
|
2457
2656
|
const statusText = readSafe(statusPath);
|
|
2458
2657
|
if (!statusText) return null;
|
|
2459
2658
|
const nameMatch = statusText.match(/Name:\s+(.+)/);
|
|
@@ -2539,6 +2738,36 @@ function runPsProcesses() {
|
|
|
2539
2738
|
return [];
|
|
2540
2739
|
}
|
|
2541
2740
|
}
|
|
2741
|
+
function runTasklistProcesses() {
|
|
2742
|
+
try {
|
|
2743
|
+
const output = execSync("tasklist /fo csv /nh 2>nul", { encoding: "utf-8", timeout: 5e3 });
|
|
2744
|
+
const result = [];
|
|
2745
|
+
for (const raw of output.split("\n")) {
|
|
2746
|
+
if (!raw.trim()) continue;
|
|
2747
|
+
const parts = raw.split('","').map((p) => p.replace(/^"|"$/g, ""));
|
|
2748
|
+
const pid = parseInt(parts[1] ?? "", 10);
|
|
2749
|
+
if (isNaN(pid)) continue;
|
|
2750
|
+
const image = parts[0] ?? "";
|
|
2751
|
+
const name = image.replace(/\.[a-z0-9]+$/i, "");
|
|
2752
|
+
const memKb = parseInt((parts[4] ?? "").replace(/[^\d]/g, ""), 10) || 0;
|
|
2753
|
+
result.push({
|
|
2754
|
+
pid,
|
|
2755
|
+
name,
|
|
2756
|
+
command: image,
|
|
2757
|
+
cpuPercent: 0,
|
|
2758
|
+
memPercent: 0,
|
|
2759
|
+
memRss: memKb,
|
|
2760
|
+
state: "R",
|
|
2761
|
+
startedAt: null,
|
|
2762
|
+
ports: [],
|
|
2763
|
+
isUserProcess: false
|
|
2764
|
+
});
|
|
2765
|
+
}
|
|
2766
|
+
return result;
|
|
2767
|
+
} catch {
|
|
2768
|
+
return [];
|
|
2769
|
+
}
|
|
2770
|
+
}
|
|
2542
2771
|
function mapBsdState(s) {
|
|
2543
2772
|
switch (s) {
|
|
2544
2773
|
case "R":
|
|
@@ -2552,135 +2781,1041 @@ function mapBsdState(s) {
|
|
|
2552
2781
|
case "I":
|
|
2553
2782
|
return "S";
|
|
2554
2783
|
default:
|
|
2555
|
-
return s;
|
|
2784
|
+
return s;
|
|
2785
|
+
}
|
|
2786
|
+
}
|
|
2787
|
+
function detectListeningPorts(_pid) {
|
|
2788
|
+
return [];
|
|
2789
|
+
}
|
|
2790
|
+
function readSafe(path) {
|
|
2791
|
+
try {
|
|
2792
|
+
return readFileSync(path, "utf-8");
|
|
2793
|
+
} catch {
|
|
2794
|
+
return null;
|
|
2795
|
+
}
|
|
2796
|
+
}
|
|
2797
|
+
function readTotalMem() {
|
|
2798
|
+
try {
|
|
2799
|
+
const memInfo = readSafe("/proc/meminfo");
|
|
2800
|
+
if (memInfo) {
|
|
2801
|
+
const match = memInfo.match(/MemTotal:\s+(\d+)/);
|
|
2802
|
+
if (match) return parseInt(match[1], 10);
|
|
2803
|
+
}
|
|
2804
|
+
} catch {
|
|
2805
|
+
}
|
|
2806
|
+
return 16e6;
|
|
2807
|
+
}
|
|
2808
|
+
function readUptime() {
|
|
2809
|
+
try {
|
|
2810
|
+
const data = readSafe("/proc/uptime");
|
|
2811
|
+
if (data) {
|
|
2812
|
+
return parseFloat(data.split(/\s+/)[0] ?? "0");
|
|
2813
|
+
}
|
|
2814
|
+
} catch {
|
|
2815
|
+
}
|
|
2816
|
+
return 0;
|
|
2817
|
+
}
|
|
2818
|
+
function formatStartTime(starttime, uptimeSeconds, hertz) {
|
|
2819
|
+
try {
|
|
2820
|
+
const bootTime = Date.now() / 1e3 - uptimeSeconds;
|
|
2821
|
+
const processStart = bootTime + starttime / hertz;
|
|
2822
|
+
return new Date(processStart * 1e3).toISOString();
|
|
2823
|
+
} catch {
|
|
2824
|
+
return null;
|
|
2825
|
+
}
|
|
2826
|
+
}
|
|
2827
|
+
function getSystemProcesses(filter) {
|
|
2828
|
+
let processes;
|
|
2829
|
+
if (process.platform === "win32") {
|
|
2830
|
+
processes = runTasklistProcesses();
|
|
2831
|
+
} else if (existsSync2("/proc")) {
|
|
2832
|
+
processes = readProcProcesses();
|
|
2833
|
+
} else {
|
|
2834
|
+
processes = runPsProcesses();
|
|
2835
|
+
}
|
|
2836
|
+
if (filter) {
|
|
2837
|
+
if (filter.userOnly) {
|
|
2838
|
+
processes = processes.filter((p) => p.isUserProcess);
|
|
2839
|
+
}
|
|
2840
|
+
if (filter.name) {
|
|
2841
|
+
const nameLower = filter.name.toLowerCase();
|
|
2842
|
+
processes = processes.filter(
|
|
2843
|
+
(p) => p.name.toLowerCase().includes(nameLower) || p.command.toLowerCase().includes(nameLower)
|
|
2844
|
+
);
|
|
2845
|
+
}
|
|
2846
|
+
if (filter.pid) {
|
|
2847
|
+
processes = processes.filter((p) => p.pid === filter.pid);
|
|
2848
|
+
}
|
|
2849
|
+
if (filter.port) {
|
|
2850
|
+
processes = processes.filter((p) => p.ports.includes(filter.port));
|
|
2851
|
+
}
|
|
2852
|
+
if (filter.sortBy) {
|
|
2853
|
+
processes.sort((a, b) => {
|
|
2854
|
+
switch (filter.sortBy) {
|
|
2855
|
+
case "cpu":
|
|
2856
|
+
return b.cpuPercent - a.cpuPercent;
|
|
2857
|
+
case "mem":
|
|
2858
|
+
return b.memPercent - a.memPercent;
|
|
2859
|
+
case "pid":
|
|
2860
|
+
return a.pid - b.pid;
|
|
2861
|
+
case "name":
|
|
2862
|
+
return a.name.localeCompare(b.name);
|
|
2863
|
+
default:
|
|
2864
|
+
return b.cpuPercent - a.cpuPercent;
|
|
2865
|
+
}
|
|
2866
|
+
});
|
|
2867
|
+
}
|
|
2868
|
+
if (filter.limit && filter.limit > 0) {
|
|
2869
|
+
processes = processes.slice(0, filter.limit);
|
|
2870
|
+
}
|
|
2871
|
+
}
|
|
2872
|
+
return processes;
|
|
2873
|
+
}
|
|
2874
|
+
function killProcess(pid, signal = "SIGTERM") {
|
|
2875
|
+
try {
|
|
2876
|
+
process.kill(pid, signal);
|
|
2877
|
+
return true;
|
|
2878
|
+
} catch {
|
|
2879
|
+
return false;
|
|
2880
|
+
}
|
|
2881
|
+
}
|
|
2882
|
+
function isProcessRunning(pid) {
|
|
2883
|
+
try {
|
|
2884
|
+
process.kill(pid, 0);
|
|
2885
|
+
return true;
|
|
2886
|
+
} catch {
|
|
2887
|
+
return false;
|
|
2888
|
+
}
|
|
2889
|
+
}
|
|
2890
|
+
function checkPort(port, host = "127.0.0.1", timeout = 1e3) {
|
|
2891
|
+
const tryHost = (h) => new Promise((resolve13) => {
|
|
2892
|
+
const socket = new net.Socket();
|
|
2893
|
+
let settled = false;
|
|
2894
|
+
const finish = (result) => {
|
|
2895
|
+
if (settled) return;
|
|
2896
|
+
settled = true;
|
|
2897
|
+
socket.destroy();
|
|
2898
|
+
resolve13(result);
|
|
2899
|
+
};
|
|
2900
|
+
socket.setTimeout(timeout);
|
|
2901
|
+
socket.once("connect", () => finish(true));
|
|
2902
|
+
socket.once("timeout", () => finish(false));
|
|
2903
|
+
socket.once("error", () => finish(false));
|
|
2904
|
+
socket.connect(port, h);
|
|
2905
|
+
});
|
|
2906
|
+
return tryHost(host).then((ok) => ok ? true : tryHost("::1"));
|
|
2907
|
+
}
|
|
2908
|
+
async function checkHttp(url, timeout = 2e3) {
|
|
2909
|
+
const controller = new AbortController();
|
|
2910
|
+
const timer = setTimeout(() => controller.abort(), timeout);
|
|
2911
|
+
try {
|
|
2912
|
+
const res = await fetch(url, { method: "GET", signal: controller.signal, redirect: "manual" });
|
|
2913
|
+
return res.status >= 200 && res.status < 400;
|
|
2914
|
+
} catch {
|
|
2915
|
+
return false;
|
|
2916
|
+
} finally {
|
|
2917
|
+
clearTimeout(timer);
|
|
2918
|
+
}
|
|
2919
|
+
}
|
|
2920
|
+
function resolveHealthUrl(healthCheck, port) {
|
|
2921
|
+
if (/^https?:\/\//i.test(healthCheck)) return healthCheck;
|
|
2922
|
+
if (healthCheck.startsWith("/")) {
|
|
2923
|
+
if (!port) return null;
|
|
2924
|
+
return `http://127.0.0.1:${port}${healthCheck}`;
|
|
2925
|
+
}
|
|
2926
|
+
return null;
|
|
2927
|
+
}
|
|
2928
|
+
function findPidOnPort(port) {
|
|
2929
|
+
if (process.platform === "win32") return findPidOnPortWindows(port);
|
|
2930
|
+
if (process.platform === "darwin") return findPidOnPortMac(port);
|
|
2931
|
+
return findPidOnPortLinux(port);
|
|
2932
|
+
}
|
|
2933
|
+
function findPidOnPortLinux(port) {
|
|
2934
|
+
try {
|
|
2935
|
+
const hexPort = port.toString(16).toUpperCase().padStart(4, "0");
|
|
2936
|
+
const inodes = /* @__PURE__ */ new Set();
|
|
2937
|
+
for (const f of ["/proc/net/tcp", "/proc/net/tcp6"]) {
|
|
2938
|
+
if (!existsSync2(f)) continue;
|
|
2939
|
+
for (const line of readFileSync(f, "utf-8").split("\n").slice(1)) {
|
|
2940
|
+
const cols = line.trim().split(/\s+/);
|
|
2941
|
+
if (cols.length < 10) continue;
|
|
2942
|
+
if (cols[3] !== "0A") continue;
|
|
2943
|
+
const localPort = cols[1]?.split(":")[1];
|
|
2944
|
+
const inode = parseInt(cols[9] ?? "", 10);
|
|
2945
|
+
if (localPort === hexPort && !Number.isNaN(inode)) inodes.add(inode);
|
|
2946
|
+
}
|
|
2947
|
+
}
|
|
2948
|
+
if (inodes.size === 0) return null;
|
|
2949
|
+
for (const pidStr of readdirSync("/proc").filter((d) => /^\d+$/.test(d))) {
|
|
2950
|
+
const pid = parseInt(pidStr, 10);
|
|
2951
|
+
try {
|
|
2952
|
+
const fdDir = `/proc/${pid}/fd`;
|
|
2953
|
+
for (const fd of readdirSync(fdDir)) {
|
|
2954
|
+
const link = readlinkSync(`${fdDir}/${fd}`);
|
|
2955
|
+
const m = link.match(/socket:\[(\d+)\]/);
|
|
2956
|
+
if (m && inodes.has(parseInt(m[1], 10))) {
|
|
2957
|
+
return { pid, command: getProcessCmdline(pid) ?? "" };
|
|
2958
|
+
}
|
|
2959
|
+
}
|
|
2960
|
+
} catch {
|
|
2961
|
+
}
|
|
2962
|
+
}
|
|
2963
|
+
return null;
|
|
2964
|
+
} catch {
|
|
2965
|
+
return null;
|
|
2966
|
+
}
|
|
2967
|
+
}
|
|
2968
|
+
function findPidOnPortMac(port) {
|
|
2969
|
+
try {
|
|
2970
|
+
const out = execSync(`lsof -i :${port} -sTCP:LISTEN -P -n 2>/dev/null`, { encoding: "utf-8", timeout: 5e3 });
|
|
2971
|
+
for (const line of out.split("\n")) {
|
|
2972
|
+
if (!line.includes("LISTEN")) continue;
|
|
2973
|
+
const parts = line.trim().split(/\s+/);
|
|
2974
|
+
if (parts.length >= 2) {
|
|
2975
|
+
const pid = parseInt(parts[1], 10);
|
|
2976
|
+
if (!isNaN(pid)) return { pid, command: parts[0] ?? "" };
|
|
2977
|
+
}
|
|
2978
|
+
}
|
|
2979
|
+
return null;
|
|
2980
|
+
} catch {
|
|
2981
|
+
return null;
|
|
2982
|
+
}
|
|
2983
|
+
}
|
|
2984
|
+
function findPidOnPortWindows(port) {
|
|
2985
|
+
try {
|
|
2986
|
+
const out = execSync(`netstat -ano -p tcp 2>nul`, { encoding: "utf-8", timeout: 5e3 });
|
|
2987
|
+
const target = `:${port} `;
|
|
2988
|
+
for (const line of out.split("\n")) {
|
|
2989
|
+
const upper = line.toUpperCase();
|
|
2990
|
+
if (!upper.includes("LISTENING")) continue;
|
|
2991
|
+
if (!upper.includes(target)) continue;
|
|
2992
|
+
const pid = parseInt(line.trim().split(/\s+/).pop() ?? "", 10);
|
|
2993
|
+
if (!isNaN(pid)) return { pid, command: getProcessCmdline(pid) ?? "" };
|
|
2994
|
+
}
|
|
2995
|
+
return null;
|
|
2996
|
+
} catch {
|
|
2997
|
+
return null;
|
|
2998
|
+
}
|
|
2999
|
+
}
|
|
3000
|
+
function getProcessCmdline(pid) {
|
|
3001
|
+
if (process.platform === "win32") return getProcessCmdlineWindows(pid);
|
|
3002
|
+
if (process.platform === "darwin") return getProcessCmdlineMac(pid);
|
|
3003
|
+
return getProcessCmdlineLinux(pid);
|
|
3004
|
+
}
|
|
3005
|
+
function getProcessCmdlineLinux(pid) {
|
|
3006
|
+
try {
|
|
3007
|
+
const raw = readFileSync(`/proc/${pid}/cmdline`, "utf-8");
|
|
3008
|
+
const joined = raw.split("\0").filter(Boolean).join(" ").trim();
|
|
3009
|
+
return joined.length > 0 ? joined : null;
|
|
3010
|
+
} catch {
|
|
3011
|
+
return null;
|
|
3012
|
+
}
|
|
3013
|
+
}
|
|
3014
|
+
function getProcessCmdlineMac(pid) {
|
|
3015
|
+
try {
|
|
3016
|
+
const out = execSync(`ps -p ${pid} -o command=`, { encoding: "utf-8", timeout: 3e3 });
|
|
3017
|
+
const trimmed = out.trim();
|
|
3018
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
3019
|
+
} catch {
|
|
3020
|
+
return null;
|
|
3021
|
+
}
|
|
3022
|
+
}
|
|
3023
|
+
function getProcessCmdlineWindows(pid) {
|
|
3024
|
+
try {
|
|
3025
|
+
const out = execSync(`wmic process where ProcessId=${pid} get CommandLine /value 2>nul`, { encoding: "utf-8", timeout: 3e3 });
|
|
3026
|
+
const line = out.split("\n").find((l) => l.includes("="));
|
|
3027
|
+
const value = line?.split("=")[1]?.trim();
|
|
3028
|
+
return value ? value : null;
|
|
3029
|
+
} catch {
|
|
3030
|
+
return null;
|
|
3031
|
+
}
|
|
3032
|
+
}
|
|
3033
|
+
function getProcessCwd(pid) {
|
|
3034
|
+
if (process.platform === "linux") {
|
|
3035
|
+
try {
|
|
3036
|
+
return readlinkSync(`/proc/${pid}/cwd`) || null;
|
|
3037
|
+
} catch {
|
|
3038
|
+
return null;
|
|
3039
|
+
}
|
|
3040
|
+
}
|
|
3041
|
+
if (process.platform === "darwin") {
|
|
3042
|
+
try {
|
|
3043
|
+
const out = execSync(`lsof -p ${pid} -a -d cwd -F n 2>/dev/null`, { encoding: "utf-8", timeout: 3e3 });
|
|
3044
|
+
const line = out.split("\n").find((l) => l.startsWith("n"));
|
|
3045
|
+
return line ? line.slice(1) : null;
|
|
3046
|
+
} catch {
|
|
3047
|
+
return null;
|
|
3048
|
+
}
|
|
3049
|
+
}
|
|
3050
|
+
return null;
|
|
3051
|
+
}
|
|
3052
|
+
function getProcessEnviron(pid) {
|
|
3053
|
+
try {
|
|
3054
|
+
const raw = readFileSync(`/proc/${pid}/environ`, "utf-8");
|
|
3055
|
+
const env = {};
|
|
3056
|
+
for (const pair of raw.split("\0")) {
|
|
3057
|
+
if (!pair) continue;
|
|
3058
|
+
const eq = pair.indexOf("=");
|
|
3059
|
+
if (eq === -1) continue;
|
|
3060
|
+
env[pair.slice(0, eq)] = pair.slice(eq + 1);
|
|
3061
|
+
}
|
|
3062
|
+
return env;
|
|
3063
|
+
} catch {
|
|
3064
|
+
return null;
|
|
3065
|
+
}
|
|
3066
|
+
}
|
|
3067
|
+
function formatProcessState(state) {
|
|
3068
|
+
switch (state) {
|
|
3069
|
+
case "R":
|
|
3070
|
+
return "Running";
|
|
3071
|
+
case "S":
|
|
3072
|
+
return "Sleeping";
|
|
3073
|
+
case "D":
|
|
3074
|
+
return "Disk Sleep";
|
|
3075
|
+
case "Z":
|
|
3076
|
+
return "Zombie";
|
|
3077
|
+
case "T":
|
|
3078
|
+
return "Stopped";
|
|
3079
|
+
case "t":
|
|
3080
|
+
return "Tracing";
|
|
3081
|
+
case "X":
|
|
3082
|
+
return "Dead";
|
|
3083
|
+
case "I":
|
|
3084
|
+
return "Idle";
|
|
3085
|
+
default:
|
|
3086
|
+
return state;
|
|
3087
|
+
}
|
|
3088
|
+
}
|
|
3089
|
+
|
|
3090
|
+
// src/commands/tracker.ts
|
|
3091
|
+
var RESTART_CAUSE = "fennec:restart";
|
|
3092
|
+
function getFennecDir() {
|
|
3093
|
+
return process.env.FENNEC_DATA_DIR ? resolve2(process.env.FENNEC_DATA_DIR) : resolve2(homedir(), ".fennec");
|
|
3094
|
+
}
|
|
3095
|
+
function getTrackedPath() {
|
|
3096
|
+
return resolve2(getFennecDir(), "tracked.json");
|
|
3097
|
+
}
|
|
3098
|
+
function getSupervisorPidPath() {
|
|
3099
|
+
return resolve2(getFennecDir(), "supervisor.pid");
|
|
3100
|
+
}
|
|
3101
|
+
function logFilePathFor(name) {
|
|
3102
|
+
return resolve2(getFennecDir(), "logs", `${name}.log`);
|
|
3103
|
+
}
|
|
3104
|
+
function readTracked() {
|
|
3105
|
+
try {
|
|
3106
|
+
const path = getTrackedPath();
|
|
3107
|
+
if (!existsSync3(path)) return [];
|
|
3108
|
+
return JSON.parse(readFileSync2(path, "utf-8"));
|
|
3109
|
+
} catch {
|
|
3110
|
+
return [];
|
|
3111
|
+
}
|
|
3112
|
+
}
|
|
3113
|
+
function saveTracked(processes) {
|
|
3114
|
+
try {
|
|
3115
|
+
const path = getTrackedPath();
|
|
3116
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
3117
|
+
writeFileSync(path, JSON.stringify(processes, null, 2), "utf-8");
|
|
3118
|
+
} catch (err) {
|
|
3119
|
+
console.error("[fennec] Failed to save tracked processes:", err instanceof Error ? err.message : String(err));
|
|
3120
|
+
}
|
|
3121
|
+
}
|
|
3122
|
+
function addTracked(proc) {
|
|
3123
|
+
const tracked = readTracked();
|
|
3124
|
+
const filtered = tracked.filter((t) => t.name !== proc.name);
|
|
3125
|
+
filtered.push(proc);
|
|
3126
|
+
saveTracked(filtered);
|
|
3127
|
+
}
|
|
3128
|
+
function removeTracked(name) {
|
|
3129
|
+
const tracked = readTracked();
|
|
3130
|
+
saveTracked(tracked.filter((t) => t.name !== name));
|
|
3131
|
+
}
|
|
3132
|
+
function setAutoRestart(name, value) {
|
|
3133
|
+
const tracked = readTracked();
|
|
3134
|
+
let changed = false;
|
|
3135
|
+
for (const t of tracked) {
|
|
3136
|
+
if (t.name === name) {
|
|
3137
|
+
t.autoRestart = value;
|
|
3138
|
+
changed = true;
|
|
3139
|
+
}
|
|
3140
|
+
}
|
|
3141
|
+
if (changed) saveTracked(tracked);
|
|
3142
|
+
}
|
|
3143
|
+
function removeTrackedByPid(pid) {
|
|
3144
|
+
const tracked = readTracked();
|
|
3145
|
+
saveTracked(tracked.filter((t) => t.pid !== pid));
|
|
3146
|
+
}
|
|
3147
|
+
function resolveArgs(proc) {
|
|
3148
|
+
if (proc.args && proc.args.length > 0) return proc.args;
|
|
3149
|
+
return proc.command.split(/\s+/).filter(Boolean);
|
|
3150
|
+
}
|
|
3151
|
+
function adoptProcess(pid, opts = {}) {
|
|
3152
|
+
if (!isProcessRunning(pid)) return null;
|
|
3153
|
+
const command2 = opts.command ?? getProcessCmdline(pid) ?? `pid ${pid}`;
|
|
3154
|
+
const cwd = opts.cwd ?? getProcessCwd(pid) ?? void 0;
|
|
3155
|
+
const name = opts.name ?? `pid_${pid}`;
|
|
3156
|
+
const entry = {
|
|
3157
|
+
name,
|
|
3158
|
+
pid,
|
|
3159
|
+
command: command2,
|
|
3160
|
+
port: opts.port,
|
|
3161
|
+
cwd,
|
|
3162
|
+
env: opts.env,
|
|
3163
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3164
|
+
autoRestart: opts.autoRestart ?? true,
|
|
3165
|
+
flapping: false
|
|
3166
|
+
};
|
|
3167
|
+
addTracked(entry);
|
|
3168
|
+
return entry;
|
|
3169
|
+
}
|
|
3170
|
+
function adoptExternalOnPort(port, name) {
|
|
3171
|
+
const occ = findPidOnPort(port);
|
|
3172
|
+
if (!occ) return null;
|
|
3173
|
+
const tracked = readTracked();
|
|
3174
|
+
if (tracked.some((t) => t.pid === occ.pid)) return null;
|
|
3175
|
+
return adoptProcess(occ.pid, { port, name });
|
|
3176
|
+
}
|
|
3177
|
+
function isTrackedRunning(proc) {
|
|
3178
|
+
if (!isProcessRunning(proc.pid)) return false;
|
|
3179
|
+
const environ = getProcessEnviron(proc.pid);
|
|
3180
|
+
if (environ && "FENNEC_APP_NAME" in environ) {
|
|
3181
|
+
return environ.FENNEC_APP_NAME === proc.name;
|
|
3182
|
+
}
|
|
3183
|
+
const cmdline = getProcessCmdline(proc.pid);
|
|
3184
|
+
if (!cmdline) return true;
|
|
3185
|
+
const argv = resolveArgs(proc);
|
|
3186
|
+
const exe = argv[0];
|
|
3187
|
+
if (!exe) return true;
|
|
3188
|
+
const exeBase = basename(exe);
|
|
3189
|
+
if (exeBase.length === 0) return true;
|
|
3190
|
+
return cmdline.includes(exeBase);
|
|
3191
|
+
}
|
|
3192
|
+
function formatUptime(seconds) {
|
|
3193
|
+
if (seconds < 60) return `${seconds}s`;
|
|
3194
|
+
if (seconds < 3600) return `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
|
|
3195
|
+
const h = Math.floor(seconds / 3600);
|
|
3196
|
+
const m = Math.floor(seconds % 3600 / 60);
|
|
3197
|
+
if (h >= 24) {
|
|
3198
|
+
const d = Math.floor(h / 24);
|
|
3199
|
+
return `${d}d ${h % 24}h ${m}m`;
|
|
3200
|
+
}
|
|
3201
|
+
return `${h}h ${m}m`;
|
|
3202
|
+
}
|
|
3203
|
+
var DEFAULT_MAX_LOG_SIZE = 10 * 1024 * 1024;
|
|
3204
|
+
var DEFAULT_MAX_LOG_FILES = 3;
|
|
3205
|
+
function rotateLogFile(filePath, maxSize = DEFAULT_MAX_LOG_SIZE, maxFiles = DEFAULT_MAX_LOG_FILES) {
|
|
3206
|
+
try {
|
|
3207
|
+
if (!existsSync3(filePath)) return false;
|
|
3208
|
+
const stats = statSync(filePath);
|
|
3209
|
+
if (stats.size < maxSize) return false;
|
|
3210
|
+
const dir = dirname(filePath);
|
|
3211
|
+
const base = basename(filePath);
|
|
3212
|
+
const oldestPath = resolve2(dir, `${base}.${maxFiles}`);
|
|
3213
|
+
if (existsSync3(oldestPath)) {
|
|
3214
|
+
try {
|
|
3215
|
+
rmSync(oldestPath);
|
|
3216
|
+
} catch {
|
|
3217
|
+
}
|
|
3218
|
+
}
|
|
3219
|
+
for (let i = maxFiles - 1; i >= 1; i--) {
|
|
3220
|
+
const src = resolve2(dir, `${base}.${i}`);
|
|
3221
|
+
const dst = resolve2(dir, `${base}.${i + 1}`);
|
|
3222
|
+
if (existsSync3(src)) {
|
|
3223
|
+
try {
|
|
3224
|
+
renameSync(src, dst);
|
|
3225
|
+
} catch {
|
|
3226
|
+
}
|
|
3227
|
+
}
|
|
3228
|
+
}
|
|
3229
|
+
try {
|
|
3230
|
+
renameSync(filePath, resolve2(dir, `${base}.1`));
|
|
3231
|
+
} catch {
|
|
3232
|
+
}
|
|
3233
|
+
return true;
|
|
3234
|
+
} catch (err) {
|
|
3235
|
+
console.error("[fennec] Log rotation failed:", err instanceof Error ? err.message : String(err));
|
|
3236
|
+
return false;
|
|
3237
|
+
}
|
|
3238
|
+
}
|
|
3239
|
+
function buildSpawnEnv(stored) {
|
|
3240
|
+
const merged = { ...process.env };
|
|
3241
|
+
for (const [k, v] of Object.entries(stored ?? {})) {
|
|
3242
|
+
if (v !== void 0) merged[k] = v;
|
|
3243
|
+
}
|
|
3244
|
+
if (process.env.FENNEC_DATA_DIR) merged.FENNEC_DATA_DIR = process.env.FENNEC_DATA_DIR;
|
|
3245
|
+
if (process.env.HOME) merged.HOME = process.env.HOME;
|
|
3246
|
+
return merged;
|
|
3247
|
+
}
|
|
3248
|
+
function respawnTracked(proc, cause) {
|
|
3249
|
+
const cmdParts = resolveArgs(proc);
|
|
3250
|
+
const logFilePath = logFilePathFor(proc.name);
|
|
3251
|
+
mkdirSync(dirname(logFilePath), { recursive: true });
|
|
3252
|
+
if (cause) annotateRestart(logFilePath, cause);
|
|
3253
|
+
if (isProcessRunning(proc.pid)) {
|
|
3254
|
+
try {
|
|
3255
|
+
process.kill(proc.pid, "SIGTERM");
|
|
3256
|
+
} catch {
|
|
3257
|
+
}
|
|
3258
|
+
}
|
|
3259
|
+
const child = spawnDaemon({
|
|
3260
|
+
cmdParts,
|
|
3261
|
+
name: proc.name,
|
|
3262
|
+
cwd: proc.cwd,
|
|
3263
|
+
logFilePath,
|
|
3264
|
+
env: buildSpawnEnv(proc.env),
|
|
3265
|
+
logMode: proc.logMode
|
|
3266
|
+
});
|
|
3267
|
+
const pid = child.pid ?? 0;
|
|
3268
|
+
addTracked({
|
|
3269
|
+
...proc,
|
|
3270
|
+
pid,
|
|
3271
|
+
args: cmdParts,
|
|
3272
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3273
|
+
restartCause: cause
|
|
3274
|
+
});
|
|
3275
|
+
return pid;
|
|
3276
|
+
}
|
|
3277
|
+
function spawnDaemon(opts) {
|
|
3278
|
+
rotateLogFile(opts.logFilePath);
|
|
3279
|
+
const env = { ...opts.env ?? buildSpawnEnv(), FENNEC_APP_NAME: opts.name };
|
|
3280
|
+
if (opts.logMode === "jsonl") {
|
|
3281
|
+
return spawnDaemonJsonl(opts, env);
|
|
3282
|
+
}
|
|
3283
|
+
const fd = openSync(opts.logFilePath, "a");
|
|
3284
|
+
const child = spawn(opts.cmdParts[0], opts.cmdParts.slice(1), {
|
|
3285
|
+
cwd: opts.cwd,
|
|
3286
|
+
env,
|
|
3287
|
+
stdio: ["ignore", fd, fd],
|
|
3288
|
+
detached: true
|
|
3289
|
+
});
|
|
3290
|
+
child.unref();
|
|
3291
|
+
child.once("spawn", () => {
|
|
3292
|
+
try {
|
|
3293
|
+
closeSync(fd);
|
|
3294
|
+
} catch {
|
|
3295
|
+
}
|
|
3296
|
+
});
|
|
3297
|
+
return child;
|
|
3298
|
+
}
|
|
3299
|
+
function spawnDaemonJsonl(opts, env) {
|
|
3300
|
+
const relay = spawn(
|
|
3301
|
+
process.execPath,
|
|
3302
|
+
["-e", JSONL_RELAY],
|
|
3303
|
+
{
|
|
3304
|
+
env: { ...process.env, FENNEC_RELAY_LOG: opts.logFilePath },
|
|
3305
|
+
stdio: ["pipe", "ignore", "ignore"],
|
|
3306
|
+
detached: true
|
|
3307
|
+
}
|
|
3308
|
+
);
|
|
3309
|
+
relay.unref();
|
|
3310
|
+
const child = spawn(opts.cmdParts[0], opts.cmdParts.slice(1), {
|
|
3311
|
+
cwd: opts.cwd,
|
|
3312
|
+
env,
|
|
3313
|
+
stdio: ["ignore", relay.stdin, relay.stdin],
|
|
3314
|
+
detached: true
|
|
3315
|
+
});
|
|
3316
|
+
child.unref();
|
|
3317
|
+
try {
|
|
3318
|
+
relay.stdin?.destroy();
|
|
3319
|
+
} catch {
|
|
3320
|
+
}
|
|
3321
|
+
return child;
|
|
3322
|
+
}
|
|
3323
|
+
var JSONL_RELAY = `
|
|
3324
|
+
const fs = require('fs');
|
|
3325
|
+
const fd = fs.openSync(process.env.FENNEC_RELAY_LOG, 'a');
|
|
3326
|
+
let buf = '';
|
|
3327
|
+
const SECRET = new RegExp('(password|passwd|token|secret|api[_-]?key|access[_-]?key).{0,60}', 'gi');
|
|
3328
|
+
function redact(s){ return s.replace(SECRET, '$1***'); }
|
|
3329
|
+
function level(s){
|
|
3330
|
+
const t = s.toLowerCase();
|
|
3331
|
+
if (t.indexOf('error')>=0 || t.indexOf('fail')>=0 || t.indexOf('fatal')>=0 || t.indexOf('exception')>=0 || t.indexOf('denied')>=0 || t.indexOf('refused')>=0 || t.indexOf('cannot')>=0 || t.indexOf('panic')>=0) return 'error';
|
|
3332
|
+
if (t.indexOf('warn')>=0) return 'warn';
|
|
3333
|
+
return 'info';
|
|
3334
|
+
}
|
|
3335
|
+
function flush(){
|
|
3336
|
+
const parts = buf.split('\\n');
|
|
3337
|
+
buf = parts.pop() || '';
|
|
3338
|
+
for (const r of parts){
|
|
3339
|
+
if (!r) continue;
|
|
3340
|
+
const line = redact(r);
|
|
3341
|
+
try { fs.writeSync(fd, JSON.stringify({ ts: new Date().toISOString(), level: level(line), source: 'app', line }) + '\\n'); } catch (e) {}
|
|
3342
|
+
}
|
|
3343
|
+
}
|
|
3344
|
+
process.stdin.on('data', (c) => { buf += c.toString('utf8'); flush(); });
|
|
3345
|
+
process.stdin.on('end', () => { flush(); try { fs.closeSync(fd); } catch (e) {} process.exit(0); });
|
|
3346
|
+
process.stdin.on('error', () => { process.exit(0); });
|
|
3347
|
+
`;
|
|
3348
|
+
function annotateRestart(logFilePath, cause) {
|
|
3349
|
+
try {
|
|
3350
|
+
const fd = openSync(logFilePath, "a");
|
|
3351
|
+
try {
|
|
3352
|
+
writeSync(fd, `
|
|
3353
|
+
${RESTART_CAUSE} cause=${cause} at=${(/* @__PURE__ */ new Date()).toISOString()}
|
|
3354
|
+
`);
|
|
3355
|
+
} finally {
|
|
3356
|
+
closeSync(fd);
|
|
3357
|
+
}
|
|
3358
|
+
} catch {
|
|
3359
|
+
}
|
|
3360
|
+
}
|
|
3361
|
+
|
|
3362
|
+
// src/commands/supervisor.ts
|
|
3363
|
+
import pc4 from "picocolors";
|
|
3364
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync2, unlinkSync, mkdirSync as mkdirSync2, openSync as openSync2, closeSync as closeSync2 } from "fs";
|
|
3365
|
+
import { spawn as spawn2 } from "child_process";
|
|
3366
|
+
import { dirname as dirname2 } from "path";
|
|
3367
|
+
var POLL_INTERVAL_MS = 3e3;
|
|
3368
|
+
var CRASH_WINDOW_MS = 6e4;
|
|
3369
|
+
var MAX_RESTARTS_IN_WINDOW = 5;
|
|
3370
|
+
var FLAPPING_THRESHOLD = 3;
|
|
3371
|
+
var PORT_GRACE_MS = 15e3;
|
|
3372
|
+
var LOG_ROTATE_CHECK_MS = 60 * 60 * 1e3;
|
|
3373
|
+
function getSupervisorPid() {
|
|
3374
|
+
const path = getSupervisorPidPath();
|
|
3375
|
+
if (!existsSync4(path)) return null;
|
|
3376
|
+
const pid = parseInt(readFileSync3(path, "utf-8").trim(), 10);
|
|
3377
|
+
if (isNaN(pid)) return null;
|
|
3378
|
+
return isProcessRunning(pid) ? pid : null;
|
|
3379
|
+
}
|
|
3380
|
+
function ensureSupervisorRunning() {
|
|
3381
|
+
const existing = getSupervisorPid();
|
|
3382
|
+
if (existing) return existing;
|
|
3383
|
+
return startSupervisorDaemon();
|
|
3384
|
+
}
|
|
3385
|
+
function startSupervisorDaemon() {
|
|
3386
|
+
const logFilePath = logFilePathFor("supervisor");
|
|
3387
|
+
mkdirSync2(dirname2(logFilePath), { recursive: true });
|
|
3388
|
+
const fd = openSync2(logFilePath, "a");
|
|
3389
|
+
const child = spawn2(process.execPath, [process.argv[1], "__supervisor"], {
|
|
3390
|
+
env: { ...process.env },
|
|
3391
|
+
stdio: ["ignore", fd, fd],
|
|
3392
|
+
detached: true
|
|
3393
|
+
});
|
|
3394
|
+
child.unref();
|
|
3395
|
+
child.once("spawn", () => {
|
|
3396
|
+
try {
|
|
3397
|
+
closeSync2(fd);
|
|
3398
|
+
} catch {
|
|
3399
|
+
}
|
|
3400
|
+
});
|
|
3401
|
+
return child.pid ?? 0;
|
|
3402
|
+
}
|
|
3403
|
+
async function supervisorCommand(args2) {
|
|
3404
|
+
const sub = args2[0] ?? "status";
|
|
3405
|
+
switch (sub) {
|
|
3406
|
+
case "start":
|
|
3407
|
+
return supervisorStart();
|
|
3408
|
+
case "stop":
|
|
3409
|
+
return supervisorStop();
|
|
3410
|
+
case "restart":
|
|
3411
|
+
await supervisorStop();
|
|
3412
|
+
return supervisorStart();
|
|
3413
|
+
case "status":
|
|
3414
|
+
return supervisorStatus();
|
|
3415
|
+
default:
|
|
3416
|
+
console.error(renderError("Unknown sub-command", `"${sub}" \u2014 use: start | stop | status | restart`));
|
|
3417
|
+
process.exit(1);
|
|
2556
3418
|
}
|
|
2557
3419
|
}
|
|
2558
|
-
function
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
return null;
|
|
3420
|
+
function supervisorStart() {
|
|
3421
|
+
const existing = getSupervisorPid();
|
|
3422
|
+
if (existing) {
|
|
3423
|
+
console.error(`
|
|
3424
|
+
${pc4.yellow("\u26A0")} Supervisor already running ${pc4.dim(`(PID ${existing})`)}
|
|
3425
|
+
`);
|
|
3426
|
+
return;
|
|
2566
3427
|
}
|
|
3428
|
+
const pid = startSupervisorDaemon();
|
|
3429
|
+
console.error(`
|
|
3430
|
+
${pc4.green("\u2713")} ${pc4.bold("Supervisor started")} ${pc4.dim(`(PID ${pid})`)}`);
|
|
3431
|
+
console.error(` ${renderKV("Logs", pc4.cyan("fennec log supervisor"))}`);
|
|
3432
|
+
console.error(` ${pc4.dim("It will auto-restart apps started with")} ${pc4.cyan("--restart")} ${pc4.dim("even if you close this terminal.")}
|
|
3433
|
+
`);
|
|
2567
3434
|
}
|
|
2568
|
-
function
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
} catch {
|
|
3435
|
+
function supervisorStop() {
|
|
3436
|
+
const pid = getSupervisorPid();
|
|
3437
|
+
if (!pid) {
|
|
3438
|
+
console.error(`
|
|
3439
|
+
${pc4.dim("Supervisor is not running.")}
|
|
3440
|
+
`);
|
|
3441
|
+
return;
|
|
2576
3442
|
}
|
|
2577
|
-
return 16e6;
|
|
2578
|
-
}
|
|
2579
|
-
function readUptime() {
|
|
2580
3443
|
try {
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
} catch {
|
|
3444
|
+
process.kill(pid, "SIGTERM");
|
|
3445
|
+
console.error(`
|
|
3446
|
+
${pc4.green("\u2713")} ${pc4.bold("Supervisor stopped")} ${pc4.dim(`(PID ${pid})`)}
|
|
3447
|
+
`);
|
|
3448
|
+
} catch (err) {
|
|
3449
|
+
console.error(renderError("Failed to stop supervisor", String(err)));
|
|
2586
3450
|
}
|
|
2587
|
-
return 0;
|
|
2588
|
-
}
|
|
2589
|
-
function formatStartTime(starttime, uptimeSeconds, hertz) {
|
|
2590
3451
|
try {
|
|
2591
|
-
|
|
2592
|
-
const processStart = bootTime + starttime / hertz;
|
|
2593
|
-
return new Date(processStart * 1e3).toISOString();
|
|
3452
|
+
unlinkSync(getSupervisorPidPath());
|
|
2594
3453
|
} catch {
|
|
2595
|
-
return null;
|
|
2596
3454
|
}
|
|
2597
3455
|
}
|
|
2598
|
-
function
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
3456
|
+
function supervisorStatus() {
|
|
3457
|
+
const pid = getSupervisorPid();
|
|
3458
|
+
const tracked = readTracked();
|
|
3459
|
+
const managed = tracked.filter((t) => t.autoRestart);
|
|
3460
|
+
console.error(`
|
|
3461
|
+
${symbols.fox} ${pc4.bold("Fennec Supervisor")}`);
|
|
3462
|
+
if (pid) {
|
|
3463
|
+
console.error(` ${pc4.green("\u25CF")} running ${pc4.dim(`(PID ${pid})`)}`);
|
|
2602
3464
|
} else {
|
|
2603
|
-
|
|
3465
|
+
console.error(` ${pc4.red("\u25CB")} not running ${pc4.dim("\u2014 start with")} ${pc4.cyan("fennec supervisor start")}`);
|
|
2604
3466
|
}
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
}
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
);
|
|
3467
|
+
console.error(`
|
|
3468
|
+
${pc4.bold("Managed apps")} ${pc4.dim(`(${managed.length} with auto-restart)`)}`);
|
|
3469
|
+
if (managed.length === 0) {
|
|
3470
|
+
console.error(` ${pc4.dim("None. Start one with")} ${pc4.cyan("fennec start <cmd> --name <name> --restart")}`);
|
|
3471
|
+
} else {
|
|
3472
|
+
for (const t of managed) {
|
|
3473
|
+
const running = isTrackedRunning(t);
|
|
3474
|
+
const dot = running ? pc4.green("\u25CF") : pc4.red("\u25CB");
|
|
3475
|
+
const state = running ? pc4.green(`running (PID ${t.pid})`) : pc4.red("stopped");
|
|
3476
|
+
console.error(` ${dot} ${pc4.bold(t.name)} ${pc4.dim("\u2014")} ${state}`);
|
|
2614
3477
|
}
|
|
2615
|
-
|
|
2616
|
-
|
|
3478
|
+
}
|
|
3479
|
+
console.error();
|
|
3480
|
+
}
|
|
3481
|
+
var PORT_FAIL_THRESHOLD = 2;
|
|
3482
|
+
async function runSupervisor() {
|
|
3483
|
+
const pidPath = getSupervisorPidPath();
|
|
3484
|
+
mkdirSync2(dirname2(pidPath), { recursive: true });
|
|
3485
|
+
const existing = getSupervisorPid();
|
|
3486
|
+
if (existing && existing !== process.pid) {
|
|
3487
|
+
log(`supervisor already running (PID ${existing}); exiting`);
|
|
3488
|
+
return;
|
|
3489
|
+
}
|
|
3490
|
+
writeFileSync2(pidPath, String(process.pid), "utf-8");
|
|
3491
|
+
log(`supervisor started (PID ${process.pid}), polling every ${POLL_INTERVAL_MS}ms`);
|
|
3492
|
+
const crashes = /* @__PURE__ */ new Map();
|
|
3493
|
+
let stopped = false;
|
|
3494
|
+
const cleanup = () => {
|
|
3495
|
+
stopped = true;
|
|
3496
|
+
try {
|
|
3497
|
+
unlinkSync(pidPath);
|
|
3498
|
+
} catch {
|
|
2617
3499
|
}
|
|
2618
|
-
|
|
2619
|
-
|
|
3500
|
+
log("supervisor stopping");
|
|
3501
|
+
process.exit(0);
|
|
3502
|
+
};
|
|
3503
|
+
process.on("SIGTERM", cleanup);
|
|
3504
|
+
process.on("SIGINT", cleanup);
|
|
3505
|
+
const tick = async () => {
|
|
3506
|
+
if (stopped) return;
|
|
3507
|
+
let tracked;
|
|
3508
|
+
try {
|
|
3509
|
+
tracked = readTracked();
|
|
3510
|
+
} catch (err) {
|
|
3511
|
+
log(`failed to read tracked.json: ${String(err)}`);
|
|
3512
|
+
return;
|
|
2620
3513
|
}
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
3514
|
+
for (const t of tracked) {
|
|
3515
|
+
if (!t.autoRestart) continue;
|
|
3516
|
+
if (t.port && isTrackedRunning(t)) {
|
|
3517
|
+
const startedAt = Date.parse(t.startedAt);
|
|
3518
|
+
const ageMs = Number.isNaN(startedAt) ? Number.POSITIVE_INFINITY : Date.now() - startedAt;
|
|
3519
|
+
if (ageMs >= PORT_GRACE_MS) {
|
|
3520
|
+
let rec2 = crashes.get(t.name);
|
|
3521
|
+
if (!rec2) {
|
|
3522
|
+
rec2 = { windowStart: Date.now(), count: 0, gaveUp: false, portFails: 0 };
|
|
3523
|
+
crashes.set(t.name, rec2);
|
|
3524
|
+
}
|
|
3525
|
+
let listening = false;
|
|
3526
|
+
const healthUrl = t.healthCheck ? resolveHealthUrl(t.healthCheck, t.port) : null;
|
|
3527
|
+
try {
|
|
3528
|
+
listening = healthUrl ? await checkHttp(healthUrl) : await checkPort(t.port);
|
|
3529
|
+
} catch {
|
|
3530
|
+
listening = false;
|
|
3531
|
+
}
|
|
3532
|
+
if (listening) {
|
|
3533
|
+
rec2.portFails = 0;
|
|
3534
|
+
if (t.flapping && Date.now() - rec2.windowStart > CRASH_WINDOW_MS) setFlapping(t, false);
|
|
3535
|
+
} else {
|
|
3536
|
+
rec2.portFails++;
|
|
3537
|
+
const probe = healthUrl ? `health check ${healthUrl}` : `port ${t.port}`;
|
|
3538
|
+
if (rec2.portFails >= PORT_FAIL_THRESHOLD) {
|
|
3539
|
+
rec2.portFails = 0;
|
|
3540
|
+
rec2.count++;
|
|
3541
|
+
if (rec2.count >= FLAPPING_THRESHOLD) setFlapping(t, true);
|
|
3542
|
+
log(`${t.name}: alive but ${probe} not responding ${PORT_FAIL_THRESHOLD}x \u2014 restarting`);
|
|
3543
|
+
try {
|
|
3544
|
+
const newPid = respawnTracked(t, "port-down");
|
|
3545
|
+
log(`${t.name}: restarted (PID ${newPid}) after ${probe} failure`);
|
|
3546
|
+
} catch (err) {
|
|
3547
|
+
log(`${t.name}: restart failed: ${String(err)}`);
|
|
3548
|
+
}
|
|
3549
|
+
} else {
|
|
3550
|
+
log(`${t.name}: ${probe} not responding (${rec2.portFails}/${PORT_FAIL_THRESHOLD})`);
|
|
3551
|
+
}
|
|
3552
|
+
}
|
|
2634
3553
|
}
|
|
2635
|
-
|
|
3554
|
+
continue;
|
|
3555
|
+
}
|
|
3556
|
+
if (isTrackedRunning(t)) continue;
|
|
3557
|
+
const now = Date.now();
|
|
3558
|
+
let rec = crashes.get(t.name);
|
|
3559
|
+
if (rec?.gaveUp && now - rec.windowStart > CRASH_WINDOW_MS) rec = void 0;
|
|
3560
|
+
if (!rec || now - rec.windowStart > CRASH_WINDOW_MS) {
|
|
3561
|
+
rec = { windowStart: now, count: 0, gaveUp: false, portFails: 0 };
|
|
3562
|
+
crashes.set(t.name, rec);
|
|
3563
|
+
if (t.flapping) setFlapping(t, false);
|
|
3564
|
+
}
|
|
3565
|
+
if (rec.gaveUp) continue;
|
|
3566
|
+
if (rec.count >= MAX_RESTARTS_IN_WINDOW) {
|
|
3567
|
+
rec.gaveUp = true;
|
|
3568
|
+
log(`${t.name}: crashed ${rec.count}x in <60s \u2014 giving up (will retry after window resets)`);
|
|
3569
|
+
continue;
|
|
3570
|
+
}
|
|
3571
|
+
rec.count++;
|
|
3572
|
+
if (rec.count >= FLAPPING_THRESHOLD) setFlapping(t, true);
|
|
3573
|
+
try {
|
|
3574
|
+
const newPid = respawnTracked(t, "crash");
|
|
3575
|
+
log(`${t.name}: restarted (PID ${newPid}) [${rec.count}/${MAX_RESTARTS_IN_WINDOW} in window]`);
|
|
3576
|
+
} catch (err) {
|
|
3577
|
+
log(`${t.name}: restart failed: ${String(err)}`);
|
|
3578
|
+
}
|
|
2636
3579
|
}
|
|
2637
|
-
|
|
2638
|
-
|
|
3580
|
+
};
|
|
3581
|
+
tick();
|
|
3582
|
+
setInterval(tick, POLL_INTERVAL_MS);
|
|
3583
|
+
setInterval(rotateAllLogs, LOG_ROTATE_CHECK_MS);
|
|
3584
|
+
await new Promise(() => {
|
|
3585
|
+
});
|
|
3586
|
+
}
|
|
3587
|
+
function rotateAllLogs() {
|
|
3588
|
+
try {
|
|
3589
|
+
for (const t of readTracked()) {
|
|
3590
|
+
try {
|
|
3591
|
+
rotateLogFile(logFilePathFor(t.name));
|
|
3592
|
+
} catch {
|
|
3593
|
+
}
|
|
2639
3594
|
}
|
|
3595
|
+
} catch (err) {
|
|
3596
|
+
log(`log rotation pass failed: ${String(err)}`);
|
|
2640
3597
|
}
|
|
2641
|
-
return processes;
|
|
2642
3598
|
}
|
|
2643
|
-
function
|
|
3599
|
+
function setFlapping(t, val) {
|
|
3600
|
+
if (!!t.flapping === val) return;
|
|
3601
|
+
t.flapping = val;
|
|
2644
3602
|
try {
|
|
2645
|
-
|
|
2646
|
-
return true;
|
|
3603
|
+
addTracked({ ...t, flapping: val });
|
|
2647
3604
|
} catch {
|
|
2648
|
-
return false;
|
|
2649
3605
|
}
|
|
2650
3606
|
}
|
|
2651
|
-
function
|
|
3607
|
+
function log(msg) {
|
|
3608
|
+
process.stdout.write(`[${(/* @__PURE__ */ new Date()).toISOString()}] ${msg}
|
|
3609
|
+
`);
|
|
3610
|
+
}
|
|
3611
|
+
|
|
3612
|
+
// src/commands/persist.ts
|
|
3613
|
+
import pc5 from "picocolors";
|
|
3614
|
+
import { existsSync as existsSync5, writeFileSync as writeFileSync3, unlinkSync as unlinkSync2, mkdirSync as mkdirSync3 } from "fs";
|
|
3615
|
+
import { resolve as resolve3, dirname as dirname3 } from "path";
|
|
3616
|
+
import { homedir as homedir2, platform } from "os";
|
|
3617
|
+
import { execFileSync } from "child_process";
|
|
3618
|
+
function detectBackend() {
|
|
3619
|
+
const p = platform();
|
|
3620
|
+
if (p === "linux" || p === "freebsd") {
|
|
3621
|
+
return "systemd";
|
|
3622
|
+
}
|
|
3623
|
+
if (p === "darwin") return "launchd";
|
|
3624
|
+
if (p === "win32") return "windows";
|
|
3625
|
+
return "unsupported";
|
|
3626
|
+
}
|
|
3627
|
+
function unitPaths(backend) {
|
|
3628
|
+
const fennecDir = getFennecDir();
|
|
3629
|
+
switch (backend) {
|
|
3630
|
+
case "systemd":
|
|
3631
|
+
return { unit: resolve3(homedir2(), ".config", "systemd", "user", "fennec-supervisor.service") };
|
|
3632
|
+
case "launchd":
|
|
3633
|
+
return { unit: resolve3(homedir2(), "Library", "LaunchAgents", "io.fennec.supervisor.plist") };
|
|
3634
|
+
case "windows":
|
|
3635
|
+
return { unit: resolve3(homedir2(), "AppData", "Roaming", "Microsoft", "Windows", "Start Menu", "Programs", "Startup", "fennec-supervisor.bat") };
|
|
3636
|
+
default:
|
|
3637
|
+
return { unit: resolve3(fennecDir, "persist.unit") };
|
|
3638
|
+
}
|
|
3639
|
+
}
|
|
3640
|
+
function buildUnit(backend, dataDir) {
|
|
3641
|
+
const nodeBin = process.execPath;
|
|
3642
|
+
const cli = process.argv[1] ?? "fennec";
|
|
3643
|
+
const envLine = `FENNEC_DATA_DIR=${dataDir}`;
|
|
3644
|
+
if (backend === "systemd") {
|
|
3645
|
+
return `[Unit]
|
|
3646
|
+
Description=Fennec process supervisor (auto-restarts managed apps)
|
|
3647
|
+
After=network.target
|
|
3648
|
+
|
|
3649
|
+
[Service]
|
|
3650
|
+
Type=simple
|
|
3651
|
+
Environment=${envLine}
|
|
3652
|
+
ExecStart=${nodeBin} ${cli} supervisor start
|
|
3653
|
+
Restart=on-failure
|
|
3654
|
+
|
|
3655
|
+
[Install]
|
|
3656
|
+
WantedBy=default.target
|
|
3657
|
+
`;
|
|
3658
|
+
}
|
|
3659
|
+
if (backend === "launchd") {
|
|
3660
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
3661
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3662
|
+
<plist version="1.0">
|
|
3663
|
+
<dict>
|
|
3664
|
+
<key>Label</key>
|
|
3665
|
+
<string>io.fennec.supervisor</string>
|
|
3666
|
+
<key>ProgramArguments</key>
|
|
3667
|
+
<array>
|
|
3668
|
+
<string>${nodeBin}</string>
|
|
3669
|
+
<string>${cli}</string>
|
|
3670
|
+
<string>supervisor</string>
|
|
3671
|
+
<string>start</string>
|
|
3672
|
+
</array>
|
|
3673
|
+
<key>EnvironmentVariables</key>
|
|
3674
|
+
<dict>
|
|
3675
|
+
<key>FENNEC_DATA_DIR</key>
|
|
3676
|
+
<string>${dataDir}</string>
|
|
3677
|
+
</dict>
|
|
3678
|
+
<key>RunAtLoad</key>
|
|
3679
|
+
<true/>
|
|
3680
|
+
<key>KeepAlive</key>
|
|
3681
|
+
<false/>
|
|
3682
|
+
</dict>
|
|
3683
|
+
</plist>
|
|
3684
|
+
`;
|
|
3685
|
+
}
|
|
3686
|
+
return `@echo off
|
|
3687
|
+
set FENNEC_DATA_DIR=${dataDir}
|
|
3688
|
+
"${nodeBin}" "${cli}" supervisor start
|
|
3689
|
+
`;
|
|
3690
|
+
}
|
|
3691
|
+
function run(cmd, args2) {
|
|
2652
3692
|
try {
|
|
2653
|
-
|
|
2654
|
-
return true;
|
|
2655
|
-
} catch {
|
|
2656
|
-
|
|
3693
|
+
const out = execFileSync(cmd, args2, { stdio: ["ignore", "pipe", "pipe"] }).toString().trim();
|
|
3694
|
+
return { ok: true, out };
|
|
3695
|
+
} catch (err) {
|
|
3696
|
+
const e = err;
|
|
3697
|
+
const out = (e.stdout ?? e.stderr ?? Buffer.from("")).toString().trim();
|
|
3698
|
+
return { ok: false, out };
|
|
2657
3699
|
}
|
|
2658
3700
|
}
|
|
2659
|
-
function
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
3701
|
+
function persistEnabled(backend = detectBackend()) {
|
|
3702
|
+
const { unit } = unitPaths(backend);
|
|
3703
|
+
return existsSync5(unit);
|
|
3704
|
+
}
|
|
3705
|
+
function ensurePersistEnabled() {
|
|
3706
|
+
const backend = detectBackend();
|
|
3707
|
+
if (backend === "unsupported") return false;
|
|
3708
|
+
if (persistEnabled(backend)) return false;
|
|
3709
|
+
persistEnable(backend);
|
|
3710
|
+
return true;
|
|
3711
|
+
}
|
|
3712
|
+
async function persistCommand(args2) {
|
|
3713
|
+
const sub = args2[0] ?? "status";
|
|
3714
|
+
const backend = detectBackend();
|
|
3715
|
+
if (backend === "unsupported") {
|
|
3716
|
+
console.error(renderError("Unsupported platform", `Boot persistence isn't available on ${platform()}. You can still run "fennec supervisor start" manually.`));
|
|
3717
|
+
process.exit(1);
|
|
3718
|
+
}
|
|
3719
|
+
switch (sub) {
|
|
3720
|
+
case "enable":
|
|
3721
|
+
return persistEnable(backend);
|
|
3722
|
+
case "disable":
|
|
3723
|
+
return persistDisable(backend);
|
|
3724
|
+
case "status":
|
|
3725
|
+
return persistStatus(backend);
|
|
2677
3726
|
default:
|
|
2678
|
-
|
|
3727
|
+
console.error(renderError("Unknown sub-command", `"${sub}" \u2014 use: enable | disable | status`));
|
|
3728
|
+
process.exit(1);
|
|
3729
|
+
}
|
|
3730
|
+
}
|
|
3731
|
+
function persistEnable(backend) {
|
|
3732
|
+
const dataDir = getFennecDir();
|
|
3733
|
+
const { unit } = unitPaths(backend);
|
|
3734
|
+
mkdirSync3(dirname3(unit), { recursive: true });
|
|
3735
|
+
writeFileSync3(unit, buildUnit(backend, dataDir), "utf-8");
|
|
3736
|
+
if (backend === "windows") {
|
|
3737
|
+
console.error(`
|
|
3738
|
+
${pc5.green("\u2713")} ${pc5.bold("Boot script installed")} ${pc5.dim(`(runs at next login)`)}`);
|
|
3739
|
+
console.error(` ${renderKV("Path", pc5.cyan(unit))}`);
|
|
3740
|
+
console.error(` ${pc5.dim("Supervisor will start automatically when you log in.")}
|
|
3741
|
+
`);
|
|
3742
|
+
return;
|
|
3743
|
+
}
|
|
3744
|
+
if (backend === "systemd") {
|
|
3745
|
+
const reg = run("systemctl", ["--user", "enable", "--now", resolve3(unit)]);
|
|
3746
|
+
if (!reg.ok) {
|
|
3747
|
+
run("systemctl", ["--user", "enable", resolve3(unit)]);
|
|
3748
|
+
run("systemctl", ["--user", "start", "fennec-supervisor.service"]);
|
|
3749
|
+
}
|
|
3750
|
+
const linger = run("loginctl", ["enable-linger"]);
|
|
3751
|
+
if (!linger.ok) {
|
|
3752
|
+
console.error(` ${pc5.yellow("\u26A0")} ${pc5.dim("Could not enable user linger \u2014 the supervisor may stop when you log out.")}`);
|
|
3753
|
+
}
|
|
3754
|
+
console.error(`
|
|
3755
|
+
${pc5.green("\u2713")} ${pc5.bold("Boot persistence enabled")} ${pc5.dim("(systemd user service)")}`);
|
|
3756
|
+
console.error(` ${renderKV("Unit", pc5.cyan("fennec-supervisor.service"))}`);
|
|
3757
|
+
console.error(` ${renderKV("Data dir", pc5.cyan(dataDir))}`);
|
|
3758
|
+
console.error(` ${pc5.dim("The supervisor (and all --restart apps) will start at login and keep running after logout.")}
|
|
3759
|
+
`);
|
|
3760
|
+
return;
|
|
3761
|
+
}
|
|
3762
|
+
if (backend === "launchd") {
|
|
3763
|
+
run("launchctl", ["load", resolve3(unit)]);
|
|
3764
|
+
console.error(`
|
|
3765
|
+
${pc5.green("\u2713")} ${pc5.bold("Boot persistence enabled")} ${pc5.dim("(launchd agent)")}`);
|
|
3766
|
+
console.error(` ${renderKV("Label", pc5.cyan("io.fennec.supervisor"))}`);
|
|
3767
|
+
console.error(` ${renderKV("Data dir", pc5.cyan(dataDir))}`);
|
|
3768
|
+
console.error(` ${pc5.dim("The supervisor will start at login.")}
|
|
3769
|
+
`);
|
|
3770
|
+
return;
|
|
3771
|
+
}
|
|
3772
|
+
}
|
|
3773
|
+
function persistDisable(backend) {
|
|
3774
|
+
const { unit } = unitPaths(backend);
|
|
3775
|
+
if (backend === "systemd") {
|
|
3776
|
+
run("systemctl", ["--user", "disable", "--now", "fennec-supervisor.service"]);
|
|
3777
|
+
} else if (backend === "launchd") {
|
|
3778
|
+
run("launchctl", ["unload", resolve3(unit)]);
|
|
3779
|
+
}
|
|
3780
|
+
try {
|
|
3781
|
+
if (existsSync5(unit)) unlinkSync2(unit);
|
|
3782
|
+
} catch {
|
|
3783
|
+
}
|
|
3784
|
+
console.error(`
|
|
3785
|
+
${pc5.green("\u2713")} ${pc5.bold("Boot persistence disabled")}
|
|
3786
|
+
`);
|
|
3787
|
+
}
|
|
3788
|
+
function persistStatus(backend) {
|
|
3789
|
+
const { unit } = unitPaths(backend);
|
|
3790
|
+
const installed = existsSync5(unit);
|
|
3791
|
+
const label = backend === "systemd" ? "fennec-supervisor.service" : backend === "launchd" ? "io.fennec.supervisor" : "fennec-supervisor.bat";
|
|
3792
|
+
console.error(`
|
|
3793
|
+
${pc5.bold("Fennec Boot Persistence")} ${pc5.dim(`(${backend})`)}`);
|
|
3794
|
+
if (!installed) {
|
|
3795
|
+
console.error(` ${pc5.red("\u25CB")} ${pc5.dim("not installed \u2014 run ")}${pc5.cyan("fennec persist enable")}${pc5.dim(" to auto-start apps after reboot")}`);
|
|
3796
|
+
console.error();
|
|
3797
|
+
return;
|
|
3798
|
+
}
|
|
3799
|
+
if (backend === "systemd") {
|
|
3800
|
+
const en = run("systemctl", ["--user", "is-enabled", "fennec-supervisor.service"]).out;
|
|
3801
|
+
const ac = run("systemctl", ["--user", "is-active", "fennec-supervisor.service"]).out;
|
|
3802
|
+
console.error(` ${pc5.green("\u25CF")} ${pc5.bold("installed")}`);
|
|
3803
|
+
console.error(` ${renderKV("Enabled", en || "?")}`);
|
|
3804
|
+
console.error(` ${renderKV("Active", ac || "?")}`);
|
|
3805
|
+
} else if (backend === "launchd") {
|
|
3806
|
+
const loaded = run("launchctl", ["list", "io.fennec.supervisor"]).ok;
|
|
3807
|
+
console.error(` ${pc5.green("\u25CF")} ${pc5.bold("installed")}`);
|
|
3808
|
+
console.error(` ${renderKV("Loaded", loaded ? pc5.green("yes") : pc5.red("no"))}`);
|
|
3809
|
+
} else {
|
|
3810
|
+
console.error(` ${pc5.green("\u25CF")} ${pc5.bold("installed")} ${pc5.dim("(runs at login)")}`);
|
|
2679
3811
|
}
|
|
3812
|
+
console.error(` ${renderKV("Unit", pc5.cyan(label))}`);
|
|
3813
|
+
console.error(` ${renderKV("Path", pc5.cyan(unit))}`);
|
|
3814
|
+
console.error();
|
|
2680
3815
|
}
|
|
2681
3816
|
|
|
2682
3817
|
// src/commands/ps.ts
|
|
2683
|
-
import
|
|
3818
|
+
import pc6 from "picocolors";
|
|
2684
3819
|
async function psCommand(args2) {
|
|
2685
3820
|
const watchFlag = args2.includes("-w") || args2.includes("--watch");
|
|
2686
3821
|
const systemFlag = args2.includes("--system") || args2.includes("-a") || args2.includes("--all");
|
|
@@ -2703,32 +3838,32 @@ async function psCommand(args2) {
|
|
|
2703
3838
|
process.stdout.write("\r\x1B[K");
|
|
2704
3839
|
if (processes.length === 0) {
|
|
2705
3840
|
console.error(`
|
|
2706
|
-
${
|
|
3841
|
+
${pc6.dim("No system processes found.")}
|
|
2707
3842
|
`);
|
|
2708
3843
|
return;
|
|
2709
3844
|
}
|
|
2710
3845
|
const columns2 = [
|
|
2711
|
-
{ key: "pid", label: "PID", align: "right", format: (v) =>
|
|
2712
|
-
{ key: "name", label: "Name", format: (v) =>
|
|
3846
|
+
{ key: "pid", label: "PID", align: "right", format: (v) => pc6.dim(String(v).padStart(6)) },
|
|
3847
|
+
{ key: "name", label: "Name", format: (v) => pc6.bold(String(v)) },
|
|
2713
3848
|
{ key: "cpu", label: "CPU%", align: "right", format: (v) => {
|
|
2714
3849
|
const n = v;
|
|
2715
|
-
return n > 10 ?
|
|
3850
|
+
return n > 10 ? pc6.red(String(n)) : n > 5 ? pc6.yellow(String(n)) : pc6.dim(String(n));
|
|
2716
3851
|
} },
|
|
2717
3852
|
{ key: "mem", label: "MEM%", align: "right", format: (v) => {
|
|
2718
3853
|
const n = v;
|
|
2719
|
-
return n > 10 ?
|
|
3854
|
+
return n > 10 ? pc6.red(String(n)) : n > 5 ? pc6.yellow(String(n)) : pc6.dim(String(n));
|
|
2720
3855
|
} },
|
|
2721
3856
|
{ key: "state", label: "State", format: (v) => {
|
|
2722
3857
|
const s = String(v);
|
|
2723
|
-
if (s === "R" || s === "Running") return
|
|
2724
|
-
if (s === "Z" || s === "Zombie") return
|
|
2725
|
-
if (s === "S" || s === "Sleeping") return
|
|
2726
|
-
return
|
|
3858
|
+
if (s === "R" || s === "Running") return pc6.green(s);
|
|
3859
|
+
if (s === "Z" || s === "Zombie") return pc6.red(s);
|
|
3860
|
+
if (s === "S" || s === "Sleeping") return pc6.cyan(s);
|
|
3861
|
+
return pc6.dim(s);
|
|
2727
3862
|
} }
|
|
2728
3863
|
];
|
|
2729
3864
|
const rows2 = processes.map((p) => ({ pid: p.pid, name: p.name, cpu: p.cpuPercent, mem: p.memPercent, state: formatProcessState(p.state) }));
|
|
2730
3865
|
console.error(`
|
|
2731
|
-
${symbols.fox} ${
|
|
3866
|
+
${symbols.fox} ${pc6.bold("System Processes")} ${pc6.dim(`(top ${processes.length} by ${sortBy})`)}
|
|
2732
3867
|
`);
|
|
2733
3868
|
console.error(renderTable(columns2, rows2));
|
|
2734
3869
|
console.error();
|
|
@@ -2741,49 +3876,49 @@ async function psCommand(args2) {
|
|
|
2741
3876
|
const tracked = readTracked();
|
|
2742
3877
|
if (tracked.length === 0) {
|
|
2743
3878
|
console.error(`
|
|
2744
|
-
${
|
|
2745
|
-
console.error(` ${
|
|
3879
|
+
${pc6.dim("No tracked processes.")}`);
|
|
3880
|
+
console.error(` ${pc6.dim("Start an app with:")} ${pc6.cyan("fennec start <command> --name <name>")}
|
|
2746
3881
|
`);
|
|
2747
3882
|
return;
|
|
2748
3883
|
}
|
|
2749
3884
|
const columns = [
|
|
2750
|
-
{ key: "name", label: "App", format: (v) =>
|
|
3885
|
+
{ key: "name", label: "App", format: (v) => pc6.bold(String(v)) },
|
|
2751
3886
|
{ key: "pid", label: "PID", align: "right" },
|
|
2752
3887
|
{ key: "status", label: "Status", format: (v) => {
|
|
2753
3888
|
const s = v;
|
|
2754
|
-
return s === "running" ?
|
|
3889
|
+
return s === "running" ? pc6.green("\u25CF running") : pc6.red("\u25CB stopped");
|
|
2755
3890
|
} },
|
|
2756
3891
|
{ key: "port", label: "Port", format: (v) => {
|
|
2757
3892
|
const p = v;
|
|
2758
|
-
return p ?
|
|
3893
|
+
return p ? pc6.yellow(`:${p}`) : pc6.dim("-");
|
|
2759
3894
|
} },
|
|
2760
3895
|
{ key: "command", label: "Command", format: (v) => {
|
|
2761
3896
|
const c = String(v);
|
|
2762
3897
|
return c.length > 50 ? c.slice(0, 50) + "\u2026" : c;
|
|
2763
3898
|
} },
|
|
2764
|
-
{ key: "uptime", label: "Uptime", format: (v) =>
|
|
3899
|
+
{ key: "uptime", label: "Uptime", format: (v) => pc6.dim(String(v)) }
|
|
2765
3900
|
];
|
|
2766
3901
|
const rows = tracked.map((t) => {
|
|
2767
|
-
const running =
|
|
3902
|
+
const running = isTrackedRunning(t);
|
|
2768
3903
|
const uptime = running ? formatUptime(Math.floor((Date.now() - new Date(t.startedAt).getTime()) / 1e3)) : "-";
|
|
2769
|
-
return { name: t.name, pid: running ? String(t.pid) :
|
|
3904
|
+
return { name: t.name, pid: running ? String(t.pid) : pc6.dim(String(t.pid)), status: running ? "running" : "stopped", port: t.port ?? null, command: t.command, uptime };
|
|
2770
3905
|
});
|
|
2771
|
-
const runningCount = tracked.filter((t) =>
|
|
3906
|
+
const runningCount = tracked.filter((t) => isTrackedRunning(t)).length;
|
|
2772
3907
|
console.error(`
|
|
2773
|
-
${symbols.fox} ${
|
|
3908
|
+
${symbols.fox} ${pc6.bold("Fennec Apps")} ${pc6.dim(`(${runningCount}/${tracked.length} running)`)}
|
|
2774
3909
|
`);
|
|
2775
3910
|
console.error(renderTable(columns, rows));
|
|
2776
|
-
console.error(` ${
|
|
2777
|
-
console.error(` ${
|
|
2778
|
-
console.error(` ${
|
|
2779
|
-
console.error(` ${
|
|
2780
|
-
console.error(` ${
|
|
3911
|
+
console.error(` ${pc6.dim("Use")} ${pc6.cyan("fennec start <command> --name <name> --port <port>")} ${pc6.dim("to add more apps.")}`);
|
|
3912
|
+
console.error(` ${pc6.dim("Use")} ${pc6.cyan("fennec log <name>")} ${pc6.dim("to view logs.")}`);
|
|
3913
|
+
console.error(` ${pc6.dim("Use")} ${pc6.cyan("fennec stop <name>")} ${pc6.dim("to pause an app.")}`);
|
|
3914
|
+
console.error(` ${pc6.dim("Use")} ${pc6.cyan("fennec spawn <name>")} ${pc6.dim("to resume a paused app.")}`);
|
|
3915
|
+
console.error(` ${pc6.dim("Use")} ${pc6.cyan("fennec kill <name>")} ${pc6.dim("to permanently remove an app.")}`);
|
|
2781
3916
|
console.error();
|
|
2782
3917
|
}
|
|
2783
3918
|
async function psJson() {
|
|
2784
3919
|
const tracked = readTracked();
|
|
2785
3920
|
const data = tracked.map((t) => {
|
|
2786
|
-
const running =
|
|
3921
|
+
const running = isTrackedRunning(t);
|
|
2787
3922
|
return {
|
|
2788
3923
|
name: t.name,
|
|
2789
3924
|
pid: t.pid,
|
|
@@ -2803,54 +3938,54 @@ async function statusCommand(_args) {
|
|
|
2803
3938
|
const topSystem = getSystemProcesses({ userOnly: true, sortBy: "cpu", limit: 5 });
|
|
2804
3939
|
const totalUserProcs = getSystemProcesses({ userOnly: true }).length;
|
|
2805
3940
|
console.error(`
|
|
2806
|
-
${symbols.fox} ${
|
|
3941
|
+
${symbols.fox} ${pc6.bold("Fennec Status")}
|
|
2807
3942
|
`);
|
|
2808
3943
|
if (tracked.length > 0) {
|
|
2809
|
-
const runningCount = tracked.filter((t) =>
|
|
2810
|
-
console.error(` ${
|
|
3944
|
+
const runningCount = tracked.filter((t) => isTrackedRunning(t)).length;
|
|
3945
|
+
console.error(` ${pc6.bold("Managed Apps")} ${pc6.dim(`(${runningCount}/${tracked.length} running)`)}
|
|
2811
3946
|
`);
|
|
2812
3947
|
for (const t of tracked) {
|
|
2813
|
-
const running =
|
|
2814
|
-
const statusIcon = running ?
|
|
2815
|
-
const portStr = t.port ? ` ${
|
|
2816
|
-
const uptime = running ?
|
|
2817
|
-
console.error(` ${statusIcon} ${
|
|
3948
|
+
const running = isTrackedRunning(t);
|
|
3949
|
+
const statusIcon = running ? pc6.green("\u25CF") : pc6.red("\u25CB");
|
|
3950
|
+
const portStr = t.port ? ` ${pc6.yellow(`:${t.port}`)}` : "";
|
|
3951
|
+
const uptime = running ? pc6.dim(formatUptime(Math.floor((Date.now() - new Date(t.startedAt).getTime()) / 1e3))) : pc6.red("stopped");
|
|
3952
|
+
console.error(` ${statusIcon} ${pc6.bold(t.name)}${portStr} ${pc6.dim(`(PID ${t.pid})`)} \u2014 ${uptime}`);
|
|
2818
3953
|
}
|
|
2819
3954
|
console.error();
|
|
2820
3955
|
} else {
|
|
2821
|
-
console.error(` ${
|
|
3956
|
+
console.error(` ${pc6.dim("No managed apps.")} ${pc6.cyan("fennec start <command> --name <name>")}
|
|
2822
3957
|
`);
|
|
2823
3958
|
}
|
|
2824
|
-
console.error(` ${
|
|
3959
|
+
console.error(` ${pc6.bold("System")} ${pc6.dim(`(${totalUserProcs} user processes)`)}`);
|
|
2825
3960
|
for (const p of topSystem) {
|
|
2826
|
-
const cpuStr = p.cpuPercent > 10 ?
|
|
2827
|
-
const memStr = p.memPercent > 10 ?
|
|
2828
|
-
console.error(` ${
|
|
3961
|
+
const cpuStr = p.cpuPercent > 10 ? pc6.red(`${p.cpuPercent}%`) : p.cpuPercent > 5 ? pc6.yellow(`${p.cpuPercent}%`) : pc6.dim(`${p.cpuPercent}%`);
|
|
3962
|
+
const memStr = p.memPercent > 10 ? pc6.red(`${p.memPercent}%`) : p.memPercent > 5 ? pc6.yellow(`${p.memPercent}%`) : pc6.dim(`${p.memPercent}%`);
|
|
3963
|
+
console.error(` ${pc6.dim(`PID ${p.pid}`)} ${pc6.bold(p.name)} \u2014 CPU: ${cpuStr} MEM: ${memStr}`);
|
|
2829
3964
|
}
|
|
2830
3965
|
if (watchFlag) await watchSystemProcesses("cpu", 15);
|
|
2831
3966
|
console.error();
|
|
2832
3967
|
}
|
|
2833
3968
|
async function watchSystemProcesses(sortBy, limit) {
|
|
2834
3969
|
console.error(`
|
|
2835
|
-
${
|
|
3970
|
+
${pc6.bold("Watching system processes")} ${pc6.dim("(Ctrl+C to stop, refreshes every 3s)")}
|
|
2836
3971
|
`);
|
|
2837
3972
|
const render = () => {
|
|
2838
3973
|
const processes = getSystemProcesses({ userOnly: true, sortBy, limit });
|
|
2839
3974
|
const columns = [
|
|
2840
|
-
{ key: "pid", label: "PID", align: "right", format: (v) =>
|
|
2841
|
-
{ key: "name", label: "Name", format: (v) =>
|
|
3975
|
+
{ key: "pid", label: "PID", align: "right", format: (v) => pc6.dim(String(v).padStart(6)) },
|
|
3976
|
+
{ key: "name", label: "Name", format: (v) => pc6.bold(String(v)) },
|
|
2842
3977
|
{ key: "cpu", label: "CPU%", align: "right", format: (v) => {
|
|
2843
3978
|
const n = v;
|
|
2844
|
-
return n > 10 ?
|
|
3979
|
+
return n > 10 ? pc6.red(String(n)) : n > 5 ? pc6.yellow(String(n)) : pc6.dim(String(n));
|
|
2845
3980
|
} },
|
|
2846
3981
|
{ key: "mem", label: "MEM%", align: "right", format: (v) => {
|
|
2847
3982
|
const n = v;
|
|
2848
|
-
return n > 10 ?
|
|
3983
|
+
return n > 10 ? pc6.red(String(n)) : n > 5 ? pc6.yellow(String(n)) : pc6.dim(String(n));
|
|
2849
3984
|
} },
|
|
2850
3985
|
{ key: "state", label: "S", align: "center" }
|
|
2851
3986
|
];
|
|
2852
3987
|
const rows = processes.map((p) => ({ pid: p.pid, name: p.name, cpu: p.cpuPercent, mem: p.memPercent, state: p.state }));
|
|
2853
|
-
return ` ${timestamp()} ${
|
|
3988
|
+
return ` ${timestamp()} ${pc6.dim(`${processes.length} processes`)}
|
|
2854
3989
|
${renderTable(columns, rows, { compact: true })}`;
|
|
2855
3990
|
};
|
|
2856
3991
|
console.error(render());
|
|
@@ -2858,19 +3993,35 @@ ${renderTable(columns, rows, { compact: true })}`;
|
|
|
2858
3993
|
process.stdout.write("\x1B[J");
|
|
2859
3994
|
console.error(render());
|
|
2860
3995
|
}, 3e3);
|
|
2861
|
-
await new Promise((
|
|
3996
|
+
await new Promise((resolve13) => {
|
|
2862
3997
|
process.once("SIGINT", () => {
|
|
2863
3998
|
clearInterval(interval);
|
|
2864
3999
|
process.stdout.write("\x1B[J");
|
|
2865
|
-
|
|
4000
|
+
resolve13();
|
|
2866
4001
|
});
|
|
2867
4002
|
});
|
|
2868
4003
|
}
|
|
2869
4004
|
|
|
2870
4005
|
// src/commands/start.ts
|
|
4006
|
+
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
4007
|
+
async function waitForReady(pid, port, timeoutMs = 8e3) {
|
|
4008
|
+
const start = Date.now();
|
|
4009
|
+
const graceMs = 700;
|
|
4010
|
+
while (Date.now() - start < timeoutMs) {
|
|
4011
|
+
if (!isProcessRunning(pid)) return { running: false, portReady: false };
|
|
4012
|
+
if (port !== void 0) {
|
|
4013
|
+
if (await checkPort(port)) return { running: true, portReady: true };
|
|
4014
|
+
await sleep(250);
|
|
4015
|
+
continue;
|
|
4016
|
+
}
|
|
4017
|
+
if (Date.now() - start >= graceMs) return { running: true, portReady: false };
|
|
4018
|
+
await sleep(150);
|
|
4019
|
+
}
|
|
4020
|
+
return { running: isProcessRunning(pid), portReady: port === void 0 ? false : false };
|
|
4021
|
+
}
|
|
2871
4022
|
async function startServer(args2) {
|
|
2872
4023
|
printBanner();
|
|
2873
|
-
console.error(` ${
|
|
4024
|
+
console.error(` ${pc7.dim("Starting Fennec MCP server...")}
|
|
2874
4025
|
`);
|
|
2875
4026
|
const configIndex = args2.indexOf("--config");
|
|
2876
4027
|
const configPath = configIndex !== -1 ? args2[configIndex + 1] : void 0;
|
|
@@ -2885,22 +4036,22 @@ async function startServer(args2) {
|
|
|
2885
4036
|
if (useSse) {
|
|
2886
4037
|
const port = server.getConfig().transport.port;
|
|
2887
4038
|
console.error(`
|
|
2888
|
-
${
|
|
2889
|
-
console.error(` ${renderKV("Transport",
|
|
2890
|
-
console.error(` ${renderKV("URL",
|
|
2891
|
-
console.error(` ${renderKV("MCP Config",
|
|
2892
|
-
console.error(` ${renderKV("OpenCode",
|
|
4039
|
+
${pc7.green("\u2713")} ${pc7.bold("Fennec server is running")}`);
|
|
4040
|
+
console.error(` ${renderKV("Transport", pc7.cyan("SSE (HTTP)"))}`);
|
|
4041
|
+
console.error(` ${renderKV("URL", pc7.cyan(`http://localhost:${port}/sse`))}`);
|
|
4042
|
+
console.error(` ${renderKV("MCP Config", pc7.cyan(`{ "type": "remote", "url": "http://localhost:${port}/sse" }`))}`);
|
|
4043
|
+
console.error(` ${renderKV("OpenCode", pc7.cyan(`type: remote, url: http://localhost:${port}/sse`))}`);
|
|
2893
4044
|
console.error(`
|
|
2894
|
-
${
|
|
4045
|
+
${pc7.dim("Press Ctrl+C to stop")}
|
|
2895
4046
|
`);
|
|
2896
4047
|
} else {
|
|
2897
4048
|
console.error(`
|
|
2898
|
-
${
|
|
4049
|
+
${pc7.green("\u2713")} ${pc7.bold("Fennec server is running")}`);
|
|
2899
4050
|
console.error(` ${renderKV("Transport", "stdio")}`);
|
|
2900
4051
|
console.error(` ${renderKV("AI Agent", "Connect via MCP protocol")}`);
|
|
2901
|
-
console.error(` ${renderKV("Tip",
|
|
4052
|
+
console.error(` ${renderKV("Tip", pc7.dim("For SSE mode: fennec start --sse"))}`);
|
|
2902
4053
|
console.error(`
|
|
2903
|
-
${
|
|
4054
|
+
${pc7.dim("Press Ctrl+C to stop")}
|
|
2904
4055
|
`);
|
|
2905
4056
|
}
|
|
2906
4057
|
} catch (error) {
|
|
@@ -2910,7 +4061,7 @@ async function startServer(args2) {
|
|
|
2910
4061
|
}
|
|
2911
4062
|
async function startCommand(args2) {
|
|
2912
4063
|
printBanner();
|
|
2913
|
-
console.error(` ${
|
|
4064
|
+
console.error(` ${pc7.dim("Starting app...")}
|
|
2914
4065
|
`);
|
|
2915
4066
|
await runCommand(args2);
|
|
2916
4067
|
}
|
|
@@ -2920,8 +4071,9 @@ async function runCommand(args2) {
|
|
|
2920
4071
|
const portIndex = args2.indexOf("--port");
|
|
2921
4072
|
const port = portIndex !== -1 ? parseInt(args2[portIndex + 1], 10) : void 0;
|
|
2922
4073
|
const cwdIndex = args2.indexOf("--cwd");
|
|
2923
|
-
const cwd = cwdIndex !== -1 ? args2[cwdIndex + 1] :
|
|
4074
|
+
const cwd = cwdIndex !== -1 ? resolve4(args2[cwdIndex + 1]) : process.cwd();
|
|
2924
4075
|
const restartFlag = args2.includes("--restart");
|
|
4076
|
+
const jsonlFlag = args2.includes("--jsonl");
|
|
2925
4077
|
const stopFlags = [nameIndex, portIndex, cwdIndex].filter((i) => i !== -1);
|
|
2926
4078
|
const cmdEnd = Math.min(...stopFlags, Infinity);
|
|
2927
4079
|
const cmdParts = args2.slice(0, cmdEnd);
|
|
@@ -2931,87 +4083,82 @@ async function runCommand(args2) {
|
|
|
2931
4083
|
process.exit(1);
|
|
2932
4084
|
}
|
|
2933
4085
|
const appName = name ?? cmdParts[0] ?? "app";
|
|
4086
|
+
if (port !== void 0) {
|
|
4087
|
+
const adopted = adoptExternalOnPort(port, appName);
|
|
4088
|
+
if (adopted) {
|
|
4089
|
+
console.error(`
|
|
4090
|
+
${pc7.green("\u2713")} ${pc7.bold("Adopted")} ${pc7.bold(adopted.name)} ${pc7.dim(`(PID ${adopted.pid}) \u2014 already listening on :${port}`)}`);
|
|
4091
|
+
console.error(` ${renderKV("Logs", pc7.cyan(`fennec log ${adopted.name}`))}`);
|
|
4092
|
+
console.error();
|
|
4093
|
+
return;
|
|
4094
|
+
}
|
|
4095
|
+
}
|
|
2934
4096
|
const tracked = readTracked();
|
|
2935
4097
|
const existing = tracked.find((t) => t.name === appName);
|
|
2936
|
-
if (existing &&
|
|
4098
|
+
if (existing && isTrackedRunning(existing)) {
|
|
2937
4099
|
console.error();
|
|
2938
|
-
console.error(` ${
|
|
2939
|
-
console.error(` ${renderKV("Logs",
|
|
2940
|
-
console.error(` ${renderKV("Stop",
|
|
4100
|
+
console.error(` ${pc7.yellow("\u26A0")} ${pc7.bold(appName)} ${pc7.dim(`is already running (PID: ${existing.pid})`)}`);
|
|
4101
|
+
console.error(` ${renderKV("Logs", pc7.cyan(`fennec log ${appName}`))}`);
|
|
4102
|
+
console.error(` ${renderKV("Stop", pc7.cyan(`fennec kill ${appName}`))}`);
|
|
2941
4103
|
console.error();
|
|
2942
4104
|
process.exit(0);
|
|
2943
4105
|
}
|
|
2944
|
-
const logDir =
|
|
2945
|
-
|
|
2946
|
-
const logFilePath =
|
|
4106
|
+
const logDir = dirname4(logFilePathFor(appName));
|
|
4107
|
+
mkdirSync4(logDir, { recursive: true });
|
|
4108
|
+
const logFilePath = logFilePathFor(appName);
|
|
2947
4109
|
console.error(`
|
|
2948
|
-
${symbols.fox} ${
|
|
4110
|
+
${symbols.fox} ${pc7.bold("Starting")} ${renderAppName(appName)} ${pc7.dim("(daemon)")}
|
|
2949
4111
|
`);
|
|
2950
4112
|
console.error(` ${renderKV("Command", cmd)}`);
|
|
2951
4113
|
if (port) console.error(` ${renderKV("Port", String(port))}`);
|
|
2952
4114
|
if (cwd) console.error(` ${renderKV("Directory", cwd)}`);
|
|
2953
|
-
if (restartFlag) console.error(` ${renderKV("Auto-restart",
|
|
4115
|
+
if (restartFlag) console.error(` ${renderKV("Auto-restart", pc7.green("enabled"))}`);
|
|
2954
4116
|
console.error(` ${divider()}`);
|
|
2955
4117
|
try {
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
});
|
|
4118
|
+
const startEnv = {};
|
|
4119
|
+
for (const [k, v] of Object.entries(process.env)) {
|
|
4120
|
+
if (v !== void 0) startEnv[k] = v;
|
|
4121
|
+
}
|
|
4122
|
+
const currentChild = spawnDaemon({ cmdParts, name: appName, cwd, logFilePath, env: buildSpawnEnv(startEnv), logMode: jsonlFlag ? "jsonl" : "text" });
|
|
2962
4123
|
const pid = currentChild.pid ?? 0;
|
|
2963
|
-
rotateLogFile(logFilePath);
|
|
2964
|
-
const logStream = createWriteStream(logFilePath, { flags: "a" });
|
|
2965
|
-
if (currentChild.stdout) currentChild.stdout.pipe(logStream);
|
|
2966
|
-
if (currentChild.stderr) currentChild.stderr.pipe(logStream);
|
|
2967
|
-
currentChild.unref();
|
|
2968
4124
|
addTracked({
|
|
2969
4125
|
name: appName,
|
|
2970
4126
|
pid,
|
|
2971
4127
|
command: cmd,
|
|
4128
|
+
args: cmdParts,
|
|
2972
4129
|
port,
|
|
2973
4130
|
cwd,
|
|
2974
|
-
|
|
4131
|
+
env: startEnv,
|
|
4132
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4133
|
+
autoRestart: restartFlag,
|
|
4134
|
+
logMode: jsonlFlag ? "jsonl" : "text"
|
|
2975
4135
|
});
|
|
2976
|
-
console.error(` ${
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
addTracked({ name: appName, pid: newPid, command: cmd, port, cwd, startedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
2995
|
-
console.error(` ${pc5.green("\u2713")} ${pc5.bold(appName)} ${pc5.dim(`restarted (PID: ${newPid})`)}`);
|
|
2996
|
-
currentChild = restarted;
|
|
2997
|
-
setupRestartWatch2(restarted);
|
|
2998
|
-
}
|
|
2999
|
-
});
|
|
3000
|
-
};
|
|
3001
|
-
var setupRestartWatch = setupRestartWatch2;
|
|
3002
|
-
console.error(` ${pc5.dim("Auto-restart enabled \u2014 watching for crashes...")}
|
|
3003
|
-
`);
|
|
3004
|
-
setupRestartWatch2(currentChild);
|
|
3005
|
-
await new Promise((resolve12) => {
|
|
3006
|
-
process.once("SIGINT", () => {
|
|
3007
|
-
currentChild.kill("SIGTERM");
|
|
3008
|
-
resolve12();
|
|
3009
|
-
});
|
|
3010
|
-
});
|
|
4136
|
+
console.error(` ${pc7.green("\u2713")} ${pc7.bold(appName)} ${pc7.dim(`started (PID: ${pid})`)}`);
|
|
4137
|
+
const spinner = createSpinner(port ? `Waiting for ${appName} on port ${port}...` : `Confirming ${appName} is running...`);
|
|
4138
|
+
const ready = await waitForReady(pid, port);
|
|
4139
|
+
spinner.stop();
|
|
4140
|
+
process.stdout.write("\r\x1B[K");
|
|
4141
|
+
if (!ready.running) {
|
|
4142
|
+
removeTracked(appName);
|
|
4143
|
+
console.error(` ${pc7.red("\u2717")} ${pc7.bold(appName)} ${pc7.dim("exited immediately after start")}`);
|
|
4144
|
+
console.error(` ${renderKV("Logs", pc7.cyan(`fennec log ${appName}`))}`);
|
|
4145
|
+
console.error();
|
|
4146
|
+
process.exit(1);
|
|
4147
|
+
}
|
|
4148
|
+
if (port) {
|
|
4149
|
+
if (ready.portReady) {
|
|
4150
|
+
console.error(` ${pc7.green("\u2713")} ${pc7.bold(appName)} ${pc7.dim(`is listening on port ${port}`)}`);
|
|
4151
|
+
} else {
|
|
4152
|
+
console.error(` ${pc7.yellow("\u26A0")} ${pc7.bold(appName)} ${pc7.dim(`is running but port ${port} is not accepting connections yet`)}`);
|
|
4153
|
+
}
|
|
3011
4154
|
}
|
|
3012
|
-
if (
|
|
3013
|
-
|
|
4155
|
+
if (restartFlag) {
|
|
4156
|
+
const supPid = ensureSupervisorRunning();
|
|
4157
|
+
ensurePersistEnabled();
|
|
4158
|
+
console.error(` ${pc7.green("\u2713")} ${pc7.bold("Auto-restart")} ${pc7.dim(`managed by supervisor (PID ${supPid}) \u2014 survives terminal close`)}`);
|
|
4159
|
+
console.error(` ${renderKV("Supervisor", pc7.cyan("fennec supervisor status"))}`);
|
|
3014
4160
|
}
|
|
4161
|
+
await psCommand([]);
|
|
3015
4162
|
} catch (error) {
|
|
3016
4163
|
console.error(renderError(`Failed to start ${appName}`, String(error)));
|
|
3017
4164
|
process.exit(1);
|
|
@@ -3020,7 +4167,7 @@ async function runCommand(args2) {
|
|
|
3020
4167
|
async function resurrectTracked() {
|
|
3021
4168
|
const tracked = readTracked();
|
|
3022
4169
|
if (tracked.length === 0) return;
|
|
3023
|
-
const dead = tracked.filter((t) => !
|
|
4170
|
+
const dead = tracked.filter((t) => !isTrackedRunning(t));
|
|
3024
4171
|
if (dead.length === 0) return;
|
|
3025
4172
|
const spinner = createSpinner(`Resurrecting ${dead.length} stopped process(es)...`);
|
|
3026
4173
|
let resurrected = 0;
|
|
@@ -3031,28 +4178,21 @@ async function resurrectTracked() {
|
|
|
3031
4178
|
continue;
|
|
3032
4179
|
}
|
|
3033
4180
|
try {
|
|
3034
|
-
const cmdParts = proc
|
|
3035
|
-
const
|
|
3036
|
-
|
|
3037
|
-
const logFilePath = resolve3(logDir, `${proc.name}.log`);
|
|
3038
|
-
const child = spawn(cmdParts[0], cmdParts.slice(1), {
|
|
3039
|
-
cwd: proc.cwd,
|
|
3040
|
-
env: { ...process.env },
|
|
3041
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
3042
|
-
detached: true
|
|
3043
|
-
});
|
|
3044
|
-
rotateLogFile(logFilePath);
|
|
3045
|
-
const logStream = createWriteStream(logFilePath, { flags: "a" });
|
|
3046
|
-
if (child.stdout) child.stdout.pipe(logStream);
|
|
3047
|
-
if (child.stderr) child.stderr.pipe(logStream);
|
|
3048
|
-
child.unref();
|
|
4181
|
+
const cmdParts = resolveArgs(proc);
|
|
4182
|
+
const logFilePath = logFilePathFor(proc.name);
|
|
4183
|
+
const child = spawnDaemon({ cmdParts, name: proc.name, cwd: proc.cwd, logFilePath, env: buildSpawnEnv(proc.env), logMode: proc.logMode });
|
|
3049
4184
|
addTracked({
|
|
3050
4185
|
name: proc.name,
|
|
3051
4186
|
pid: child.pid ?? 0,
|
|
3052
4187
|
command: proc.command,
|
|
4188
|
+
args: cmdParts,
|
|
3053
4189
|
port: proc.port,
|
|
3054
4190
|
cwd: proc.cwd,
|
|
3055
|
-
|
|
4191
|
+
env: proc.env,
|
|
4192
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4193
|
+
autoRestart: proc.autoRestart,
|
|
4194
|
+
restartCause: proc.restartCause,
|
|
4195
|
+
logMode: proc.logMode
|
|
3056
4196
|
});
|
|
3057
4197
|
resurrected++;
|
|
3058
4198
|
} catch {
|
|
@@ -3062,15 +4202,15 @@ async function resurrectTracked() {
|
|
|
3062
4202
|
spinner.stop();
|
|
3063
4203
|
process.stdout.write("\r\x1B[K");
|
|
3064
4204
|
if (resurrected > 0) {
|
|
3065
|
-
console.error(` ${
|
|
4205
|
+
console.error(` ${pc7.green("\u25CF")} ${pc7.bold(`Resurrected ${resurrected} process(es)`)}`);
|
|
3066
4206
|
}
|
|
3067
4207
|
if (failed > 0) {
|
|
3068
|
-
console.error(` ${
|
|
4208
|
+
console.error(` ${pc7.red("\u25CF")} ${pc7.bold(`${failed} process(es) failed to resurrect`)}`);
|
|
3069
4209
|
}
|
|
3070
4210
|
}
|
|
3071
4211
|
|
|
3072
4212
|
// src/commands/kill.ts
|
|
3073
|
-
import
|
|
4213
|
+
import pc8 from "picocolors";
|
|
3074
4214
|
async function killCommand(args2) {
|
|
3075
4215
|
const rawTarget = args2[0];
|
|
3076
4216
|
const signalIndex = args2.indexOf("--signal");
|
|
@@ -3097,21 +4237,29 @@ async function killCommand(args2) {
|
|
|
3097
4237
|
} else {
|
|
3098
4238
|
const tracked = readTracked();
|
|
3099
4239
|
const trackedMatch = tracked.find((t) => t.name === rawTarget);
|
|
3100
|
-
if (trackedMatch
|
|
4240
|
+
if (trackedMatch) {
|
|
4241
|
+
if (!isTrackedRunning(trackedMatch)) {
|
|
4242
|
+
console.error(`
|
|
4243
|
+
${pc8.yellow("\u26A0")} ${pc8.bold(rawTarget)} ${pc8.dim("is already stopped (no running process to kill)")}`);
|
|
4244
|
+
console.error(` ${pc8.dim("Use")} ${pc8.cyan("fennec spawn")} ${pc8.dim("to resume")}`);
|
|
4245
|
+
process.exit(0);
|
|
4246
|
+
}
|
|
3101
4247
|
targetPid = trackedMatch.pid;
|
|
3102
4248
|
displayName = `${trackedMatch.name} (PID ${targetPid})`;
|
|
3103
4249
|
} else {
|
|
3104
|
-
const
|
|
4250
|
+
const currentPid = process.pid;
|
|
4251
|
+
let matches = getSystemProcesses({ name: rawTarget, userOnly: true, sortBy: "cpu" });
|
|
4252
|
+
matches = matches.filter((m) => m.pid !== currentPid);
|
|
3105
4253
|
if (matches.length === 0) {
|
|
3106
4254
|
console.error(renderError("Process not found", `No running process matching "${rawTarget}"`));
|
|
3107
4255
|
process.exit(1);
|
|
3108
4256
|
}
|
|
3109
4257
|
if (matches.length > 1) {
|
|
3110
4258
|
console.error(`
|
|
3111
|
-
${
|
|
4259
|
+
${pc8.bold(`Multiple processes match "${rawTarget}":`)}`);
|
|
3112
4260
|
const selected = await selectPrompt("Select which to kill:", matches.map((p, i) => ({ value: String(i), label: `${p.name} (PID ${p.pid})`, description: `${p.command.slice(0, 80)} \u2014 CPU: ${p.cpuPercent}% MEM: ${p.memPercent}%` })));
|
|
3113
4261
|
if (selected === null) {
|
|
3114
|
-
console.error(` ${
|
|
4262
|
+
console.error(` ${pc8.dim("Cancelled")}`);
|
|
3115
4263
|
return;
|
|
3116
4264
|
}
|
|
3117
4265
|
const idx = parseInt(selected, 10);
|
|
@@ -3123,9 +4271,10 @@ async function killCommand(args2) {
|
|
|
3123
4271
|
}
|
|
3124
4272
|
}
|
|
3125
4273
|
}
|
|
3126
|
-
const
|
|
4274
|
+
const force = args2.includes("-y") || args2.includes("--yes");
|
|
4275
|
+
const confirmed = force || await confirmPrompt(`Kill ${pc8.bold(displayName)} with ${pc8.yellow(signal)}?`, false);
|
|
3127
4276
|
if (!confirmed) {
|
|
3128
|
-
console.error(` ${
|
|
4277
|
+
console.error(` ${pc8.dim("Cancelled")}`);
|
|
3129
4278
|
return;
|
|
3130
4279
|
}
|
|
3131
4280
|
const spinner = createSpinner(`Sending ${signal} to ${displayName}...`);
|
|
@@ -3135,16 +4284,16 @@ async function killCommand(args2) {
|
|
|
3135
4284
|
const stillRunning = isProcessRunning(targetPid);
|
|
3136
4285
|
if (stillRunning && signal !== "SIGKILL") {
|
|
3137
4286
|
spinner.warn(`${displayName} did not respond to ${signal}`);
|
|
3138
|
-
const forceKill = await confirmPrompt(`Send ${
|
|
4287
|
+
const forceKill = force || await confirmPrompt(`Send ${pc8.red("SIGKILL")} to force stop?`, true);
|
|
3139
4288
|
if (forceKill) {
|
|
3140
4289
|
const forceSpinner = createSpinner(`Sending SIGKILL to ${displayName}...`);
|
|
3141
4290
|
killProcess(targetPid, "SIGKILL");
|
|
3142
4291
|
await new Promise((r) => setTimeout(r, 300));
|
|
3143
4292
|
isProcessRunning(targetPid) ? forceSpinner.fail(`${displayName} could not be stopped (permission denied?)`) : forceSpinner.succeed(`${displayName} force stopped`);
|
|
3144
4293
|
} else {
|
|
3145
|
-
console.error(` ${
|
|
4294
|
+
console.error(` ${pc8.dim("Retrying with SIGTERM...")}`);
|
|
3146
4295
|
killProcess(targetPid, "SIGTERM");
|
|
3147
|
-
console.error(` ${
|
|
4296
|
+
console.error(` ${pc8.yellow("\u26A0")} ${displayName} ${pc8.dim("may still be running")}`);
|
|
3148
4297
|
}
|
|
3149
4298
|
} else {
|
|
3150
4299
|
removeTrackedByPid(targetPid);
|
|
@@ -3158,17 +4307,17 @@ async function killCommand(args2) {
|
|
|
3158
4307
|
async function killAll(signal) {
|
|
3159
4308
|
const userProcs = getSystemProcesses({ userOnly: true, sortBy: "cpu", limit: 200 });
|
|
3160
4309
|
if (userProcs.length === 0) {
|
|
3161
|
-
console.error(` ${
|
|
4310
|
+
console.error(` ${pc8.dim("No user processes to kill.")}`);
|
|
3162
4311
|
return;
|
|
3163
4312
|
}
|
|
3164
4313
|
console.error(`
|
|
3165
|
-
${
|
|
3166
|
-
console.error(` ${
|
|
3167
|
-
console.error(` ${
|
|
4314
|
+
${pc8.bold(`Kill ${userProcs.length} user processes?`)}`);
|
|
4315
|
+
console.error(` ${pc8.dim("This will stop ALL your running processes.")}`);
|
|
4316
|
+
console.error(` ${pc8.yellow("\u26A0 System processes will not be affected.")}
|
|
3168
4317
|
`);
|
|
3169
|
-
const confirmed = await confirmPrompt(`${
|
|
4318
|
+
const confirmed = await confirmPrompt(`${pc8.red("Are you sure?")} ${pc8.dim("This cannot be undone")}`, false);
|
|
3170
4319
|
if (!confirmed) {
|
|
3171
|
-
console.error(` ${
|
|
4320
|
+
console.error(` ${pc8.dim("Cancelled.")}`);
|
|
3172
4321
|
return;
|
|
3173
4322
|
}
|
|
3174
4323
|
const spinner = createSpinner(`Killing ${userProcs.length} processes...`);
|
|
@@ -3184,15 +4333,15 @@ async function killAll(signal) {
|
|
|
3184
4333
|
}
|
|
3185
4334
|
await new Promise((r) => setTimeout(r, 500));
|
|
3186
4335
|
killed > 0 ? spinner.succeed(`${killed} process(es) killed`) : spinner.fail("Failed to kill processes");
|
|
3187
|
-
if (failed > 0) console.error(` ${
|
|
4336
|
+
if (failed > 0) console.error(` ${pc8.yellow(`${failed} process(es) could not be killed`)} ${pc8.dim("(try with sudo)")}`);
|
|
3188
4337
|
}
|
|
3189
4338
|
|
|
3190
4339
|
// src/commands/stop.ts
|
|
3191
|
-
import
|
|
4340
|
+
import pc9 from "picocolors";
|
|
3192
4341
|
async function stopCommand(args2) {
|
|
3193
4342
|
const stopAll = args2.includes("--all") || args2.includes("-a");
|
|
3194
4343
|
if (stopAll) {
|
|
3195
|
-
await stopAllTracked();
|
|
4344
|
+
await stopAllTracked(args2);
|
|
3196
4345
|
return;
|
|
3197
4346
|
}
|
|
3198
4347
|
const rawTarget = args2[0];
|
|
@@ -3203,42 +4352,45 @@ async function stopCommand(args2) {
|
|
|
3203
4352
|
const tracked = readTracked();
|
|
3204
4353
|
const trackedMatch = tracked.find((t) => t.name === rawTarget);
|
|
3205
4354
|
if (!trackedMatch) {
|
|
3206
|
-
console.error(renderError("Not found", `No tracked process named "${rawTarget}". Use ${
|
|
4355
|
+
console.error(renderError("Not found", `No tracked process named "${rawTarget}". Use ${pc9.cyan("fennec kill <pid>")} to kill a system process.`));
|
|
3207
4356
|
process.exit(1);
|
|
3208
4357
|
}
|
|
3209
|
-
if (!
|
|
4358
|
+
if (!isTrackedRunning(trackedMatch)) {
|
|
3210
4359
|
console.error(`
|
|
3211
|
-
${
|
|
4360
|
+
${pc9.yellow("\u26A0")} ${pc9.bold(rawTarget)} ${pc9.dim("is already stopped")}`);
|
|
3212
4361
|
console.error();
|
|
3213
4362
|
process.exit(0);
|
|
3214
4363
|
}
|
|
3215
4364
|
const displayName = `${trackedMatch.name} (PID ${trackedMatch.pid})`;
|
|
3216
|
-
const
|
|
4365
|
+
const force = args2.includes("-y") || args2.includes("--yes");
|
|
4366
|
+
const confirmed = force || await confirmPrompt(`Stop ${pc9.bold(displayName)}? ${pc9.dim("It can be re-spawned later via fennec spawn")}`, false);
|
|
3217
4367
|
if (!confirmed) {
|
|
3218
|
-
console.error(` ${
|
|
4368
|
+
console.error(` ${pc9.dim("Cancelled")}`);
|
|
3219
4369
|
return;
|
|
3220
4370
|
}
|
|
4371
|
+
setAutoRestart(trackedMatch.name, false);
|
|
3221
4372
|
await stopSingleProcess(trackedMatch.pid, trackedMatch.name);
|
|
3222
4373
|
}
|
|
3223
|
-
async function stopAllTracked() {
|
|
4374
|
+
async function stopAllTracked(args2) {
|
|
3224
4375
|
const tracked = readTracked();
|
|
3225
|
-
const running = tracked.filter((t) =>
|
|
4376
|
+
const running = tracked.filter((t) => isTrackedRunning(t));
|
|
3226
4377
|
if (running.length === 0) {
|
|
3227
4378
|
console.error(`
|
|
3228
|
-
${
|
|
4379
|
+
${pc9.dim("No running tracked processes to stop.")}
|
|
3229
4380
|
`);
|
|
3230
4381
|
return;
|
|
3231
4382
|
}
|
|
3232
4383
|
console.error(`
|
|
3233
|
-
${
|
|
4384
|
+
${pc9.yellow("\u26A0")} ${pc9.bold(`Stop all ${running.length} running process(es)?`)} ${pc9.dim("(They can be re-spawned later)")}
|
|
3234
4385
|
`);
|
|
3235
4386
|
for (const t of running) {
|
|
3236
|
-
console.error(` ${
|
|
4387
|
+
console.error(` ${pc9.green("\u25CF")} ${pc9.bold(t.name)} ${pc9.dim(`(PID ${t.pid})`)}`);
|
|
3237
4388
|
}
|
|
3238
4389
|
console.error();
|
|
3239
|
-
const
|
|
4390
|
+
const forceAll = args2.includes("-y") || args2.includes("--yes");
|
|
4391
|
+
const confirmed = forceAll || await confirmPrompt("Stop all?", false);
|
|
3240
4392
|
if (!confirmed) {
|
|
3241
|
-
console.error(` ${
|
|
4393
|
+
console.error(` ${pc9.dim("Cancelled")}
|
|
3242
4394
|
`);
|
|
3243
4395
|
return;
|
|
3244
4396
|
}
|
|
@@ -3246,6 +4398,7 @@ async function stopAllTracked() {
|
|
|
3246
4398
|
let stopped = 0;
|
|
3247
4399
|
let failed = 0;
|
|
3248
4400
|
for (const t of running) {
|
|
4401
|
+
setAutoRestart(t.name, false);
|
|
3249
4402
|
if (killProcess(t.pid, "SIGTERM")) {
|
|
3250
4403
|
stopped++;
|
|
3251
4404
|
} else {
|
|
@@ -3256,10 +4409,10 @@ async function stopAllTracked() {
|
|
|
3256
4409
|
spinner.stop();
|
|
3257
4410
|
process.stdout.write("\r\x1B[K");
|
|
3258
4411
|
if (stopped > 0) {
|
|
3259
|
-
console.error(` ${
|
|
4412
|
+
console.error(` ${pc9.green("\u2713")} ${pc9.bold(`Stopped ${stopped} process(es)`)} ${pc9.dim("(kept in tracked.json)")}`);
|
|
3260
4413
|
}
|
|
3261
4414
|
if (failed > 0) {
|
|
3262
|
-
console.error(` ${
|
|
4415
|
+
console.error(` ${pc9.red("\u2717")} ${pc9.bold(`${failed} process(es) failed to stop`)}`);
|
|
3263
4416
|
}
|
|
3264
4417
|
console.error();
|
|
3265
4418
|
await psCommand([]);
|
|
@@ -3273,14 +4426,14 @@ async function stopSingleProcess(pid, name) {
|
|
|
3273
4426
|
const stillRunning = isProcessRunning(pid);
|
|
3274
4427
|
if (stillRunning) {
|
|
3275
4428
|
spinner.warn(`${displayName} did not respond to SIGTERM`);
|
|
3276
|
-
const forceKill = await confirmPrompt(`Send ${
|
|
4429
|
+
const forceKill = await confirmPrompt(`Send ${pc9.red("SIGKILL")} to force stop?`, true);
|
|
3277
4430
|
if (forceKill) {
|
|
3278
4431
|
const forceSpinner = createSpinner(`Sending SIGKILL to ${displayName}...`);
|
|
3279
4432
|
killProcess(pid, "SIGKILL");
|
|
3280
4433
|
await new Promise((r) => setTimeout(r, 300));
|
|
3281
4434
|
isProcessRunning(pid) ? forceSpinner.fail(`${displayName} could not be stopped (permission denied?)`) : forceSpinner.succeed(`${displayName} force stopped`);
|
|
3282
4435
|
} else {
|
|
3283
|
-
console.error(` ${
|
|
4436
|
+
console.error(` ${pc9.yellow("\u26A0")} ${displayName} ${pc9.dim("may still be running")}`);
|
|
3284
4437
|
}
|
|
3285
4438
|
} else {
|
|
3286
4439
|
spinner.succeed(`${displayName} stopped`);
|
|
@@ -3294,11 +4447,10 @@ async function stopSingleProcess(pid, name) {
|
|
|
3294
4447
|
}
|
|
3295
4448
|
|
|
3296
4449
|
// src/commands/spawn.ts
|
|
3297
|
-
import
|
|
3298
|
-
import {
|
|
3299
|
-
import { resolve as
|
|
4450
|
+
import pc10 from "picocolors";
|
|
4451
|
+
import { mkdirSync as mkdirSync5 } from "fs";
|
|
4452
|
+
import { resolve as resolve5 } from "path";
|
|
3300
4453
|
import { homedir as homedir3 } from "os";
|
|
3301
|
-
import { spawn as spawn2 } from "child_process";
|
|
3302
4454
|
async function spawnCommand(args2) {
|
|
3303
4455
|
const spawnAll = args2.includes("--all") || args2.includes("-a");
|
|
3304
4456
|
const name = args2[0];
|
|
@@ -3313,13 +4465,13 @@ async function spawnCommand(args2) {
|
|
|
3313
4465
|
const tracked = readTracked();
|
|
3314
4466
|
const match = tracked.find((t) => t.name === name);
|
|
3315
4467
|
if (!match) {
|
|
3316
|
-
console.error(renderError("Not found", `No tracked process named "${name}". Use ${
|
|
4468
|
+
console.error(renderError("Not found", `No tracked process named "${name}". Use ${pc10.cyan("fennec spawn")} to see available processes, or ${pc10.cyan("fennec start <command> --name <name>")} to create a new one.`));
|
|
3317
4469
|
process.exit(1);
|
|
3318
4470
|
}
|
|
3319
|
-
if (
|
|
4471
|
+
if (isTrackedRunning(match)) {
|
|
3320
4472
|
console.error(`
|
|
3321
|
-
${
|
|
3322
|
-
console.error(` ${
|
|
4473
|
+
${pc10.yellow("\u26A0")} ${pc10.bold(name)} ${pc10.dim("is already running (PID:")} ${match.pid}${pc10.dim(")")}`);
|
|
4474
|
+
console.error(` ${pc10.dim("Use")} ${pc10.cyan(`fennec stop ${name}`)} ${pc10.dim("to stop it first.")}`);
|
|
3323
4475
|
console.error();
|
|
3324
4476
|
process.exit(0);
|
|
3325
4477
|
}
|
|
@@ -3331,18 +4483,18 @@ async function spawnCommand(args2) {
|
|
|
3331
4483
|
}
|
|
3332
4484
|
async function spawnAllStopped() {
|
|
3333
4485
|
const tracked = readTracked();
|
|
3334
|
-
const toSpawn = tracked.filter((t) => !
|
|
4486
|
+
const toSpawn = tracked.filter((t) => !isTrackedRunning(t) && t.command);
|
|
3335
4487
|
if (toSpawn.length === 0) {
|
|
3336
4488
|
console.error(`
|
|
3337
|
-
${
|
|
4489
|
+
${pc10.dim("No stopped processes with saved commands to spawn.")}
|
|
3338
4490
|
`);
|
|
3339
4491
|
return;
|
|
3340
4492
|
}
|
|
3341
4493
|
console.error(`
|
|
3342
|
-
${symbols.fox} ${
|
|
4494
|
+
${symbols.fox} ${pc10.bold(`Spawning ${toSpawn.length} process(es)...`)}
|
|
3343
4495
|
`);
|
|
3344
4496
|
for (const t of toSpawn) {
|
|
3345
|
-
console.error(` ${
|
|
4497
|
+
console.error(` ${pc10.dim("\xB7")} ${pc10.bold(t.name)} ${pc10.dim(`(${t.command})`)}`);
|
|
3346
4498
|
}
|
|
3347
4499
|
console.error();
|
|
3348
4500
|
const spinner = createSpinner("Spawning...");
|
|
@@ -3350,30 +4502,28 @@ async function spawnAllStopped() {
|
|
|
3350
4502
|
let failed = 0;
|
|
3351
4503
|
for (const proc of toSpawn) {
|
|
3352
4504
|
try {
|
|
3353
|
-
const cmdParts = proc
|
|
3354
|
-
const logDir =
|
|
3355
|
-
|
|
3356
|
-
const logFilePath =
|
|
3357
|
-
const child =
|
|
3358
|
-
cwd: proc.cwd,
|
|
3359
|
-
env: { ...process.env },
|
|
3360
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
3361
|
-
detached: true
|
|
3362
|
-
});
|
|
4505
|
+
const cmdParts = resolveArgs(proc);
|
|
4506
|
+
const logDir = resolve5(homedir3(), ".fennec", "logs");
|
|
4507
|
+
mkdirSync5(logDir, { recursive: true });
|
|
4508
|
+
const logFilePath = resolve5(logDir, `${proc.name}.log`);
|
|
4509
|
+
const child = spawnDaemon({ cmdParts, name: proc.name, cwd: proc.cwd, logFilePath, env: buildSpawnEnv(proc.env), logMode: proc.logMode });
|
|
3363
4510
|
const pid = child.pid ?? 0;
|
|
3364
|
-
rotateLogFile(logFilePath);
|
|
3365
|
-
const logStream = createWriteStream2(logFilePath, { flags: "a" });
|
|
3366
|
-
if (child.stdout) child.stdout.pipe(logStream);
|
|
3367
|
-
if (child.stderr) child.stderr.pipe(logStream);
|
|
3368
|
-
child.unref();
|
|
3369
4511
|
addTracked({
|
|
3370
4512
|
name: proc.name,
|
|
3371
4513
|
pid,
|
|
3372
4514
|
command: proc.command,
|
|
4515
|
+
args: cmdParts,
|
|
3373
4516
|
port: proc.port,
|
|
3374
4517
|
cwd: proc.cwd,
|
|
3375
|
-
|
|
4518
|
+
env: proc.env,
|
|
4519
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4520
|
+
autoRestart: proc.autoRestart,
|
|
4521
|
+
logMode: proc.logMode
|
|
3376
4522
|
});
|
|
4523
|
+
if (proc.autoRestart) {
|
|
4524
|
+
ensureSupervisorRunning();
|
|
4525
|
+
ensurePersistEnabled();
|
|
4526
|
+
}
|
|
3377
4527
|
spawned++;
|
|
3378
4528
|
} catch {
|
|
3379
4529
|
failed++;
|
|
@@ -3382,10 +4532,10 @@ async function spawnAllStopped() {
|
|
|
3382
4532
|
spinner.stop();
|
|
3383
4533
|
process.stdout.write("\r\x1B[K");
|
|
3384
4534
|
if (spawned > 0) {
|
|
3385
|
-
console.error(` ${
|
|
4535
|
+
console.error(` ${pc10.green("\u2713")} ${pc10.bold(`Spawned ${spawned} process(es)`)}`);
|
|
3386
4536
|
}
|
|
3387
4537
|
if (failed > 0) {
|
|
3388
|
-
console.error(` ${
|
|
4538
|
+
console.error(` ${pc10.red("\u2717")} ${pc10.bold(`${failed} process(es) failed to spawn`)}`);
|
|
3389
4539
|
}
|
|
3390
4540
|
console.error();
|
|
3391
4541
|
await psCommand([]);
|
|
@@ -3394,25 +4544,25 @@ async function spawnList() {
|
|
|
3394
4544
|
const tracked = readTracked();
|
|
3395
4545
|
if (tracked.length === 0) {
|
|
3396
4546
|
console.error(`
|
|
3397
|
-
${
|
|
3398
|
-
console.error(` ${
|
|
4547
|
+
${pc10.dim("No tracked processes found.")}`);
|
|
4548
|
+
console.error(` ${pc10.dim("Start an app first:")} ${pc10.cyan("fennec start <command> --name <name>")}`);
|
|
3399
4549
|
console.error();
|
|
3400
4550
|
return;
|
|
3401
4551
|
}
|
|
3402
|
-
const stopped = tracked.filter((t) => !
|
|
3403
|
-
const running = tracked.filter((t) =>
|
|
4552
|
+
const stopped = tracked.filter((t) => !isTrackedRunning(t));
|
|
4553
|
+
const running = tracked.filter((t) => isTrackedRunning(t));
|
|
3404
4554
|
if (stopped.length === 0) {
|
|
3405
4555
|
console.error(`
|
|
3406
|
-
${
|
|
4556
|
+
${pc10.dim("All tracked processes are running. Nothing to spawn.")}`);
|
|
3407
4557
|
console.error();
|
|
3408
4558
|
return;
|
|
3409
4559
|
}
|
|
3410
4560
|
console.error(`
|
|
3411
|
-
${symbols.fox} ${
|
|
4561
|
+
${symbols.fox} ${pc10.bold("Available to spawn")} ${pc10.dim(`(${stopped.length}/${tracked.length} stopped)`)}`);
|
|
3412
4562
|
if (running.length > 0) {
|
|
3413
|
-
console.error(` ${
|
|
4563
|
+
console.error(` ${pc10.dim("Running (use stop first):")}`);
|
|
3414
4564
|
for (const r of running) {
|
|
3415
|
-
console.error(` ${
|
|
4565
|
+
console.error(` ${pc10.green("\u25CF")} ${pc10.bold(r.name)} ${pc10.dim(`(PID ${r.pid})`)}`);
|
|
3416
4566
|
}
|
|
3417
4567
|
console.error();
|
|
3418
4568
|
}
|
|
@@ -3422,7 +4572,7 @@ async function spawnList() {
|
|
|
3422
4572
|
description: t.command.length > 80 ? t.command.slice(0, 80) + "..." : t.command
|
|
3423
4573
|
})));
|
|
3424
4574
|
if (selected === null) {
|
|
3425
|
-
console.error(` ${
|
|
4575
|
+
console.error(` ${pc10.dim("Cancelled")}`);
|
|
3426
4576
|
return;
|
|
3427
4577
|
}
|
|
3428
4578
|
const match = stopped.find((t) => t.name === selected);
|
|
@@ -3431,38 +4581,36 @@ async function spawnList() {
|
|
|
3431
4581
|
}
|
|
3432
4582
|
}
|
|
3433
4583
|
async function respawnProcess(proc) {
|
|
3434
|
-
const cmdParts = proc
|
|
3435
|
-
const logDir =
|
|
3436
|
-
|
|
3437
|
-
const logFilePath =
|
|
4584
|
+
const cmdParts = resolveArgs(proc);
|
|
4585
|
+
const logDir = resolve5(homedir3(), ".fennec", "logs");
|
|
4586
|
+
mkdirSync5(logDir, { recursive: true });
|
|
4587
|
+
const logFilePath = resolve5(logDir, `${proc.name}.log`);
|
|
3438
4588
|
console.error(`
|
|
3439
|
-
${symbols.fox} ${
|
|
4589
|
+
${symbols.fox} ${pc10.bold("Spawning")} ${renderAppName(proc.name)} ${pc10.dim("(from saved config)")}
|
|
3440
4590
|
`);
|
|
3441
4591
|
console.error(` ${renderKV("Command", proc.command)}`);
|
|
3442
4592
|
if (proc.cwd) console.error(` ${renderKV("Directory", proc.cwd)}`);
|
|
3443
4593
|
if (proc.port) console.error(` ${renderKV("Port", String(proc.port))}`);
|
|
3444
4594
|
try {
|
|
3445
|
-
const child =
|
|
3446
|
-
cwd: proc.cwd,
|
|
3447
|
-
env: { ...process.env },
|
|
3448
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
3449
|
-
detached: true
|
|
3450
|
-
});
|
|
4595
|
+
const child = spawnDaemon({ cmdParts, name: proc.name, cwd: proc.cwd, logFilePath, env: buildSpawnEnv(proc.env), logMode: proc.logMode });
|
|
3451
4596
|
const pid = child.pid ?? 0;
|
|
3452
|
-
rotateLogFile(logFilePath);
|
|
3453
|
-
const logStream = createWriteStream2(logFilePath, { flags: "a" });
|
|
3454
|
-
if (child.stdout) child.stdout.pipe(logStream);
|
|
3455
|
-
if (child.stderr) child.stderr.pipe(logStream);
|
|
3456
|
-
child.unref();
|
|
3457
4597
|
addTracked({
|
|
3458
4598
|
name: proc.name,
|
|
3459
4599
|
pid,
|
|
3460
4600
|
command: proc.command,
|
|
4601
|
+
args: cmdParts,
|
|
3461
4602
|
port: proc.port,
|
|
3462
4603
|
cwd: proc.cwd,
|
|
3463
|
-
|
|
4604
|
+
env: proc.env,
|
|
4605
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4606
|
+
autoRestart: proc.autoRestart,
|
|
4607
|
+
logMode: proc.logMode
|
|
3464
4608
|
});
|
|
3465
|
-
|
|
4609
|
+
if (proc.autoRestart) {
|
|
4610
|
+
ensureSupervisorRunning();
|
|
4611
|
+
ensurePersistEnabled();
|
|
4612
|
+
}
|
|
4613
|
+
console.error(` ${pc10.green("\u2713")} ${pc10.bold(proc.name)} ${pc10.dim(`spawned (PID: ${pid})`)}`);
|
|
3466
4614
|
await psCommand([]);
|
|
3467
4615
|
} catch (error) {
|
|
3468
4616
|
console.error(renderError(`Failed to spawn ${proc.name}`, String(error)));
|
|
@@ -3471,11 +4619,10 @@ async function respawnProcess(proc) {
|
|
|
3471
4619
|
}
|
|
3472
4620
|
|
|
3473
4621
|
// src/commands/restart.ts
|
|
3474
|
-
import
|
|
3475
|
-
import { mkdirSync as
|
|
3476
|
-
import { resolve as
|
|
4622
|
+
import pc11 from "picocolors";
|
|
4623
|
+
import { mkdirSync as mkdirSync6 } from "fs";
|
|
4624
|
+
import { resolve as resolve6 } from "path";
|
|
3477
4625
|
import { homedir as homedir4 } from "os";
|
|
3478
|
-
import { spawn as spawn3 } from "child_process";
|
|
3479
4626
|
async function restartCommand(args2) {
|
|
3480
4627
|
const name = args2[0];
|
|
3481
4628
|
if (!name) {
|
|
@@ -3500,9 +4647,9 @@ async function restartCommand(args2) {
|
|
|
3500
4647
|
targetPid = processes[0].pid;
|
|
3501
4648
|
}
|
|
3502
4649
|
}
|
|
3503
|
-
const confirmed = await confirmPrompt(`Restart ${
|
|
4650
|
+
const confirmed = await confirmPrompt(`Restart ${pc11.bold(`${name} (PID ${targetPid})`)}?`, false);
|
|
3504
4651
|
if (!confirmed) {
|
|
3505
|
-
console.error(` ${
|
|
4652
|
+
console.error(` ${pc11.dim("Cancelled")}`);
|
|
3506
4653
|
return;
|
|
3507
4654
|
}
|
|
3508
4655
|
const spinner = createSpinner(`Stopping PID ${targetPid}...`);
|
|
@@ -3521,86 +4668,581 @@ async function restartCommand(args2) {
|
|
|
3521
4668
|
if (trackedEntry) {
|
|
3522
4669
|
const respawnSpinner = createSpinner(`Re-spawning ${trackedEntry.name}...`);
|
|
3523
4670
|
try {
|
|
3524
|
-
const cmdParts = trackedEntry
|
|
3525
|
-
const logDir =
|
|
3526
|
-
|
|
3527
|
-
const logFilePath =
|
|
3528
|
-
const child =
|
|
3529
|
-
cwd: trackedEntry.cwd,
|
|
3530
|
-
env: { ...process.env },
|
|
3531
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
3532
|
-
detached: true
|
|
3533
|
-
});
|
|
3534
|
-
rotateLogFile(logFilePath);
|
|
3535
|
-
const logStream = createWriteStream3(logFilePath, { flags: "a" });
|
|
3536
|
-
if (child.stdout) child.stdout.pipe(logStream);
|
|
3537
|
-
if (child.stderr) child.stderr.pipe(logStream);
|
|
3538
|
-
child.unref();
|
|
4671
|
+
const cmdParts = resolveArgs(trackedEntry);
|
|
4672
|
+
const logDir = resolve6(homedir4(), ".fennec", "logs");
|
|
4673
|
+
mkdirSync6(logDir, { recursive: true });
|
|
4674
|
+
const logFilePath = resolve6(logDir, `${trackedEntry.name}.log`);
|
|
4675
|
+
const child = spawnDaemon({ cmdParts, name: trackedEntry.name, cwd: trackedEntry.cwd, logFilePath, env: buildSpawnEnv(trackedEntry.env), logMode: trackedEntry.logMode });
|
|
3539
4676
|
removeTrackedByPid(targetPid);
|
|
3540
4677
|
addTracked({
|
|
3541
4678
|
name: trackedEntry.name,
|
|
3542
4679
|
pid: child.pid ?? 0,
|
|
3543
4680
|
command: trackedEntry.command,
|
|
4681
|
+
args: cmdParts,
|
|
3544
4682
|
port: trackedEntry.port,
|
|
3545
4683
|
cwd: trackedEntry.cwd,
|
|
3546
|
-
|
|
4684
|
+
env: trackedEntry.env,
|
|
4685
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4686
|
+
autoRestart: trackedEntry.autoRestart,
|
|
4687
|
+
logMode: trackedEntry.logMode
|
|
3547
4688
|
});
|
|
3548
4689
|
respawnSpinner.succeed(`${trackedEntry.name} restarted (PID: ${child.pid})`);
|
|
3549
4690
|
} catch (error) {
|
|
3550
4691
|
respawnSpinner.fail(`Failed to re-spawn ${trackedEntry.name}: ${String(error)}`);
|
|
3551
|
-
console.error(` ${
|
|
4692
|
+
console.error(` ${pc11.dim("Config preserved. Re-run manually:")} ${renderCommand(trackedEntry.command)}`);
|
|
4693
|
+
}
|
|
4694
|
+
} else {
|
|
4695
|
+
console.error(` ${pc11.dim("No tracked config found. Re-run manually:")} ${renderCommand(name)}`);
|
|
4696
|
+
}
|
|
4697
|
+
}
|
|
4698
|
+
|
|
4699
|
+
// src/commands/adopt.ts
|
|
4700
|
+
import pc12 from "picocolors";
|
|
4701
|
+
function adoptCommand(args2) {
|
|
4702
|
+
const pidArg = args2.find((a) => /^\d+$/.test(a));
|
|
4703
|
+
if (!pidArg) {
|
|
4704
|
+
console.error(renderError("Missing PID", `Usage: fennec adopt <pid> [--name <name>] [--port <port>]`));
|
|
4705
|
+
process.exit(1);
|
|
4706
|
+
}
|
|
4707
|
+
const pid = parseInt(pidArg, 10);
|
|
4708
|
+
const nameIdx = args2.indexOf("--name");
|
|
4709
|
+
const name = nameIdx !== -1 ? args2[nameIdx + 1] : void 0;
|
|
4710
|
+
const portIdx = args2.indexOf("--port");
|
|
4711
|
+
const port = portIdx !== -1 ? parseInt(args2[portIdx + 1] ?? "", 10) : void 0;
|
|
4712
|
+
const entry = adoptProcess(pid, { name, port: Number.isNaN(port) ? void 0 : port });
|
|
4713
|
+
if (!entry) {
|
|
4714
|
+
console.error(renderError("Cannot adopt", `No running process with PID ${pid}`));
|
|
4715
|
+
process.exit(1);
|
|
4716
|
+
}
|
|
4717
|
+
console.error(`
|
|
4718
|
+
${symbols.fox} ${pc12.green("\u2713")} ${pc12.bold("Adopted")} ${pc12.bold(entry.name)} ${pc12.dim(`(PID ${pid})`)}`);
|
|
4719
|
+
if (entry.port) console.error(` ${pc12.dim("port")} ${pc12.yellow(`:${entry.port}`)}`);
|
|
4720
|
+
if (entry.command) console.error(` ${pc12.dim("command")} ${entry.command}`);
|
|
4721
|
+
console.error(` ${pc12.dim("status")} now tracked + supervised by the supervisor`);
|
|
4722
|
+
console.error(` ${pc12.dim("logs")} ${pc12.cyan(logFilePathFor(entry.name))}
|
|
4723
|
+
`);
|
|
4724
|
+
}
|
|
4725
|
+
|
|
4726
|
+
// src/commands/inspect.ts
|
|
4727
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
4728
|
+
import pc13 from "picocolors";
|
|
4729
|
+
|
|
4730
|
+
// src/utils/redact.ts
|
|
4731
|
+
import { existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
|
|
4732
|
+
var SECRET_PATTERNS = [
|
|
4733
|
+
// Bearer / auth tokens
|
|
4734
|
+
{ name: "bearer", re: /\bBearer\s+[A-Za-z0-9._\-]+/gi },
|
|
4735
|
+
// Authorization: <scheme> <token>
|
|
4736
|
+
{ name: "authorization", re: /\bAuthorization\s*:\s*\S+/gi },
|
|
4737
|
+
// Generic API keys
|
|
4738
|
+
{ name: "apikey", re: /\b(api[_-]?key|apikey|access[_-]?key|secret[_-]?key|private[_-]?key)\s*[:=]\s*\S+/gi },
|
|
4739
|
+
// AWS
|
|
4740
|
+
{ name: "aws", re: /\b(AKIA|ASIA)[0-9A-Z]{16}\b/g },
|
|
4741
|
+
// Slack tokens
|
|
4742
|
+
{ name: "slack", re: /\bxox[baprs]-[0-9A-Za-z\-]{10,}/g },
|
|
4743
|
+
// Stripe
|
|
4744
|
+
{ name: "stripe", re: /\b(sk|rk|pk)_(live|test)_[0-9A-Za-z]{16,}\b/g },
|
|
4745
|
+
// JWT
|
|
4746
|
+
{ name: "jwt", re: /\beyJ[A-Za-z0-9_\-]+\.[A-Za-z0-9_\-]+\.[A-Za-z0-9_\-]*/g },
|
|
4747
|
+
// Google API
|
|
4748
|
+
{ name: "google", re: /\bAIza[0-9A-Za-z_\-]{35}\b/g },
|
|
4749
|
+
// GitHub PAT
|
|
4750
|
+
{ name: "github", re: /\bgh[pousr]_[0-9A-Za-z]{36,}\b/g },
|
|
4751
|
+
// Generic long hex/alpha tokens (>=32 chars) in common contexts
|
|
4752
|
+
{ name: "token", re: /\b(token|secret|password|passwd|pwd|client[_-]?secret)\s*[:=]\s*\S+/gi },
|
|
4753
|
+
// Connection strings with credentials
|
|
4754
|
+
{ name: "connstr", re: /\b(mongodb(\+srv)?|postgres(ql)?|mysql|redis|amqp|sqlserver):\/\/[^\s:]+:[^\s@]+@/gi },
|
|
4755
|
+
// Private key blocks
|
|
4756
|
+
{ name: "pem", re: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g }
|
|
4757
|
+
];
|
|
4758
|
+
function redactLine(line, opts = {}) {
|
|
4759
|
+
if (opts.enabled === false) return line;
|
|
4760
|
+
let out = line;
|
|
4761
|
+
for (const { name, re } of SECRET_PATTERNS) {
|
|
4762
|
+
out = out.replace(re, (m) => {
|
|
4763
|
+
const head = m.slice(0, Math.min(m.length, name.length + 4));
|
|
4764
|
+
return `${head}\u2026[REDACTED]`;
|
|
4765
|
+
});
|
|
4766
|
+
}
|
|
4767
|
+
return out;
|
|
4768
|
+
}
|
|
4769
|
+
function redactionCount(line) {
|
|
4770
|
+
let n = 0;
|
|
4771
|
+
for (const { re } of SECRET_PATTERNS) {
|
|
4772
|
+
const m = line.match(re);
|
|
4773
|
+
if (m) n += m.length;
|
|
4774
|
+
}
|
|
4775
|
+
return n;
|
|
4776
|
+
}
|
|
4777
|
+
var ANSI_RE = /\x1b\[[0-9;]*m/g;
|
|
4778
|
+
function stripAnsi(s) {
|
|
4779
|
+
return s.replace(ANSI_RE, "");
|
|
4780
|
+
}
|
|
4781
|
+
var LEVEL_RE = [
|
|
4782
|
+
{ level: "error", re: /\b(ERROR|FATAL|CRITICAL|EXCEPTION|UNCAUGHT)\b/i },
|
|
4783
|
+
{ level: "warn", re: /\b(WARN|WARNING)\b/i },
|
|
4784
|
+
{ level: "info", re: /\b(INFO|NOTICE)\b/i },
|
|
4785
|
+
{ level: "debug", re: /\b(DEBUG|TRACE)\b/i }
|
|
4786
|
+
];
|
|
4787
|
+
function classifyLevel(line) {
|
|
4788
|
+
for (const { level, re } of LEVEL_RE) {
|
|
4789
|
+
if (re.test(line)) return level;
|
|
4790
|
+
}
|
|
4791
|
+
if (/\b(error|fail|failed|failure|denied|reject)\b/i.test(line)) return "error";
|
|
4792
|
+
if (/\b(warn)\b/i.test(line)) return "warn";
|
|
4793
|
+
return "other";
|
|
4794
|
+
}
|
|
4795
|
+
function extractTimestamp(line) {
|
|
4796
|
+
const m = line.match(/^\s*\[?(\d{4}-\d{2}-\d{2}T[\d:.]+Z?)\]?/);
|
|
4797
|
+
if (m) return m[1];
|
|
4798
|
+
const trimmed = line.trimStart();
|
|
4799
|
+
if (trimmed.startsWith("{")) {
|
|
4800
|
+
try {
|
|
4801
|
+
const obj = JSON.parse(line);
|
|
4802
|
+
const ts = obj.ts ?? obj.time ?? obj.timestamp;
|
|
4803
|
+
if (typeof ts === "string") return ts;
|
|
4804
|
+
if (typeof ts === "number") return new Date(ts).toISOString();
|
|
4805
|
+
} catch {
|
|
3552
4806
|
}
|
|
4807
|
+
}
|
|
4808
|
+
return null;
|
|
4809
|
+
}
|
|
4810
|
+
function readLogLines(path, opts = {}) {
|
|
4811
|
+
if (!existsSync6(path)) return [];
|
|
4812
|
+
const raw = readFileSync5(path, "utf-8");
|
|
4813
|
+
let lines = raw.split("\n").filter(Boolean);
|
|
4814
|
+
const now = Date.now();
|
|
4815
|
+
if (opts.sinceMs && opts.parseTimestamp) {
|
|
4816
|
+
lines = lines.filter((l) => {
|
|
4817
|
+
const ts = extractTimestamp(l);
|
|
4818
|
+
if (ts === null) return true;
|
|
4819
|
+
const t = Date.parse(ts);
|
|
4820
|
+
return !isNaN(t) && now - t <= opts.sinceMs;
|
|
4821
|
+
});
|
|
4822
|
+
}
|
|
4823
|
+
if (opts.redact === false) {
|
|
4824
|
+
lines = lines.map(stripAnsi);
|
|
3553
4825
|
} else {
|
|
3554
|
-
|
|
4826
|
+
lines = lines.map((l) => stripAnsi(redactLine(l)));
|
|
4827
|
+
}
|
|
4828
|
+
if (opts.tail && opts.tail > 0 && lines.length > opts.tail) {
|
|
4829
|
+
lines = lines.slice(-opts.tail);
|
|
4830
|
+
}
|
|
4831
|
+
return lines;
|
|
4832
|
+
}
|
|
4833
|
+
function parseDuration(input) {
|
|
4834
|
+
const m = input.trim().match(/^(\d+)\s*(s|m|h|d)$/i);
|
|
4835
|
+
if (!m) return null;
|
|
4836
|
+
const n = parseInt(m[1], 10);
|
|
4837
|
+
const unit = m[2].toLowerCase();
|
|
4838
|
+
const mul = unit === "s" ? 1e3 : unit === "m" ? 6e4 : unit === "h" ? 36e5 : 864e5;
|
|
4839
|
+
return n * mul;
|
|
4840
|
+
}
|
|
4841
|
+
|
|
4842
|
+
// src/commands/inspect.ts
|
|
4843
|
+
var DEFAULT_TAIL = 40;
|
|
4844
|
+
var MAX_TAIL = 200;
|
|
4845
|
+
function readMetrics(pid) {
|
|
4846
|
+
const m = {};
|
|
4847
|
+
try {
|
|
4848
|
+
const status = readFileSync6(`/proc/${pid}/status`, "utf-8");
|
|
4849
|
+
const rssLine = status.split("\n").find((l) => l.startsWith("VmRSS:"));
|
|
4850
|
+
if (rssLine) {
|
|
4851
|
+
const kb = parseInt(rssLine.replace(/[^\d]/g, ""), 10);
|
|
4852
|
+
if (!isNaN(kb)) m.rssMb = Math.round(kb / 1024 * 10) / 10;
|
|
4853
|
+
}
|
|
4854
|
+
} catch {
|
|
4855
|
+
}
|
|
4856
|
+
return m;
|
|
4857
|
+
}
|
|
4858
|
+
async function inspectCommand(args2) {
|
|
4859
|
+
const target = args2[0];
|
|
4860
|
+
if (!target || target.startsWith("--")) {
|
|
4861
|
+
console.error(pc13.dim(`Usage: fennec inspect <name|pid> [--plain] [--tail N] [--since 10m]`));
|
|
4862
|
+
process.exit(1);
|
|
4863
|
+
}
|
|
4864
|
+
const plain = args2.includes("--plain");
|
|
4865
|
+
const tailIndex = args2.indexOf("--tail");
|
|
4866
|
+
const tail = tailIndex !== -1 ? Math.min(parseInt(args2[tailIndex + 1], 10) || DEFAULT_TAIL, MAX_TAIL) : DEFAULT_TAIL;
|
|
4867
|
+
const sinceIndex = args2.indexOf("--since");
|
|
4868
|
+
const sinceMs = sinceIndex !== -1 ? parseDuration(args2[sinceIndex + 1]) : void 0;
|
|
4869
|
+
if (sinceIndex !== -1 && sinceMs === void 0) {
|
|
4870
|
+
console.error(pc13.red(`Invalid --since. Use e.g. 10m, 1h, 30s.`));
|
|
4871
|
+
process.exit(1);
|
|
4872
|
+
}
|
|
4873
|
+
const tracked = readTracked();
|
|
4874
|
+
const proc = tracked.find((t) => t.name === target || parseInt(target, 10) === t.pid && !isNaN(parseInt(target, 10)));
|
|
4875
|
+
if (!proc) {
|
|
4876
|
+
console.error(pc13.red(`No tracked app named "${target}".`));
|
|
4877
|
+
process.exit(1);
|
|
4878
|
+
}
|
|
4879
|
+
const running = isTrackedRunning(proc);
|
|
4880
|
+
const logPath = logFilePathFor(proc.name);
|
|
4881
|
+
const lines = readLogLines(logPath, { tail: Math.max(tail, sinceMs ? MAX_TAIL : 0), sinceMs: sinceMs ?? void 0, redact: true, parseTimestamp: true });
|
|
4882
|
+
const recent = lines.slice(-tail);
|
|
4883
|
+
const errorLines = lines.filter((l) => classifyLevel(l) === "error").slice(-15);
|
|
4884
|
+
const redactedLines = recent.reduce((acc, l) => acc + (redactionCount(stripAnsi(l)) > 0 ? 1 : 0), 0);
|
|
4885
|
+
const metrics = running ? readMetrics(proc.pid) : {};
|
|
4886
|
+
let portHealthy = null;
|
|
4887
|
+
if (proc.port) {
|
|
4888
|
+
try {
|
|
4889
|
+
portHealthy = await checkPort(proc.port);
|
|
4890
|
+
} catch {
|
|
4891
|
+
portHealthy = false;
|
|
4892
|
+
}
|
|
4893
|
+
}
|
|
4894
|
+
const startedAt = Date.parse(proc.startedAt);
|
|
4895
|
+
const uptimeSec = running && !isNaN(startedAt) ? Math.floor((Date.now() - startedAt) / 1e3) : 0;
|
|
4896
|
+
if (plain) {
|
|
4897
|
+
const dot = running ? pc13.green("\u25CF") : pc13.red("\u25CB");
|
|
4898
|
+
const portStr = proc.port ? ` :${proc.port}${portHealthy === false ? pc13.red(" (port down!)") : portHealthy ? pc13.green(" (up)") : ""}` : "";
|
|
4899
|
+
console.error(`
|
|
4900
|
+
${symbols.fox} ${renderAppName(proc.name)}${portStr} ${dot} ${running ? pc13.green(`running PID ${proc.pid}`) : pc13.red("stopped")}`);
|
|
4901
|
+
console.error(` ${pc13.dim("uptime")} ${pc13.cyan(uptimeSec ? formatUptime(uptimeSec) : "\u2014")} ${pc13.dim("rss")} ${pc13.cyan(metrics.rssMb ? `${metrics.rssMb}MB` : "\u2014")} ${pc13.dim("errors(last)")} ${pc13.cyan(String(errorLines.length))} ${pc13.dim("redacted")} ${pc13.cyan(String(redactedLines))}`);
|
|
4902
|
+
if (errorLines.length) {
|
|
4903
|
+
console.error(`
|
|
4904
|
+
${pc13.bold(pc13.red("Recent errors:"))}`);
|
|
4905
|
+
for (const l of errorLines.slice(-8)) console.error(` ${pc13.red(stripAnsi(l).slice(0, 300))}`);
|
|
4906
|
+
}
|
|
4907
|
+
console.error();
|
|
4908
|
+
return;
|
|
4909
|
+
}
|
|
4910
|
+
const payload = {
|
|
4911
|
+
name: proc.name,
|
|
4912
|
+
running,
|
|
4913
|
+
pid: running ? proc.pid ?? null : null,
|
|
4914
|
+
port: proc.port ?? null,
|
|
4915
|
+
portHealthy,
|
|
4916
|
+
autoRestart: proc.autoRestart ?? false,
|
|
4917
|
+
uptimeSec,
|
|
4918
|
+
metrics,
|
|
4919
|
+
logTail: recent,
|
|
4920
|
+
errorCount: errorLines.length,
|
|
4921
|
+
errors: errorLines,
|
|
4922
|
+
redacted: true,
|
|
4923
|
+
redactedLines,
|
|
4924
|
+
note: "Secrets redacted by default. Bounded snapshot for AI observation."
|
|
4925
|
+
};
|
|
4926
|
+
process.stdout.write(JSON.stringify(payload, null, 2) + "\n");
|
|
4927
|
+
}
|
|
4928
|
+
|
|
4929
|
+
// src/commands/dev.ts
|
|
4930
|
+
import pc14 from "picocolors";
|
|
4931
|
+
import { existsSync as existsSync8, readFileSync as readFileSync7 } from "fs";
|
|
4932
|
+
import { resolve as resolve7, dirname as dirname5 } from "path";
|
|
4933
|
+
import "os";
|
|
4934
|
+
import yaml from "js-yaml";
|
|
4935
|
+
function loadConfig(path) {
|
|
4936
|
+
if (!existsSync8(path)) {
|
|
4937
|
+
console.error(renderError("Config not found", path));
|
|
4938
|
+
process.exit(1);
|
|
4939
|
+
}
|
|
4940
|
+
try {
|
|
4941
|
+
const raw = readFileSync7(path, "utf-8");
|
|
4942
|
+
const parsed = yaml.load(raw);
|
|
4943
|
+
return parsed ?? {};
|
|
4944
|
+
} catch (err) {
|
|
4945
|
+
console.error(renderError("Failed to parse config", String(err)));
|
|
4946
|
+
process.exit(1);
|
|
4947
|
+
}
|
|
4948
|
+
}
|
|
4949
|
+
function resolveCwd(baseDir, cwd) {
|
|
4950
|
+
if (!cwd) return baseDir;
|
|
4951
|
+
return resolve7(baseDir, cwd);
|
|
4952
|
+
}
|
|
4953
|
+
async function waitForPort(port, timeoutMs = 3e4) {
|
|
4954
|
+
const deadline = Date.now() + timeoutMs;
|
|
4955
|
+
while (Date.now() < deadline) {
|
|
4956
|
+
try {
|
|
4957
|
+
if (await checkPort(port)) return true;
|
|
4958
|
+
} catch {
|
|
4959
|
+
}
|
|
4960
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
4961
|
+
}
|
|
4962
|
+
return false;
|
|
4963
|
+
}
|
|
4964
|
+
function configMatches(existing, desired) {
|
|
4965
|
+
if (existing.command !== desired.command) return false;
|
|
4966
|
+
if (JSON.stringify(existing.args ?? []) !== JSON.stringify(desired.args)) return false;
|
|
4967
|
+
if (existing.cwd !== desired.cwd) return false;
|
|
4968
|
+
if ((existing.port ?? null) !== (desired.port ?? null)) return false;
|
|
4969
|
+
if (existing.logMode !== desired.logMode) return false;
|
|
4970
|
+
if ((existing.autoRestart ?? true) !== desired.autoRestart) return false;
|
|
4971
|
+
if ((existing.healthCheck ?? null) !== (desired.healthCheck ?? null)) return false;
|
|
4972
|
+
if (JSON.stringify(existing.env ?? {}) !== JSON.stringify(desired.env)) return false;
|
|
4973
|
+
return true;
|
|
4974
|
+
}
|
|
4975
|
+
async function startApp(app, baseDir) {
|
|
4976
|
+
const cwd = resolveCwd(baseDir, app.cwd);
|
|
4977
|
+
const cmdParts = [app.command, ...app.args ?? []];
|
|
4978
|
+
const logFilePath = logFilePathFor(app.name);
|
|
4979
|
+
const env = { ...app.env ?? {} };
|
|
4980
|
+
const port = app.waitForPort ?? app.port;
|
|
4981
|
+
const restart = app.restart ?? true;
|
|
4982
|
+
const logMode = app.jsonl ? "jsonl" : "text";
|
|
4983
|
+
const desired = {
|
|
4984
|
+
command: cmdParts.join(" "),
|
|
4985
|
+
args: cmdParts,
|
|
4986
|
+
cwd,
|
|
4987
|
+
port,
|
|
4988
|
+
env,
|
|
4989
|
+
logMode,
|
|
4990
|
+
autoRestart: restart,
|
|
4991
|
+
healthCheck: app.healthCheck
|
|
4992
|
+
};
|
|
4993
|
+
const existing = readTracked().find((t) => t.name === app.name);
|
|
4994
|
+
if (existing && isTrackedRunning(existing) && configMatches(existing, desired)) {
|
|
4995
|
+
return "skipped";
|
|
4996
|
+
}
|
|
4997
|
+
const wasRunning = !!(existing && isTrackedRunning(existing));
|
|
4998
|
+
if (wasRunning) {
|
|
4999
|
+
try {
|
|
5000
|
+
process.kill(existing.pid, "SIGTERM");
|
|
5001
|
+
} catch {
|
|
5002
|
+
}
|
|
5003
|
+
}
|
|
5004
|
+
if (port && !(existing && isTrackedRunning(existing))) {
|
|
5005
|
+
const adopted = adoptExternalOnPort(port, app.name);
|
|
5006
|
+
if (adopted) {
|
|
5007
|
+
return "adopted";
|
|
5008
|
+
}
|
|
5009
|
+
if (await checkPort(port)) {
|
|
5010
|
+
console.error(
|
|
5011
|
+
` ${pc14.yellow("\u26A0")} ${pc14.dim(`port :${port} already responding \u2014 ${app.name} may fail to bind (EADDRINUSE)`)}`
|
|
5012
|
+
);
|
|
5013
|
+
}
|
|
5014
|
+
}
|
|
5015
|
+
const child = spawnDaemon({
|
|
5016
|
+
cmdParts,
|
|
5017
|
+
name: app.name,
|
|
5018
|
+
cwd,
|
|
5019
|
+
logFilePath,
|
|
5020
|
+
env: buildSpawnEnv(env),
|
|
5021
|
+
logMode
|
|
5022
|
+
});
|
|
5023
|
+
addTracked({
|
|
5024
|
+
name: app.name,
|
|
5025
|
+
pid: child.pid ?? 0,
|
|
5026
|
+
command: desired.command,
|
|
5027
|
+
args: cmdParts,
|
|
5028
|
+
port,
|
|
5029
|
+
cwd,
|
|
5030
|
+
env,
|
|
5031
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5032
|
+
autoRestart: restart,
|
|
5033
|
+
logMode,
|
|
5034
|
+
flapping: false,
|
|
5035
|
+
healthCheck: app.healthCheck
|
|
5036
|
+
});
|
|
5037
|
+
return wasRunning ? "restarted" : "started";
|
|
5038
|
+
}
|
|
5039
|
+
async function devCommand(args2) {
|
|
5040
|
+
const sub = args2[0] ?? "up";
|
|
5041
|
+
const configIndex = args2.indexOf("--config");
|
|
5042
|
+
const configPath = configIndex !== -1 ? resolve7(args2[configIndex + 1]) : resolve7(process.cwd(), "fennec.config.yaml");
|
|
5043
|
+
if (sub === "down") {
|
|
5044
|
+
return devDown();
|
|
5045
|
+
}
|
|
5046
|
+
if (sub === "status") {
|
|
5047
|
+
return devStatus();
|
|
5048
|
+
}
|
|
5049
|
+
if (sub === "restart") {
|
|
5050
|
+
return devRestart(args2.slice(1));
|
|
5051
|
+
}
|
|
5052
|
+
if (sub !== "up") {
|
|
5053
|
+
console.error(renderError("Unknown sub-command", `"${sub}" \u2014 use: up | down | status | restart`));
|
|
5054
|
+
process.exit(1);
|
|
5055
|
+
}
|
|
5056
|
+
const config = loadConfig(configPath);
|
|
5057
|
+
const apps = config.apps ?? [];
|
|
5058
|
+
if (apps.length === 0) {
|
|
5059
|
+
console.error(renderError("No apps defined", `Add an "apps:" list to ${configPath}`));
|
|
5060
|
+
process.exit(1);
|
|
5061
|
+
}
|
|
5062
|
+
const baseDir = dirname5(configPath);
|
|
5063
|
+
const started = /* @__PURE__ */ new Set();
|
|
5064
|
+
const ordered = topoOrder(apps);
|
|
5065
|
+
const portOwner = /* @__PURE__ */ new Map();
|
|
5066
|
+
for (const a of apps) {
|
|
5067
|
+
const p = a.waitForPort ?? a.port;
|
|
5068
|
+
if (p) {
|
|
5069
|
+
const prev = portOwner.get(p);
|
|
5070
|
+
if (prev) {
|
|
5071
|
+
console.error(renderError("Port conflict", `apps "${prev}" and "${a.name}" both declare port :${p}`));
|
|
5072
|
+
process.exit(1);
|
|
5073
|
+
}
|
|
5074
|
+
portOwner.set(p, a.name);
|
|
5075
|
+
}
|
|
5076
|
+
}
|
|
5077
|
+
console.error(`
|
|
5078
|
+
${symbols.fox} ${pc14.bold("fennec dev up")} ${pc14.dim(`\u2014 ${apps.length} app(s) from ${configPath}`)}`);
|
|
5079
|
+
ensureSupervisorRunning();
|
|
5080
|
+
ensurePersistEnabled();
|
|
5081
|
+
for (const app of ordered) {
|
|
5082
|
+
for (const dep of app.dependsOn ?? []) {
|
|
5083
|
+
const depApp = apps.find((a) => a.name === dep);
|
|
5084
|
+
const port = depApp?.waitForPort ?? depApp?.port;
|
|
5085
|
+
if (port) {
|
|
5086
|
+
const sp = createSpinner(`Waiting for ${dep} :${port}...`);
|
|
5087
|
+
const ok = await waitForPort(port);
|
|
5088
|
+
sp.stop();
|
|
5089
|
+
if (!ok) console.error(` ${pc14.yellow("\u26A0")} ${pc14.dim(`${dep} :${port} not ready in time (continuing)`)}`);
|
|
5090
|
+
}
|
|
5091
|
+
}
|
|
5092
|
+
const res = await startApp(app, baseDir);
|
|
5093
|
+
const pid = readTracked().find((t) => t.name === app.name)?.pid ?? "?";
|
|
5094
|
+
const tag = res === "skipped" ? pc14.dim("already running (skipped)") : res === "restarted" ? pc14.yellow(`restarted (PID ${pid})`) : res === "adopted" ? pc14.cyan(`adopted external process on :${app.port ?? app.waitForPort}`) : pc14.dim(`started (PID ${pid})`);
|
|
5095
|
+
console.error(` ${pc14.green("\u2713")} ${pc14.bold(app.name)} ${tag}`);
|
|
5096
|
+
}
|
|
5097
|
+
console.error(`
|
|
5098
|
+
${pc14.green("\u2713")} Stack up. Observe with ${pc14.cyan("fennec observe")} or ${pc14.cyan("fennec dev status")}.`);
|
|
5099
|
+
console.error(` ${pc14.dim("Apps auto-restart (supervisor) + survive reboot (persist).")}
|
|
5100
|
+
`);
|
|
5101
|
+
}
|
|
5102
|
+
function devDown() {
|
|
5103
|
+
const tracked = readTracked().filter((t) => t.autoRestart);
|
|
5104
|
+
if (tracked.length === 0) {
|
|
5105
|
+
console.error(`
|
|
5106
|
+
${pc14.dim("No supervised apps to stop.")}
|
|
5107
|
+
`);
|
|
5108
|
+
return;
|
|
5109
|
+
}
|
|
5110
|
+
console.error(`
|
|
5111
|
+
${symbols.fox} ${pc14.bold("fennec dev down")} ${pc14.dim(`\u2014 stopping ${tracked.length} app(s)`)}`);
|
|
5112
|
+
for (const t of tracked) {
|
|
5113
|
+
try {
|
|
5114
|
+
if (isTrackedRunning(t)) process.kill(t.pid, "SIGTERM");
|
|
5115
|
+
} catch {
|
|
5116
|
+
}
|
|
5117
|
+
t.autoRestart = false;
|
|
5118
|
+
addTracked({ ...t, autoRestart: false });
|
|
5119
|
+
console.error(` ${pc14.red("\u25A0")} ${pc14.bold(t.name)}`);
|
|
5120
|
+
}
|
|
5121
|
+
console.error(`
|
|
5122
|
+
${pc14.green("\u2713")} Stack stopped (auto-restart disabled). Use ${pc14.cyan("fennec dev up")} to bring it back.
|
|
5123
|
+
`);
|
|
5124
|
+
}
|
|
5125
|
+
function devRestart(names) {
|
|
5126
|
+
ensureSupervisorRunning();
|
|
5127
|
+
ensurePersistEnabled();
|
|
5128
|
+
const tracked = readTracked();
|
|
5129
|
+
const targets = names.length ? tracked.filter((t) => names.includes(t.name)) : tracked.filter((t) => t.autoRestart);
|
|
5130
|
+
const missing = names.filter((n) => !tracked.some((t) => t.name === n));
|
|
5131
|
+
if (missing.length) {
|
|
5132
|
+
console.error(renderError("Not tracked", `No such app(s): ${missing.join(", ")} \u2014 run "fennec dev status" to list tracked apps.`));
|
|
5133
|
+
}
|
|
5134
|
+
if (targets.length === 0) {
|
|
5135
|
+
console.error(`
|
|
5136
|
+
${pc14.dim("Nothing to restart.")}
|
|
5137
|
+
`);
|
|
5138
|
+
return;
|
|
5139
|
+
}
|
|
5140
|
+
console.error(`
|
|
5141
|
+
${symbols.fox} ${pc14.bold("fennec dev restart")} ${pc14.dim(`\u2014 ${targets.length} app(s)`)}`);
|
|
5142
|
+
for (const t of targets) {
|
|
5143
|
+
try {
|
|
5144
|
+
const newPid = respawnTracked(t, "manual");
|
|
5145
|
+
console.error(` ${pc14.green("\u2713")} ${pc14.bold(t.name)} ${pc14.dim(`restarted (PID ${newPid})`)}`);
|
|
5146
|
+
} catch (err) {
|
|
5147
|
+
console.error(` ${pc14.red("\u2717")} ${pc14.bold(t.name)} ${pc14.dim(`restart failed: ${String(err)}`)}`);
|
|
5148
|
+
}
|
|
5149
|
+
}
|
|
5150
|
+
console.error();
|
|
5151
|
+
}
|
|
5152
|
+
function devStatus() {
|
|
5153
|
+
const tracked = readTracked();
|
|
5154
|
+
console.error(`
|
|
5155
|
+
${symbols.fox} ${pc14.bold("fennec dev status")}`);
|
|
5156
|
+
if (tracked.length === 0) {
|
|
5157
|
+
console.error(` ${pc14.dim("Nothing tracked.")}
|
|
5158
|
+
`);
|
|
5159
|
+
return;
|
|
5160
|
+
}
|
|
5161
|
+
for (const t of tracked) {
|
|
5162
|
+
const running = isTrackedRunning(t);
|
|
5163
|
+
const dot = running ? pc14.green("\u25CF") : pc14.red("\u25CB");
|
|
5164
|
+
const state = running ? pc14.green(`running (PID ${t.pid})`) : pc14.red("stopped");
|
|
5165
|
+
const portStr = t.port ? ` ${pc14.yellow(`:${t.port}`)}` : "";
|
|
5166
|
+
const cause = t.restartCause ? pc14.dim(` \xB7 last-restart: ${t.restartCause}`) : "";
|
|
5167
|
+
const flap = t.flapping ? pc14.red(" \xB7 \u26A0 flapping") : "";
|
|
5168
|
+
console.error(` ${dot} ${pc14.bold(t.name)}${portStr} ${pc14.dim("\u2014")} ${state}${cause}${flap}`);
|
|
3555
5169
|
}
|
|
5170
|
+
console.error();
|
|
5171
|
+
}
|
|
5172
|
+
function topoOrder(apps) {
|
|
5173
|
+
const byName = new Map(apps.map((a) => [a.name, a]));
|
|
5174
|
+
const visited = /* @__PURE__ */ new Set();
|
|
5175
|
+
const out = [];
|
|
5176
|
+
const visit = (app, stack) => {
|
|
5177
|
+
if (visited.has(app.name)) return;
|
|
5178
|
+
if (stack.has(app.name)) return;
|
|
5179
|
+
stack.add(app.name);
|
|
5180
|
+
for (const dep of app.dependsOn ?? []) {
|
|
5181
|
+
const d = byName.get(dep);
|
|
5182
|
+
if (d) visit(d, stack);
|
|
5183
|
+
}
|
|
5184
|
+
stack.delete(app.name);
|
|
5185
|
+
visited.add(app.name);
|
|
5186
|
+
out.push(app);
|
|
5187
|
+
};
|
|
5188
|
+
for (const app of apps) visit(app, /* @__PURE__ */ new Set());
|
|
5189
|
+
return out;
|
|
3556
5190
|
}
|
|
3557
5191
|
|
|
3558
5192
|
// src/commands/log.ts
|
|
3559
|
-
import { existsSync as
|
|
3560
|
-
import {
|
|
3561
|
-
import
|
|
3562
|
-
|
|
3563
|
-
import pc10 from "picocolors";
|
|
5193
|
+
import { existsSync as existsSync9, unlinkSync as unlinkSync3, statSync as statSync2, openSync as openSync3, fstatSync, readSync, closeSync as closeSync3 } from "fs";
|
|
5194
|
+
import { execSync as execSync2 } from "child_process";
|
|
5195
|
+
import pc15 from "picocolors";
|
|
5196
|
+
var MAX_LOG_LINES = 500;
|
|
3564
5197
|
async function logCommand(args2) {
|
|
3565
5198
|
const target = args2[0];
|
|
3566
|
-
if (!target) {
|
|
3567
|
-
|
|
3568
|
-
|
|
5199
|
+
if (!target || target.startsWith("--")) {
|
|
5200
|
+
listAvailableApps();
|
|
5201
|
+
return;
|
|
3569
5202
|
}
|
|
3570
5203
|
const clearFlag = args2.includes("--clear");
|
|
5204
|
+
const jsonFlag = args2.includes("--json");
|
|
5205
|
+
const noRedact = args2.includes("--no-redact");
|
|
3571
5206
|
const linesIndex = args2.indexOf("--lines");
|
|
3572
|
-
const lineCount = linesIndex !== -1 ? parseInt(args2[linesIndex + 1], 10) : 30;
|
|
5207
|
+
const lineCount = linesIndex !== -1 ? Math.min(parseInt(args2[linesIndex + 1], 10), MAX_LOG_LINES) : 30;
|
|
3573
5208
|
const followFlag = args2.includes("-f") || args2.includes("--follow");
|
|
3574
5209
|
const levelFilter = args2.includes("--level") ? args2[args2.indexOf("--level") + 1]?.toLowerCase() : void 0;
|
|
5210
|
+
const sinceIndex = args2.indexOf("--since");
|
|
5211
|
+
const sinceMs = sinceIndex !== -1 ? parseDuration(args2[sinceIndex + 1]) : void 0;
|
|
5212
|
+
if (sinceIndex !== -1 && sinceMs === void 0) {
|
|
5213
|
+
console.error(renderError("Invalid --since", `Use a duration like 10m, 1h, 30s, 2d`));
|
|
5214
|
+
process.exit(1);
|
|
5215
|
+
}
|
|
3575
5216
|
if (levelFilter && !["error", "warn", "info", "debug"].includes(levelFilter)) {
|
|
3576
5217
|
console.error(renderError("Invalid level", `"${levelFilter}" is not a valid level. Use: error, warn, info, debug`));
|
|
3577
5218
|
process.exit(1);
|
|
3578
5219
|
}
|
|
5220
|
+
const redact = !noRedact;
|
|
3579
5221
|
let logFilePath = null;
|
|
3580
5222
|
let displayName = target;
|
|
3581
5223
|
const tracked = readTracked();
|
|
3582
5224
|
const trackedMatch = tracked.find((t) => t.name === target || parseInt(target, 10) === t.pid && !isNaN(parseInt(target, 10)));
|
|
3583
5225
|
if (trackedMatch) {
|
|
3584
5226
|
displayName = trackedMatch.name;
|
|
3585
|
-
logFilePath =
|
|
5227
|
+
logFilePath = logFilePathFor(trackedMatch.name);
|
|
3586
5228
|
} else {
|
|
3587
5229
|
const pid = parseInt(target, 10);
|
|
3588
5230
|
if (!isNaN(pid) && String(pid) === target) {
|
|
3589
5231
|
const procPath = `/proc/${pid}/fd/1`;
|
|
3590
|
-
if (
|
|
5232
|
+
if (existsSync9(procPath)) logFilePath = procPath;
|
|
3591
5233
|
}
|
|
3592
5234
|
}
|
|
3593
5235
|
if (clearFlag) {
|
|
3594
|
-
if (!logFilePath || !
|
|
5236
|
+
if (!logFilePath || !existsSync9(logFilePath)) {
|
|
3595
5237
|
console.error(`
|
|
3596
|
-
${
|
|
5238
|
+
${pc15.yellow("\u26A0")} ${pc15.dim("No log file found for")} ${pc15.bold(displayName)}
|
|
3597
5239
|
`);
|
|
3598
5240
|
process.exit(0);
|
|
3599
5241
|
}
|
|
3600
5242
|
try {
|
|
3601
|
-
|
|
5243
|
+
unlinkSync3(logFilePath);
|
|
3602
5244
|
console.error(`
|
|
3603
|
-
${
|
|
5245
|
+
${pc15.green("\u2713")} ${pc15.bold("Log cleared")} ${pc15.dim(`\u2014 ${logFilePath}`)}
|
|
3604
5246
|
`);
|
|
3605
5247
|
} catch (err) {
|
|
3606
5248
|
console.error(renderError("Failed to clear log", String(err)));
|
|
@@ -3608,18 +5250,75 @@ async function logCommand(args2) {
|
|
|
3608
5250
|
}
|
|
3609
5251
|
return;
|
|
3610
5252
|
}
|
|
5253
|
+
if (jsonFlag) {
|
|
5254
|
+
let logLines = [];
|
|
5255
|
+
if (logFilePath && existsSync9(logFilePath)) {
|
|
5256
|
+
logLines = readLogLines(logFilePath, {
|
|
5257
|
+
tail: Math.max(lineCount, sinceMs ? MAX_LOG_LINES : 0),
|
|
5258
|
+
sinceMs: sinceMs ?? void 0,
|
|
5259
|
+
redact,
|
|
5260
|
+
parseTimestamp: true
|
|
5261
|
+
});
|
|
5262
|
+
} else {
|
|
5263
|
+
const pid = trackedMatch?.pid ?? parseInt(target, 10);
|
|
5264
|
+
if (!isNaN(pid)) {
|
|
5265
|
+
try {
|
|
5266
|
+
const output = execSync2(`journalctl --no-pager -n ${Math.max(lineCount, 500)} _PID=${pid} 2>/dev/null || echo ""`, { encoding: "utf-8", timeout: 3e3 });
|
|
5267
|
+
logLines = output.trim().split("\n").filter(Boolean).map((l) => redact ? redactLine(stripAnsi(l)) : stripAnsi(l));
|
|
5268
|
+
} catch {
|
|
5269
|
+
logLines = ["(no logs available)"];
|
|
5270
|
+
}
|
|
5271
|
+
} else {
|
|
5272
|
+
logLines = ["(no logs available)"];
|
|
5273
|
+
}
|
|
5274
|
+
}
|
|
5275
|
+
if (levelFilter) {
|
|
5276
|
+
const levelRegex = new RegExp(`\\b(${levelFilter.toUpperCase()})\\b`, "i");
|
|
5277
|
+
logLines = logLines.filter((line) => levelRegex.test(line));
|
|
5278
|
+
}
|
|
5279
|
+
const running = trackedMatch ? isTrackedRunning(trackedMatch) : false;
|
|
5280
|
+
const sliced = logLines.slice(-lineCount);
|
|
5281
|
+
const redactedHits = sliced.reduce((acc, l) => acc + (redactionCount(stripAnsi(l)) > 0 ? 1 : 0), 0);
|
|
5282
|
+
const payload = {
|
|
5283
|
+
app: displayName,
|
|
5284
|
+
running,
|
|
5285
|
+
pid: trackedMatch?.pid != null ? trackedMatch.pid : null,
|
|
5286
|
+
port: trackedMatch?.port != null ? trackedMatch.port : null,
|
|
5287
|
+
lines: sliced,
|
|
5288
|
+
count: sliced.length,
|
|
5289
|
+
redacted: redact,
|
|
5290
|
+
redactedLines: redactedHits,
|
|
5291
|
+
note: "Secrets are redacted by default; use --no-redact to disable."
|
|
5292
|
+
};
|
|
5293
|
+
process.stdout.write(JSON.stringify(payload, null, 2) + "\n");
|
|
5294
|
+
return;
|
|
5295
|
+
}
|
|
5296
|
+
if (trackedMatch) {
|
|
5297
|
+
const running = isTrackedRunning(trackedMatch);
|
|
5298
|
+
const statusStr = running ? pc15.green(`\u25CF running (PID ${trackedMatch.pid})`) : pc15.red("\u25CB stopped");
|
|
5299
|
+
const portStr = trackedMatch.port ? ` ${pc15.yellow(`:${trackedMatch.port}`)}` : "";
|
|
5300
|
+
console.error(`
|
|
5301
|
+
${symbols.fox} ${renderAppName(displayName)}${portStr} \u2014 ${statusStr}`);
|
|
5302
|
+
if (!running) {
|
|
5303
|
+
console.error(` ${pc15.dim("This app is not running. Showing last captured logs.")} ${pc15.cyan(`fennec spawn ${displayName}`)} ${pc15.dim("to restart.")}`);
|
|
5304
|
+
}
|
|
5305
|
+
}
|
|
3611
5306
|
const spinner = createSpinner(`Reading logs for ${displayName}...`);
|
|
3612
5307
|
try {
|
|
3613
5308
|
let logLines = [];
|
|
3614
|
-
if (logFilePath &&
|
|
3615
|
-
|
|
3616
|
-
|
|
5309
|
+
if (logFilePath && existsSync9(logFilePath)) {
|
|
5310
|
+
logLines = readLogLines(logFilePath, {
|
|
5311
|
+
tail: Math.max(lineCount, sinceMs ? MAX_LOG_LINES : 0),
|
|
5312
|
+
sinceMs: sinceMs ?? void 0,
|
|
5313
|
+
redact,
|
|
5314
|
+
parseTimestamp: true
|
|
5315
|
+
});
|
|
3617
5316
|
} else {
|
|
3618
5317
|
const pid = trackedMatch?.pid ?? parseInt(target, 10);
|
|
3619
5318
|
if (!isNaN(pid)) {
|
|
3620
5319
|
try {
|
|
3621
|
-
const output = execSync2(`journalctl --no-pager -n ${lineCount} _PID=${pid} 2>/dev/null || echo ""`, { encoding: "utf-8", timeout: 3e3 });
|
|
3622
|
-
logLines = output.trim().split("\n").filter(Boolean);
|
|
5320
|
+
const output = execSync2(`journalctl --no-pager -n ${Math.max(lineCount, 500)} _PID=${pid} 2>/dev/null || echo ""`, { encoding: "utf-8", timeout: 3e3 });
|
|
5321
|
+
logLines = output.trim().split("\n").filter(Boolean).map((l) => redact ? redactLine(stripAnsi(l)) : stripAnsi(l));
|
|
3623
5322
|
} catch {
|
|
3624
5323
|
logLines = ["(no logs available)"];
|
|
3625
5324
|
}
|
|
@@ -3634,48 +5333,22 @@ async function logCommand(args2) {
|
|
|
3634
5333
|
spinner.stop();
|
|
3635
5334
|
process.stdout.write("\r\x1B[K");
|
|
3636
5335
|
console.error(`
|
|
3637
|
-
${symbols.fox} ${
|
|
5336
|
+
${symbols.fox} ${pc15.bold("Logs")} ${renderAppName(displayName)} ${pc15.dim(`(last ${logLines.length} line${logLines.length !== 1 ? "s" : ""})${redact ? pc15.dim(" \xB7 secrets redacted") : ""})`)}`);
|
|
3638
5337
|
const sliced = logLines.slice(-lineCount);
|
|
3639
5338
|
for (const line of sliced) {
|
|
3640
5339
|
const display = line.length > 300 ? line.slice(0, 300) + "\u2026" : line;
|
|
3641
|
-
const prefix = followFlag ? `${timestamp()} ` : "";
|
|
3642
5340
|
if (line.toLowerCase().includes("error") || line.toLowerCase().includes("fail") || line.toLowerCase().includes("fatal")) {
|
|
3643
|
-
console.error(` ${
|
|
5341
|
+
console.error(` ${pc15.red(display)}`);
|
|
3644
5342
|
} else if (line.toLowerCase().includes("warn")) {
|
|
3645
|
-
console.error(` ${
|
|
5343
|
+
console.error(` ${pc15.yellow(display)}`);
|
|
3646
5344
|
} else if (line.toLowerCase().includes("info") || line.includes("[")) {
|
|
3647
|
-
console.error(` ${
|
|
5345
|
+
console.error(` ${pc15.cyan(display)}`);
|
|
3648
5346
|
} else {
|
|
3649
|
-
console.error(` ${
|
|
5347
|
+
console.error(` ${display}`);
|
|
3650
5348
|
}
|
|
3651
5349
|
}
|
|
3652
5350
|
if (followFlag && logFilePath) {
|
|
3653
|
-
|
|
3654
|
-
if (levelFilter) {
|
|
3655
|
-
const tail = spawn4("tail", ["-n", "0", "-f", logFilePath], { stdio: ["ignore", "pipe", "pipe"] });
|
|
3656
|
-
const grep = spawn4("grep", ["--line-buffered", "-i", levelFilter], { stdio: ["pipe", "inherit", "inherit"] });
|
|
3657
|
-
if (tail.stdout) tail.stdout.pipe(grep.stdin);
|
|
3658
|
-
await new Promise((resolve12) => {
|
|
3659
|
-
tail.on("exit", () => resolve12());
|
|
3660
|
-
process.once("SIGINT", () => {
|
|
3661
|
-
tail.kill("SIGTERM");
|
|
3662
|
-
grep.kill("SIGTERM");
|
|
3663
|
-
resolve12();
|
|
3664
|
-
});
|
|
3665
|
-
});
|
|
3666
|
-
} else {
|
|
3667
|
-
console.error(`
|
|
3668
|
-
${pc10.dim("Following... (Ctrl+C to stop)")}
|
|
3669
|
-
`);
|
|
3670
|
-
const tail = spawn4("tail", tailArgs.concat([logFilePath]), { stdio: "inherit" });
|
|
3671
|
-
await new Promise((resolve12) => {
|
|
3672
|
-
tail.on("exit", () => resolve12());
|
|
3673
|
-
process.once("SIGINT", () => {
|
|
3674
|
-
tail.kill("SIGTERM");
|
|
3675
|
-
resolve12();
|
|
3676
|
-
});
|
|
3677
|
-
});
|
|
3678
|
-
}
|
|
5351
|
+
await followLog(logFilePath, { redact, levelFilter, json: jsonFlag });
|
|
3679
5352
|
}
|
|
3680
5353
|
console.error();
|
|
3681
5354
|
} catch (error) {
|
|
@@ -3683,10 +5356,97 @@ async function logCommand(args2) {
|
|
|
3683
5356
|
console.error(renderError("Log read failed", String(error)));
|
|
3684
5357
|
}
|
|
3685
5358
|
}
|
|
5359
|
+
async function followLog(logFilePath, opts) {
|
|
5360
|
+
let lastSize = 0;
|
|
5361
|
+
try {
|
|
5362
|
+
lastSize = statSync2(logFilePath).size;
|
|
5363
|
+
} catch {
|
|
5364
|
+
}
|
|
5365
|
+
console.error(`
|
|
5366
|
+
${pc15.dim("Following... (Ctrl+C to stop)")}
|
|
5367
|
+
`);
|
|
5368
|
+
const levelRe = opts.levelFilter ? new RegExp(`\\b(${opts.levelFilter.toUpperCase()})\\b`, "i") : null;
|
|
5369
|
+
const emit = (raw) => {
|
|
5370
|
+
const line = opts.redact ? redactLine(raw) : raw;
|
|
5371
|
+
if (levelRe && !levelRe.test(line)) return;
|
|
5372
|
+
if (opts.json) {
|
|
5373
|
+
try {
|
|
5374
|
+
process.stdout.write(JSON.stringify(JSON.parse(line)) + "\n");
|
|
5375
|
+
return;
|
|
5376
|
+
} catch {
|
|
5377
|
+
}
|
|
5378
|
+
}
|
|
5379
|
+
const display = line.length > 300 ? line.slice(0, 300) + "\u2026" : line;
|
|
5380
|
+
if (line.toLowerCase().includes("error") || line.toLowerCase().includes("fail") || line.toLowerCase().includes("fatal")) {
|
|
5381
|
+
console.error(` ${pc15.red(display)}`);
|
|
5382
|
+
} else if (line.toLowerCase().includes("warn")) {
|
|
5383
|
+
console.error(` ${pc15.yellow(display)}`);
|
|
5384
|
+
} else if (line.toLowerCase().includes("info") || line.includes("[")) {
|
|
5385
|
+
console.error(` ${pc15.cyan(display)}`);
|
|
5386
|
+
} else {
|
|
5387
|
+
console.error(` ${display}`);
|
|
5388
|
+
}
|
|
5389
|
+
};
|
|
5390
|
+
const timer = setInterval(() => {
|
|
5391
|
+
let fd;
|
|
5392
|
+
try {
|
|
5393
|
+
fd = openSync3(logFilePath, "r");
|
|
5394
|
+
} catch {
|
|
5395
|
+
return;
|
|
5396
|
+
}
|
|
5397
|
+
try {
|
|
5398
|
+
const size = fstatSync(fd).size;
|
|
5399
|
+
if (size < lastSize) lastSize = 0;
|
|
5400
|
+
if (size > lastSize) {
|
|
5401
|
+
const buf = Buffer.alloc(size - lastSize);
|
|
5402
|
+
readSync(fd, buf, 0, buf.length, lastSize);
|
|
5403
|
+
lastSize = size;
|
|
5404
|
+
for (const l of buf.toString("utf8").split("\n")) {
|
|
5405
|
+
if (l.length) emit(l);
|
|
5406
|
+
}
|
|
5407
|
+
}
|
|
5408
|
+
} finally {
|
|
5409
|
+
closeSync3(fd);
|
|
5410
|
+
}
|
|
5411
|
+
}, 400);
|
|
5412
|
+
await new Promise((resolve13) => {
|
|
5413
|
+
const stop = () => {
|
|
5414
|
+
clearInterval(timer);
|
|
5415
|
+
resolve13();
|
|
5416
|
+
};
|
|
5417
|
+
process.once("SIGINT", stop);
|
|
5418
|
+
process.once("SIGTERM", stop);
|
|
5419
|
+
});
|
|
5420
|
+
}
|
|
5421
|
+
function listAvailableApps() {
|
|
5422
|
+
const tracked = readTracked();
|
|
5423
|
+
if (tracked.length === 0) {
|
|
5424
|
+
console.error(`
|
|
5425
|
+
${symbols.fox} ${pc15.bold("No tracked apps to show logs for.")}`);
|
|
5426
|
+
console.error(` ${pc15.dim("Start one with:")} ${pc15.cyan("fennec start <command> --name <name>")}
|
|
5427
|
+
`);
|
|
5428
|
+
return;
|
|
5429
|
+
}
|
|
5430
|
+
console.error(`
|
|
5431
|
+
${symbols.fox} ${pc15.bold("Logs")} ${pc15.dim("\u2014 pick an app:")}
|
|
5432
|
+
`);
|
|
5433
|
+
for (const t of tracked) {
|
|
5434
|
+
const running = isTrackedRunning(t);
|
|
5435
|
+
const dot = running ? pc15.green("\u25CF") : pc15.red("\u25CB");
|
|
5436
|
+
const state = running ? pc15.green(`running (PID ${t.pid})`) : pc15.red("stopped");
|
|
5437
|
+
const portStr = t.port ? ` ${pc15.yellow(`:${t.port}`)}` : "";
|
|
5438
|
+
console.error(` ${dot} ${pc15.bold(t.name)}${portStr} ${pc15.dim("\u2014")} ${state}`);
|
|
5439
|
+
console.error(` ${pc15.cyan(`fennec log ${t.name}`)} ${pc15.dim("\xB7")} ${pc15.cyan(`fennec log ${t.name} -f`)}`);
|
|
5440
|
+
}
|
|
5441
|
+
console.error(`
|
|
5442
|
+
${pc15.dim("Flags:")} ${pc15.cyan("-f")} ${pc15.dim("follow")} ${pc15.dim("\xB7")} ${pc15.cyan("--lines N")} ${pc15.dim("\xB7")} ${pc15.cyan("--since 10m|1h")} ${pc15.dim("\xB7")} ${pc15.cyan("--level error|warn|info|debug")} ${pc15.dim("\xB7")} ${pc15.cyan("--json")} ${pc15.dim("(AI)")} ${pc15.dim("\xB7")} ${pc15.cyan("--no-redact")} ${pc15.dim("\xB7")} ${pc15.cyan("--clear")}`);
|
|
5443
|
+
console.error(` ${pc15.dim("AI mode:")} ${pc15.cyan("fennec log <app> --json --since 10m")} ${pc15.dim("\u2014 bounded, redacted, machine-readable. Secrets are redacted by default.")}
|
|
5444
|
+
`);
|
|
5445
|
+
}
|
|
3686
5446
|
|
|
3687
5447
|
// src/commands/attach.ts
|
|
3688
5448
|
import { PortDetector as PortDetector3 } from "@plumpslabs/fennec-core";
|
|
3689
|
-
import
|
|
5449
|
+
import pc16 from "picocolors";
|
|
3690
5450
|
async function attachCommand(args2) {
|
|
3691
5451
|
const port = parseInt(args2[0], 10);
|
|
3692
5452
|
if (isNaN(port)) {
|
|
@@ -3704,7 +5464,7 @@ async function attachCommand(args2) {
|
|
|
3704
5464
|
spinner.succeed(`Attached to :${port}`);
|
|
3705
5465
|
console.error(` ${renderKV("Name", renderAppName(name))}`);
|
|
3706
5466
|
console.error(` ${renderKV("PID", String(info.pid))}`);
|
|
3707
|
-
console.error(` ${renderKV("Command", info.command ||
|
|
5467
|
+
console.error(` ${renderKV("Command", info.command || pc16.dim("unknown"))}`);
|
|
3708
5468
|
} else {
|
|
3709
5469
|
spinner.fail(`No process found on port ${port}`);
|
|
3710
5470
|
process.exit(1);
|
|
@@ -3718,35 +5478,35 @@ async function attachCommand(args2) {
|
|
|
3718
5478
|
|
|
3719
5479
|
// src/commands/sessions.ts
|
|
3720
5480
|
import { SessionStore } from "@plumpslabs/fennec-core";
|
|
3721
|
-
import
|
|
5481
|
+
import pc17 from "picocolors";
|
|
3722
5482
|
async function sessionsCommand() {
|
|
3723
5483
|
const store = new SessionStore("./.fennec/sessions");
|
|
3724
5484
|
const sessions = store.list();
|
|
3725
5485
|
if (sessions.length === 0) {
|
|
3726
5486
|
console.error(`
|
|
3727
|
-
${
|
|
5487
|
+
${pc17.dim("No saved sessions found.")}
|
|
3728
5488
|
`);
|
|
3729
5489
|
return;
|
|
3730
5490
|
}
|
|
3731
5491
|
const columns = [
|
|
3732
|
-
{ key: "name", label: "Name", format: (v) =>
|
|
5492
|
+
{ key: "name", label: "Name", format: (v) => pc17.bold(String(v)) },
|
|
3733
5493
|
{ key: "origin", label: "Origin" },
|
|
3734
|
-
{ key: "savedAt", label: "Saved", format: (v) =>
|
|
5494
|
+
{ key: "savedAt", label: "Saved", format: (v) => pc17.dim(String(v)) }
|
|
3735
5495
|
];
|
|
3736
5496
|
const rows = sessions.map((s) => ({ name: s.name, origin: s.origin, savedAt: new Date(s.savedAt).toLocaleString() }));
|
|
3737
5497
|
console.error(`
|
|
3738
|
-
${symbols.fox} ${
|
|
5498
|
+
${symbols.fox} ${pc17.bold("Saved Sessions")}
|
|
3739
5499
|
`);
|
|
3740
5500
|
console.error(renderTable(columns, rows));
|
|
3741
|
-
console.error(` ${
|
|
5501
|
+
console.error(` ${pc17.dim(`${sessions.length} session(s)`)}
|
|
3742
5502
|
`);
|
|
3743
5503
|
}
|
|
3744
5504
|
|
|
3745
5505
|
// src/commands/setup.ts
|
|
3746
|
-
import
|
|
5506
|
+
import pc18 from "picocolors";
|
|
3747
5507
|
async function setupCommand() {
|
|
3748
5508
|
console.error(`
|
|
3749
|
-
${symbols.fox} ${
|
|
5509
|
+
${symbols.fox} ${pc18.bold("Fennec Setup")}
|
|
3750
5510
|
`);
|
|
3751
5511
|
const mcpClient = await selectPrompt("Which MCP client are you using?", [
|
|
3752
5512
|
{ value: "claude", label: "Claude Desktop", description: "Anthropic's AI desktop app" },
|
|
@@ -3755,12 +5515,12 @@ async function setupCommand() {
|
|
|
3755
5515
|
{ value: "other", label: "Other MCP client", description: "Any MCP-compatible client" }
|
|
3756
5516
|
]);
|
|
3757
5517
|
if (!mcpClient) {
|
|
3758
|
-
console.error(` ${
|
|
5518
|
+
console.error(` ${pc18.dim("Setup cancelled.")}
|
|
3759
5519
|
`);
|
|
3760
5520
|
return;
|
|
3761
5521
|
}
|
|
3762
5522
|
console.error(`
|
|
3763
|
-
${
|
|
5523
|
+
${pc18.green("\u2713")} Selected: ${pc18.bold(mcpClient)}
|
|
3764
5524
|
`);
|
|
3765
5525
|
const configSnippet = `{
|
|
3766
5526
|
"mcpServers": {
|
|
@@ -3770,24 +5530,24 @@ async function setupCommand() {
|
|
|
3770
5530
|
}
|
|
3771
5531
|
}
|
|
3772
5532
|
}`;
|
|
3773
|
-
console.error(` ${
|
|
5533
|
+
console.error(` ${pc18.bold("Add this to your MCP client config:")}
|
|
3774
5534
|
`);
|
|
3775
|
-
console.error(` ${
|
|
5535
|
+
console.error(` ${pc18.dim("```")}`);
|
|
3776
5536
|
console.error(configSnippet.split("\n").map((l) => ` ${l}`).join("\n"));
|
|
3777
|
-
console.error(` ${
|
|
5537
|
+
console.error(` ${pc18.dim("```")}
|
|
3778
5538
|
`);
|
|
3779
|
-
console.error(` ${renderSuccess("Setup complete!")} ${
|
|
5539
|
+
console.error(` ${renderSuccess("Setup complete!")} ${pc18.dim("Run")} ${renderCommand("fennec start")} ${pc18.dim("to begin.")}
|
|
3780
5540
|
`);
|
|
3781
5541
|
}
|
|
3782
5542
|
|
|
3783
5543
|
// src/commands/management.ts
|
|
3784
|
-
import { existsSync as
|
|
3785
|
-
import { resolve as
|
|
5544
|
+
import { existsSync as existsSync10, writeFileSync as writeFileSync4 } from "fs";
|
|
5545
|
+
import { resolve as resolve8 } from "path";
|
|
3786
5546
|
import { execSync as execSync3 } from "child_process";
|
|
3787
|
-
import
|
|
5547
|
+
import pc19 from "picocolors";
|
|
3788
5548
|
async function installBrowsersCommand() {
|
|
3789
5549
|
console.error(`
|
|
3790
|
-
${
|
|
5550
|
+
${pc19.bold("Installing Browser Engines")}
|
|
3791
5551
|
`);
|
|
3792
5552
|
const spinner = createSpinner("Installing Chromium...");
|
|
3793
5553
|
try {
|
|
@@ -3795,21 +5555,21 @@ async function installBrowsersCommand() {
|
|
|
3795
5555
|
spinner.succeed("Chromium installed successfully");
|
|
3796
5556
|
} catch {
|
|
3797
5557
|
spinner.fail("Failed to install Chromium");
|
|
3798
|
-
console.error(` ${
|
|
5558
|
+
console.error(` ${pc19.yellow("\u2192")} Try running: ${renderCommand("npx playwright install chromium")}`);
|
|
3799
5559
|
}
|
|
3800
5560
|
console.error(`
|
|
3801
|
-
${
|
|
5561
|
+
${pc19.green("\u2713")} Browser installation complete.
|
|
3802
5562
|
`);
|
|
3803
5563
|
}
|
|
3804
5564
|
async function initCommand() {
|
|
3805
5565
|
console.error(`
|
|
3806
|
-
${
|
|
5566
|
+
${pc19.bold("Initialize Fennec Configuration")}
|
|
3807
5567
|
`);
|
|
3808
|
-
const configFile =
|
|
3809
|
-
if (
|
|
3810
|
-
const overwrite = await confirmPrompt(`${
|
|
5568
|
+
const configFile = resolve8("./fennec.config.yaml");
|
|
5569
|
+
if (existsSync10(configFile)) {
|
|
5570
|
+
const overwrite = await confirmPrompt(`${pc19.yellow("fennec.config.yaml")} already exists. Overwrite?`, false);
|
|
3811
5571
|
if (!overwrite) {
|
|
3812
|
-
console.error(` ${
|
|
5572
|
+
console.error(` ${pc19.dim("Cancelled.")}
|
|
3813
5573
|
`);
|
|
3814
5574
|
return;
|
|
3815
5575
|
}
|
|
@@ -3888,22 +5648,22 @@ logging:
|
|
|
3888
5648
|
format: pretty
|
|
3889
5649
|
file: null
|
|
3890
5650
|
`;
|
|
3891
|
-
|
|
3892
|
-
spinner.succeed(`Configuration written to ${
|
|
5651
|
+
writeFileSync4(configFile, config, "utf-8");
|
|
5652
|
+
spinner.succeed(`Configuration written to ${pc19.bold(configFile)}`);
|
|
3893
5653
|
console.error(`
|
|
3894
|
-
${
|
|
5654
|
+
${pc19.dim("Edit the file to customize Fennec behavior.")}
|
|
3895
5655
|
`);
|
|
3896
5656
|
}
|
|
3897
5657
|
|
|
3898
5658
|
// src/commands/health.ts
|
|
3899
|
-
import { existsSync as
|
|
5659
|
+
import { existsSync as existsSync11, readdirSync as readdirSync2, statSync as statSync3 } from "fs";
|
|
3900
5660
|
import { execSync as execSync4 } from "child_process";
|
|
3901
|
-
import { resolve as
|
|
5661
|
+
import { resolve as resolve9 } from "path";
|
|
3902
5662
|
import { homedir as homedir6 } from "os";
|
|
3903
|
-
import
|
|
5663
|
+
import pc20 from "picocolors";
|
|
3904
5664
|
async function healthCommand() {
|
|
3905
5665
|
printBanner();
|
|
3906
|
-
console.error(` ${
|
|
5666
|
+
console.error(` ${pc20.bold("Fennec Health Check")}
|
|
3907
5667
|
`);
|
|
3908
5668
|
let adbStatus = "unknown";
|
|
3909
5669
|
try {
|
|
@@ -3913,20 +5673,20 @@ async function healthCommand() {
|
|
|
3913
5673
|
stdio: ["ignore", "pipe", "pipe"]
|
|
3914
5674
|
});
|
|
3915
5675
|
const version = result.split("\n")[0]?.trim() ?? "installed";
|
|
3916
|
-
adbStatus =
|
|
5676
|
+
adbStatus = pc20.green(version);
|
|
3917
5677
|
} catch {
|
|
3918
|
-
adbStatus =
|
|
5678
|
+
adbStatus = pc20.dim("not found") + " (optional)";
|
|
3919
5679
|
}
|
|
3920
5680
|
const tracked = readTracked();
|
|
3921
5681
|
const runningCount = tracked.filter((t) => isProcessRunning(t.pid)).length;
|
|
3922
|
-
const logDir =
|
|
5682
|
+
const logDir = resolve9(homedir6(), ".fennec", "logs");
|
|
3923
5683
|
let logSize = "0 B";
|
|
3924
5684
|
try {
|
|
3925
|
-
if (
|
|
5685
|
+
if (existsSync11(logDir)) {
|
|
3926
5686
|
const files = readdirSync2(logDir);
|
|
3927
5687
|
let totalBytes = 0;
|
|
3928
5688
|
for (const f of files) {
|
|
3929
|
-
const fStat =
|
|
5689
|
+
const fStat = statSync3(resolve9(logDir, f));
|
|
3930
5690
|
totalBytes += fStat.size;
|
|
3931
5691
|
}
|
|
3932
5692
|
logSize = totalBytes > 1024 * 1024 ? `${(totalBytes / (1024 * 1024)).toFixed(1)} MB` : `${(totalBytes / 1024).toFixed(1)} KB`;
|
|
@@ -3939,7 +5699,7 @@ async function healthCommand() {
|
|
|
3939
5699
|
memoryInfo = `${(mem.rss / (1024 * 1024)).toFixed(0)} MB RSS / ${(mem.heapUsed / (1024 * 1024)).toFixed(0)} MB heap`;
|
|
3940
5700
|
} catch {
|
|
3941
5701
|
}
|
|
3942
|
-
console.error(` ${symbols.fox} ${
|
|
5702
|
+
console.error(` ${symbols.fox} ${pc20.bold("System")}
|
|
3943
5703
|
`);
|
|
3944
5704
|
console.error(` ${renderKV("Node.js", process.version)}`);
|
|
3945
5705
|
console.error(` ${renderKV("Platform", `${process.platform} ${process.arch}`)}`);
|
|
@@ -3947,7 +5707,7 @@ async function healthCommand() {
|
|
|
3947
5707
|
console.error(` ${renderKV("ADB", adbStatus)}`);
|
|
3948
5708
|
console.error(` ${renderKV("PID", String(process.pid))}`);
|
|
3949
5709
|
console.error();
|
|
3950
|
-
console.error(` ${symbols.fox} ${
|
|
5710
|
+
console.error(` ${symbols.fox} ${pc20.bold("Processes")}
|
|
3951
5711
|
`);
|
|
3952
5712
|
console.error(` ${renderKV("Tracked", String(tracked.length))}`);
|
|
3953
5713
|
console.error(` ${renderKV("Running", String(runningCount))}`);
|
|
@@ -3955,55 +5715,55 @@ async function healthCommand() {
|
|
|
3955
5715
|
console.error();
|
|
3956
5716
|
const allHealthy = runningCount === tracked.length || tracked.length === 0;
|
|
3957
5717
|
if (allHealthy) {
|
|
3958
|
-
console.error(` ${
|
|
5718
|
+
console.error(` ${pc20.green("\u2713")} ${pc20.bold("All systems healthy")}
|
|
3959
5719
|
`);
|
|
3960
5720
|
} else {
|
|
3961
5721
|
const stopped = tracked.length - runningCount;
|
|
3962
|
-
console.error(` ${
|
|
5722
|
+
console.error(` ${pc20.yellow("\u26A0")} ${pc20.bold(`${stopped} process(es) stopped`)} ${pc20.dim("fennec ps")}
|
|
3963
5723
|
`);
|
|
3964
5724
|
}
|
|
3965
5725
|
}
|
|
3966
5726
|
|
|
3967
5727
|
// src/commands/cleanup.ts
|
|
3968
|
-
import
|
|
5728
|
+
import pc21 from "picocolors";
|
|
3969
5729
|
async function cleanupCommand() {
|
|
3970
5730
|
const tracked = readTracked();
|
|
3971
5731
|
if (tracked.length === 0) {
|
|
3972
5732
|
console.error(`
|
|
3973
|
-
${
|
|
5733
|
+
${pc21.dim("No tracked processes to clean up.")}
|
|
3974
5734
|
`);
|
|
3975
5735
|
return;
|
|
3976
5736
|
}
|
|
3977
5737
|
const toRemove = tracked.filter((t) => !isProcessRunning(t.pid) && !t.command);
|
|
3978
5738
|
if (toRemove.length === 0) {
|
|
3979
5739
|
console.error(`
|
|
3980
|
-
${
|
|
5740
|
+
${pc21.green("\u2713")} ${pc21.bold("All clean")} ${pc21.dim("\u2014 no dead entries without saved commands.")}
|
|
3981
5741
|
`);
|
|
3982
5742
|
return;
|
|
3983
5743
|
}
|
|
3984
5744
|
console.error(`
|
|
3985
|
-
${
|
|
5745
|
+
${pc21.yellow("\u26A0")} ${pc21.bold(`Found ${toRemove.length} dead entr${toRemove.length > 1 ? "ies" : "y"} without saved commands`)}
|
|
3986
5746
|
`);
|
|
3987
5747
|
for (const entry of toRemove) {
|
|
3988
|
-
console.error(` ${
|
|
5748
|
+
console.error(` ${pc21.dim("\xB7")} ${pc21.bold(entry.name)} ${pc21.dim(`(PID ${entry.pid})`)}`);
|
|
3989
5749
|
}
|
|
3990
5750
|
console.error();
|
|
3991
5751
|
const confirmed = await confirmPrompt(`Remove ${toRemove.length} entr${toRemove.length > 1 ? "ies" : "y"}?`, false);
|
|
3992
5752
|
if (!confirmed) {
|
|
3993
|
-
console.error(` ${
|
|
5753
|
+
console.error(` ${pc21.dim("Cancelled")}
|
|
3994
5754
|
`);
|
|
3995
5755
|
return;
|
|
3996
5756
|
}
|
|
3997
5757
|
const remaining = tracked.filter((t) => !toRemove.includes(t));
|
|
3998
5758
|
saveTracked(remaining);
|
|
3999
5759
|
console.error(`
|
|
4000
|
-
${
|
|
5760
|
+
${pc21.green("\u2713")} ${pc21.bold(`Removed ${toRemove.length} entr${toRemove.length > 1 ? "ies" : "y"}`)} ${pc21.dim(`(${remaining.length} remaining)`)}
|
|
4001
5761
|
`);
|
|
4002
5762
|
}
|
|
4003
5763
|
|
|
4004
5764
|
// src/commands/info.ts
|
|
4005
|
-
import
|
|
4006
|
-
import { resolve as
|
|
5765
|
+
import pc22 from "picocolors";
|
|
5766
|
+
import { resolve as resolve10 } from "path";
|
|
4007
5767
|
import { homedir as homedir7 } from "os";
|
|
4008
5768
|
async function infoCommand(args2) {
|
|
4009
5769
|
const name = args2[0];
|
|
@@ -4018,33 +5778,33 @@ async function infoCommand(args2) {
|
|
|
4018
5778
|
process.exit(1);
|
|
4019
5779
|
}
|
|
4020
5780
|
const running = isProcessRunning(match.pid);
|
|
4021
|
-
const statusIcon = running ?
|
|
4022
|
-
const statusText = running ?
|
|
5781
|
+
const statusIcon = running ? pc22.green("\u25CF") : pc22.red("\u25CB");
|
|
5782
|
+
const statusText = running ? pc22.green("running") : pc22.red("stopped");
|
|
4023
5783
|
const uptime = running ? formatUptime(Math.floor((Date.now() - new Date(match.startedAt).getTime()) / 1e3)) : "-";
|
|
4024
|
-
const logPath =
|
|
5784
|
+
const logPath = resolve10(homedir7(), ".fennec", "logs", `${match.name}.log`);
|
|
4025
5785
|
console.error(`
|
|
4026
|
-
${symbols.fox} ${
|
|
5786
|
+
${symbols.fox} ${pc22.bold(match.name)} ${pc22.dim("\u2014 Process Info")}
|
|
4027
5787
|
`);
|
|
4028
|
-
console.error(` ${renderKVColor("Name", match.name,
|
|
5788
|
+
console.error(` ${renderKVColor("Name", match.name, pc22.bold)}`);
|
|
4029
5789
|
console.error(` ${renderKVColor("Status", `${statusIcon} ${statusText}`)}`);
|
|
4030
5790
|
console.error(` ${renderKVColor("PID", String(match.pid))}`);
|
|
4031
5791
|
console.error(` ${renderKVColor("Port", match.port ? String(match.port) : "-")}`);
|
|
4032
|
-
console.error(` ${renderKVColor("Command", match.command ||
|
|
5792
|
+
console.error(` ${renderKVColor("Command", match.command || pc22.dim("(none)"))}`);
|
|
4033
5793
|
if (match.cwd) console.error(` ${renderKVColor("CWD", match.cwd)}`);
|
|
4034
5794
|
console.error(` ${renderKVColor("Started", new Date(match.startedAt).toLocaleString())}`);
|
|
4035
5795
|
console.error(` ${renderKVColor("Uptime", uptime)}`);
|
|
4036
5796
|
console.error(` ${renderKVColor("Log Path", logPath)}`);
|
|
4037
5797
|
if (!running && match.command) {
|
|
4038
5798
|
console.error(`
|
|
4039
|
-
${
|
|
5799
|
+
${pc22.dim("Re-spawn with:")} ${pc22.cyan(`fennec spawn ${match.name}`)}`);
|
|
4040
5800
|
}
|
|
4041
5801
|
console.error();
|
|
4042
5802
|
}
|
|
4043
5803
|
|
|
4044
5804
|
// src/commands/rename.ts
|
|
4045
|
-
import
|
|
4046
|
-
import { existsSync as
|
|
4047
|
-
import { resolve as
|
|
5805
|
+
import pc23 from "picocolors";
|
|
5806
|
+
import { existsSync as existsSync12, renameSync as renameSync2 } from "fs";
|
|
5807
|
+
import { resolve as resolve11 } from "path";
|
|
4048
5808
|
import { homedir as homedir8 } from "os";
|
|
4049
5809
|
async function renameCommand(args2) {
|
|
4050
5810
|
const [oldName, newName] = args2;
|
|
@@ -4054,7 +5814,7 @@ async function renameCommand(args2) {
|
|
|
4054
5814
|
}
|
|
4055
5815
|
if (oldName === newName) {
|
|
4056
5816
|
console.error(`
|
|
4057
|
-
${
|
|
5817
|
+
${pc23.yellow("\u26A0")} ${pc23.dim("Old and new names are the same.")}
|
|
4058
5818
|
`);
|
|
4059
5819
|
process.exit(0);
|
|
4060
5820
|
}
|
|
@@ -4069,25 +5829,25 @@ async function renameCommand(args2) {
|
|
|
4069
5829
|
process.exit(1);
|
|
4070
5830
|
}
|
|
4071
5831
|
console.error(`
|
|
4072
|
-
${symbols.fox} ${
|
|
5832
|
+
${symbols.fox} ${pc23.bold("Rename Process")}
|
|
4073
5833
|
`);
|
|
4074
5834
|
console.error(` ${renderKVColor("From", oldName)}`);
|
|
4075
5835
|
console.error(` ${renderKVColor("To", newName)}`);
|
|
4076
5836
|
if (isProcessRunning(match.pid)) {
|
|
4077
|
-
console.error(` ${
|
|
5837
|
+
console.error(` ${pc23.yellow("\u26A0")} ${pc23.dim("Process is still running \u2014 log entries will go to the old file until stopped.")}`);
|
|
4078
5838
|
}
|
|
4079
5839
|
console.error();
|
|
4080
|
-
const confirmed = await confirmPrompt(`Rename ${
|
|
5840
|
+
const confirmed = await confirmPrompt(`Rename ${pc23.bold(oldName)} ${pc23.dim("\u2192")} ${pc23.bold(newName)}?`, true);
|
|
4081
5841
|
if (!confirmed) {
|
|
4082
|
-
console.error(` ${
|
|
5842
|
+
console.error(` ${pc23.dim("Cancelled")}
|
|
4083
5843
|
`);
|
|
4084
5844
|
return;
|
|
4085
5845
|
}
|
|
4086
5846
|
const spinner = createSpinner(`Renaming ${oldName} \u2192 ${newName}...`);
|
|
4087
|
-
const logDir =
|
|
4088
|
-
const oldLog =
|
|
4089
|
-
const newLog =
|
|
4090
|
-
if (
|
|
5847
|
+
const logDir = resolve11(homedir8(), ".fennec", "logs");
|
|
5848
|
+
const oldLog = resolve11(logDir, `${oldName}.log`);
|
|
5849
|
+
const newLog = resolve11(logDir, `${newName}.log`);
|
|
5850
|
+
if (existsSync12(oldLog) && !existsSync12(newLog)) {
|
|
4091
5851
|
try {
|
|
4092
5852
|
renameSync2(oldLog, newLog);
|
|
4093
5853
|
} catch {
|
|
@@ -4095,14 +5855,14 @@ async function renameCommand(args2) {
|
|
|
4095
5855
|
}
|
|
4096
5856
|
match.name = newName;
|
|
4097
5857
|
saveTracked(tracked);
|
|
4098
|
-
spinner.succeed(`${
|
|
5858
|
+
spinner.succeed(`${pc23.bold(oldName)} ${pc23.dim("\u2192")} ${pc23.bold(newName)}`);
|
|
4099
5859
|
console.error();
|
|
4100
5860
|
}
|
|
4101
5861
|
|
|
4102
5862
|
// src/commands/export-import.ts
|
|
4103
|
-
import
|
|
4104
|
-
import { existsSync as
|
|
4105
|
-
import { resolve as
|
|
5863
|
+
import pc24 from "picocolors";
|
|
5864
|
+
import { existsSync as existsSync13, writeFileSync as writeFileSync5, readFileSync as readFileSync9 } from "fs";
|
|
5865
|
+
import { resolve as resolve12 } from "path";
|
|
4106
5866
|
async function exportCommand(args2) {
|
|
4107
5867
|
const tracked = readTracked();
|
|
4108
5868
|
const fileIndex = args2.indexOf("--file");
|
|
@@ -4113,16 +5873,16 @@ async function exportCommand(args2) {
|
|
|
4113
5873
|
}
|
|
4114
5874
|
if (tracked.length === 0) {
|
|
4115
5875
|
console.error(`
|
|
4116
|
-
${
|
|
5876
|
+
${pc24.dim("No tracked processes to export.")}
|
|
4117
5877
|
`);
|
|
4118
5878
|
return;
|
|
4119
5879
|
}
|
|
4120
5880
|
const json = JSON.stringify(tracked, null, 2);
|
|
4121
5881
|
if (filePath) {
|
|
4122
5882
|
try {
|
|
4123
|
-
|
|
5883
|
+
writeFileSync5(resolve12(filePath), json, "utf-8");
|
|
4124
5884
|
console.error(`
|
|
4125
|
-
${
|
|
5885
|
+
${pc24.green("\u2713")} ${pc24.bold(`Exported ${tracked.length} process(es)`)} ${pc24.dim(`\u2192 ${filePath}`)}
|
|
4126
5886
|
`);
|
|
4127
5887
|
} catch (err) {
|
|
4128
5888
|
console.error(renderError("Export failed", String(err)));
|
|
@@ -4138,14 +5898,14 @@ async function importCommand(args2) {
|
|
|
4138
5898
|
console.error(renderError("Missing file", "Usage: fennec import <file>"));
|
|
4139
5899
|
process.exit(1);
|
|
4140
5900
|
}
|
|
4141
|
-
const resolvedPath =
|
|
4142
|
-
if (!
|
|
5901
|
+
const resolvedPath = resolve12(filePath);
|
|
5902
|
+
if (!existsSync13(resolvedPath)) {
|
|
4143
5903
|
console.error(renderError("File not found", `"${filePath}" does not exist.`));
|
|
4144
5904
|
process.exit(1);
|
|
4145
5905
|
}
|
|
4146
5906
|
let imported;
|
|
4147
5907
|
try {
|
|
4148
|
-
imported = JSON.parse(
|
|
5908
|
+
imported = JSON.parse(readFileSync9(resolvedPath, "utf-8"));
|
|
4149
5909
|
if (!Array.isArray(imported) || imported.length === 0) {
|
|
4150
5910
|
console.error(renderError("Invalid file", "File must contain a non-empty JSON array of process objects."));
|
|
4151
5911
|
process.exit(1);
|
|
@@ -4156,20 +5916,20 @@ async function importCommand(args2) {
|
|
|
4156
5916
|
}
|
|
4157
5917
|
const existing = readTracked();
|
|
4158
5918
|
console.error(`
|
|
4159
|
-
${symbols.fox} ${
|
|
5919
|
+
${symbols.fox} ${pc24.bold("Import Processes")}
|
|
4160
5920
|
`);
|
|
4161
5921
|
console.error(` ${renderKVColor("File", resolvedPath)}`);
|
|
4162
5922
|
console.error(` ${renderKVColor("Importing", `${imported.length} process(es)`)}`);
|
|
4163
5923
|
console.error(` ${renderKVColor("Existing", `${existing.length} process(es)`)}`);
|
|
4164
5924
|
console.error(`
|
|
4165
|
-
${
|
|
5925
|
+
${pc24.bold("Processes to import:")}`);
|
|
4166
5926
|
for (const p of imported) {
|
|
4167
|
-
console.error(` ${
|
|
5927
|
+
console.error(` ${pc24.green("+")} ${pc24.bold(p.name)} ${pc24.dim(`(${p.command})`)}`);
|
|
4168
5928
|
}
|
|
4169
5929
|
console.error();
|
|
4170
5930
|
const confirmed = await confirmPrompt("Merge into tracked.json? Existing processes with the same name will be overwritten.", true);
|
|
4171
5931
|
if (!confirmed) {
|
|
4172
|
-
console.error(` ${
|
|
5932
|
+
console.error(` ${pc24.dim("Cancelled")}
|
|
4173
5933
|
`);
|
|
4174
5934
|
return;
|
|
4175
5935
|
}
|
|
@@ -4183,13 +5943,18 @@ async function importCommand(args2) {
|
|
|
4183
5943
|
}
|
|
4184
5944
|
}
|
|
4185
5945
|
saveTracked(merged);
|
|
4186
|
-
console.error(` ${
|
|
5946
|
+
console.error(` ${pc24.green("\u2713")} ${pc24.bold(`Imported ${imported.length} process(es)`)} ${pc24.dim(`(${merged.length} total)`)}
|
|
4187
5947
|
`);
|
|
4188
5948
|
}
|
|
4189
5949
|
|
|
4190
5950
|
// src/index.ts
|
|
4191
5951
|
var [, , command, ...args] = process.argv;
|
|
4192
5952
|
async function main() {
|
|
5953
|
+
if (command && command !== "help" && command !== "--help" && command !== "-h" && (args.includes("--help") || args.includes("-h"))) {
|
|
5954
|
+
printBanner();
|
|
5955
|
+
showCommandHelp(command);
|
|
5956
|
+
return;
|
|
5957
|
+
}
|
|
4193
5958
|
if (!command || command === "start") {
|
|
4194
5959
|
if (args.length === 0 || args[0]?.startsWith("--")) {
|
|
4195
5960
|
await startServer(args);
|
|
@@ -4204,7 +5969,7 @@ async function main() {
|
|
|
4204
5969
|
} else if (command === "status") {
|
|
4205
5970
|
printBanner();
|
|
4206
5971
|
await statusCommand(args);
|
|
4207
|
-
} else if (command === "log") {
|
|
5972
|
+
} else if (command === "log" || command === "logs") {
|
|
4208
5973
|
await logCommand(args);
|
|
4209
5974
|
} else if (command === "spawn") {
|
|
4210
5975
|
await spawnCommand(args);
|
|
@@ -4214,6 +5979,21 @@ async function main() {
|
|
|
4214
5979
|
await killCommand(args);
|
|
4215
5980
|
} else if (command === "restart") {
|
|
4216
5981
|
await restartCommand(args);
|
|
5982
|
+
} else if (command === "adopt") {
|
|
5983
|
+
adoptCommand(args);
|
|
5984
|
+
} else if (command === "supervisor") {
|
|
5985
|
+
printBanner();
|
|
5986
|
+
await supervisorCommand(args);
|
|
5987
|
+
} else if (command === "__supervisor") {
|
|
5988
|
+
await runSupervisor();
|
|
5989
|
+
} else if (command === "persist") {
|
|
5990
|
+
printBanner();
|
|
5991
|
+
await persistCommand(args);
|
|
5992
|
+
} else if (command === "inspect") {
|
|
5993
|
+
await inspectCommand(args);
|
|
5994
|
+
} else if (command === "dev") {
|
|
5995
|
+
printBanner();
|
|
5996
|
+
await devCommand(args);
|
|
4217
5997
|
} else if (command === "info") {
|
|
4218
5998
|
printBanner();
|
|
4219
5999
|
await infoCommand(args);
|
|
@@ -4252,7 +6032,11 @@ async function main() {
|
|
|
4252
6032
|
printBanner();
|
|
4253
6033
|
} else if (command === "help" || command === "--help" || command === "-h") {
|
|
4254
6034
|
printBanner();
|
|
4255
|
-
|
|
6035
|
+
if (args[0] && !args[0].startsWith("-")) {
|
|
6036
|
+
showCommandHelp(args[0]);
|
|
6037
|
+
} else {
|
|
6038
|
+
showHelp();
|
|
6039
|
+
}
|
|
4256
6040
|
} else {
|
|
4257
6041
|
console.error(renderError(`Unknown command: ${command}`, "Run 'fennec help' for usage information"));
|
|
4258
6042
|
process.exit(1);
|