@plumpslabs/fennec-cli 1.13.4 → 1.13.6

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/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 stripAnsi = require_strip_ansi();
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 = stripAnsi(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 pad(str, len, pad2, dir) {
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(pad2, padlen) + str;
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(pad2, left) + str + repeat(pad2, right);
196
+ str = repeat(pad3, left) + str + repeat(pad3, right);
197
197
  break;
198
198
  }
199
199
  default: {
200
- str = str + repeat(pad2, padlen);
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 pad = options?.padding ?? 1;
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": pad,
1914
- "padding-right": pad,
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 pad = " ".repeat(indent);
1959
- return `${pad}${pc.dim(key + sep)} ${value}`;
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((resolve12) => {
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
- resolve12(ans.trim());
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((resolve12) => {
2034
+ const answer = await new Promise((resolve13) => {
2035
2035
  rl.question(`
2036
2036
  ${message} ${pc.dim("(" + hint + ")")}: `, (ans) => {
2037
2037
  rl.close();
2038
- resolve12(ans.trim().toLowerCase());
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.4";
2094
+ var VERSION = "1.13.6";
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(50));
2140
- console.error(`
2141
-
2142
- ${pc3.bold("Usage:")} fennec ${pc3.dim("<command>")} ${pc3.dim("[options]")}
2143
-
2144
- ${sep}
2145
- ${pc3.bold("Server")}
2146
- ${sep}
2147
-
2148
- ${pc3.cyan("start")} ${pc3.dim("Start Fennec MCP server (default)")}
2149
- ${pc3.dim("--config")} Path to config file
2150
- ${pc3.dim("--transport")} Transport type ${pc3.dim("(stdio | sse) [default: stdio]")}
2151
- ${pc3.dim("--api-port")} Management API port ${pc3.dim("[default: 3456]")}
2152
-
2153
- ${pc3.cyan("start")} ${pc3.dim("<command>")} ${pc3.dim("Start an app under Fennec observation (new!)")}
2154
- ${pc3.dim("--name")} App name ${pc3.dim("(required for tracking)")}
2155
- ${pc3.dim("--port")} Port the app listens on ${pc3.dim("(optional)")}
2156
- ${pc3.dim("--cwd")} Working directory
2157
- ${pc3.dim("--restart")} Auto-restart on crash
2158
-
2159
- ${sep}
2160
- ${pc3.bold("Process Management")}
2161
- ${sep}
2162
-
2163
- ${pc3.cyan("ps")} ${pc3.dim("[options]")} ${pc3.dim("List Fennec-tracked apps (like PM2)")}
2164
- ${pc3.dim("--system, -a")} Show all system processes
2165
- ${pc3.dim("--name")} Filter by app name
2166
- ${pc3.dim("-w, --watch")} Watch mode ${pc3.dim("(system processes refresh every 3s)")}
2167
-
2168
- ${pc3.cyan("run")} ${pc3.dim("<command>")} ${pc3.dim("Run a command under Fennec observation")}
2169
- ${pc3.dim("--name")} Process name ${pc3.dim("(required)")}
2170
- ${pc3.dim("--cwd")} Working directory
2171
- ${pc3.dim("--restart")} Auto-restart on crash
2172
-
2173
- ${pc3.cyan("status")} ${pc3.dim("[name]")} ${pc3.dim("Show system overview & top processes")}
2174
- ${pc3.dim("-w, --watch")} Watch mode ${pc3.dim("(refresh every 3s)")}
2175
-
2176
- ${pc3.cyan("log")} ${pc3.dim("<pid|name>")} ${pc3.dim("Show logs for a process")}
2177
- ${pc3.dim("--lines")} Number of lines ${pc3.dim("[default: 30]")}
2178
- ${pc3.dim("-f, --follow")} Follow mode ${pc3.dim("(journalctl --follow)")}
2179
-
2180
- ${pc3.cyan("kill")} ${pc3.dim("<pid|name|all>")} ${pc3.dim("Kill process(es) by PID, name, or all")}
2181
- ${pc3.dim("--signal")} Signal to send ${pc3.dim("[default: SIGTERM]")}
2182
- ${pc3.dim("--all")} Kill all user processes
2183
-
2184
- ${pc3.cyan("restart")} ${pc3.dim("<pid|name>")} ${pc3.dim("Kill + show re-run command")}
2185
-
2186
- ${sep}
2187
- ${pc3.bold("Observation")}
2188
- ${sep}
2189
-
2190
- ${pc3.cyan("attach")} ${pc3.dim("<port>")} ${pc3.dim("Observe a process by port")}
2191
- ${pc3.dim("--name")} Process name
2192
-
2193
- ${pc3.cyan("pipe")} ${pc3.dim("Pipe stdin to log watcher")}
2194
- ${pc3.dim("--name")} Watcher name ${pc3.dim("(required)")}
2195
-
2196
- ${pc3.cyan("watch")} ${pc3.dim("Watch a log file")}
2197
- ${pc3.dim("--file")} File path ${pc3.dim("(required)")}
2198
- ${pc3.dim("--name")} Watcher name
2199
-
2200
- ${pc3.cyan("attach-pid")} ${pc3.dim("<pid>")} ${pc3.dim("Attach to process by PID")}
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 { createWriteStream, mkdirSync as mkdirSync2 } from "fs";
2334
- import { resolve as resolve3 } from "path";
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 pc5 from "picocolors";
2618
+ import pc7 from "picocolors";
2339
2619
 
2340
2620
  // src/commands/tracker.ts
2341
- import { existsSync as existsSync2, writeFileSync, readFileSync, mkdirSync, renameSync, statSync, rmSync } from "fs";
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 as readFileSync2, existsSync as existsSync3 } from "fs";
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 (!existsSync3(statPath)) return null;
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":
@@ -2558,129 +2787,1035 @@ function mapBsdState(s) {
2558
2787
  function detectListeningPorts(_pid) {
2559
2788
  return [];
2560
2789
  }
2561
- function readSafe(path) {
2562
- try {
2563
- return readFileSync2(path, "utf-8");
2564
- } catch {
2565
- return null;
2790
+ function readSafe(path) {
2791
+ try {
2792
+ return readFileSync(path, "utf-8");
2793
+ } catch {
2794
+ return null;
2795
+ }
2796
+ }
2797
+ function readTotalMem() {
2798
+ try {
2799
+ const memInfo = readSafe("/proc/meminfo");
2800
+ if (memInfo) {
2801
+ const match = memInfo.match(/MemTotal:\s+(\d+)/);
2802
+ if (match) return parseInt(match[1], 10);
2803
+ }
2804
+ } catch {
2805
+ }
2806
+ return 16e6;
2807
+ }
2808
+ function readUptime() {
2809
+ try {
2810
+ const data = readSafe("/proc/uptime");
2811
+ if (data) {
2812
+ return parseFloat(data.split(/\s+/)[0] ?? "0");
2813
+ }
2814
+ } catch {
2815
+ }
2816
+ return 0;
2817
+ }
2818
+ function formatStartTime(starttime, uptimeSeconds, hertz) {
2819
+ try {
2820
+ const bootTime = Date.now() / 1e3 - uptimeSeconds;
2821
+ const processStart = bootTime + starttime / hertz;
2822
+ return new Date(processStart * 1e3).toISOString();
2823
+ } catch {
2824
+ return null;
2825
+ }
2826
+ }
2827
+ function getSystemProcesses(filter) {
2828
+ let processes;
2829
+ if (process.platform === "win32") {
2830
+ processes = runTasklistProcesses();
2831
+ } else if (existsSync2("/proc")) {
2832
+ processes = readProcProcesses();
2833
+ } else {
2834
+ processes = runPsProcesses();
2835
+ }
2836
+ if (filter) {
2837
+ if (filter.userOnly) {
2838
+ processes = processes.filter((p) => p.isUserProcess);
2839
+ }
2840
+ if (filter.name) {
2841
+ const nameLower = filter.name.toLowerCase();
2842
+ processes = processes.filter(
2843
+ (p) => p.name.toLowerCase().includes(nameLower) || p.command.toLowerCase().includes(nameLower)
2844
+ );
2845
+ }
2846
+ if (filter.pid) {
2847
+ processes = processes.filter((p) => p.pid === filter.pid);
2848
+ }
2849
+ if (filter.port) {
2850
+ processes = processes.filter((p) => p.ports.includes(filter.port));
2851
+ }
2852
+ if (filter.sortBy) {
2853
+ processes.sort((a, b) => {
2854
+ switch (filter.sortBy) {
2855
+ case "cpu":
2856
+ return b.cpuPercent - a.cpuPercent;
2857
+ case "mem":
2858
+ return b.memPercent - a.memPercent;
2859
+ case "pid":
2860
+ return a.pid - b.pid;
2861
+ case "name":
2862
+ return a.name.localeCompare(b.name);
2863
+ default:
2864
+ return b.cpuPercent - a.cpuPercent;
2865
+ }
2866
+ });
2867
+ }
2868
+ if (filter.limit && filter.limit > 0) {
2869
+ processes = processes.slice(0, filter.limit);
2870
+ }
2871
+ }
2872
+ return processes;
2873
+ }
2874
+ function killProcess(pid, signal = "SIGTERM") {
2875
+ try {
2876
+ process.kill(pid, signal);
2877
+ return true;
2878
+ } catch {
2879
+ return false;
2880
+ }
2881
+ }
2882
+ function isProcessRunning(pid) {
2883
+ try {
2884
+ process.kill(pid, 0);
2885
+ return true;
2886
+ } catch {
2887
+ return false;
2888
+ }
2889
+ }
2890
+ function checkPort(port, host = "127.0.0.1", timeout = 1e3) {
2891
+ const tryHost = (h) => new Promise((resolve13) => {
2892
+ const socket = new net.Socket();
2893
+ let settled = false;
2894
+ const finish = (result) => {
2895
+ if (settled) return;
2896
+ settled = true;
2897
+ socket.destroy();
2898
+ resolve13(result);
2899
+ };
2900
+ socket.setTimeout(timeout);
2901
+ socket.once("connect", () => finish(true));
2902
+ socket.once("timeout", () => finish(false));
2903
+ socket.once("error", () => finish(false));
2904
+ socket.connect(port, h);
2905
+ });
2906
+ return tryHost(host).then((ok) => ok ? true : tryHost("::1"));
2907
+ }
2908
+ async function checkHttp(url, timeout = 2e3) {
2909
+ const controller = new AbortController();
2910
+ const timer = setTimeout(() => controller.abort(), timeout);
2911
+ try {
2912
+ const res = await fetch(url, { method: "GET", signal: controller.signal, redirect: "manual" });
2913
+ return res.status >= 200 && res.status < 400;
2914
+ } catch {
2915
+ return false;
2916
+ } finally {
2917
+ clearTimeout(timer);
2918
+ }
2919
+ }
2920
+ function resolveHealthUrl(healthCheck, port) {
2921
+ if (/^https?:\/\//i.test(healthCheck)) return healthCheck;
2922
+ if (healthCheck.startsWith("/")) {
2923
+ if (!port) return null;
2924
+ return `http://127.0.0.1:${port}${healthCheck}`;
2925
+ }
2926
+ return null;
2927
+ }
2928
+ function findPidOnPort(port) {
2929
+ if (process.platform === "win32") return findPidOnPortWindows(port);
2930
+ if (process.platform === "darwin") return findPidOnPortMac(port);
2931
+ return findPidOnPortLinux(port);
2932
+ }
2933
+ function findPidOnPortLinux(port) {
2934
+ try {
2935
+ const hexPort = port.toString(16).toUpperCase().padStart(4, "0");
2936
+ const inodes = /* @__PURE__ */ new Set();
2937
+ for (const f of ["/proc/net/tcp", "/proc/net/tcp6"]) {
2938
+ if (!existsSync2(f)) continue;
2939
+ for (const line of readFileSync(f, "utf-8").split("\n").slice(1)) {
2940
+ const cols = line.trim().split(/\s+/);
2941
+ if (cols.length < 10) continue;
2942
+ if (cols[3] !== "0A") continue;
2943
+ const localPort = cols[1]?.split(":")[1];
2944
+ const inode = parseInt(cols[9] ?? "", 10);
2945
+ if (localPort === hexPort && !Number.isNaN(inode)) inodes.add(inode);
2946
+ }
2947
+ }
2948
+ if (inodes.size === 0) return null;
2949
+ for (const pidStr of readdirSync("/proc").filter((d) => /^\d+$/.test(d))) {
2950
+ const pid = parseInt(pidStr, 10);
2951
+ try {
2952
+ const fdDir = `/proc/${pid}/fd`;
2953
+ for (const fd of readdirSync(fdDir)) {
2954
+ const link = readlinkSync(`${fdDir}/${fd}`);
2955
+ const m = link.match(/socket:\[(\d+)\]/);
2956
+ if (m && inodes.has(parseInt(m[1], 10))) {
2957
+ return { pid, command: getProcessCmdline(pid) ?? "" };
2958
+ }
2959
+ }
2960
+ } catch {
2961
+ }
2962
+ }
2963
+ return null;
2964
+ } catch {
2965
+ return null;
2966
+ }
2967
+ }
2968
+ function findPidOnPortMac(port) {
2969
+ try {
2970
+ const out = execSync(`lsof -i :${port} -sTCP:LISTEN -P -n 2>/dev/null`, { encoding: "utf-8", timeout: 5e3 });
2971
+ for (const line of out.split("\n")) {
2972
+ if (!line.includes("LISTEN")) continue;
2973
+ const parts = line.trim().split(/\s+/);
2974
+ if (parts.length >= 2) {
2975
+ const pid = parseInt(parts[1], 10);
2976
+ if (!isNaN(pid)) return { pid, command: parts[0] ?? "" };
2977
+ }
2978
+ }
2979
+ return null;
2980
+ } catch {
2981
+ return null;
2982
+ }
2983
+ }
2984
+ function findPidOnPortWindows(port) {
2985
+ try {
2986
+ const out = execSync(`netstat -ano -p tcp 2>nul`, { encoding: "utf-8", timeout: 5e3 });
2987
+ const target = `:${port} `;
2988
+ for (const line of out.split("\n")) {
2989
+ const upper = line.toUpperCase();
2990
+ if (!upper.includes("LISTENING")) continue;
2991
+ if (!upper.includes(target)) continue;
2992
+ const pid = parseInt(line.trim().split(/\s+/).pop() ?? "", 10);
2993
+ if (!isNaN(pid)) return { pid, command: getProcessCmdline(pid) ?? "" };
2994
+ }
2995
+ return null;
2996
+ } catch {
2997
+ return null;
2998
+ }
2999
+ }
3000
+ function getProcessCmdline(pid) {
3001
+ if (process.platform === "win32") return getProcessCmdlineWindows(pid);
3002
+ if (process.platform === "darwin") return getProcessCmdlineMac(pid);
3003
+ return getProcessCmdlineLinux(pid);
3004
+ }
3005
+ function getProcessCmdlineLinux(pid) {
3006
+ try {
3007
+ const raw = readFileSync(`/proc/${pid}/cmdline`, "utf-8");
3008
+ const joined = raw.split("\0").filter(Boolean).join(" ").trim();
3009
+ return joined.length > 0 ? joined : null;
3010
+ } catch {
3011
+ return null;
3012
+ }
3013
+ }
3014
+ function getProcessCmdlineMac(pid) {
3015
+ try {
3016
+ const out = execSync(`ps -p ${pid} -o command=`, { encoding: "utf-8", timeout: 3e3 });
3017
+ const trimmed = out.trim();
3018
+ return trimmed.length > 0 ? trimmed : null;
3019
+ } catch {
3020
+ return null;
3021
+ }
3022
+ }
3023
+ function getProcessCmdlineWindows(pid) {
3024
+ try {
3025
+ const out = execSync(`wmic process where ProcessId=${pid} get CommandLine /value 2>nul`, { encoding: "utf-8", timeout: 3e3 });
3026
+ const line = out.split("\n").find((l) => l.includes("="));
3027
+ const value = line?.split("=")[1]?.trim();
3028
+ return value ? value : null;
3029
+ } catch {
3030
+ return null;
3031
+ }
3032
+ }
3033
+ function getProcessCwd(pid) {
3034
+ if (process.platform === "linux") {
3035
+ try {
3036
+ return readlinkSync(`/proc/${pid}/cwd`) || null;
3037
+ } catch {
3038
+ return null;
3039
+ }
3040
+ }
3041
+ if (process.platform === "darwin") {
3042
+ try {
3043
+ const out = execSync(`lsof -p ${pid} -a -d cwd -F n 2>/dev/null`, { encoding: "utf-8", timeout: 3e3 });
3044
+ const line = out.split("\n").find((l) => l.startsWith("n"));
3045
+ return line ? line.slice(1) : null;
3046
+ } catch {
3047
+ return null;
3048
+ }
3049
+ }
3050
+ return null;
3051
+ }
3052
+ function getProcessEnviron(pid) {
3053
+ try {
3054
+ const raw = readFileSync(`/proc/${pid}/environ`, "utf-8");
3055
+ const env = {};
3056
+ for (const pair of raw.split("\0")) {
3057
+ if (!pair) continue;
3058
+ const eq = pair.indexOf("=");
3059
+ if (eq === -1) continue;
3060
+ env[pair.slice(0, eq)] = pair.slice(eq + 1);
3061
+ }
3062
+ return env;
3063
+ } catch {
3064
+ return null;
3065
+ }
3066
+ }
3067
+ function formatProcessState(state) {
3068
+ switch (state) {
3069
+ case "R":
3070
+ return "Running";
3071
+ case "S":
3072
+ return "Sleeping";
3073
+ case "D":
3074
+ return "Disk Sleep";
3075
+ case "Z":
3076
+ return "Zombie";
3077
+ case "T":
3078
+ return "Stopped";
3079
+ case "t":
3080
+ return "Tracing";
3081
+ case "X":
3082
+ return "Dead";
3083
+ case "I":
3084
+ return "Idle";
3085
+ default:
3086
+ return state;
3087
+ }
3088
+ }
3089
+
3090
+ // src/commands/tracker.ts
3091
+ var RESTART_CAUSE = "fennec:restart";
3092
+ function getFennecDir() {
3093
+ return process.env.FENNEC_DATA_DIR ? resolve2(process.env.FENNEC_DATA_DIR) : resolve2(homedir(), ".fennec");
3094
+ }
3095
+ function getTrackedPath() {
3096
+ return resolve2(getFennecDir(), "tracked.json");
3097
+ }
3098
+ function getSupervisorPidPath() {
3099
+ return resolve2(getFennecDir(), "supervisor.pid");
3100
+ }
3101
+ function logFilePathFor(name) {
3102
+ return resolve2(getFennecDir(), "logs", `${name}.log`);
3103
+ }
3104
+ function readTracked() {
3105
+ try {
3106
+ const path = getTrackedPath();
3107
+ if (!existsSync3(path)) return [];
3108
+ return JSON.parse(readFileSync2(path, "utf-8"));
3109
+ } catch {
3110
+ return [];
3111
+ }
3112
+ }
3113
+ function saveTracked(processes) {
3114
+ try {
3115
+ const path = getTrackedPath();
3116
+ mkdirSync(dirname(path), { recursive: true });
3117
+ writeFileSync(path, JSON.stringify(processes, null, 2), "utf-8");
3118
+ } catch (err) {
3119
+ console.error("[fennec] Failed to save tracked processes:", err instanceof Error ? err.message : String(err));
3120
+ }
3121
+ }
3122
+ function addTracked(proc) {
3123
+ const tracked = readTracked();
3124
+ const filtered = tracked.filter((t) => t.name !== proc.name);
3125
+ filtered.push(proc);
3126
+ saveTracked(filtered);
3127
+ }
3128
+ function removeTracked(name) {
3129
+ const tracked = readTracked();
3130
+ saveTracked(tracked.filter((t) => t.name !== name));
3131
+ }
3132
+ function setAutoRestart(name, value) {
3133
+ const tracked = readTracked();
3134
+ let changed = false;
3135
+ for (const t of tracked) {
3136
+ if (t.name === name) {
3137
+ t.autoRestart = value;
3138
+ changed = true;
3139
+ }
3140
+ }
3141
+ if (changed) saveTracked(tracked);
3142
+ }
3143
+ function removeTrackedByPid(pid) {
3144
+ const tracked = readTracked();
3145
+ saveTracked(tracked.filter((t) => t.pid !== pid));
3146
+ }
3147
+ function resolveArgs(proc) {
3148
+ if (proc.args && proc.args.length > 0) return proc.args;
3149
+ return proc.command.split(/\s+/).filter(Boolean);
3150
+ }
3151
+ function adoptProcess(pid, opts = {}) {
3152
+ if (!isProcessRunning(pid)) return null;
3153
+ const command2 = opts.command ?? getProcessCmdline(pid) ?? `pid ${pid}`;
3154
+ const cwd = opts.cwd ?? getProcessCwd(pid) ?? void 0;
3155
+ const name = opts.name ?? `pid_${pid}`;
3156
+ const entry = {
3157
+ name,
3158
+ pid,
3159
+ command: command2,
3160
+ port: opts.port,
3161
+ cwd,
3162
+ env: opts.env,
3163
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
3164
+ autoRestart: opts.autoRestart ?? true,
3165
+ flapping: false
3166
+ };
3167
+ addTracked(entry);
3168
+ return entry;
3169
+ }
3170
+ function adoptExternalOnPort(port, name) {
3171
+ const occ = findPidOnPort(port);
3172
+ if (!occ) return null;
3173
+ const tracked = readTracked();
3174
+ if (tracked.some((t) => t.pid === occ.pid)) return null;
3175
+ return adoptProcess(occ.pid, { port, name });
3176
+ }
3177
+ function isTrackedRunning(proc) {
3178
+ if (!isProcessRunning(proc.pid)) return false;
3179
+ const environ = getProcessEnviron(proc.pid);
3180
+ if (environ && "FENNEC_APP_NAME" in environ) {
3181
+ return environ.FENNEC_APP_NAME === proc.name;
3182
+ }
3183
+ const cmdline = getProcessCmdline(proc.pid);
3184
+ if (!cmdline) return true;
3185
+ const argv = resolveArgs(proc);
3186
+ const exe = argv[0];
3187
+ if (!exe) return true;
3188
+ const exeBase = basename(exe);
3189
+ if (exeBase.length === 0) return true;
3190
+ return cmdline.includes(exeBase);
3191
+ }
3192
+ function formatUptime(seconds) {
3193
+ if (seconds < 60) return `${seconds}s`;
3194
+ if (seconds < 3600) return `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
3195
+ const h = Math.floor(seconds / 3600);
3196
+ const m = Math.floor(seconds % 3600 / 60);
3197
+ if (h >= 24) {
3198
+ const d = Math.floor(h / 24);
3199
+ return `${d}d ${h % 24}h ${m}m`;
3200
+ }
3201
+ return `${h}h ${m}m`;
3202
+ }
3203
+ var DEFAULT_MAX_LOG_SIZE = 10 * 1024 * 1024;
3204
+ var DEFAULT_MAX_LOG_FILES = 3;
3205
+ function rotateLogFile(filePath, maxSize = DEFAULT_MAX_LOG_SIZE, maxFiles = DEFAULT_MAX_LOG_FILES) {
3206
+ try {
3207
+ if (!existsSync3(filePath)) return false;
3208
+ const stats = statSync(filePath);
3209
+ if (stats.size < maxSize) return false;
3210
+ const dir = dirname(filePath);
3211
+ const base = basename(filePath);
3212
+ const oldestPath = resolve2(dir, `${base}.${maxFiles}`);
3213
+ if (existsSync3(oldestPath)) {
3214
+ try {
3215
+ rmSync(oldestPath);
3216
+ } catch {
3217
+ }
3218
+ }
3219
+ for (let i = maxFiles - 1; i >= 1; i--) {
3220
+ const src = resolve2(dir, `${base}.${i}`);
3221
+ const dst = resolve2(dir, `${base}.${i + 1}`);
3222
+ if (existsSync3(src)) {
3223
+ try {
3224
+ renameSync(src, dst);
3225
+ } catch {
3226
+ }
3227
+ }
3228
+ }
3229
+ try {
3230
+ renameSync(filePath, resolve2(dir, `${base}.1`));
3231
+ } catch {
3232
+ }
3233
+ return true;
3234
+ } catch (err) {
3235
+ console.error("[fennec] Log rotation failed:", err instanceof Error ? err.message : String(err));
3236
+ return false;
3237
+ }
3238
+ }
3239
+ function buildSpawnEnv(stored) {
3240
+ const merged = { ...process.env };
3241
+ for (const [k, v] of Object.entries(stored ?? {})) {
3242
+ if (v !== void 0) merged[k] = v;
3243
+ }
3244
+ if (process.env.FENNEC_DATA_DIR) merged.FENNEC_DATA_DIR = process.env.FENNEC_DATA_DIR;
3245
+ if (process.env.HOME) merged.HOME = process.env.HOME;
3246
+ return merged;
3247
+ }
3248
+ function respawnTracked(proc, cause) {
3249
+ const cmdParts = resolveArgs(proc);
3250
+ const logFilePath = logFilePathFor(proc.name);
3251
+ mkdirSync(dirname(logFilePath), { recursive: true });
3252
+ if (cause) annotateRestart(logFilePath, cause);
3253
+ if (isProcessRunning(proc.pid)) {
3254
+ try {
3255
+ process.kill(proc.pid, "SIGTERM");
3256
+ } catch {
3257
+ }
3258
+ }
3259
+ const child = spawnDaemon({
3260
+ cmdParts,
3261
+ name: proc.name,
3262
+ cwd: proc.cwd,
3263
+ logFilePath,
3264
+ env: buildSpawnEnv(proc.env),
3265
+ logMode: proc.logMode
3266
+ });
3267
+ const pid = child.pid ?? 0;
3268
+ addTracked({
3269
+ ...proc,
3270
+ pid,
3271
+ args: cmdParts,
3272
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
3273
+ restartCause: cause
3274
+ });
3275
+ return pid;
3276
+ }
3277
+ function spawnDaemon(opts) {
3278
+ rotateLogFile(opts.logFilePath);
3279
+ const env = { ...opts.env ?? buildSpawnEnv(), FENNEC_APP_NAME: opts.name };
3280
+ if (opts.logMode === "jsonl") {
3281
+ return spawnDaemonJsonl(opts, env);
3282
+ }
3283
+ const fd = openSync(opts.logFilePath, "a");
3284
+ const child = spawn(opts.cmdParts[0], opts.cmdParts.slice(1), {
3285
+ cwd: opts.cwd,
3286
+ env,
3287
+ stdio: ["ignore", fd, fd],
3288
+ detached: true
3289
+ });
3290
+ child.unref();
3291
+ child.once("spawn", () => {
3292
+ try {
3293
+ closeSync(fd);
3294
+ } catch {
3295
+ }
3296
+ });
3297
+ return child;
3298
+ }
3299
+ function spawnDaemonJsonl(opts, env) {
3300
+ const relay = spawn(
3301
+ process.execPath,
3302
+ ["-e", JSONL_RELAY],
3303
+ {
3304
+ env: { ...process.env, FENNEC_RELAY_LOG: opts.logFilePath },
3305
+ stdio: ["pipe", "ignore", "ignore"],
3306
+ detached: true
3307
+ }
3308
+ );
3309
+ relay.unref();
3310
+ const child = spawn(opts.cmdParts[0], opts.cmdParts.slice(1), {
3311
+ cwd: opts.cwd,
3312
+ env,
3313
+ stdio: ["ignore", relay.stdin, relay.stdin],
3314
+ detached: true
3315
+ });
3316
+ child.unref();
3317
+ try {
3318
+ relay.stdin?.destroy();
3319
+ } catch {
3320
+ }
3321
+ return child;
3322
+ }
3323
+ var JSONL_RELAY = `
3324
+ const fs = require('fs');
3325
+ const fd = fs.openSync(process.env.FENNEC_RELAY_LOG, 'a');
3326
+ let buf = '';
3327
+ const SECRET = new RegExp('(password|passwd|token|secret|api[_-]?key|access[_-]?key).{0,60}', 'gi');
3328
+ function redact(s){ return s.replace(SECRET, '$1***'); }
3329
+ function level(s){
3330
+ const t = s.toLowerCase();
3331
+ if (t.indexOf('error')>=0 || t.indexOf('fail')>=0 || t.indexOf('fatal')>=0 || t.indexOf('exception')>=0 || t.indexOf('denied')>=0 || t.indexOf('refused')>=0 || t.indexOf('cannot')>=0 || t.indexOf('panic')>=0) return 'error';
3332
+ if (t.indexOf('warn')>=0) return 'warn';
3333
+ return 'info';
3334
+ }
3335
+ function flush(){
3336
+ const parts = buf.split('\\n');
3337
+ buf = parts.pop() || '';
3338
+ for (const r of parts){
3339
+ if (!r) continue;
3340
+ const line = redact(r);
3341
+ try { fs.writeSync(fd, JSON.stringify({ ts: new Date().toISOString(), level: level(line), source: 'app', line }) + '\\n'); } catch (e) {}
3342
+ }
3343
+ }
3344
+ process.stdin.on('data', (c) => { buf += c.toString('utf8'); flush(); });
3345
+ process.stdin.on('end', () => { flush(); try { fs.closeSync(fd); } catch (e) {} process.exit(0); });
3346
+ process.stdin.on('error', () => { process.exit(0); });
3347
+ `;
3348
+ function annotateRestart(logFilePath, cause) {
3349
+ try {
3350
+ const fd = openSync(logFilePath, "a");
3351
+ try {
3352
+ writeSync(fd, `
3353
+ ${RESTART_CAUSE} cause=${cause} at=${(/* @__PURE__ */ new Date()).toISOString()}
3354
+ `);
3355
+ } finally {
3356
+ closeSync(fd);
3357
+ }
3358
+ } catch {
3359
+ }
3360
+ }
3361
+
3362
+ // src/commands/supervisor.ts
3363
+ import pc4 from "picocolors";
3364
+ import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync2, unlinkSync, mkdirSync as mkdirSync2, openSync as openSync2, closeSync as closeSync2 } from "fs";
3365
+ import { spawn as spawn2 } from "child_process";
3366
+ import { dirname as dirname2 } from "path";
3367
+ var POLL_INTERVAL_MS = 3e3;
3368
+ var CRASH_WINDOW_MS = 6e4;
3369
+ var MAX_RESTARTS_IN_WINDOW = 5;
3370
+ var FLAPPING_THRESHOLD = 3;
3371
+ var PORT_GRACE_MS = 15e3;
3372
+ var LOG_ROTATE_CHECK_MS = 60 * 60 * 1e3;
3373
+ function getSupervisorPid() {
3374
+ const path = getSupervisorPidPath();
3375
+ if (!existsSync4(path)) return null;
3376
+ const pid = parseInt(readFileSync3(path, "utf-8").trim(), 10);
3377
+ if (isNaN(pid)) return null;
3378
+ return isProcessRunning(pid) ? pid : null;
3379
+ }
3380
+ function ensureSupervisorRunning() {
3381
+ const existing = getSupervisorPid();
3382
+ if (existing) return existing;
3383
+ return startSupervisorDaemon();
3384
+ }
3385
+ function startSupervisorDaemon() {
3386
+ const logFilePath = logFilePathFor("supervisor");
3387
+ mkdirSync2(dirname2(logFilePath), { recursive: true });
3388
+ const fd = openSync2(logFilePath, "a");
3389
+ const child = spawn2(process.execPath, [process.argv[1], "__supervisor"], {
3390
+ env: { ...process.env },
3391
+ stdio: ["ignore", fd, fd],
3392
+ detached: true
3393
+ });
3394
+ child.unref();
3395
+ child.once("spawn", () => {
3396
+ try {
3397
+ closeSync2(fd);
3398
+ } catch {
3399
+ }
3400
+ });
3401
+ return child.pid ?? 0;
3402
+ }
3403
+ async function supervisorCommand(args2) {
3404
+ const sub = args2[0] ?? "status";
3405
+ switch (sub) {
3406
+ case "start":
3407
+ return supervisorStart();
3408
+ case "stop":
3409
+ return supervisorStop();
3410
+ case "restart":
3411
+ await supervisorStop();
3412
+ return supervisorStart();
3413
+ case "status":
3414
+ return supervisorStatus();
3415
+ default:
3416
+ console.error(renderError("Unknown sub-command", `"${sub}" \u2014 use: start | stop | status | restart`));
3417
+ process.exit(1);
2566
3418
  }
2567
3419
  }
2568
- function readTotalMem() {
2569
- try {
2570
- const memInfo = readSafe("/proc/meminfo");
2571
- if (memInfo) {
2572
- const match = memInfo.match(/MemTotal:\s+(\d+)/);
2573
- if (match) return parseInt(match[1], 10);
2574
- }
2575
- } catch {
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
- return 16e6;
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 readUptime() {
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
- const data = readSafe("/proc/uptime");
2582
- if (data) {
2583
- return parseFloat(data.split(/\s+/)[0] ?? "0");
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
- const bootTime = Date.now() / 1e3 - uptimeSeconds;
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 getSystemProcesses(filter) {
2599
- let processes;
2600
- if (existsSync3("/proc")) {
2601
- processes = readProcProcesses();
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
- processes = runPsProcesses();
3465
+ console.error(` ${pc4.red("\u25CB")} not running ${pc4.dim("\u2014 start with")} ${pc4.cyan("fennec supervisor start")}`);
2604
3466
  }
2605
- if (filter) {
2606
- if (filter.userOnly) {
2607
- processes = processes.filter((p) => p.isUserProcess);
2608
- }
2609
- if (filter.name) {
2610
- const nameLower = filter.name.toLowerCase();
2611
- processes = processes.filter(
2612
- (p) => p.name.toLowerCase().includes(nameLower) || p.command.toLowerCase().includes(nameLower)
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
- if (filter.pid) {
2616
- processes = processes.filter((p) => p.pid === filter.pid);
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
- if (filter.port) {
2619
- processes = processes.filter((p) => p.ports.includes(filter.port));
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
- if (filter.sortBy) {
2622
- processes.sort((a, b) => {
2623
- switch (filter.sortBy) {
2624
- case "cpu":
2625
- return b.cpuPercent - a.cpuPercent;
2626
- case "mem":
2627
- return b.memPercent - a.memPercent;
2628
- case "pid":
2629
- return a.pid - b.pid;
2630
- case "name":
2631
- return a.name.localeCompare(b.name);
2632
- default:
2633
- return b.cpuPercent - a.cpuPercent;
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
- if (filter.limit && filter.limit > 0) {
2638
- processes = processes.slice(0, filter.limit);
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 killProcess(pid, signal = "SIGTERM") {
3599
+ function setFlapping(t, val) {
3600
+ if (!!t.flapping === val) return;
3601
+ t.flapping = val;
2644
3602
  try {
2645
- process.kill(pid, signal);
2646
- return true;
3603
+ addTracked({ ...t, flapping: val });
2647
3604
  } catch {
2648
- return false;
2649
3605
  }
2650
3606
  }
2651
- function isProcessRunning(pid) {
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
- process.kill(pid, 0);
2654
- return true;
2655
- } catch {
2656
- return false;
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 formatProcessState(state) {
2660
- switch (state) {
2661
- case "R":
2662
- return "Running";
2663
- case "S":
2664
- return "Sleeping";
2665
- case "D":
2666
- return "Disk Sleep";
2667
- case "Z":
2668
- return "Zombie";
2669
- case "T":
2670
- return "Stopped";
2671
- case "t":
2672
- return "Tracing";
2673
- case "X":
2674
- return "Dead";
2675
- case "I":
2676
- return "Idle";
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
- return state;
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 pc4 from "picocolors";
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
- ${pc4.dim("No system processes found.")}
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) => pc4.dim(String(v).padStart(6)) },
2712
- { key: "name", label: "Name", format: (v) => pc4.bold(String(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 ? pc4.red(String(n)) : n > 5 ? pc4.yellow(String(n)) : pc4.dim(String(n));
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 ? pc4.red(String(n)) : n > 5 ? pc4.yellow(String(n)) : pc4.dim(String(n));
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 pc4.green(s);
2724
- if (s === "Z" || s === "Zombie") return pc4.red(s);
2725
- if (s === "S" || s === "Sleeping") return pc4.cyan(s);
2726
- return pc4.dim(s);
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} ${pc4.bold("System Processes")} ${pc4.dim(`(top ${processes.length} by ${sortBy})`)}
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
- ${pc4.dim("No tracked processes.")}`);
2745
- console.error(` ${pc4.dim("Start an app with:")} ${pc4.cyan("fennec start <command> --name <name>")}
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) => pc4.bold(String(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" ? pc4.green("\u25CF running") : pc4.red("\u25CB stopped");
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 ? pc4.yellow(`:${p}`) : pc4.dim("-");
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) => pc4.dim(String(v)) }
3899
+ { key: "uptime", label: "Uptime", format: (v) => pc6.dim(String(v)) }
2765
3900
  ];
2766
3901
  const rows = tracked.map((t) => {
2767
- const running = isProcessRunning(t.pid);
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) : pc4.dim(String(t.pid)), status: running ? "running" : "stopped", port: t.port ?? null, command: t.command, uptime };
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) => isProcessRunning(t.pid)).length;
3906
+ const runningCount = tracked.filter((t) => isTrackedRunning(t)).length;
2772
3907
  console.error(`
2773
- ${symbols.fox} ${pc4.bold("Fennec Apps")} ${pc4.dim(`(${runningCount}/${tracked.length} running)`)}
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(` ${pc4.dim("Use")} ${pc4.cyan("fennec start <command> --name <name> --port <port>")} ${pc4.dim("to add more apps.")}`);
2777
- console.error(` ${pc4.dim("Use")} ${pc4.cyan("fennec log <name>")} ${pc4.dim("to view logs.")}`);
2778
- console.error(` ${pc4.dim("Use")} ${pc4.cyan("fennec stop <name>")} ${pc4.dim("to pause an app.")}`);
2779
- console.error(` ${pc4.dim("Use")} ${pc4.cyan("fennec spawn <name>")} ${pc4.dim("to resume a paused app.")}`);
2780
- console.error(` ${pc4.dim("Use")} ${pc4.cyan("fennec kill <name>")} ${pc4.dim("to permanently remove an app.")}`);
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 = isProcessRunning(t.pid);
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} ${pc4.bold("Fennec Status")}
3941
+ ${symbols.fox} ${pc6.bold("Fennec Status")}
2807
3942
  `);
2808
3943
  if (tracked.length > 0) {
2809
- const runningCount = tracked.filter((t) => isProcessRunning(t.pid)).length;
2810
- console.error(` ${pc4.bold("Managed Apps")} ${pc4.dim(`(${runningCount}/${tracked.length} running)`)}
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 = isProcessRunning(t.pid);
2814
- const statusIcon = running ? pc4.green("\u25CF") : pc4.red("\u25CB");
2815
- const portStr = t.port ? ` ${pc4.yellow(`:${t.port}`)}` : "";
2816
- const uptime = running ? pc4.dim(formatUptime(Math.floor((Date.now() - new Date(t.startedAt).getTime()) / 1e3))) : pc4.red("stopped");
2817
- console.error(` ${statusIcon} ${pc4.bold(t.name)}${portStr} ${pc4.dim(`(PID ${t.pid})`)} \u2014 ${uptime}`);
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(` ${pc4.dim("No managed apps.")} ${pc4.cyan("fennec start <command> --name <name>")}
3956
+ console.error(` ${pc6.dim("No managed apps.")} ${pc6.cyan("fennec start <command> --name <name>")}
2822
3957
  `);
2823
3958
  }
2824
- console.error(` ${pc4.bold("System")} ${pc4.dim(`(${totalUserProcs} user processes)`)}`);
3959
+ console.error(` ${pc6.bold("System")} ${pc6.dim(`(${totalUserProcs} user processes)`)}`);
2825
3960
  for (const p of topSystem) {
2826
- const cpuStr = p.cpuPercent > 10 ? pc4.red(`${p.cpuPercent}%`) : p.cpuPercent > 5 ? pc4.yellow(`${p.cpuPercent}%`) : pc4.dim(`${p.cpuPercent}%`);
2827
- const memStr = p.memPercent > 10 ? pc4.red(`${p.memPercent}%`) : p.memPercent > 5 ? pc4.yellow(`${p.memPercent}%`) : pc4.dim(`${p.memPercent}%`);
2828
- console.error(` ${pc4.dim(`PID ${p.pid}`)} ${pc4.bold(p.name)} \u2014 CPU: ${cpuStr} MEM: ${memStr}`);
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
- ${pc4.bold("Watching system processes")} ${pc4.dim("(Ctrl+C to stop, refreshes every 3s)")}
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) => pc4.dim(String(v).padStart(6)) },
2841
- { key: "name", label: "Name", format: (v) => pc4.bold(String(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 ? pc4.red(String(n)) : n > 5 ? pc4.yellow(String(n)) : pc4.dim(String(n));
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 ? pc4.red(String(n)) : n > 5 ? pc4.yellow(String(n)) : pc4.dim(String(n));
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()} ${pc4.dim(`${processes.length} processes`)}
3988
+ return ` ${timestamp()} ${pc6.dim(`${processes.length} processes`)}
2854
3989
  ${renderTable(columns, rows, { compact: true })}`;
2855
3990
  };
2856
3991
  console.error(render());
@@ -2858,19 +3993,35 @@ ${renderTable(columns, rows, { compact: true })}`;
2858
3993
  process.stdout.write("\x1B[J");
2859
3994
  console.error(render());
2860
3995
  }, 3e3);
2861
- await new Promise((resolve12) => {
3996
+ await new Promise((resolve13) => {
2862
3997
  process.once("SIGINT", () => {
2863
3998
  clearInterval(interval);
2864
3999
  process.stdout.write("\x1B[J");
2865
- resolve12();
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(` ${pc5.dim("Starting Fennec MCP server...")}
4024
+ console.error(` ${pc7.dim("Starting Fennec MCP server...")}
2874
4025
  `);
2875
4026
  const configIndex = args2.indexOf("--config");
2876
4027
  const configPath = configIndex !== -1 ? args2[configIndex + 1] : void 0;
@@ -2885,22 +4036,22 @@ async function startServer(args2) {
2885
4036
  if (useSse) {
2886
4037
  const port = server.getConfig().transport.port;
2887
4038
  console.error(`
2888
- ${pc5.green("\u2713")} ${pc5.bold("Fennec server is running")}`);
2889
- console.error(` ${renderKV("Transport", pc5.cyan("SSE (HTTP)"))}`);
2890
- console.error(` ${renderKV("URL", pc5.cyan(`http://localhost:${port}/sse`))}`);
2891
- console.error(` ${renderKV("MCP Config", pc5.cyan(`{ "type": "remote", "url": "http://localhost:${port}/sse" }`))}`);
2892
- console.error(` ${renderKV("OpenCode", pc5.cyan(`type: remote, url: http://localhost:${port}/sse`))}`);
4039
+ ${pc7.green("\u2713")} ${pc7.bold("Fennec server is running")}`);
4040
+ console.error(` ${renderKV("Transport", pc7.cyan("SSE (HTTP)"))}`);
4041
+ console.error(` ${renderKV("URL", pc7.cyan(`http://localhost:${port}/sse`))}`);
4042
+ console.error(` ${renderKV("MCP Config", pc7.cyan(`{ "type": "remote", "url": "http://localhost:${port}/sse" }`))}`);
4043
+ console.error(` ${renderKV("OpenCode", pc7.cyan(`type: remote, url: http://localhost:${port}/sse`))}`);
2893
4044
  console.error(`
2894
- ${pc5.dim("Press Ctrl+C to stop")}
4045
+ ${pc7.dim("Press Ctrl+C to stop")}
2895
4046
  `);
2896
4047
  } else {
2897
4048
  console.error(`
2898
- ${pc5.green("\u2713")} ${pc5.bold("Fennec server is running")}`);
4049
+ ${pc7.green("\u2713")} ${pc7.bold("Fennec server is running")}`);
2899
4050
  console.error(` ${renderKV("Transport", "stdio")}`);
2900
4051
  console.error(` ${renderKV("AI Agent", "Connect via MCP protocol")}`);
2901
- console.error(` ${renderKV("Tip", pc5.dim("For SSE mode: fennec start --sse"))}`);
4052
+ console.error(` ${renderKV("Tip", pc7.dim("For SSE mode: fennec start --sse"))}`);
2902
4053
  console.error(`
2903
- ${pc5.dim("Press Ctrl+C to stop")}
4054
+ ${pc7.dim("Press Ctrl+C to stop")}
2904
4055
  `);
2905
4056
  }
2906
4057
  } catch (error) {
@@ -2910,7 +4061,7 @@ async function startServer(args2) {
2910
4061
  }
2911
4062
  async function startCommand(args2) {
2912
4063
  printBanner();
2913
- console.error(` ${pc5.dim("Starting app...")}
4064
+ console.error(` ${pc7.dim("Starting app...")}
2914
4065
  `);
2915
4066
  await runCommand(args2);
2916
4067
  }
@@ -2920,8 +4071,9 @@ async function runCommand(args2) {
2920
4071
  const portIndex = args2.indexOf("--port");
2921
4072
  const port = portIndex !== -1 ? parseInt(args2[portIndex + 1], 10) : void 0;
2922
4073
  const cwdIndex = args2.indexOf("--cwd");
2923
- const cwd = cwdIndex !== -1 ? args2[cwdIndex + 1] : void 0;
4074
+ const cwd = cwdIndex !== -1 ? resolve4(args2[cwdIndex + 1]) : process.cwd();
2924
4075
  const restartFlag = args2.includes("--restart");
4076
+ const jsonlFlag = args2.includes("--jsonl");
2925
4077
  const stopFlags = [nameIndex, portIndex, cwdIndex].filter((i) => i !== -1);
2926
4078
  const cmdEnd = Math.min(...stopFlags, Infinity);
2927
4079
  const cmdParts = args2.slice(0, cmdEnd);
@@ -2931,87 +4083,82 @@ async function runCommand(args2) {
2931
4083
  process.exit(1);
2932
4084
  }
2933
4085
  const appName = name ?? cmdParts[0] ?? "app";
4086
+ if (port !== void 0) {
4087
+ const adopted = adoptExternalOnPort(port, appName);
4088
+ if (adopted) {
4089
+ console.error(`
4090
+ ${pc7.green("\u2713")} ${pc7.bold("Adopted")} ${pc7.bold(adopted.name)} ${pc7.dim(`(PID ${adopted.pid}) \u2014 already listening on :${port}`)}`);
4091
+ console.error(` ${renderKV("Logs", pc7.cyan(`fennec log ${adopted.name}`))}`);
4092
+ console.error();
4093
+ return;
4094
+ }
4095
+ }
2934
4096
  const tracked = readTracked();
2935
4097
  const existing = tracked.find((t) => t.name === appName);
2936
- if (existing && isProcessRunning(existing.pid)) {
4098
+ if (existing && isTrackedRunning(existing)) {
2937
4099
  console.error();
2938
- console.error(` ${pc5.yellow("\u26A0")} ${pc5.bold(appName)} ${pc5.dim(`is already running (PID: ${existing.pid})`)}`);
2939
- console.error(` ${renderKV("Logs", pc5.cyan(`fennec log ${appName}`))}`);
2940
- console.error(` ${renderKV("Stop", pc5.cyan(`fennec kill ${appName}`))}`);
4100
+ console.error(` ${pc7.yellow("\u26A0")} ${pc7.bold(appName)} ${pc7.dim(`is already running (PID: ${existing.pid})`)}`);
4101
+ console.error(` ${renderKV("Logs", pc7.cyan(`fennec log ${appName}`))}`);
4102
+ console.error(` ${renderKV("Stop", pc7.cyan(`fennec kill ${appName}`))}`);
2941
4103
  console.error();
2942
4104
  process.exit(0);
2943
4105
  }
2944
- const logDir = resolve3(homedir2(), ".fennec", "logs");
2945
- mkdirSync2(logDir, { recursive: true });
2946
- const logFilePath = resolve3(logDir, `${appName}.log`);
4106
+ const logDir = dirname4(logFilePathFor(appName));
4107
+ mkdirSync4(logDir, { recursive: true });
4108
+ const logFilePath = logFilePathFor(appName);
2947
4109
  console.error(`
2948
- ${symbols.fox} ${pc5.bold("Starting")} ${renderAppName(appName)} ${pc5.dim("(daemon)")}
4110
+ ${symbols.fox} ${pc7.bold("Starting")} ${renderAppName(appName)} ${pc7.dim("(daemon)")}
2949
4111
  `);
2950
4112
  console.error(` ${renderKV("Command", cmd)}`);
2951
4113
  if (port) console.error(` ${renderKV("Port", String(port))}`);
2952
4114
  if (cwd) console.error(` ${renderKV("Directory", cwd)}`);
2953
- if (restartFlag) console.error(` ${renderKV("Auto-restart", pc5.green("enabled"))}`);
4115
+ if (restartFlag) console.error(` ${renderKV("Auto-restart", pc7.green("enabled"))}`);
2954
4116
  console.error(` ${divider()}`);
2955
4117
  try {
2956
- let currentChild = spawn(cmdParts[0], cmdParts.slice(1), {
2957
- cwd,
2958
- env: { ...process.env },
2959
- stdio: ["ignore", "pipe", "pipe"],
2960
- detached: true
2961
- });
4118
+ const startEnv = {};
4119
+ for (const [k, v] of Object.entries(process.env)) {
4120
+ if (v !== void 0) startEnv[k] = v;
4121
+ }
4122
+ const currentChild = spawnDaemon({ cmdParts, name: appName, cwd, logFilePath, env: buildSpawnEnv(startEnv), logMode: jsonlFlag ? "jsonl" : "text" });
2962
4123
  const pid = currentChild.pid ?? 0;
2963
- rotateLogFile(logFilePath);
2964
- const logStream = createWriteStream(logFilePath, { flags: "a" });
2965
- if (currentChild.stdout) currentChild.stdout.pipe(logStream);
2966
- if (currentChild.stderr) currentChild.stderr.pipe(logStream);
2967
- currentChild.unref();
2968
4124
  addTracked({
2969
4125
  name: appName,
2970
4126
  pid,
2971
4127
  command: cmd,
4128
+ args: cmdParts,
2972
4129
  port,
2973
4130
  cwd,
2974
- startedAt: (/* @__PURE__ */ new Date()).toISOString()
4131
+ env: startEnv,
4132
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
4133
+ autoRestart: restartFlag,
4134
+ logMode: jsonlFlag ? "jsonl" : "text"
2975
4135
  });
2976
- console.error(` ${pc5.green("\u2713")} ${pc5.bold(appName)} ${pc5.dim(`started (PID: ${pid})`)}`);
2977
- if (restartFlag) {
2978
- let setupRestartWatch2 = function(c) {
2979
- c.on("exit", (code, signal) => {
2980
- if (code !== 0 && signal !== "SIGTERM" && signal !== "SIGKILL") {
2981
- console.error(` ${pc5.yellow("\u26A0")} ${pc5.bold(appName)} ${pc5.dim(`exited with code ${code}, restarting...`)}`);
2982
- const restarted = spawn(cmdParts[0], cmdParts.slice(1), {
2983
- cwd,
2984
- env: { ...process.env },
2985
- stdio: ["ignore", "pipe", "pipe"],
2986
- detached: true
2987
- });
2988
- const newPid = restarted.pid ?? 0;
2989
- rotateLogFile(logFilePath);
2990
- const rs = createWriteStream(logFilePath, { flags: "a" });
2991
- if (restarted.stdout) restarted.stdout.pipe(rs);
2992
- if (restarted.stderr) restarted.stderr.pipe(rs);
2993
- restarted.unref();
2994
- addTracked({ name: appName, pid: newPid, command: cmd, port, cwd, startedAt: (/* @__PURE__ */ new Date()).toISOString() });
2995
- console.error(` ${pc5.green("\u2713")} ${pc5.bold(appName)} ${pc5.dim(`restarted (PID: ${newPid})`)}`);
2996
- currentChild = restarted;
2997
- setupRestartWatch2(restarted);
2998
- }
2999
- });
3000
- };
3001
- var setupRestartWatch = setupRestartWatch2;
3002
- console.error(` ${pc5.dim("Auto-restart enabled \u2014 watching for crashes...")}
3003
- `);
3004
- setupRestartWatch2(currentChild);
3005
- await new Promise((resolve12) => {
3006
- process.once("SIGINT", () => {
3007
- currentChild.kill("SIGTERM");
3008
- resolve12();
3009
- });
3010
- });
4136
+ console.error(` ${pc7.green("\u2713")} ${pc7.bold(appName)} ${pc7.dim(`started (PID: ${pid})`)}`);
4137
+ const spinner = createSpinner(port ? `Waiting for ${appName} on port ${port}...` : `Confirming ${appName} is running...`);
4138
+ const ready = await waitForReady(pid, port);
4139
+ spinner.stop();
4140
+ process.stdout.write("\r\x1B[K");
4141
+ if (!ready.running) {
4142
+ removeTracked(appName);
4143
+ console.error(` ${pc7.red("\u2717")} ${pc7.bold(appName)} ${pc7.dim("exited immediately after start")}`);
4144
+ console.error(` ${renderKV("Logs", pc7.cyan(`fennec log ${appName}`))}`);
4145
+ console.error();
4146
+ process.exit(1);
4147
+ }
4148
+ if (port) {
4149
+ if (ready.portReady) {
4150
+ console.error(` ${pc7.green("\u2713")} ${pc7.bold(appName)} ${pc7.dim(`is listening on port ${port}`)}`);
4151
+ } else {
4152
+ console.error(` ${pc7.yellow("\u26A0")} ${pc7.bold(appName)} ${pc7.dim(`is running but port ${port} is not accepting connections yet`)}`);
4153
+ }
3011
4154
  }
3012
- if (!restartFlag) {
3013
- await psCommand([]);
4155
+ if (restartFlag) {
4156
+ const supPid = ensureSupervisorRunning();
4157
+ ensurePersistEnabled();
4158
+ console.error(` ${pc7.green("\u2713")} ${pc7.bold("Auto-restart")} ${pc7.dim(`managed by supervisor (PID ${supPid}) \u2014 survives terminal close`)}`);
4159
+ console.error(` ${renderKV("Supervisor", pc7.cyan("fennec supervisor status"))}`);
3014
4160
  }
4161
+ await psCommand([]);
3015
4162
  } catch (error) {
3016
4163
  console.error(renderError(`Failed to start ${appName}`, String(error)));
3017
4164
  process.exit(1);
@@ -3020,7 +4167,7 @@ async function runCommand(args2) {
3020
4167
  async function resurrectTracked() {
3021
4168
  const tracked = readTracked();
3022
4169
  if (tracked.length === 0) return;
3023
- const dead = tracked.filter((t) => !isProcessRunning(t.pid));
4170
+ const dead = tracked.filter((t) => !isTrackedRunning(t));
3024
4171
  if (dead.length === 0) return;
3025
4172
  const spinner = createSpinner(`Resurrecting ${dead.length} stopped process(es)...`);
3026
4173
  let resurrected = 0;
@@ -3031,28 +4178,21 @@ async function resurrectTracked() {
3031
4178
  continue;
3032
4179
  }
3033
4180
  try {
3034
- const cmdParts = proc.command.split(/\s+/);
3035
- const logDir = resolve3(homedir2(), ".fennec", "logs");
3036
- mkdirSync2(logDir, { recursive: true });
3037
- const logFilePath = resolve3(logDir, `${proc.name}.log`);
3038
- const child = spawn(cmdParts[0], cmdParts.slice(1), {
3039
- cwd: proc.cwd,
3040
- env: { ...process.env },
3041
- stdio: ["ignore", "pipe", "pipe"],
3042
- detached: true
3043
- });
3044
- rotateLogFile(logFilePath);
3045
- const logStream = createWriteStream(logFilePath, { flags: "a" });
3046
- if (child.stdout) child.stdout.pipe(logStream);
3047
- if (child.stderr) child.stderr.pipe(logStream);
3048
- child.unref();
4181
+ const cmdParts = resolveArgs(proc);
4182
+ const logFilePath = logFilePathFor(proc.name);
4183
+ const child = spawnDaemon({ cmdParts, name: proc.name, cwd: proc.cwd, logFilePath, env: buildSpawnEnv(proc.env), logMode: proc.logMode });
3049
4184
  addTracked({
3050
4185
  name: proc.name,
3051
4186
  pid: child.pid ?? 0,
3052
4187
  command: proc.command,
4188
+ args: cmdParts,
3053
4189
  port: proc.port,
3054
4190
  cwd: proc.cwd,
3055
- startedAt: (/* @__PURE__ */ new Date()).toISOString()
4191
+ env: proc.env,
4192
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
4193
+ autoRestart: proc.autoRestart,
4194
+ restartCause: proc.restartCause,
4195
+ logMode: proc.logMode
3056
4196
  });
3057
4197
  resurrected++;
3058
4198
  } catch {
@@ -3062,22 +4202,23 @@ async function resurrectTracked() {
3062
4202
  spinner.stop();
3063
4203
  process.stdout.write("\r\x1B[K");
3064
4204
  if (resurrected > 0) {
3065
- console.error(` ${pc5.green("\u25CF")} ${pc5.bold(`Resurrected ${resurrected} process(es)`)}`);
4205
+ console.error(` ${pc7.green("\u25CF")} ${pc7.bold(`Resurrected ${resurrected} process(es)`)}`);
3066
4206
  }
3067
4207
  if (failed > 0) {
3068
- console.error(` ${pc5.red("\u25CF")} ${pc5.bold(`${failed} process(es) failed to resurrect`)}`);
4208
+ console.error(` ${pc7.red("\u25CF")} ${pc7.bold(`${failed} process(es) failed to resurrect`)}`);
3069
4209
  }
3070
4210
  }
3071
4211
 
3072
4212
  // src/commands/kill.ts
3073
- import pc6 from "picocolors";
4213
+ import pc8 from "picocolors";
3074
4214
  async function killCommand(args2) {
3075
4215
  const rawTarget = args2[0];
3076
4216
  const signalIndex = args2.indexOf("--signal");
3077
4217
  const signalRaw = signalIndex !== -1 ? args2[signalIndex + 1] : "SIGTERM";
3078
4218
  const signal = signalRaw ?? "SIGTERM";
4219
+ const force = args2.includes("-y") || args2.includes("--yes");
3079
4220
  if (rawTarget === "all" || args2.includes("--all") || args2.includes("-a")) {
3080
- await killAll(signal);
4221
+ await killAll(signal, force);
3081
4222
  return;
3082
4223
  }
3083
4224
  if (!rawTarget) {
@@ -3097,21 +4238,29 @@ async function killCommand(args2) {
3097
4238
  } else {
3098
4239
  const tracked = readTracked();
3099
4240
  const trackedMatch = tracked.find((t) => t.name === rawTarget);
3100
- if (trackedMatch && isProcessRunning(trackedMatch.pid)) {
4241
+ if (trackedMatch) {
4242
+ if (!isTrackedRunning(trackedMatch)) {
4243
+ console.error(`
4244
+ ${pc8.yellow("\u26A0")} ${pc8.bold(rawTarget)} ${pc8.dim("is already stopped (no running process to kill)")}`);
4245
+ console.error(` ${pc8.dim("Use")} ${pc8.cyan("fennec spawn")} ${pc8.dim("to resume")}`);
4246
+ process.exit(0);
4247
+ }
3101
4248
  targetPid = trackedMatch.pid;
3102
4249
  displayName = `${trackedMatch.name} (PID ${targetPid})`;
3103
4250
  } else {
3104
- const matches = getSystemProcesses({ name: rawTarget, userOnly: true, sortBy: "cpu" });
4251
+ const currentPid = process.pid;
4252
+ let matches = getSystemProcesses({ name: rawTarget, userOnly: true, sortBy: "cpu" });
4253
+ matches = matches.filter((m) => m.pid !== currentPid);
3105
4254
  if (matches.length === 0) {
3106
4255
  console.error(renderError("Process not found", `No running process matching "${rawTarget}"`));
3107
4256
  process.exit(1);
3108
4257
  }
3109
4258
  if (matches.length > 1) {
3110
4259
  console.error(`
3111
- ${pc6.bold(`Multiple processes match "${rawTarget}":`)}`);
4260
+ ${pc8.bold(`Multiple processes match "${rawTarget}":`)}`);
3112
4261
  const selected = await selectPrompt("Select which to kill:", matches.map((p, i) => ({ value: String(i), label: `${p.name} (PID ${p.pid})`, description: `${p.command.slice(0, 80)} \u2014 CPU: ${p.cpuPercent}% MEM: ${p.memPercent}%` })));
3113
4262
  if (selected === null) {
3114
- console.error(` ${pc6.dim("Cancelled")}`);
4263
+ console.error(` ${pc8.dim("Cancelled")}`);
3115
4264
  return;
3116
4265
  }
3117
4266
  const idx = parseInt(selected, 10);
@@ -3123,9 +4272,9 @@ async function killCommand(args2) {
3123
4272
  }
3124
4273
  }
3125
4274
  }
3126
- const confirmed = await confirmPrompt(`Kill ${pc6.bold(displayName)} with ${pc6.yellow(signal)}?`, false);
4275
+ const confirmed = force || await confirmPrompt(`Kill ${pc8.bold(displayName)} with ${pc8.yellow(signal)}?`, false);
3127
4276
  if (!confirmed) {
3128
- console.error(` ${pc6.dim("Cancelled")}`);
4277
+ console.error(` ${pc8.dim("Cancelled")}`);
3129
4278
  return;
3130
4279
  }
3131
4280
  const spinner = createSpinner(`Sending ${signal} to ${displayName}...`);
@@ -3135,16 +4284,16 @@ async function killCommand(args2) {
3135
4284
  const stillRunning = isProcessRunning(targetPid);
3136
4285
  if (stillRunning && signal !== "SIGKILL") {
3137
4286
  spinner.warn(`${displayName} did not respond to ${signal}`);
3138
- const forceKill = await confirmPrompt(`Send ${pc6.red("SIGKILL")} to force stop?`, true);
4287
+ const forceKill = force || await confirmPrompt(`Send ${pc8.red("SIGKILL")} to force stop?`, true);
3139
4288
  if (forceKill) {
3140
4289
  const forceSpinner = createSpinner(`Sending SIGKILL to ${displayName}...`);
3141
4290
  killProcess(targetPid, "SIGKILL");
3142
4291
  await new Promise((r) => setTimeout(r, 300));
3143
4292
  isProcessRunning(targetPid) ? forceSpinner.fail(`${displayName} could not be stopped (permission denied?)`) : forceSpinner.succeed(`${displayName} force stopped`);
3144
4293
  } else {
3145
- console.error(` ${pc6.dim("Retrying with SIGTERM...")}`);
4294
+ console.error(` ${pc8.dim("Retrying with SIGTERM...")}`);
3146
4295
  killProcess(targetPid, "SIGTERM");
3147
- console.error(` ${pc6.yellow("\u26A0")} ${displayName} ${pc6.dim("may still be running")}`);
4296
+ console.error(` ${pc8.yellow("\u26A0")} ${displayName} ${pc8.dim("may still be running")}`);
3148
4297
  }
3149
4298
  } else {
3150
4299
  removeTrackedByPid(targetPid);
@@ -3155,44 +4304,43 @@ async function killCommand(args2) {
3155
4304
  console.error(renderError("Permission denied", "Try running with sudo or use a different signal."));
3156
4305
  }
3157
4306
  }
3158
- async function killAll(signal) {
3159
- const userProcs = getSystemProcesses({ userOnly: true, sortBy: "cpu", limit: 200 });
3160
- if (userProcs.length === 0) {
3161
- console.error(` ${pc6.dim("No user processes to kill.")}`);
4307
+ async function killAll(signal, force) {
4308
+ const tracked = readTracked();
4309
+ const running = tracked.filter((t) => isTrackedRunning(t));
4310
+ if (running.length === 0) {
4311
+ console.error(` ${pc8.dim("No running tracked apps to kill.")}`);
3162
4312
  return;
3163
4313
  }
3164
4314
  console.error(`
3165
- ${pc6.bold(`Kill ${userProcs.length} user processes?`)}`);
3166
- console.error(` ${pc6.dim("This will stop ALL your running processes.")}`);
3167
- console.error(` ${pc6.yellow("\u26A0 System processes will not be affected.")}
4315
+ ${pc8.bold(`Kill ${running.length} tracked app(s)?`)}`);
4316
+ console.error(` ${pc8.dim("This stops every Fennec-tracked running process only \u2014 other processes are untouched.")}
3168
4317
  `);
3169
- const confirmed = await confirmPrompt(`${pc6.red("Are you sure?")} ${pc6.dim("This cannot be undone")}`, false);
4318
+ const confirmed = force || await confirmPrompt(`${pc8.red("Are you sure?")} ${pc8.dim("This cannot be undone")}`, false);
3170
4319
  if (!confirmed) {
3171
- console.error(` ${pc6.dim("Cancelled.")}`);
4320
+ console.error(` ${pc8.dim("Cancelled.")}`);
3172
4321
  return;
3173
4322
  }
3174
- const spinner = createSpinner(`Killing ${userProcs.length} processes...`);
4323
+ const spinner = createSpinner(`Killing ${running.length} tracked app(s)...`);
3175
4324
  let killed = 0, failed = 0;
3176
- for (const proc of userProcs) {
3177
- if (killProcess(proc.pid, signal)) {
4325
+ for (const t of running) {
4326
+ if (killProcess(t.pid, signal)) {
3178
4327
  killed++;
3179
- removeTrackedByPid(proc.pid);
4328
+ removeTrackedByPid(t.pid);
3180
4329
  } else {
3181
4330
  failed++;
3182
4331
  }
3183
- if (killed % 10 === 0) await new Promise((r) => setTimeout(r, 50));
3184
4332
  }
3185
- await new Promise((r) => setTimeout(r, 500));
3186
- killed > 0 ? spinner.succeed(`${killed} process(es) killed`) : spinner.fail("Failed to kill processes");
3187
- if (failed > 0) console.error(` ${pc6.yellow(`${failed} process(es) could not be killed`)} ${pc6.dim("(try with sudo)")}`);
4333
+ await new Promise((r) => setTimeout(r, 300));
4334
+ killed > 0 ? spinner.succeed(`${killed} tracked app(s) killed`) : spinner.fail("Failed to kill apps");
4335
+ if (failed > 0) console.error(` ${pc8.yellow(`${failed} app(s) could not be killed`)}`);
3188
4336
  }
3189
4337
 
3190
4338
  // src/commands/stop.ts
3191
- import pc7 from "picocolors";
4339
+ import pc9 from "picocolors";
3192
4340
  async function stopCommand(args2) {
3193
4341
  const stopAll = args2.includes("--all") || args2.includes("-a");
3194
4342
  if (stopAll) {
3195
- await stopAllTracked();
4343
+ await stopAllTracked(args2);
3196
4344
  return;
3197
4345
  }
3198
4346
  const rawTarget = args2[0];
@@ -3203,42 +4351,45 @@ async function stopCommand(args2) {
3203
4351
  const tracked = readTracked();
3204
4352
  const trackedMatch = tracked.find((t) => t.name === rawTarget);
3205
4353
  if (!trackedMatch) {
3206
- console.error(renderError("Not found", `No tracked process named "${rawTarget}". Use ${pc7.cyan("fennec kill <pid>")} to kill a system process.`));
4354
+ console.error(renderError("Not found", `No tracked process named "${rawTarget}". Use ${pc9.cyan("fennec kill <pid>")} to kill a system process.`));
3207
4355
  process.exit(1);
3208
4356
  }
3209
- if (!isProcessRunning(trackedMatch.pid)) {
4357
+ if (!isTrackedRunning(trackedMatch)) {
3210
4358
  console.error(`
3211
- ${pc7.yellow("\u26A0")} ${pc7.bold(rawTarget)} ${pc7.dim("is already stopped")}`);
4359
+ ${pc9.yellow("\u26A0")} ${pc9.bold(rawTarget)} ${pc9.dim("is already stopped")}`);
3212
4360
  console.error();
3213
4361
  process.exit(0);
3214
4362
  }
3215
4363
  const displayName = `${trackedMatch.name} (PID ${trackedMatch.pid})`;
3216
- const confirmed = await confirmPrompt(`Stop ${pc7.bold(displayName)}? ${pc7.dim("It can be re-spawned later via fennec spawn")}`, false);
4364
+ const force = args2.includes("-y") || args2.includes("--yes");
4365
+ const confirmed = force || await confirmPrompt(`Stop ${pc9.bold(displayName)}? ${pc9.dim("It can be re-spawned later via fennec spawn")}`, false);
3217
4366
  if (!confirmed) {
3218
- console.error(` ${pc7.dim("Cancelled")}`);
4367
+ console.error(` ${pc9.dim("Cancelled")}`);
3219
4368
  return;
3220
4369
  }
4370
+ setAutoRestart(trackedMatch.name, false);
3221
4371
  await stopSingleProcess(trackedMatch.pid, trackedMatch.name);
3222
4372
  }
3223
- async function stopAllTracked() {
4373
+ async function stopAllTracked(args2) {
3224
4374
  const tracked = readTracked();
3225
- const running = tracked.filter((t) => isProcessRunning(t.pid));
4375
+ const running = tracked.filter((t) => isTrackedRunning(t));
3226
4376
  if (running.length === 0) {
3227
4377
  console.error(`
3228
- ${pc7.dim("No running tracked processes to stop.")}
4378
+ ${pc9.dim("No running tracked processes to stop.")}
3229
4379
  `);
3230
4380
  return;
3231
4381
  }
3232
4382
  console.error(`
3233
- ${pc7.yellow("\u26A0")} ${pc7.bold(`Stop all ${running.length} running process(es)?`)} ${pc7.dim("(They can be re-spawned later)")}
4383
+ ${pc9.yellow("\u26A0")} ${pc9.bold(`Stop all ${running.length} running process(es)?`)} ${pc9.dim("(They can be re-spawned later)")}
3234
4384
  `);
3235
4385
  for (const t of running) {
3236
- console.error(` ${pc7.green("\u25CF")} ${pc7.bold(t.name)} ${pc7.dim(`(PID ${t.pid})`)}`);
4386
+ console.error(` ${pc9.green("\u25CF")} ${pc9.bold(t.name)} ${pc9.dim(`(PID ${t.pid})`)}`);
3237
4387
  }
3238
4388
  console.error();
3239
- const confirmed = await confirmPrompt("Stop all?", false);
4389
+ const forceAll = args2.includes("-y") || args2.includes("--yes");
4390
+ const confirmed = forceAll || await confirmPrompt("Stop all?", false);
3240
4391
  if (!confirmed) {
3241
- console.error(` ${pc7.dim("Cancelled")}
4392
+ console.error(` ${pc9.dim("Cancelled")}
3242
4393
  `);
3243
4394
  return;
3244
4395
  }
@@ -3246,6 +4397,7 @@ async function stopAllTracked() {
3246
4397
  let stopped = 0;
3247
4398
  let failed = 0;
3248
4399
  for (const t of running) {
4400
+ setAutoRestart(t.name, false);
3249
4401
  if (killProcess(t.pid, "SIGTERM")) {
3250
4402
  stopped++;
3251
4403
  } else {
@@ -3256,10 +4408,10 @@ async function stopAllTracked() {
3256
4408
  spinner.stop();
3257
4409
  process.stdout.write("\r\x1B[K");
3258
4410
  if (stopped > 0) {
3259
- console.error(` ${pc7.green("\u2713")} ${pc7.bold(`Stopped ${stopped} process(es)`)} ${pc7.dim("(kept in tracked.json)")}`);
4411
+ console.error(` ${pc9.green("\u2713")} ${pc9.bold(`Stopped ${stopped} process(es)`)} ${pc9.dim("(kept in tracked.json)")}`);
3260
4412
  }
3261
4413
  if (failed > 0) {
3262
- console.error(` ${pc7.red("\u2717")} ${pc7.bold(`${failed} process(es) failed to stop`)}`);
4414
+ console.error(` ${pc9.red("\u2717")} ${pc9.bold(`${failed} process(es) failed to stop`)}`);
3263
4415
  }
3264
4416
  console.error();
3265
4417
  await psCommand([]);
@@ -3273,14 +4425,14 @@ async function stopSingleProcess(pid, name) {
3273
4425
  const stillRunning = isProcessRunning(pid);
3274
4426
  if (stillRunning) {
3275
4427
  spinner.warn(`${displayName} did not respond to SIGTERM`);
3276
- const forceKill = await confirmPrompt(`Send ${pc7.red("SIGKILL")} to force stop?`, true);
4428
+ const forceKill = await confirmPrompt(`Send ${pc9.red("SIGKILL")} to force stop?`, true);
3277
4429
  if (forceKill) {
3278
4430
  const forceSpinner = createSpinner(`Sending SIGKILL to ${displayName}...`);
3279
4431
  killProcess(pid, "SIGKILL");
3280
4432
  await new Promise((r) => setTimeout(r, 300));
3281
4433
  isProcessRunning(pid) ? forceSpinner.fail(`${displayName} could not be stopped (permission denied?)`) : forceSpinner.succeed(`${displayName} force stopped`);
3282
4434
  } else {
3283
- console.error(` ${pc7.yellow("\u26A0")} ${displayName} ${pc7.dim("may still be running")}`);
4435
+ console.error(` ${pc9.yellow("\u26A0")} ${displayName} ${pc9.dim("may still be running")}`);
3284
4436
  }
3285
4437
  } else {
3286
4438
  spinner.succeed(`${displayName} stopped`);
@@ -3294,11 +4446,10 @@ async function stopSingleProcess(pid, name) {
3294
4446
  }
3295
4447
 
3296
4448
  // src/commands/spawn.ts
3297
- import pc8 from "picocolors";
3298
- import { createWriteStream as createWriteStream2, mkdirSync as mkdirSync3 } from "fs";
3299
- import { resolve as resolve4 } from "path";
4449
+ import pc10 from "picocolors";
4450
+ import { mkdirSync as mkdirSync5 } from "fs";
4451
+ import { resolve as resolve5 } from "path";
3300
4452
  import { homedir as homedir3 } from "os";
3301
- import { spawn as spawn2 } from "child_process";
3302
4453
  async function spawnCommand(args2) {
3303
4454
  const spawnAll = args2.includes("--all") || args2.includes("-a");
3304
4455
  const name = args2[0];
@@ -3313,13 +4464,13 @@ async function spawnCommand(args2) {
3313
4464
  const tracked = readTracked();
3314
4465
  const match = tracked.find((t) => t.name === name);
3315
4466
  if (!match) {
3316
- console.error(renderError("Not found", `No tracked process named "${name}". Use ${pc8.cyan("fennec spawn")} to see available processes, or ${pc8.cyan("fennec start <command> --name <name>")} to create a new one.`));
4467
+ console.error(renderError("Not found", `No tracked process named "${name}". Use ${pc10.cyan("fennec spawn")} to see available processes, or ${pc10.cyan("fennec start <command> --name <name>")} to create a new one.`));
3317
4468
  process.exit(1);
3318
4469
  }
3319
- if (isProcessRunning(match.pid)) {
4470
+ if (isTrackedRunning(match)) {
3320
4471
  console.error(`
3321
- ${pc8.yellow("\u26A0")} ${pc8.bold(name)} ${pc8.dim("is already running (PID:")} ${match.pid}${pc8.dim(")")}`);
3322
- console.error(` ${pc8.dim("Use")} ${pc8.cyan(`fennec stop ${name}`)} ${pc8.dim("to stop it first.")}`);
4472
+ ${pc10.yellow("\u26A0")} ${pc10.bold(name)} ${pc10.dim("is already running (PID:")} ${match.pid}${pc10.dim(")")}`);
4473
+ console.error(` ${pc10.dim("Use")} ${pc10.cyan(`fennec stop ${name}`)} ${pc10.dim("to stop it first.")}`);
3323
4474
  console.error();
3324
4475
  process.exit(0);
3325
4476
  }
@@ -3331,18 +4482,18 @@ async function spawnCommand(args2) {
3331
4482
  }
3332
4483
  async function spawnAllStopped() {
3333
4484
  const tracked = readTracked();
3334
- const toSpawn = tracked.filter((t) => !isProcessRunning(t.pid) && t.command);
4485
+ const toSpawn = tracked.filter((t) => !isTrackedRunning(t) && t.command);
3335
4486
  if (toSpawn.length === 0) {
3336
4487
  console.error(`
3337
- ${pc8.dim("No stopped processes with saved commands to spawn.")}
4488
+ ${pc10.dim("No stopped processes with saved commands to spawn.")}
3338
4489
  `);
3339
4490
  return;
3340
4491
  }
3341
4492
  console.error(`
3342
- ${symbols.fox} ${pc8.bold(`Spawning ${toSpawn.length} process(es)...`)}
4493
+ ${symbols.fox} ${pc10.bold(`Spawning ${toSpawn.length} process(es)...`)}
3343
4494
  `);
3344
4495
  for (const t of toSpawn) {
3345
- console.error(` ${pc8.dim("\xB7")} ${pc8.bold(t.name)} ${pc8.dim(`(${t.command})`)}`);
4496
+ console.error(` ${pc10.dim("\xB7")} ${pc10.bold(t.name)} ${pc10.dim(`(${t.command})`)}`);
3346
4497
  }
3347
4498
  console.error();
3348
4499
  const spinner = createSpinner("Spawning...");
@@ -3350,30 +4501,28 @@ async function spawnAllStopped() {
3350
4501
  let failed = 0;
3351
4502
  for (const proc of toSpawn) {
3352
4503
  try {
3353
- const cmdParts = proc.command.split(/\s+/);
3354
- const logDir = resolve4(homedir3(), ".fennec", "logs");
3355
- mkdirSync3(logDir, { recursive: true });
3356
- const logFilePath = resolve4(logDir, `${proc.name}.log`);
3357
- const child = spawn2(cmdParts[0], cmdParts.slice(1), {
3358
- cwd: proc.cwd,
3359
- env: { ...process.env },
3360
- stdio: ["ignore", "pipe", "pipe"],
3361
- detached: true
3362
- });
4504
+ const cmdParts = resolveArgs(proc);
4505
+ const logDir = resolve5(homedir3(), ".fennec", "logs");
4506
+ mkdirSync5(logDir, { recursive: true });
4507
+ const logFilePath = resolve5(logDir, `${proc.name}.log`);
4508
+ const child = spawnDaemon({ cmdParts, name: proc.name, cwd: proc.cwd, logFilePath, env: buildSpawnEnv(proc.env), logMode: proc.logMode });
3363
4509
  const pid = child.pid ?? 0;
3364
- rotateLogFile(logFilePath);
3365
- const logStream = createWriteStream2(logFilePath, { flags: "a" });
3366
- if (child.stdout) child.stdout.pipe(logStream);
3367
- if (child.stderr) child.stderr.pipe(logStream);
3368
- child.unref();
3369
4510
  addTracked({
3370
4511
  name: proc.name,
3371
4512
  pid,
3372
4513
  command: proc.command,
4514
+ args: cmdParts,
3373
4515
  port: proc.port,
3374
4516
  cwd: proc.cwd,
3375
- startedAt: (/* @__PURE__ */ new Date()).toISOString()
4517
+ env: proc.env,
4518
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
4519
+ autoRestart: proc.autoRestart,
4520
+ logMode: proc.logMode
3376
4521
  });
4522
+ if (proc.autoRestart) {
4523
+ ensureSupervisorRunning();
4524
+ ensurePersistEnabled();
4525
+ }
3377
4526
  spawned++;
3378
4527
  } catch {
3379
4528
  failed++;
@@ -3382,10 +4531,10 @@ async function spawnAllStopped() {
3382
4531
  spinner.stop();
3383
4532
  process.stdout.write("\r\x1B[K");
3384
4533
  if (spawned > 0) {
3385
- console.error(` ${pc8.green("\u2713")} ${pc8.bold(`Spawned ${spawned} process(es)`)}`);
4534
+ console.error(` ${pc10.green("\u2713")} ${pc10.bold(`Spawned ${spawned} process(es)`)}`);
3386
4535
  }
3387
4536
  if (failed > 0) {
3388
- console.error(` ${pc8.red("\u2717")} ${pc8.bold(`${failed} process(es) failed to spawn`)}`);
4537
+ console.error(` ${pc10.red("\u2717")} ${pc10.bold(`${failed} process(es) failed to spawn`)}`);
3389
4538
  }
3390
4539
  console.error();
3391
4540
  await psCommand([]);
@@ -3394,25 +4543,25 @@ async function spawnList() {
3394
4543
  const tracked = readTracked();
3395
4544
  if (tracked.length === 0) {
3396
4545
  console.error(`
3397
- ${pc8.dim("No tracked processes found.")}`);
3398
- console.error(` ${pc8.dim("Start an app first:")} ${pc8.cyan("fennec start <command> --name <name>")}`);
4546
+ ${pc10.dim("No tracked processes found.")}`);
4547
+ console.error(` ${pc10.dim("Start an app first:")} ${pc10.cyan("fennec start <command> --name <name>")}`);
3399
4548
  console.error();
3400
4549
  return;
3401
4550
  }
3402
- const stopped = tracked.filter((t) => !isProcessRunning(t.pid));
3403
- const running = tracked.filter((t) => isProcessRunning(t.pid));
4551
+ const stopped = tracked.filter((t) => !isTrackedRunning(t));
4552
+ const running = tracked.filter((t) => isTrackedRunning(t));
3404
4553
  if (stopped.length === 0) {
3405
4554
  console.error(`
3406
- ${pc8.dim("All tracked processes are running. Nothing to spawn.")}`);
4555
+ ${pc10.dim("All tracked processes are running. Nothing to spawn.")}`);
3407
4556
  console.error();
3408
4557
  return;
3409
4558
  }
3410
4559
  console.error(`
3411
- ${symbols.fox} ${pc8.bold("Available to spawn")} ${pc8.dim(`(${stopped.length}/${tracked.length} stopped)`)}`);
4560
+ ${symbols.fox} ${pc10.bold("Available to spawn")} ${pc10.dim(`(${stopped.length}/${tracked.length} stopped)`)}`);
3412
4561
  if (running.length > 0) {
3413
- console.error(` ${pc8.dim("Running (use stop first):")}`);
4562
+ console.error(` ${pc10.dim("Running (use stop first):")}`);
3414
4563
  for (const r of running) {
3415
- console.error(` ${pc8.green("\u25CF")} ${pc8.bold(r.name)} ${pc8.dim(`(PID ${r.pid})`)}`);
4564
+ console.error(` ${pc10.green("\u25CF")} ${pc10.bold(r.name)} ${pc10.dim(`(PID ${r.pid})`)}`);
3416
4565
  }
3417
4566
  console.error();
3418
4567
  }
@@ -3422,7 +4571,7 @@ async function spawnList() {
3422
4571
  description: t.command.length > 80 ? t.command.slice(0, 80) + "..." : t.command
3423
4572
  })));
3424
4573
  if (selected === null) {
3425
- console.error(` ${pc8.dim("Cancelled")}`);
4574
+ console.error(` ${pc10.dim("Cancelled")}`);
3426
4575
  return;
3427
4576
  }
3428
4577
  const match = stopped.find((t) => t.name === selected);
@@ -3431,38 +4580,36 @@ async function spawnList() {
3431
4580
  }
3432
4581
  }
3433
4582
  async function respawnProcess(proc) {
3434
- const cmdParts = proc.command.split(/\s+/);
3435
- const logDir = resolve4(homedir3(), ".fennec", "logs");
3436
- mkdirSync3(logDir, { recursive: true });
3437
- const logFilePath = resolve4(logDir, `${proc.name}.log`);
4583
+ const cmdParts = resolveArgs(proc);
4584
+ const logDir = resolve5(homedir3(), ".fennec", "logs");
4585
+ mkdirSync5(logDir, { recursive: true });
4586
+ const logFilePath = resolve5(logDir, `${proc.name}.log`);
3438
4587
  console.error(`
3439
- ${symbols.fox} ${pc8.bold("Spawning")} ${renderAppName(proc.name)} ${pc8.dim("(from saved config)")}
4588
+ ${symbols.fox} ${pc10.bold("Spawning")} ${renderAppName(proc.name)} ${pc10.dim("(from saved config)")}
3440
4589
  `);
3441
4590
  console.error(` ${renderKV("Command", proc.command)}`);
3442
4591
  if (proc.cwd) console.error(` ${renderKV("Directory", proc.cwd)}`);
3443
4592
  if (proc.port) console.error(` ${renderKV("Port", String(proc.port))}`);
3444
4593
  try {
3445
- const child = spawn2(cmdParts[0], cmdParts.slice(1), {
3446
- cwd: proc.cwd,
3447
- env: { ...process.env },
3448
- stdio: ["ignore", "pipe", "pipe"],
3449
- detached: true
3450
- });
4594
+ const child = spawnDaemon({ cmdParts, name: proc.name, cwd: proc.cwd, logFilePath, env: buildSpawnEnv(proc.env), logMode: proc.logMode });
3451
4595
  const pid = child.pid ?? 0;
3452
- rotateLogFile(logFilePath);
3453
- const logStream = createWriteStream2(logFilePath, { flags: "a" });
3454
- if (child.stdout) child.stdout.pipe(logStream);
3455
- if (child.stderr) child.stderr.pipe(logStream);
3456
- child.unref();
3457
4596
  addTracked({
3458
4597
  name: proc.name,
3459
4598
  pid,
3460
4599
  command: proc.command,
4600
+ args: cmdParts,
3461
4601
  port: proc.port,
3462
4602
  cwd: proc.cwd,
3463
- startedAt: (/* @__PURE__ */ new Date()).toISOString()
4603
+ env: proc.env,
4604
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
4605
+ autoRestart: proc.autoRestart,
4606
+ logMode: proc.logMode
3464
4607
  });
3465
- console.error(` ${pc8.green("\u2713")} ${pc8.bold(proc.name)} ${pc8.dim(`spawned (PID: ${pid})`)}`);
4608
+ if (proc.autoRestart) {
4609
+ ensureSupervisorRunning();
4610
+ ensurePersistEnabled();
4611
+ }
4612
+ console.error(` ${pc10.green("\u2713")} ${pc10.bold(proc.name)} ${pc10.dim(`spawned (PID: ${pid})`)}`);
3466
4613
  await psCommand([]);
3467
4614
  } catch (error) {
3468
4615
  console.error(renderError(`Failed to spawn ${proc.name}`, String(error)));
@@ -3471,11 +4618,10 @@ async function respawnProcess(proc) {
3471
4618
  }
3472
4619
 
3473
4620
  // src/commands/restart.ts
3474
- import pc9 from "picocolors";
3475
- import { mkdirSync as mkdirSync4, createWriteStream as createWriteStream3 } from "fs";
3476
- import { resolve as resolve5 } from "path";
4621
+ import pc11 from "picocolors";
4622
+ import { mkdirSync as mkdirSync6 } from "fs";
4623
+ import { resolve as resolve6 } from "path";
3477
4624
  import { homedir as homedir4 } from "os";
3478
- import { spawn as spawn3 } from "child_process";
3479
4625
  async function restartCommand(args2) {
3480
4626
  const name = args2[0];
3481
4627
  if (!name) {
@@ -3500,9 +4646,9 @@ async function restartCommand(args2) {
3500
4646
  targetPid = processes[0].pid;
3501
4647
  }
3502
4648
  }
3503
- const confirmed = await confirmPrompt(`Restart ${pc9.bold(`${name} (PID ${targetPid})`)}?`, false);
4649
+ const confirmed = await confirmPrompt(`Restart ${pc11.bold(`${name} (PID ${targetPid})`)}?`, false);
3504
4650
  if (!confirmed) {
3505
- console.error(` ${pc9.dim("Cancelled")}`);
4651
+ console.error(` ${pc11.dim("Cancelled")}`);
3506
4652
  return;
3507
4653
  }
3508
4654
  const spinner = createSpinner(`Stopping PID ${targetPid}...`);
@@ -3521,86 +4667,581 @@ async function restartCommand(args2) {
3521
4667
  if (trackedEntry) {
3522
4668
  const respawnSpinner = createSpinner(`Re-spawning ${trackedEntry.name}...`);
3523
4669
  try {
3524
- const cmdParts = trackedEntry.command.split(" ");
3525
- const logDir = resolve5(homedir4(), ".fennec", "logs");
3526
- mkdirSync4(logDir, { recursive: true });
3527
- const logFilePath = resolve5(logDir, `${trackedEntry.name}.log`);
3528
- const child = spawn3(cmdParts[0], cmdParts.slice(1), {
3529
- cwd: trackedEntry.cwd,
3530
- env: { ...process.env },
3531
- stdio: ["ignore", "pipe", "pipe"],
3532
- detached: true
3533
- });
3534
- rotateLogFile(logFilePath);
3535
- const logStream = createWriteStream3(logFilePath, { flags: "a" });
3536
- if (child.stdout) child.stdout.pipe(logStream);
3537
- if (child.stderr) child.stderr.pipe(logStream);
3538
- child.unref();
4670
+ const cmdParts = resolveArgs(trackedEntry);
4671
+ const logDir = resolve6(homedir4(), ".fennec", "logs");
4672
+ mkdirSync6(logDir, { recursive: true });
4673
+ const logFilePath = resolve6(logDir, `${trackedEntry.name}.log`);
4674
+ const child = spawnDaemon({ cmdParts, name: trackedEntry.name, cwd: trackedEntry.cwd, logFilePath, env: buildSpawnEnv(trackedEntry.env), logMode: trackedEntry.logMode });
3539
4675
  removeTrackedByPid(targetPid);
3540
4676
  addTracked({
3541
4677
  name: trackedEntry.name,
3542
4678
  pid: child.pid ?? 0,
3543
4679
  command: trackedEntry.command,
4680
+ args: cmdParts,
3544
4681
  port: trackedEntry.port,
3545
4682
  cwd: trackedEntry.cwd,
3546
- startedAt: (/* @__PURE__ */ new Date()).toISOString()
4683
+ env: trackedEntry.env,
4684
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
4685
+ autoRestart: trackedEntry.autoRestart,
4686
+ logMode: trackedEntry.logMode
3547
4687
  });
3548
4688
  respawnSpinner.succeed(`${trackedEntry.name} restarted (PID: ${child.pid})`);
3549
4689
  } catch (error) {
3550
4690
  respawnSpinner.fail(`Failed to re-spawn ${trackedEntry.name}: ${String(error)}`);
3551
- console.error(` ${pc9.dim("Config preserved. Re-run manually:")} ${renderCommand(trackedEntry.command)}`);
4691
+ console.error(` ${pc11.dim("Config preserved. Re-run manually:")} ${renderCommand(trackedEntry.command)}`);
4692
+ }
4693
+ } else {
4694
+ console.error(` ${pc11.dim("No tracked config found. Re-run manually:")} ${renderCommand(name)}`);
4695
+ }
4696
+ }
4697
+
4698
+ // src/commands/adopt.ts
4699
+ import pc12 from "picocolors";
4700
+ function adoptCommand(args2) {
4701
+ const pidArg = args2.find((a) => /^\d+$/.test(a));
4702
+ if (!pidArg) {
4703
+ console.error(renderError("Missing PID", `Usage: fennec adopt <pid> [--name <name>] [--port <port>]`));
4704
+ process.exit(1);
4705
+ }
4706
+ const pid = parseInt(pidArg, 10);
4707
+ const nameIdx = args2.indexOf("--name");
4708
+ const name = nameIdx !== -1 ? args2[nameIdx + 1] : void 0;
4709
+ const portIdx = args2.indexOf("--port");
4710
+ const port = portIdx !== -1 ? parseInt(args2[portIdx + 1] ?? "", 10) : void 0;
4711
+ const entry = adoptProcess(pid, { name, port: Number.isNaN(port) ? void 0 : port });
4712
+ if (!entry) {
4713
+ console.error(renderError("Cannot adopt", `No running process with PID ${pid}`));
4714
+ process.exit(1);
4715
+ }
4716
+ console.error(`
4717
+ ${symbols.fox} ${pc12.green("\u2713")} ${pc12.bold("Adopted")} ${pc12.bold(entry.name)} ${pc12.dim(`(PID ${pid})`)}`);
4718
+ if (entry.port) console.error(` ${pc12.dim("port")} ${pc12.yellow(`:${entry.port}`)}`);
4719
+ if (entry.command) console.error(` ${pc12.dim("command")} ${entry.command}`);
4720
+ console.error(` ${pc12.dim("status")} now tracked + supervised by the supervisor`);
4721
+ console.error(` ${pc12.dim("logs")} ${pc12.cyan(logFilePathFor(entry.name))}
4722
+ `);
4723
+ }
4724
+
4725
+ // src/commands/inspect.ts
4726
+ import { readFileSync as readFileSync6 } from "fs";
4727
+ import pc13 from "picocolors";
4728
+
4729
+ // src/utils/redact.ts
4730
+ import { existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
4731
+ var SECRET_PATTERNS = [
4732
+ // Bearer / auth tokens
4733
+ { name: "bearer", re: /\bBearer\s+[A-Za-z0-9._\-]+/gi },
4734
+ // Authorization: <scheme> <token>
4735
+ { name: "authorization", re: /\bAuthorization\s*:\s*\S+/gi },
4736
+ // Generic API keys
4737
+ { name: "apikey", re: /\b(api[_-]?key|apikey|access[_-]?key|secret[_-]?key|private[_-]?key)\s*[:=]\s*\S+/gi },
4738
+ // AWS
4739
+ { name: "aws", re: /\b(AKIA|ASIA)[0-9A-Z]{16}\b/g },
4740
+ // Slack tokens
4741
+ { name: "slack", re: /\bxox[baprs]-[0-9A-Za-z\-]{10,}/g },
4742
+ // Stripe
4743
+ { name: "stripe", re: /\b(sk|rk|pk)_(live|test)_[0-9A-Za-z]{16,}\b/g },
4744
+ // JWT
4745
+ { name: "jwt", re: /\beyJ[A-Za-z0-9_\-]+\.[A-Za-z0-9_\-]+\.[A-Za-z0-9_\-]*/g },
4746
+ // Google API
4747
+ { name: "google", re: /\bAIza[0-9A-Za-z_\-]{35}\b/g },
4748
+ // GitHub PAT
4749
+ { name: "github", re: /\bgh[pousr]_[0-9A-Za-z]{36,}\b/g },
4750
+ // Generic long hex/alpha tokens (>=32 chars) in common contexts
4751
+ { name: "token", re: /\b(token|secret|password|passwd|pwd|client[_-]?secret)\s*[:=]\s*\S+/gi },
4752
+ // Connection strings with credentials
4753
+ { name: "connstr", re: /\b(mongodb(\+srv)?|postgres(ql)?|mysql|redis|amqp|sqlserver):\/\/[^\s:]+:[^\s@]+@/gi },
4754
+ // Private key blocks
4755
+ { name: "pem", re: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g }
4756
+ ];
4757
+ function redactLine(line, opts = {}) {
4758
+ if (opts.enabled === false) return line;
4759
+ let out = line;
4760
+ for (const { name, re } of SECRET_PATTERNS) {
4761
+ out = out.replace(re, (m) => {
4762
+ const head = m.slice(0, Math.min(m.length, name.length + 4));
4763
+ return `${head}\u2026[REDACTED]`;
4764
+ });
4765
+ }
4766
+ return out;
4767
+ }
4768
+ function redactionCount(line) {
4769
+ let n = 0;
4770
+ for (const { re } of SECRET_PATTERNS) {
4771
+ const m = line.match(re);
4772
+ if (m) n += m.length;
4773
+ }
4774
+ return n;
4775
+ }
4776
+ var ANSI_RE = /\x1b\[[0-9;]*m/g;
4777
+ function stripAnsi(s) {
4778
+ return s.replace(ANSI_RE, "");
4779
+ }
4780
+ var LEVEL_RE = [
4781
+ { level: "error", re: /\b(ERROR|FATAL|CRITICAL|EXCEPTION|UNCAUGHT)\b/i },
4782
+ { level: "warn", re: /\b(WARN|WARNING)\b/i },
4783
+ { level: "info", re: /\b(INFO|NOTICE)\b/i },
4784
+ { level: "debug", re: /\b(DEBUG|TRACE)\b/i }
4785
+ ];
4786
+ function classifyLevel(line) {
4787
+ for (const { level, re } of LEVEL_RE) {
4788
+ if (re.test(line)) return level;
4789
+ }
4790
+ if (/\b(error|fail|failed|failure|denied|reject)\b/i.test(line)) return "error";
4791
+ if (/\b(warn)\b/i.test(line)) return "warn";
4792
+ return "other";
4793
+ }
4794
+ function extractTimestamp(line) {
4795
+ const m = line.match(/^\s*\[?(\d{4}-\d{2}-\d{2}T[\d:.]+Z?)\]?/);
4796
+ if (m) return m[1];
4797
+ const trimmed = line.trimStart();
4798
+ if (trimmed.startsWith("{")) {
4799
+ try {
4800
+ const obj = JSON.parse(line);
4801
+ const ts = obj.ts ?? obj.time ?? obj.timestamp;
4802
+ if (typeof ts === "string") return ts;
4803
+ if (typeof ts === "number") return new Date(ts).toISOString();
4804
+ } catch {
3552
4805
  }
4806
+ }
4807
+ return null;
4808
+ }
4809
+ function readLogLines(path, opts = {}) {
4810
+ if (!existsSync6(path)) return [];
4811
+ const raw = readFileSync5(path, "utf-8");
4812
+ let lines = raw.split("\n").filter(Boolean);
4813
+ const now = Date.now();
4814
+ if (opts.sinceMs && opts.parseTimestamp) {
4815
+ lines = lines.filter((l) => {
4816
+ const ts = extractTimestamp(l);
4817
+ if (ts === null) return true;
4818
+ const t = Date.parse(ts);
4819
+ return !isNaN(t) && now - t <= opts.sinceMs;
4820
+ });
4821
+ }
4822
+ if (opts.redact === false) {
4823
+ lines = lines.map(stripAnsi);
3553
4824
  } else {
3554
- console.error(` ${pc9.dim("No tracked config found. Re-run manually:")} ${renderCommand(name)}`);
4825
+ lines = lines.map((l) => stripAnsi(redactLine(l)));
4826
+ }
4827
+ if (opts.tail && opts.tail > 0 && lines.length > opts.tail) {
4828
+ lines = lines.slice(-opts.tail);
4829
+ }
4830
+ return lines;
4831
+ }
4832
+ function parseDuration(input) {
4833
+ const m = input.trim().match(/^(\d+)\s*(s|m|h|d)$/i);
4834
+ if (!m) return null;
4835
+ const n = parseInt(m[1], 10);
4836
+ const unit = m[2].toLowerCase();
4837
+ const mul = unit === "s" ? 1e3 : unit === "m" ? 6e4 : unit === "h" ? 36e5 : 864e5;
4838
+ return n * mul;
4839
+ }
4840
+
4841
+ // src/commands/inspect.ts
4842
+ var DEFAULT_TAIL = 40;
4843
+ var MAX_TAIL = 200;
4844
+ function readMetrics(pid) {
4845
+ const m = {};
4846
+ try {
4847
+ const status = readFileSync6(`/proc/${pid}/status`, "utf-8");
4848
+ const rssLine = status.split("\n").find((l) => l.startsWith("VmRSS:"));
4849
+ if (rssLine) {
4850
+ const kb = parseInt(rssLine.replace(/[^\d]/g, ""), 10);
4851
+ if (!isNaN(kb)) m.rssMb = Math.round(kb / 1024 * 10) / 10;
4852
+ }
4853
+ } catch {
4854
+ }
4855
+ return m;
4856
+ }
4857
+ async function inspectCommand(args2) {
4858
+ const target = args2[0];
4859
+ if (!target || target.startsWith("--")) {
4860
+ console.error(pc13.dim(`Usage: fennec inspect <name|pid> [--plain] [--tail N] [--since 10m]`));
4861
+ process.exit(1);
4862
+ }
4863
+ const plain = args2.includes("--plain");
4864
+ const tailIndex = args2.indexOf("--tail");
4865
+ const tail = tailIndex !== -1 ? Math.min(parseInt(args2[tailIndex + 1], 10) || DEFAULT_TAIL, MAX_TAIL) : DEFAULT_TAIL;
4866
+ const sinceIndex = args2.indexOf("--since");
4867
+ const sinceMs = sinceIndex !== -1 ? parseDuration(args2[sinceIndex + 1]) : void 0;
4868
+ if (sinceIndex !== -1 && sinceMs === void 0) {
4869
+ console.error(pc13.red(`Invalid --since. Use e.g. 10m, 1h, 30s.`));
4870
+ process.exit(1);
4871
+ }
4872
+ const tracked = readTracked();
4873
+ const proc = tracked.find((t) => t.name === target || parseInt(target, 10) === t.pid && !isNaN(parseInt(target, 10)));
4874
+ if (!proc) {
4875
+ console.error(pc13.red(`No tracked app named "${target}".`));
4876
+ process.exit(1);
4877
+ }
4878
+ const running = isTrackedRunning(proc);
4879
+ const logPath = logFilePathFor(proc.name);
4880
+ const lines = readLogLines(logPath, { tail: Math.max(tail, sinceMs ? MAX_TAIL : 0), sinceMs: sinceMs ?? void 0, redact: true, parseTimestamp: true });
4881
+ const recent = lines.slice(-tail);
4882
+ const errorLines = lines.filter((l) => classifyLevel(l) === "error").slice(-15);
4883
+ const redactedLines = recent.reduce((acc, l) => acc + (redactionCount(stripAnsi(l)) > 0 ? 1 : 0), 0);
4884
+ const metrics = running ? readMetrics(proc.pid) : {};
4885
+ let portHealthy = null;
4886
+ if (proc.port) {
4887
+ try {
4888
+ portHealthy = await checkPort(proc.port);
4889
+ } catch {
4890
+ portHealthy = false;
4891
+ }
4892
+ }
4893
+ const startedAt = Date.parse(proc.startedAt);
4894
+ const uptimeSec = running && !isNaN(startedAt) ? Math.floor((Date.now() - startedAt) / 1e3) : 0;
4895
+ if (plain) {
4896
+ const dot = running ? pc13.green("\u25CF") : pc13.red("\u25CB");
4897
+ const portStr = proc.port ? ` :${proc.port}${portHealthy === false ? pc13.red(" (port down!)") : portHealthy ? pc13.green(" (up)") : ""}` : "";
4898
+ console.error(`
4899
+ ${symbols.fox} ${renderAppName(proc.name)}${portStr} ${dot} ${running ? pc13.green(`running PID ${proc.pid}`) : pc13.red("stopped")}`);
4900
+ 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))}`);
4901
+ if (errorLines.length) {
4902
+ console.error(`
4903
+ ${pc13.bold(pc13.red("Recent errors:"))}`);
4904
+ for (const l of errorLines.slice(-8)) console.error(` ${pc13.red(stripAnsi(l).slice(0, 300))}`);
4905
+ }
4906
+ console.error();
4907
+ return;
4908
+ }
4909
+ const payload = {
4910
+ name: proc.name,
4911
+ running,
4912
+ pid: running ? proc.pid ?? null : null,
4913
+ port: proc.port ?? null,
4914
+ portHealthy,
4915
+ autoRestart: proc.autoRestart ?? false,
4916
+ uptimeSec,
4917
+ metrics,
4918
+ logTail: recent,
4919
+ errorCount: errorLines.length,
4920
+ errors: errorLines,
4921
+ redacted: true,
4922
+ redactedLines,
4923
+ note: "Secrets redacted by default. Bounded snapshot for AI observation."
4924
+ };
4925
+ process.stdout.write(JSON.stringify(payload, null, 2) + "\n");
4926
+ }
4927
+
4928
+ // src/commands/dev.ts
4929
+ import pc14 from "picocolors";
4930
+ import { existsSync as existsSync8, readFileSync as readFileSync7 } from "fs";
4931
+ import { resolve as resolve7, dirname as dirname5 } from "path";
4932
+ import "os";
4933
+ import yaml from "js-yaml";
4934
+ function loadConfig(path) {
4935
+ if (!existsSync8(path)) {
4936
+ console.error(renderError("Config not found", path));
4937
+ process.exit(1);
4938
+ }
4939
+ try {
4940
+ const raw = readFileSync7(path, "utf-8");
4941
+ const parsed = yaml.load(raw);
4942
+ return parsed ?? {};
4943
+ } catch (err) {
4944
+ console.error(renderError("Failed to parse config", String(err)));
4945
+ process.exit(1);
4946
+ }
4947
+ }
4948
+ function resolveCwd(baseDir, cwd) {
4949
+ if (!cwd) return baseDir;
4950
+ return resolve7(baseDir, cwd);
4951
+ }
4952
+ async function waitForPort(port, timeoutMs = 3e4) {
4953
+ const deadline = Date.now() + timeoutMs;
4954
+ while (Date.now() < deadline) {
4955
+ try {
4956
+ if (await checkPort(port)) return true;
4957
+ } catch {
4958
+ }
4959
+ await new Promise((r) => setTimeout(r, 500));
4960
+ }
4961
+ return false;
4962
+ }
4963
+ function configMatches(existing, desired) {
4964
+ if (existing.command !== desired.command) return false;
4965
+ if (JSON.stringify(existing.args ?? []) !== JSON.stringify(desired.args)) return false;
4966
+ if (existing.cwd !== desired.cwd) return false;
4967
+ if ((existing.port ?? null) !== (desired.port ?? null)) return false;
4968
+ if (existing.logMode !== desired.logMode) return false;
4969
+ if ((existing.autoRestart ?? true) !== desired.autoRestart) return false;
4970
+ if ((existing.healthCheck ?? null) !== (desired.healthCheck ?? null)) return false;
4971
+ if (JSON.stringify(existing.env ?? {}) !== JSON.stringify(desired.env)) return false;
4972
+ return true;
4973
+ }
4974
+ async function startApp(app, baseDir) {
4975
+ const cwd = resolveCwd(baseDir, app.cwd);
4976
+ const cmdParts = [app.command, ...app.args ?? []];
4977
+ const logFilePath = logFilePathFor(app.name);
4978
+ const env = { ...app.env ?? {} };
4979
+ const port = app.waitForPort ?? app.port;
4980
+ const restart = app.restart ?? true;
4981
+ const logMode = app.jsonl ? "jsonl" : "text";
4982
+ const desired = {
4983
+ command: cmdParts.join(" "),
4984
+ args: cmdParts,
4985
+ cwd,
4986
+ port,
4987
+ env,
4988
+ logMode,
4989
+ autoRestart: restart,
4990
+ healthCheck: app.healthCheck
4991
+ };
4992
+ const existing = readTracked().find((t) => t.name === app.name);
4993
+ if (existing && isTrackedRunning(existing) && configMatches(existing, desired)) {
4994
+ return "skipped";
4995
+ }
4996
+ const wasRunning = !!(existing && isTrackedRunning(existing));
4997
+ if (wasRunning) {
4998
+ try {
4999
+ process.kill(existing.pid, "SIGTERM");
5000
+ } catch {
5001
+ }
5002
+ }
5003
+ if (port && !(existing && isTrackedRunning(existing))) {
5004
+ const adopted = adoptExternalOnPort(port, app.name);
5005
+ if (adopted) {
5006
+ return "adopted";
5007
+ }
5008
+ if (await checkPort(port)) {
5009
+ console.error(
5010
+ ` ${pc14.yellow("\u26A0")} ${pc14.dim(`port :${port} already responding \u2014 ${app.name} may fail to bind (EADDRINUSE)`)}`
5011
+ );
5012
+ }
5013
+ }
5014
+ const child = spawnDaemon({
5015
+ cmdParts,
5016
+ name: app.name,
5017
+ cwd,
5018
+ logFilePath,
5019
+ env: buildSpawnEnv(env),
5020
+ logMode
5021
+ });
5022
+ addTracked({
5023
+ name: app.name,
5024
+ pid: child.pid ?? 0,
5025
+ command: desired.command,
5026
+ args: cmdParts,
5027
+ port,
5028
+ cwd,
5029
+ env,
5030
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
5031
+ autoRestart: restart,
5032
+ logMode,
5033
+ flapping: false,
5034
+ healthCheck: app.healthCheck
5035
+ });
5036
+ return wasRunning ? "restarted" : "started";
5037
+ }
5038
+ async function devCommand(args2) {
5039
+ const sub = args2[0] ?? "up";
5040
+ const configIndex = args2.indexOf("--config");
5041
+ const configPath = configIndex !== -1 ? resolve7(args2[configIndex + 1]) : resolve7(process.cwd(), "fennec.config.yaml");
5042
+ if (sub === "down") {
5043
+ return devDown();
5044
+ }
5045
+ if (sub === "status") {
5046
+ return devStatus();
5047
+ }
5048
+ if (sub === "restart") {
5049
+ return devRestart(args2.slice(1));
5050
+ }
5051
+ if (sub !== "up") {
5052
+ console.error(renderError("Unknown sub-command", `"${sub}" \u2014 use: up | down | status | restart`));
5053
+ process.exit(1);
5054
+ }
5055
+ const config = loadConfig(configPath);
5056
+ const apps = config.apps ?? [];
5057
+ if (apps.length === 0) {
5058
+ console.error(renderError("No apps defined", `Add an "apps:" list to ${configPath}`));
5059
+ process.exit(1);
5060
+ }
5061
+ const baseDir = dirname5(configPath);
5062
+ const started = /* @__PURE__ */ new Set();
5063
+ const ordered = topoOrder(apps);
5064
+ const portOwner = /* @__PURE__ */ new Map();
5065
+ for (const a of apps) {
5066
+ const p = a.waitForPort ?? a.port;
5067
+ if (p) {
5068
+ const prev = portOwner.get(p);
5069
+ if (prev) {
5070
+ console.error(renderError("Port conflict", `apps "${prev}" and "${a.name}" both declare port :${p}`));
5071
+ process.exit(1);
5072
+ }
5073
+ portOwner.set(p, a.name);
5074
+ }
5075
+ }
5076
+ console.error(`
5077
+ ${symbols.fox} ${pc14.bold("fennec dev up")} ${pc14.dim(`\u2014 ${apps.length} app(s) from ${configPath}`)}`);
5078
+ ensureSupervisorRunning();
5079
+ ensurePersistEnabled();
5080
+ for (const app of ordered) {
5081
+ for (const dep of app.dependsOn ?? []) {
5082
+ const depApp = apps.find((a) => a.name === dep);
5083
+ const port = depApp?.waitForPort ?? depApp?.port;
5084
+ if (port) {
5085
+ const sp = createSpinner(`Waiting for ${dep} :${port}...`);
5086
+ const ok = await waitForPort(port);
5087
+ sp.stop();
5088
+ if (!ok) console.error(` ${pc14.yellow("\u26A0")} ${pc14.dim(`${dep} :${port} not ready in time (continuing)`)}`);
5089
+ }
5090
+ }
5091
+ const res = await startApp(app, baseDir);
5092
+ const pid = readTracked().find((t) => t.name === app.name)?.pid ?? "?";
5093
+ 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})`);
5094
+ console.error(` ${pc14.green("\u2713")} ${pc14.bold(app.name)} ${tag}`);
5095
+ }
5096
+ console.error(`
5097
+ ${pc14.green("\u2713")} Stack up. Observe with ${pc14.cyan("fennec observe")} or ${pc14.cyan("fennec dev status")}.`);
5098
+ console.error(` ${pc14.dim("Apps auto-restart (supervisor) + survive reboot (persist).")}
5099
+ `);
5100
+ }
5101
+ function devDown() {
5102
+ const tracked = readTracked().filter((t) => t.autoRestart);
5103
+ if (tracked.length === 0) {
5104
+ console.error(`
5105
+ ${pc14.dim("No supervised apps to stop.")}
5106
+ `);
5107
+ return;
5108
+ }
5109
+ console.error(`
5110
+ ${symbols.fox} ${pc14.bold("fennec dev down")} ${pc14.dim(`\u2014 stopping ${tracked.length} app(s)`)}`);
5111
+ for (const t of tracked) {
5112
+ try {
5113
+ if (isTrackedRunning(t)) process.kill(t.pid, "SIGTERM");
5114
+ } catch {
5115
+ }
5116
+ t.autoRestart = false;
5117
+ addTracked({ ...t, autoRestart: false });
5118
+ console.error(` ${pc14.red("\u25A0")} ${pc14.bold(t.name)}`);
5119
+ }
5120
+ console.error(`
5121
+ ${pc14.green("\u2713")} Stack stopped (auto-restart disabled). Use ${pc14.cyan("fennec dev up")} to bring it back.
5122
+ `);
5123
+ }
5124
+ function devRestart(names) {
5125
+ ensureSupervisorRunning();
5126
+ ensurePersistEnabled();
5127
+ const tracked = readTracked();
5128
+ const targets = names.length ? tracked.filter((t) => names.includes(t.name)) : tracked.filter((t) => t.autoRestart);
5129
+ const missing = names.filter((n) => !tracked.some((t) => t.name === n));
5130
+ if (missing.length) {
5131
+ console.error(renderError("Not tracked", `No such app(s): ${missing.join(", ")} \u2014 run "fennec dev status" to list tracked apps.`));
5132
+ }
5133
+ if (targets.length === 0) {
5134
+ console.error(`
5135
+ ${pc14.dim("Nothing to restart.")}
5136
+ `);
5137
+ return;
5138
+ }
5139
+ console.error(`
5140
+ ${symbols.fox} ${pc14.bold("fennec dev restart")} ${pc14.dim(`\u2014 ${targets.length} app(s)`)}`);
5141
+ for (const t of targets) {
5142
+ try {
5143
+ const newPid = respawnTracked(t, "manual");
5144
+ console.error(` ${pc14.green("\u2713")} ${pc14.bold(t.name)} ${pc14.dim(`restarted (PID ${newPid})`)}`);
5145
+ } catch (err) {
5146
+ console.error(` ${pc14.red("\u2717")} ${pc14.bold(t.name)} ${pc14.dim(`restart failed: ${String(err)}`)}`);
5147
+ }
5148
+ }
5149
+ console.error();
5150
+ }
5151
+ function devStatus() {
5152
+ const tracked = readTracked();
5153
+ console.error(`
5154
+ ${symbols.fox} ${pc14.bold("fennec dev status")}`);
5155
+ if (tracked.length === 0) {
5156
+ console.error(` ${pc14.dim("Nothing tracked.")}
5157
+ `);
5158
+ return;
5159
+ }
5160
+ for (const t of tracked) {
5161
+ const running = isTrackedRunning(t);
5162
+ const dot = running ? pc14.green("\u25CF") : pc14.red("\u25CB");
5163
+ const state = running ? pc14.green(`running (PID ${t.pid})`) : pc14.red("stopped");
5164
+ const portStr = t.port ? ` ${pc14.yellow(`:${t.port}`)}` : "";
5165
+ const cause = t.restartCause ? pc14.dim(` \xB7 last-restart: ${t.restartCause}`) : "";
5166
+ const flap = t.flapping ? pc14.red(" \xB7 \u26A0 flapping") : "";
5167
+ console.error(` ${dot} ${pc14.bold(t.name)}${portStr} ${pc14.dim("\u2014")} ${state}${cause}${flap}`);
3555
5168
  }
5169
+ console.error();
5170
+ }
5171
+ function topoOrder(apps) {
5172
+ const byName = new Map(apps.map((a) => [a.name, a]));
5173
+ const visited = /* @__PURE__ */ new Set();
5174
+ const out = [];
5175
+ const visit = (app, stack) => {
5176
+ if (visited.has(app.name)) return;
5177
+ if (stack.has(app.name)) return;
5178
+ stack.add(app.name);
5179
+ for (const dep of app.dependsOn ?? []) {
5180
+ const d = byName.get(dep);
5181
+ if (d) visit(d, stack);
5182
+ }
5183
+ stack.delete(app.name);
5184
+ visited.add(app.name);
5185
+ out.push(app);
5186
+ };
5187
+ for (const app of apps) visit(app, /* @__PURE__ */ new Set());
5188
+ return out;
3556
5189
  }
3557
5190
 
3558
5191
  // src/commands/log.ts
3559
- import { existsSync as existsSync4, readFileSync as readFileSync3, unlinkSync } from "fs";
3560
- import { resolve as resolve6 } from "path";
3561
- import { homedir as homedir5 } from "os";
3562
- import { spawn as spawn4, execSync as execSync2 } from "child_process";
3563
- import pc10 from "picocolors";
5192
+ import { existsSync as existsSync9, unlinkSync as unlinkSync3, statSync as statSync2, openSync as openSync3, fstatSync, readSync, closeSync as closeSync3 } from "fs";
5193
+ import { execSync as execSync2 } from "child_process";
5194
+ import pc15 from "picocolors";
5195
+ var MAX_LOG_LINES = 500;
3564
5196
  async function logCommand(args2) {
3565
5197
  const target = args2[0];
3566
- if (!target) {
3567
- console.error(renderError("Missing process name/pid", "Usage: fennec log <name|pid> [--lines N] [-f] [--clear] [--level error|warn|info|debug]"));
3568
- process.exit(1);
5198
+ if (!target || target.startsWith("--")) {
5199
+ listAvailableApps();
5200
+ return;
3569
5201
  }
3570
5202
  const clearFlag = args2.includes("--clear");
5203
+ const jsonFlag = args2.includes("--json");
5204
+ const noRedact = args2.includes("--no-redact");
3571
5205
  const linesIndex = args2.indexOf("--lines");
3572
- const lineCount = linesIndex !== -1 ? parseInt(args2[linesIndex + 1], 10) : 30;
5206
+ const lineCount = linesIndex !== -1 ? Math.min(parseInt(args2[linesIndex + 1], 10), MAX_LOG_LINES) : 30;
3573
5207
  const followFlag = args2.includes("-f") || args2.includes("--follow");
3574
5208
  const levelFilter = args2.includes("--level") ? args2[args2.indexOf("--level") + 1]?.toLowerCase() : void 0;
5209
+ const sinceIndex = args2.indexOf("--since");
5210
+ const sinceMs = sinceIndex !== -1 ? parseDuration(args2[sinceIndex + 1]) : void 0;
5211
+ if (sinceIndex !== -1 && sinceMs === void 0) {
5212
+ console.error(renderError("Invalid --since", `Use a duration like 10m, 1h, 30s, 2d`));
5213
+ process.exit(1);
5214
+ }
3575
5215
  if (levelFilter && !["error", "warn", "info", "debug"].includes(levelFilter)) {
3576
5216
  console.error(renderError("Invalid level", `"${levelFilter}" is not a valid level. Use: error, warn, info, debug`));
3577
5217
  process.exit(1);
3578
5218
  }
5219
+ const redact = !noRedact;
3579
5220
  let logFilePath = null;
3580
5221
  let displayName = target;
3581
5222
  const tracked = readTracked();
3582
5223
  const trackedMatch = tracked.find((t) => t.name === target || parseInt(target, 10) === t.pid && !isNaN(parseInt(target, 10)));
3583
5224
  if (trackedMatch) {
3584
5225
  displayName = trackedMatch.name;
3585
- logFilePath = resolve6(homedir5(), ".fennec", "logs", `${trackedMatch.name}.log`);
5226
+ logFilePath = logFilePathFor(trackedMatch.name);
3586
5227
  } else {
3587
5228
  const pid = parseInt(target, 10);
3588
5229
  if (!isNaN(pid) && String(pid) === target) {
3589
5230
  const procPath = `/proc/${pid}/fd/1`;
3590
- if (existsSync4(procPath)) logFilePath = procPath;
5231
+ if (existsSync9(procPath)) logFilePath = procPath;
3591
5232
  }
3592
5233
  }
3593
5234
  if (clearFlag) {
3594
- if (!logFilePath || !existsSync4(logFilePath)) {
5235
+ if (!logFilePath || !existsSync9(logFilePath)) {
3595
5236
  console.error(`
3596
- ${pc10.yellow("\u26A0")} ${pc10.dim("No log file found for")} ${pc10.bold(displayName)}
5237
+ ${pc15.yellow("\u26A0")} ${pc15.dim("No log file found for")} ${pc15.bold(displayName)}
3597
5238
  `);
3598
5239
  process.exit(0);
3599
5240
  }
3600
5241
  try {
3601
- unlinkSync(logFilePath);
5242
+ unlinkSync3(logFilePath);
3602
5243
  console.error(`
3603
- ${pc10.green("\u2713")} ${pc10.bold("Log cleared")} ${pc10.dim(`\u2014 ${logFilePath}`)}
5244
+ ${pc15.green("\u2713")} ${pc15.bold("Log cleared")} ${pc15.dim(`\u2014 ${logFilePath}`)}
3604
5245
  `);
3605
5246
  } catch (err) {
3606
5247
  console.error(renderError("Failed to clear log", String(err)));
@@ -3608,18 +5249,75 @@ async function logCommand(args2) {
3608
5249
  }
3609
5250
  return;
3610
5251
  }
5252
+ if (jsonFlag) {
5253
+ let logLines = [];
5254
+ if (logFilePath && existsSync9(logFilePath)) {
5255
+ logLines = readLogLines(logFilePath, {
5256
+ tail: Math.max(lineCount, sinceMs ? MAX_LOG_LINES : 0),
5257
+ sinceMs: sinceMs ?? void 0,
5258
+ redact,
5259
+ parseTimestamp: true
5260
+ });
5261
+ } else {
5262
+ const pid = trackedMatch?.pid ?? parseInt(target, 10);
5263
+ if (!isNaN(pid)) {
5264
+ try {
5265
+ const output = execSync2(`journalctl --no-pager -n ${Math.max(lineCount, 500)} _PID=${pid} 2>/dev/null || echo ""`, { encoding: "utf-8", timeout: 3e3 });
5266
+ logLines = output.trim().split("\n").filter(Boolean).map((l) => redact ? redactLine(stripAnsi(l)) : stripAnsi(l));
5267
+ } catch {
5268
+ logLines = ["(no logs available)"];
5269
+ }
5270
+ } else {
5271
+ logLines = ["(no logs available)"];
5272
+ }
5273
+ }
5274
+ if (levelFilter) {
5275
+ const levelRegex = new RegExp(`\\b(${levelFilter.toUpperCase()})\\b`, "i");
5276
+ logLines = logLines.filter((line) => levelRegex.test(line));
5277
+ }
5278
+ const running = trackedMatch ? isTrackedRunning(trackedMatch) : false;
5279
+ const sliced = logLines.slice(-lineCount);
5280
+ const redactedHits = sliced.reduce((acc, l) => acc + (redactionCount(stripAnsi(l)) > 0 ? 1 : 0), 0);
5281
+ const payload = {
5282
+ app: displayName,
5283
+ running,
5284
+ pid: trackedMatch?.pid != null ? trackedMatch.pid : null,
5285
+ port: trackedMatch?.port != null ? trackedMatch.port : null,
5286
+ lines: sliced,
5287
+ count: sliced.length,
5288
+ redacted: redact,
5289
+ redactedLines: redactedHits,
5290
+ note: "Secrets are redacted by default; use --no-redact to disable."
5291
+ };
5292
+ process.stdout.write(JSON.stringify(payload, null, 2) + "\n");
5293
+ return;
5294
+ }
5295
+ if (trackedMatch) {
5296
+ const running = isTrackedRunning(trackedMatch);
5297
+ const statusStr = running ? pc15.green(`\u25CF running (PID ${trackedMatch.pid})`) : pc15.red("\u25CB stopped");
5298
+ const portStr = trackedMatch.port ? ` ${pc15.yellow(`:${trackedMatch.port}`)}` : "";
5299
+ console.error(`
5300
+ ${symbols.fox} ${renderAppName(displayName)}${portStr} \u2014 ${statusStr}`);
5301
+ if (!running) {
5302
+ console.error(` ${pc15.dim("This app is not running. Showing last captured logs.")} ${pc15.cyan(`fennec spawn ${displayName}`)} ${pc15.dim("to restart.")}`);
5303
+ }
5304
+ }
3611
5305
  const spinner = createSpinner(`Reading logs for ${displayName}...`);
3612
5306
  try {
3613
5307
  let logLines = [];
3614
- if (logFilePath && existsSync4(logFilePath)) {
3615
- const content = readFileSync3(logFilePath, "utf-8");
3616
- logLines = content.split("\n").filter(Boolean);
5308
+ if (logFilePath && existsSync9(logFilePath)) {
5309
+ logLines = readLogLines(logFilePath, {
5310
+ tail: Math.max(lineCount, sinceMs ? MAX_LOG_LINES : 0),
5311
+ sinceMs: sinceMs ?? void 0,
5312
+ redact,
5313
+ parseTimestamp: true
5314
+ });
3617
5315
  } else {
3618
5316
  const pid = trackedMatch?.pid ?? parseInt(target, 10);
3619
5317
  if (!isNaN(pid)) {
3620
5318
  try {
3621
- const output = execSync2(`journalctl --no-pager -n ${lineCount} _PID=${pid} 2>/dev/null || echo ""`, { encoding: "utf-8", timeout: 3e3 });
3622
- logLines = output.trim().split("\n").filter(Boolean);
5319
+ const output = execSync2(`journalctl --no-pager -n ${Math.max(lineCount, 500)} _PID=${pid} 2>/dev/null || echo ""`, { encoding: "utf-8", timeout: 3e3 });
5320
+ logLines = output.trim().split("\n").filter(Boolean).map((l) => redact ? redactLine(stripAnsi(l)) : stripAnsi(l));
3623
5321
  } catch {
3624
5322
  logLines = ["(no logs available)"];
3625
5323
  }
@@ -3634,48 +5332,22 @@ async function logCommand(args2) {
3634
5332
  spinner.stop();
3635
5333
  process.stdout.write("\r\x1B[K");
3636
5334
  console.error(`
3637
- ${symbols.fox} ${pc10.bold("Logs")} ${renderAppName(displayName)} ${pc10.dim(`(last ${logLines.length} line${logLines.length !== 1 ? "s" : ""})`)}`);
5335
+ ${symbols.fox} ${pc15.bold("Logs")} ${renderAppName(displayName)} ${pc15.dim(`(last ${logLines.length} line${logLines.length !== 1 ? "s" : ""})${redact ? pc15.dim(" \xB7 secrets redacted") : ""})`)}`);
3638
5336
  const sliced = logLines.slice(-lineCount);
3639
5337
  for (const line of sliced) {
3640
5338
  const display = line.length > 300 ? line.slice(0, 300) + "\u2026" : line;
3641
- const prefix = followFlag ? `${timestamp()} ` : "";
3642
5339
  if (line.toLowerCase().includes("error") || line.toLowerCase().includes("fail") || line.toLowerCase().includes("fatal")) {
3643
- console.error(` ${pc10.red(prefix + display)}`);
5340
+ console.error(` ${pc15.red(display)}`);
3644
5341
  } else if (line.toLowerCase().includes("warn")) {
3645
- console.error(` ${pc10.yellow(prefix + display)}`);
5342
+ console.error(` ${pc15.yellow(display)}`);
3646
5343
  } else if (line.toLowerCase().includes("info") || line.includes("[")) {
3647
- console.error(` ${pc10.cyan(prefix + display)}`);
5344
+ console.error(` ${pc15.cyan(display)}`);
3648
5345
  } else {
3649
- console.error(` ${prefix}${display}`);
5346
+ console.error(` ${display}`);
3650
5347
  }
3651
5348
  }
3652
5349
  if (followFlag && logFilePath) {
3653
- const tailArgs = ["-n", "0", "-f"];
3654
- if (levelFilter) {
3655
- const tail = spawn4("tail", ["-n", "0", "-f", logFilePath], { stdio: ["ignore", "pipe", "pipe"] });
3656
- const grep = spawn4("grep", ["--line-buffered", "-i", levelFilter], { stdio: ["pipe", "inherit", "inherit"] });
3657
- if (tail.stdout) tail.stdout.pipe(grep.stdin);
3658
- await new Promise((resolve12) => {
3659
- tail.on("exit", () => resolve12());
3660
- process.once("SIGINT", () => {
3661
- tail.kill("SIGTERM");
3662
- grep.kill("SIGTERM");
3663
- resolve12();
3664
- });
3665
- });
3666
- } else {
3667
- console.error(`
3668
- ${pc10.dim("Following... (Ctrl+C to stop)")}
3669
- `);
3670
- const tail = spawn4("tail", tailArgs.concat([logFilePath]), { stdio: "inherit" });
3671
- await new Promise((resolve12) => {
3672
- tail.on("exit", () => resolve12());
3673
- process.once("SIGINT", () => {
3674
- tail.kill("SIGTERM");
3675
- resolve12();
3676
- });
3677
- });
3678
- }
5350
+ await followLog(logFilePath, { redact, levelFilter, json: jsonFlag });
3679
5351
  }
3680
5352
  console.error();
3681
5353
  } catch (error) {
@@ -3683,10 +5355,97 @@ async function logCommand(args2) {
3683
5355
  console.error(renderError("Log read failed", String(error)));
3684
5356
  }
3685
5357
  }
5358
+ async function followLog(logFilePath, opts) {
5359
+ let lastSize = 0;
5360
+ try {
5361
+ lastSize = statSync2(logFilePath).size;
5362
+ } catch {
5363
+ }
5364
+ console.error(`
5365
+ ${pc15.dim("Following... (Ctrl+C to stop)")}
5366
+ `);
5367
+ const levelRe = opts.levelFilter ? new RegExp(`\\b(${opts.levelFilter.toUpperCase()})\\b`, "i") : null;
5368
+ const emit = (raw) => {
5369
+ const line = opts.redact ? redactLine(raw) : raw;
5370
+ if (levelRe && !levelRe.test(line)) return;
5371
+ if (opts.json) {
5372
+ try {
5373
+ process.stdout.write(JSON.stringify(JSON.parse(line)) + "\n");
5374
+ return;
5375
+ } catch {
5376
+ }
5377
+ }
5378
+ const display = line.length > 300 ? line.slice(0, 300) + "\u2026" : line;
5379
+ if (line.toLowerCase().includes("error") || line.toLowerCase().includes("fail") || line.toLowerCase().includes("fatal")) {
5380
+ console.error(` ${pc15.red(display)}`);
5381
+ } else if (line.toLowerCase().includes("warn")) {
5382
+ console.error(` ${pc15.yellow(display)}`);
5383
+ } else if (line.toLowerCase().includes("info") || line.includes("[")) {
5384
+ console.error(` ${pc15.cyan(display)}`);
5385
+ } else {
5386
+ console.error(` ${display}`);
5387
+ }
5388
+ };
5389
+ const timer = setInterval(() => {
5390
+ let fd;
5391
+ try {
5392
+ fd = openSync3(logFilePath, "r");
5393
+ } catch {
5394
+ return;
5395
+ }
5396
+ try {
5397
+ const size = fstatSync(fd).size;
5398
+ if (size < lastSize) lastSize = 0;
5399
+ if (size > lastSize) {
5400
+ const buf = Buffer.alloc(size - lastSize);
5401
+ readSync(fd, buf, 0, buf.length, lastSize);
5402
+ lastSize = size;
5403
+ for (const l of buf.toString("utf8").split("\n")) {
5404
+ if (l.length) emit(l);
5405
+ }
5406
+ }
5407
+ } finally {
5408
+ closeSync3(fd);
5409
+ }
5410
+ }, 400);
5411
+ await new Promise((resolve13) => {
5412
+ const stop = () => {
5413
+ clearInterval(timer);
5414
+ resolve13();
5415
+ };
5416
+ process.once("SIGINT", stop);
5417
+ process.once("SIGTERM", stop);
5418
+ });
5419
+ }
5420
+ function listAvailableApps() {
5421
+ const tracked = readTracked();
5422
+ if (tracked.length === 0) {
5423
+ console.error(`
5424
+ ${symbols.fox} ${pc15.bold("No tracked apps to show logs for.")}`);
5425
+ console.error(` ${pc15.dim("Start one with:")} ${pc15.cyan("fennec start <command> --name <name>")}
5426
+ `);
5427
+ return;
5428
+ }
5429
+ console.error(`
5430
+ ${symbols.fox} ${pc15.bold("Logs")} ${pc15.dim("\u2014 pick an app:")}
5431
+ `);
5432
+ for (const t of tracked) {
5433
+ const running = isTrackedRunning(t);
5434
+ const dot = running ? pc15.green("\u25CF") : pc15.red("\u25CB");
5435
+ const state = running ? pc15.green(`running (PID ${t.pid})`) : pc15.red("stopped");
5436
+ const portStr = t.port ? ` ${pc15.yellow(`:${t.port}`)}` : "";
5437
+ console.error(` ${dot} ${pc15.bold(t.name)}${portStr} ${pc15.dim("\u2014")} ${state}`);
5438
+ console.error(` ${pc15.cyan(`fennec log ${t.name}`)} ${pc15.dim("\xB7")} ${pc15.cyan(`fennec log ${t.name} -f`)}`);
5439
+ }
5440
+ console.error(`
5441
+ ${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")}`);
5442
+ 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.")}
5443
+ `);
5444
+ }
3686
5445
 
3687
5446
  // src/commands/attach.ts
3688
5447
  import { PortDetector as PortDetector3 } from "@plumpslabs/fennec-core";
3689
- import pc11 from "picocolors";
5448
+ import pc16 from "picocolors";
3690
5449
  async function attachCommand(args2) {
3691
5450
  const port = parseInt(args2[0], 10);
3692
5451
  if (isNaN(port)) {
@@ -3704,7 +5463,7 @@ async function attachCommand(args2) {
3704
5463
  spinner.succeed(`Attached to :${port}`);
3705
5464
  console.error(` ${renderKV("Name", renderAppName(name))}`);
3706
5465
  console.error(` ${renderKV("PID", String(info.pid))}`);
3707
- console.error(` ${renderKV("Command", info.command || pc11.dim("unknown"))}`);
5466
+ console.error(` ${renderKV("Command", info.command || pc16.dim("unknown"))}`);
3708
5467
  } else {
3709
5468
  spinner.fail(`No process found on port ${port}`);
3710
5469
  process.exit(1);
@@ -3718,35 +5477,35 @@ async function attachCommand(args2) {
3718
5477
 
3719
5478
  // src/commands/sessions.ts
3720
5479
  import { SessionStore } from "@plumpslabs/fennec-core";
3721
- import pc12 from "picocolors";
5480
+ import pc17 from "picocolors";
3722
5481
  async function sessionsCommand() {
3723
5482
  const store = new SessionStore("./.fennec/sessions");
3724
5483
  const sessions = store.list();
3725
5484
  if (sessions.length === 0) {
3726
5485
  console.error(`
3727
- ${pc12.dim("No saved sessions found.")}
5486
+ ${pc17.dim("No saved sessions found.")}
3728
5487
  `);
3729
5488
  return;
3730
5489
  }
3731
5490
  const columns = [
3732
- { key: "name", label: "Name", format: (v) => pc12.bold(String(v)) },
5491
+ { key: "name", label: "Name", format: (v) => pc17.bold(String(v)) },
3733
5492
  { key: "origin", label: "Origin" },
3734
- { key: "savedAt", label: "Saved", format: (v) => pc12.dim(String(v)) }
5493
+ { key: "savedAt", label: "Saved", format: (v) => pc17.dim(String(v)) }
3735
5494
  ];
3736
5495
  const rows = sessions.map((s) => ({ name: s.name, origin: s.origin, savedAt: new Date(s.savedAt).toLocaleString() }));
3737
5496
  console.error(`
3738
- ${symbols.fox} ${pc12.bold("Saved Sessions")}
5497
+ ${symbols.fox} ${pc17.bold("Saved Sessions")}
3739
5498
  `);
3740
5499
  console.error(renderTable(columns, rows));
3741
- console.error(` ${pc12.dim(`${sessions.length} session(s)`)}
5500
+ console.error(` ${pc17.dim(`${sessions.length} session(s)`)}
3742
5501
  `);
3743
5502
  }
3744
5503
 
3745
5504
  // src/commands/setup.ts
3746
- import pc13 from "picocolors";
5505
+ import pc18 from "picocolors";
3747
5506
  async function setupCommand() {
3748
5507
  console.error(`
3749
- ${symbols.fox} ${pc13.bold("Fennec Setup")}
5508
+ ${symbols.fox} ${pc18.bold("Fennec Setup")}
3750
5509
  `);
3751
5510
  const mcpClient = await selectPrompt("Which MCP client are you using?", [
3752
5511
  { value: "claude", label: "Claude Desktop", description: "Anthropic's AI desktop app" },
@@ -3755,12 +5514,12 @@ async function setupCommand() {
3755
5514
  { value: "other", label: "Other MCP client", description: "Any MCP-compatible client" }
3756
5515
  ]);
3757
5516
  if (!mcpClient) {
3758
- console.error(` ${pc13.dim("Setup cancelled.")}
5517
+ console.error(` ${pc18.dim("Setup cancelled.")}
3759
5518
  `);
3760
5519
  return;
3761
5520
  }
3762
5521
  console.error(`
3763
- ${pc13.green("\u2713")} Selected: ${pc13.bold(mcpClient)}
5522
+ ${pc18.green("\u2713")} Selected: ${pc18.bold(mcpClient)}
3764
5523
  `);
3765
5524
  const configSnippet = `{
3766
5525
  "mcpServers": {
@@ -3770,24 +5529,24 @@ async function setupCommand() {
3770
5529
  }
3771
5530
  }
3772
5531
  }`;
3773
- console.error(` ${pc13.bold("Add this to your MCP client config:")}
5532
+ console.error(` ${pc18.bold("Add this to your MCP client config:")}
3774
5533
  `);
3775
- console.error(` ${pc13.dim("```")}`);
5534
+ console.error(` ${pc18.dim("```")}`);
3776
5535
  console.error(configSnippet.split("\n").map((l) => ` ${l}`).join("\n"));
3777
- console.error(` ${pc13.dim("```")}
5536
+ console.error(` ${pc18.dim("```")}
3778
5537
  `);
3779
- console.error(` ${renderSuccess("Setup complete!")} ${pc13.dim("Run")} ${renderCommand("fennec start")} ${pc13.dim("to begin.")}
5538
+ console.error(` ${renderSuccess("Setup complete!")} ${pc18.dim("Run")} ${renderCommand("fennec start")} ${pc18.dim("to begin.")}
3780
5539
  `);
3781
5540
  }
3782
5541
 
3783
5542
  // src/commands/management.ts
3784
- import { existsSync as existsSync5, writeFileSync as writeFileSync2 } from "fs";
3785
- import { resolve as resolve7 } from "path";
5543
+ import { existsSync as existsSync10, writeFileSync as writeFileSync4 } from "fs";
5544
+ import { resolve as resolve8 } from "path";
3786
5545
  import { execSync as execSync3 } from "child_process";
3787
- import pc14 from "picocolors";
5546
+ import pc19 from "picocolors";
3788
5547
  async function installBrowsersCommand() {
3789
5548
  console.error(`
3790
- ${pc14.bold("Installing Browser Engines")}
5549
+ ${pc19.bold("Installing Browser Engines")}
3791
5550
  `);
3792
5551
  const spinner = createSpinner("Installing Chromium...");
3793
5552
  try {
@@ -3795,21 +5554,21 @@ async function installBrowsersCommand() {
3795
5554
  spinner.succeed("Chromium installed successfully");
3796
5555
  } catch {
3797
5556
  spinner.fail("Failed to install Chromium");
3798
- console.error(` ${pc14.yellow("\u2192")} Try running: ${renderCommand("npx playwright install chromium")}`);
5557
+ console.error(` ${pc19.yellow("\u2192")} Try running: ${renderCommand("npx playwright install chromium")}`);
3799
5558
  }
3800
5559
  console.error(`
3801
- ${pc14.green("\u2713")} Browser installation complete.
5560
+ ${pc19.green("\u2713")} Browser installation complete.
3802
5561
  `);
3803
5562
  }
3804
5563
  async function initCommand() {
3805
5564
  console.error(`
3806
- ${pc14.bold("Initialize Fennec Configuration")}
5565
+ ${pc19.bold("Initialize Fennec Configuration")}
3807
5566
  `);
3808
- const configFile = resolve7("./fennec.config.yaml");
3809
- if (existsSync5(configFile)) {
3810
- const overwrite = await confirmPrompt(`${pc14.yellow("fennec.config.yaml")} already exists. Overwrite?`, false);
5567
+ const configFile = resolve8("./fennec.config.yaml");
5568
+ if (existsSync10(configFile)) {
5569
+ const overwrite = await confirmPrompt(`${pc19.yellow("fennec.config.yaml")} already exists. Overwrite?`, false);
3811
5570
  if (!overwrite) {
3812
- console.error(` ${pc14.dim("Cancelled.")}
5571
+ console.error(` ${pc19.dim("Cancelled.")}
3813
5572
  `);
3814
5573
  return;
3815
5574
  }
@@ -3888,22 +5647,22 @@ logging:
3888
5647
  format: pretty
3889
5648
  file: null
3890
5649
  `;
3891
- writeFileSync2(configFile, config, "utf-8");
3892
- spinner.succeed(`Configuration written to ${pc14.bold(configFile)}`);
5650
+ writeFileSync4(configFile, config, "utf-8");
5651
+ spinner.succeed(`Configuration written to ${pc19.bold(configFile)}`);
3893
5652
  console.error(`
3894
- ${pc14.dim("Edit the file to customize Fennec behavior.")}
5653
+ ${pc19.dim("Edit the file to customize Fennec behavior.")}
3895
5654
  `);
3896
5655
  }
3897
5656
 
3898
5657
  // src/commands/health.ts
3899
- import { existsSync as existsSync6, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
5658
+ import { existsSync as existsSync11, readdirSync as readdirSync2, statSync as statSync3 } from "fs";
3900
5659
  import { execSync as execSync4 } from "child_process";
3901
- import { resolve as resolve8 } from "path";
5660
+ import { resolve as resolve9 } from "path";
3902
5661
  import { homedir as homedir6 } from "os";
3903
- import pc15 from "picocolors";
5662
+ import pc20 from "picocolors";
3904
5663
  async function healthCommand() {
3905
5664
  printBanner();
3906
- console.error(` ${pc15.bold("Fennec Health Check")}
5665
+ console.error(` ${pc20.bold("Fennec Health Check")}
3907
5666
  `);
3908
5667
  let adbStatus = "unknown";
3909
5668
  try {
@@ -3913,20 +5672,20 @@ async function healthCommand() {
3913
5672
  stdio: ["ignore", "pipe", "pipe"]
3914
5673
  });
3915
5674
  const version = result.split("\n")[0]?.trim() ?? "installed";
3916
- adbStatus = pc15.green(version);
5675
+ adbStatus = pc20.green(version);
3917
5676
  } catch {
3918
- adbStatus = pc15.dim("not found") + " (optional)";
5677
+ adbStatus = pc20.dim("not found") + " (optional)";
3919
5678
  }
3920
5679
  const tracked = readTracked();
3921
5680
  const runningCount = tracked.filter((t) => isProcessRunning(t.pid)).length;
3922
- const logDir = resolve8(homedir6(), ".fennec", "logs");
5681
+ const logDir = resolve9(homedir6(), ".fennec", "logs");
3923
5682
  let logSize = "0 B";
3924
5683
  try {
3925
- if (existsSync6(logDir)) {
5684
+ if (existsSync11(logDir)) {
3926
5685
  const files = readdirSync2(logDir);
3927
5686
  let totalBytes = 0;
3928
5687
  for (const f of files) {
3929
- const fStat = statSync2(resolve8(logDir, f));
5688
+ const fStat = statSync3(resolve9(logDir, f));
3930
5689
  totalBytes += fStat.size;
3931
5690
  }
3932
5691
  logSize = totalBytes > 1024 * 1024 ? `${(totalBytes / (1024 * 1024)).toFixed(1)} MB` : `${(totalBytes / 1024).toFixed(1)} KB`;
@@ -3939,7 +5698,7 @@ async function healthCommand() {
3939
5698
  memoryInfo = `${(mem.rss / (1024 * 1024)).toFixed(0)} MB RSS / ${(mem.heapUsed / (1024 * 1024)).toFixed(0)} MB heap`;
3940
5699
  } catch {
3941
5700
  }
3942
- console.error(` ${symbols.fox} ${pc15.bold("System")}
5701
+ console.error(` ${symbols.fox} ${pc20.bold("System")}
3943
5702
  `);
3944
5703
  console.error(` ${renderKV("Node.js", process.version)}`);
3945
5704
  console.error(` ${renderKV("Platform", `${process.platform} ${process.arch}`)}`);
@@ -3947,7 +5706,7 @@ async function healthCommand() {
3947
5706
  console.error(` ${renderKV("ADB", adbStatus)}`);
3948
5707
  console.error(` ${renderKV("PID", String(process.pid))}`);
3949
5708
  console.error();
3950
- console.error(` ${symbols.fox} ${pc15.bold("Processes")}
5709
+ console.error(` ${symbols.fox} ${pc20.bold("Processes")}
3951
5710
  `);
3952
5711
  console.error(` ${renderKV("Tracked", String(tracked.length))}`);
3953
5712
  console.error(` ${renderKV("Running", String(runningCount))}`);
@@ -3955,55 +5714,55 @@ async function healthCommand() {
3955
5714
  console.error();
3956
5715
  const allHealthy = runningCount === tracked.length || tracked.length === 0;
3957
5716
  if (allHealthy) {
3958
- console.error(` ${pc15.green("\u2713")} ${pc15.bold("All systems healthy")}
5717
+ console.error(` ${pc20.green("\u2713")} ${pc20.bold("All systems healthy")}
3959
5718
  `);
3960
5719
  } else {
3961
5720
  const stopped = tracked.length - runningCount;
3962
- console.error(` ${pc15.yellow("\u26A0")} ${pc15.bold(`${stopped} process(es) stopped`)} ${pc15.dim("fennec ps")}
5721
+ console.error(` ${pc20.yellow("\u26A0")} ${pc20.bold(`${stopped} process(es) stopped`)} ${pc20.dim("fennec ps")}
3963
5722
  `);
3964
5723
  }
3965
5724
  }
3966
5725
 
3967
5726
  // src/commands/cleanup.ts
3968
- import pc16 from "picocolors";
5727
+ import pc21 from "picocolors";
3969
5728
  async function cleanupCommand() {
3970
5729
  const tracked = readTracked();
3971
5730
  if (tracked.length === 0) {
3972
5731
  console.error(`
3973
- ${pc16.dim("No tracked processes to clean up.")}
5732
+ ${pc21.dim("No tracked processes to clean up.")}
3974
5733
  `);
3975
5734
  return;
3976
5735
  }
3977
5736
  const toRemove = tracked.filter((t) => !isProcessRunning(t.pid) && !t.command);
3978
5737
  if (toRemove.length === 0) {
3979
5738
  console.error(`
3980
- ${pc16.green("\u2713")} ${pc16.bold("All clean")} ${pc16.dim("\u2014 no dead entries without saved commands.")}
5739
+ ${pc21.green("\u2713")} ${pc21.bold("All clean")} ${pc21.dim("\u2014 no dead entries without saved commands.")}
3981
5740
  `);
3982
5741
  return;
3983
5742
  }
3984
5743
  console.error(`
3985
- ${pc16.yellow("\u26A0")} ${pc16.bold(`Found ${toRemove.length} dead entr${toRemove.length > 1 ? "ies" : "y"} without saved commands`)}
5744
+ ${pc21.yellow("\u26A0")} ${pc21.bold(`Found ${toRemove.length} dead entr${toRemove.length > 1 ? "ies" : "y"} without saved commands`)}
3986
5745
  `);
3987
5746
  for (const entry of toRemove) {
3988
- console.error(` ${pc16.dim("\xB7")} ${pc16.bold(entry.name)} ${pc16.dim(`(PID ${entry.pid})`)}`);
5747
+ console.error(` ${pc21.dim("\xB7")} ${pc21.bold(entry.name)} ${pc21.dim(`(PID ${entry.pid})`)}`);
3989
5748
  }
3990
5749
  console.error();
3991
5750
  const confirmed = await confirmPrompt(`Remove ${toRemove.length} entr${toRemove.length > 1 ? "ies" : "y"}?`, false);
3992
5751
  if (!confirmed) {
3993
- console.error(` ${pc16.dim("Cancelled")}
5752
+ console.error(` ${pc21.dim("Cancelled")}
3994
5753
  `);
3995
5754
  return;
3996
5755
  }
3997
5756
  const remaining = tracked.filter((t) => !toRemove.includes(t));
3998
5757
  saveTracked(remaining);
3999
5758
  console.error(`
4000
- ${pc16.green("\u2713")} ${pc16.bold(`Removed ${toRemove.length} entr${toRemove.length > 1 ? "ies" : "y"}`)} ${pc16.dim(`(${remaining.length} remaining)`)}
5759
+ ${pc21.green("\u2713")} ${pc21.bold(`Removed ${toRemove.length} entr${toRemove.length > 1 ? "ies" : "y"}`)} ${pc21.dim(`(${remaining.length} remaining)`)}
4001
5760
  `);
4002
5761
  }
4003
5762
 
4004
5763
  // src/commands/info.ts
4005
- import pc17 from "picocolors";
4006
- import { resolve as resolve9 } from "path";
5764
+ import pc22 from "picocolors";
5765
+ import { resolve as resolve10 } from "path";
4007
5766
  import { homedir as homedir7 } from "os";
4008
5767
  async function infoCommand(args2) {
4009
5768
  const name = args2[0];
@@ -4018,33 +5777,33 @@ async function infoCommand(args2) {
4018
5777
  process.exit(1);
4019
5778
  }
4020
5779
  const running = isProcessRunning(match.pid);
4021
- const statusIcon = running ? pc17.green("\u25CF") : pc17.red("\u25CB");
4022
- const statusText = running ? pc17.green("running") : pc17.red("stopped");
5780
+ const statusIcon = running ? pc22.green("\u25CF") : pc22.red("\u25CB");
5781
+ const statusText = running ? pc22.green("running") : pc22.red("stopped");
4023
5782
  const uptime = running ? formatUptime(Math.floor((Date.now() - new Date(match.startedAt).getTime()) / 1e3)) : "-";
4024
- const logPath = resolve9(homedir7(), ".fennec", "logs", `${match.name}.log`);
5783
+ const logPath = resolve10(homedir7(), ".fennec", "logs", `${match.name}.log`);
4025
5784
  console.error(`
4026
- ${symbols.fox} ${pc17.bold(match.name)} ${pc17.dim("\u2014 Process Info")}
5785
+ ${symbols.fox} ${pc22.bold(match.name)} ${pc22.dim("\u2014 Process Info")}
4027
5786
  `);
4028
- console.error(` ${renderKVColor("Name", match.name, pc17.bold)}`);
5787
+ console.error(` ${renderKVColor("Name", match.name, pc22.bold)}`);
4029
5788
  console.error(` ${renderKVColor("Status", `${statusIcon} ${statusText}`)}`);
4030
5789
  console.error(` ${renderKVColor("PID", String(match.pid))}`);
4031
5790
  console.error(` ${renderKVColor("Port", match.port ? String(match.port) : "-")}`);
4032
- console.error(` ${renderKVColor("Command", match.command || pc17.dim("(none)"))}`);
5791
+ console.error(` ${renderKVColor("Command", match.command || pc22.dim("(none)"))}`);
4033
5792
  if (match.cwd) console.error(` ${renderKVColor("CWD", match.cwd)}`);
4034
5793
  console.error(` ${renderKVColor("Started", new Date(match.startedAt).toLocaleString())}`);
4035
5794
  console.error(` ${renderKVColor("Uptime", uptime)}`);
4036
5795
  console.error(` ${renderKVColor("Log Path", logPath)}`);
4037
5796
  if (!running && match.command) {
4038
5797
  console.error(`
4039
- ${pc17.dim("Re-spawn with:")} ${pc17.cyan(`fennec spawn ${match.name}`)}`);
5798
+ ${pc22.dim("Re-spawn with:")} ${pc22.cyan(`fennec spawn ${match.name}`)}`);
4040
5799
  }
4041
5800
  console.error();
4042
5801
  }
4043
5802
 
4044
5803
  // src/commands/rename.ts
4045
- import pc18 from "picocolors";
4046
- import { existsSync as existsSync7, renameSync as renameSync2 } from "fs";
4047
- import { resolve as resolve10 } from "path";
5804
+ import pc23 from "picocolors";
5805
+ import { existsSync as existsSync12, renameSync as renameSync2 } from "fs";
5806
+ import { resolve as resolve11 } from "path";
4048
5807
  import { homedir as homedir8 } from "os";
4049
5808
  async function renameCommand(args2) {
4050
5809
  const [oldName, newName] = args2;
@@ -4054,7 +5813,7 @@ async function renameCommand(args2) {
4054
5813
  }
4055
5814
  if (oldName === newName) {
4056
5815
  console.error(`
4057
- ${pc18.yellow("\u26A0")} ${pc18.dim("Old and new names are the same.")}
5816
+ ${pc23.yellow("\u26A0")} ${pc23.dim("Old and new names are the same.")}
4058
5817
  `);
4059
5818
  process.exit(0);
4060
5819
  }
@@ -4069,25 +5828,25 @@ async function renameCommand(args2) {
4069
5828
  process.exit(1);
4070
5829
  }
4071
5830
  console.error(`
4072
- ${symbols.fox} ${pc18.bold("Rename Process")}
5831
+ ${symbols.fox} ${pc23.bold("Rename Process")}
4073
5832
  `);
4074
5833
  console.error(` ${renderKVColor("From", oldName)}`);
4075
5834
  console.error(` ${renderKVColor("To", newName)}`);
4076
5835
  if (isProcessRunning(match.pid)) {
4077
- console.error(` ${pc18.yellow("\u26A0")} ${pc18.dim("Process is still running \u2014 log entries will go to the old file until stopped.")}`);
5836
+ console.error(` ${pc23.yellow("\u26A0")} ${pc23.dim("Process is still running \u2014 log entries will go to the old file until stopped.")}`);
4078
5837
  }
4079
5838
  console.error();
4080
- const confirmed = await confirmPrompt(`Rename ${pc18.bold(oldName)} ${pc18.dim("\u2192")} ${pc18.bold(newName)}?`, true);
5839
+ const confirmed = await confirmPrompt(`Rename ${pc23.bold(oldName)} ${pc23.dim("\u2192")} ${pc23.bold(newName)}?`, true);
4081
5840
  if (!confirmed) {
4082
- console.error(` ${pc18.dim("Cancelled")}
5841
+ console.error(` ${pc23.dim("Cancelled")}
4083
5842
  `);
4084
5843
  return;
4085
5844
  }
4086
5845
  const spinner = createSpinner(`Renaming ${oldName} \u2192 ${newName}...`);
4087
- const logDir = resolve10(homedir8(), ".fennec", "logs");
4088
- const oldLog = resolve10(logDir, `${oldName}.log`);
4089
- const newLog = resolve10(logDir, `${newName}.log`);
4090
- if (existsSync7(oldLog) && !existsSync7(newLog)) {
5846
+ const logDir = resolve11(homedir8(), ".fennec", "logs");
5847
+ const oldLog = resolve11(logDir, `${oldName}.log`);
5848
+ const newLog = resolve11(logDir, `${newName}.log`);
5849
+ if (existsSync12(oldLog) && !existsSync12(newLog)) {
4091
5850
  try {
4092
5851
  renameSync2(oldLog, newLog);
4093
5852
  } catch {
@@ -4095,14 +5854,14 @@ async function renameCommand(args2) {
4095
5854
  }
4096
5855
  match.name = newName;
4097
5856
  saveTracked(tracked);
4098
- spinner.succeed(`${pc18.bold(oldName)} ${pc18.dim("\u2192")} ${pc18.bold(newName)}`);
5857
+ spinner.succeed(`${pc23.bold(oldName)} ${pc23.dim("\u2192")} ${pc23.bold(newName)}`);
4099
5858
  console.error();
4100
5859
  }
4101
5860
 
4102
5861
  // src/commands/export-import.ts
4103
- import pc19 from "picocolors";
4104
- import { existsSync as existsSync8, writeFileSync as writeFileSync3, readFileSync as readFileSync4 } from "fs";
4105
- import { resolve as resolve11 } from "path";
5862
+ import pc24 from "picocolors";
5863
+ import { existsSync as existsSync13, writeFileSync as writeFileSync5, readFileSync as readFileSync9 } from "fs";
5864
+ import { resolve as resolve12 } from "path";
4106
5865
  async function exportCommand(args2) {
4107
5866
  const tracked = readTracked();
4108
5867
  const fileIndex = args2.indexOf("--file");
@@ -4113,16 +5872,16 @@ async function exportCommand(args2) {
4113
5872
  }
4114
5873
  if (tracked.length === 0) {
4115
5874
  console.error(`
4116
- ${pc19.dim("No tracked processes to export.")}
5875
+ ${pc24.dim("No tracked processes to export.")}
4117
5876
  `);
4118
5877
  return;
4119
5878
  }
4120
5879
  const json = JSON.stringify(tracked, null, 2);
4121
5880
  if (filePath) {
4122
5881
  try {
4123
- writeFileSync3(resolve11(filePath), json, "utf-8");
5882
+ writeFileSync5(resolve12(filePath), json, "utf-8");
4124
5883
  console.error(`
4125
- ${pc19.green("\u2713")} ${pc19.bold(`Exported ${tracked.length} process(es)`)} ${pc19.dim(`\u2192 ${filePath}`)}
5884
+ ${pc24.green("\u2713")} ${pc24.bold(`Exported ${tracked.length} process(es)`)} ${pc24.dim(`\u2192 ${filePath}`)}
4126
5885
  `);
4127
5886
  } catch (err) {
4128
5887
  console.error(renderError("Export failed", String(err)));
@@ -4138,14 +5897,14 @@ async function importCommand(args2) {
4138
5897
  console.error(renderError("Missing file", "Usage: fennec import <file>"));
4139
5898
  process.exit(1);
4140
5899
  }
4141
- const resolvedPath = resolve11(filePath);
4142
- if (!existsSync8(resolvedPath)) {
5900
+ const resolvedPath = resolve12(filePath);
5901
+ if (!existsSync13(resolvedPath)) {
4143
5902
  console.error(renderError("File not found", `"${filePath}" does not exist.`));
4144
5903
  process.exit(1);
4145
5904
  }
4146
5905
  let imported;
4147
5906
  try {
4148
- imported = JSON.parse(readFileSync4(resolvedPath, "utf-8"));
5907
+ imported = JSON.parse(readFileSync9(resolvedPath, "utf-8"));
4149
5908
  if (!Array.isArray(imported) || imported.length === 0) {
4150
5909
  console.error(renderError("Invalid file", "File must contain a non-empty JSON array of process objects."));
4151
5910
  process.exit(1);
@@ -4156,20 +5915,20 @@ async function importCommand(args2) {
4156
5915
  }
4157
5916
  const existing = readTracked();
4158
5917
  console.error(`
4159
- ${symbols.fox} ${pc19.bold("Import Processes")}
5918
+ ${symbols.fox} ${pc24.bold("Import Processes")}
4160
5919
  `);
4161
5920
  console.error(` ${renderKVColor("File", resolvedPath)}`);
4162
5921
  console.error(` ${renderKVColor("Importing", `${imported.length} process(es)`)}`);
4163
5922
  console.error(` ${renderKVColor("Existing", `${existing.length} process(es)`)}`);
4164
5923
  console.error(`
4165
- ${pc19.bold("Processes to import:")}`);
5924
+ ${pc24.bold("Processes to import:")}`);
4166
5925
  for (const p of imported) {
4167
- console.error(` ${pc19.green("+")} ${pc19.bold(p.name)} ${pc19.dim(`(${p.command})`)}`);
5926
+ console.error(` ${pc24.green("+")} ${pc24.bold(p.name)} ${pc24.dim(`(${p.command})`)}`);
4168
5927
  }
4169
5928
  console.error();
4170
5929
  const confirmed = await confirmPrompt("Merge into tracked.json? Existing processes with the same name will be overwritten.", true);
4171
5930
  if (!confirmed) {
4172
- console.error(` ${pc19.dim("Cancelled")}
5931
+ console.error(` ${pc24.dim("Cancelled")}
4173
5932
  `);
4174
5933
  return;
4175
5934
  }
@@ -4183,13 +5942,18 @@ async function importCommand(args2) {
4183
5942
  }
4184
5943
  }
4185
5944
  saveTracked(merged);
4186
- console.error(` ${pc19.green("\u2713")} ${pc19.bold(`Imported ${imported.length} process(es)`)} ${pc19.dim(`(${merged.length} total)`)}
5945
+ console.error(` ${pc24.green("\u2713")} ${pc24.bold(`Imported ${imported.length} process(es)`)} ${pc24.dim(`(${merged.length} total)`)}
4187
5946
  `);
4188
5947
  }
4189
5948
 
4190
5949
  // src/index.ts
4191
5950
  var [, , command, ...args] = process.argv;
4192
5951
  async function main() {
5952
+ if (command && command !== "help" && command !== "--help" && command !== "-h" && (args.includes("--help") || args.includes("-h"))) {
5953
+ printBanner();
5954
+ showCommandHelp(command);
5955
+ return;
5956
+ }
4193
5957
  if (!command || command === "start") {
4194
5958
  if (args.length === 0 || args[0]?.startsWith("--")) {
4195
5959
  await startServer(args);
@@ -4204,7 +5968,7 @@ async function main() {
4204
5968
  } else if (command === "status") {
4205
5969
  printBanner();
4206
5970
  await statusCommand(args);
4207
- } else if (command === "log") {
5971
+ } else if (command === "log" || command === "logs") {
4208
5972
  await logCommand(args);
4209
5973
  } else if (command === "spawn") {
4210
5974
  await spawnCommand(args);
@@ -4214,6 +5978,21 @@ async function main() {
4214
5978
  await killCommand(args);
4215
5979
  } else if (command === "restart") {
4216
5980
  await restartCommand(args);
5981
+ } else if (command === "adopt") {
5982
+ adoptCommand(args);
5983
+ } else if (command === "supervisor") {
5984
+ printBanner();
5985
+ await supervisorCommand(args);
5986
+ } else if (command === "__supervisor") {
5987
+ await runSupervisor();
5988
+ } else if (command === "persist") {
5989
+ printBanner();
5990
+ await persistCommand(args);
5991
+ } else if (command === "inspect") {
5992
+ await inspectCommand(args);
5993
+ } else if (command === "dev") {
5994
+ printBanner();
5995
+ await devCommand(args);
4217
5996
  } else if (command === "info") {
4218
5997
  printBanner();
4219
5998
  await infoCommand(args);
@@ -4252,7 +6031,11 @@ async function main() {
4252
6031
  printBanner();
4253
6032
  } else if (command === "help" || command === "--help" || command === "-h") {
4254
6033
  printBanner();
4255
- showHelp();
6034
+ if (args[0] && !args[0].startsWith("-")) {
6035
+ showCommandHelp(args[0]);
6036
+ } else {
6037
+ showHelp();
6038
+ }
4256
6039
  } else {
4257
6040
  console.error(renderError(`Unknown command: ${command}`, "Run 'fennec help' for usage information"));
4258
6041
  process.exit(1);