@plumpslabs/fennec-cli 1.12.3 → 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.3";
2094
+ var VERSION = "1.13.0";
2092
2095
  var cachedBanner = null;
2093
2096
  var cachedCompact = null;
2094
2097
  function generateBanner() {
@@ -2681,8 +2684,13 @@ import pc4 from "picocolors";
2681
2684
  async function psCommand(args2) {
2682
2685
  const watchFlag = args2.includes("-w") || args2.includes("--watch");
2683
2686
  const systemFlag = args2.includes("--system") || args2.includes("-a") || args2.includes("--all");
2687
+ const jsonFlag = args2.includes("--json");
2684
2688
  const nameFilter = args2.includes("--name") ? args2[args2.indexOf("--name") + 1] : void 0;
2685
2689
  const sortBy = args2.includes("--sort") ? args2[args2.indexOf("--sort") + 1] : "name";
2690
+ if (jsonFlag) {
2691
+ await psJson();
2692
+ return;
2693
+ }
2686
2694
  if (watchFlag && systemFlag) {
2687
2695
  await watchSystemProcesses(sortBy, 15);
2688
2696
  return;
@@ -2767,9 +2775,28 @@ async function psCommand(args2) {
2767
2775
  console.error(renderTable(columns, rows));
2768
2776
  console.error(` ${pc4.dim("Use")} ${pc4.cyan("fennec start <command> --name <name> --port <port>")} ${pc4.dim("to add more apps.")}`);
2769
2777
  console.error(` ${pc4.dim("Use")} ${pc4.cyan("fennec log <name>")} ${pc4.dim("to view logs.")}`);
2770
- console.error(` ${pc4.dim("Use")} ${pc4.cyan("fennec kill <name>")} ${pc4.dim("to stop an app.")}`);
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.")}`);
2771
2781
  console.error();
2772
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
+ }
2773
2800
  async function statusCommand(_args) {
2774
2801
  const watchFlag = _args.includes("-w") || _args.includes("--watch");
2775
2802
  const tracked = readTracked();
@@ -2831,11 +2858,11 @@ ${renderTable(columns, rows, { compact: true })}`;
2831
2858
  process.stdout.write("\x1B[J");
2832
2859
  console.error(render());
2833
2860
  }, 3e3);
2834
- await new Promise((resolve8) => {
2861
+ await new Promise((resolve12) => {
2835
2862
  process.once("SIGINT", () => {
2836
2863
  clearInterval(interval);
2837
2864
  process.stdout.write("\x1B[J");
2838
- resolve8();
2865
+ resolve12();
2839
2866
  });
2840
2867
  });
2841
2868
  }
@@ -2876,12 +2903,13 @@ async function runCommand(args2) {
2876
2903
  const port = portIndex !== -1 ? parseInt(args2[portIndex + 1], 10) : void 0;
2877
2904
  const cwdIndex = args2.indexOf("--cwd");
2878
2905
  const cwd = cwdIndex !== -1 ? args2[cwdIndex + 1] : void 0;
2906
+ const restartFlag = args2.includes("--restart");
2879
2907
  const stopFlags = [nameIndex, portIndex, cwdIndex].filter((i) => i !== -1);
2880
2908
  const cmdEnd = Math.min(...stopFlags, Infinity);
2881
2909
  const cmdParts = args2.slice(0, cmdEnd);
2882
2910
  const cmd = cmdParts.join(" ");
2883
2911
  if (!cmd) {
2884
- 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]"));
2885
2913
  process.exit(1);
2886
2914
  }
2887
2915
  const appName = name ?? cmdParts[0] ?? "app";
@@ -2904,20 +2932,21 @@ async function runCommand(args2) {
2904
2932
  console.error(` ${renderKV("Command", cmd)}`);
2905
2933
  if (port) console.error(` ${renderKV("Port", String(port))}`);
2906
2934
  if (cwd) console.error(` ${renderKV("Directory", cwd)}`);
2935
+ if (restartFlag) console.error(` ${renderKV("Auto-restart", pc5.green("enabled"))}`);
2907
2936
  console.error(` ${divider()}`);
2908
2937
  try {
2909
- const child = spawn(cmdParts[0], cmdParts.slice(1), {
2938
+ let currentChild = spawn(cmdParts[0], cmdParts.slice(1), {
2910
2939
  cwd,
2911
2940
  env: { ...process.env },
2912
2941
  stdio: ["ignore", "pipe", "pipe"],
2913
2942
  detached: true
2914
2943
  });
2915
- const pid = child.pid ?? 0;
2944
+ const pid = currentChild.pid ?? 0;
2916
2945
  rotateLogFile(logFilePath);
2917
2946
  const logStream = createWriteStream(logFilePath, { flags: "a" });
2918
- if (child.stdout) child.stdout.pipe(logStream);
2919
- if (child.stderr) child.stderr.pipe(logStream);
2920
- child.unref();
2947
+ if (currentChild.stdout) currentChild.stdout.pipe(logStream);
2948
+ if (currentChild.stderr) currentChild.stderr.pipe(logStream);
2949
+ currentChild.unref();
2921
2950
  addTracked({
2922
2951
  name: appName,
2923
2952
  pid,
@@ -2927,7 +2956,44 @@ async function runCommand(args2) {
2927
2956
  startedAt: (/* @__PURE__ */ new Date()).toISOString()
2928
2957
  });
2929
2958
  console.error(` ${pc5.green("\u2713")} ${pc5.bold(appName)} ${pc5.dim(`started (PID: ${pid})`)}`);
2930
- await psCommand([]);
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);
2980
+ }
2981
+ });
2982
+ };
2983
+ var setupRestartWatch = setupRestartWatch2;
2984
+ console.error(` ${pc5.dim("Auto-restart enabled \u2014 watching for crashes...")}
2985
+ `);
2986
+ setupRestartWatch2(currentChild);
2987
+ await new Promise((resolve12) => {
2988
+ process.once("SIGINT", () => {
2989
+ currentChild.kill("SIGTERM");
2990
+ resolve12();
2991
+ });
2992
+ });
2993
+ }
2994
+ if (!restartFlag) {
2995
+ await psCommand([]);
2996
+ }
2931
2997
  } catch (error) {
2932
2998
  console.error(renderError(`Failed to start ${appName}`, String(error)));
2933
2999
  process.exit(1);
@@ -3103,12 +3169,295 @@ async function killAll(signal) {
3103
3169
  if (failed > 0) console.error(` ${pc6.yellow(`${failed} process(es) could not be killed`)} ${pc6.dim("(try with sudo)")}`);
3104
3170
  }
3105
3171
 
3106
- // src/commands/restart.ts
3172
+ // src/commands/stop.ts
3107
3173
  import pc7 from "picocolors";
3108
- 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";
3109
3281
  import { resolve as resolve4 } from "path";
3110
3282
  import { homedir as homedir3 } from "os";
3111
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";
3112
3461
  async function restartCommand(args2) {
3113
3462
  const name = args2[0];
3114
3463
  if (!name) {
@@ -3133,9 +3482,9 @@ async function restartCommand(args2) {
3133
3482
  targetPid = processes[0].pid;
3134
3483
  }
3135
3484
  }
3136
- const confirmed = await confirmPrompt(`Restart ${pc7.bold(`${name} (PID ${targetPid})`)}?`, false);
3485
+ const confirmed = await confirmPrompt(`Restart ${pc9.bold(`${name} (PID ${targetPid})`)}?`, false);
3137
3486
  if (!confirmed) {
3138
- console.error(` ${pc7.dim("Cancelled")}`);
3487
+ console.error(` ${pc9.dim("Cancelled")}`);
3139
3488
  return;
3140
3489
  }
3141
3490
  const spinner = createSpinner(`Stopping PID ${targetPid}...`);
@@ -3155,17 +3504,17 @@ async function restartCommand(args2) {
3155
3504
  const respawnSpinner = createSpinner(`Re-spawning ${trackedEntry.name}...`);
3156
3505
  try {
3157
3506
  const cmdParts = trackedEntry.command.split(" ");
3158
- const logDir = resolve4(homedir3(), ".fennec", "logs");
3159
- mkdirSync3(logDir, { recursive: true });
3160
- const logFilePath = resolve4(logDir, `${trackedEntry.name}.log`);
3161
- 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), {
3162
3511
  cwd: trackedEntry.cwd,
3163
3512
  env: { ...process.env },
3164
3513
  stdio: ["ignore", "pipe", "pipe"],
3165
3514
  detached: true
3166
3515
  });
3167
3516
  rotateLogFile(logFilePath);
3168
- const logStream = createWriteStream2(logFilePath, { flags: "a" });
3517
+ const logStream = createWriteStream3(logFilePath, { flags: "a" });
3169
3518
  if (child.stdout) child.stdout.pipe(logStream);
3170
3519
  if (child.stderr) child.stderr.pipe(logStream);
3171
3520
  child.unref();
@@ -3181,35 +3530,41 @@ async function restartCommand(args2) {
3181
3530
  respawnSpinner.succeed(`${trackedEntry.name} restarted (PID: ${child.pid})`);
3182
3531
  } catch (error) {
3183
3532
  respawnSpinner.fail(`Failed to re-spawn ${trackedEntry.name}: ${String(error)}`);
3184
- 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)}`);
3185
3534
  }
3186
3535
  } else {
3187
- 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)}`);
3188
3537
  }
3189
3538
  }
3190
3539
 
3191
3540
  // src/commands/log.ts
3192
- import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
3193
- import { resolve as resolve5 } from "path";
3194
- import { homedir as homedir4 } from "os";
3195
- import { spawn as spawn3, execSync as execSync2 } from "child_process";
3196
- 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";
3197
3546
  async function logCommand(args2) {
3198
3547
  const target = args2[0];
3199
3548
  if (!target) {
3200
- 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]"));
3201
3550
  process.exit(1);
3202
3551
  }
3552
+ const clearFlag = args2.includes("--clear");
3203
3553
  const linesIndex = args2.indexOf("--lines");
3204
3554
  const lineCount = linesIndex !== -1 ? parseInt(args2[linesIndex + 1], 10) : 30;
3205
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
+ }
3206
3561
  let logFilePath = null;
3207
3562
  let displayName = target;
3208
3563
  const tracked = readTracked();
3209
3564
  const trackedMatch = tracked.find((t) => t.name === target || parseInt(target, 10) === t.pid && !isNaN(parseInt(target, 10)));
3210
3565
  if (trackedMatch) {
3211
3566
  displayName = trackedMatch.name;
3212
- logFilePath = resolve5(homedir4(), ".fennec", "logs", `${trackedMatch.name}.log`);
3567
+ logFilePath = resolve6(homedir5(), ".fennec", "logs", `${trackedMatch.name}.log`);
3213
3568
  } else {
3214
3569
  const pid = parseInt(target, 10);
3215
3570
  if (!isNaN(pid) && String(pid) === target) {
@@ -3217,6 +3572,24 @@ async function logCommand(args2) {
3217
3572
  if (existsSync4(procPath)) logFilePath = procPath;
3218
3573
  }
3219
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
+ }
3220
3593
  const spinner = createSpinner(`Reading logs for ${displayName}...`);
3221
3594
  try {
3222
3595
  let logLines = [];
@@ -3236,31 +3609,55 @@ async function logCommand(args2) {
3236
3609
  logLines = ["(no logs available)"];
3237
3610
  }
3238
3611
  }
3612
+ if (levelFilter) {
3613
+ const levelRegex = new RegExp(`\\b(${levelFilter.toUpperCase()})\\b`, "i");
3614
+ logLines = logLines.filter((line) => levelRegex.test(line));
3615
+ }
3239
3616
  spinner.stop();
3240
3617
  process.stdout.write("\r\x1B[K");
3241
3618
  console.error(`
