@plumpslabs/fennec-cli 1.14.3 → 1.14.4

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
@@ -1922,7 +1922,7 @@ function renderTable(columns, rows, options) {
1922
1922
  "top-mid": "\u252C",
1923
1923
  "top-left": "\u250C",
1924
1924
  "top-right": "\u2510",
1925
- "bottom": "\u2500",
1925
+ bottom: "\u2500",
1926
1926
  "bottom-mid": "\u2534",
1927
1927
  "bottom-left": "\u2514",
1928
1928
  "bottom-right": "\u2518",
@@ -2166,7 +2166,10 @@ var COMMANDS = {
2166
2166
  ["--name <name>", "App name used for tracking (recommended)"],
2167
2167
  ["--port <port>", "Port the app listens on \u2014 Fennec waits until it accepts connections"],
2168
2168
  ["--cwd <dir>", "Working directory to run the command in"],
2169
- ["--restart", "Auto-restart if it crashes or its port stops listening (detached supervisor, survives terminal close)"],
2169
+ [
2170
+ "--restart",
2171
+ "Auto-restart if it crashes or its port stops listening (detached supervisor, survives terminal close)"
2172
+ ],
2170
2173
  ["--group <group>", "Logical group/namespace for bulk ops (kill/spawn/stop --group <group>)"]
2171
2174
  ],
2172
2175
  examples: [
@@ -2268,7 +2271,13 @@ var COMMANDS = {
2268
2271
  ["--group <g>, -g <g>", "Kill only apps in group <g> (other groups untouched)"],
2269
2272
  ["-y, --yes", "Skip the confirmation prompt"]
2270
2273
  ],
2271
- examples: ["kill web", "kill 12345 --signal SIGKILL", "kill be-crm fe-crm -y", "kill all -y", "kill --group backend -y"]
2274
+ examples: [
2275
+ "kill web",
2276
+ "kill 12345 --signal SIGKILL",
2277
+ "kill be-crm fe-crm -y",
2278
+ "kill all -y",
2279
+ "kill --group backend -y"
2280
+ ]
2272
2281
  },
2273
2282
  supervisor: {
2274
2283
  name: "supervisor",
@@ -2462,7 +2471,27 @@ function findCommandDoc(cmd) {
2462
2471
  }
2463
2472
  var GROUPS = [
2464
2473
  { title: "Server", keys: ["start-server"] },
2465
- { title: "Apps & Processes", keys: ["start", "run", "ps", "status", "log", "spawn", "stop", "restart", "kill", "group", "supervisor", "persist", "dev", "inspect", "info", "rename"] },
2474
+ {
2475
+ title: "Apps & Processes",
2476
+ keys: [
2477
+ "start",
2478
+ "run",
2479
+ "ps",
2480
+ "status",
2481
+ "log",
2482
+ "spawn",
2483
+ "stop",
2484
+ "restart",
2485
+ "kill",
2486
+ "group",
2487
+ "supervisor",
2488
+ "persist",
2489
+ "dev",
2490
+ "inspect",
2491
+ "info",
2492
+ "rename"
2493
+ ]
2494
+ },
2466
2495
  { title: "Observation", keys: ["attach", "attach-pid", "attach-port", "pipe", "watch"] },
2467
2496
  { title: "Data", keys: ["export", "import", "cleanup"] },
2468
2497
  { title: "Configuration", keys: ["init", "setup", "install-browsers", "sessions"] },
@@ -2476,7 +2505,9 @@ function showHelp() {
2476
2505
  const lines = [];
2477
2506
  lines.push("");
2478
2507
  lines.push(` ${pc3.bold("Usage:")} fennec ${pc3.dim("<command>")} ${pc3.dim("[options]")}`);
2479
- lines.push(` ${pc3.dim("Run")} ${pc3.cyan("fennec <command> --help")} ${pc3.dim("for details on any command.")}`);
2508
+ lines.push(
2509
+ ` ${pc3.dim("Run")} ${pc3.cyan("fennec <command> --help")} ${pc3.dim("for details on any command.")}`
2510
+ );
2480
2511
  for (const group of GROUPS) {
2481
2512
  lines.push("");
2482
2513
  lines.push(` ${sep}`);
@@ -2501,8 +2532,10 @@ function showCommandHelp(cmd) {
2501
2532
  if (!doc) {
2502
2533
  console.error(`
2503
2534
  ${pc3.yellow("\u26A0")} Unknown command: ${pc3.bold(cmd)}`);
2504
- console.error(` ${pc3.dim("Run")} ${pc3.cyan("fennec help")} ${pc3.dim("to see all commands.")}
2505
- `);
2535
+ console.error(
2536
+ ` ${pc3.dim("Run")} ${pc3.cyan("fennec help")} ${pc3.dim("to see all commands.")}
2537
+ `
2538
+ );
2506
2539
  return;
2507
2540
  }
2508
2541
  const lines = [];
@@ -2611,7 +2644,9 @@ async function attachPortCommand(args2) {
2611
2644
  const detector = new PortDetector2();
2612
2645
  const info = detector.detectByPort(port);
2613
2646
  if (info) {
2614
- console.log(`Found process on port ${port}: PID ${info.pid}${info.command ? ` (${info.command})` : ""}`);
2647
+ console.log(
2648
+ `Found process on port ${port}: PID ${info.pid}${info.command ? ` (${info.command})` : ""}`
2649
+ );
2615
2650
  } else {
2616
2651
  console.error(`No process found listening on port ${port}`);
2617
2652
  process.exit(1);
@@ -2650,7 +2685,18 @@ import { FennecServer } from "@plumpslabs/fennec-core";
2650
2685
  import pc7 from "picocolors";
2651
2686
 
2652
2687
  // src/commands/tracker.ts
2653
- import { existsSync as existsSync3, writeFileSync, readFileSync as readFileSync2, mkdirSync, renameSync, statSync, rmSync, openSync, closeSync, writeSync } from "fs";
2688
+ import {
2689
+ existsSync as existsSync3,
2690
+ writeFileSync,
2691
+ readFileSync as readFileSync2,
2692
+ mkdirSync,
2693
+ renameSync,
2694
+ statSync,
2695
+ rmSync,
2696
+ openSync,
2697
+ closeSync,
2698
+ writeSync
2699
+ } from "fs";
2654
2700
  import { spawn } from "child_process";
2655
2701
  import { resolve as resolve2, dirname, basename } from "path";
2656
2702
  import { homedir } from "os";
@@ -2712,7 +2758,10 @@ function readSingleProcProcess(pid, currentUid, totalMemKb) {
2712
2758
  if (uptimeSeconds > 0) {
2713
2759
  const elapsedJiffies = uptimeSeconds * hertz;
2714
2760
  const processJiffies = totalJiffies;
2715
- cpuPercent = Math.min(100, processJiffies / Math.max(1, elapsedJiffies) * 100 * cpus().length);
2761
+ cpuPercent = Math.min(
2762
+ 100,
2763
+ processJiffies / Math.max(1, elapsedJiffies) * 100 * cpus().length
2764
+ );
2716
2765
  }
2717
2766
  startedAt = formatStartTime(starttime, uptimeSeconds, hertz);
2718
2767
  }
@@ -2942,7 +2991,10 @@ function isProcessRunning(pid) {
2942
2991
  function getProcessMemRss(pid) {
2943
2992
  if (process.platform === "win32") {
2944
2993
  try {
2945
- const out = execSync(`tasklist /fi "PID eq ${pid}" /fo csv /nh 2>nul`, { encoding: "utf-8", timeout: 3e3 });
2994
+ const out = execSync(`tasklist /fi "PID eq ${pid}" /fo csv /nh 2>nul`, {
2995
+ encoding: "utf-8",
2996
+ timeout: 3e3
2997
+ });
2946
2998
  for (const raw of out.split("\n")) {
2947
2999
  if (!raw.trim()) continue;
2948
3000
  const parts = raw.split('","').map((p) => p.replace(/^"|"$/g, ""));
@@ -2957,7 +3009,10 @@ function getProcessMemRss(pid) {
2957
3009
  }
2958
3010
  if (process.platform === "darwin" || process.platform === "freebsd" || process.platform === "openbsd" || process.platform === "netbsd") {
2959
3011
  try {
2960
- const out = execSync(`ps -o rss= -p ${pid} 2>/dev/null`, { encoding: "utf-8", timeout: 3e3 }).trim();
3012
+ const out = execSync(`ps -o rss= -p ${pid} 2>/dev/null`, {
3013
+ encoding: "utf-8",
3014
+ timeout: 3e3
3015
+ }).trim();
2961
3016
  const kb = parseInt(out, 10);
2962
3017
  return isNaN(kb) ? null : kb;
2963
3018
  } catch {
@@ -3052,7 +3107,10 @@ function findPidOnPortLinux(port) {
3052
3107
  }
3053
3108
  function findPidOnPortMac(port) {
3054
3109
  try {
3055
- const out = execSync(`lsof -i :${port} -sTCP:LISTEN -P -n 2>/dev/null`, { encoding: "utf-8", timeout: 5e3 });
3110
+ const out = execSync(`lsof -i :${port} -sTCP:LISTEN -P -n 2>/dev/null`, {
3111
+ encoding: "utf-8",
3112
+ timeout: 5e3
3113
+ });
3056
3114
  for (const line of out.split("\n")) {
3057
3115
  if (!line.includes("LISTEN")) continue;
3058
3116
  const parts = line.trim().split(/\s+/);
@@ -3107,7 +3165,10 @@ function getProcessCmdlineMac(pid) {
3107
3165
  }
3108
3166
  function getProcessCmdlineWindows(pid) {
3109
3167
  try {
3110
- const out = execSync(`wmic process where ProcessId=${pid} get CommandLine /value 2>nul`, { encoding: "utf-8", timeout: 3e3 });
3168
+ const out = execSync(`wmic process where ProcessId=${pid} get CommandLine /value 2>nul`, {
3169
+ encoding: "utf-8",
3170
+ timeout: 3e3
3171
+ });
3111
3172
  const line = out.split("\n").find((l) => l.includes("="));
3112
3173
  const value = line?.split("=")[1]?.trim();
3113
3174
  return value ? value : null;
@@ -3125,7 +3186,10 @@ function getProcessCwd(pid) {
3125
3186
  }
3126
3187
  if (process.platform === "darwin") {
3127
3188
  try {
3128
- const out = execSync(`lsof -p ${pid} -a -d cwd -F n 2>/dev/null`, { encoding: "utf-8", timeout: 3e3 });
3189
+ const out = execSync(`lsof -p ${pid} -a -d cwd -F n 2>/dev/null`, {
3190
+ encoding: "utf-8",
3191
+ timeout: 3e3
3192
+ });
3129
3193
  const line = out.split("\n").find((l) => l.startsWith("n"));
3130
3194
  return line ? line.slice(1) : null;
3131
3195
  } catch {
@@ -3239,7 +3303,10 @@ function saveTracked(processes) {
3239
3303
  mkdirSync(dirname(path), { recursive: true });
3240
3304
  writeFileSync(path, JSON.stringify(processes, null, 2), "utf-8");
3241
3305
  } catch (err) {
3242
- console.error("[fennec] Failed to save tracked processes:", err instanceof Error ? err.message : String(err));
3306
+ console.error(
3307
+ "[fennec] Failed to save tracked processes:",
3308
+ err instanceof Error ? err.message : String(err)
3309
+ );
3243
3310
  }
3244
3311
  }
3245
3312
  function addTracked(proc) {
@@ -3355,7 +3422,10 @@ function rotateLogFile(filePath, maxSize = DEFAULT_MAX_LOG_SIZE, maxFiles = DEFA
3355
3422
  }
3356
3423
  return true;
3357
3424
  } catch (err) {
3358
- console.error("[fennec] Log rotation failed:", err instanceof Error ? err.message : String(err));
3425
+ console.error(
3426
+ "[fennec] Log rotation failed:",
3427
+ err instanceof Error ? err.message : String(err)
3428
+ );
3359
3429
  return false;
3360
3430
  }
3361
3431
  }
@@ -3420,15 +3490,11 @@ function spawnDaemon(opts) {
3420
3490
  return child;
3421
3491
  }
3422
3492
  function spawnDaemonJsonl(opts, env) {
3423
- const relay = spawn(
3424
- process.execPath,
3425
- ["-e", JSONL_RELAY],
3426
- {
3427
- env: { ...process.env, FENNEC_RELAY_LOG: opts.logFilePath },
3428
- stdio: ["pipe", "ignore", "ignore"],
3429
- detached: true
3430
- }
3431
- );
3493
+ const relay = spawn(process.execPath, ["-e", JSONL_RELAY], {
3494
+ env: { ...process.env, FENNEC_RELAY_LOG: opts.logFilePath },
3495
+ stdio: ["pipe", "ignore", "ignore"],
3496
+ detached: true
3497
+ });
3432
3498
  relay.unref();
3433
3499
  const child = spawn(opts.cmdParts[0], opts.cmdParts.slice(1), {
3434
3500
  cwd: opts.cwd,
@@ -3484,7 +3550,16 @@ ${RESTART_CAUSE} cause=${cause} at=${(/* @__PURE__ */ new Date()).toISOString()}
3484
3550
 
3485
3551
  // src/commands/supervisor.ts
3486
3552
  import pc4 from "picocolors";
3487
- import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync2, unlinkSync, mkdirSync as mkdirSync2, openSync as openSync2, closeSync as closeSync2 } from "fs";
3553
+ import {
3554
+ existsSync as existsSync4,
3555
+ readFileSync as readFileSync3,
3556
+ writeFileSync as writeFileSync2,
3557
+ unlinkSync,
3558
+ mkdirSync as mkdirSync2,
3559
+ openSync as openSync2,
3560
+ closeSync as closeSync2,
3561
+ statSync as statSync2
3562
+ } from "fs";
3488
3563
  import { spawn as spawn2 } from "child_process";
3489
3564
  import { dirname as dirname2 } from "path";
3490
3565
  var POLL_INTERVAL_MS = 3e3;
@@ -3506,6 +3581,38 @@ function ensureSupervisorRunning() {
3506
3581
  return startSupervisorDaemon();
3507
3582
  }
3508
3583
  function startSupervisorDaemon() {
3584
+ const already = getSupervisorPid();
3585
+ if (already) return already;
3586
+ const lockPath = `${getSupervisorPidPath()}.lock`;
3587
+ const SUPERVISOR_LOCK_STALE_MS = 6e4;
3588
+ const acquireLock = () => {
3589
+ try {
3590
+ const lfd = openSync2(lockPath, "wx");
3591
+ closeSync2(lfd);
3592
+ return true;
3593
+ } catch {
3594
+ return false;
3595
+ }
3596
+ };
3597
+ if (!acquireLock()) {
3598
+ const live = getSupervisorPid();
3599
+ if (live) return live;
3600
+ let stale = false;
3601
+ try {
3602
+ stale = Date.now() - statSync2(lockPath).mtimeMs > SUPERVISOR_LOCK_STALE_MS;
3603
+ } catch {
3604
+ stale = false;
3605
+ }
3606
+ if (stale) {
3607
+ try {
3608
+ unlinkSync(lockPath);
3609
+ } catch {
3610
+ }
3611
+ if (!acquireLock()) return getSupervisorPid() ?? 0;
3612
+ } else {
3613
+ return 0;
3614
+ }
3615
+ }
3509
3616
  const logFilePath = logFilePathFor("supervisor");
3510
3617
  mkdirSync2(dirname2(logFilePath), { recursive: true });
3511
3618
  const fd = openSync2(logFilePath, "a");
@@ -3536,24 +3643,30 @@ async function supervisorCommand(args2) {
3536
3643
  case "status":
3537
3644
  return supervisorStatus();
3538
3645
  default:
3539
- console.error(renderError("Unknown sub-command", `"${sub}" \u2014 use: start | stop | status | restart`));
3646
+ console.error(
3647
+ renderError("Unknown sub-command", `"${sub}" \u2014 use: start | stop | status | restart`)
3648
+ );
3540
3649
  process.exit(1);
3541
3650
  }
3542
3651
  }
3543
3652
  function supervisorStart() {
3544
3653
  const existing = getSupervisorPid();
3545
3654
  if (existing) {
3546
- console.error(`
3655
+ console.error(
3656
+ `
3547
3657
  ${pc4.yellow("\u26A0")} Supervisor already running ${pc4.dim(`(PID ${existing})`)}
3548
- `);
3658
+ `
3659
+ );
3549
3660
  return;
3550
3661
  }
3551
3662
  const pid = startSupervisorDaemon();
3552
3663
  console.error(`
3553
3664
  ${pc4.green("\u2713")} ${pc4.bold("Supervisor started")} ${pc4.dim(`(PID ${pid})`)}`);
3554
3665
  console.error(` ${renderKV("Logs", pc4.cyan("fennec log supervisor"))}`);
3555
- console.error(` ${pc4.dim("It will auto-restart apps started with")} ${pc4.cyan("--restart")} ${pc4.dim("even if you close this terminal.")}
3556
- `);
3666
+ console.error(
3667
+ ` ${pc4.dim("It will auto-restart apps started with")} ${pc4.cyan("--restart")} ${pc4.dim("even if you close this terminal.")}
3668
+ `
3669
+ );
3557
3670
  }
3558
3671
  function supervisorStop() {
3559
3672
  const pid = getSupervisorPid();
@@ -3565,9 +3678,11 @@ function supervisorStop() {
3565
3678
  }
3566
3679
  try {
3567
3680
  killTree(pid, "SIGTERM");
3568
- console.error(`
3681
+ console.error(
3682
+ `
3569
3683
  ${pc4.green("\u2713")} ${pc4.bold("Supervisor stopped")} ${pc4.dim(`(PID ${pid})`)}
3570
- `);
3684
+ `
3685
+ );
3571
3686
  } catch (err) {
3572
3687
  console.error(renderError("Failed to stop supervisor", String(err)));
3573
3688
  }
@@ -3585,12 +3700,18 @@ function supervisorStatus() {
3585
3700
  if (pid) {
3586
3701
  console.error(` ${pc4.green("\u25CF")} running ${pc4.dim(`(PID ${pid})`)}`);
3587
3702
  } else {
3588
- console.error(` ${pc4.red("\u25CB")} not running ${pc4.dim("\u2014 start with")} ${pc4.cyan("fennec supervisor start")}`);
3703
+ console.error(
3704
+ ` ${pc4.red("\u25CB")} not running ${pc4.dim("\u2014 start with")} ${pc4.cyan("fennec supervisor start")}`
3705
+ );
3589
3706
  }
3590
- console.error(`
3591
- ${pc4.bold("Managed apps")} ${pc4.dim(`(${managed.length} with auto-restart)`)}`);
3707
+ console.error(
3708
+ `
3709
+ ${pc4.bold("Managed apps")} ${pc4.dim(`(${managed.length} with auto-restart)`)}`
3710
+ );
3592
3711
  if (managed.length === 0) {
3593
- console.error(` ${pc4.dim("None. Start one with")} ${pc4.cyan("fennec start <cmd> --name <name> --restart")}`);
3712
+ console.error(
3713
+ ` ${pc4.dim("None. Start one with")} ${pc4.cyan("fennec start <cmd> --name <name> --restart")}`
3714
+ );
3594
3715
  } else {
3595
3716
  for (const t of managed) {
3596
3717
  const running = isTrackedRunning(t);
@@ -3611,6 +3732,10 @@ async function runSupervisor() {
3611
3732
  return;
3612
3733
  }
3613
3734
  writeFileSync2(pidPath, String(process.pid), "utf-8");
3735
+ try {
3736
+ unlinkSync(`${pidPath}.lock`);
3737
+ } catch {
3738
+ }
3614
3739
  log(`supervisor started (PID ${process.pid}), polling every ${POLL_INTERVAL_MS}ms`);
3615
3740
  const crashes = /* @__PURE__ */ new Map();
3616
3741
  let stopped = false;
@@ -3620,6 +3745,10 @@ async function runSupervisor() {
3620
3745
  unlinkSync(pidPath);
3621
3746
  } catch {
3622
3747
  }
3748
+ try {
3749
+ unlinkSync(`${pidPath}.lock`);
3750
+ } catch {
3751
+ }
3623
3752
  log("supervisor stopping");
3624
3753
  process.exit(0);
3625
3754
  };
@@ -3662,7 +3791,9 @@ async function runSupervisor() {
3662
3791
  rec2.portFails = 0;
3663
3792
  rec2.count++;
3664
3793
  if (rec2.count >= FLAPPING_THRESHOLD) setFlapping(t, true);
3665
- log(`${t.name}: alive but ${probe} not responding ${PORT_FAIL_THRESHOLD}x \u2014 restarting`);
3794
+ log(
3795
+ `${t.name}: alive but ${probe} not responding ${PORT_FAIL_THRESHOLD}x \u2014 restarting`
3796
+ );
3666
3797
  try {
3667
3798
  const newPid = respawnTracked(t, "port-down");
3668
3799
  log(`${t.name}: restarted (PID ${newPid}) after ${probe} failure`);
@@ -3688,14 +3819,18 @@ async function runSupervisor() {
3688
3819
  if (rec.gaveUp) continue;
3689
3820
  if (rec.count >= MAX_RESTARTS_IN_WINDOW) {
3690
3821
  rec.gaveUp = true;
3691
- log(`${t.name}: crashed ${rec.count}x in <60s \u2014 giving up (will retry after window resets)`);
3822
+ log(
3823
+ `${t.name}: crashed ${rec.count}x in <60s \u2014 giving up (will retry after window resets)`
3824
+ );
3692
3825
  continue;
3693
3826
  }
3694
3827
  rec.count++;
3695
3828
  if (rec.count >= FLAPPING_THRESHOLD) setFlapping(t, true);
3696
3829
  try {
3697
3830
  const newPid = respawnTracked(t, "crash");
3698
- log(`${t.name}: restarted (PID ${newPid}) [${rec.count}/${MAX_RESTARTS_IN_WINDOW} in window]`);
3831
+ log(
3832
+ `${t.name}: restarted (PID ${newPid}) [${rec.count}/${MAX_RESTARTS_IN_WINDOW} in window]`
3833
+ );
3699
3834
  } catch (err) {
3700
3835
  log(`${t.name}: restart failed: ${String(err)}`);
3701
3836
  }
@@ -3751,11 +3886,25 @@ function unitPaths(backend) {
3751
3886
  const fennecDir = getFennecDir();
3752
3887
  switch (backend) {
3753
3888
  case "systemd":
3754
- return { unit: resolve3(homedir2(), ".config", "systemd", "user", "fennec-supervisor.service") };
3889
+ return {
3890
+ unit: resolve3(homedir2(), ".config", "systemd", "user", "fennec-supervisor.service")
3891
+ };
3755
3892
  case "launchd":
3756
3893
  return { unit: resolve3(homedir2(), "Library", "LaunchAgents", "io.fennec.supervisor.plist") };
3757
3894
  case "windows":
3758
- return { unit: resolve3(homedir2(), "AppData", "Roaming", "Microsoft", "Windows", "Start Menu", "Programs", "Startup", "fennec-supervisor.bat") };
3895
+ return {
3896
+ unit: resolve3(
3897
+ homedir2(),
3898
+ "AppData",
3899
+ "Roaming",
3900
+ "Microsoft",
3901
+ "Windows",
3902
+ "Start Menu",
3903
+ "Programs",
3904
+ "Startup",
3905
+ "fennec-supervisor.bat"
3906
+ )
3907
+ };
3759
3908
  default:
3760
3909
  return { unit: resolve3(fennecDir, "persist.unit") };
3761
3910
  }
@@ -3836,7 +3985,12 @@ async function persistCommand(args2) {
3836
3985
  const sub = args2[0] ?? "status";
3837
3986
  const backend = detectBackend();
3838
3987
  if (backend === "unsupported") {
3839
- console.error(renderError("Unsupported platform", `Boot persistence isn't available on ${platform()}. You can still run "fennec supervisor start" manually.`));
3988
+ console.error(
3989
+ renderError(
3990
+ "Unsupported platform",
3991
+ `Boot persistence isn't available on ${platform()}. You can still run "fennec supervisor start" manually.`
3992
+ )
3993
+ );
3840
3994
  process.exit(1);
3841
3995
  }
3842
3996
  switch (sub) {
@@ -3847,7 +4001,9 @@ async function persistCommand(args2) {
3847
4001
  case "status":
3848
4002
  return persistStatus(backend);
3849
4003
  default:
3850
- console.error(renderError("Unknown sub-command", `"${sub}" \u2014 use: enable | disable | status`));
4004
+ console.error(
4005
+ renderError("Unknown sub-command", `"${sub}" \u2014 use: enable | disable | status`)
4006
+ );
3851
4007
  process.exit(1);
3852
4008
  }
3853
4009
  }
@@ -3857,8 +4013,10 @@ function persistEnable(backend) {
3857
4013
  mkdirSync3(dirname3(unit), { recursive: true });
3858
4014
  writeFileSync3(unit, buildUnit(backend, dataDir), "utf-8");
3859
4015
  if (backend === "windows") {
3860
- console.error(`
3861
- ${pc5.green("\u2713")} ${pc5.bold("Boot script installed")} ${pc5.dim(`(runs at next login)`)}`);
4016
+ console.error(
4017
+ `
4018
+ ${pc5.green("\u2713")} ${pc5.bold("Boot script installed")} ${pc5.dim(`(runs at next login)`)}`
4019
+ );
3862
4020
  console.error(` ${renderKV("Path", pc5.cyan(unit))}`);
3863
4021
  console.error(` ${pc5.dim("Supervisor will start automatically when you log in.")}
3864
4022
  `);
@@ -3872,20 +4030,28 @@ function persistEnable(backend) {
3872
4030
  }
3873
4031
  const linger = run("loginctl", ["enable-linger"]);
3874
4032
  if (!linger.ok) {
3875
- console.error(` ${pc5.yellow("\u26A0")} ${pc5.dim("Could not enable user linger \u2014 the supervisor may stop when you log out.")}`);
4033
+ console.error(
4034
+ ` ${pc5.yellow("\u26A0")} ${pc5.dim("Could not enable user linger \u2014 the supervisor may stop when you log out.")}`
4035
+ );
3876
4036
  }
3877
- console.error(`
3878
- ${pc5.green("\u2713")} ${pc5.bold("Boot persistence enabled")} ${pc5.dim("(systemd user service)")}`);
4037
+ console.error(
4038
+ `
4039
+ ${pc5.green("\u2713")} ${pc5.bold("Boot persistence enabled")} ${pc5.dim("(systemd user service)")}`
4040
+ );
3879
4041
  console.error(` ${renderKV("Unit", pc5.cyan("fennec-supervisor.service"))}`);
3880
4042
  console.error(` ${renderKV("Data dir", pc5.cyan(dataDir))}`);
3881
- console.error(` ${pc5.dim("The supervisor (and all --restart apps) will start at login and keep running after logout.")}
3882
- `);
4043
+ console.error(
4044
+ ` ${pc5.dim("The supervisor (and all --restart apps) will start at login and keep running after logout.")}
4045
+ `
4046
+ );
3883
4047
  return;
3884
4048
  }
3885
4049
  if (backend === "launchd") {
3886
4050
  run("launchctl", ["load", resolve3(unit)]);
3887
- console.error(`
3888
- ${pc5.green("\u2713")} ${pc5.bold("Boot persistence enabled")} ${pc5.dim("(launchd agent)")}`);
4051
+ console.error(
4052
+ `
4053
+ ${pc5.green("\u2713")} ${pc5.bold("Boot persistence enabled")} ${pc5.dim("(launchd agent)")}`
4054
+ );
3889
4055
  console.error(` ${renderKV("Label", pc5.cyan("io.fennec.supervisor"))}`);
3890
4056
  console.error(` ${renderKV("Data dir", pc5.cyan(dataDir))}`);
3891
4057
  console.error(` ${pc5.dim("The supervisor will start at login.")}
@@ -3915,7 +4081,9 @@ function persistStatus(backend) {
3915
4081
  console.error(`
3916
4082
  ${pc5.bold("Fennec Boot Persistence")} ${pc5.dim(`(${backend})`)}`);
3917
4083
  if (!installed) {
3918
- console.error(` ${pc5.red("\u25CB")} ${pc5.dim("not installed \u2014 run ")}${pc5.cyan("fennec persist enable")}${pc5.dim(" to auto-start apps after reboot")}`);
4084
+ console.error(
4085
+ ` ${pc5.red("\u25CB")} ${pc5.dim("not installed \u2014 run ")}${pc5.cyan("fennec persist enable")}${pc5.dim(" to auto-start apps after reboot")}`
4086
+ );
3919
4087
  console.error();
3920
4088
  return;
3921
4089
  }
@@ -3969,26 +4137,48 @@ async function psCommand(args2) {
3969
4137
  const columns2 = [
3970
4138
  { key: "pid", label: "PID", align: "right", format: (v) => pc6.dim(String(v).padStart(6)) },
3971
4139
  { key: "name", label: "Name", format: (v) => pc6.bold(String(v)) },
3972
- { key: "cpu", label: "CPU%", align: "right", format: (v) => {
3973
- const n = v;
3974
- return n > 10 ? pc6.red(String(n)) : n > 5 ? pc6.yellow(String(n)) : pc6.dim(String(n));
3975
- } },
3976
- { key: "mem", label: "MEM%", align: "right", format: (v) => {
3977
- const n = v;
3978
- return n > 10 ? pc6.red(String(n)) : n > 5 ? pc6.yellow(String(n)) : pc6.dim(String(n));
3979
- } },
3980
- { key: "state", label: "State", format: (v) => {
3981
- const s = String(v);
3982
- if (s === "R" || s === "Running") return pc6.green(s);
3983
- if (s === "Z" || s === "Zombie") return pc6.red(s);
3984
- if (s === "S" || s === "Sleeping") return pc6.cyan(s);
3985
- return pc6.dim(s);
3986
- } }
4140
+ {
4141
+ key: "cpu",
4142
+ label: "CPU%",
4143
+ align: "right",
4144
+ format: (v) => {
4145
+ const n = v;
4146
+ return n > 10 ? pc6.red(String(n)) : n > 5 ? pc6.yellow(String(n)) : pc6.dim(String(n));
4147
+ }
4148
+ },
4149
+ {
4150
+ key: "mem",
4151
+ label: "MEM%",
4152
+ align: "right",
4153
+ format: (v) => {
4154
+ const n = v;
4155
+ return n > 10 ? pc6.red(String(n)) : n > 5 ? pc6.yellow(String(n)) : pc6.dim(String(n));
4156
+ }
4157
+ },
4158
+ {
4159
+ key: "state",
4160
+ label: "State",
4161
+ format: (v) => {
4162
+ const s = String(v);
4163
+ if (s === "R" || s === "Running") return pc6.green(s);
4164
+ if (s === "Z" || s === "Zombie") return pc6.red(s);
4165
+ if (s === "S" || s === "Sleeping") return pc6.cyan(s);
4166
+ return pc6.dim(s);
4167
+ }
4168
+ }
3987
4169
  ];
3988
- const rows2 = processes.map((p) => ({ pid: p.pid, name: p.name, cpu: p.cpuPercent, mem: p.memPercent, state: formatProcessState(p.state) }));
3989
- console.error(`
4170
+ const rows2 = processes.map((p) => ({
4171
+ pid: p.pid,
4172
+ name: p.name,
4173
+ cpu: p.cpuPercent,
4174
+ mem: p.memPercent,
4175
+ state: formatProcessState(p.state)
4176
+ }));
4177
+ console.error(
4178
+ `
3990
4179
  ${symbols.fox} ${pc6.bold("System Processes")} ${pc6.dim(`(top ${processes.length} by ${sortBy})`)}
3991
- `);
4180
+ `
4181
+ );
3992
4182
  console.error(renderTable(columns2, rows2));
3993
4183
  console.error();
3994
4184
  } catch (error) {
@@ -4000,54 +4190,98 @@ async function psCommand(args2) {
4000
4190
  const trackedAll = readTracked();
4001
4191
  const tracked = groupFilter ? trackedAll.filter((t) => t.group === groupFilter) : trackedAll;
4002
4192
  if (tracked.length === 0) {
4003
- console.error(`
4004
- ${pc6.dim(groupFilter ? `No tracked processes in group "${groupFilter}".` : "No tracked processes.")}`);
4005
- console.error(` ${pc6.dim("Start an app with:")} ${pc6.cyan("fennec start <command> --name <name>")}
4006
- `);
4193
+ console.error(
4194
+ `
4195
+ ${pc6.dim(groupFilter ? `No tracked processes in group "${groupFilter}".` : "No tracked processes.")}`
4196
+ );
4197
+ console.error(
4198
+ ` ${pc6.dim("Start an app with:")} ${pc6.cyan("fennec start <command> --name <name>")}
4199
+ `
4200
+ );
4007
4201
  return;
4008
4202
  }
4009
4203
  const columns = [
4010
4204
  { key: "name", label: "App", format: (v) => pc6.bold(String(v)) },
4011
4205
  { key: "pid", label: "PID", align: "right" },
4012
- { key: "status", label: "Status", format: (v) => {
4013
- const s = v;
4014
- return s === "running" ? pc6.green("\u25CF running") : pc6.red("\u25CB stopped");
4015
- } },
4016
- { key: "group", label: "Group", format: (v) => {
4017
- const g = String(v);
4018
- return g === "-" ? pc6.dim("-") : pc6.cyan(g);
4019
- } },
4020
- { key: "port", label: "Port", format: (v) => {
4021
- const p = v;
4022
- return p ? pc6.yellow(`:${p}`) : pc6.dim("-");
4023
- } },
4024
- { key: "mem", label: "MEM", align: "right", format: (v) => {
4025
- const kb = v;
4026
- return kb && kb > 0 ? pc6.dim(`${(kb / 1024).toFixed(0)}MB`) : pc6.dim("-");
4027
- } },
4028
- { key: "command", label: "Command", format: (v) => {
4029
- const c = String(v);
4030
- return c.length > 50 ? c.slice(0, 50) + "\u2026" : c;
4031
- } },
4206
+ {
4207
+ key: "status",
4208
+ label: "Status",
4209
+ format: (v) => {
4210
+ const s = v;
4211
+ return s === "running" ? pc6.green("\u25CF running") : pc6.red("\u25CB stopped");
4212
+ }
4213
+ },
4214
+ {
4215
+ key: "group",
4216
+ label: "Group",
4217
+ format: (v) => {
4218
+ const g = String(v);
4219
+ return g === "-" ? pc6.dim("-") : pc6.cyan(g);
4220
+ }
4221
+ },
4222
+ {
4223
+ key: "port",
4224
+ label: "Port",
4225
+ format: (v) => {
4226
+ const p = v;
4227
+ return p ? pc6.yellow(`:${p}`) : pc6.dim("-");
4228
+ }
4229
+ },
4230
+ {
4231
+ key: "mem",
4232
+ label: "MEM",
4233
+ align: "right",
4234
+ format: (v) => {
4235
+ const kb = v;
4236
+ return kb && kb > 0 ? pc6.dim(`${(kb / 1024).toFixed(0)}MB`) : pc6.dim("-");
4237
+ }
4238
+ },
4239
+ {
4240
+ key: "command",
4241
+ label: "Command",
4242
+ format: (v) => {
4243
+ const c = String(v);
4244
+ return c.length > 50 ? c.slice(0, 50) + "\u2026" : c;
4245
+ }
4246
+ },
4032
4247
  { key: "uptime", label: "Uptime", format: (v) => pc6.dim(String(v)) }
4033
4248
  ];
4034
4249
  const rows = tracked.map((t) => {
4035
4250
  const running = isTrackedRunning(t);
4036
4251
  const uptime = running ? formatUptime(Math.floor((Date.now() - new Date(t.startedAt).getTime()) / 1e3)) : "-";
4037
4252
  const memKb = running ? getProcessMemRss(t.pid) ?? null : null;
4038
- return { name: t.name, pid: running ? String(t.pid) : pc6.dim(String(t.pid)), status: running ? "running" : "stopped", group: t.group ?? "-", port: t.port ?? null, mem: memKb, command: t.command, uptime };
4253
+ return {
4254
+ name: t.name,
4255
+ pid: running ? String(t.pid) : pc6.dim(String(t.pid)),
4256
+ status: running ? "running" : "stopped",
4257
+ group: t.group ?? "-",
4258
+ port: t.port ?? null,
4259
+ mem: memKb,
4260
+ command: t.command,
4261
+ uptime
4262
+ };
4039
4263
  });
4040
4264
  const runningCount = tracked.filter((t) => isTrackedRunning(t)).length;
4041
4265
  const scope = groupFilter ? ` in group ${pc6.cyan(groupFilter)}` : "";
4042
- console.error(`
4266
+ console.error(
4267
+ `
4043
4268
  ${symbols.fox} ${pc6.bold("Fennec Apps")} ${pc6.dim(`(${runningCount}/${tracked.length} running${scope})`)}
4044
- `);
4269
+ `
4270
+ );
4045
4271
  console.error(renderTable(columns, rows));
4046
- console.error(` ${pc6.dim("Use")} ${pc6.cyan("fennec start <command> --name <name> --port <port>")} ${pc6.dim("to add more apps.")}`);
4272
+ console.error(
4273
+ ` ${pc6.dim("Use")} ${pc6.cyan("fennec start <command> --name <name> --port <port>")} ${pc6.dim("to add more apps.")}`
4274
+ );
4047
4275
  console.error(` ${pc6.dim("Use")} ${pc6.cyan("fennec log <name>")} ${pc6.dim("to view logs.")}`);
4048
- console.error(` ${pc6.dim("Use")} ${pc6.cyan("fennec stop <name>")} ${pc6.dim("to pause an app.")}`);
4049
- console.error(` ${pc6.dim("Use")} ${pc6.cyan("fennec spawn <name>")} ${pc6.dim("to resume a paused app.")}`);
4050
- console.error(` ${pc6.dim("Use")} ${pc6.cyan("fennec kill <name>")} ${pc6.dim("to permanently remove an app.")}`);
4276
+ console.error(
4277
+ ` ${pc6.dim("Use")} ${pc6.cyan("fennec stop <name>")} ${pc6.dim("to pause an app.")}`
4278
+ );
4279
+ console.error(
4280
+ ` ${pc6.dim("Use")} ${pc6.cyan("fennec spawn <name>")} ${pc6.dim("to resume a paused app.")}`
4281
+ );
4282
+ console.error(
4283
+ ` ${pc6.dim("Use")} ${pc6.cyan("fennec kill <name>")} ${pc6.dim("to permanently remove an app.")}`
4284
+ );
4051
4285
  console.error(` ${pc6.dim("Filter by group with:")} ${pc6.cyan("fennec ps --group <group>")}`);
4052
4286
  console.error();
4053
4287
  }
@@ -4083,19 +4317,25 @@ async function statusCommand(_args) {
4083
4317
  `);
4084
4318
  if (tracked.length > 0) {
4085
4319
  const runningCount = tracked.filter((t) => isTrackedRunning(t)).length;
4086
- console.error(` ${pc6.bold("Managed Apps")} ${pc6.dim(`(${runningCount}/${tracked.length} running)`)}
4087
- `);
4320
+ console.error(
4321
+ ` ${pc6.bold("Managed Apps")} ${pc6.dim(`(${runningCount}/${tracked.length} running)`)}
4322
+ `
4323
+ );
4088
4324
  for (const t of tracked) {
4089
4325
  const running = isTrackedRunning(t);
4090
4326
  const statusIcon = running ? pc6.green("\u25CF") : pc6.red("\u25CB");
4091
4327
  const portStr = t.port ? ` ${pc6.yellow(`:${t.port}`)}` : "";
4092
4328
  const uptime = running ? pc6.dim(formatUptime(Math.floor((Date.now() - new Date(t.startedAt).getTime()) / 1e3))) : pc6.red("stopped");
4093
- console.error(` ${statusIcon} ${pc6.bold(t.name)}${portStr} ${pc6.dim(`(PID ${t.pid})`)} \u2014 ${uptime}`);
4329
+ console.error(
4330
+ ` ${statusIcon} ${pc6.bold(t.name)}${portStr} ${pc6.dim(`(PID ${t.pid})`)} \u2014 ${uptime}`
4331
+ );
4094
4332
  }
4095
4333
  console.error();
4096
4334
  } else {
4097
- console.error(` ${pc6.dim("No managed apps.")} ${pc6.cyan("fennec start <command> --name <name>")}
4098
- `);
4335
+ console.error(
4336
+ ` ${pc6.dim("No managed apps.")} ${pc6.cyan("fennec start <command> --name <name>")}
4337
+ `
4338
+ );
4099
4339
  }
4100
4340
  console.error(` ${pc6.bold("System")} ${pc6.dim(`(${totalUserProcs} user processes)`)}`);
4101
4341
  for (const p of topSystem) {
@@ -4107,25 +4347,47 @@ async function statusCommand(_args) {
4107
4347
  console.error();
4108
4348
  }
4109
4349
  async function watchSystemProcesses(sortBy, limit) {
4110
- console.error(`
4350
+ console.error(
4351
+ `
4111
4352
  ${pc6.bold("Watching system processes")} ${pc6.dim("(Ctrl+C to stop, refreshes every 3s)")}
4112
- `);
4353
+ `
4354
+ );
4113
4355
  const render = () => {
4114
- const processes = getSystemProcesses({ userOnly: true, sortBy, limit });
4356
+ const processes = getSystemProcesses({
4357
+ userOnly: true,
4358
+ sortBy,
4359
+ limit
4360
+ });
4115
4361
  const columns = [
4116
4362
  { key: "pid", label: "PID", align: "right", format: (v) => pc6.dim(String(v).padStart(6)) },
4117
4363
  { key: "name", label: "Name", format: (v) => pc6.bold(String(v)) },
4118
- { key: "cpu", label: "CPU%", align: "right", format: (v) => {
4119
- const n = v;
4120
- return n > 10 ? pc6.red(String(n)) : n > 5 ? pc6.yellow(String(n)) : pc6.dim(String(n));
4121
- } },
4122
- { key: "mem", label: "MEM%", align: "right", format: (v) => {
4123
- const n = v;
4124
- return n > 10 ? pc6.red(String(n)) : n > 5 ? pc6.yellow(String(n)) : pc6.dim(String(n));
4125
- } },
4364
+ {
4365
+ key: "cpu",
4366
+ label: "CPU%",
4367
+ align: "right",
4368
+ format: (v) => {
4369
+ const n = v;
4370
+ return n > 10 ? pc6.red(String(n)) : n > 5 ? pc6.yellow(String(n)) : pc6.dim(String(n));
4371
+ }
4372
+ },
4373
+ {
4374
+ key: "mem",
4375
+ label: "MEM%",
4376
+ align: "right",
4377
+ format: (v) => {
4378
+ const n = v;
4379
+ return n > 10 ? pc6.red(String(n)) : n > 5 ? pc6.yellow(String(n)) : pc6.dim(String(n));
4380
+ }
4381
+ },
4126
4382
  { key: "state", label: "S", align: "center" }
4127
4383
  ];
4128
- const rows = processes.map((p) => ({ pid: p.pid, name: p.name, cpu: p.cpuPercent, mem: p.memPercent, state: p.state }));
4384
+ const rows = processes.map((p) => ({
4385
+ pid: p.pid,
4386
+ name: p.name,
4387
+ cpu: p.cpuPercent,
4388
+ mem: p.memPercent,
4389
+ state: p.state
4390
+ }));
4129
4391
  return ` ${timestamp()} ${pc6.dim(`${processes.length} processes`)}
4130
4392
  ${renderTable(columns, rows, { compact: true })}`;
4131
4393
  };
@@ -4180,8 +4442,12 @@ async function startServer(args2) {
4180
4442
  ${pc7.green("\u2713")} ${pc7.bold("Fennec server is running")}`);
4181
4443
  console.error(` ${renderKV("Transport", pc7.cyan("SSE (HTTP)"))}`);
4182
4444
  console.error(` ${renderKV("URL", pc7.cyan(`http://localhost:${port}/sse`))}`);
4183
- console.error(` ${renderKV("MCP Config", pc7.cyan(`{ "type": "remote", "url": "http://localhost:${port}/sse" }`))}`);
4184
- console.error(` ${renderKV("OpenCode", pc7.cyan(`type: remote, url: http://localhost:${port}/sse`))}`);
4445
+ console.error(
4446
+ ` ${renderKV("MCP Config", pc7.cyan(`{ "type": "remote", "url": "http://localhost:${port}/sse" }`))}`
4447
+ );
4448
+ console.error(
4449
+ ` ${renderKV("OpenCode", pc7.cyan(`type: remote, url: http://localhost:${port}/sse`))}`
4450
+ );
4185
4451
  console.error(`
4186
4452
  ${pc7.dim("Press Ctrl+C to stop")}
4187
4453
  `);
@@ -4221,15 +4487,22 @@ async function runCommand(args2) {
4221
4487
  const cmdParts = args2.slice(0, cmdEnd);
4222
4488
  const cmd = cmdParts.join(" ");
4223
4489
  if (!cmd) {
4224
- console.error(renderError("Missing command", "Usage: fennec start|run <command> --name <name> [--port <port>] [--cwd <dir>] [--restart]"));
4490
+ console.error(
4491
+ renderError(
4492
+ "Missing command",
4493
+ "Usage: fennec start|run <command> --name <name> [--port <port>] [--cwd <dir>] [--restart]"
4494
+ )
4495
+ );
4225
4496
  process.exit(1);
4226
4497
  }
4227
4498
  const appName = name ?? cmdParts[0] ?? "app";
4228
4499
  if (port !== void 0) {
4229
4500
  const adopted = adoptExternalOnPort(port, appName);
4230
4501
  if (adopted) {
4231
- console.error(`
4232
- ${pc7.green("\u2713")} ${pc7.bold("Adopted")} ${pc7.bold(adopted.name)} ${pc7.dim(`(PID ${adopted.pid}) \u2014 already listening on :${port}`)}`);
4502
+ console.error(
4503
+ `
4504
+ ${pc7.green("\u2713")} ${pc7.bold("Adopted")} ${pc7.bold(adopted.name)} ${pc7.dim(`(PID ${adopted.pid}) \u2014 already listening on :${port}`)}`
4505
+ );
4233
4506
  console.error(` ${renderKV("Logs", pc7.cyan(`fennec log ${adopted.name}`))}`);
4234
4507
  console.error();
4235
4508
  return;
@@ -4239,7 +4512,9 @@ async function runCommand(args2) {
4239
4512
  const existing = tracked.find((t) => t.name === appName);
4240
4513
  if (existing && isTrackedRunning(existing)) {
4241
4514
  console.error();
4242
- console.error(` ${pc7.yellow("\u26A0")} ${pc7.bold(appName)} ${pc7.dim(`is already running (PID: ${existing.pid})`)}`);
4515
+ console.error(
4516
+ ` ${pc7.yellow("\u26A0")} ${pc7.bold(appName)} ${pc7.dim(`is already running (PID: ${existing.pid})`)}`
4517
+ );
4243
4518
  console.error(` ${renderKV("Logs", pc7.cyan(`fennec log ${appName}`))}`);
4244
4519
  console.error(` ${renderKV("Stop", pc7.cyan(`fennec kill ${appName}`))}`);
4245
4520
  console.error();
@@ -4248,9 +4523,11 @@ async function runCommand(args2) {
4248
4523
  const logDir = dirname4(logFilePathFor(appName));
4249
4524
  mkdirSync4(logDir, { recursive: true });
4250
4525
  const logFilePath = logFilePathFor(appName);
4251
- console.error(`
4526
+ console.error(
4527
+ `
4252
4528
  ${symbols.fox} ${pc7.bold("Starting")} ${renderAppName(appName)} ${pc7.dim("(daemon)")}
4253
- `);
4529
+ `
4530
+ );
4254
4531
  console.error(` ${renderKV("Command", cmd)}`);
4255
4532
  if (port) console.error(` ${renderKV("Port", String(port))}`);
4256
4533
  if (cwd) console.error(` ${renderKV("Directory", cwd)}`);
@@ -4262,7 +4539,14 @@ async function runCommand(args2) {
4262
4539
  for (const [k, v] of Object.entries(process.env)) {
4263
4540
  if (v !== void 0) startEnv[k] = v;
4264
4541
  }
4265
- const currentChild = spawnDaemon({ cmdParts, name: appName, cwd, logFilePath, env: buildSpawnEnv(startEnv), logMode: jsonlFlag ? "jsonl" : "text" });
4542
+ const currentChild = spawnDaemon({
4543
+ cmdParts,
4544
+ name: appName,
4545
+ cwd,
4546
+ logFilePath,
4547
+ env: buildSpawnEnv(startEnv),
4548
+ logMode: jsonlFlag ? "jsonl" : "text"
4549
+ });
4266
4550
  const pid = currentChild.pid ?? 0;
4267
4551
  addTracked({
4268
4552
  name: appName,
@@ -4278,28 +4562,38 @@ async function runCommand(args2) {
4278
4562
  group
4279
4563
  });
4280
4564
  console.error(` ${pc7.green("\u2713")} ${pc7.bold(appName)} ${pc7.dim(`started (PID: ${pid})`)}`);
4281
- const spinner = createSpinner(port ? `Waiting for ${appName} on port ${port}...` : `Confirming ${appName} is running...`);
4565
+ const spinner = createSpinner(
4566
+ port ? `Waiting for ${appName} on port ${port}...` : `Confirming ${appName} is running...`
4567
+ );
4282
4568
  const ready = await waitForReady(pid, port);
4283
4569
  spinner.stop();
4284
4570
  process.stdout.write("\r\x1B[K");
4285
4571
  if (!ready.running) {
4286
4572
  removeTracked(appName);
4287
- console.error(` ${pc7.red("\u2717")} ${pc7.bold(appName)} ${pc7.dim("exited immediately after start")}`);
4573
+ console.error(
4574
+ ` ${pc7.red("\u2717")} ${pc7.bold(appName)} ${pc7.dim("exited immediately after start")}`
4575
+ );
4288
4576
  console.error(` ${renderKV("Logs", pc7.cyan(`fennec log ${appName}`))}`);
4289
4577
  console.error();
4290
4578
  process.exit(1);
4291
4579
  }
4292
4580
  if (port) {
4293
4581
  if (ready.portReady) {
4294
- console.error(` ${pc7.green("\u2713")} ${pc7.bold(appName)} ${pc7.dim(`is listening on port ${port}`)}`);
4582
+ console.error(
4583
+ ` ${pc7.green("\u2713")} ${pc7.bold(appName)} ${pc7.dim(`is listening on port ${port}`)}`
4584
+ );
4295
4585
  } else {
4296
- console.error(` ${pc7.yellow("\u26A0")} ${pc7.bold(appName)} ${pc7.dim(`is running but port ${port} is not accepting connections yet`)}`);
4586
+ console.error(
4587
+ ` ${pc7.yellow("\u26A0")} ${pc7.bold(appName)} ${pc7.dim(`is running but port ${port} is not accepting connections yet`)}`
4588
+ );
4297
4589
  }
4298
4590
  }
4299
4591
  if (restartFlag) {
4300
4592
  const supPid = ensureSupervisorRunning();
4301
4593
  ensurePersistEnabled();
4302
- console.error(` ${pc7.green("\u2713")} ${pc7.bold("Auto-restart")} ${pc7.dim(`managed by supervisor (PID ${supPid}) \u2014 survives terminal close`)}`);
4594
+ console.error(
4595
+ ` ${pc7.green("\u2713")} ${pc7.bold("Auto-restart")} ${pc7.dim(`managed by supervisor (PID ${supPid}) \u2014 survives terminal close`)}`
4596
+ );
4303
4597
  console.error(` ${renderKV("Supervisor", pc7.cyan("fennec supervisor status"))}`);
4304
4598
  }
4305
4599
  await psCommand([]);
@@ -4324,7 +4618,14 @@ async function resurrectTracked() {
4324
4618
  try {
4325
4619
  const cmdParts = resolveArgs(proc);
4326
4620
  const logFilePath = logFilePathFor(proc.name);
4327
- const child = spawnDaemon({ cmdParts, name: proc.name, cwd: proc.cwd, logFilePath, env: buildSpawnEnv(proc.env), logMode: proc.logMode });
4621
+ const child = spawnDaemon({
4622
+ cmdParts,
4623
+ name: proc.name,
4624
+ cwd: proc.cwd,
4625
+ logFilePath,
4626
+ env: buildSpawnEnv(proc.env),
4627
+ logMode: proc.logMode
4628
+ });
4328
4629
  addTracked({
4329
4630
  name: proc.name,
4330
4631
  pid: child.pid ?? 0,
@@ -4379,7 +4680,12 @@ async function killCommand(args2) {
4379
4680
  await killAll(signal, force);
4380
4681
  return;
4381
4682
  }
4382
- console.error(renderError("Missing target", "Usage: fennec kill <pid|name|all> [--signal SIGTERM|SIGKILL|SIGINT] [--group <group>] [-y]"));
4683
+ console.error(
4684
+ renderError(
4685
+ "Missing target",
4686
+ "Usage: fennec kill <pid|name|all> [--signal SIGTERM|SIGKILL|SIGINT] [--group <group>] [-y]"
4687
+ )
4688
+ );
4383
4689
  process.exit(1);
4384
4690
  }
4385
4691
  async function killOne(rawTarget, signal, force, multi) {
@@ -4404,8 +4710,10 @@ async function killOne(rawTarget, signal, force, multi) {
4404
4710
  if (trackedMatch) {
4405
4711
  if (!isTrackedRunning(trackedMatch)) {
4406
4712
  removeTrackedByPid(trackedMatch.pid);
4407
- console.error(`
4408
- ${pc8.green("\u2713")} ${pc8.bold(rawTarget)} ${pc8.dim("removed from tracked apps (was already stopped)")}`);
4713
+ console.error(
4714
+ `
4715
+ ${pc8.green("\u2713")} ${pc8.bold(rawTarget)} ${pc8.dim("removed from tracked apps (was already stopped)")}`
4716
+ );
4409
4717
  console.error();
4410
4718
  return;
4411
4719
  }
@@ -4451,29 +4759,42 @@ Use ${pc8.cyan("fennec kill <pid>")} to kill a system process by its PID.`;
4451
4759
  }
4452
4760
  } else {
4453
4761
  spinner.fail(`Failed to kill ${displayName}`);
4454
- console.error(renderError("Permission denied", "Try running with sudo or use a different signal."));
4762
+ console.error(
4763
+ renderError("Permission denied", "Try running with sudo or use a different signal.")
4764
+ );
4455
4765
  }
4456
4766
  }
4457
4767
  async function killGroup(group, signal, force) {
4458
4768
  const tracked = readTracked();
4459
4769
  const inGroup = tracked.filter((t) => t.group === group);
4460
4770
  if (inGroup.length === 0) {
4461
- console.error(renderError("Empty group", `No tracked entries in group "${group}".
4462
- Known groups: ${pc8.cyan(getGroups().join(", ") || "(none)")}`));
4771
+ console.error(
4772
+ renderError(
4773
+ "Empty group",
4774
+ `No tracked entries in group "${group}".
4775
+ Known groups: ${pc8.cyan(getGroups().join(", ") || "(none)")}`
4776
+ )
4777
+ );
4463
4778
  process.exit(1);
4464
4779
  }
4465
4780
  const running = inGroup.filter((t) => isTrackedRunning(t));
4466
4781
  const stopped = inGroup.filter((t) => !isTrackedRunning(t));
4467
- console.error(`
4468
- ${pc8.bold(`Kill group ${pc8.cyan(group)}: ${running.length} running + remove ${stopped.length} stopped?`)}`);
4469
- console.error(` ${pc8.dim("Permanently removes only apps in this group \u2014 other groups are untouched.")}
4470
- `);
4782
+ console.error(
4783
+ `
4784
+ ${pc8.bold(`Kill group ${pc8.cyan(group)}: ${running.length} running + remove ${stopped.length} stopped?`)}`
4785
+ );
4786
+ console.error(
4787
+ ` ${pc8.dim("Permanently removes only apps in this group \u2014 other groups are untouched.")}
4788
+ `
4789
+ );
4471
4790
  const confirmed = force || await confirmPrompt(`${pc8.red("Are you sure?")} ${pc8.dim("This cannot be undone")}`, false);
4472
4791
  if (!confirmed) {
4473
4792
  console.error(` ${pc8.dim("Cancelled.")}`);
4474
4793
  return;
4475
4794
  }
4476
- const spinner = createSpinner(`Killing ${running.length} + removing ${stopped.length} tracked app(s) in ${group}...`);
4795
+ const spinner = createSpinner(
4796
+ `Killing ${running.length} + removing ${stopped.length} tracked app(s) in ${group}...`
4797
+ );
4477
4798
  let killed = 0, failed = 0;
4478
4799
  for (const t of running) {
4479
4800
  if (killTree(t.pid, signal)) {
@@ -4488,7 +4809,9 @@ Known groups: ${pc8.cyan(getGroups().join(", ") || "(none)")}`));
4488
4809
  }
4489
4810
  await new Promise((r) => setTimeout(r, 300));
4490
4811
  const total = running.length + stopped.length;
4491
- total > 0 ? spinner.succeed(`${killed} killed, ${stopped.length} stopped entries removed from group ${group}`) : spinner.fail("Failed to kill apps");
4812
+ total > 0 ? spinner.succeed(
4813
+ `${killed} killed, ${stopped.length} stopped entries removed from group ${group}`
4814
+ ) : spinner.fail("Failed to kill apps");
4492
4815
  if (failed > 0) console.error(` ${pc8.yellow(`${failed} running app(s) could not be killed`)}`);
4493
4816
  }
4494
4817
  async function killAll(signal, force) {
@@ -4499,16 +4822,22 @@ async function killAll(signal, force) {
4499
4822
  }
4500
4823
  const running = tracked.filter((t) => isTrackedRunning(t));
4501
4824
  const stopped = tracked.filter((t) => !isTrackedRunning(t));
4502
- console.error(`
4503
- ${pc8.bold(`Kill ${running.length} running + remove ${stopped.length} stopped tracked app(s)?`)}`);
4504
- console.error(` ${pc8.dim("Permanently removes every Fennec-tracked app \u2014 other processes are untouched.")}
4505
- `);
4825
+ console.error(
4826
+ `
4827
+ ${pc8.bold(`Kill ${running.length} running + remove ${stopped.length} stopped tracked app(s)?`)}`
4828
+ );
4829
+ console.error(
4830
+ ` ${pc8.dim("Permanently removes every Fennec-tracked app \u2014 other processes are untouched.")}
4831
+ `
4832
+ );
4506
4833
  const confirmed = force || await confirmPrompt(`${pc8.red("Are you sure?")} ${pc8.dim("This cannot be undone")}`, false);
4507
4834
  if (!confirmed) {
4508
4835
  console.error(` ${pc8.dim("Cancelled.")}`);
4509
4836
  return;
4510
4837
  }
4511
- const spinner = createSpinner(`Killing ${running.length} + removing ${stopped.length} tracked app(s)...`);
4838
+ const spinner = createSpinner(
4839
+ `Killing ${running.length} + removing ${stopped.length} tracked app(s)...`
4840
+ );
4512
4841
  let killed = 0, failed = 0;
4513
4842
  for (const t of running) {
4514
4843
  if (killTree(t.pid, signal)) {
@@ -4549,7 +4878,9 @@ async function stopCommand(args2) {
4549
4878
  }
4550
4879
  return;
4551
4880
  }
4552
- console.error(renderError("Missing name", "Usage: fennec stop <name|--all> [--group <group>] [-y]"));
4881
+ console.error(
4882
+ renderError("Missing name", "Usage: fennec stop <name|--all> [--group <group>] [-y]")
4883
+ );
4553
4884
  process.exit(1);
4554
4885
  }
4555
4886
  async function stopOne(rawTarget, args2, multi = false) {
@@ -4573,7 +4904,10 @@ async function stopOne(rawTarget, args2, multi = false) {
4573
4904
  }
4574
4905
  const displayName = `${trackedMatch.name} (PID ${trackedMatch.pid})`;
4575
4906
  const force = args2.includes("-y") || args2.includes("--yes");
4576
- const confirmed = force || await confirmPrompt(`Stop ${pc9.bold(displayName)}? ${pc9.dim("It can be re-spawned later via fennec spawn")}`, false);
4907
+ const confirmed = force || await confirmPrompt(
4908
+ `Stop ${pc9.bold(displayName)}? ${pc9.dim("It can be re-spawned later via fennec spawn")}`,
4909
+ false
4910
+ );
4577
4911
  if (!confirmed) {
4578
4912
  console.error(` ${pc9.dim("Cancelled")}`);
4579
4913
  return;
@@ -4585,15 +4919,19 @@ async function stopAllTracked(args2, group) {
4585
4919
  const tracked = readTracked();
4586
4920
  const running = tracked.filter((t) => isTrackedRunning(t) && (!group || t.group === group));
4587
4921
  if (running.length === 0) {
4588
- console.error(`
4922
+ console.error(
4923
+ `
4589
4924
  ${pc9.dim(group ? `No running tracked processes in group "${group}".` : "No running tracked processes to stop.")}
4590
- `);
4925
+ `
4926
+ );
4591
4927
  return;
4592
4928
  }
4593
4929
  const scope = group ? ` group ${pc9.cyan(group)}` : "";
4594
- console.error(`
4930
+ console.error(
4931
+ `
4595
4932
  ${pc9.yellow("\u26A0")} ${pc9.bold(`Stop all ${running.length} running process(es)${scope}?`)} ${pc9.dim("(They can be re-spawned later)")}
4596
- `);
4933
+ `
4934
+ );
4597
4935
  for (const t of running) {
4598
4936
  console.error(` ${pc9.green("\u25CF")} ${pc9.bold(t.name)} ${pc9.dim(`(PID ${t.pid})`)}`);
4599
4937
  }
@@ -4620,7 +4958,9 @@ async function stopAllTracked(args2, group) {
4620
4958
  spinner.stop();
4621
4959
  process.stdout.write("\r\x1B[K");
4622
4960
  if (stopped > 0) {
4623
- console.error(` ${pc9.green("\u2713")} ${pc9.bold(`Stopped ${stopped} process(es)`)} ${pc9.dim("(kept in tracked.json)")}`);
4961
+ console.error(
4962
+ ` ${pc9.green("\u2713")} ${pc9.bold(`Stopped ${stopped} process(es)`)} ${pc9.dim("(kept in tracked.json)")}`
4963
+ );
4624
4964
  }
4625
4965
  if (failed > 0) {
4626
4966
  console.error(` ${pc9.red("\u2717")} ${pc9.bold(`${failed} process(es) failed to stop`)}`);
@@ -4679,8 +5019,13 @@ async function spawnCommand(args2) {
4679
5019
  const tracked = readTracked();
4680
5020
  const inGroup = tracked.filter((t) => t.group === group && !isTrackedRunning(t) && t.command);
4681
5021
  if (inGroup.length === 0) {
4682
- console.error(renderError("Empty group", `No stopped entries with a saved command in group "${group}".
4683
- Known groups: ${pc10.cyan(getGroups().join(", ") || "(none)")}`));
5022
+ console.error(
5023
+ renderError(
5024
+ "Empty group",
5025
+ `No stopped entries with a saved command in group "${group}".
5026
+ Known groups: ${pc10.cyan(getGroups().join(", ") || "(none)")}`
5027
+ )
5028
+ );
4684
5029
  process.exit(1);
4685
5030
  }
4686
5031
  await spawnAllStopped(inGroup);
@@ -4705,9 +5050,13 @@ async function spawnOne(name, multi) {
4705
5050
  process.exit(1);
4706
5051
  }
4707
5052
  if (isTrackedRunning(match)) {
4708
- console.error(`
4709
- ${pc10.yellow("\u26A0")} ${pc10.bold(name)} ${pc10.dim("is already running (PID:")} ${match.pid}${pc10.dim(")")}`);
4710
- console.error(` ${pc10.dim("Use")} ${pc10.cyan(`fennec stop ${name}`)} ${pc10.dim("to stop it first.")}`);
5053
+ console.error(
5054
+ `
5055
+ ${pc10.yellow("\u26A0")} ${pc10.bold(name)} ${pc10.dim("is already running (PID:")} ${match.pid}${pc10.dim(")")}`
5056
+ );
5057
+ console.error(
5058
+ ` ${pc10.dim("Use")} ${pc10.cyan(`fennec stop ${name}`)} ${pc10.dim("to stop it first.")}`
5059
+ );
4711
5060
  console.error();
4712
5061
  if (!multi) process.exit(0);
4713
5062
  return;
@@ -4747,7 +5096,14 @@ async function spawnAllStopped(procs) {
4747
5096
  const logDir = resolve5(homedir3(), ".fennec", "logs");
4748
5097
  mkdirSync5(logDir, { recursive: true });
4749
5098
  const logFilePath = resolve5(logDir, `${proc.name}.log`);
4750
- const child = spawnDaemon({ cmdParts, name: proc.name, cwd: proc.cwd, logFilePath, env: buildSpawnEnv(proc.env), logMode: proc.logMode });
5099
+ const child = spawnDaemon({
5100
+ cmdParts,
5101
+ name: proc.name,
5102
+ cwd: proc.cwd,
5103
+ logFilePath,
5104
+ env: buildSpawnEnv(proc.env),
5105
+ logMode: proc.logMode
5106
+ });
4751
5107
  const pid = child.pid ?? 0;
4752
5108
  addTracked({
4753
5109
  name: proc.name,
@@ -4759,7 +5115,10 @@ async function spawnAllStopped(procs) {
4759
5115
  env: proc.env,
4760
5116
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
4761
5117
  autoRestart: proc.autoRestart,
4762
- logMode: proc.logMode
5118
+ logMode: proc.logMode,
5119
+ // Preserve the logical group across stop -> spawn (addTracked REPLACES
5120
+ // the entry by name, so group must be carried over explicitly).
5121
+ group: proc.group
4763
5122
  });
4764
5123
  if (proc.autoRestart) {
4765
5124
  ensureSupervisorRunning();
@@ -4786,7 +5145,9 @@ async function spawnList() {
4786
5145
  if (tracked.length === 0) {
4787
5146
  console.error(`
4788
5147
  ${pc10.dim("No tracked processes found.")}`);
4789
- console.error(` ${pc10.dim("Start an app first:")} ${pc10.cyan("fennec start <command> --name <name>")}`);
5148
+ console.error(
5149
+ ` ${pc10.dim("Start an app first:")} ${pc10.cyan("fennec start <command> --name <name>")}`
5150
+ );
4790
5151
  console.error();
4791
5152
  return;
4792
5153
  }
@@ -4798,8 +5159,10 @@ async function spawnList() {
4798
5159
  console.error();
4799
5160
  return;
4800
5161
  }
4801
- console.error(`
4802
- ${symbols.fox} ${pc10.bold("Available to spawn")} ${pc10.dim(`(${stopped.length}/${tracked.length} stopped)`)}`);
5162
+ console.error(
5163
+ `
5164
+ ${symbols.fox} ${pc10.bold("Available to spawn")} ${pc10.dim(`(${stopped.length}/${tracked.length} stopped)`)}`
5165
+ );
4803
5166
  if (running.length > 0) {
4804
5167
  console.error(` ${pc10.dim("Running (use stop first):")}`);
4805
5168
  for (const r of running) {
@@ -4807,11 +5170,14 @@ async function spawnList() {
4807
5170
  }
4808
5171
  console.error();
4809
5172
  }
4810
- const selected = await selectPrompt("Select a process to spawn:", stopped.map((t) => ({
4811
- value: t.name,
4812
- label: t.name,
4813
- description: t.command.length > 80 ? t.command.slice(0, 80) + "..." : t.command
4814
- })));
5173
+ const selected = await selectPrompt(
5174
+ "Select a process to spawn:",
5175
+ stopped.map((t) => ({
5176
+ value: t.name,
5177
+ label: t.name,
5178
+ description: t.command.length > 80 ? t.command.slice(0, 80) + "..." : t.command
5179
+ }))
5180
+ );
4815
5181
  if (selected === null) {
4816
5182
  console.error(` ${pc10.dim("Cancelled")}`);
4817
5183
  return;
@@ -4826,14 +5192,23 @@ async function respawnProcess(proc) {
4826
5192
  const logDir = resolve5(homedir3(), ".fennec", "logs");
4827
5193
  mkdirSync5(logDir, { recursive: true });
4828
5194
  const logFilePath = resolve5(logDir, `${proc.name}.log`);
4829
- console.error(`
5195
+ console.error(
5196
+ `
4830
5197
  ${symbols.fox} ${pc10.bold("Spawning")} ${renderAppName(proc.name)} ${pc10.dim("(from saved config)")}
4831
- `);
5198
+ `
5199
+ );
4832
5200
  console.error(` ${renderKV("Command", proc.command)}`);
4833
5201
  if (proc.cwd) console.error(` ${renderKV("Directory", proc.cwd)}`);
4834
5202
  if (proc.port) console.error(` ${renderKV("Port", String(proc.port))}`);
4835
5203
  try {
4836
- const child = spawnDaemon({ cmdParts, name: proc.name, cwd: proc.cwd, logFilePath, env: buildSpawnEnv(proc.env), logMode: proc.logMode });
5204
+ const child = spawnDaemon({
5205
+ cmdParts,
5206
+ name: proc.name,
5207
+ cwd: proc.cwd,
5208
+ logFilePath,
5209
+ env: buildSpawnEnv(proc.env),
5210
+ logMode: proc.logMode
5211
+ });
4837
5212
  const pid = child.pid ?? 0;
4838
5213
  addTracked({
4839
5214
  name: proc.name,
@@ -4845,7 +5220,8 @@ async function respawnProcess(proc) {
4845
5220
  env: proc.env,
4846
5221
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
4847
5222
  autoRestart: proc.autoRestart,
4848
- logMode: proc.logMode
5223
+ logMode: proc.logMode,
5224
+ group: proc.group
4849
5225
  });
4850
5226
  if (proc.autoRestart) {
4851
5227
  ensureSupervisorRunning();
@@ -4876,7 +5252,12 @@ async function restartCommand(args2) {
4876
5252
  await restartOne(target.value, force, false);
4877
5253
  return;
4878
5254
  }
4879
- console.error(renderError("Missing process name/pid", "Usage: fennec restart <name|pid> [-y] [--group <group>]"));
5255
+ console.error(
5256
+ renderError(
5257
+ "Missing process name/pid",
5258
+ "Usage: fennec restart <name|pid> [-y] [--group <group>]"
5259
+ )
5260
+ );
4880
5261
  process.exit(1);
4881
5262
  }
4882
5263
  async function restartOne(raw, force, multi) {
@@ -4916,7 +5297,14 @@ fennec restart only re-spawns Fennec-tracked apps (from their saved config).`;
4916
5297
  try {
4917
5298
  const cmdParts = resolveArgs(trackedEntry);
4918
5299
  const logFilePath = logFilePathFor(trackedEntry.name);
4919
- const child = spawnDaemon({ cmdParts, name: trackedEntry.name, cwd: trackedEntry.cwd, logFilePath, env: buildSpawnEnv(trackedEntry.env), logMode: trackedEntry.logMode });
5300
+ const child = spawnDaemon({
5301
+ cmdParts,
5302
+ name: trackedEntry.name,
5303
+ cwd: trackedEntry.cwd,
5304
+ logFilePath,
5305
+ env: buildSpawnEnv(trackedEntry.env),
5306
+ logMode: trackedEntry.logMode
5307
+ });
4920
5308
  removeTrackedByPid(targetPid);
4921
5309
  addTracked({
4922
5310
  name: trackedEntry.name,
@@ -4928,23 +5316,34 @@ fennec restart only re-spawns Fennec-tracked apps (from their saved config).`;
4928
5316
  env: trackedEntry.env,
4929
5317
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
4930
5318
  autoRestart: trackedEntry.autoRestart,
4931
- logMode: trackedEntry.logMode
5319
+ logMode: trackedEntry.logMode,
5320
+ group: trackedEntry.group
4932
5321
  });
4933
5322
  respawnSpinner.succeed(`${trackedEntry.name} restarted (PID: ${child.pid})`);
4934
5323
  } catch (error) {
4935
5324
  respawnSpinner.fail(`Failed to re-spawn ${trackedEntry.name}: ${String(error)}`);
4936
- console.error(` ${pc11.dim("Config preserved. Re-run manually:")} ${renderCommand(trackedEntry.command)}`);
5325
+ console.error(
5326
+ ` ${pc11.dim("Config preserved. Re-run manually:")} ${renderCommand(trackedEntry.command)}`
5327
+ );
4937
5328
  }
4938
5329
  }
4939
5330
  async function restartGroup(group, force) {
4940
5331
  const tracked = readTracked();
4941
5332
  const inGroup = tracked.filter((t) => t.group === group);
4942
5333
  if (inGroup.length === 0) {
4943
- console.error(renderError("Empty group", `No tracked entries in group "${group}".
4944
- Known groups: ${pc11.cyan(getGroups().join(", ") || "(none)")}`));
5334
+ console.error(
5335
+ renderError(
5336
+ "Empty group",
5337
+ `No tracked entries in group "${group}".
5338
+ Known groups: ${pc11.cyan(getGroups().join(", ") || "(none)")}`
5339
+ )
5340
+ );
4945
5341
  process.exit(1);
4946
5342
  }
4947
- const confirmed = force || await confirmPrompt(`Restart ${pc11.bold(`${inGroup.length}`)} process(es) in group ${pc11.cyan(group)}?`, false);
5343
+ const confirmed = force || await confirmPrompt(
5344
+ `Restart ${pc11.bold(`${inGroup.length}`)} process(es) in group ${pc11.cyan(group)}?`,
5345
+ false
5346
+ );
4948
5347
  if (!confirmed) {
4949
5348
  console.error(` ${pc11.dim("Cancelled")}`);
4950
5349
  return;
@@ -4954,7 +5353,14 @@ Known groups: ${pc11.cyan(getGroups().join(", ") || "(none)")}`));
4954
5353
  try {
4955
5354
  const cmdParts = resolveArgs(trackedEntry);
4956
5355
  const logFilePath = logFilePathFor(trackedEntry.name);
4957
- const child = spawnDaemon({ cmdParts, name: trackedEntry.name, cwd: trackedEntry.cwd, logFilePath, env: buildSpawnEnv(trackedEntry.env), logMode: trackedEntry.logMode });
5356
+ const child = spawnDaemon({
5357
+ cmdParts,
5358
+ name: trackedEntry.name,
5359
+ cwd: trackedEntry.cwd,
5360
+ logFilePath,
5361
+ env: buildSpawnEnv(trackedEntry.env),
5362
+ logMode: trackedEntry.logMode
5363
+ });
4958
5364
  removeTrackedByPid(trackedEntry.pid);
4959
5365
  addTracked({
4960
5366
  name: trackedEntry.name,
@@ -4962,6 +5368,7 @@ Known groups: ${pc11.cyan(getGroups().join(", ") || "(none)")}`));
4962
5368
  command: trackedEntry.command,
4963
5369
  args: cmdParts,
4964
5370
  port: trackedEntry.port,
5371
+ group: trackedEntry.group,
4965
5372
  cwd: trackedEntry.cwd,
4966
5373
  env: trackedEntry.env,
4967
5374
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -4971,7 +5378,9 @@ Known groups: ${pc11.cyan(getGroups().join(", ") || "(none)")}`));
4971
5378
  spinner.succeed(`${trackedEntry.name} restarted (PID: ${child.pid})`);
4972
5379
  } catch (error) {
4973
5380
  spinner.fail(`Failed to re-spawn ${trackedEntry.name}: ${String(error)}`);
4974
- console.error(` ${pc11.dim("Config preserved. Re-run manually:")} ${renderCommand(trackedEntry.command)}`);
5381
+ console.error(
5382
+ ` ${pc11.dim("Config preserved. Re-run manually:")} ${renderCommand(trackedEntry.command)}`
5383
+ );
4975
5384
  }
4976
5385
  }
4977
5386
  }
@@ -4981,7 +5390,9 @@ import pc12 from "picocolors";
4981
5390
  function adoptCommand(args2) {
4982
5391
  const pidArg = args2.find((a) => /^\d+$/.test(a));
4983
5392
  if (!pidArg) {
4984
- console.error(renderError("Missing PID", `Usage: fennec adopt <pid> [--name <name>] [--port <port>]`));
5393
+ console.error(
5394
+ renderError("Missing PID", `Usage: fennec adopt <pid> [--name <name>] [--port <port>]`)
5395
+ );
4985
5396
  process.exit(1);
4986
5397
  }
4987
5398
  const pid = parseInt(pidArg, 10);
@@ -4994,8 +5405,10 @@ function adoptCommand(args2) {
4994
5405
  console.error(renderError("Cannot adopt", `No running process with PID ${pid}`));
4995
5406
  process.exit(1);
4996
5407
  }
4997
- console.error(`
4998
- ${symbols.fox} ${pc12.green("\u2713")} ${pc12.bold("Adopted")} ${pc12.bold(entry.name)} ${pc12.dim(`(PID ${pid})`)}`);
5408
+ console.error(
5409
+ `
5410
+ ${symbols.fox} ${pc12.green("\u2713")} ${pc12.bold("Adopted")} ${pc12.bold(entry.name)} ${pc12.dim(`(PID ${pid})`)}`
5411
+ );
4999
5412
  if (entry.port) console.error(` ${pc12.dim("port")} ${pc12.yellow(`:${entry.port}`)}`);
5000
5413
  if (entry.command) console.error(` ${pc12.dim("command")} ${entry.command}`);
5001
5414
  console.error(` ${pc12.dim("status")} now tracked + supervised by the supervisor`);
@@ -5015,7 +5428,10 @@ var SECRET_PATTERNS = [
5015
5428
  // Authorization: <scheme> <token>
5016
5429
  { name: "authorization", re: /\bAuthorization\s*:\s*\S+/gi },
5017
5430
  // Generic API keys
5018
- { name: "apikey", re: /\b(api[_-]?key|apikey|access[_-]?key|secret[_-]?key|private[_-]?key)\s*[:=]\s*\S+/gi },
5431
+ {
5432
+ name: "apikey",
5433
+ re: /\b(api[_-]?key|apikey|access[_-]?key|secret[_-]?key|private[_-]?key)\s*[:=]\s*\S+/gi
5434
+ },
5019
5435
  // AWS
5020
5436
  { name: "aws", re: /\b(AKIA|ASIA)[0-9A-Z]{16}\b/g },
5021
5437
  // Slack tokens
@@ -5031,9 +5447,15 @@ var SECRET_PATTERNS = [
5031
5447
  // Generic long hex/alpha tokens (>=32 chars) in common contexts
5032
5448
  { name: "token", re: /\b(token|secret|password|passwd|pwd|client[_-]?secret)\s*[:=]\s*\S+/gi },
5033
5449
  // Connection strings with credentials
5034
- { name: "connstr", re: /\b(mongodb(\+srv)?|postgres(ql)?|mysql|redis|amqp|sqlserver):\/\/[^\s:]+:[^\s@]+@/gi },
5450
+ {
5451
+ name: "connstr",
5452
+ re: /\b(mongodb(\+srv)?|postgres(ql)?|mysql|redis|amqp|sqlserver):\/\/[^\s:]+:[^\s@]+@/gi
5453
+ },
5035
5454
  // Private key blocks
5036
- { name: "pem", re: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g }
5455
+ {
5456
+ name: "pem",
5457
+ re: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g
5458
+ }
5037
5459
  ];
5038
5460
  function redactLine(line, opts = {}) {
5039
5461
  if (opts.enabled === false) return line;
@@ -5151,17 +5573,27 @@ async function inspectCommand(args2) {
5151
5573
  process.exit(1);
5152
5574
  }
5153
5575
  const tracked = readTracked();
5154
- const proc = tracked.find((t) => t.name === target || parseInt(target, 10) === t.pid && !isNaN(parseInt(target, 10)));
5576
+ const proc = tracked.find(
5577
+ (t) => t.name === target || parseInt(target, 10) === t.pid && !isNaN(parseInt(target, 10))
5578
+ );
5155
5579
  if (!proc) {
5156
5580
  console.error(pc13.red(`No tracked app named "${target}".`));
5157
5581
  process.exit(1);
5158
5582
  }
5159
5583
  const running = isTrackedRunning(proc);
5160
5584
  const logPath = logFilePathFor(proc.name);
5161
- const lines = readLogLines(logPath, { tail: Math.max(tail, sinceMs ? MAX_TAIL : 0), sinceMs: sinceMs ?? void 0, redact: true, parseTimestamp: true });
5585
+ const lines = readLogLines(logPath, {
5586
+ tail: Math.max(tail, sinceMs ? MAX_TAIL : 0),
5587
+ sinceMs: sinceMs ?? void 0,
5588
+ redact: true,
5589
+ parseTimestamp: true
5590
+ });
5162
5591
  const recent = lines.slice(-tail);
5163
5592
  const errorLines = lines.filter((l) => classifyLevel(l) === "error").slice(-15);
5164
- const redactedLines = recent.reduce((acc, l) => acc + (redactionCount(stripAnsi(l)) > 0 ? 1 : 0), 0);
5593
+ const redactedLines = recent.reduce(
5594
+ (acc, l) => acc + (redactionCount(stripAnsi(l)) > 0 ? 1 : 0),
5595
+ 0
5596
+ );
5165
5597
  const metrics = running ? readMetrics(proc.pid) : {};
5166
5598
  let portHealthy = null;
5167
5599
  if (proc.port) {
@@ -5176,13 +5608,18 @@ async function inspectCommand(args2) {
5176
5608
  if (plain) {
5177
5609
  const dot = running ? pc13.green("\u25CF") : pc13.red("\u25CB");
5178
5610
  const portStr = proc.port ? ` :${proc.port}${portHealthy === false ? pc13.red(" (port down!)") : portHealthy ? pc13.green(" (up)") : ""}` : "";
5179
- console.error(`
5180
- ${symbols.fox} ${renderAppName(proc.name)}${portStr} ${dot} ${running ? pc13.green(`running PID ${proc.pid}`) : pc13.red("stopped")}`);
5181
- 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))}`);
5611
+ console.error(
5612
+ `
5613
+ ${symbols.fox} ${renderAppName(proc.name)}${portStr} ${dot} ${running ? pc13.green(`running PID ${proc.pid}`) : pc13.red("stopped")}`
5614
+ );
5615
+ console.error(
5616
+ ` ${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))}`
5617
+ );
5182
5618
  if (errorLines.length) {
5183
5619
  console.error(`
5184
5620
  ${pc13.bold(pc13.red("Recent errors:"))}`);
5185
- for (const l of errorLines.slice(-8)) console.error(` ${pc13.red(stripAnsi(l).slice(0, 300))}`);
5621
+ for (const l of errorLines.slice(-8))
5622
+ console.error(` ${pc13.red(stripAnsi(l).slice(0, 300))}`);
5186
5623
  }
5187
5624
  console.error();
5188
5625
  return;
@@ -5330,7 +5767,9 @@ async function devCommand(args2) {
5330
5767
  return devRestart(args2.slice(1));
5331
5768
  }
5332
5769
  if (sub !== "up") {
5333
- console.error(renderError("Unknown sub-command", `"${sub}" \u2014 use: up | down | status | restart`));
5770
+ console.error(
5771
+ renderError("Unknown sub-command", `"${sub}" \u2014 use: up | down | status | restart`)
5772
+ );
5334
5773
  process.exit(1);
5335
5774
  }
5336
5775
  const config = loadConfig(configPath);
@@ -5348,14 +5787,18 @@ async function devCommand(args2) {
5348
5787
  if (p) {
5349
5788
  const prev = portOwner.get(p);
5350
5789
  if (prev) {
5351
- console.error(renderError("Port conflict", `apps "${prev}" and "${a.name}" both declare port :${p}`));
5790
+ console.error(
5791
+ renderError("Port conflict", `apps "${prev}" and "${a.name}" both declare port :${p}`)
5792
+ );
5352
5793
  process.exit(1);
5353
5794
  }
5354
5795
  portOwner.set(p, a.name);
5355
5796
  }
5356
5797
  }
5357
- console.error(`
5358
- ${symbols.fox} ${pc14.bold("fennec dev up")} ${pc14.dim(`\u2014 ${apps.length} app(s) from ${configPath}`)}`);
5798
+ console.error(
5799
+ `
5800
+ ${symbols.fox} ${pc14.bold("fennec dev up")} ${pc14.dim(`\u2014 ${apps.length} app(s) from ${configPath}`)}`
5801
+ );
5359
5802
  ensureSupervisorRunning();
5360
5803
  ensurePersistEnabled();
5361
5804
  for (const app of ordered) {
@@ -5366,7 +5809,10 @@ async function devCommand(args2) {
5366
5809
  const sp = createSpinner(`Waiting for ${dep} :${port}...`);
5367
5810
  const ok = await waitForPort(port);
5368
5811
  sp.stop();
5369
- if (!ok) console.error(` ${pc14.yellow("\u26A0")} ${pc14.dim(`${dep} :${port} not ready in time (continuing)`)}`);
5812
+ if (!ok)
5813
+ console.error(
5814
+ ` ${pc14.yellow("\u26A0")} ${pc14.dim(`${dep} :${port} not ready in time (continuing)`)}`
5815
+ );
5370
5816
  }
5371
5817
  }
5372
5818
  const res = await startApp(app, baseDir);
@@ -5374,8 +5820,10 @@ async function devCommand(args2) {
5374
5820
  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})`);
5375
5821
  console.error(` ${pc14.green("\u2713")} ${pc14.bold(app.name)} ${tag}`);
5376
5822
  }
5377
- console.error(`
5378
- ${pc14.green("\u2713")} Stack up. Observe with ${pc14.cyan("fennec observe")} or ${pc14.cyan("fennec dev status")}.`);
5823
+ console.error(
5824
+ `
5825
+ ${pc14.green("\u2713")} Stack up. Observe with ${pc14.cyan("fennec observe")} or ${pc14.cyan("fennec dev status")}.`
5826
+ );
5379
5827
  console.error(` ${pc14.dim("Apps auto-restart (supervisor) + survive reboot (persist).")}
5380
5828
  `);
5381
5829
  }
@@ -5387,8 +5835,10 @@ function devDown() {
5387
5835
  `);
5388
5836
  return;
5389
5837
  }
5390
- console.error(`
5391
- ${symbols.fox} ${pc14.bold("fennec dev down")} ${pc14.dim(`\u2014 stopping ${tracked.length} app(s)`)}`);
5838
+ console.error(
5839
+ `
5840
+ ${symbols.fox} ${pc14.bold("fennec dev down")} ${pc14.dim(`\u2014 stopping ${tracked.length} app(s)`)}`
5841
+ );
5392
5842
  for (const t of tracked) {
5393
5843
  try {
5394
5844
  if (isTrackedRunning(t)) killTree(t.pid, "SIGTERM");
@@ -5398,9 +5848,11 @@ function devDown() {
5398
5848
  addTracked({ ...t, autoRestart: false });
5399
5849
  console.error(` ${pc14.red("\u25A0")} ${pc14.bold(t.name)}`);
5400
5850
  }
5401
- console.error(`
5851
+ console.error(
5852
+ `
5402
5853
  ${pc14.green("\u2713")} Stack stopped (auto-restart disabled). Use ${pc14.cyan("fennec dev up")} to bring it back.
5403
- `);
5854
+ `
5855
+ );
5404
5856
  }
5405
5857
  function devRestart(names) {
5406
5858
  ensureSupervisorRunning();
@@ -5409,7 +5861,12 @@ function devRestart(names) {
5409
5861
  const targets = names.length ? tracked.filter((t) => names.includes(t.name)) : tracked.filter((t) => t.autoRestart);
5410
5862
  const missing = names.filter((n) => !tracked.some((t) => t.name === n));
5411
5863
  if (missing.length) {
5412
- console.error(renderError("Not tracked", `No such app(s): ${missing.join(", ")} \u2014 run "fennec dev status" to list tracked apps.`));
5864
+ console.error(
5865
+ renderError(
5866
+ "Not tracked",
5867
+ `No such app(s): ${missing.join(", ")} \u2014 run "fennec dev status" to list tracked apps.`
5868
+ )
5869
+ );
5413
5870
  }
5414
5871
  if (targets.length === 0) {
5415
5872
  console.error(`
@@ -5417,14 +5874,18 @@ function devRestart(names) {
5417
5874
  `);
5418
5875
  return;
5419
5876
  }
5420
- console.error(`
5421
- ${symbols.fox} ${pc14.bold("fennec dev restart")} ${pc14.dim(`\u2014 ${targets.length} app(s)`)}`);
5877
+ console.error(
5878
+ `
5879
+ ${symbols.fox} ${pc14.bold("fennec dev restart")} ${pc14.dim(`\u2014 ${targets.length} app(s)`)}`
5880
+ );
5422
5881
  for (const t of targets) {
5423
5882
  try {
5424
5883
  const newPid = respawnTracked(t, "manual");
5425
5884
  console.error(` ${pc14.green("\u2713")} ${pc14.bold(t.name)} ${pc14.dim(`restarted (PID ${newPid})`)}`);
5426
5885
  } catch (err) {
5427
- console.error(` ${pc14.red("\u2717")} ${pc14.bold(t.name)} ${pc14.dim(`restart failed: ${String(err)}`)}`);
5886
+ console.error(
5887
+ ` ${pc14.red("\u2717")} ${pc14.bold(t.name)} ${pc14.dim(`restart failed: ${String(err)}`)}`
5888
+ );
5428
5889
  }
5429
5890
  }
5430
5891
  console.error();
@@ -5470,7 +5931,15 @@ function topoOrder(apps) {
5470
5931
  }
5471
5932
 
5472
5933
  // src/commands/log.ts
5473
- import { existsSync as existsSync9, unlinkSync as unlinkSync3, statSync as statSync2, openSync as openSync3, fstatSync, readSync, closeSync as closeSync3 } from "fs";
5934
+ import {
5935
+ existsSync as existsSync9,
5936
+ unlinkSync as unlinkSync3,
5937
+ statSync as statSync3,
5938
+ openSync as openSync3,
5939
+ fstatSync,
5940
+ readSync,
5941
+ closeSync as closeSync3
5942
+ } from "fs";
5474
5943
  import { execSync as execSync2 } from "child_process";
5475
5944
  import pc15 from "picocolors";
5476
5945
  var MAX_LOG_LINES = 500;
@@ -5494,14 +5963,21 @@ async function logCommand(args2) {
5494
5963
  process.exit(1);
5495
5964
  }
5496
5965
  if (levelFilter && !["error", "warn", "info", "debug"].includes(levelFilter)) {
5497
- console.error(renderError("Invalid level", `"${levelFilter}" is not a valid level. Use: error, warn, info, debug`));
5966
+ console.error(
5967
+ renderError(
5968
+ "Invalid level",
5969
+ `"${levelFilter}" is not a valid level. Use: error, warn, info, debug`
5970
+ )
5971
+ );
5498
5972
  process.exit(1);
5499
5973
  }
5500
5974
  const redact = !noRedact;
5501
5975
  let logFilePath = null;
5502
5976
  let displayName = target;
5503
5977
  const tracked = readTracked();
5504
- const trackedMatch = tracked.find((t) => t.name === target || parseInt(target, 10) === t.pid && !isNaN(parseInt(target, 10)));
5978
+ const trackedMatch = tracked.find(
5979
+ (t) => t.name === target || parseInt(target, 10) === t.pid && !isNaN(parseInt(target, 10))
5980
+ );
5505
5981
  if (trackedMatch) {
5506
5982
  displayName = trackedMatch.name;
5507
5983
  logFilePath = logFilePathFor(trackedMatch.name);
@@ -5514,16 +5990,20 @@ async function logCommand(args2) {
5514
5990
  }
5515
5991
  if (clearFlag) {
5516
5992
  if (!logFilePath || !existsSync9(logFilePath)) {
5517
- console.error(`
5993
+ console.error(
5994
+ `
5518
5995
  ${pc15.yellow("\u26A0")} ${pc15.dim("No log file found for")} ${pc15.bold(displayName)}
5519
- `);
5996
+ `
5997
+ );
5520
5998
  process.exit(0);
5521
5999
  }
5522
6000
  try {
5523
6001
  unlinkSync3(logFilePath);
5524
- console.error(`
6002
+ console.error(
6003
+ `
5525
6004
  ${pc15.green("\u2713")} ${pc15.bold("Log cleared")} ${pc15.dim(`\u2014 ${logFilePath}`)}
5526
- `);
6005
+ `
6006
+ );
5527
6007
  } catch (err) {
5528
6008
  console.error(renderError("Failed to clear log", String(err)));
5529
6009
  process.exit(1);
@@ -5543,7 +6023,10 @@ async function logCommand(args2) {
5543
6023
  const pid = trackedMatch?.pid ?? parseInt(target, 10);
5544
6024
  if (!isNaN(pid)) {
5545
6025
  try {
5546
- const output = execSync2(`journalctl --no-pager -n ${Math.max(lineCount, 500)} _PID=${pid} 2>/dev/null || echo ""`, { encoding: "utf-8", timeout: 3e3 });
6026
+ const output = execSync2(
6027
+ `journalctl --no-pager -n ${Math.max(lineCount, 500)} _PID=${pid} 2>/dev/null || echo ""`,
6028
+ { encoding: "utf-8", timeout: 3e3 }
6029
+ );
5547
6030
  logLines = output.trim().split("\n").filter(Boolean).map((l) => redact ? redactLine(stripAnsi(l)) : stripAnsi(l));
5548
6031
  } catch {
5549
6032
  logLines = ["(no logs available)"];
@@ -5558,7 +6041,10 @@ async function logCommand(args2) {
5558
6041
  }
5559
6042
  const running = trackedMatch ? isTrackedRunning(trackedMatch) : false;
5560
6043
  const sliced = logLines.slice(-lineCount);
5561
- const redactedHits = sliced.reduce((acc, l) => acc + (redactionCount(stripAnsi(l)) > 0 ? 1 : 0), 0);
6044
+ const redactedHits = sliced.reduce(
6045
+ (acc, l) => acc + (redactionCount(stripAnsi(l)) > 0 ? 1 : 0),
6046
+ 0
6047
+ );
5562
6048
  const payload = {
5563
6049
  app: displayName,
5564
6050
  running,
@@ -5580,7 +6066,9 @@ async function logCommand(args2) {
5580
6066
  console.error(`
5581
6067
  ${symbols.fox} ${renderAppName(displayName)}${portStr} \u2014 ${statusStr}`);
5582
6068
  if (!running) {
5583
- console.error(` ${pc15.dim("This app is not running. Showing last captured logs.")} ${pc15.cyan(`fennec spawn ${displayName}`)} ${pc15.dim("to restart.")}`);
6069
+ console.error(
6070
+ ` ${pc15.dim("This app is not running. Showing last captured logs.")} ${pc15.cyan(`fennec spawn ${displayName}`)} ${pc15.dim("to restart.")}`
6071
+ );
5584
6072
  }
5585
6073
  }
5586
6074
  const spinner = createSpinner(`Reading logs for ${displayName}...`);
@@ -5597,7 +6085,10 @@ async function logCommand(args2) {
5597
6085
  const pid = trackedMatch?.pid ?? parseInt(target, 10);
5598
6086
  if (!isNaN(pid)) {
5599
6087
  try {
5600
- const output = execSync2(`journalctl --no-pager -n ${Math.max(lineCount, 500)} _PID=${pid} 2>/dev/null || echo ""`, { encoding: "utf-8", timeout: 3e3 });
6088
+ const output = execSync2(
6089
+ `journalctl --no-pager -n ${Math.max(lineCount, 500)} _PID=${pid} 2>/dev/null || echo ""`,
6090
+ { encoding: "utf-8", timeout: 3e3 }
6091
+ );
5601
6092
  logLines = output.trim().split("\n").filter(Boolean).map((l) => redact ? redactLine(stripAnsi(l)) : stripAnsi(l));
5602
6093
  } catch {
5603
6094
  logLines = ["(no logs available)"];
@@ -5612,8 +6103,10 @@ async function logCommand(args2) {
5612
6103
  }
5613
6104
  spinner.stop();
5614
6105
  process.stdout.write("\r\x1B[K");
5615
- console.error(`
5616
- ${symbols.fox} ${pc15.bold("Logs")} ${renderAppName(displayName)} ${pc15.dim(`(last ${logLines.length} line${logLines.length !== 1 ? "s" : ""})${redact ? pc15.dim(" \xB7 secrets redacted") : ""})`)}`);
6106
+ console.error(
6107
+ `
6108
+ ${symbols.fox} ${pc15.bold("Logs")} ${renderAppName(displayName)} ${pc15.dim(`(last ${logLines.length} line${logLines.length !== 1 ? "s" : ""})${redact ? pc15.dim(" \xB7 secrets redacted") : ""})`)}`
6109
+ );
5617
6110
  const sliced = logLines.slice(-lineCount);
5618
6111
  for (const line of sliced) {
5619
6112
  const display = line.length > 300 ? line.slice(0, 300) + "\u2026" : line;
@@ -5639,7 +6132,7 @@ async function logCommand(args2) {
5639
6132
  async function followLog(logFilePath, opts) {
5640
6133
  let lastSize = 0;
5641
6134
  try {
5642
- lastSize = statSync2(logFilePath).size;
6135
+ lastSize = statSync3(logFilePath).size;
5643
6136
  } catch {
5644
6137
  }
5645
6138
  console.error(`
@@ -5703,8 +6196,10 @@ function listAvailableApps() {
5703
6196
  if (tracked.length === 0) {
5704
6197
  console.error(`
5705
6198
  ${symbols.fox} ${pc15.bold("No tracked apps to show logs for.")}`);
5706
- console.error(` ${pc15.dim("Start one with:")} ${pc15.cyan("fennec start <command> --name <name>")}
5707
- `);
6199
+ console.error(
6200
+ ` ${pc15.dim("Start one with:")} ${pc15.cyan("fennec start <command> --name <name>")}
6201
+ `
6202
+ );
5708
6203
  return;
5709
6204
  }
5710
6205
  console.error(`
@@ -5716,12 +6211,18 @@ function listAvailableApps() {
5716
6211
  const state = running ? pc15.green(`running (PID ${t.pid})`) : pc15.red("stopped");
5717
6212
  const portStr = t.port ? ` ${pc15.yellow(`:${t.port}`)}` : "";
5718
6213
  console.error(` ${dot} ${pc15.bold(t.name)}${portStr} ${pc15.dim("\u2014")} ${state}`);
5719
- console.error(` ${pc15.cyan(`fennec log ${t.name}`)} ${pc15.dim("\xB7")} ${pc15.cyan(`fennec log ${t.name} -f`)}`);
6214
+ console.error(
6215
+ ` ${pc15.cyan(`fennec log ${t.name}`)} ${pc15.dim("\xB7")} ${pc15.cyan(`fennec log ${t.name} -f`)}`
6216
+ );
5720
6217
  }
5721
- console.error(`
5722
- ${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")}`);
5723
- 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.")}
5724
- `);
6218
+ console.error(
6219
+ `
6220
+ ${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")}`
6221
+ );
6222
+ console.error(
6223
+ ` ${pc15.dim("AI mode:")} ${pc15.cyan("fennec log <app> --json --since 10m")} ${pc15.dim("\u2014 bounded, redacted, machine-readable. Secrets are redacted by default.")}
6224
+ `
6225
+ );
5725
6226
  }
5726
6227
 
5727
6228
  // src/commands/attach.ts
@@ -5791,9 +6292,11 @@ async function storeCommand(args2) {
5791
6292
  mgr.ensure();
5792
6293
  if (!kind) {
5793
6294
  const scan = mgr.scan();
5794
- console.error(`
6295
+ console.error(
6296
+ `
5795
6297
  ${symbols.fox} ${pc17.bold("Fennec Store")} ${pc17.dim(local ? "(local ./.fennec)" : "(global)")}
5796
- `);
6298
+ `
6299
+ );
5797
6300
  const columns = [
5798
6301
  { key: "kind", label: "Kind", format: (v) => pc17.bold(String(v)) },
5799
6302
  { key: "count", label: "Items", format: (v) => pc17.cyan(String(v)) },
@@ -5831,9 +6334,11 @@ async function storeCommand(args2) {
5831
6334
  if (!action || action === "ls" || action === "list") {
5832
6335
  const sessions = store.list();
5833
6336
  if (sessions.length === 0) {
5834
- console.error(`
6337
+ console.error(
6338
+ `
5835
6339
  ${pc17.dim(`No saved sessions found${local ? " in ./.fennec/sessions" : " in the global store"}.`)}
5836
- `);
6340
+ `
6341
+ );
5837
6342
  return;
5838
6343
  }
5839
6344
  const columns = [
@@ -5856,12 +6361,19 @@ async function storeCommand(args2) {
5856
6361
  }
5857
6362
  if (action === "info") {
5858
6363
  if (!name) {
5859
- console.error(renderError("Missing name", "Usage: fennec store session info <name> [--show-secrets]"));
6364
+ console.error(
6365
+ renderError("Missing name", "Usage: fennec store session info <name> [--show-secrets]")
6366
+ );
5860
6367
  process.exit(1);
5861
6368
  }
5862
6369
  const session = store.load(name);
5863
6370
  if (!session) {
5864
- console.error(renderError("Session not found", `No session named "${name}". Use 'fennec store session' to list.`));
6371
+ console.error(
6372
+ renderError(
6373
+ "Session not found",
6374
+ `No session named "${name}". Use 'fennec store session' to list.`
6375
+ )
6376
+ );
5865
6377
  process.exit(1);
5866
6378
  }
5867
6379
  const shown = showSecrets ? session : redactSession(session);
@@ -5873,7 +6385,10 @@ async function storeCommand(args2) {
5873
6385
  if (shown.metadata) {
5874
6386
  for (const [k, v] of Object.entries(shown.metadata)) body += renderKV(k, String(v));
5875
6387
  }
5876
- body += renderKV("Cookies", `${shown.cookies.length} (domains: ${cookieDomains.join(", ") || "-"})`);
6388
+ body += renderKV(
6389
+ "Cookies",
6390
+ `${shown.cookies.length} (domains: ${cookieDomains.join(", ") || "-"})`
6391
+ );
5877
6392
  body += renderKV("localStorage keys", String(Object.keys(shown.localStorage).length));
5878
6393
  body += renderKV("Values", showSecrets ? "shown" : pc17.dim("masked \u2014 use --show-secrets"));
5879
6394
  console.error(`
@@ -5897,9 +6412,11 @@ async function storeCommand(args2) {
5897
6412
  return;
5898
6413
  }
5899
6414
  const ok = store.delete(name);
5900
- console.error(ok ? ` ${pc17.green("\u2713")} ${pc17.bold(name)} deleted
6415
+ console.error(
6416
+ ok ? ` ${pc17.green("\u2713")} ${pc17.bold(name)} deleted
5901
6417
  ` : ` ${pc17.red("\u2717")} failed to delete ${pc17.bold(name)}
5902
- `);
6418
+ `
6419
+ );
5903
6420
  return;
5904
6421
  }
5905
6422
  console.error(renderError("Unknown action", "Actions: ls | info <name> | rm <name>"));
@@ -6013,11 +6530,15 @@ async function setupCommand() {
6013
6530
  console.error(` ${pc19.bold("Add this to your MCP client config:")}
6014
6531
  `);
6015
6532
  console.error(` ${pc19.dim("```")}`);
6016
- console.error(configSnippet.split("\n").map((l) => ` ${l}`).join("\n"));
6533
+ console.error(
6534
+ configSnippet.split("\n").map((l) => ` ${l}`).join("\n")
6535
+ );
6017
6536
  console.error(` ${pc19.dim("```")}
6018
6537
  `);
6019
- console.error(` ${renderSuccess("Setup complete!")} ${pc19.dim("Run")} ${renderCommand("fennec start")} ${pc19.dim("to begin.")}
6020
- `);
6538
+ console.error(
6539
+ ` ${renderSuccess("Setup complete!")} ${pc19.dim("Run")} ${renderCommand("fennec start")} ${pc19.dim("to begin.")}
6540
+ `
6541
+ );
6021
6542
  }
6022
6543
 
6023
6544
  // src/commands/management.ts
@@ -6035,7 +6556,9 @@ async function installBrowsersCommand() {
6035
6556
  spinner.succeed("Chromium installed successfully");
6036
6557
  } catch {
6037
6558
  spinner.fail("Failed to install Chromium");
6038
- console.error(` ${pc20.yellow("\u2192")} Try running: ${renderCommand("npx playwright install chromium")}`);
6559
+ console.error(
6560
+ ` ${pc20.yellow("\u2192")} Try running: ${renderCommand("npx playwright install chromium")}`
6561
+ );
6039
6562
  }
6040
6563
  console.error(`
6041
6564
  ${pc20.green("\u2713")} Browser installation complete.
@@ -6047,7 +6570,10 @@ async function initCommand() {
6047
6570
  `);
6048
6571
  const configFile = resolve7("./fennec.config.yaml");
6049
6572
  if (existsSync11(configFile)) {
6050
- const overwrite = await confirmPrompt(`${pc20.yellow("fennec.config.yaml")} already exists. Overwrite?`, false);
6573
+ const overwrite = await confirmPrompt(
6574
+ `${pc20.yellow("fennec.config.yaml")} already exists. Overwrite?`,
6575
+ false
6576
+ );
6051
6577
  if (!overwrite) {
6052
6578
  console.error(` ${pc20.dim("Cancelled.")}
6053
6579
  `);
@@ -6136,7 +6662,7 @@ logging:
6136
6662
  }
6137
6663
 
6138
6664
  // src/commands/health.ts
6139
- import { existsSync as existsSync12, readdirSync as readdirSync2, statSync as statSync3 } from "fs";
6665
+ import { existsSync as existsSync12, readdirSync as readdirSync2, statSync as statSync4 } from "fs";
6140
6666
  import { execSync as execSync5 } from "child_process";
6141
6667
  import { resolve as resolve8 } from "path";
6142
6668
  import { homedir as homedir5 } from "os";
@@ -6166,7 +6692,7 @@ async function healthCommand() {
6166
6692
  const files = readdirSync2(logDir);
6167
6693
  let totalBytes = 0;
6168
6694
  for (const f of files) {
6169
- const fStat = statSync3(resolve8(logDir, f));
6695
+ const fStat = statSync4(resolve8(logDir, f));
6170
6696
  totalBytes += fStat.size;
6171
6697
  }
6172
6698
  logSize = totalBytes > 1024 * 1024 ? `${(totalBytes / (1024 * 1024)).toFixed(1)} MB` : `${(totalBytes / 1024).toFixed(1)} KB`;
@@ -6199,8 +6725,10 @@ async function healthCommand() {
6199
6725
  `);
6200
6726
  } else {
6201
6727
  const stopped = tracked.length - runningCount;
6202
- console.error(` ${pc21.yellow("\u26A0")} ${pc21.bold(`${stopped} process(es) stopped`)} ${pc21.dim("fennec ps")}
6203
- `);
6728
+ console.error(
6729
+ ` ${pc21.yellow("\u26A0")} ${pc21.bold(`${stopped} process(es) stopped`)} ${pc21.dim("fennec ps")}
6730
+ `
6731
+ );
6204
6732
  }
6205
6733
  }
6206
6734
 
@@ -6216,19 +6744,26 @@ async function cleanupCommand() {
6216
6744
  }
6217
6745
  const toRemove = tracked.filter((t) => !isProcessRunning(t.pid) && !t.command);
6218
6746
  if (toRemove.length === 0) {
6219
- console.error(`
6747
+ console.error(
6748
+ `
6220
6749
  ${pc22.green("\u2713")} ${pc22.bold("All clean")} ${pc22.dim("\u2014 no dead entries without saved commands.")}
6221
- `);
6750
+ `
6751
+ );
6222
6752
  return;
6223
6753
  }
6224
- console.error(`
6754
+ console.error(
6755
+ `
6225
6756
  ${pc22.yellow("\u26A0")} ${pc22.bold(`Found ${toRemove.length} dead entr${toRemove.length > 1 ? "ies" : "y"} without saved commands`)}
6226
- `);
6757
+ `
6758
+ );
6227
6759
  for (const entry of toRemove) {
6228
6760
  console.error(` ${pc22.dim("\xB7")} ${pc22.bold(entry.name)} ${pc22.dim(`(PID ${entry.pid})`)}`);
6229
6761
  }
6230
6762
  console.error();
6231
- const confirmed = await confirmPrompt(`Remove ${toRemove.length} entr${toRemove.length > 1 ? "ies" : "y"}?`, false);
6763
+ const confirmed = await confirmPrompt(
6764
+ `Remove ${toRemove.length} entr${toRemove.length > 1 ? "ies" : "y"}?`,
6765
+ false
6766
+ );
6232
6767
  if (!confirmed) {
6233
6768
  console.error(` ${pc22.dim("Cancelled")}
6234
6769
  `);
@@ -6236,9 +6771,11 @@ async function cleanupCommand() {
6236
6771
  }
6237
6772
  const remaining = tracked.filter((t) => !toRemove.includes(t));
6238
6773
  saveTracked(remaining);
6239
- console.error(`
6774
+ console.error(
6775
+ `
6240
6776
  ${pc22.green("\u2713")} ${pc22.bold(`Removed ${toRemove.length} entr${toRemove.length > 1 ? "ies" : "y"}`)} ${pc22.dim(`(${remaining.length} remaining)`)}
6241
- `);
6777
+ `
6778
+ );
6242
6779
  }
6243
6780
 
6244
6781
  // src/commands/info.ts
@@ -6314,10 +6851,15 @@ async function renameCommand(args2) {
6314
6851
  console.error(` ${renderKVColor("From", oldName)}`);
6315
6852
  console.error(` ${renderKVColor("To", newName)}`);
6316
6853
  if (isProcessRunning(match.pid)) {
6317
- console.error(` ${pc24.yellow("\u26A0")} ${pc24.dim("Process is still running \u2014 log entries will go to the old file until stopped.")}`);
6854
+ console.error(
6855
+ ` ${pc24.yellow("\u26A0")} ${pc24.dim("Process is still running \u2014 log entries will go to the old file until stopped.")}`
6856
+ );
6318
6857
  }
6319
6858
  console.error();
6320
- const confirmed = await confirmPrompt(`Rename ${pc24.bold(oldName)} ${pc24.dim("\u2192")} ${pc24.bold(newName)}?`, true);
6859
+ const confirmed = await confirmPrompt(
6860
+ `Rename ${pc24.bold(oldName)} ${pc24.dim("\u2192")} ${pc24.bold(newName)}?`,
6861
+ true
6862
+ );
6321
6863
  if (!confirmed) {
6322
6864
  console.error(` ${pc24.dim("Cancelled")}
6323
6865
  `);
@@ -6361,9 +6903,11 @@ async function exportCommand(args2) {
6361
6903
  if (filePath) {
6362
6904
  try {
6363
6905
  writeFileSync5(resolve11(filePath), json, "utf-8");
6364
- console.error(`
6906
+ console.error(
6907
+ `
6365
6908
  ${pc25.green("\u2713")} ${pc25.bold(`Exported ${tracked.length} process(es)`)} ${pc25.dim(`\u2192 ${filePath}`)}
6366
- `);
6909
+ `
6910
+ );
6367
6911
  } catch (err) {
6368
6912
  console.error(renderError("Export failed", String(err)));
6369
6913
  process.exit(1);
@@ -6387,7 +6931,9 @@ async function importCommand(args2) {
6387
6931
  try {
6388
6932
  imported = JSON.parse(readFileSync10(resolvedPath, "utf-8"));
6389
6933
  if (!Array.isArray(imported) || imported.length === 0) {
6390
- console.error(renderError("Invalid file", "File must contain a non-empty JSON array of process objects."));
6934
+ console.error(
6935
+ renderError("Invalid file", "File must contain a non-empty JSON array of process objects.")
6936
+ );
6391
6937
  process.exit(1);
6392
6938
  }
6393
6939
  } catch (err) {
@@ -6407,7 +6953,10 @@ async function importCommand(args2) {
6407
6953
  console.error(` ${pc25.green("+")} ${pc25.bold(p.name)} ${pc25.dim(`(${p.command})`)}`);
6408
6954
  }
6409
6955
  console.error();
6410
- const confirmed = await confirmPrompt("Merge into tracked.json? Existing processes with the same name will be overwritten.", true);
6956
+ const confirmed = await confirmPrompt(
6957
+ "Merge into tracked.json? Existing processes with the same name will be overwritten.",
6958
+ true
6959
+ );
6411
6960
  if (!confirmed) {
6412
6961
  console.error(` ${pc25.dim("Cancelled")}
6413
6962
  `);
@@ -6423,8 +6972,10 @@ async function importCommand(args2) {
6423
6972
  }
6424
6973
  }
6425
6974
  saveTracked(merged);
6426
- console.error(` ${pc25.green("\u2713")} ${pc25.bold(`Imported ${imported.length} process(es)`)} ${pc25.dim(`(${merged.length} total)`)}
6427
- `);
6975
+ console.error(
6976
+ ` ${pc25.green("\u2713")} ${pc25.bold(`Imported ${imported.length} process(es)`)} ${pc25.dim(`(${merged.length} total)`)}
6977
+ `
6978
+ );
6428
6979
  }
6429
6980
 
6430
6981
  // src/commands/group.ts
@@ -6442,44 +6993,87 @@ async function groupCommand(args2) {
6442
6993
  }
6443
6994
  const columns = [
6444
6995
  { key: "name", label: "App", format: (v) => pc26.bold(String(v)) },
6445
- { key: "group", label: "Group", format: (v) => {
6446
- const g = String(v);
6447
- return g === "-" ? pc26.dim("-") : pc26.cyan(g);
6448
- } },
6449
- { key: "command", label: "Command", format: (v) => {
6450
- const c = String(v);
6451
- return c.length > 50 ? c.slice(0, 50) + "\u2026" : c;
6452
- } }
6996
+ {
6997
+ key: "group",
6998
+ label: "Group",
6999
+ format: (v) => {
7000
+ const g = String(v);
7001
+ return g === "-" ? pc26.dim("-") : pc26.cyan(g);
7002
+ }
7003
+ },
7004
+ {
7005
+ key: "command",
7006
+ label: "Command",
7007
+ format: (v) => {
7008
+ const c = String(v);
7009
+ return c.length > 50 ? c.slice(0, 50) + "\u2026" : c;
7010
+ }
7011
+ }
6453
7012
  ];
6454
- const rows = tracked.map((t) => ({ name: t.name, group: t.group ?? "-", command: t.command }));
6455
- console.error(`
7013
+ const rows = tracked.map((t) => ({
7014
+ name: t.name,
7015
+ group: t.group ?? "-",
7016
+ command: t.command
7017
+ }));
7018
+ console.error(
7019
+ `
6456
7020
  ${pc26.bold("Fennec Apps by Group")} ${pc26.dim(`(${getGroups().length} group(s))`)}
6457
- `);
7021
+ `
7022
+ );
6458
7023
  console.error(renderTable(columns, rows));
6459
- console.error(` ${pc26.dim("Assign:")} ${pc26.cyan("fennec group <name> <group>")} ${pc26.dim("Clear:")} ${pc26.cyan("fennec group <name> --unset")}`);
7024
+ console.error(` ${pc26.dim("Assign one:")} ${pc26.cyan("fennec group <name> <group>")}`);
7025
+ console.error(
7026
+ ` ${pc26.dim("Assign bulk:")} ${pc26.cyan("fennec group <group> <name...>")} ${pc26.dim("Clear:")} ${pc26.cyan("fennec group <name> --unset")}`
7027
+ );
6460
7028
  console.error();
6461
7029
  return;
6462
7030
  }
6463
- const name = positional[0];
6464
- const newGroup = positional[1];
6465
- if (!newGroup && !unset) {
6466
- console.error(renderError("Missing group", "Usage: fennec group <name> <group> (or: fennec group <name> --unset)"));
6467
- process.exit(1);
6468
- }
6469
- const ok = setGroup(name, unset ? void 0 : newGroup);
6470
- if (!ok) {
6471
- console.error(renderError("Not found", `No tracked process named "${name}".`));
6472
- process.exit(1);
6473
- }
6474
7031
  if (unset) {
7032
+ const name = positional[0];
7033
+ const ok = setGroup(name, void 0);
7034
+ if (!ok) {
7035
+ console.error(renderError("Not found", `No tracked process named "${name}".`));
7036
+ process.exit(1);
7037
+ }
6475
7038
  console.error(`
6476
7039
  ${pc26.green("\u2713")} ${pc26.bold(name)} ${pc26.dim("removed from its group")}
6477
7040
  `);
7041
+ return;
7042
+ }
7043
+ let group;
7044
+ let names;
7045
+ if (positional.length >= 3) {
7046
+ group = positional[0];
7047
+ names = positional.slice(1);
6478
7048
  } else {
6479
- console.error(`
6480
- ${pc26.green("\u2713")} ${pc26.bold(name)} ${pc26.dim("assigned to group")} ${pc26.cyan(newGroup)}
6481
- `);
7049
+ const [a, b] = positional;
7050
+ const tracked = readTracked();
7051
+ const aIsApp = tracked.some((t) => t.name === a);
7052
+ if (aIsApp) {
7053
+ names = [a];
7054
+ group = b;
7055
+ } else {
7056
+ group = a;
7057
+ names = [b];
7058
+ }
7059
+ }
7060
+ const okNames = [];
7061
+ const failed = [];
7062
+ for (const n of names) {
7063
+ if (setGroup(n, group)) okNames.push(n);
7064
+ else failed.push(n);
6482
7065
  }
7066
+ if (okNames.length > 0) {
7067
+ console.error(
7068
+ `
7069
+ ${pc26.green("\u2713")} ${pc26.bold(okNames.join(", "))} ${pc26.dim(`assigned to group`)} ${pc26.cyan(group)}`
7070
+ );
7071
+ }
7072
+ if (failed.length > 0) {
7073
+ console.error(` ${pc26.red("\u2717")} ${pc26.dim(`not found: ${failed.join(", ")}`)}`);
7074
+ }
7075
+ if (okNames.length === 0 && failed.length > 0) process.exit(1);
7076
+ console.error();
6483
7077
  }
6484
7078
 
6485
7079
  // src/index.ts
@@ -6581,7 +7175,9 @@ async function main() {
6581
7175
  showHelp();
6582
7176
  }
6583
7177
  } else {
6584
- console.error(renderError(`Unknown command: ${command}`, "Run 'fennec help' for usage information"));
7178
+ console.error(
7179
+ renderError(`Unknown command: ${command}`, "Run 'fennec help' for usage information")
7180
+ );
6585
7181
  process.exit(1);
6586
7182
  }
6587
7183
  }