@plumpslabs/fennec-cli 1.13.3 → 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 +2470 -668
- 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":
|
|
@@ -2560,127 +2789,1033 @@ function detectListeningPorts(_pid) {
|
|
|
2560
2789
|
}
|
|
2561
2790
|
function readSafe(path) {
|
|
2562
2791
|
try {
|
|
2563
|
-
return
|
|
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;
|
|
2564
3063
|
} catch {
|
|
2565
3064
|
return null;
|
|
2566
3065
|
}
|
|
2567
3066
|
}
|
|
2568
|
-
function
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
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);
|
|
3418
|
+
}
|
|
3419
|
+
}
|
|
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;
|
|
2576
3427
|
}
|
|
2577
|
-
|
|
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
|
+
`);
|
|
2578
3434
|
}
|
|
2579
|
-
function
|
|
3435
|
+
function supervisorStop() {
|
|
3436
|
+
const pid = getSupervisorPid();
|
|
3437
|
+
if (!pid) {
|
|
3438
|
+
console.error(`
|
|
3439
|
+
${pc4.dim("Supervisor is not running.")}
|
|
3440
|
+
`);
|
|
3441
|
+
return;
|
|
3442
|
+
}
|
|
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,33 +3993,67 @@ ${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;
|
|
4028
|
+
const useSse = args2.includes("--sse");
|
|
4029
|
+
if (useSse) {
|
|
4030
|
+
process.env.FENNEC_TRANSPORT_TYPE = "sse";
|
|
4031
|
+
}
|
|
2877
4032
|
try {
|
|
2878
4033
|
const server = new FennecServer(configPath);
|
|
2879
4034
|
await resurrectTracked();
|
|
2880
4035
|
await server.start();
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
${
|
|
4036
|
+
if (useSse) {
|
|
4037
|
+
const port = server.getConfig().transport.port;
|
|
4038
|
+
console.error(`
|
|
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`))}`);
|
|
4044
|
+
console.error(`
|
|
4045
|
+
${pc7.dim("Press Ctrl+C to stop")}
|
|
2887
4046
|
`);
|
|
4047
|
+
} else {
|
|
4048
|
+
console.error(`
|
|
4049
|
+
${pc7.green("\u2713")} ${pc7.bold("Fennec server is running")}`);
|
|
4050
|
+
console.error(` ${renderKV("Transport", "stdio")}`);
|
|
4051
|
+
console.error(` ${renderKV("AI Agent", "Connect via MCP protocol")}`);
|
|
4052
|
+
console.error(` ${renderKV("Tip", pc7.dim("For SSE mode: fennec start --sse"))}`);
|
|
4053
|
+
console.error(`
|
|
4054
|
+
${pc7.dim("Press Ctrl+C to stop")}
|
|
4055
|
+
`);
|
|
4056
|
+
}
|
|
2888
4057
|
} catch (error) {
|
|
2889
4058
|
console.error(renderError("Failed to start server", String(error)));
|
|
2890
4059
|
process.exit(1);
|
|
@@ -2892,7 +4061,7 @@ async function startServer(args2) {
|
|
|
2892
4061
|
}
|
|
2893
4062
|
async function startCommand(args2) {
|
|
2894
4063
|
printBanner();
|
|
2895
|
-
console.error(` ${
|
|
4064
|
+
console.error(` ${pc7.dim("Starting app...")}
|
|
2896
4065
|
`);
|
|
2897
4066
|
await runCommand(args2);
|
|
2898
4067
|
}
|
|
@@ -2902,8 +4071,9 @@ async function runCommand(args2) {
|
|
|
2902
4071
|
const portIndex = args2.indexOf("--port");
|
|
2903
4072
|
const port = portIndex !== -1 ? parseInt(args2[portIndex + 1], 10) : void 0;
|
|
2904
4073
|
const cwdIndex = args2.indexOf("--cwd");
|
|
2905
|
-
const cwd = cwdIndex !== -1 ? args2[cwdIndex + 1] :
|
|
4074
|
+
const cwd = cwdIndex !== -1 ? resolve4(args2[cwdIndex + 1]) : process.cwd();
|
|
2906
4075
|
const restartFlag = args2.includes("--restart");
|
|
4076
|
+
const jsonlFlag = args2.includes("--jsonl");
|
|
2907
4077
|
const stopFlags = [nameIndex, portIndex, cwdIndex].filter((i) => i !== -1);
|
|
2908
4078
|
const cmdEnd = Math.min(...stopFlags, Infinity);
|
|
2909
4079
|
const cmdParts = args2.slice(0, cmdEnd);
|
|
@@ -2913,87 +4083,82 @@ async function runCommand(args2) {
|
|
|
2913
4083
|
process.exit(1);
|
|
2914
4084
|
}
|
|
2915
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
|
+
}
|
|
2916
4096
|
const tracked = readTracked();
|
|
2917
4097
|
const existing = tracked.find((t) => t.name === appName);
|
|
2918
|
-
if (existing &&
|
|
4098
|
+
if (existing && isTrackedRunning(existing)) {
|
|
2919
4099
|
console.error();
|
|
2920
|
-
console.error(` ${
|
|
2921
|
-
console.error(` ${renderKV("Logs",
|
|
2922
|
-
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}`))}`);
|
|
2923
4103
|
console.error();
|
|
2924
4104
|
process.exit(0);
|
|
2925
4105
|
}
|
|
2926
|
-
const logDir =
|
|
2927
|
-
|
|
2928
|
-
const logFilePath =
|
|
4106
|
+
const logDir = dirname4(logFilePathFor(appName));
|
|
4107
|
+
mkdirSync4(logDir, { recursive: true });
|
|
4108
|
+
const logFilePath = logFilePathFor(appName);
|
|
2929
4109
|
console.error(`
|
|
2930
|
-
${symbols.fox} ${
|
|
4110
|
+
${symbols.fox} ${pc7.bold("Starting")} ${renderAppName(appName)} ${pc7.dim("(daemon)")}
|
|
2931
4111
|
`);
|
|
2932
4112
|
console.error(` ${renderKV("Command", cmd)}`);
|
|
2933
4113
|
if (port) console.error(` ${renderKV("Port", String(port))}`);
|
|
2934
4114
|
if (cwd) console.error(` ${renderKV("Directory", cwd)}`);
|
|
2935
|
-
if (restartFlag) console.error(` ${renderKV("Auto-restart",
|
|
4115
|
+
if (restartFlag) console.error(` ${renderKV("Auto-restart", pc7.green("enabled"))}`);
|
|
2936
4116
|
console.error(` ${divider()}`);
|
|
2937
4117
|
try {
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
});
|
|
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" });
|
|
2944
4123
|
const pid = currentChild.pid ?? 0;
|
|
2945
|
-
rotateLogFile(logFilePath);
|
|
2946
|
-
const logStream = createWriteStream(logFilePath, { flags: "a" });
|
|
2947
|
-
if (currentChild.stdout) currentChild.stdout.pipe(logStream);
|
|
2948
|
-
if (currentChild.stderr) currentChild.stderr.pipe(logStream);
|
|
2949
|
-
currentChild.unref();
|
|
2950
4124
|
addTracked({
|
|
2951
4125
|
name: appName,
|
|
2952
4126
|
pid,
|
|
2953
4127
|
command: cmd,
|
|
4128
|
+
args: cmdParts,
|
|
2954
4129
|
port,
|
|
2955
4130
|
cwd,
|
|
2956
|
-
|
|
4131
|
+
env: startEnv,
|
|
4132
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4133
|
+
autoRestart: restartFlag,
|
|
4134
|
+
logMode: jsonlFlag ? "jsonl" : "text"
|
|
2957
4135
|
});
|
|
2958
|
-
console.error(` ${
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
addTracked({ name: appName, pid: newPid, command: cmd, port, cwd, startedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
2977
|
-
console.error(` ${pc5.green("\u2713")} ${pc5.bold(appName)} ${pc5.dim(`restarted (PID: ${newPid})`)}`);
|
|
2978
|
-
currentChild = restarted;
|
|
2979
|
-
setupRestartWatch2(restarted);
|
|
2980
|
-
}
|
|
2981
|
-
});
|
|
2982
|
-
};
|
|
2983
|
-
var setupRestartWatch = setupRestartWatch2;
|
|
2984
|
-
console.error(` ${pc5.dim("Auto-restart enabled \u2014 watching for crashes...")}
|
|
2985
|
-
`);
|
|
2986
|
-
setupRestartWatch2(currentChild);
|
|
2987
|
-
await new Promise((resolve12) => {
|
|
2988
|
-
process.once("SIGINT", () => {
|
|
2989
|
-
currentChild.kill("SIGTERM");
|
|
2990
|
-
resolve12();
|
|
2991
|
-
});
|
|
2992
|
-
});
|
|
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
|
+
}
|
|
2993
4154
|
}
|
|
2994
|
-
if (
|
|
2995
|
-
|
|
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"))}`);
|
|
2996
4160
|
}
|
|
4161
|
+
await psCommand([]);
|
|
2997
4162
|
} catch (error) {
|
|
2998
4163
|
console.error(renderError(`Failed to start ${appName}`, String(error)));
|
|
2999
4164
|
process.exit(1);
|
|
@@ -3002,7 +4167,7 @@ async function runCommand(args2) {
|
|
|
3002
4167
|
async function resurrectTracked() {
|
|
3003
4168
|
const tracked = readTracked();
|
|
3004
4169
|
if (tracked.length === 0) return;
|
|
3005
|
-
const dead = tracked.filter((t) => !
|
|
4170
|
+
const dead = tracked.filter((t) => !isTrackedRunning(t));
|
|
3006
4171
|
if (dead.length === 0) return;
|
|
3007
4172
|
const spinner = createSpinner(`Resurrecting ${dead.length} stopped process(es)...`);
|
|
3008
4173
|
let resurrected = 0;
|
|
@@ -3013,28 +4178,21 @@ async function resurrectTracked() {
|
|
|
3013
4178
|
continue;
|
|
3014
4179
|
}
|
|
3015
4180
|
try {
|
|
3016
|
-
const cmdParts = proc
|
|
3017
|
-
const
|
|
3018
|
-
|
|
3019
|
-
const logFilePath = resolve3(logDir, `${proc.name}.log`);
|
|
3020
|
-
const child = spawn(cmdParts[0], cmdParts.slice(1), {
|
|
3021
|
-
cwd: proc.cwd,
|
|
3022
|
-
env: { ...process.env },
|
|
3023
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
3024
|
-
detached: true
|
|
3025
|
-
});
|
|
3026
|
-
rotateLogFile(logFilePath);
|
|
3027
|
-
const logStream = createWriteStream(logFilePath, { flags: "a" });
|
|
3028
|
-
if (child.stdout) child.stdout.pipe(logStream);
|
|
3029
|
-
if (child.stderr) child.stderr.pipe(logStream);
|
|
3030
|
-
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 });
|
|
3031
4184
|
addTracked({
|
|
3032
4185
|
name: proc.name,
|
|
3033
4186
|
pid: child.pid ?? 0,
|
|
3034
4187
|
command: proc.command,
|
|
4188
|
+
args: cmdParts,
|
|
3035
4189
|
port: proc.port,
|
|
3036
4190
|
cwd: proc.cwd,
|
|
3037
|
-
|
|
4191
|
+
env: proc.env,
|
|
4192
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4193
|
+
autoRestart: proc.autoRestart,
|
|
4194
|
+
restartCause: proc.restartCause,
|
|
4195
|
+
logMode: proc.logMode
|
|
3038
4196
|
});
|
|
3039
4197
|
resurrected++;
|
|
3040
4198
|
} catch {
|
|
@@ -3044,15 +4202,15 @@ async function resurrectTracked() {
|
|
|
3044
4202
|
spinner.stop();
|
|
3045
4203
|
process.stdout.write("\r\x1B[K");
|
|
3046
4204
|
if (resurrected > 0) {
|
|
3047
|
-
console.error(` ${
|
|
4205
|
+
console.error(` ${pc7.green("\u25CF")} ${pc7.bold(`Resurrected ${resurrected} process(es)`)}`);
|
|
3048
4206
|
}
|
|
3049
4207
|
if (failed > 0) {
|
|
3050
|
-
console.error(` ${
|
|
4208
|
+
console.error(` ${pc7.red("\u25CF")} ${pc7.bold(`${failed} process(es) failed to resurrect`)}`);
|
|
3051
4209
|
}
|
|
3052
4210
|
}
|
|
3053
4211
|
|
|
3054
4212
|
// src/commands/kill.ts
|
|
3055
|
-
import
|
|
4213
|
+
import pc8 from "picocolors";
|
|
3056
4214
|
async function killCommand(args2) {
|
|
3057
4215
|
const rawTarget = args2[0];
|
|
3058
4216
|
const signalIndex = args2.indexOf("--signal");
|
|
@@ -3079,21 +4237,29 @@ async function killCommand(args2) {
|
|
|
3079
4237
|
} else {
|
|
3080
4238
|
const tracked = readTracked();
|
|
3081
4239
|
const trackedMatch = tracked.find((t) => t.name === rawTarget);
|
|
3082
|
-
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
|
+
}
|
|
3083
4247
|
targetPid = trackedMatch.pid;
|
|
3084
4248
|
displayName = `${trackedMatch.name} (PID ${targetPid})`;
|
|
3085
4249
|
} else {
|
|
3086
|
-
const
|
|
4250
|
+
const currentPid = process.pid;
|
|
4251
|
+
let matches = getSystemProcesses({ name: rawTarget, userOnly: true, sortBy: "cpu" });
|
|
4252
|
+
matches = matches.filter((m) => m.pid !== currentPid);
|
|
3087
4253
|
if (matches.length === 0) {
|
|
3088
4254
|
console.error(renderError("Process not found", `No running process matching "${rawTarget}"`));
|
|
3089
4255
|
process.exit(1);
|
|
3090
4256
|
}
|
|
3091
4257
|
if (matches.length > 1) {
|
|
3092
4258
|
console.error(`
|
|
3093
|
-
${
|
|
4259
|
+
${pc8.bold(`Multiple processes match "${rawTarget}":`)}`);
|
|
3094
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}%` })));
|
|
3095
4261
|
if (selected === null) {
|
|
3096
|
-
console.error(` ${
|
|
4262
|
+
console.error(` ${pc8.dim("Cancelled")}`);
|
|
3097
4263
|
return;
|
|
3098
4264
|
}
|
|
3099
4265
|
const idx = parseInt(selected, 10);
|
|
@@ -3105,9 +4271,10 @@ async function killCommand(args2) {
|
|
|
3105
4271
|
}
|
|
3106
4272
|
}
|
|
3107
4273
|
}
|
|
3108
|
-
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);
|
|
3109
4276
|
if (!confirmed) {
|
|
3110
|
-
console.error(` ${
|
|
4277
|
+
console.error(` ${pc8.dim("Cancelled")}`);
|
|
3111
4278
|
return;
|
|
3112
4279
|
}
|
|
3113
4280
|
const spinner = createSpinner(`Sending ${signal} to ${displayName}...`);
|
|
@@ -3117,16 +4284,16 @@ async function killCommand(args2) {
|
|
|
3117
4284
|
const stillRunning = isProcessRunning(targetPid);
|
|
3118
4285
|
if (stillRunning && signal !== "SIGKILL") {
|
|
3119
4286
|
spinner.warn(`${displayName} did not respond to ${signal}`);
|
|
3120
|
-
const forceKill = await confirmPrompt(`Send ${
|
|
4287
|
+
const forceKill = force || await confirmPrompt(`Send ${pc8.red("SIGKILL")} to force stop?`, true);
|
|
3121
4288
|
if (forceKill) {
|
|
3122
4289
|
const forceSpinner = createSpinner(`Sending SIGKILL to ${displayName}...`);
|
|
3123
4290
|
killProcess(targetPid, "SIGKILL");
|
|
3124
4291
|
await new Promise((r) => setTimeout(r, 300));
|
|
3125
4292
|
isProcessRunning(targetPid) ? forceSpinner.fail(`${displayName} could not be stopped (permission denied?)`) : forceSpinner.succeed(`${displayName} force stopped`);
|
|
3126
4293
|
} else {
|
|
3127
|
-
console.error(` ${
|
|
4294
|
+
console.error(` ${pc8.dim("Retrying with SIGTERM...")}`);
|
|
3128
4295
|
killProcess(targetPid, "SIGTERM");
|
|
3129
|
-
console.error(` ${
|
|
4296
|
+
console.error(` ${pc8.yellow("\u26A0")} ${displayName} ${pc8.dim("may still be running")}`);
|
|
3130
4297
|
}
|
|
3131
4298
|
} else {
|
|
3132
4299
|
removeTrackedByPid(targetPid);
|
|
@@ -3140,17 +4307,17 @@ async function killCommand(args2) {
|
|
|
3140
4307
|
async function killAll(signal) {
|
|
3141
4308
|
const userProcs = getSystemProcesses({ userOnly: true, sortBy: "cpu", limit: 200 });
|
|
3142
4309
|
if (userProcs.length === 0) {
|
|
3143
|
-
console.error(` ${
|
|
4310
|
+
console.error(` ${pc8.dim("No user processes to kill.")}`);
|
|
3144
4311
|
return;
|
|
3145
4312
|
}
|
|
3146
4313
|
console.error(`
|
|
3147
|
-
${
|
|
3148
|
-
console.error(` ${
|
|
3149
|
-
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.")}
|
|
3150
4317
|
`);
|
|
3151
|
-
const confirmed = await confirmPrompt(`${
|
|
4318
|
+
const confirmed = await confirmPrompt(`${pc8.red("Are you sure?")} ${pc8.dim("This cannot be undone")}`, false);
|
|
3152
4319
|
if (!confirmed) {
|
|
3153
|
-
console.error(` ${
|
|
4320
|
+
console.error(` ${pc8.dim("Cancelled.")}`);
|
|
3154
4321
|
return;
|
|
3155
4322
|
}
|
|
3156
4323
|
const spinner = createSpinner(`Killing ${userProcs.length} processes...`);
|
|
@@ -3166,15 +4333,15 @@ async function killAll(signal) {
|
|
|
3166
4333
|
}
|
|
3167
4334
|
await new Promise((r) => setTimeout(r, 500));
|
|
3168
4335
|
killed > 0 ? spinner.succeed(`${killed} process(es) killed`) : spinner.fail("Failed to kill processes");
|
|
3169
|
-
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)")}`);
|
|
3170
4337
|
}
|
|
3171
4338
|
|
|
3172
4339
|
// src/commands/stop.ts
|
|
3173
|
-
import
|
|
4340
|
+
import pc9 from "picocolors";
|
|
3174
4341
|
async function stopCommand(args2) {
|
|
3175
4342
|
const stopAll = args2.includes("--all") || args2.includes("-a");
|
|
3176
4343
|
if (stopAll) {
|
|
3177
|
-
await stopAllTracked();
|
|
4344
|
+
await stopAllTracked(args2);
|
|
3178
4345
|
return;
|
|
3179
4346
|
}
|
|
3180
4347
|
const rawTarget = args2[0];
|
|
@@ -3185,42 +4352,45 @@ async function stopCommand(args2) {
|
|
|
3185
4352
|
const tracked = readTracked();
|
|
3186
4353
|
const trackedMatch = tracked.find((t) => t.name === rawTarget);
|
|
3187
4354
|
if (!trackedMatch) {
|
|
3188
|
-
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.`));
|
|
3189
4356
|
process.exit(1);
|
|
3190
4357
|
}
|
|
3191
|
-
if (!
|
|
4358
|
+
if (!isTrackedRunning(trackedMatch)) {
|
|
3192
4359
|
console.error(`
|
|
3193
|
-
${
|
|
4360
|
+
${pc9.yellow("\u26A0")} ${pc9.bold(rawTarget)} ${pc9.dim("is already stopped")}`);
|
|
3194
4361
|
console.error();
|
|
3195
4362
|
process.exit(0);
|
|
3196
4363
|
}
|
|
3197
4364
|
const displayName = `${trackedMatch.name} (PID ${trackedMatch.pid})`;
|
|
3198
|
-
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);
|
|
3199
4367
|
if (!confirmed) {
|
|
3200
|
-
console.error(` ${
|
|
4368
|
+
console.error(` ${pc9.dim("Cancelled")}`);
|
|
3201
4369
|
return;
|
|
3202
4370
|
}
|
|
4371
|
+
setAutoRestart(trackedMatch.name, false);
|
|
3203
4372
|
await stopSingleProcess(trackedMatch.pid, trackedMatch.name);
|
|
3204
4373
|
}
|
|
3205
|
-
async function stopAllTracked() {
|
|
4374
|
+
async function stopAllTracked(args2) {
|
|
3206
4375
|
const tracked = readTracked();
|
|
3207
|
-
const running = tracked.filter((t) =>
|
|
4376
|
+
const running = tracked.filter((t) => isTrackedRunning(t));
|
|
3208
4377
|
if (running.length === 0) {
|
|
3209
4378
|
console.error(`
|
|
3210
|
-
${
|
|
4379
|
+
${pc9.dim("No running tracked processes to stop.")}
|
|
3211
4380
|
`);
|
|
3212
4381
|
return;
|
|
3213
4382
|
}
|
|
3214
4383
|
console.error(`
|
|
3215
|
-
${
|
|
4384
|
+
${pc9.yellow("\u26A0")} ${pc9.bold(`Stop all ${running.length} running process(es)?`)} ${pc9.dim("(They can be re-spawned later)")}
|
|
3216
4385
|
`);
|
|
3217
4386
|
for (const t of running) {
|
|
3218
|
-
console.error(` ${
|
|
4387
|
+
console.error(` ${pc9.green("\u25CF")} ${pc9.bold(t.name)} ${pc9.dim(`(PID ${t.pid})`)}`);
|
|
3219
4388
|
}
|
|
3220
4389
|
console.error();
|
|
3221
|
-
const
|
|
4390
|
+
const forceAll = args2.includes("-y") || args2.includes("--yes");
|
|
4391
|
+
const confirmed = forceAll || await confirmPrompt("Stop all?", false);
|
|
3222
4392
|
if (!confirmed) {
|
|
3223
|
-
console.error(` ${
|
|
4393
|
+
console.error(` ${pc9.dim("Cancelled")}
|
|
3224
4394
|
`);
|
|
3225
4395
|
return;
|
|
3226
4396
|
}
|
|
@@ -3228,6 +4398,7 @@ async function stopAllTracked() {
|
|
|
3228
4398
|
let stopped = 0;
|
|
3229
4399
|
let failed = 0;
|
|
3230
4400
|
for (const t of running) {
|
|
4401
|
+
setAutoRestart(t.name, false);
|
|
3231
4402
|
if (killProcess(t.pid, "SIGTERM")) {
|
|
3232
4403
|
stopped++;
|
|
3233
4404
|
} else {
|
|
@@ -3238,10 +4409,10 @@ async function stopAllTracked() {
|
|
|
3238
4409
|
spinner.stop();
|
|
3239
4410
|
process.stdout.write("\r\x1B[K");
|
|
3240
4411
|
if (stopped > 0) {
|
|
3241
|
-
console.error(` ${
|
|
4412
|
+
console.error(` ${pc9.green("\u2713")} ${pc9.bold(`Stopped ${stopped} process(es)`)} ${pc9.dim("(kept in tracked.json)")}`);
|
|
3242
4413
|
}
|
|
3243
4414
|
if (failed > 0) {
|
|
3244
|
-
console.error(` ${
|
|
4415
|
+
console.error(` ${pc9.red("\u2717")} ${pc9.bold(`${failed} process(es) failed to stop`)}`);
|
|
3245
4416
|
}
|
|
3246
4417
|
console.error();
|
|
3247
4418
|
await psCommand([]);
|
|
@@ -3255,14 +4426,14 @@ async function stopSingleProcess(pid, name) {
|
|
|
3255
4426
|
const stillRunning = isProcessRunning(pid);
|
|
3256
4427
|
if (stillRunning) {
|
|
3257
4428
|
spinner.warn(`${displayName} did not respond to SIGTERM`);
|
|
3258
|
-
const forceKill = await confirmPrompt(`Send ${
|
|
4429
|
+
const forceKill = await confirmPrompt(`Send ${pc9.red("SIGKILL")} to force stop?`, true);
|
|
3259
4430
|
if (forceKill) {
|
|
3260
4431
|
const forceSpinner = createSpinner(`Sending SIGKILL to ${displayName}...`);
|
|
3261
4432
|
killProcess(pid, "SIGKILL");
|
|
3262
4433
|
await new Promise((r) => setTimeout(r, 300));
|
|
3263
4434
|
isProcessRunning(pid) ? forceSpinner.fail(`${displayName} could not be stopped (permission denied?)`) : forceSpinner.succeed(`${displayName} force stopped`);
|
|
3264
4435
|
} else {
|
|
3265
|
-
console.error(` ${
|
|
4436
|
+
console.error(` ${pc9.yellow("\u26A0")} ${displayName} ${pc9.dim("may still be running")}`);
|
|
3266
4437
|
}
|
|
3267
4438
|
} else {
|
|
3268
4439
|
spinner.succeed(`${displayName} stopped`);
|
|
@@ -3276,11 +4447,10 @@ async function stopSingleProcess(pid, name) {
|
|
|
3276
4447
|
}
|
|
3277
4448
|
|
|
3278
4449
|
// src/commands/spawn.ts
|
|
3279
|
-
import
|
|
3280
|
-
import {
|
|
3281
|
-
import { resolve as
|
|
4450
|
+
import pc10 from "picocolors";
|
|
4451
|
+
import { mkdirSync as mkdirSync5 } from "fs";
|
|
4452
|
+
import { resolve as resolve5 } from "path";
|
|
3282
4453
|
import { homedir as homedir3 } from "os";
|
|
3283
|
-
import { spawn as spawn2 } from "child_process";
|
|
3284
4454
|
async function spawnCommand(args2) {
|
|
3285
4455
|
const spawnAll = args2.includes("--all") || args2.includes("-a");
|
|
3286
4456
|
const name = args2[0];
|
|
@@ -3295,13 +4465,13 @@ async function spawnCommand(args2) {
|
|
|
3295
4465
|
const tracked = readTracked();
|
|
3296
4466
|
const match = tracked.find((t) => t.name === name);
|
|
3297
4467
|
if (!match) {
|
|
3298
|
-
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.`));
|
|
3299
4469
|
process.exit(1);
|
|
3300
4470
|
}
|
|
3301
|
-
if (
|
|
4471
|
+
if (isTrackedRunning(match)) {
|
|
3302
4472
|
console.error(`
|
|
3303
|
-
${
|
|
3304
|
-
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.")}`);
|
|
3305
4475
|
console.error();
|
|
3306
4476
|
process.exit(0);
|
|
3307
4477
|
}
|
|
@@ -3313,18 +4483,18 @@ async function spawnCommand(args2) {
|
|
|
3313
4483
|
}
|
|
3314
4484
|
async function spawnAllStopped() {
|
|
3315
4485
|
const tracked = readTracked();
|
|
3316
|
-
const toSpawn = tracked.filter((t) => !
|
|
4486
|
+
const toSpawn = tracked.filter((t) => !isTrackedRunning(t) && t.command);
|
|
3317
4487
|
if (toSpawn.length === 0) {
|
|
3318
4488
|
console.error(`
|
|
3319
|
-
${
|
|
4489
|
+
${pc10.dim("No stopped processes with saved commands to spawn.")}
|
|
3320
4490
|
`);
|
|
3321
4491
|
return;
|
|
3322
4492
|
}
|
|
3323
4493
|
console.error(`
|
|
3324
|
-
${symbols.fox} ${
|
|
4494
|
+
${symbols.fox} ${pc10.bold(`Spawning ${toSpawn.length} process(es)...`)}
|
|
3325
4495
|
`);
|
|
3326
4496
|
for (const t of toSpawn) {
|
|
3327
|
-
console.error(` ${
|
|
4497
|
+
console.error(` ${pc10.dim("\xB7")} ${pc10.bold(t.name)} ${pc10.dim(`(${t.command})`)}`);
|
|
3328
4498
|
}
|
|
3329
4499
|
console.error();
|
|
3330
4500
|
const spinner = createSpinner("Spawning...");
|
|
@@ -3332,30 +4502,28 @@ async function spawnAllStopped() {
|
|
|
3332
4502
|
let failed = 0;
|
|
3333
4503
|
for (const proc of toSpawn) {
|
|
3334
4504
|
try {
|
|
3335
|
-
const cmdParts = proc
|
|
3336
|
-
const logDir =
|
|
3337
|
-
|
|
3338
|
-
const logFilePath =
|
|
3339
|
-
const child =
|
|
3340
|
-
cwd: proc.cwd,
|
|
3341
|
-
env: { ...process.env },
|
|
3342
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
3343
|
-
detached: true
|
|
3344
|
-
});
|
|
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 });
|
|
3345
4510
|
const pid = child.pid ?? 0;
|
|
3346
|
-
rotateLogFile(logFilePath);
|
|
3347
|
-
const logStream = createWriteStream2(logFilePath, { flags: "a" });
|
|
3348
|
-
if (child.stdout) child.stdout.pipe(logStream);
|
|
3349
|
-
if (child.stderr) child.stderr.pipe(logStream);
|
|
3350
|
-
child.unref();
|
|
3351
4511
|
addTracked({
|
|
3352
4512
|
name: proc.name,
|
|
3353
4513
|
pid,
|
|
3354
4514
|
command: proc.command,
|
|
4515
|
+
args: cmdParts,
|
|
3355
4516
|
port: proc.port,
|
|
3356
4517
|
cwd: proc.cwd,
|
|
3357
|
-
|
|
4518
|
+
env: proc.env,
|
|
4519
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4520
|
+
autoRestart: proc.autoRestart,
|
|
4521
|
+
logMode: proc.logMode
|
|
3358
4522
|
});
|
|
4523
|
+
if (proc.autoRestart) {
|
|
4524
|
+
ensureSupervisorRunning();
|
|
4525
|
+
ensurePersistEnabled();
|
|
4526
|
+
}
|
|
3359
4527
|
spawned++;
|
|
3360
4528
|
} catch {
|
|
3361
4529
|
failed++;
|
|
@@ -3364,10 +4532,10 @@ async function spawnAllStopped() {
|
|
|
3364
4532
|
spinner.stop();
|
|
3365
4533
|
process.stdout.write("\r\x1B[K");
|
|
3366
4534
|
if (spawned > 0) {
|
|
3367
|
-
console.error(` ${
|
|
4535
|
+
console.error(` ${pc10.green("\u2713")} ${pc10.bold(`Spawned ${spawned} process(es)`)}`);
|
|
3368
4536
|
}
|
|
3369
4537
|
if (failed > 0) {
|
|
3370
|
-
console.error(` ${
|
|
4538
|
+
console.error(` ${pc10.red("\u2717")} ${pc10.bold(`${failed} process(es) failed to spawn`)}`);
|
|
3371
4539
|
}
|
|
3372
4540
|
console.error();
|
|
3373
4541
|
await psCommand([]);
|
|
@@ -3376,25 +4544,25 @@ async function spawnList() {
|
|
|
3376
4544
|
const tracked = readTracked();
|
|
3377
4545
|
if (tracked.length === 0) {
|
|
3378
4546
|
console.error(`
|
|
3379
|
-
${
|
|
3380
|
-
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>")}`);
|
|
3381
4549
|
console.error();
|
|
3382
4550
|
return;
|
|
3383
4551
|
}
|
|
3384
|
-
const stopped = tracked.filter((t) => !
|
|
3385
|
-
const running = tracked.filter((t) =>
|
|
4552
|
+
const stopped = tracked.filter((t) => !isTrackedRunning(t));
|
|
4553
|
+
const running = tracked.filter((t) => isTrackedRunning(t));
|
|
3386
4554
|
if (stopped.length === 0) {
|
|
3387
4555
|
console.error(`
|
|
3388
|
-
${
|
|
4556
|
+
${pc10.dim("All tracked processes are running. Nothing to spawn.")}`);
|
|
3389
4557
|
console.error();
|
|
3390
4558
|
return;
|
|
3391
4559
|
}
|
|
3392
4560
|
console.error(`
|
|
3393
|
-
${symbols.fox} ${
|
|
4561
|
+
${symbols.fox} ${pc10.bold("Available to spawn")} ${pc10.dim(`(${stopped.length}/${tracked.length} stopped)`)}`);
|
|
3394
4562
|
if (running.length > 0) {
|
|
3395
|
-
console.error(` ${
|
|
4563
|
+
console.error(` ${pc10.dim("Running (use stop first):")}`);
|
|
3396
4564
|
for (const r of running) {
|
|
3397
|
-
console.error(` ${
|
|
4565
|
+
console.error(` ${pc10.green("\u25CF")} ${pc10.bold(r.name)} ${pc10.dim(`(PID ${r.pid})`)}`);
|
|
3398
4566
|
}
|
|
3399
4567
|
console.error();
|
|
3400
4568
|
}
|
|
@@ -3404,7 +4572,7 @@ async function spawnList() {
|
|
|
3404
4572
|
description: t.command.length > 80 ? t.command.slice(0, 80) + "..." : t.command
|
|
3405
4573
|
})));
|
|
3406
4574
|
if (selected === null) {
|
|
3407
|
-
console.error(` ${
|
|
4575
|
+
console.error(` ${pc10.dim("Cancelled")}`);
|
|
3408
4576
|
return;
|
|
3409
4577
|
}
|
|
3410
4578
|
const match = stopped.find((t) => t.name === selected);
|
|
@@ -3413,38 +4581,36 @@ async function spawnList() {
|
|
|
3413
4581
|
}
|
|
3414
4582
|
}
|
|
3415
4583
|
async function respawnProcess(proc) {
|
|
3416
|
-
const cmdParts = proc
|
|
3417
|
-
const logDir =
|
|
3418
|
-
|
|
3419
|
-
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`);
|
|
3420
4588
|
console.error(`
|
|
3421
|
-
${symbols.fox} ${
|
|
4589
|
+
${symbols.fox} ${pc10.bold("Spawning")} ${renderAppName(proc.name)} ${pc10.dim("(from saved config)")}
|
|
3422
4590
|
`);
|
|
3423
4591
|
console.error(` ${renderKV("Command", proc.command)}`);
|
|
3424
4592
|
if (proc.cwd) console.error(` ${renderKV("Directory", proc.cwd)}`);
|
|
3425
4593
|
if (proc.port) console.error(` ${renderKV("Port", String(proc.port))}`);
|
|
3426
4594
|
try {
|
|
3427
|
-
const child =
|
|
3428
|
-
cwd: proc.cwd,
|
|
3429
|
-
env: { ...process.env },
|
|
3430
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
3431
|
-
detached: true
|
|
3432
|
-
});
|
|
4595
|
+
const child = spawnDaemon({ cmdParts, name: proc.name, cwd: proc.cwd, logFilePath, env: buildSpawnEnv(proc.env), logMode: proc.logMode });
|
|
3433
4596
|
const pid = child.pid ?? 0;
|
|
3434
|
-
rotateLogFile(logFilePath);
|
|
3435
|
-
const logStream = createWriteStream2(logFilePath, { flags: "a" });
|
|
3436
|
-
if (child.stdout) child.stdout.pipe(logStream);
|
|
3437
|
-
if (child.stderr) child.stderr.pipe(logStream);
|
|
3438
|
-
child.unref();
|
|
3439
4597
|
addTracked({
|
|
3440
4598
|
name: proc.name,
|
|
3441
4599
|
pid,
|
|
3442
4600
|
command: proc.command,
|
|
4601
|
+
args: cmdParts,
|
|
3443
4602
|
port: proc.port,
|
|
3444
4603
|
cwd: proc.cwd,
|
|
3445
|
-
|
|
4604
|
+
env: proc.env,
|
|
4605
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4606
|
+
autoRestart: proc.autoRestart,
|
|
4607
|
+
logMode: proc.logMode
|
|
3446
4608
|
});
|
|
3447
|
-
|
|
4609
|
+
if (proc.autoRestart) {
|
|
4610
|
+
ensureSupervisorRunning();
|
|
4611
|
+
ensurePersistEnabled();
|
|
4612
|
+
}
|
|
4613
|
+
console.error(` ${pc10.green("\u2713")} ${pc10.bold(proc.name)} ${pc10.dim(`spawned (PID: ${pid})`)}`);
|
|
3448
4614
|
await psCommand([]);
|
|
3449
4615
|
} catch (error) {
|
|
3450
4616
|
console.error(renderError(`Failed to spawn ${proc.name}`, String(error)));
|
|
@@ -3453,11 +4619,10 @@ async function respawnProcess(proc) {
|
|
|
3453
4619
|
}
|
|
3454
4620
|
|
|
3455
4621
|
// src/commands/restart.ts
|
|
3456
|
-
import
|
|
3457
|
-
import { mkdirSync as
|
|
3458
|
-
import { resolve as
|
|
4622
|
+
import pc11 from "picocolors";
|
|
4623
|
+
import { mkdirSync as mkdirSync6 } from "fs";
|
|
4624
|
+
import { resolve as resolve6 } from "path";
|
|
3459
4625
|
import { homedir as homedir4 } from "os";
|
|
3460
|
-
import { spawn as spawn3 } from "child_process";
|
|
3461
4626
|
async function restartCommand(args2) {
|
|
3462
4627
|
const name = args2[0];
|
|
3463
4628
|
if (!name) {
|
|
@@ -3482,9 +4647,9 @@ async function restartCommand(args2) {
|
|
|
3482
4647
|
targetPid = processes[0].pid;
|
|
3483
4648
|
}
|
|
3484
4649
|
}
|
|
3485
|
-
const confirmed = await confirmPrompt(`Restart ${
|
|
4650
|
+
const confirmed = await confirmPrompt(`Restart ${pc11.bold(`${name} (PID ${targetPid})`)}?`, false);
|
|
3486
4651
|
if (!confirmed) {
|
|
3487
|
-
console.error(` ${
|
|
4652
|
+
console.error(` ${pc11.dim("Cancelled")}`);
|
|
3488
4653
|
return;
|
|
3489
4654
|
}
|
|
3490
4655
|
const spinner = createSpinner(`Stopping PID ${targetPid}...`);
|
|
@@ -3503,86 +4668,581 @@ async function restartCommand(args2) {
|
|
|
3503
4668
|
if (trackedEntry) {
|
|
3504
4669
|
const respawnSpinner = createSpinner(`Re-spawning ${trackedEntry.name}...`);
|
|
3505
4670
|
try {
|
|
3506
|
-
const cmdParts = trackedEntry
|
|
3507
|
-
const logDir =
|
|
3508
|
-
|
|
3509
|
-
const logFilePath =
|
|
3510
|
-
const child =
|
|
3511
|
-
cwd: trackedEntry.cwd,
|
|
3512
|
-
env: { ...process.env },
|
|
3513
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
3514
|
-
detached: true
|
|
3515
|
-
});
|
|
3516
|
-
rotateLogFile(logFilePath);
|
|
3517
|
-
const logStream = createWriteStream3(logFilePath, { flags: "a" });
|
|
3518
|
-
if (child.stdout) child.stdout.pipe(logStream);
|
|
3519
|
-
if (child.stderr) child.stderr.pipe(logStream);
|
|
3520
|
-
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 });
|
|
3521
4676
|
removeTrackedByPid(targetPid);
|
|
3522
4677
|
addTracked({
|
|
3523
4678
|
name: trackedEntry.name,
|
|
3524
4679
|
pid: child.pid ?? 0,
|
|
3525
4680
|
command: trackedEntry.command,
|
|
4681
|
+
args: cmdParts,
|
|
3526
4682
|
port: trackedEntry.port,
|
|
3527
4683
|
cwd: trackedEntry.cwd,
|
|
3528
|
-
|
|
4684
|
+
env: trackedEntry.env,
|
|
4685
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4686
|
+
autoRestart: trackedEntry.autoRestart,
|
|
4687
|
+
logMode: trackedEntry.logMode
|
|
3529
4688
|
});
|
|
3530
4689
|
respawnSpinner.succeed(`${trackedEntry.name} restarted (PID: ${child.pid})`);
|
|
3531
4690
|
} catch (error) {
|
|
3532
4691
|
respawnSpinner.fail(`Failed to re-spawn ${trackedEntry.name}: ${String(error)}`);
|
|
3533
|
-
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 {
|
|
3534
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);
|
|
3535
4825
|
} else {
|
|
3536
|
-
|
|
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}`);
|
|
3537
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;
|
|
3538
5190
|
}
|
|
3539
5191
|
|
|
3540
5192
|
// src/commands/log.ts
|
|
3541
|
-
import { existsSync as
|
|
3542
|
-
import {
|
|
3543
|
-
import
|
|
3544
|
-
|
|
3545
|
-
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;
|
|
3546
5197
|
async function logCommand(args2) {
|
|
3547
5198
|
const target = args2[0];
|
|
3548
|
-
if (!target) {
|
|
3549
|
-
|
|
3550
|
-
|
|
5199
|
+
if (!target || target.startsWith("--")) {
|
|
5200
|
+
listAvailableApps();
|
|
5201
|
+
return;
|
|
3551
5202
|
}
|
|
3552
5203
|
const clearFlag = args2.includes("--clear");
|
|
5204
|
+
const jsonFlag = args2.includes("--json");
|
|
5205
|
+
const noRedact = args2.includes("--no-redact");
|
|
3553
5206
|
const linesIndex = args2.indexOf("--lines");
|
|
3554
|
-
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;
|
|
3555
5208
|
const followFlag = args2.includes("-f") || args2.includes("--follow");
|
|
3556
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
|
+
}
|
|
3557
5216
|
if (levelFilter && !["error", "warn", "info", "debug"].includes(levelFilter)) {
|
|
3558
5217
|
console.error(renderError("Invalid level", `"${levelFilter}" is not a valid level. Use: error, warn, info, debug`));
|
|
3559
5218
|
process.exit(1);
|
|
3560
5219
|
}
|
|
5220
|
+
const redact = !noRedact;
|
|
3561
5221
|
let logFilePath = null;
|
|
3562
5222
|
let displayName = target;
|
|
3563
5223
|
const tracked = readTracked();
|
|
3564
5224
|
const trackedMatch = tracked.find((t) => t.name === target || parseInt(target, 10) === t.pid && !isNaN(parseInt(target, 10)));
|
|
3565
5225
|
if (trackedMatch) {
|
|
3566
5226
|
displayName = trackedMatch.name;
|
|
3567
|
-
logFilePath =
|
|
5227
|
+
logFilePath = logFilePathFor(trackedMatch.name);
|
|
3568
5228
|
} else {
|
|
3569
5229
|
const pid = parseInt(target, 10);
|
|
3570
5230
|
if (!isNaN(pid) && String(pid) === target) {
|
|
3571
5231
|
const procPath = `/proc/${pid}/fd/1`;
|
|
3572
|
-
if (
|
|
5232
|
+
if (existsSync9(procPath)) logFilePath = procPath;
|
|
3573
5233
|
}
|
|
3574
5234
|
}
|
|
3575
5235
|
if (clearFlag) {
|
|
3576
|
-
if (!logFilePath || !
|
|
5236
|
+
if (!logFilePath || !existsSync9(logFilePath)) {
|
|
3577
5237
|
console.error(`
|
|
3578
|
-
${
|
|
5238
|
+
${pc15.yellow("\u26A0")} ${pc15.dim("No log file found for")} ${pc15.bold(displayName)}
|
|
3579
5239
|
`);
|
|
3580
5240
|
process.exit(0);
|
|
3581
5241
|
}
|
|
3582
5242
|
try {
|
|
3583
|
-
|
|
5243
|
+
unlinkSync3(logFilePath);
|
|
3584
5244
|
console.error(`
|
|
3585
|
-
${
|
|
5245
|
+
${pc15.green("\u2713")} ${pc15.bold("Log cleared")} ${pc15.dim(`\u2014 ${logFilePath}`)}
|
|
3586
5246
|
`);
|
|
3587
5247
|
} catch (err) {
|
|
3588
5248
|
console.error(renderError("Failed to clear log", String(err)));
|
|
@@ -3590,18 +5250,75 @@ async function logCommand(args2) {
|
|
|
3590
5250
|
}
|
|
3591
5251
|
return;
|
|
3592
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
|
+
}
|
|
3593
5306
|
const spinner = createSpinner(`Reading logs for ${displayName}...`);
|
|
3594
5307
|
try {
|
|
3595
5308
|
let logLines = [];
|
|
3596
|
-
if (logFilePath &&
|
|
3597
|
-
|
|
3598
|
-
|
|
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
|
+
});
|
|
3599
5316
|
} else {
|
|
3600
5317
|
const pid = trackedMatch?.pid ?? parseInt(target, 10);
|
|
3601
5318
|
if (!isNaN(pid)) {
|
|
3602
5319
|
try {
|
|
3603
|
-
const output = execSync2(`journalctl --no-pager -n ${lineCount} _PID=${pid} 2>/dev/null || echo ""`, { encoding: "utf-8", timeout: 3e3 });
|
|
3604
|
-
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));
|
|
3605
5322
|
} catch {
|
|
3606
5323
|
logLines = ["(no logs available)"];
|
|
3607
5324
|
}
|
|
@@ -3616,48 +5333,22 @@ async function logCommand(args2) {
|
|
|
3616
5333
|
spinner.stop();
|
|
3617
5334
|
process.stdout.write("\r\x1B[K");
|
|
3618
5335
|
console.error(`
|
|
3619
|
-
${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") : ""})`)}`);
|
|
3620
5337
|
const sliced = logLines.slice(-lineCount);
|
|
3621
5338
|
for (const line of sliced) {
|
|
3622
5339
|
const display = line.length > 300 ? line.slice(0, 300) + "\u2026" : line;
|
|
3623
|
-
const prefix = followFlag ? `${timestamp()} ` : "";
|
|
3624
5340
|
if (line.toLowerCase().includes("error") || line.toLowerCase().includes("fail") || line.toLowerCase().includes("fatal")) {
|
|
3625
|
-
console.error(` ${
|
|
5341
|
+
console.error(` ${pc15.red(display)}`);
|
|
3626
5342
|
} else if (line.toLowerCase().includes("warn")) {
|
|
3627
|
-
console.error(` ${
|
|
5343
|
+
console.error(` ${pc15.yellow(display)}`);
|
|
3628
5344
|
} else if (line.toLowerCase().includes("info") || line.includes("[")) {
|
|
3629
|
-
console.error(` ${
|
|
5345
|
+
console.error(` ${pc15.cyan(display)}`);
|
|
3630
5346
|
} else {
|
|
3631
|
-
console.error(` ${
|
|
5347
|
+
console.error(` ${display}`);
|
|
3632
5348
|
}
|
|
3633
5349
|
}
|
|
3634
5350
|
if (followFlag && logFilePath) {
|
|
3635
|
-
|
|
3636
|
-
if (levelFilter) {
|
|
3637
|
-
const tail = spawn4("tail", ["-n", "0", "-f", logFilePath], { stdio: ["ignore", "pipe", "pipe"] });
|
|
3638
|
-
const grep = spawn4("grep", ["--line-buffered", "-i", levelFilter], { stdio: ["pipe", "inherit", "inherit"] });
|
|
3639
|
-
if (tail.stdout) tail.stdout.pipe(grep.stdin);
|
|
3640
|
-
await new Promise((resolve12) => {
|
|
3641
|
-
tail.on("exit", () => resolve12());
|
|
3642
|
-
process.once("SIGINT", () => {
|
|
3643
|
-
tail.kill("SIGTERM");
|
|
3644
|
-
grep.kill("SIGTERM");
|
|
3645
|
-
resolve12();
|
|
3646
|
-
});
|
|
3647
|
-
});
|
|
3648
|
-
} else {
|
|
3649
|
-
console.error(`
|
|
3650
|
-
${pc10.dim("Following... (Ctrl+C to stop)")}
|
|
3651
|
-
`);
|
|
3652
|
-
const tail = spawn4("tail", tailArgs.concat([logFilePath]), { stdio: "inherit" });
|
|
3653
|
-
await new Promise((resolve12) => {
|
|
3654
|
-
tail.on("exit", () => resolve12());
|
|
3655
|
-
process.once("SIGINT", () => {
|
|
3656
|
-
tail.kill("SIGTERM");
|
|
3657
|
-
resolve12();
|
|
3658
|
-
});
|
|
3659
|
-
});
|
|
3660
|
-
}
|
|
5351
|
+
await followLog(logFilePath, { redact, levelFilter, json: jsonFlag });
|
|
3661
5352
|
}
|
|
3662
5353
|
console.error();
|
|
3663
5354
|
} catch (error) {
|
|
@@ -3665,10 +5356,97 @@ async function logCommand(args2) {
|
|
|
3665
5356
|
console.error(renderError("Log read failed", String(error)));
|
|
3666
5357
|
}
|
|
3667
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
|
+
}
|
|
3668
5446
|
|
|
3669
5447
|
// src/commands/attach.ts
|
|
3670
5448
|
import { PortDetector as PortDetector3 } from "@plumpslabs/fennec-core";
|
|
3671
|
-
import
|
|
5449
|
+
import pc16 from "picocolors";
|
|
3672
5450
|
async function attachCommand(args2) {
|
|
3673
5451
|
const port = parseInt(args2[0], 10);
|
|
3674
5452
|
if (isNaN(port)) {
|
|
@@ -3686,7 +5464,7 @@ async function attachCommand(args2) {
|
|
|
3686
5464
|
spinner.succeed(`Attached to :${port}`);
|
|
3687
5465
|
console.error(` ${renderKV("Name", renderAppName(name))}`);
|
|
3688
5466
|
console.error(` ${renderKV("PID", String(info.pid))}`);
|
|
3689
|
-
console.error(` ${renderKV("Command", info.command ||
|
|
5467
|
+
console.error(` ${renderKV("Command", info.command || pc16.dim("unknown"))}`);
|
|
3690
5468
|
} else {
|
|
3691
5469
|
spinner.fail(`No process found on port ${port}`);
|
|
3692
5470
|
process.exit(1);
|
|
@@ -3700,35 +5478,35 @@ async function attachCommand(args2) {
|
|
|
3700
5478
|
|
|
3701
5479
|
// src/commands/sessions.ts
|
|
3702
5480
|
import { SessionStore } from "@plumpslabs/fennec-core";
|
|
3703
|
-
import
|
|
5481
|
+
import pc17 from "picocolors";
|
|
3704
5482
|
async function sessionsCommand() {
|
|
3705
5483
|
const store = new SessionStore("./.fennec/sessions");
|
|
3706
5484
|
const sessions = store.list();
|
|
3707
5485
|
if (sessions.length === 0) {
|
|
3708
5486
|
console.error(`
|
|
3709
|
-
${
|
|
5487
|
+
${pc17.dim("No saved sessions found.")}
|
|
3710
5488
|
`);
|
|
3711
5489
|
return;
|
|
3712
5490
|
}
|
|
3713
5491
|
const columns = [
|
|
3714
|
-
{ key: "name", label: "Name", format: (v) =>
|
|
5492
|
+
{ key: "name", label: "Name", format: (v) => pc17.bold(String(v)) },
|
|
3715
5493
|
{ key: "origin", label: "Origin" },
|
|
3716
|
-
{ key: "savedAt", label: "Saved", format: (v) =>
|
|
5494
|
+
{ key: "savedAt", label: "Saved", format: (v) => pc17.dim(String(v)) }
|
|
3717
5495
|
];
|
|
3718
5496
|
const rows = sessions.map((s) => ({ name: s.name, origin: s.origin, savedAt: new Date(s.savedAt).toLocaleString() }));
|
|
3719
5497
|
console.error(`
|
|
3720
|
-
${symbols.fox} ${
|
|
5498
|
+
${symbols.fox} ${pc17.bold("Saved Sessions")}
|
|
3721
5499
|
`);
|
|
3722
5500
|
console.error(renderTable(columns, rows));
|
|
3723
|
-
console.error(` ${
|
|
5501
|
+
console.error(` ${pc17.dim(`${sessions.length} session(s)`)}
|
|
3724
5502
|
`);
|
|
3725
5503
|
}
|
|
3726
5504
|
|
|
3727
5505
|
// src/commands/setup.ts
|
|
3728
|
-
import
|
|
5506
|
+
import pc18 from "picocolors";
|
|
3729
5507
|
async function setupCommand() {
|
|
3730
5508
|
console.error(`
|
|
3731
|
-
${symbols.fox} ${
|
|
5509
|
+
${symbols.fox} ${pc18.bold("Fennec Setup")}
|
|
3732
5510
|
`);
|
|
3733
5511
|
const mcpClient = await selectPrompt("Which MCP client are you using?", [
|
|
3734
5512
|
{ value: "claude", label: "Claude Desktop", description: "Anthropic's AI desktop app" },
|
|
@@ -3737,12 +5515,12 @@ async function setupCommand() {
|
|
|
3737
5515
|
{ value: "other", label: "Other MCP client", description: "Any MCP-compatible client" }
|
|
3738
5516
|
]);
|
|
3739
5517
|
if (!mcpClient) {
|
|
3740
|
-
console.error(` ${
|
|
5518
|
+
console.error(` ${pc18.dim("Setup cancelled.")}
|
|
3741
5519
|
`);
|
|
3742
5520
|
return;
|
|
3743
5521
|
}
|
|
3744
5522
|
console.error(`
|
|
3745
|
-
${
|
|
5523
|
+
${pc18.green("\u2713")} Selected: ${pc18.bold(mcpClient)}
|
|
3746
5524
|
`);
|
|
3747
5525
|
const configSnippet = `{
|
|
3748
5526
|
"mcpServers": {
|
|
@@ -3752,24 +5530,24 @@ async function setupCommand() {
|
|
|
3752
5530
|
}
|
|
3753
5531
|
}
|
|
3754
5532
|
}`;
|
|
3755
|
-
console.error(` ${
|
|
5533
|
+
console.error(` ${pc18.bold("Add this to your MCP client config:")}
|
|
3756
5534
|
`);
|
|
3757
|
-
console.error(` ${
|
|
5535
|
+
console.error(` ${pc18.dim("```")}`);
|
|
3758
5536
|
console.error(configSnippet.split("\n").map((l) => ` ${l}`).join("\n"));
|
|
3759
|
-
console.error(` ${
|
|
5537
|
+
console.error(` ${pc18.dim("```")}
|
|
3760
5538
|
`);
|
|
3761
|
-
console.error(` ${renderSuccess("Setup complete!")} ${
|
|
5539
|
+
console.error(` ${renderSuccess("Setup complete!")} ${pc18.dim("Run")} ${renderCommand("fennec start")} ${pc18.dim("to begin.")}
|
|
3762
5540
|
`);
|
|
3763
5541
|
}
|
|
3764
5542
|
|
|
3765
5543
|
// src/commands/management.ts
|
|
3766
|
-
import { existsSync as
|
|
3767
|
-
import { resolve as
|
|
5544
|
+
import { existsSync as existsSync10, writeFileSync as writeFileSync4 } from "fs";
|
|
5545
|
+
import { resolve as resolve8 } from "path";
|
|
3768
5546
|
import { execSync as execSync3 } from "child_process";
|
|
3769
|
-
import
|
|
5547
|
+
import pc19 from "picocolors";
|
|
3770
5548
|
async function installBrowsersCommand() {
|
|
3771
5549
|
console.error(`
|
|
3772
|
-
${
|
|
5550
|
+
${pc19.bold("Installing Browser Engines")}
|
|
3773
5551
|
`);
|
|
3774
5552
|
const spinner = createSpinner("Installing Chromium...");
|
|
3775
5553
|
try {
|
|
@@ -3777,21 +5555,21 @@ async function installBrowsersCommand() {
|
|
|
3777
5555
|
spinner.succeed("Chromium installed successfully");
|
|
3778
5556
|
} catch {
|
|
3779
5557
|
spinner.fail("Failed to install Chromium");
|
|
3780
|
-
console.error(` ${
|
|
5558
|
+
console.error(` ${pc19.yellow("\u2192")} Try running: ${renderCommand("npx playwright install chromium")}`);
|
|
3781
5559
|
}
|
|
3782
5560
|
console.error(`
|
|
3783
|
-
${
|
|
5561
|
+
${pc19.green("\u2713")} Browser installation complete.
|
|
3784
5562
|
`);
|
|
3785
5563
|
}
|
|
3786
5564
|
async function initCommand() {
|
|
3787
5565
|
console.error(`
|
|
3788
|
-
${
|
|
5566
|
+
${pc19.bold("Initialize Fennec Configuration")}
|
|
3789
5567
|
`);
|
|
3790
|
-
const configFile =
|
|
3791
|
-
if (
|
|
3792
|
-
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);
|
|
3793
5571
|
if (!overwrite) {
|
|
3794
|
-
console.error(` ${
|
|
5572
|
+
console.error(` ${pc19.dim("Cancelled.")}
|
|
3795
5573
|
`);
|
|
3796
5574
|
return;
|
|
3797
5575
|
}
|
|
@@ -3870,22 +5648,22 @@ logging:
|
|
|
3870
5648
|
format: pretty
|
|
3871
5649
|
file: null
|
|
3872
5650
|
`;
|
|
3873
|
-
|
|
3874
|
-
spinner.succeed(`Configuration written to ${
|
|
5651
|
+
writeFileSync4(configFile, config, "utf-8");
|
|
5652
|
+
spinner.succeed(`Configuration written to ${pc19.bold(configFile)}`);
|
|
3875
5653
|
console.error(`
|
|
3876
|
-
${
|
|
5654
|
+
${pc19.dim("Edit the file to customize Fennec behavior.")}
|
|
3877
5655
|
`);
|
|
3878
5656
|
}
|
|
3879
5657
|
|
|
3880
5658
|
// src/commands/health.ts
|
|
3881
|
-
import { existsSync as
|
|
5659
|
+
import { existsSync as existsSync11, readdirSync as readdirSync2, statSync as statSync3 } from "fs";
|
|
3882
5660
|
import { execSync as execSync4 } from "child_process";
|
|
3883
|
-
import { resolve as
|
|
5661
|
+
import { resolve as resolve9 } from "path";
|
|
3884
5662
|
import { homedir as homedir6 } from "os";
|
|
3885
|
-
import
|
|
5663
|
+
import pc20 from "picocolors";
|
|
3886
5664
|
async function healthCommand() {
|
|
3887
5665
|
printBanner();
|
|
3888
|
-
console.error(` ${
|
|
5666
|
+
console.error(` ${pc20.bold("Fennec Health Check")}
|
|
3889
5667
|
`);
|
|
3890
5668
|
let adbStatus = "unknown";
|
|
3891
5669
|
try {
|
|
@@ -3895,20 +5673,20 @@ async function healthCommand() {
|
|
|
3895
5673
|
stdio: ["ignore", "pipe", "pipe"]
|
|
3896
5674
|
});
|
|
3897
5675
|
const version = result.split("\n")[0]?.trim() ?? "installed";
|
|
3898
|
-
adbStatus =
|
|
5676
|
+
adbStatus = pc20.green(version);
|
|
3899
5677
|
} catch {
|
|
3900
|
-
adbStatus =
|
|
5678
|
+
adbStatus = pc20.dim("not found") + " (optional)";
|
|
3901
5679
|
}
|
|
3902
5680
|
const tracked = readTracked();
|
|
3903
5681
|
const runningCount = tracked.filter((t) => isProcessRunning(t.pid)).length;
|
|
3904
|
-
const logDir =
|
|
5682
|
+
const logDir = resolve9(homedir6(), ".fennec", "logs");
|
|
3905
5683
|
let logSize = "0 B";
|
|
3906
5684
|
try {
|
|
3907
|
-
if (
|
|
5685
|
+
if (existsSync11(logDir)) {
|
|
3908
5686
|
const files = readdirSync2(logDir);
|
|
3909
5687
|
let totalBytes = 0;
|
|
3910
5688
|
for (const f of files) {
|
|
3911
|
-
const fStat =
|
|
5689
|
+
const fStat = statSync3(resolve9(logDir, f));
|
|
3912
5690
|
totalBytes += fStat.size;
|
|
3913
5691
|
}
|
|
3914
5692
|
logSize = totalBytes > 1024 * 1024 ? `${(totalBytes / (1024 * 1024)).toFixed(1)} MB` : `${(totalBytes / 1024).toFixed(1)} KB`;
|
|
@@ -3921,7 +5699,7 @@ async function healthCommand() {
|
|
|
3921
5699
|
memoryInfo = `${(mem.rss / (1024 * 1024)).toFixed(0)} MB RSS / ${(mem.heapUsed / (1024 * 1024)).toFixed(0)} MB heap`;
|
|
3922
5700
|
} catch {
|
|
3923
5701
|
}
|
|
3924
|
-
console.error(` ${symbols.fox} ${
|
|
5702
|
+
console.error(` ${symbols.fox} ${pc20.bold("System")}
|
|
3925
5703
|
`);
|
|
3926
5704
|
console.error(` ${renderKV("Node.js", process.version)}`);
|
|
3927
5705
|
console.error(` ${renderKV("Platform", `${process.platform} ${process.arch}`)}`);
|
|
@@ -3929,7 +5707,7 @@ async function healthCommand() {
|
|
|
3929
5707
|
console.error(` ${renderKV("ADB", adbStatus)}`);
|
|
3930
5708
|
console.error(` ${renderKV("PID", String(process.pid))}`);
|
|
3931
5709
|
console.error();
|
|
3932
|
-
console.error(` ${symbols.fox} ${
|
|
5710
|
+
console.error(` ${symbols.fox} ${pc20.bold("Processes")}
|
|
3933
5711
|
`);
|
|
3934
5712
|
console.error(` ${renderKV("Tracked", String(tracked.length))}`);
|
|
3935
5713
|
console.error(` ${renderKV("Running", String(runningCount))}`);
|
|
@@ -3937,55 +5715,55 @@ async function healthCommand() {
|
|
|
3937
5715
|
console.error();
|
|
3938
5716
|
const allHealthy = runningCount === tracked.length || tracked.length === 0;
|
|
3939
5717
|
if (allHealthy) {
|
|
3940
|
-
console.error(` ${
|
|
5718
|
+
console.error(` ${pc20.green("\u2713")} ${pc20.bold("All systems healthy")}
|
|
3941
5719
|
`);
|
|
3942
5720
|
} else {
|
|
3943
5721
|
const stopped = tracked.length - runningCount;
|
|
3944
|
-
console.error(` ${
|
|
5722
|
+
console.error(` ${pc20.yellow("\u26A0")} ${pc20.bold(`${stopped} process(es) stopped`)} ${pc20.dim("fennec ps")}
|
|
3945
5723
|
`);
|
|
3946
5724
|
}
|
|
3947
5725
|
}
|
|
3948
5726
|
|
|
3949
5727
|
// src/commands/cleanup.ts
|
|
3950
|
-
import
|
|
5728
|
+
import pc21 from "picocolors";
|
|
3951
5729
|
async function cleanupCommand() {
|
|
3952
5730
|
const tracked = readTracked();
|
|
3953
5731
|
if (tracked.length === 0) {
|
|
3954
5732
|
console.error(`
|
|
3955
|
-
${
|
|
5733
|
+
${pc21.dim("No tracked processes to clean up.")}
|
|
3956
5734
|
`);
|
|
3957
5735
|
return;
|
|
3958
5736
|
}
|
|
3959
5737
|
const toRemove = tracked.filter((t) => !isProcessRunning(t.pid) && !t.command);
|
|
3960
5738
|
if (toRemove.length === 0) {
|
|
3961
5739
|
console.error(`
|
|
3962
|
-
${
|
|
5740
|
+
${pc21.green("\u2713")} ${pc21.bold("All clean")} ${pc21.dim("\u2014 no dead entries without saved commands.")}
|
|
3963
5741
|
`);
|
|
3964
5742
|
return;
|
|
3965
5743
|
}
|
|
3966
5744
|
console.error(`
|
|
3967
|
-
${
|
|
5745
|
+
${pc21.yellow("\u26A0")} ${pc21.bold(`Found ${toRemove.length} dead entr${toRemove.length > 1 ? "ies" : "y"} without saved commands`)}
|
|
3968
5746
|
`);
|
|
3969
5747
|
for (const entry of toRemove) {
|
|
3970
|
-
console.error(` ${
|
|
5748
|
+
console.error(` ${pc21.dim("\xB7")} ${pc21.bold(entry.name)} ${pc21.dim(`(PID ${entry.pid})`)}`);
|
|
3971
5749
|
}
|
|
3972
5750
|
console.error();
|
|
3973
5751
|
const confirmed = await confirmPrompt(`Remove ${toRemove.length} entr${toRemove.length > 1 ? "ies" : "y"}?`, false);
|
|
3974
5752
|
if (!confirmed) {
|
|
3975
|
-
console.error(` ${
|
|
5753
|
+
console.error(` ${pc21.dim("Cancelled")}
|
|
3976
5754
|
`);
|
|
3977
5755
|
return;
|
|
3978
5756
|
}
|
|
3979
5757
|
const remaining = tracked.filter((t) => !toRemove.includes(t));
|
|
3980
5758
|
saveTracked(remaining);
|
|
3981
5759
|
console.error(`
|
|
3982
|
-
${
|
|
5760
|
+
${pc21.green("\u2713")} ${pc21.bold(`Removed ${toRemove.length} entr${toRemove.length > 1 ? "ies" : "y"}`)} ${pc21.dim(`(${remaining.length} remaining)`)}
|
|
3983
5761
|
`);
|
|
3984
5762
|
}
|
|
3985
5763
|
|
|
3986
5764
|
// src/commands/info.ts
|
|
3987
|
-
import
|
|
3988
|
-
import { resolve as
|
|
5765
|
+
import pc22 from "picocolors";
|
|
5766
|
+
import { resolve as resolve10 } from "path";
|
|
3989
5767
|
import { homedir as homedir7 } from "os";
|
|
3990
5768
|
async function infoCommand(args2) {
|
|
3991
5769
|
const name = args2[0];
|
|
@@ -4000,33 +5778,33 @@ async function infoCommand(args2) {
|
|
|
4000
5778
|
process.exit(1);
|
|
4001
5779
|
}
|
|
4002
5780
|
const running = isProcessRunning(match.pid);
|
|
4003
|
-
const statusIcon = running ?
|
|
4004
|
-
const statusText = running ?
|
|
5781
|
+
const statusIcon = running ? pc22.green("\u25CF") : pc22.red("\u25CB");
|
|
5782
|
+
const statusText = running ? pc22.green("running") : pc22.red("stopped");
|
|
4005
5783
|
const uptime = running ? formatUptime(Math.floor((Date.now() - new Date(match.startedAt).getTime()) / 1e3)) : "-";
|
|
4006
|
-
const logPath =
|
|
5784
|
+
const logPath = resolve10(homedir7(), ".fennec", "logs", `${match.name}.log`);
|
|
4007
5785
|
console.error(`
|
|
4008
|
-
${symbols.fox} ${
|
|
5786
|
+
${symbols.fox} ${pc22.bold(match.name)} ${pc22.dim("\u2014 Process Info")}
|
|
4009
5787
|
`);
|
|
4010
|
-
console.error(` ${renderKVColor("Name", match.name,
|
|
5788
|
+
console.error(` ${renderKVColor("Name", match.name, pc22.bold)}`);
|
|
4011
5789
|
console.error(` ${renderKVColor("Status", `${statusIcon} ${statusText}`)}`);
|
|
4012
5790
|
console.error(` ${renderKVColor("PID", String(match.pid))}`);
|
|
4013
5791
|
console.error(` ${renderKVColor("Port", match.port ? String(match.port) : "-")}`);
|
|
4014
|
-
console.error(` ${renderKVColor("Command", match.command ||
|
|
5792
|
+
console.error(` ${renderKVColor("Command", match.command || pc22.dim("(none)"))}`);
|
|
4015
5793
|
if (match.cwd) console.error(` ${renderKVColor("CWD", match.cwd)}`);
|
|
4016
5794
|
console.error(` ${renderKVColor("Started", new Date(match.startedAt).toLocaleString())}`);
|
|
4017
5795
|
console.error(` ${renderKVColor("Uptime", uptime)}`);
|
|
4018
5796
|
console.error(` ${renderKVColor("Log Path", logPath)}`);
|
|
4019
5797
|
if (!running && match.command) {
|
|
4020
5798
|
console.error(`
|
|
4021
|
-
${
|
|
5799
|
+
${pc22.dim("Re-spawn with:")} ${pc22.cyan(`fennec spawn ${match.name}`)}`);
|
|
4022
5800
|
}
|
|
4023
5801
|
console.error();
|
|
4024
5802
|
}
|
|
4025
5803
|
|
|
4026
5804
|
// src/commands/rename.ts
|
|
4027
|
-
import
|
|
4028
|
-
import { existsSync as
|
|
4029
|
-
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";
|
|
4030
5808
|
import { homedir as homedir8 } from "os";
|
|
4031
5809
|
async function renameCommand(args2) {
|
|
4032
5810
|
const [oldName, newName] = args2;
|
|
@@ -4036,7 +5814,7 @@ async function renameCommand(args2) {
|
|
|
4036
5814
|
}
|
|
4037
5815
|
if (oldName === newName) {
|
|
4038
5816
|
console.error(`
|
|
4039
|
-
${
|
|
5817
|
+
${pc23.yellow("\u26A0")} ${pc23.dim("Old and new names are the same.")}
|
|
4040
5818
|
`);
|
|
4041
5819
|
process.exit(0);
|
|
4042
5820
|
}
|
|
@@ -4051,25 +5829,25 @@ async function renameCommand(args2) {
|
|
|
4051
5829
|
process.exit(1);
|
|
4052
5830
|
}
|
|
4053
5831
|
console.error(`
|
|
4054
|
-
${symbols.fox} ${
|
|
5832
|
+
${symbols.fox} ${pc23.bold("Rename Process")}
|
|
4055
5833
|
`);
|
|
4056
5834
|
console.error(` ${renderKVColor("From", oldName)}`);
|
|
4057
5835
|
console.error(` ${renderKVColor("To", newName)}`);
|
|
4058
5836
|
if (isProcessRunning(match.pid)) {
|
|
4059
|
-
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.")}`);
|
|
4060
5838
|
}
|
|
4061
5839
|
console.error();
|
|
4062
|
-
const confirmed = await confirmPrompt(`Rename ${
|
|
5840
|
+
const confirmed = await confirmPrompt(`Rename ${pc23.bold(oldName)} ${pc23.dim("\u2192")} ${pc23.bold(newName)}?`, true);
|
|
4063
5841
|
if (!confirmed) {
|
|
4064
|
-
console.error(` ${
|
|
5842
|
+
console.error(` ${pc23.dim("Cancelled")}
|
|
4065
5843
|
`);
|
|
4066
5844
|
return;
|
|
4067
5845
|
}
|
|
4068
5846
|
const spinner = createSpinner(`Renaming ${oldName} \u2192 ${newName}...`);
|
|
4069
|
-
const logDir =
|
|
4070
|
-
const oldLog =
|
|
4071
|
-
const newLog =
|
|
4072
|
-
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)) {
|
|
4073
5851
|
try {
|
|
4074
5852
|
renameSync2(oldLog, newLog);
|
|
4075
5853
|
} catch {
|
|
@@ -4077,14 +5855,14 @@ async function renameCommand(args2) {
|
|
|
4077
5855
|
}
|
|
4078
5856
|
match.name = newName;
|
|
4079
5857
|
saveTracked(tracked);
|
|
4080
|
-
spinner.succeed(`${
|
|
5858
|
+
spinner.succeed(`${pc23.bold(oldName)} ${pc23.dim("\u2192")} ${pc23.bold(newName)}`);
|
|
4081
5859
|
console.error();
|
|
4082
5860
|
}
|
|
4083
5861
|
|
|
4084
5862
|
// src/commands/export-import.ts
|
|
4085
|
-
import
|
|
4086
|
-
import { existsSync as
|
|
4087
|
-
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";
|
|
4088
5866
|
async function exportCommand(args2) {
|
|
4089
5867
|
const tracked = readTracked();
|
|
4090
5868
|
const fileIndex = args2.indexOf("--file");
|
|
@@ -4095,16 +5873,16 @@ async function exportCommand(args2) {
|
|
|
4095
5873
|
}
|
|
4096
5874
|
if (tracked.length === 0) {
|
|
4097
5875
|
console.error(`
|
|
4098
|
-
${
|
|
5876
|
+
${pc24.dim("No tracked processes to export.")}
|
|
4099
5877
|
`);
|
|
4100
5878
|
return;
|
|
4101
5879
|
}
|
|
4102
5880
|
const json = JSON.stringify(tracked, null, 2);
|
|
4103
5881
|
if (filePath) {
|
|
4104
5882
|
try {
|
|
4105
|
-
|
|
5883
|
+
writeFileSync5(resolve12(filePath), json, "utf-8");
|
|
4106
5884
|
console.error(`
|
|
4107
|
-
${
|
|
5885
|
+
${pc24.green("\u2713")} ${pc24.bold(`Exported ${tracked.length} process(es)`)} ${pc24.dim(`\u2192 ${filePath}`)}
|
|
4108
5886
|
`);
|
|
4109
5887
|
} catch (err) {
|
|
4110
5888
|
console.error(renderError("Export failed", String(err)));
|
|
@@ -4120,14 +5898,14 @@ async function importCommand(args2) {
|
|
|
4120
5898
|
console.error(renderError("Missing file", "Usage: fennec import <file>"));
|
|
4121
5899
|
process.exit(1);
|
|
4122
5900
|
}
|
|
4123
|
-
const resolvedPath =
|
|
4124
|
-
if (!
|
|
5901
|
+
const resolvedPath = resolve12(filePath);
|
|
5902
|
+
if (!existsSync13(resolvedPath)) {
|
|
4125
5903
|
console.error(renderError("File not found", `"${filePath}" does not exist.`));
|
|
4126
5904
|
process.exit(1);
|
|
4127
5905
|
}
|
|
4128
5906
|
let imported;
|
|
4129
5907
|
try {
|
|
4130
|
-
imported = JSON.parse(
|
|
5908
|
+
imported = JSON.parse(readFileSync9(resolvedPath, "utf-8"));
|
|
4131
5909
|
if (!Array.isArray(imported) || imported.length === 0) {
|
|
4132
5910
|
console.error(renderError("Invalid file", "File must contain a non-empty JSON array of process objects."));
|
|
4133
5911
|
process.exit(1);
|
|
@@ -4138,20 +5916,20 @@ async function importCommand(args2) {
|
|
|
4138
5916
|
}
|
|
4139
5917
|
const existing = readTracked();
|
|
4140
5918
|
console.error(`
|
|
4141
|
-
${symbols.fox} ${
|
|
5919
|
+
${symbols.fox} ${pc24.bold("Import Processes")}
|
|
4142
5920
|
`);
|
|
4143
5921
|
console.error(` ${renderKVColor("File", resolvedPath)}`);
|
|
4144
5922
|
console.error(` ${renderKVColor("Importing", `${imported.length} process(es)`)}`);
|
|
4145
5923
|
console.error(` ${renderKVColor("Existing", `${existing.length} process(es)`)}`);
|
|
4146
5924
|
console.error(`
|
|
4147
|
-
${
|
|
5925
|
+
${pc24.bold("Processes to import:")}`);
|
|
4148
5926
|
for (const p of imported) {
|
|
4149
|
-
console.error(` ${
|
|
5927
|
+
console.error(` ${pc24.green("+")} ${pc24.bold(p.name)} ${pc24.dim(`(${p.command})`)}`);
|
|
4150
5928
|
}
|
|
4151
5929
|
console.error();
|
|
4152
5930
|
const confirmed = await confirmPrompt("Merge into tracked.json? Existing processes with the same name will be overwritten.", true);
|
|
4153
5931
|
if (!confirmed) {
|
|
4154
|
-
console.error(` ${
|
|
5932
|
+
console.error(` ${pc24.dim("Cancelled")}
|
|
4155
5933
|
`);
|
|
4156
5934
|
return;
|
|
4157
5935
|
}
|
|
@@ -4165,13 +5943,18 @@ async function importCommand(args2) {
|
|
|
4165
5943
|
}
|
|
4166
5944
|
}
|
|
4167
5945
|
saveTracked(merged);
|
|
4168
|
-
console.error(` ${
|
|
5946
|
+
console.error(` ${pc24.green("\u2713")} ${pc24.bold(`Imported ${imported.length} process(es)`)} ${pc24.dim(`(${merged.length} total)`)}
|
|
4169
5947
|
`);
|
|
4170
5948
|
}
|
|
4171
5949
|
|
|
4172
5950
|
// src/index.ts
|
|
4173
5951
|
var [, , command, ...args] = process.argv;
|
|
4174
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
|
+
}
|
|
4175
5958
|
if (!command || command === "start") {
|
|
4176
5959
|
if (args.length === 0 || args[0]?.startsWith("--")) {
|
|
4177
5960
|
await startServer(args);
|
|
@@ -4186,7 +5969,7 @@ async function main() {
|
|
|
4186
5969
|
} else if (command === "status") {
|
|
4187
5970
|
printBanner();
|
|
4188
5971
|
await statusCommand(args);
|
|
4189
|
-
} else if (command === "log") {
|
|
5972
|
+
} else if (command === "log" || command === "logs") {
|
|
4190
5973
|
await logCommand(args);
|
|
4191
5974
|
} else if (command === "spawn") {
|
|
4192
5975
|
await spawnCommand(args);
|
|
@@ -4196,6 +5979,21 @@ async function main() {
|
|
|
4196
5979
|
await killCommand(args);
|
|
4197
5980
|
} else if (command === "restart") {
|
|
4198
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);
|
|
4199
5997
|
} else if (command === "info") {
|
|
4200
5998
|
printBanner();
|
|
4201
5999
|
await infoCommand(args);
|
|
@@ -4234,7 +6032,11 @@ async function main() {
|
|
|
4234
6032
|
printBanner();
|
|
4235
6033
|
} else if (command === "help" || command === "--help" || command === "-h") {
|
|
4236
6034
|
printBanner();
|
|
4237
|
-
|
|
6035
|
+
if (args[0] && !args[0].startsWith("-")) {
|
|
6036
|
+
showCommandHelp(args[0]);
|
|
6037
|
+
} else {
|
|
6038
|
+
showHelp();
|
|
6039
|
+
}
|
|
4238
6040
|
} else {
|
|
4239
6041
|
console.error(renderError(`Unknown command: ${command}`, "Run 'fennec help' for usage information"));
|
|
4240
6042
|
process.exit(1);
|