3242
- ${symbols.fox} ${pc8.bold("Logs")} ${renderAppName(displayName)} ${pc8.dim(`(last ${logLines.length} lines)`)}
3243
- `);
3619
+ ${symbols.fox} ${pc10.bold("Logs")} ${renderAppName(displayName)} ${pc10.dim(`(last ${logLines.length} line${logLines.length !== 1 ? "s" : ""})`)}`);
3244
3620
  const sliced = logLines.slice(-lineCount);
3245
3621
  for (const line of sliced) {
3246
3622
  const display = line.length > 300 ? line.slice(0, 300) + "\u2026" : line;
3247
- if (line.toLowerCase().includes("error") || line.toLowerCase().includes("fail")) console.error(` ${pc8.red(display)}`);
3248
- else if (line.toLowerCase().includes("warn")) console.error(` ${pc8.yellow(display)}`);
3249
- else if (line.toLowerCase().includes("info") || line.includes("[")) console.error(` ${pc8.cyan(display)}`);
3250
- 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
+ }
3251
3633
  }
3252
3634
  if (followFlag && logFilePath) {
3253
- console.error(`
3254
- ${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)")}
3255
3651
  `);
3256
- const tail = spawn3("tail", ["-n", "0", "-f", logFilePath], { stdio: "inherit" });
3257
- await new Promise((resolve8) => {
3258
- tail.on("exit", () => resolve8());
3259
- process.once("SIGINT", () => {
3260
- tail.kill("SIGTERM");
3261
- resolve8();
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
+ });
3262
3659
  });
3263
- });
3660
+ }
3264
3661
  }
