@plumpslabs/fennec-cli 1.12.2 → 1.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1958,6 +1958,9 @@ function renderKV(key, value, options) {
1958
1958
  const pad = " ".repeat(indent);
1959
1959
  return `${pad}${pc.dim(key + sep)} ${value}`;
1960
1960
  }
1961
+ function renderKVColor(key, value, color = pc.bold) {
1962
+ return ` ${pc.dim(key + ":")} ${color(value)}`;
1963
+ }
1961
1964
  function createSpinner(text) {
1962
1965
  const frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
1963
1966
  let i = 0;
@@ -2010,10 +2013,10 @@ async function selectPrompt(message, options) {
2010
2013
  console.log(` ${pc.dim("0) Cancel")}
2011
2014
  `);
2012
2015
  const rl = createInterface({ input: process.stdin, output: process.stdout });
2013
- const answer = await new Promise((resolve8) => {
2016
+ const answer = await new Promise((resolve12) => {
2014
2017
  rl.question(` ${pc.bold("Enter number")} ${pc.dim("(0-" + options.length + ")")}: `, (ans) => {
2015
2018
  rl.close();
2016
- resolve8(ans.trim());
2019
+ resolve12(ans.trim());
2017
2020
  });
2018
2021
  });
2019
2022
  const num = parseInt(answer, 10);
@@ -2028,11 +2031,11 @@ async function selectPrompt(message, options) {
2028
2031
  async function confirmPrompt(message, defaultValue = false) {
2029
2032
  const hint = defaultValue ? pc.bold("Y") + pc.dim("/n") : pc.dim("y/") + pc.bold("N");
2030
2033
  const rl = createInterface({ input: process.stdin, output: process.stdout });
2031
- const answer = await new Promise((resolve8) => {
2034
+ const answer = await new Promise((resolve12) => {
2032
2035
  rl.question(`
2033
2036
  ${message} ${pc.dim("(" + hint + ")")}: `, (ans) => {
2034
2037
  rl.close();
2035
- resolve8(ans.trim().toLowerCase());
2038
+ resolve12(ans.trim().toLowerCase());
2036
2039
  });
2037
2040
  });
2038
2041
  if (!answer) return defaultValue;
@@ -2088,7 +2091,7 @@ function hexColor(color) {
2088
2091
  }
2089
2092
  var fennecOrange2 = hexColor("#FF6432");
2090
2093
  var fennecGold = hexColor("#FFB347");
2091
- var VERSION = "1.12.2";
2094
+ var VERSION = "1.13.0";
2092
2095
  var cachedBanner = null;
2093
2096
  var cachedCompact = null;
2094
2097
  function generateBanner() {
@@ -2220,6 +2223,7 @@ function showHelp() {
2220
2223
  }
2221
2224
 
2222
2225
  // src/commands/pipe.ts
2226
+ import { PipeWatcher } from "@plumpslabs/fennec-core";
2223
2227
  async function pipeCommand(args2) {
2224
2228
  const nameIndex = args2.indexOf("--name");
2225
2229
  const name = nameIndex !== -1 ? args2[nameIndex + 1] : "pipe";
@@ -2227,7 +2231,6 @@ async function pipeCommand(args2) {
2227
2231
  console.error("Error: --name is required for pipe command");
2228
2232
  process.exit(1);
2229
2233
  }
2230
- const { PipeWatcher } = await import("@plumpslabs/fennec-core");
2231
2234
  const watcher = new PipeWatcher();
2232
2235
  const { write } = watcher.createPipe(name);
2233
2236
  console.error(`Pipe watcher '${name}' active. Forwarding stdin...`);
@@ -2265,13 +2268,13 @@ async function pipeCommand(args2) {
2265
2268
  }
2266
2269
 
2267
2270
  // src/commands/attach-pid.ts
2271
+ import { PortDetector } from "@plumpslabs/fennec-core";
2268
2272
  async function attachPidCommand(args2) {
2269
2273
  const pid = parseInt(args2[0], 10);
2270
2274
  if (isNaN(pid)) {
2271
2275
  console.error("Error: valid PID is required");
2272
2276
  process.exit(1);
2273
2277
  }
2274
- const { PortDetector } = await import("@plumpslabs/fennec-core");
2275
2278
  const detector = new PortDetector();
2276
2279
  const info = detector.detectByPid(pid);
2277
2280
  if (info) {
@@ -2284,14 +2287,14 @@ async function attachPidCommand(args2) {
2284
2287
  }
2285
2288
 
2286
2289
  // src/commands/attach-port.ts
2290
+ import { PortDetector as PortDetector2 } from "@plumpslabs/fennec-core";
2287
2291
  async function attachPortCommand(args2) {
2288
2292
  const port = parseInt(args2[0], 10);
2289
2293
  if (isNaN(port)) {
2290
2294
  console.error("Error: valid port number is required");
2291
2295
  process.exit(1);
2292
2296
  }
2293
- const { PortDetector } = await import("@plumpslabs/fennec-core");
2294
- const detector = new PortDetector();
2297
+ const detector = new PortDetector2();
2295
2298
  const info = detector.detectByPort(port);
2296
2299
  if (info) {
2297
2300
  console.log(`Found process on port ${port}: PID ${info.pid}${info.command ? ` (${info.command})` : ""}`);
@@ -2304,6 +2307,7 @@ async function attachPortCommand(args2) {
2304
2307
  // src/commands/watch.ts
2305
2308
  import { existsSync } from "fs";
2306
2309
  import { resolve } from "path";
2310
+ import { LogWatcher } from "@plumpslabs/fennec-core";
2307
2311
  async function watchCommand(args2) {
2308
2312
  const fileIndex = args2.indexOf("--file");
2309
2313
  const filePath = fileIndex !== -1 ? args2[fileIndex + 1] : void 0;
@@ -2318,7 +2322,6 @@ async function watchCommand(args2) {
2318
2322
  console.error(`Error: File not found: ${resolvedPath}`);
2319
2323
  process.exit(1);
2320
2324
  }
2321
- const { LogWatcher } = await import("@plumpslabs/fennec-core");
2322
2325
  const watcher = new LogWatcher();
2323
2326
  const watcherId = watcher.watchFile(resolvedPath, name);
2324
2327
  console.log(`Watching file: ${resolvedPath}`);
@@ -2327,12 +2330,12 @@ async function watchCommand(args2) {
2327
2330
  }
2328
2331
 
2329
2332
  // src/commands/start.ts
2330
- import { createWriteStream, readFileSync as readFileSync3, mkdirSync as mkdirSync2 } from "fs";
2333
+ import { createWriteStream, mkdirSync as mkdirSync2 } from "fs";
2331
2334
  import { resolve as resolve3 } from "path";
2332
2335
  import { homedir as homedir2 } from "os";
2333
2336
  import { spawn } from "child_process";
2334
2337
  import { FennecServer } from "@plumpslabs/fennec-core";
2335
- import pc4 from "picocolors";
2338
+ import pc5 from "picocolors";
2336
2339
 
2337
2340
  // src/commands/tracker.ts
2338
2341
  import { existsSync as existsSync2, writeFileSync, readFileSync, mkdirSync, renameSync, statSync, rmSync } from "fs";
@@ -2676,10 +2679,198 @@ function formatProcessState(state) {
2676
2679
  }
2677
2680
  }
2678
2681
 
2682
+ // src/commands/ps.ts
2683
+ import pc4 from "picocolors";
2684
+ async function psCommand(args2) {
2685
+ const watchFlag = args2.includes("-w") || args2.includes("--watch");
2686
+ const systemFlag = args2.includes("--system") || args2.includes("-a") || args2.includes("--all");
2687
+ const jsonFlag = args2.includes("--json");
2688
+ const nameFilter = args2.includes("--name") ? args2[args2.indexOf("--name") + 1] : void 0;
2689
+ const sortBy = args2.includes("--sort") ? args2[args2.indexOf("--sort") + 1] : "name";
2690
+ if (jsonFlag) {
2691
+ await psJson();
2692
+ return;
2693
+ }
2694
+ if (watchFlag && systemFlag) {
2695
+ await watchSystemProcesses(sortBy, 15);
2696
+ return;
2697
+ }
2698
+ if (systemFlag) {
2699
+ const spinner = createSpinner("Scanning system processes...");
2700
+ try {
2701
+ const processes = getSystemProcesses({ name: nameFilter, userOnly: true, sortBy, limit: 30 });
2702
+ spinner.stop();
2703
+ process.stdout.write("\r\x1B[K");
2704
+ if (processes.length === 0) {
2705
+ console.error(`
2706
+ ${pc4.dim("No system processes found.")}
2707
+ `);
2708
+ return;
2709
+ }
2710
+ const columns2 = [
2711
+ { key: "pid", label: "PID", align: "right", format: (v) => pc4.dim(String(v).padStart(6)) },
2712
+ { key: "name", label: "Name", format: (v) => pc4.bold(String(v)) },
2713
+ { key: "cpu", label: "CPU%", align: "right", format: (v) => {
2714
+ const n = v;
2715
+ return n > 10 ? pc4.red(String(n)) : n > 5 ? pc4.yellow(String(n)) : pc4.dim(String(n));
2716
+ } },
2717
+ { key: "mem", label: "MEM%", align: "right", format: (v) => {
2718
+ const n = v;
2719
+ return n > 10 ? pc4.red(String(n)) : n > 5 ? pc4.yellow(String(n)) : pc4.dim(String(n));
2720
+ } },
2721
+ { key: "state", label: "State", format: (v) => {
2722
+ const s = String(v);
2723
+ if (s === "R" || s === "Running") return pc4.green(s);
2724
+ if (s === "Z" || s === "Zombie") return pc4.red(s);
2725
+ if (s === "S" || s === "Sleeping") return pc4.cyan(s);
2726
+ return pc4.dim(s);
2727
+ } }
2728
+ ];
2729
+ const rows2 = processes.map((p) => ({ pid: p.pid, name: p.name, cpu: p.cpuPercent, mem: p.memPercent, state: formatProcessState(p.state) }));
2730
+ console.error(`
2731
+ ${symbols.fox} ${pc4.bold("System Processes")} ${pc4.dim(`(top ${processes.length} by ${sortBy})`)}
2732
+ `);
2733
+ console.error(renderTable(columns2, rows2));
2734
+ console.error();
2735
+ } catch (error) {
2736
+ spinner.fail("Failed to scan processes");
2737
+ console.error(renderError("Process scan failed", String(error)));
2738
+ }
2739
+ return;
2740
+ }
2741
+ const tracked = readTracked();
2742
+ if (tracked.length === 0) {
2743
+ console.error(`
2744
+ ${pc4.dim("No tracked processes.")}`);
2745
+ console.error(` ${pc4.dim("Start an app with:")} ${pc4.cyan("fennec start <command> --name <name>")}
2746
+ `);
2747
+ return;
2748
+ }
2749
+ const columns = [
2750
+ { key: "name", label: "App", format: (v) => pc4.bold(String(v)) },
2751
+ { key: "pid", label: "PID", align: "right" },
2752
+ { key: "status", label: "Status", format: (v) => {
2753
+ const s = v;
2754
+ return s === "running" ? pc4.green("\u25CF running") : pc4.red("\u25CB stopped");
2755
+ } },
2756
+ { key: "port", label: "Port", format: (v) => {
2757
+ const p = v;
2758
+ return p ? pc4.yellow(`:${p}`) : pc4.dim("-");
2759
+ } },
2760
+ { key: "command", label: "Command", format: (v) => {
2761
+ const c = String(v);
2762
+ return c.length > 50 ? c.slice(0, 50) + "\u2026" : c;
2763
+ } },
2764
+ { key: "uptime", label: "Uptime", format: (v) => pc4.dim(String(v)) }
2765
+ ];
2766
+ const rows = tracked.map((t) => {
2767
+ const running = isProcessRunning(t.pid);
2768
+ const uptime = running ? formatUptime(Math.floor((Date.now() - new Date(t.startedAt).getTime()) / 1e3)) : "-";
2769
+ return { name: t.name, pid: running ? String(t.pid) : pc4.dim(String(t.pid)), status: running ? "running" : "stopped", port: t.port ?? null, command: t.command, uptime };
2770
+ });
2771
+ const runningCount = tracked.filter((t) => isProcessRunning(t.pid)).length;
2772
+ console.error(`
2773
+ ${symbols.fox} ${pc4.bold("Fennec Apps")} ${pc4.dim(`(${runningCount}/${tracked.length} running)`)}
2774
+ `);
2775
+ console.error(renderTable(columns, rows));
2776
+ console.error(` ${pc4.dim("Use")} ${pc4.cyan("fennec start <command> --name <name> --port <port>")} ${pc4.dim("to add more apps.")}`);
2777
+ console.error(` ${pc4.dim("Use")} ${pc4.cyan("fennec log <name>")} ${pc4.dim("to view logs.")}`);
2778
+ console.error(` ${pc4.dim("Use")} ${pc4.cyan("fennec stop <name>")} ${pc4.dim("to pause an app.")}`);
2779
+ console.error(` ${pc4.dim("Use")} ${pc4.cyan("fennec spawn <name>")} ${pc4.dim("to resume a paused app.")}`);
2780
+ console.error(` ${pc4.dim("Use")} ${pc4.cyan("fennec kill <name>")} ${pc4.dim("to permanently remove an app.")}`);
2781
+ console.error();
2782
+ }
2783
+ async function psJson() {
2784
+ const tracked = readTracked();
2785
+ const data = tracked.map((t) => {
2786
+ const running = isProcessRunning(t.pid);
2787
+ return {
2788
+ name: t.name,
2789
+ pid: t.pid,
2790
+ status: running ? "running" : "stopped",
2791
+ port: t.port ?? null,
2792
+ command: t.command,
2793
+ cwd: t.cwd ?? null,
2794
+ startedAt: t.startedAt,
2795
+ uptime: running ? Math.floor((Date.now() - new Date(t.startedAt).getTime()) / 1e3) : null
2796
+ };
2797
+ });
2798
+ console.log(JSON.stringify(data, null, 2));
2799
+ }
2800
+ async function statusCommand(_args) {
2801
+ const watchFlag = _args.includes("-w") || _args.includes("--watch");
2802
+ const tracked = readTracked();
2803
+ const topSystem = getSystemProcesses({ userOnly: true, sortBy: "cpu", limit: 5 });
2804
+ const totalUserProcs = getSystemProcesses({ userOnly: true }).length;
2805
+ console.error(`
2806
+ ${symbols.fox} ${pc4.bold("Fennec Status")}
2807
+ `);
2808
+ if (tracked.length > 0) {
2809
+ const runningCount = tracked.filter((t) => isProcessRunning(t.pid)).length;
2810
+ console.error(` ${pc4.bold("Managed Apps")} ${pc4.dim(`(${runningCount}/${tracked.length} running)`)}
2811
+ `);
2812
+ for (const t of tracked) {
2813
+ const running = isProcessRunning(t.pid);
2814
+ const statusIcon = running ? pc4.green("\u25CF") : pc4.red("\u25CB");
2815
+ const portStr = t.port ? ` ${pc4.yellow(`:${t.port}`)}` : "";
2816
+ const uptime = running ? pc4.dim(formatUptime(Math.floor((Date.now() - new Date(t.startedAt).getTime()) / 1e3))) : pc4.red("stopped");
2817
+ console.error(` ${statusIcon} ${pc4.bold(t.name)}${portStr} ${pc4.dim(`(PID ${t.pid})`)} \u2014 ${uptime}`);
2818
+ }
2819
+ console.error();
2820
+ } else {
2821
+ console.error(` ${pc4.dim("No managed apps.")} ${pc4.cyan("fennec start <command> --name <name>")}
2822
+ `);
2823
+ }
2824
+ console.error(` ${pc4.bold("System")} ${pc4.dim(`(${totalUserProcs} user processes)`)}`);
2825
+ for (const p of topSystem) {
2826
+ const cpuStr = p.cpuPercent > 10 ? pc4.red(`${p.cpuPercent}%`) : p.cpuPercent > 5 ? pc4.yellow(`${p.cpuPercent}%`) : pc4.dim(`${p.cpuPercent}%`);
2827
+ const memStr = p.memPercent > 10 ? pc4.red(`${p.memPercent}%`) : p.memPercent > 5 ? pc4.yellow(`${p.memPercent}%`) : pc4.dim(`${p.memPercent}%`);
2828
+ console.error(` ${pc4.dim(`PID ${p.pid}`)} ${pc4.bold(p.name)} \u2014 CPU: ${cpuStr} MEM: ${memStr}`);
2829
+ }
2830
+ if (watchFlag) await watchSystemProcesses("cpu", 15);
2831
+ console.error();
2832
+ }
2833
+ async function watchSystemProcesses(sortBy, limit) {
2834
+ console.error(`
2835
+ ${pc4.bold("Watching system processes")} ${pc4.dim("(Ctrl+C to stop, refreshes every 3s)")}
2836
+ `);
2837
+ const render = () => {
2838
+ const processes = getSystemProcesses({ userOnly: true, sortBy, limit });
2839
+ const columns = [
2840
+ { key: "pid", label: "PID", align: "right", format: (v) => pc4.dim(String(v).padStart(6)) },
2841
+ { key: "name", label: "Name", format: (v) => pc4.bold(String(v)) },
2842
+ { key: "cpu", label: "CPU%", align: "right", format: (v) => {
2843
+ const n = v;
2844
+ return n > 10 ? pc4.red(String(n)) : n > 5 ? pc4.yellow(String(n)) : pc4.dim(String(n));
2845
+ } },
2846
+ { key: "mem", label: "MEM%", align: "right", format: (v) => {
2847
+ const n = v;
2848
+ return n > 10 ? pc4.red(String(n)) : n > 5 ? pc4.yellow(String(n)) : pc4.dim(String(n));
2849
+ } },
2850
+ { key: "state", label: "S", align: "center" }
2851
+ ];
2852
+ const rows = processes.map((p) => ({ pid: p.pid, name: p.name, cpu: p.cpuPercent, mem: p.memPercent, state: p.state }));
2853
+ return ` ${timestamp()} ${pc4.dim(`${processes.length} processes`)}
2854
+ ${renderTable(columns, rows, { compact: true })}`;
2855
+ };
2856
+ console.error(render());
2857
+ const interval = setInterval(() => {
2858
+ process.stdout.write("\x1B[J");
2859
+ console.error(render());
2860
+ }, 3e3);
2861
+ await new Promise((resolve12) => {
2862
+ process.once("SIGINT", () => {
2863
+ clearInterval(interval);
2864
+ process.stdout.write("\x1B[J");
2865
+ resolve12();
2866
+ });
2867
+ });
2868
+ }
2869
+
2679
2870
  // src/commands/start.ts
2680
2871
  async function startServer(args2) {
2681
2872
  printBanner();
2682
- console.error(` ${pc4.dim("Starting Fennec MCP server...")}
2873
+ console.error(` ${pc5.dim("Starting Fennec MCP server...")}
2683
2874
  `);
2684
2875
  const configIndex = args2.indexOf("--config");
2685
2876
  const configPath = configIndex !== -1 ? args2[configIndex + 1] : void 0;
@@ -2688,11 +2879,11 @@ async function startServer(args2) {
2688
2879
  await resurrectTracked();
2689
2880
  await server.start();
2690
2881
  console.error(`
2691
- ${pc4.green("\u2713")} ${pc4.bold("Fennec server is running")}`);
2882
+ ${pc5.green("\u2713")} ${pc5.bold("Fennec server is running")}`);
2692
2883
  console.error(` ${renderKV("Transport", "stdio")}`);
2693
2884
  console.error(` ${renderKV("AI Agent", "Connect via MCP protocol")}`);
2694
2885
  console.error(`
2695
- ${pc4.dim("Press Ctrl+C to stop")}
2886
+ ${pc5.dim("Press Ctrl+C to stop")}
2696
2887
  `);
2697
2888
  } catch (error) {
2698
2889
  console.error(renderError("Failed to start server", String(error)));
@@ -2701,7 +2892,7 @@ async function startServer(args2) {
2701
2892
  }
2702
2893
  async function startCommand(args2) {
2703
2894
  printBanner();
2704
- console.error(` ${pc4.dim("Starting app...")}
2895
+ console.error(` ${pc5.dim("Starting app...")}
2705
2896
  `);
2706
2897
  await runCommand(args2);
2707
2898
  }
@@ -2712,12 +2903,13 @@ async function runCommand(args2) {
2712
2903
  const port = portIndex !== -1 ? parseInt(args2[portIndex + 1], 10) : void 0;
2713
2904
  const cwdIndex = args2.indexOf("--cwd");
2714
2905
  const cwd = cwdIndex !== -1 ? args2[cwdIndex + 1] : void 0;
2906
+ const restartFlag = args2.includes("--restart");
2715
2907
  const stopFlags = [nameIndex, portIndex, cwdIndex].filter((i) => i !== -1);
2716
2908
  const cmdEnd = Math.min(...stopFlags, Infinity);
2717
2909
  const cmdParts = args2.slice(0, cmdEnd);
2718
2910
  const cmd = cmdParts.join(" ");
2719
2911
  if (!cmd) {
2720
- console.error(renderError("Missing command", "Usage: fennec start|run <command> --name <name> [--port <port>] [--cwd <dir>]"));
2912
+ console.error(renderError("Missing command", "Usage: fennec start|run <command> --name <name> [--port <port>] [--cwd <dir>] [--restart]"));
2721
2913
  process.exit(1);
2722
2914
  }
2723
2915
  const appName = name ?? cmdParts[0] ?? "app";
@@ -2725,9 +2917,9 @@ async function runCommand(args2) {
2725
2917
  const existing = tracked.find((t) => t.name === appName);
2726
2918
  if (existing && isProcessRunning(existing.pid)) {
2727
2919
  console.error();
2728
- console.error(` ${pc4.yellow("\u26A0")} ${pc4.bold(appName)} ${pc4.dim(`is already running (PID: ${existing.pid})`)}`);
2729
- console.error(` ${renderKV("Logs", pc4.cyan(`fennec log ${appName}`))}`);
2730
- console.error(` ${renderKV("Stop", pc4.cyan(`fennec kill ${appName}`))}`);
2920
+ console.error(` ${pc5.yellow("\u26A0")} ${pc5.bold(appName)} ${pc5.dim(`is already running (PID: ${existing.pid})`)}`);
2921
+ console.error(` ${renderKV("Logs", pc5.cyan(`fennec log ${appName}`))}`);
2922
+ console.error(` ${renderKV("Stop", pc5.cyan(`fennec kill ${appName}`))}`);
2731
2923
  console.error();
2732
2924
  process.exit(0);
2733
2925
  }
@@ -2735,25 +2927,26 @@ async function runCommand(args2) {
2735
2927
  mkdirSync2(logDir, { recursive: true });
2736
2928
  const logFilePath = resolve3(logDir, `${appName}.log`);
2737
2929
  console.error(`
2738
- ${symbols.fox} ${pc4.bold("Starting")} ${renderAppName(appName)} ${pc4.dim("(daemon)")}
2930
+ ${symbols.fox} ${pc5.bold("Starting")} ${renderAppName(appName)} ${pc5.dim("(daemon)")}
2739
2931
  `);
2740
2932
  console.error(` ${renderKV("Command", cmd)}`);
2741
2933
  if (port) console.error(` ${renderKV("Port", String(port))}`);
2742
2934
  if (cwd) console.error(` ${renderKV("Directory", cwd)}`);
2935
+ if (restartFlag) console.error(` ${renderKV("Auto-restart", pc5.green("enabled"))}`);
2743
2936
  console.error(` ${divider()}`);
2744
2937
  try {
2745
- const child = spawn(cmdParts[0], cmdParts.slice(1), {
2938
+ let currentChild = spawn(cmdParts[0], cmdParts.slice(1), {
2746
2939
  cwd,
2747
2940
  env: { ...process.env },
2748
2941
  stdio: ["ignore", "pipe", "pipe"],
2749
2942
  detached: true
2750
2943
  });
2751
- const pid = child.pid ?? 0;
2944
+ const pid = currentChild.pid ?? 0;
2752
2945
  rotateLogFile(logFilePath);
2753
2946
  const logStream = createWriteStream(logFilePath, { flags: "a" });
2754
- if (child.stdout) child.stdout.pipe(logStream);
2755
- if (child.stderr) child.stderr.pipe(logStream);
2756
- child.unref();
2947
+ if (currentChild.stdout) currentChild.stdout.pipe(logStream);
2948
+ if (currentChild.stderr) currentChild.stderr.pipe(logStream);
2949
+ currentChild.unref();
2757
2950
  addTracked({
2758
2951
  name: appName,
2759
2952
  pid,
@@ -2762,32 +2955,45 @@ async function runCommand(args2) {
2762
2955
  cwd,
2763
2956
  startedAt: (/* @__PURE__ */ new Date()).toISOString()
2764
2957
  });
2765
- console.error(`
2766
- ${pc4.green("\u25CF")} ${pc4.bold(appName)} ${pc4.dim(`started (PID: ${pid})`)}${port ? pc4.dim(` :${port}`) : ""}
2767
- `);
2768
- await new Promise((r) => setTimeout(r, 1500));
2769
- try {
2770
- const initialLogs = readFileSync3(logFilePath, "utf-8").split("\n").filter(Boolean).slice(-8);
2771
- if (initialLogs.length > 0) {
2772
- for (const line of initialLogs) {
2773
- const truncated = line.length > 150 ? line.slice(0, 150) + "\u2026" : line;
2774
- if (line.toLowerCase().includes("error")) {
2775
- console.error(` ${pc4.dim("\u2502")} ${pc4.red(truncated)}`);
2776
- } else if (line.toLowerCase().includes("warn")) {
2777
- console.error(` ${pc4.dim("\u2502")} ${pc4.yellow(truncated)}`);
2778
- } else {
2779
- console.error(` ${pc4.dim("\u2502")} ${truncated}`);
2958
+ console.error(` ${pc5.green("\u2713")} ${pc5.bold(appName)} ${pc5.dim(`started (PID: ${pid})`)}`);
2959
+ if (restartFlag) {
2960
+ let setupRestartWatch2 = function(c) {
2961
+ c.on("exit", (code, signal) => {
2962
+ if (code !== 0 && signal !== "SIGTERM" && signal !== "SIGKILL") {
2963
+ console.error(` ${pc5.yellow("\u26A0")} ${pc5.bold(appName)} ${pc5.dim(`exited with code ${code}, restarting...`)}`);
2964
+ const restarted = spawn(cmdParts[0], cmdParts.slice(1), {
2965
+ cwd,
2966
+ env: { ...process.env },
2967
+ stdio: ["ignore", "pipe", "pipe"],
2968
+ detached: true
2969
+ });
2970
+ const newPid = restarted.pid ?? 0;
2971
+ rotateLogFile(logFilePath);
2972
+ const rs = createWriteStream(logFilePath, { flags: "a" });
2973
+ if (restarted.stdout) restarted.stdout.pipe(rs);
2974
+ if (restarted.stderr) restarted.stderr.pipe(rs);
2975
+ restarted.unref();
2976
+ addTracked({ name: appName, pid: newPid, command: cmd, port, cwd, startedAt: (/* @__PURE__ */ new Date()).toISOString() });
2977
+ console.error(` ${pc5.green("\u2713")} ${pc5.bold(appName)} ${pc5.dim(`restarted (PID: ${newPid})`)}`);
2978
+ currentChild = restarted;
2979
+ setupRestartWatch2(restarted);
2780
2980
  }
2781
- }
2782
- }
2783
- } catch {
2981
+ });
2982
+ };
2983
+ var setupRestartWatch = setupRestartWatch2;
2984
+ console.error(` ${pc5.dim("Auto-restart enabled \u2014 watching for crashes...")}
2985
+ `);
2986
+ setupRestartWatch2(currentChild);
2987
+ await new Promise((resolve12) => {
2988
+ process.once("SIGINT", () => {
2989
+ currentChild.kill("SIGTERM");
2990
+ resolve12();
2991
+ });
2992
+ });
2993
+ }
2994
+ if (!restartFlag) {
2995
+ await psCommand([]);
2784
2996
  }
2785
- console.error();
2786
- console.error(` ${pc4.green("\u2713")} ${pc4.bold(appName)} running in background`);
2787
- console.error(` ${renderKV("Logs", pc4.cyan(`fennec log ${appName}`))}`);
2788
- console.error(` ${renderKV("Status", pc4.cyan("fennec ps"))}`);
2789
- console.error(` ${renderKV("Stop", pc4.cyan(`fennec kill ${appName}`))}`);
2790
- console.error();
2791
2997
  } catch (error) {
2792
2998
  console.error(renderError(`Failed to start ${appName}`, String(error)));
2793
2999
  process.exit(1);
@@ -2838,227 +3044,70 @@ async function resurrectTracked() {
2838
3044
  spinner.stop();
2839
3045
  process.stdout.write("\r\x1B[K");
2840
3046
  if (resurrected > 0) {
2841
- console.error(` ${pc4.green("\u25CF")} ${pc4.bold(`Resurrected ${resurrected} process(es)`)}`);
3047
+ console.error(` ${pc5.green("\u25CF")} ${pc5.bold(`Resurrected ${resurrected} process(es)`)}`);
2842
3048
  }
2843
3049
  if (failed > 0) {
2844
- console.error(` ${pc4.red("\u25CF")} ${pc4.bold(`${failed} process(es) failed to resurrect`)}`);
3050
+ console.error(` ${pc5.red("\u25CF")} ${pc5.bold(`${failed} process(es) failed to resurrect`)}`);
2845
3051
  }
2846
3052
  }
2847
3053
 
2848
- // src/commands/ps.ts
2849
- import pc5 from "picocolors";
2850
- async function psCommand(args2) {
2851
- const watchFlag = args2.includes("-w") || args2.includes("--watch");
2852
- const systemFlag = args2.includes("--system") || args2.includes("-a") || args2.includes("--all");
2853
- const nameFilter = args2.includes("--name") ? args2[args2.indexOf("--name") + 1] : void 0;
2854
- const sortBy = args2.includes("--sort") ? args2[args2.indexOf("--sort") + 1] : "name";
2855
- if (watchFlag && systemFlag) {
2856
- await watchSystemProcesses(sortBy, 15);
3054
+ // src/commands/kill.ts
3055
+ import pc6 from "picocolors";
3056
+ async function killCommand(args2) {
3057
+ const rawTarget = args2[0];
3058
+ const signalIndex = args2.indexOf("--signal");
3059
+ const signalRaw = signalIndex !== -1 ? args2[signalIndex + 1] : "SIGTERM";
3060
+ const signal = signalRaw ?? "SIGTERM";
3061
+ if (rawTarget === "all" || args2.includes("--all") || args2.includes("-a")) {
3062
+ await killAll(signal);
2857
3063
  return;
2858
3064
  }
2859
- if (systemFlag) {
2860
- const spinner = createSpinner("Scanning system processes...");
2861
- try {
2862
- const processes = getSystemProcesses({ name: nameFilter, userOnly: true, sortBy, limit: 30 });
2863
- spinner.stop();
2864
- process.stdout.write("\r\x1B[K");
2865
- if (processes.length === 0) {
3065
+ if (!rawTarget) {
3066
+ console.error(renderError("Missing target", "Usage: fennec kill <pid|name|all> [--signal SIGTERM|SIGKILL|SIGINT]"));
3067
+ process.exit(1);
3068
+ }
3069
+ let targetPid;
3070
+ let displayName;
3071
+ const pid = parseInt(rawTarget, 10);
3072
+ if (!isNaN(pid) && String(pid) === rawTarget) {
3073
+ if (!isProcessRunning(pid)) {
3074
+ console.error(renderError("Process not found", `No process with PID ${pid} is running`));
3075
+ process.exit(1);
3076
+ }
3077
+ targetPid = pid;
3078
+ displayName = `PID ${pid}`;
3079
+ } else {
3080
+ const tracked = readTracked();
3081
+ const trackedMatch = tracked.find((t) => t.name === rawTarget);
3082
+ if (trackedMatch && isProcessRunning(trackedMatch.pid)) {
3083
+ targetPid = trackedMatch.pid;
3084
+ displayName = `${trackedMatch.name} (PID ${targetPid})`;
3085
+ } else {
3086
+ const matches = getSystemProcesses({ name: rawTarget, userOnly: true, sortBy: "cpu" });
3087
+ if (matches.length === 0) {
3088
+ console.error(renderError("Process not found", `No running process matching "${rawTarget}"`));
3089
+ process.exit(1);
3090
+ }
3091
+ if (matches.length > 1) {
2866
3092
  console.error(`
2867
- ${pc5.dim("No system processes found.")}
2868
- `);
2869
- return;
3093
+ ${pc6.bold(`Multiple processes match "${rawTarget}":`)}`);
3094
+ const selected = await selectPrompt("Select which to kill:", matches.map((p, i) => ({ value: String(i), label: `${p.name} (PID ${p.pid})`, description: `${p.command.slice(0, 80)} \u2014 CPU: ${p.cpuPercent}% MEM: ${p.memPercent}%` })));
3095
+ if (selected === null) {
3096
+ console.error(` ${pc6.dim("Cancelled")}`);
3097
+ return;
3098
+ }
3099
+ const idx = parseInt(selected, 10);
3100
+ targetPid = matches[idx].pid;
3101
+ displayName = `${matches[idx].name} (PID ${targetPid})`;
3102
+ } else {
3103
+ targetPid = matches[0].pid;
3104
+ displayName = `${matches[0].name} (PID ${targetPid})`;
2870
3105
  }
2871
- const columns2 = [
2872
- { key: "pid", label: "PID", align: "right", format: (v) => pc5.dim(String(v).padStart(6)) },
2873
- { key: "name", label: "Name", format: (v) => pc5.bold(String(v)) },
2874
- { key: "cpu", label: "CPU%", align: "right", format: (v) => {
2875
- const n = v;
2876
- return n > 10 ? pc5.red(String(n)) : n > 5 ? pc5.yellow(String(n)) : pc5.dim(String(n));
2877
- } },
2878
- { key: "mem", label: "MEM%", align: "right", format: (v) => {
2879
- const n = v;
2880
- return n > 10 ? pc5.red(String(n)) : n > 5 ? pc5.yellow(String(n)) : pc5.dim(String(n));
2881
- } },
2882
- { key: "state", label: "State", format: (v) => {
2883
- const s = String(v);
2884
- if (s === "R" || s === "Running") return pc5.green(s);
2885
- if (s === "Z" || s === "Zombie") return pc5.red(s);
2886
- if (s === "S" || s === "Sleeping") return pc5.cyan(s);
2887
- return pc5.dim(s);
2888
- } }
2889
- ];
2890
- const rows2 = processes.map((p) => ({ pid: p.pid, name: p.name, cpu: p.cpuPercent, mem: p.memPercent, state: formatProcessState(p.state) }));
2891
- console.error(`
2892
- ${symbols.fox} ${pc5.bold("System Processes")} ${pc5.dim(`(top ${processes.length} by ${sortBy})`)}
2893
- `);
2894
- console.error(renderTable(columns2, rows2));
2895
- console.error();
2896
- } catch (error) {
2897
- spinner.fail("Failed to scan processes");
2898
- console.error(renderError("Process scan failed", String(error)));
2899
3106
  }
2900
- return;
2901
3107
  }
2902
- const tracked = readTracked();
2903
- if (tracked.length === 0) {
2904
- console.error(`
2905
- ${pc5.dim("No tracked processes.")}`);
2906
- console.error(` ${pc5.dim("Start an app with:")} ${pc5.cyan("fennec start <command> --name <name>")}
2907
- `);
2908
- return;
2909
- }
2910
- const columns = [
2911
- { key: "name", label: "App", format: (v) => pc5.bold(String(v)) },
2912
- { key: "pid", label: "PID", align: "right" },
2913
- { key: "status", label: "Status", format: (v) => {
2914
- const s = v;
2915
- return s === "running" ? pc5.green("\u25CF running") : pc5.red("\u25CB stopped");
2916
- } },
2917
- { key: "port", label: "Port", format: (v) => {
2918
- const p = v;
2919
- return p ? pc5.yellow(`:${p}`) : pc5.dim("-");
2920
- } },
2921
- { key: "command", label: "Command", format: (v) => {
2922
- const c = String(v);
2923
- return c.length > 50 ? c.slice(0, 50) + "\u2026" : c;
2924
- } },
2925
- { key: "uptime", label: "Uptime", format: (v) => pc5.dim(String(v)) }
2926
- ];
2927
- const rows = tracked.map((t) => {
2928
- const running = isProcessRunning(t.pid);
2929
- const uptime = running ? formatUptime(Math.floor((Date.now() - new Date(t.startedAt).getTime()) / 1e3)) : "-";
2930
- return { name: t.name, pid: running ? String(t.pid) : pc5.dim(String(t.pid)), status: running ? "running" : "stopped", port: t.port ?? null, command: t.command, uptime };
2931
- });
2932
- const runningCount = tracked.filter((t) => isProcessRunning(t.pid)).length;
2933
- console.error(`
2934
- ${symbols.fox} ${pc5.bold("Fennec Apps")} ${pc5.dim(`(${runningCount}/${tracked.length} running)`)}
2935
- `);
2936
- console.error(renderTable(columns, rows));
2937
- console.error(` ${pc5.dim("Use")} ${pc5.cyan("fennec start <command> --name <name> --port <port>")} ${pc5.dim("to add more apps.")}`);
2938
- console.error(` ${pc5.dim("Use")} ${pc5.cyan("fennec log <name>")} ${pc5.dim("to view logs.")}`);
2939
- console.error(` ${pc5.dim("Use")} ${pc5.cyan("fennec kill <name>")} ${pc5.dim("to stop an app.")}`);
2940
- console.error();
2941
- }
2942
- async function statusCommand(_args) {
2943
- const watchFlag = _args.includes("-w") || _args.includes("--watch");
2944
- const tracked = readTracked();
2945
- const topSystem = getSystemProcesses({ userOnly: true, sortBy: "cpu", limit: 5 });
2946
- const totalUserProcs = getSystemProcesses({ userOnly: true }).length;
2947
- console.error(`
2948
- ${symbols.fox} ${pc5.bold("Fennec Status")}
2949
- `);
2950
- if (tracked.length > 0) {
2951
- const runningCount = tracked.filter((t) => isProcessRunning(t.pid)).length;
2952
- console.error(` ${pc5.bold("Managed Apps")} ${pc5.dim(`(${runningCount}/${tracked.length} running)`)}
2953
- `);
2954
- for (const t of tracked) {
2955
- const running = isProcessRunning(t.pid);
2956
- const statusIcon = running ? pc5.green("\u25CF") : pc5.red("\u25CB");
2957
- const portStr = t.port ? ` ${pc5.yellow(`:${t.port}`)}` : "";
2958
- const uptime = running ? pc5.dim(formatUptime(Math.floor((Date.now() - new Date(t.startedAt).getTime()) / 1e3))) : pc5.red("stopped");
2959
- console.error(` ${statusIcon} ${pc5.bold(t.name)}${portStr} ${pc5.dim(`(PID ${t.pid})`)} \u2014 ${uptime}`);
2960
- }
2961
- console.error();
2962
- } else {
2963
- console.error(` ${pc5.dim("No managed apps.")} ${pc5.cyan("fennec start <command> --name <name>")}
2964
- `);
2965
- }
2966
- console.error(` ${pc5.bold("System")} ${pc5.dim(`(${totalUserProcs} user processes)`)}`);
2967
- for (const p of topSystem) {
2968
- const cpuStr = p.cpuPercent > 10 ? pc5.red(`${p.cpuPercent}%`) : p.cpuPercent > 5 ? pc5.yellow(`${p.cpuPercent}%`) : pc5.dim(`${p.cpuPercent}%`);
2969
- const memStr = p.memPercent > 10 ? pc5.red(`${p.memPercent}%`) : p.memPercent > 5 ? pc5.yellow(`${p.memPercent}%`) : pc5.dim(`${p.memPercent}%`);
2970
- console.error(` ${pc5.dim(`PID ${p.pid}`)} ${pc5.bold(p.name)} \u2014 CPU: ${cpuStr} MEM: ${memStr}`);
2971
- }
2972
- if (watchFlag) await watchSystemProcesses("cpu", 15);
2973
- console.error();
2974
- }
2975
- async function watchSystemProcesses(sortBy, limit) {
2976
- console.error(`
2977
- ${pc5.bold("Watching system processes")} ${pc5.dim("(Ctrl+C to stop, refreshes every 3s)")}
2978
- `);
2979
- const render = () => {
2980
- const processes = getSystemProcesses({ userOnly: true, sortBy, limit });
2981
- const columns = [
2982
- { key: "pid", label: "PID", align: "right", format: (v) => pc5.dim(String(v).padStart(6)) },
2983
- { key: "name", label: "Name", format: (v) => pc5.bold(String(v)) },
2984
- { key: "cpu", label: "CPU%", align: "right", format: (v) => {
2985
- const n = v;
2986
- return n > 10 ? pc5.red(String(n)) : n > 5 ? pc5.yellow(String(n)) : pc5.dim(String(n));
2987
- } },
2988
- { key: "mem", label: "MEM%", align: "right", format: (v) => {
2989
- const n = v;
2990
- return n > 10 ? pc5.red(String(n)) : n > 5 ? pc5.yellow(String(n)) : pc5.dim(String(n));
2991
- } },
2992
- { key: "state", label: "S", align: "center" }
2993
- ];
2994
- const rows = processes.map((p) => ({ pid: p.pid, name: p.name, cpu: p.cpuPercent, mem: p.memPercent, state: p.state }));
2995
- return ` ${timestamp()} ${pc5.dim(`${processes.length} processes`)}
2996
- ${renderTable(columns, rows, { compact: true })}`;
2997
- };
2998
- console.error(render());
2999
- const interval = setInterval(() => {
3000
- process.stdout.write("\x1B[J");
3001
- console.error(render());
3002
- }, 3e3);
3003
- await new Promise((resolve8) => {
3004
- process.once("SIGINT", () => {
3005
- clearInterval(interval);
3006
- process.stdout.write("\x1B[J");
3007
- resolve8();
3008
- });
3009
- });
3010
- }
3011
-
3012
- // src/commands/kill.ts
3013
- import pc6 from "picocolors";
3014
- async function killCommand(args2) {
3015
- const rawTarget = args2[0];
3016
- const signalIndex = args2.indexOf("--signal");
3017
- const signalRaw = signalIndex !== -1 ? args2[signalIndex + 1] : "SIGTERM";
3018
- const signal = signalRaw ?? "SIGTERM";
3019
- if (rawTarget === "all" || args2.includes("--all") || args2.includes("-a")) {
3020
- await killAll(signal);
3021
- return;
3022
- }
3023
- if (!rawTarget) {
3024
- console.error(renderError("Missing target", "Usage: fennec kill <pid|name|all> [--signal SIGTERM|SIGKILL|SIGINT]"));
3025
- process.exit(1);
3026
- }
3027
- let targetPid;
3028
- let displayName;
3029
- const pid = parseInt(rawTarget, 10);
3030
- if (!isNaN(pid) && String(pid) === rawTarget) {
3031
- if (!isProcessRunning(pid)) {
3032
- console.error(renderError("Process not found", `No process with PID ${pid} is running`));
3033
- process.exit(1);
3034
- }
3035
- targetPid = pid;
3036
- displayName = `PID ${pid}`;
3037
- } else {
3038
- const matches = getSystemProcesses({ name: rawTarget, userOnly: true, sortBy: "cpu" });
3039
- if (matches.length === 0) {
3040
- console.error(renderError("Process not found", `No running process matching "${rawTarget}"`));
3041
- process.exit(1);
3042
- }
3043
- if (matches.length > 1) {
3044
- console.error(`
3045
- ${pc6.bold(`Multiple processes match "${rawTarget}":`)}`);
3046
- const selected = await selectPrompt("Select which to kill:", matches.map((p, i) => ({ value: String(i), label: `${p.name} (PID ${p.pid})`, description: `${p.command.slice(0, 80)} \u2014 CPU: ${p.cpuPercent}% MEM: ${p.memPercent}%` })));
3047
- if (selected === null) {
3048
- console.error(` ${pc6.dim("Cancelled")}`);
3049
- return;
3050
- }
3051
- const idx = parseInt(selected, 10);
3052
- targetPid = matches[idx].pid;
3053
- displayName = `${matches[idx].name} (PID ${targetPid})`;
3054
- } else {
3055
- targetPid = matches[0].pid;
3056
- displayName = `${matches[0].name} (PID ${targetPid})`;
3057
- }
3058
- }
3059
- const confirmed = await confirmPrompt(`Kill ${pc6.bold(displayName)} with ${pc6.yellow(signal)}?`, false);
3060
- if (!confirmed) {
3061
- console.error(` ${pc6.dim("Cancelled")}`);
3108
+ const confirmed = await confirmPrompt(`Kill ${pc6.bold(displayName)} with ${pc6.yellow(signal)}?`, false);
3109
+ if (!confirmed) {
3110
+ console.error(` ${pc6.dim("Cancelled")}`);
3062
3111
  return;
3063
3112
  }
3064
3113
  const spinner = createSpinner(`Sending ${signal} to ${displayName}...`);
@@ -3120,12 +3169,295 @@ async function killAll(signal) {
3120
3169
  if (failed > 0) console.error(` ${pc6.yellow(`${failed} process(es) could not be killed`)} ${pc6.dim("(try with sudo)")}`);
3121
3170
  }
3122
3171
 
3123
- // src/commands/restart.ts
3172
+ // src/commands/stop.ts
3124
3173
  import pc7 from "picocolors";
3125
- import { mkdirSync as mkdirSync3, createWriteStream as createWriteStream2 } from "fs";
3174
+ async function stopCommand(args2) {
3175
+ const stopAll = args2.includes("--all") || args2.includes("-a");
3176
+ if (stopAll) {
3177
+ await stopAllTracked();
3178
+ return;
3179
+ }
3180
+ const rawTarget = args2[0];
3181
+ if (!rawTarget) {
3182
+ console.error(renderError("Missing name", "Usage: fennec stop <name|--all>"));
3183
+ process.exit(1);
3184
+ }
3185
+ const tracked = readTracked();
3186
+ const trackedMatch = tracked.find((t) => t.name === rawTarget);
3187
+ if (!trackedMatch) {
3188
+ console.error(renderError("Not found", `No tracked process named "${rawTarget}". Use ${pc7.cyan("fennec kill <pid>")} to kill a system process.`));
3189
+ process.exit(1);
3190
+ }
3191
+ if (!isProcessRunning(trackedMatch.pid)) {
3192
+ console.error(`
3193
+ ${pc7.yellow("\u26A0")} ${pc7.bold(rawTarget)} ${pc7.dim("is already stopped")}`);
3194
+ console.error();
3195
+ process.exit(0);
3196
+ }
3197
+ const displayName = `${trackedMatch.name} (PID ${trackedMatch.pid})`;
3198
+ const confirmed = await confirmPrompt(`Stop ${pc7.bold(displayName)}? ${pc7.dim("It can be re-spawned later via fennec spawn")}`, false);
3199
+ if (!confirmed) {
3200
+ console.error(` ${pc7.dim("Cancelled")}`);
3201
+ return;
3202
+ }
3203
+ await stopSingleProcess(trackedMatch.pid, trackedMatch.name);
3204
+ }
3205
+ async function stopAllTracked() {
3206
+ const tracked = readTracked();
3207
+ const running = tracked.filter((t) => isProcessRunning(t.pid));
3208
+ if (running.length === 0) {
3209
+ console.error(`
3210
+ ${pc7.dim("No running tracked processes to stop.")}
3211
+ `);
3212
+ return;
3213
+ }
3214
+ console.error(`
3215
+ ${pc7.yellow("\u26A0")} ${pc7.bold(`Stop all ${running.length} running process(es)?`)} ${pc7.dim("(They can be re-spawned later)")}
3216
+ `);
3217
+ for (const t of running) {
3218
+ console.error(` ${pc7.green("\u25CF")} ${pc7.bold(t.name)} ${pc7.dim(`(PID ${t.pid})`)}`);
3219
+ }
3220
+ console.error();
3221
+ const confirmed = await confirmPrompt("Stop all?", false);
3222
+ if (!confirmed) {
3223
+ console.error(` ${pc7.dim("Cancelled")}
3224
+ `);
3225
+ return;
3226
+ }
3227
+ const spinner = createSpinner(`Stopping ${running.length} process(es)...`);
3228
+ let stopped = 0;
3229
+ let failed = 0;
3230
+ for (const t of running) {
3231
+ if (killProcess(t.pid, "SIGTERM")) {
3232
+ stopped++;
3233
+ } else {
3234
+ failed++;
3235
+ }
3236
+ }
3237
+ await new Promise((r) => setTimeout(r, 300));
3238
+ spinner.stop();
3239
+ process.stdout.write("\r\x1B[K");
3240
+ if (stopped > 0) {
3241
+ console.error(` ${pc7.green("\u2713")} ${pc7.bold(`Stopped ${stopped} process(es)`)} ${pc7.dim("(kept in tracked.json)")}`);
3242
+ }
3243
+ if (failed > 0) {
3244
+ console.error(` ${pc7.red("\u2717")} ${pc7.bold(`${failed} process(es) failed to stop`)}`);
3245
+ }
3246
+ console.error();
3247
+ await psCommand([]);
3248
+ }
3249
+ async function stopSingleProcess(pid, name) {
3250
+ const displayName = `${name} (PID ${pid})`;
3251
+ const spinner = createSpinner(`Stopping ${displayName}...`);
3252
+ const success = killProcess(pid, "SIGTERM");
3253
+ if (success) {
3254
+ await new Promise((r) => setTimeout(r, 200));
3255
+ const stillRunning = isProcessRunning(pid);
3256
+ if (stillRunning) {
3257
+ spinner.warn(`${displayName} did not respond to SIGTERM`);
3258
+ const forceKill = await confirmPrompt(`Send ${pc7.red("SIGKILL")} to force stop?`, true);
3259
+ if (forceKill) {
3260
+ const forceSpinner = createSpinner(`Sending SIGKILL to ${displayName}...`);
3261
+ killProcess(pid, "SIGKILL");
3262
+ await new Promise((r) => setTimeout(r, 300));
3263
+ isProcessRunning(pid) ? forceSpinner.fail(`${displayName} could not be stopped (permission denied?)`) : forceSpinner.succeed(`${displayName} force stopped`);
3264
+ } else {
3265
+ console.error(` ${pc7.yellow("\u26A0")} ${displayName} ${pc7.dim("may still be running")}`);
3266
+ }
3267
+ } else {
3268
+ spinner.succeed(`${displayName} stopped`);
3269
+ }
3270
+ console.error();
3271
+ await psCommand([]);
3272
+ } else {
3273
+ spinner.fail(`Failed to stop ${displayName}`);
3274
+ console.error(renderError("Permission denied", "Try running with sudo or use fennec kill."));
3275
+ }
3276
+ }
3277
+
3278
+ // src/commands/spawn.ts
3279
+ import pc8 from "picocolors";
3280
+ import { createWriteStream as createWriteStream2, mkdirSync as mkdirSync3 } from "fs";
3126
3281
  import { resolve as resolve4 } from "path";
3127
3282
  import { homedir as homedir3 } from "os";
3128
3283
  import { spawn as spawn2 } from "child_process";
3284
+ async function spawnCommand(args2) {
3285
+ const spawnAll = args2.includes("--all") || args2.includes("-a");
3286
+ const name = args2[0];
3287
+ if (spawnAll) {
3288
+ await spawnAllStopped();
3289
+ return;
3290
+ }
3291
+ if (!name) {
3292
+ await spawnList();
3293
+ return;
3294
+ }
3295
+ const tracked = readTracked();
3296
+ const match = tracked.find((t) => t.name === name);
3297
+ if (!match) {
3298
+ console.error(renderError("Not found", `No tracked process named "${name}". Use ${pc8.cyan("fennec spawn")} to see available processes, or ${pc8.cyan("fennec start <command> --name <name>")} to create a new one.`));
3299
+ process.exit(1);
3300
+ }
3301
+ if (isProcessRunning(match.pid)) {
3302
+ console.error(`
3303
+ ${pc8.yellow("\u26A0")} ${pc8.bold(name)} ${pc8.dim("is already running (PID:")} ${match.pid}${pc8.dim(")")}`);
3304
+ console.error(` ${pc8.dim("Use")} ${pc8.cyan(`fennec stop ${name}`)} ${pc8.dim("to stop it first.")}`);
3305
+ console.error();
3306
+ process.exit(0);
3307
+ }
3308
+ if (!match.command) {
3309
+ console.error(renderError("No command saved", `"${name}" has no saved command and cannot be re-spawned.`));
3310
+ process.exit(1);
3311
+ }
3312
+ await respawnProcess(match);
3313
+ }
3314
+ async function spawnAllStopped() {
3315
+ const tracked = readTracked();
3316
+ const toSpawn = tracked.filter((t) => !isProcessRunning(t.pid) && t.command);
3317
+ if (toSpawn.length === 0) {
3318
+ console.error(`
3319
+ ${pc8.dim("No stopped processes with saved commands to spawn.")}
3320
+ `);
3321
+ return;
3322
+ }
3323
+ console.error(`
3324
+ ${symbols.fox} ${pc8.bold(`Spawning ${toSpawn.length} process(es)...`)}
3325
+ `);
3326
+ for (const t of toSpawn) {
3327
+ console.error(` ${pc8.dim("\xB7")} ${pc8.bold(t.name)} ${pc8.dim(`(${t.command})`)}`);
3328
+ }
3329
+ console.error();
3330
+ const spinner = createSpinner("Spawning...");
3331
+ let spawned = 0;
3332
+ let failed = 0;
3333
+ for (const proc of toSpawn) {
3334
+ try {
3335
+ const cmdParts = proc.command.split(/\s+/);
3336
+ const logDir = resolve4(homedir3(), ".fennec", "logs");
3337
+ mkdirSync3(logDir, { recursive: true });
3338
+ const logFilePath = resolve4(logDir, `${proc.name}.log`);
3339
+ const child = spawn2(cmdParts[0], cmdParts.slice(1), {
3340
+ cwd: proc.cwd,
3341
+ env: { ...process.env },
3342
+ stdio: ["ignore", "pipe", "pipe"],
3343
+ detached: true
3344
+ });
3345
+ const pid = child.pid ?? 0;
3346
+ rotateLogFile(logFilePath);
3347
+ const logStream = createWriteStream2(logFilePath, { flags: "a" });
3348
+ if (child.stdout) child.stdout.pipe(logStream);
3349
+ if (child.stderr) child.stderr.pipe(logStream);
3350
+ child.unref();
3351
+ addTracked({
3352
+ name: proc.name,
3353
+ pid,
3354
+ command: proc.command,
3355
+ port: proc.port,
3356
+ cwd: proc.cwd,
3357
+ startedAt: (/* @__PURE__ */ new Date()).toISOString()
3358
+ });
3359
+ spawned++;
3360
+ } catch {
3361
+ failed++;
3362
+ }
3363
+ }
3364
+ spinner.stop();
3365
+ process.stdout.write("\r\x1B[K");
3366
+ if (spawned > 0) {
3367
+ console.error(` ${pc8.green("\u2713")} ${pc8.bold(`Spawned ${spawned} process(es)`)}`);
3368
+ }
3369
+ if (failed > 0) {
3370
+ console.error(` ${pc8.red("\u2717")} ${pc8.bold(`${failed} process(es) failed to spawn`)}`);
3371
+ }
3372
+ console.error();
3373
+ await psCommand([]);
3374
+ }
3375
+ async function spawnList() {
3376
+ const tracked = readTracked();
3377
+ if (tracked.length === 0) {
3378
+ console.error(`
3379
+ ${pc8.dim("No tracked processes found.")}`);
3380
+ console.error(` ${pc8.dim("Start an app first:")} ${pc8.cyan("fennec start <command> --name <name>")}`);
3381
+ console.error();
3382
+ return;
3383
+ }
3384
+ const stopped = tracked.filter((t) => !isProcessRunning(t.pid));
3385
+ const running = tracked.filter((t) => isProcessRunning(t.pid));
3386
+ if (stopped.length === 0) {
3387
+ console.error(`
3388
+ ${pc8.dim("All tracked processes are running. Nothing to spawn.")}`);
3389
+ console.error();
3390
+ return;
3391
+ }
3392
+ console.error(`
3393
+ ${symbols.fox} ${pc8.bold("Available to spawn")} ${pc8.dim(`(${stopped.length}/${tracked.length} stopped)`)}`);
3394
+ if (running.length > 0) {
3395
+ console.error(` ${pc8.dim("Running (use stop first):")}`);
3396
+ for (const r of running) {
3397
+ console.error(` ${pc8.green("\u25CF")} ${pc8.bold(r.name)} ${pc8.dim(`(PID ${r.pid})`)}`);
3398
+ }
3399
+ console.error();
3400
+ }
3401
+ const selected = await selectPrompt("Select a process to spawn:", stopped.map((t) => ({
3402
+ value: t.name,
3403
+ label: t.name,
3404
+ description: t.command.length > 80 ? t.command.slice(0, 80) + "..." : t.command
3405
+ })));
3406
+ if (selected === null) {
3407
+ console.error(` ${pc8.dim("Cancelled")}`);
3408
+ return;
3409
+ }
3410
+ const match = stopped.find((t) => t.name === selected);
3411
+ if (match) {
3412
+ await respawnProcess(match);
3413
+ }
3414
+ }
3415
+ async function respawnProcess(proc) {
3416
+ const cmdParts = proc.command.split(/\s+/);
3417
+ const logDir = resolve4(homedir3(), ".fennec", "logs");
3418
+ mkdirSync3(logDir, { recursive: true });
3419
+ const logFilePath = resolve4(logDir, `${proc.name}.log`);
3420
+ console.error(`
3421
+ ${symbols.fox} ${pc8.bold("Spawning")} ${renderAppName(proc.name)} ${pc8.dim("(from saved config)")}
3422
+ `);
3423
+ console.error(` ${renderKV("Command", proc.command)}`);
3424
+ if (proc.cwd) console.error(` ${renderKV("Directory", proc.cwd)}`);
3425
+ if (proc.port) console.error(` ${renderKV("Port", String(proc.port))}`);
3426
+ try {
3427
+ const child = spawn2(cmdParts[0], cmdParts.slice(1), {
3428
+ cwd: proc.cwd,
3429
+ env: { ...process.env },
3430
+ stdio: ["ignore", "pipe", "pipe"],
3431
+ detached: true
3432
+ });
3433
+ const pid = child.pid ?? 0;
3434
+ rotateLogFile(logFilePath);
3435
+ const logStream = createWriteStream2(logFilePath, { flags: "a" });
3436
+ if (child.stdout) child.stdout.pipe(logStream);
3437
+ if (child.stderr) child.stderr.pipe(logStream);
3438
+ child.unref();
3439
+ addTracked({
3440
+ name: proc.name,
3441
+ pid,
3442
+ command: proc.command,
3443
+ port: proc.port,
3444
+ cwd: proc.cwd,
3445
+ startedAt: (/* @__PURE__ */ new Date()).toISOString()
3446
+ });
3447
+ console.error(` ${pc8.green("\u2713")} ${pc8.bold(proc.name)} ${pc8.dim(`spawned (PID: ${pid})`)}`);
3448
+ await psCommand([]);
3449
+ } catch (error) {
3450
+ console.error(renderError(`Failed to spawn ${proc.name}`, String(error)));
3451
+ process.exit(1);
3452
+ }
3453
+ }
3454
+
3455
+ // src/commands/restart.ts
3456
+ import pc9 from "picocolors";
3457
+ import { mkdirSync as mkdirSync4, createWriteStream as createWriteStream3 } from "fs";
3458
+ import { resolve as resolve5 } from "path";
3459
+ import { homedir as homedir4 } from "os";
3460
+ import { spawn as spawn3 } from "child_process";
3129
3461
  async function restartCommand(args2) {
3130
3462
  const name = args2[0];
3131
3463
  if (!name) {
@@ -3150,9 +3482,9 @@ async function restartCommand(args2) {
3150
3482
  targetPid = processes[0].pid;
3151
3483
  }
3152
3484
  }
3153
- const confirmed = await confirmPrompt(`Restart ${pc7.bold(`${name} (PID ${targetPid})`)}?`, false);
3485
+ const confirmed = await confirmPrompt(`Restart ${pc9.bold(`${name} (PID ${targetPid})`)}?`, false);
3154
3486
  if (!confirmed) {
3155
- console.error(` ${pc7.dim("Cancelled")}`);
3487
+ console.error(` ${pc9.dim("Cancelled")}`);
3156
3488
  return;
3157
3489
  }
3158
3490
  const spinner = createSpinner(`Stopping PID ${targetPid}...`);
@@ -3172,17 +3504,17 @@ async function restartCommand(args2) {
3172
3504
  const respawnSpinner = createSpinner(`Re-spawning ${trackedEntry.name}...`);
3173
3505
  try {
3174
3506
  const cmdParts = trackedEntry.command.split(" ");
3175
- const logDir = resolve4(homedir3(), ".fennec", "logs");
3176
- mkdirSync3(logDir, { recursive: true });
3177
- const logFilePath = resolve4(logDir, `${trackedEntry.name}.log`);
3178
- const child = spawn2(cmdParts[0], cmdParts.slice(1), {
3507
+ const logDir = resolve5(homedir4(), ".fennec", "logs");
3508
+ mkdirSync4(logDir, { recursive: true });
3509
+ const logFilePath = resolve5(logDir, `${trackedEntry.name}.log`);
3510
+ const child = spawn3(cmdParts[0], cmdParts.slice(1), {
3179
3511
  cwd: trackedEntry.cwd,
3180
3512
  env: { ...process.env },
3181
3513
  stdio: ["ignore", "pipe", "pipe"],
3182
3514
  detached: true
3183
3515
  });
3184
3516
  rotateLogFile(logFilePath);
3185
- const logStream = createWriteStream2(logFilePath, { flags: "a" });
3517
+ const logStream = createWriteStream3(logFilePath, { flags: "a" });
3186
3518
  if (child.stdout) child.stdout.pipe(logStream);
3187
3519
  if (child.stderr) child.stderr.pipe(logStream);
3188
3520
  child.unref();
@@ -3198,35 +3530,41 @@ async function restartCommand(args2) {
3198
3530
  respawnSpinner.succeed(`${trackedEntry.name} restarted (PID: ${child.pid})`);
3199
3531
  } catch (error) {
3200
3532
  respawnSpinner.fail(`Failed to re-spawn ${trackedEntry.name}: ${String(error)}`);
3201
- console.error(` ${pc7.dim("Config preserved. Re-run manually:")} ${renderCommand(trackedEntry.command)}`);
3533
+ console.error(` ${pc9.dim("Config preserved. Re-run manually:")} ${renderCommand(trackedEntry.command)}`);
3202
3534
  }
3203
3535
  } else {
3204
- console.error(` ${pc7.dim("No tracked config found. Re-run manually:")} ${renderCommand(name)}`);
3536
+ console.error(` ${pc9.dim("No tracked config found. Re-run manually:")} ${renderCommand(name)}`);
3205
3537
  }
3206
3538
  }
3207
3539
 
3208
3540
  // src/commands/log.ts
3209
- import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
3210
- import { resolve as resolve5 } from "path";
3211
- import { homedir as homedir4 } from "os";
3212
- import { spawn as spawn3, execSync as execSync2 } from "child_process";
3213
- import pc8 from "picocolors";
3541
+ import { existsSync as existsSync4, readFileSync as readFileSync3, unlinkSync } from "fs";
3542
+ import { resolve as resolve6 } from "path";
3543
+ import { homedir as homedir5 } from "os";
3544
+ import { spawn as spawn4, execSync as execSync2 } from "child_process";
3545
+ import pc10 from "picocolors";
3214
3546
  async function logCommand(args2) {
3215
3547
  const target = args2[0];
3216
3548
  if (!target) {
3217
- console.error(renderError("Missing process name/pid", "Usage: fennec log <name|pid> [--lines N] [-f]"));
3549
+ console.error(renderError("Missing process name/pid", "Usage: fennec log <name|pid> [--lines N] [-f] [--clear] [--level error|warn|info|debug]"));
3218
3550
  process.exit(1);
3219
3551
  }
3552
+ const clearFlag = args2.includes("--clear");
3220
3553
  const linesIndex = args2.indexOf("--lines");
3221
3554
  const lineCount = linesIndex !== -1 ? parseInt(args2[linesIndex + 1], 10) : 30;
3222
3555
  const followFlag = args2.includes("-f") || args2.includes("--follow");
3556
+ const levelFilter = args2.includes("--level") ? args2[args2.indexOf("--level") + 1]?.toLowerCase() : void 0;
3557
+ if (levelFilter && !["error", "warn", "info", "debug"].includes(levelFilter)) {
3558
+ console.error(renderError("Invalid level", `"${levelFilter}" is not a valid level. Use: error, warn, info, debug`));
3559
+ process.exit(1);
3560
+ }
3223
3561
  let logFilePath = null;
3224
3562
  let displayName = target;
3225
3563
  const tracked = readTracked();
3226
3564
  const trackedMatch = tracked.find((t) => t.name === target || parseInt(target, 10) === t.pid && !isNaN(parseInt(target, 10)));
3227
3565
  if (trackedMatch) {
3228
3566
  displayName = trackedMatch.name;
3229
- logFilePath = resolve5(homedir4(), ".fennec", "logs", `${trackedMatch.name}.log`);
3567
+ logFilePath = resolve6(homedir5(), ".fennec", "logs", `${trackedMatch.name}.log`);
3230
3568
  } else {
3231
3569
  const pid = parseInt(target, 10);
3232
3570
  if (!isNaN(pid) && String(pid) === target) {
@@ -3234,11 +3572,29 @@ async function logCommand(args2) {
3234
3572
  if (existsSync4(procPath)) logFilePath = procPath;
3235
3573
  }
3236
3574
  }
3575
+ if (clearFlag) {
3576
+ if (!logFilePath || !existsSync4(logFilePath)) {
3577
+ console.error(`
3578
+ ${pc10.yellow("\u26A0")} ${pc10.dim("No log file found for")} ${pc10.bold(displayName)}
3579
+ `);
3580
+ process.exit(0);
3581
+ }
3582
+ try {
3583
+ unlinkSync(logFilePath);
3584
+ console.error(`
3585
+ ${pc10.green("\u2713")} ${pc10.bold("Log cleared")} ${pc10.dim(`\u2014 ${logFilePath}`)}
3586
+ `);
3587
+ } catch (err) {
3588
+ console.error(renderError("Failed to clear log", String(err)));
3589
+ process.exit(1);
3590
+ }
3591
+ return;
3592
+ }
3237
3593
  const spinner = createSpinner(`Reading logs for ${displayName}...`);
3238
3594
  try {
3239
3595
  let logLines = [];
3240
3596
  if (logFilePath && existsSync4(logFilePath)) {
3241
- const content = readFileSync4(logFilePath, "utf-8");
3597
+ const content = readFileSync3(logFilePath, "utf-8");
3242
3598
  logLines = content.split("\n").filter(Boolean);
3243
3599
  } else {
3244
3600
  const pid = trackedMatch?.pid ?? parseInt(target, 10);
@@ -3253,27 +3609,55 @@ async function logCommand(args2) {
3253
3609
  logLines = ["(no logs available)"];
3254
3610
  }
3255
3611
  }
3612
+ if (levelFilter) {
3613
+ const levelRegex = new RegExp(`\\b(${levelFilter.toUpperCase()})\\b`, "i");
3614
+ logLines = logLines.filter((line) => levelRegex.test(line));
3615
+ }
3256
3616
  spinner.stop();
3257
3617
  process.stdout.write("\r\x1B[K");
3258
3618
  console.error(`
3259
- ${symbols.fox} ${pc8.bold("Logs")} ${renderAppName(displayName)} ${pc8.dim(`(last ${logLines.length} lines)`)}
3260
- `);
3619
+ ${symbols.fox} ${pc10.bold("Logs")} ${renderAppName(displayName)} ${pc10.dim(`(last ${logLines.length} line${logLines.length !== 1 ? "s" : ""})`)}`);
3261
3620
  const sliced = logLines.slice(-lineCount);
3262
3621
  for (const line of sliced) {
3263
3622
  const display = line.length > 300 ? line.slice(0, 300) + "\u2026" : line;
3264
- if (line.toLowerCase().includes("error") || line.toLowerCase().includes("fail")) console.error(` ${pc8.red(display)}`);
3265
- else if (line.toLowerCase().includes("warn")) console.error(` ${pc8.yellow(display)}`);
3266
- else if (line.toLowerCase().includes("info") || line.includes("[")) console.error(` ${pc8.cyan(display)}`);
3267
- else console.error(` ${display}`);
3623
+ const prefix = followFlag ? `${timestamp()} ` : "";
3624
+ if (line.toLowerCase().includes("error") || line.toLowerCase().includes("fail") || line.toLowerCase().includes("fatal")) {
3625
+ console.error(` ${pc10.red(prefix + display)}`);
3626
+ } else if (line.toLowerCase().includes("warn")) {
3627
+ console.error(` ${pc10.yellow(prefix + display)}`);
3628
+ } else if (line.toLowerCase().includes("info") || line.includes("[")) {
3629
+ console.error(` ${pc10.cyan(prefix + display)}`);
3630
+ } else {
3631
+ console.error(` ${prefix}${display}`);
3632
+ }
3268
3633
  }
3269
3634
  if (followFlag && logFilePath) {
3270
- console.error(`
3271
- ${pc8.dim("Following... (Ctrl+C to stop)")}
3635
+ const tailArgs = ["-n", "0", "-f"];
3636
+ if (levelFilter) {
3637
+ const tail = spawn4("tail", ["-n", "0", "-f", logFilePath], { stdio: ["ignore", "pipe", "pipe"] });
3638
+ const grep = spawn4("grep", ["--line-buffered", "-i", levelFilter], { stdio: ["pipe", "inherit", "inherit"] });
3639
+ if (tail.stdout) tail.stdout.pipe(grep.stdin);
3640
+ await new Promise((resolve12) => {
3641
+ tail.on("exit", () => resolve12());
3642
+ process.once("SIGINT", () => {
3643
+ tail.kill("SIGTERM");
3644
+ grep.kill("SIGTERM");
3645
+ resolve12();
3646
+ });
3647
+ });
3648
+ } else {
3649
+ console.error(`
3650
+ ${pc10.dim("Following... (Ctrl+C to stop)")}
3272
3651
  `);
3273
- const tail = spawn3("tail", ["-n", "0", "-f", logFilePath], { stdio: "inherit" });
3274
- tail.on("exit", () => process.exit(0));
3275
- await new Promise(() => {
3276
- });
3652
+ const tail = spawn4("tail", tailArgs.concat([logFilePath]), { stdio: "inherit" });
3653
+ await new Promise((resolve12) => {
3654
+ tail.on("exit", () => resolve12());
3655
+ process.once("SIGINT", () => {
3656
+ tail.kill("SIGTERM");
3657
+ resolve12();
3658
+ });
3659
+ });
3660
+ }
3277
3661
  }
3278
3662
  console.error();
3279
3663
  } catch (error) {
@@ -3283,7 +3667,8 @@ async function logCommand(args2) {
3283
3667
  }
3284
3668
 
3285
3669
  // src/commands/attach.ts
3286
- import pc9 from "picocolors";
3670
+ import { PortDetector as PortDetector3 } from "@plumpslabs/fennec-core";
3671
+ import pc11 from "picocolors";
3287
3672
  async function attachCommand(args2) {
3288
3673
  const port = parseInt(args2[0], 10);
3289
3674
  if (isNaN(port)) {
@@ -3295,14 +3680,13 @@ async function attachCommand(args2) {
3295
3680
  const name = rawName ?? `port-${port}`;
3296
3681
  const spinner = createSpinner(`Attaching to :${port}...`);
3297
3682
  try {
3298
- const { PortDetector } = await import("@plumpslabs/fennec-core");
3299
- const detector = new PortDetector();
3683
+ const detector = new PortDetector3();
3300
3684
  const info = detector.detectByPort(port);
3301
3685
  if (info) {
3302
3686
  spinner.succeed(`Attached to :${port}`);
3303
3687
  console.error(` ${renderKV("Name", renderAppName(name))}`);
3304
3688
  console.error(` ${renderKV("PID", String(info.pid))}`);
3305
- console.error(` ${renderKV("Command", info.command || pc9.dim("unknown"))}`);
3689
+ console.error(` ${renderKV("Command", info.command || pc11.dim("unknown"))}`);
3306
3690
  } else {
3307
3691
  spinner.fail(`No process found on port ${port}`);
3308
3692
  process.exit(1);
@@ -3316,35 +3700,35 @@ async function attachCommand(args2) {
3316
3700
 
3317
3701
  // src/commands/sessions.ts
3318
3702
  import { SessionStore } from "@plumpslabs/fennec-core";
3319
- import pc10 from "picocolors";
3703
+ import pc12 from "picocolors";
3320
3704
  async function sessionsCommand() {
3321
3705
  const store = new SessionStore("./.fennec/sessions");
3322
3706
  const sessions = store.list();
3323
3707
  if (sessions.length === 0) {
3324
3708
  console.error(`
3325
- ${pc10.dim("No saved sessions found.")}
3709
+ ${pc12.dim("No saved sessions found.")}
3326
3710
  `);
3327
3711
  return;
3328
3712
  }
3329
3713
  const columns = [
3330
- { key: "name", label: "Name", format: (v) => pc10.bold(String(v)) },
3714
+ { key: "name", label: "Name", format: (v) => pc12.bold(String(v)) },
3331
3715
  { key: "origin", label: "Origin" },
3332
- { key: "savedAt", label: "Saved", format: (v) => pc10.dim(String(v)) }
3716
+ { key: "savedAt", label: "Saved", format: (v) => pc12.dim(String(v)) }
3333
3717
  ];
3334
3718
  const rows = sessions.map((s) => ({ name: s.name, origin: s.origin, savedAt: new Date(s.savedAt).toLocaleString() }));
3335
3719
  console.error(`
3336
- ${symbols.fox} ${pc10.bold("Saved Sessions")}
3720
+ ${symbols.fox} ${pc12.bold("Saved Sessions")}
3337
3721
  `);
3338
3722
  console.error(renderTable(columns, rows));
3339
- console.error(` ${pc10.dim(`${sessions.length} session(s)`)}
3723
+ console.error(` ${pc12.dim(`${sessions.length} session(s)`)}
3340
3724
  `);
3341
3725
  }
3342
3726
 
3343
3727
  // src/commands/setup.ts
3344
- import pc11 from "picocolors";
3728
+ import pc13 from "picocolors";
3345
3729
  async function setupCommand() {
3346
3730
  console.error(`
3347
- ${symbols.fox} ${pc11.bold("Fennec Setup")}
3731
+ ${symbols.fox} ${pc13.bold("Fennec Setup")}
3348
3732
  `);
3349
3733
  const mcpClient = await selectPrompt("Which MCP client are you using?", [
3350
3734
  { value: "claude", label: "Claude Desktop", description: "Anthropic's AI desktop app" },
@@ -3353,12 +3737,12 @@ async function setupCommand() {
3353
3737
  { value: "other", label: "Other MCP client", description: "Any MCP-compatible client" }
3354
3738
  ]);
3355
3739
  if (!mcpClient) {
3356
- console.error(` ${pc11.dim("Setup cancelled.")}
3740
+ console.error(` ${pc13.dim("Setup cancelled.")}
3357
3741
  `);
3358
3742
  return;
3359
3743
  }
3360
3744
  console.error(`
3361
- ${pc11.green("\u2713")} Selected: ${pc11.bold(mcpClient)}
3745
+ ${pc13.green("\u2713")} Selected: ${pc13.bold(mcpClient)}
3362
3746
  `);
3363
3747
  const configSnippet = `{
3364
3748
  "mcpServers": {
@@ -3368,24 +3752,24 @@ async function setupCommand() {
3368
3752
  }
3369
3753
  }
3370
3754
  }`;
3371
- console.error(` ${pc11.bold("Add this to your MCP client config:")}
3755
+ console.error(` ${pc13.bold("Add this to your MCP client config:")}
3372
3756
  `);
3373
- console.error(` ${pc11.dim("```")}`);
3757
+ console.error(` ${pc13.dim("```")}`);
3374
3758
  console.error(configSnippet.split("\n").map((l) => ` ${l}`).join("\n"));
3375
- console.error(` ${pc11.dim("```")}
3759
+ console.error(` ${pc13.dim("```")}
3376
3760
  `);
3377
- console.error(` ${renderSuccess("Setup complete!")} ${pc11.dim("Run")} ${renderCommand("fennec start")} ${pc11.dim("to begin.")}
3761
+ console.error(` ${renderSuccess("Setup complete!")} ${pc13.dim("Run")} ${renderCommand("fennec start")} ${pc13.dim("to begin.")}
3378
3762
  `);
3379
3763
  }
3380
3764
 
3381
3765
  // src/commands/management.ts
3382
3766
  import { existsSync as existsSync5, writeFileSync as writeFileSync2 } from "fs";
3383
- import { resolve as resolve6 } from "path";
3767
+ import { resolve as resolve7 } from "path";
3384
3768
  import { execSync as execSync3 } from "child_process";
3385
- import pc12 from "picocolors";
3769
+ import pc14 from "picocolors";
3386
3770
  async function installBrowsersCommand() {
3387
3771
  console.error(`
3388
- ${pc12.bold("Installing Browser Engines")}
3772
+ ${pc14.bold("Installing Browser Engines")}
3389
3773
  `);
3390
3774
  const spinner = createSpinner("Installing Chromium...");
3391
3775
  try {
@@ -3393,21 +3777,21 @@ async function installBrowsersCommand() {
3393
3777
  spinner.succeed("Chromium installed successfully");
3394
3778
  } catch {
3395
3779
  spinner.fail("Failed to install Chromium");
3396
- console.error(` ${pc12.yellow("\u2192")} Try running: ${renderCommand("npx playwright install chromium")}`);
3780
+ console.error(` ${pc14.yellow("\u2192")} Try running: ${renderCommand("npx playwright install chromium")}`);
3397
3781
  }
3398
3782
  console.error(`
3399
- ${pc12.green("\u2713")} Browser installation complete.
3783
+ ${pc14.green("\u2713")} Browser installation complete.
3400
3784
  `);
3401
3785
  }
3402
3786
  async function initCommand() {
3403
3787
  console.error(`
3404
- ${pc12.bold("Initialize Fennec Configuration")}
3788
+ ${pc14.bold("Initialize Fennec Configuration")}
3405
3789
  `);
3406
- const configFile = resolve6("./fennec.config.yaml");
3790
+ const configFile = resolve7("./fennec.config.yaml");
3407
3791
  if (existsSync5(configFile)) {
3408
- const overwrite = await confirmPrompt(`${pc12.yellow("fennec.config.yaml")} already exists. Overwrite?`, false);
3792
+ const overwrite = await confirmPrompt(`${pc14.yellow("fennec.config.yaml")} already exists. Overwrite?`, false);
3409
3793
  if (!overwrite) {
3410
- console.error(` ${pc12.dim("Cancelled.")}
3794
+ console.error(` ${pc14.dim("Cancelled.")}
3411
3795
  `);
3412
3796
  return;
3413
3797
  }
@@ -3487,21 +3871,21 @@ logging:
3487
3871
  file: null
3488
3872
  `;
3489
3873
  writeFileSync2(configFile, config, "utf-8");
3490
- spinner.succeed(`Configuration written to ${pc12.bold(configFile)}`);
3874
+ spinner.succeed(`Configuration written to ${pc14.bold(configFile)}`);
3491
3875
  console.error(`
3492
- ${pc12.dim("Edit the file to customize Fennec behavior.")}
3876
+ ${pc14.dim("Edit the file to customize Fennec behavior.")}
3493
3877
  `);
3494
3878
  }
3495
3879
 
3496
3880
  // src/commands/health.ts
3497
3881
  import { existsSync as existsSync6, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
3498
3882
  import { execSync as execSync4 } from "child_process";
3499
- import { resolve as resolve7 } from "path";
3500
- import { homedir as homedir5 } from "os";
3501
- import pc13 from "picocolors";
3883
+ import { resolve as resolve8 } from "path";
3884
+ import { homedir as homedir6 } from "os";
3885
+ import pc15 from "picocolors";
3502
3886
  async function healthCommand() {
3503
3887
  printBanner();
3504
- console.error(` ${pc13.bold("Fennec Health Check")}
3888
+ console.error(` ${pc15.bold("Fennec Health Check")}
3505
3889
  `);
3506
3890
  let adbStatus = "unknown";
3507
3891
  try {
@@ -3511,20 +3895,20 @@ async function healthCommand() {
3511
3895
  stdio: ["ignore", "pipe", "pipe"]
3512
3896
  });
3513
3897
  const version = result.split("\n")[0]?.trim() ?? "installed";
3514
- adbStatus = pc13.green(version);
3898
+ adbStatus = pc15.green(version);
3515
3899
  } catch {
3516
- adbStatus = pc13.dim("not found") + " (optional)";
3900
+ adbStatus = pc15.dim("not found") + " (optional)";
3517
3901
  }
3518
3902
  const tracked = readTracked();
3519
3903
  const runningCount = tracked.filter((t) => isProcessRunning(t.pid)).length;
3520
- const logDir = resolve7(homedir5(), ".fennec", "logs");
3904
+ const logDir = resolve8(homedir6(), ".fennec", "logs");
3521
3905
  let logSize = "0 B";
3522
3906
  try {
3523
3907
  if (existsSync6(logDir)) {
3524
3908
  const files = readdirSync2(logDir);
3525
3909
  let totalBytes = 0;
3526
3910
  for (const f of files) {
3527
- const fStat = statSync2(resolve7(logDir, f));
3911
+ const fStat = statSync2(resolve8(logDir, f));
3528
3912
  totalBytes += fStat.size;
3529
3913
  }
3530
3914
  logSize = totalBytes > 1024 * 1024 ? `${(totalBytes / (1024 * 1024)).toFixed(1)} MB` : `${(totalBytes / 1024).toFixed(1)} KB`;
@@ -3537,7 +3921,7 @@ async function healthCommand() {
3537
3921
  memoryInfo = `${(mem.rss / (1024 * 1024)).toFixed(0)} MB RSS / ${(mem.heapUsed / (1024 * 1024)).toFixed(0)} MB heap`;
3538
3922
  } catch {
3539
3923
  }
3540
- console.error(` ${symbols.fox} ${pc13.bold("System")}
3924
+ console.error(` ${symbols.fox} ${pc15.bold("System")}
3541
3925
  `);
3542
3926
  console.error(` ${renderKV("Node.js", process.version)}`);
3543
3927
  console.error(` ${renderKV("Platform", `${process.platform} ${process.arch}`)}`);
@@ -3545,7 +3929,7 @@ async function healthCommand() {
3545
3929
  console.error(` ${renderKV("ADB", adbStatus)}`);
3546
3930
  console.error(` ${renderKV("PID", String(process.pid))}`);
3547
3931
  console.error();
3548
- console.error(` ${symbols.fox} ${pc13.bold("Processes")}
3932
+ console.error(` ${symbols.fox} ${pc15.bold("Processes")}
3549
3933
  `);
3550
3934
  console.error(` ${renderKV("Tracked", String(tracked.length))}`);
3551
3935
  console.error(` ${renderKV("Running", String(runningCount))}`);
@@ -3553,15 +3937,238 @@ async function healthCommand() {
3553
3937
  console.error();
3554
3938
  const allHealthy = runningCount === tracked.length || tracked.length === 0;
3555
3939
  if (allHealthy) {
3556
- console.error(` ${pc13.green("\u2713")} ${pc13.bold("All systems healthy")}
3940
+ console.error(` ${pc15.green("\u2713")} ${pc15.bold("All systems healthy")}
3557
3941
  `);
3558
3942
  } else {
3559
3943
  const stopped = tracked.length - runningCount;
3560
- console.error(` ${pc13.yellow("\u26A0")} ${pc13.bold(`${stopped} process(es) stopped`)} ${pc13.dim("fennec ps")}
3944
+ console.error(` ${pc15.yellow("\u26A0")} ${pc15.bold(`${stopped} process(es) stopped`)} ${pc15.dim("fennec ps")}
3561
3945
  `);
3562
3946
  }
3563
3947
  }
3564
3948
 
3949
+ // src/commands/cleanup.ts
3950
+ import pc16 from "picocolors";
3951
+ async function cleanupCommand() {
3952
+ const tracked = readTracked();
3953
+ if (tracked.length === 0) {
3954
+ console.error(`
3955
+ ${pc16.dim("No tracked processes to clean up.")}
3956
+ `);
3957
+ return;
3958
+ }
3959
+ const toRemove = tracked.filter((t) => !isProcessRunning(t.pid) && !t.command);
3960
+ if (toRemove.length === 0) {
3961
+ console.error(`
3962
+ ${pc16.green("\u2713")} ${pc16.bold("All clean")} ${pc16.dim("\u2014 no dead entries without saved commands.")}
3963
+ `);
3964
+ return;
3965
+ }
3966
+ console.error(`
3967
+ ${pc16.yellow("\u26A0")} ${pc16.bold(`Found ${toRemove.length} dead entr${toRemove.length > 1 ? "ies" : "y"} without saved commands`)}
3968
+ `);
3969
+ for (const entry of toRemove) {
3970
+ console.error(` ${pc16.dim("\xB7")} ${pc16.bold(entry.name)} ${pc16.dim(`(PID ${entry.pid})`)}`);
3971
+ }
3972
+ console.error();
3973
+ const confirmed = await confirmPrompt(`Remove ${toRemove.length} entr${toRemove.length > 1 ? "ies" : "y"}?`, false);
3974
+ if (!confirmed) {
3975
+ console.error(` ${pc16.dim("Cancelled")}
3976
+ `);
3977
+ return;
3978
+ }
3979
+ const remaining = tracked.filter((t) => !toRemove.includes(t));
3980
+ saveTracked(remaining);
3981
+ console.error(`
3982
+ ${pc16.green("\u2713")} ${pc16.bold(`Removed ${toRemove.length} entr${toRemove.length > 1 ? "ies" : "y"}`)} ${pc16.dim(`(${remaining.length} remaining)`)}
3983
+ `);
3984
+ }
3985
+
3986
+ // src/commands/info.ts
3987
+ import pc17 from "picocolors";
3988
+ import { resolve as resolve9 } from "path";
3989
+ import { homedir as homedir7 } from "os";
3990
+ async function infoCommand(args2) {
3991
+ const name = args2[0];
3992
+ if (!name) {
3993
+ console.error(renderError("Missing name", "Usage: fennec info <name>"));
3994
+ process.exit(1);
3995
+ }
3996
+ const tracked = readTracked();
3997
+ const match = tracked.find((t) => t.name === name);
3998
+ if (!match) {
3999
+ console.error(renderError("Not found", `No tracked process named "${name}".`));
4000
+ process.exit(1);
4001
+ }
4002
+ const running = isProcessRunning(match.pid);
4003
+ const statusIcon = running ? pc17.green("\u25CF") : pc17.red("\u25CB");
4004
+ const statusText = running ? pc17.green("running") : pc17.red("stopped");
4005
+ const uptime = running ? formatUptime(Math.floor((Date.now() - new Date(match.startedAt).getTime()) / 1e3)) : "-";
4006
+ const logPath = resolve9(homedir7(), ".fennec", "logs", `${match.name}.log`);
4007
+ console.error(`
4008
+ ${symbols.fox} ${pc17.bold(match.name)} ${pc17.dim("\u2014 Process Info")}
4009
+ `);
4010
+ console.error(` ${renderKVColor("Name", match.name, pc17.bold)}`);
4011
+ console.error(` ${renderKVColor("Status", `${statusIcon} ${statusText}`)}`);
4012
+ console.error(` ${renderKVColor("PID", String(match.pid))}`);
4013
+ console.error(` ${renderKVColor("Port", match.port ? String(match.port) : "-")}`);
4014
+ console.error(` ${renderKVColor("Command", match.command || pc17.dim("(none)"))}`);
4015
+ if (match.cwd) console.error(` ${renderKVColor("CWD", match.cwd)}`);
4016
+ console.error(` ${renderKVColor("Started", new Date(match.startedAt).toLocaleString())}`);
4017
+ console.error(` ${renderKVColor("Uptime", uptime)}`);
4018
+ console.error(` ${renderKVColor("Log Path", logPath)}`);
4019
+ if (!running && match.command) {
4020
+ console.error(`
4021
+ ${pc17.dim("Re-spawn with:")} ${pc17.cyan(`fennec spawn ${match.name}`)}`);
4022
+ }
4023
+ console.error();
4024
+ }
4025
+
4026
+ // src/commands/rename.ts
4027
+ import pc18 from "picocolors";
4028
+ import { existsSync as existsSync7, renameSync as renameSync2 } from "fs";
4029
+ import { resolve as resolve10 } from "path";
4030
+ import { homedir as homedir8 } from "os";
4031
+ async function renameCommand(args2) {
4032
+ const [oldName, newName] = args2;
4033
+ if (!oldName || !newName) {
4034
+ console.error(renderError("Missing arguments", "Usage: fennec rename <old-name> <new-name>"));
4035
+ process.exit(1);
4036
+ }
4037
+ if (oldName === newName) {
4038
+ console.error(`
4039
+ ${pc18.yellow("\u26A0")} ${pc18.dim("Old and new names are the same.")}
4040
+ `);
4041
+ process.exit(0);
4042
+ }
4043
+ const tracked = readTracked();
4044
+ const match = tracked.find((t) => t.name === oldName);
4045
+ if (!match) {
4046
+ console.error(renderError("Not found", `No tracked process named "${oldName}".`));
4047
+ process.exit(1);
4048
+ }
4049
+ if (tracked.some((t) => t.name === newName)) {
4050
+ console.error(renderError("Name taken", `A process named "${newName}" already exists.`));
4051
+ process.exit(1);
4052
+ }
4053
+ console.error(`
4054
+ ${symbols.fox} ${pc18.bold("Rename Process")}
4055
+ `);
4056
+ console.error(` ${renderKVColor("From", oldName)}`);
4057
+ console.error(` ${renderKVColor("To", newName)}`);
4058
+ if (isProcessRunning(match.pid)) {
4059
+ console.error(` ${pc18.yellow("\u26A0")} ${pc18.dim("Process is still running \u2014 log entries will go to the old file until stopped.")}`);
4060
+ }
4061
+ console.error();
4062
+ const confirmed = await confirmPrompt(`Rename ${pc18.bold(oldName)} ${pc18.dim("\u2192")} ${pc18.bold(newName)}?`, true);
4063
+ if (!confirmed) {
4064
+ console.error(` ${pc18.dim("Cancelled")}
4065
+ `);
4066
+ return;
4067
+ }
4068
+ const spinner = createSpinner(`Renaming ${oldName} \u2192 ${newName}...`);
4069
+ const logDir = resolve10(homedir8(), ".fennec", "logs");
4070
+ const oldLog = resolve10(logDir, `${oldName}.log`);
4071
+ const newLog = resolve10(logDir, `${newName}.log`);
4072
+ if (existsSync7(oldLog) && !existsSync7(newLog)) {
4073
+ try {
4074
+ renameSync2(oldLog, newLog);
4075
+ } catch {
4076
+ }
4077
+ }
4078
+ match.name = newName;
4079
+ saveTracked(tracked);
4080
+ spinner.succeed(`${pc18.bold(oldName)} ${pc18.dim("\u2192")} ${pc18.bold(newName)}`);
4081
+ console.error();
4082
+ }
4083
+
4084
+ // src/commands/export-import.ts
4085
+ import pc19 from "picocolors";
4086
+ import { existsSync as existsSync8, writeFileSync as writeFileSync3, readFileSync as readFileSync4 } from "fs";
4087
+ import { resolve as resolve11 } from "path";
4088
+ async function exportCommand(args2) {
4089
+ const tracked = readTracked();
4090
+ const fileIndex = args2.indexOf("--file");
4091
+ const filePath = fileIndex !== -1 ? args2[fileIndex + 1] : void 0;
4092
+ if (fileIndex !== -1 && !filePath) {
4093
+ console.error(renderError("Missing file path", "Usage: fennec export --file <path>"));
4094
+ process.exit(1);
4095
+ }
4096
+ if (tracked.length === 0) {
4097
+ console.error(`
4098
+ ${pc19.dim("No tracked processes to export.")}
4099
+ `);
4100
+ return;
4101
+ }
4102
+ const json = JSON.stringify(tracked, null, 2);
4103
+ if (filePath) {
4104
+ try {
4105
+ writeFileSync3(resolve11(filePath), json, "utf-8");
4106
+ console.error(`
4107
+ ${pc19.green("\u2713")} ${pc19.bold(`Exported ${tracked.length} process(es)`)} ${pc19.dim(`\u2192 ${filePath}`)}
4108
+ `);
4109
+ } catch (err) {
4110
+ console.error(renderError("Export failed", String(err)));
4111
+ process.exit(1);
4112
+ }
4113
+ } else {
4114
+ console.log(json);
4115
+ }
4116
+ }
4117
+ async function importCommand(args2) {
4118
+ const filePath = args2[0];
4119
+ if (!filePath) {
4120
+ console.error(renderError("Missing file", "Usage: fennec import <file>"));
4121
+ process.exit(1);
4122
+ }
4123
+ const resolvedPath = resolve11(filePath);
4124
+ if (!existsSync8(resolvedPath)) {
4125
+ console.error(renderError("File not found", `"${filePath}" does not exist.`));
4126
+ process.exit(1);
4127
+ }
4128
+ let imported;
4129
+ try {
4130
+ imported = JSON.parse(readFileSync4(resolvedPath, "utf-8"));
4131
+ if (!Array.isArray(imported) || imported.length === 0) {
4132
+ console.error(renderError("Invalid file", "File must contain a non-empty JSON array of process objects."));
4133
+ process.exit(1);
4134
+ }
4135
+ } catch (err) {
4136
+ console.error(renderError("Invalid JSON", String(err)));
4137
+ process.exit(1);
4138
+ }
4139
+ const existing = readTracked();
4140
+ console.error(`
4141
+ ${symbols.fox} ${pc19.bold("Import Processes")}
4142
+ `);
4143
+ console.error(` ${renderKVColor("File", resolvedPath)}`);
4144
+ console.error(` ${renderKVColor("Importing", `${imported.length} process(es)`)}`);
4145
+ console.error(` ${renderKVColor("Existing", `${existing.length} process(es)`)}`);
4146
+ console.error(`
4147
+ ${pc19.bold("Processes to import:")}`);
4148
+ for (const p of imported) {
4149
+ console.error(` ${pc19.green("+")} ${pc19.bold(p.name)} ${pc19.dim(`(${p.command})`)}`);
4150
+ }
4151
+ console.error();
4152
+ const confirmed = await confirmPrompt("Merge into tracked.json? Existing processes with the same name will be overwritten.", true);
4153
+ if (!confirmed) {
4154
+ console.error(` ${pc19.dim("Cancelled")}
4155
+ `);
4156
+ return;
4157
+ }
4158
+ const merged = [...existing];
4159
+ for (const proc of imported) {
4160
+ const idx = merged.findIndex((t) => t.name === proc.name);
4161
+ if (idx !== -1) {
4162
+ merged[idx] = { ...merged[idx], ...proc };
4163
+ } else {
4164
+ merged.push(proc);
4165
+ }
4166
+ }
4167
+ saveTracked(merged);
4168
+ console.error(` ${pc19.green("\u2713")} ${pc19.bold(`Imported ${imported.length} process(es)`)} ${pc19.dim(`(${merged.length} total)`)}
4169
+ `);
4170
+ }
4171
+
3565
4172
  // src/index.ts
3566
4173
  var [, , command, ...args] = process.argv;
3567
4174
  async function main() {
@@ -3581,10 +4188,26 @@ async function main() {
3581
4188
  await statusCommand(args);
3582
4189
  } else if (command === "log") {
3583
4190
  await logCommand(args);
4191
+ } else if (command === "spawn") {
4192
+ await spawnCommand(args);
4193
+ } else if (command === "stop") {
4194
+ await stopCommand(args);
3584
4195
  } else if (command === "kill") {
3585
4196
  await killCommand(args);
3586
4197
  } else if (command === "restart") {
3587
4198
  await restartCommand(args);
4199
+ } else if (command === "info") {
4200
+ printBanner();
4201
+ await infoCommand(args);
4202
+ } else if (command === "cleanup") {
4203
+ printBanner();
4204
+ await cleanupCommand();
4205
+ } else if (command === "rename") {
4206
+ await renameCommand(args);
4207
+ } else if (command === "export") {
4208
+ await exportCommand(args);
4209
+ } else if (command === "import") {
4210
+ await importCommand(args);
3588
4211
  } else if (command === "attach") {
3589
4212
  await attachCommand(args);
3590
4213
  } else if (command === "pipe") {