3265
3662
  console.error();
3266
3663
  } catch (error) {
@@ -3271,7 +3668,7 @@ async function logCommand(args2) {
3271
3668
 
3272
3669
  // src/commands/attach.ts
3273
3670
  import { PortDetector as PortDetector3 } from "@plumpslabs/fennec-core";
3274
- import pc9 from "picocolors";
3671
+ import pc11 from "picocolors";
3275
3672
  async function attachCommand(args2) {
3276
3673
  const port = parseInt(args2[0], 10);
3277
3674
  if (isNaN(port)) {
@@ -3289,7 +3686,7 @@ async function attachCommand(args2) {
3289
3686
  spinner.succeed(`Attached to :${port}`);
3290
3687
  console.error(` ${renderKV("Name", renderAppName(name))}`);
3291
3688
  console.error(` ${renderKV("PID", String(info.pid))}`);
3292
- console.error(` ${renderKV("Command", info.command || pc9.dim("unknown"))}`);
3689
+ console.error(` ${renderKV("Command", info.command || pc11.dim("unknown"))}`);
3293
3690
  } else {
3294
3691
  spinner.fail(`No process found on port ${port}`);
3295
3692
  process.exit(1);
@@ -3303,35 +3700,35 @@ async function attachCommand(args2) {
3303
3700
 
3304
3701
  // src/commands/sessions.ts
3305
3702
  import { SessionStore } from "@plumpslabs/fennec-core";
3306
- import pc10 from "picocolors";
3703
+ import pc12 from "picocolors";
3307
3704
  async function sessionsCommand() {
3308
3705
  const store = new SessionStore("./.fennec/sessions");
3309
3706
  const sessions = store.list();
3310
3707
  if (sessions.length === 0) {
3311
3708
  console.error(`
3312
- ${pc10.dim("No saved sessions found.")}
3709
+ ${pc12.dim("No saved sessions found.")}
3313
3710
  `);
3314
3711
  return;
3315
3712
  }
3316
3713
  const columns = [
3317
- { key: "name", label: "Name", format: (v) => pc10.bold(String(v)) },
3714
+ { key: "name", label: "Name", format: (v) => pc12.bold(String(v)) },
3318
3715
  { key: "origin", label: "Origin" },
3319
- { key: "savedAt", label: "Saved", format: (v) => pc10.dim(String(v)) }
3716
+ { key: "savedAt", label: "Saved", format: (v) => pc12.dim(String(v)) }
3320
3717
  ];
3321
3718
  const rows = sessions.map((s) => ({ name: s.name, origin: s.origin, savedAt: new Date(s.savedAt).toLocaleString() }));
3322
3719
  console.error(`
3323
- ${symbols.fox} ${pc10.bold("Saved Sessions")}
3720
+ ${symbols.fox} ${pc12.bold("Saved Sessions")}
3324
3721
  `);
3325
3722
  console.error(renderTable(columns, rows));
3326
- console.error(` ${pc10.dim(`${sessions.length} session(s)`)}
3723
+ console.error(` ${pc12.dim(`${sessions.length} session(s)`)}
3327
3724
  `);
3328
3725
  }
3329
3726
 
3330
3727
  // src/commands/setup.ts
3331
- import pc11 from "picocolors";
3728
+ import pc13 from "picocolors";
3332
3729
  async function setupCommand() {
3333
3730
  console.error(`
3334
- ${symbols.fox} ${pc11.bold("Fennec Setup")}
3731
+ ${symbols.fox} ${pc13.bold("Fennec Setup")}
3335
3732
  `);
3336
3733
  const mcpClient = await selectPrompt("Which MCP client are you using?", [
3337
3734
  { value: "claude", label: "Claude Desktop", description: "Anthropic's AI desktop app" },
@@ -3340,12 +3737,12 @@ async function setupCommand() {
3340
3737
  { value: "other", label: "Other MCP client", description: "Any MCP-compatible client" }
3341
3738
  ]);
3342
3739
  if (!mcpClient) {
3343
- console.error(` ${pc11.dim("Setup cancelled.")}
3740
+ console.error(` ${pc13.dim("Setup cancelled.")}
3344
3741
  `);
3345
3742
  return;
3346
3743
  }
3347
3744
  console.error(`
3348
- ${pc11.green("\u2713")} Selected: ${pc11.bold(mcpClient)}
3745
+ ${pc13.green("\u2713")} Selected: ${pc13.bold(mcpClient)}
3349
3746
  `);
3350
3747
  const configSnippet = `{
3351
3748
  "mcpServers": {
@@ -3355,24 +3752,24 @@ async function setupCommand() {
3355
3752
  }
3356
3753
  }
3357
3754
  }`;
3358
- console.error(` ${pc11.bold("Add this to your MCP client config:")}
3755
+ console.error(` ${pc13.bold("Add this to your MCP client config:")}
3359
3756
  `);
3360
- console.error(` ${pc11.dim("```")}`);
3757
+ console.error(` ${pc13.dim("```")}`);
3361
3758
  console.error(configSnippet.split("\n").map((l) => ` ${l}`).join("\n"));
3362
- console.error(` ${pc11.dim("```")}
3759
+ console.error(` ${pc13.dim("```")}
3363
3760
  `);
3364
- 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.")}
3365
3762
  `);
3366
3763
  }
3367
3764
 
3368
3765
  // src/commands/management.ts
3369
3766
  import { existsSync as existsSync5, writeFileSync as writeFileSync2 } from "fs";
3370
- import { resolve as resolve6 } from "path";
3767
+ import { resolve as resolve7 } from "path";
3371
3768
  import { execSync as execSync3 } from "child_process";
3372
- import pc12 from "picocolors";
3769
+ import pc14 from "picocolors";
3373
3770
  async function installBrowsersCommand() {
3374
3771
  console.error(`
3375
- ${pc12.bold("Installing Browser Engines")}
3772
+ ${pc14.bold("Installing Browser Engines")}
3376
3773
  `);
3377
3774
  const spinner = createSpinner("Installing Chromium...");
3378
3775
  try {
@@ -3380,21 +3777,21 @@ async function installBrowsersCommand() {
3380
3777
  spinner.succeed("Chromium installed successfully");
3381
3778
  } catch {
3382
3779
  spinner.fail("Failed to install Chromium");
3383
- 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")}`);
3384
3781
  }
3385
3782
  console.error(`
3386
- ${pc12.green("\u2713")} Browser installation complete.
3783
+ ${pc14.green("\u2713")} Browser installation complete.
3387
3784
  `);
3388
3785
  }
3389
3786
  async function initCommand() {
3390
3787
  console.error(`
3391
- ${pc12.bold("Initialize Fennec Configuration")}
3788
+ ${pc14.bold("Initialize Fennec Configuration")}
3392
3789
  `);
3393
- const configFile = resolve6("./fennec.config.yaml");
3790
+ const configFile = resolve7("./fennec.config.yaml");
3394
3791
  if (existsSync5(configFile)) {
3395
- 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);
3396
3793
  if (!overwrite) {
3397
- console.error(` ${pc12.dim("Cancelled.")}
3794
+ console.error(` ${pc14.dim("Cancelled.")}
3398
3795
  `);
3399
3796
  return;
3400
3797
  }
@@ -3474,21 +3871,21 @@ logging:
3474
3871
  file: null
3475
3872
  `;
3476
3873
  writeFileSync2(configFile, config, "utf-8");
3477
- spinner.succeed(`Configuration written to ${pc12.bold(configFile)}`);
3874
+ spinner.succeed(`Configuration written to ${pc14.bold(configFile)}`);
3478
3875
  console.error(`
3479
- ${pc12.dim("Edit the file to customize Fennec behavior.")}
3876
+ ${pc14.dim("Edit the file to customize Fennec behavior.")}
3480
3877
  `);
3481
3878
  }
3482
3879
 
3483
3880
  // src/commands/health.ts
3484
3881
  import { existsSync as existsSync6, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
3485
3882
  import { execSync as execSync4 } from "child_process";
3486
- import { resolve as resolve7 } from "path";
3487
- import { homedir as homedir5 } from "os";
3488
- import pc13 from "picocolors";
3883
+ import { resolve as resolve8 } from "path";
3884
+ import { homedir as homedir6 } from "os";
3885
+ import pc15 from "picocolors";
3489
3886
  async function healthCommand() {
3490
3887
  printBanner();
3491
- console.error(` ${pc13.bold("Fennec Health Check")}
3888
+ console.error(` ${pc15.bold("Fennec Health Check")}
3492
3889
  `);
3493
3890
  let adbStatus = "unknown";
3494
3891
  try {
@@ -3498,20 +3895,20 @@ async function healthCommand() {
3498
3895
  stdio: ["ignore", "pipe", "pipe"]
3499
3896
  });
3500
3897
  const version = result.split("\n")[0]?.trim() ?? "installed";
3501
- adbStatus = pc13.green(version);
3898
+ adbStatus = pc15.green(version);
3502
3899
  } catch {
3503
- adbStatus = pc13.dim("not found") + " (optional)";
3900
+ adbStatus = pc15.dim("not found") + " (optional)";
3504
3901
  }
3505
3902
  const tracked = readTracked();
3506
3903
  const runningCount = tracked.filter((t) => isProcessRunning(t.pid)).length;
3507
- const logDir = resolve7(homedir5(), ".fennec", "logs");
3904
+ const logDir = resolve8(homedir6(), ".fennec", "logs");
3508
3905
  let logSize = "0 B";
3509
3906
  try {
3510
3907
  if (existsSync6(logDir)) {
3511
3908
  const files = readdirSync2(logDir);
3512
3909
  let totalBytes = 0;
3513
3910
  for (const f of files) {
3514
- const fStat = statSync2(resolve7(logDir, f));
3911
+ const fStat = statSync2(resolve8(logDir, f));
3515
3912
  totalBytes += fStat.size;
3516
3913
  }
3517
3914
  logSize = totalBytes > 1024 * 1024 ? `${(totalBytes / (1024 * 1024)).toFixed(1)} MB` : `${(totalBytes / 1024).toFixed(1)} KB`;
@@ -3524,7 +3921,7 @@ async function healthCommand() {
3524
3921
  memoryInfo = `${(mem.rss / (1024 * 1024)).toFixed(0)} MB RSS / ${(mem.heapUsed / (1024 * 1024)).toFixed(0)} MB heap`;
3525
3922
  } catch {
3526
3923
  }
3527
- console.error(` ${symbols.fox} ${pc13.bold("System")}
3924
+ console.error(` ${symbols.fox} ${pc15.bold("System")}
3528
3925
  `);
3529
3926
  console.error(` ${renderKV("Node.js", process.version)}`);
3530
3927
  console.error(` ${renderKV("Platform", `${process.platform} ${process.arch}`)}`);
@@ -3532,7 +3929,7 @@ async function healthCommand() {
3532
3929
  console.error(` ${renderKV("ADB", adbStatus)}`);
3533
3930
  console.error(` ${renderKV("PID", String(process.pid))}`);
3534
3931
  console.error();
3535
- console.error(` ${symbols.fox} ${pc13.bold("Processes")}
3932
+ console.error(` ${symbols.fox} ${pc15.bold("Processes")}
3536
3933
  `);
3537
3934
  console.error(` ${renderKV("Tracked", String(tracked.length))}`);
3538
3935
  console.error(` ${renderKV("Running", String(runningCount))}`);
@@ -3540,15 +3937,238 @@ async function healthCommand() {
3540
3937
  console.error();
3541
3938
  const allHealthy = runningCount === tracked.length || tracked.length === 0;
3542
3939
  if (allHealthy) {
3543
- console.error(` ${pc13.green("\u2713")} ${pc13.bold("All systems healthy")}
3940
+ console.error(` ${pc15.green("\u2713")} ${pc15.bold("All systems healthy")}
3544
3941
  `);
3545
3942
  } else {
3546
3943
  const stopped = tracked.length - runningCount;
3547
- 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")}
3548
3945
  `);
3549
3946
  }
3550
3947
  }
3551
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
+
3552
4172
  // src/index.ts
3553
4173
  var [, , command, ...args] = process.argv;
3554
4174
  async function main() {
@@ -3568,10 +4188,26 @@ async function main() {
3568
4188
  await statusCommand(args);
3569
4189
  } else if (command === "log") {
3570
4190
  await logCommand(args);
4191
+ } else if (command === "spawn") {
4192
+ await spawnCommand(args);
4193
+ } else if (command === "stop") {
4194
+ await stopCommand(args);
3571
4195
  } else if (command === "kill") {
3572
4196
  await killCommand(args);
3573
4197
  } else if (command === "restart") {
3574
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);
3575
4211
  } else if (command === "attach") {
3576
4212
  await attachCommand(args);
3577
4213
  } else if (command === "pipe") {