@plumpslabs/fennec-cli 1.11.0 → 1.11.2

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
@@ -1,26 +1,30 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { existsSync as existsSync2, writeFileSync } from "fs";
5
- import { resolve as resolve2 } from "path";
6
- import { execSync } from "child_process";
4
+ import { existsSync as existsSync3, writeFileSync, readFileSync as readFileSync2, mkdirSync } from "fs";
5
+ import { resolve as resolve2, dirname } from "path";
6
+ import { homedir } from "os";
7
+ import { execSync as execSync2 } from "child_process";
7
8
  import { FennecServer, SessionStore, ProcessManager } from "@plumpslabs/fennec-core";
8
9
  import pc4 from "picocolors";
9
10
 
10
11
  // src/utils/banner.ts
11
12
  import pc from "picocolors";
12
13
  function hexColor(color) {
13
- return pc.hex(color);
14
+ if (typeof pc.hex === "function") {
15
+ return pc.hex(color);
16
+ }
17
+ return (s) => s;
14
18
  }
15
19
  var fennecOrange = hexColor("#FF6432");
16
20
  var fennecYellow = hexColor("#FFB347");
17
21
  var FOX = `
18
22
  ${fennecOrange("/\\\\ /\\\\")}
19
- ${fennecOrange("(")} ${fennecYellow("o o")} ${fennecOrange(") ") + pc.bold("Fennec")} ${pc.dim("v1.10.0")}
23
+ ${fennecOrange("(")} ${fennecYellow("o o")} ${fennecOrange(") ") + pc.bold("Fennec")} ${pc.dim("v1.11.2")}
20
24
  ${fennecOrange("=(")} ${fennecYellow(" Y ")} ${fennecOrange(")=")} ${pc.dim("ears everywhere in your stack.")}
21
25
  ${fennecOrange(") (")}
22
26
  `;
23
- var FOX_COMPACT = `${pc.redBright(" \u{1F98A}")} ${pc.bold("Fennec")} ${pc.dim("v1.10.0")}`;
27
+ var FOX_COMPACT = `${pc.redBright(" \u{1F98A}")} ${pc.bold("Fennec")} ${pc.dim("v1.11.2")}`;
24
28
  function printBanner() {
25
29
  if (process.stdout.isTTY) {
26
30
  console.error(FOX);
@@ -42,33 +46,43 @@ function showHelp() {
42
46
  ${pc2.bold("Server")}
43
47
  ${sep}
44
48
 
45
- ${pc2.cyan("start")} ${pc2.dim("Start the MCP server (default)")}
49
+ ${pc2.cyan("start")} ${pc2.dim("Start Fennec MCP server (default)")}
46
50
  ${pc2.dim("--config")} Path to config file
47
51
  ${pc2.dim("--transport")} Transport type ${pc2.dim("(stdio | sse) [default: stdio]")}
48
- ${pc2.dim("--port")} SSE port ${pc2.dim("[default: 3333]")}
49
52
  ${pc2.dim("--api-port")} Management API port ${pc2.dim("[default: 3456]")}
50
53
 
54
+ ${pc2.cyan("start")} ${pc2.dim("<command>")} ${pc2.dim("Start an app under Fennec observation (new!)")}
55
+ ${pc2.dim("--name")} App name ${pc2.dim("(required for tracking)")}
56
+ ${pc2.dim("--port")} Port the app listens on ${pc2.dim("(optional)")}
57
+ ${pc2.dim("--cwd")} Working directory
58
+ ${pc2.dim("--restart")} Auto-restart on crash
59
+
51
60
  ${sep}
52
61
  ${pc2.bold("Process Management")}
53
62
  ${sep}
54
63
 
64
+ ${pc2.cyan("ps")} ${pc2.dim("[options]")} ${pc2.dim("List Fennec-tracked apps (like PM2)")}
65
+ ${pc2.dim("--system, -a")} Show all system processes
66
+ ${pc2.dim("--name")} Filter by app name
67
+ ${pc2.dim("-w, --watch")} Watch mode ${pc2.dim("(system processes refresh every 3s)")}
68
+
55
69
  ${pc2.cyan("run")} ${pc2.dim("<command>")} ${pc2.dim("Run a command under Fennec observation")}
56
70
  ${pc2.dim("--name")} Process name ${pc2.dim("(required)")}
57
71
  ${pc2.dim("--cwd")} Working directory
58
72
  ${pc2.dim("--restart")} Auto-restart on crash
59
73
 
60
- ${pc2.cyan("status")} ${pc2.dim("[name]")} ${pc2.dim("Show observed processes")}
61
- ${pc2.dim("-w, --watch")} Watch mode ${pc2.dim("(refresh every 2s)")}
74
+ ${pc2.cyan("status")} ${pc2.dim("[name]")} ${pc2.dim("Show system overview & top processes")}
75
+ ${pc2.dim("-w, --watch")} Watch mode ${pc2.dim("(refresh every 3s)")}
62
76
 
63
- ${pc2.cyan("log")} ${pc2.dim("<name>")} ${pc2.dim("Show logs for a process")}
64
- ${pc2.dim("--lines")} Number of lines ${pc2.dim("[default: 50]")}
65
- ${pc2.dim("--level")} Filter by level ${pc2.dim("(error | warn | info)")}
66
- ${pc2.dim("-f, --follow")} Follow mode ${pc2.dim("(tail -f)")}
77
+ ${pc2.cyan("log")} ${pc2.dim("<pid|name>")} ${pc2.dim("Show logs for a process")}
78
+ ${pc2.dim("--lines")} Number of lines ${pc2.dim("[default: 30]")}
79
+ ${pc2.dim("-f, --follow")} Follow mode ${pc2.dim("(journalctl --follow)")}
67
80
 
68
- ${pc2.cyan("kill")} ${pc2.dim("<name>")} ${pc2.dim("Stop a process")}
81
+ ${pc2.cyan("kill")} ${pc2.dim("<pid|name|all>")} ${pc2.dim("Kill process(es) by PID, name, or all")}
69
82
  ${pc2.dim("--signal")} Signal to send ${pc2.dim("[default: SIGTERM]")}
83
+ ${pc2.dim("--all")} Kill all user processes
70
84
 
71
- ${pc2.cyan("restart")} ${pc2.dim("<name>")} ${pc2.dim("Restart a process")}
85
+ ${pc2.cyan("restart")} ${pc2.dim("<pid|name>")} ${pc2.dim("Kill + show re-run command")}
72
86
 
73
87
  ${sep}
74
88
  ${pc2.bold("Observation")}
@@ -220,7 +234,10 @@ async function watchCommand(args2) {
220
234
  import { createInterface } from "readline";
221
235
  import pc3 from "picocolors";
222
236
  function hex(color) {
223
- return pc3.hex(color);
237
+ if (typeof pc3.hex === "function") {
238
+ return pc3.hex(color);
239
+ }
240
+ return (s) => s;
224
241
  }
225
242
  var fennecOrange3 = hex("#FF6432");
226
243
  var blueAccent = hex("#4A90D9");
@@ -262,20 +279,6 @@ var symbols = {
262
279
  fox: "\u{1F98A}",
263
280
  ears: "\u23DC"
264
281
  };
265
- function statusBadge(status) {
266
- switch (status) {
267
- case "running":
268
- return pc3.green("\u25CF running");
269
- case "stopped":
270
- return pc3.dim("\u25CB stopped");
271
- case "error":
272
- return pc3.red("\u2717 error");
273
- case "starting":
274
- return pc3.yellow("\u25CC starting");
275
- case "unknown":
276
- return pc3.dim("? unknown");
277
- }
278
- }
279
282
  function logLevel(label) {
280
283
  const upper = label.toUpperCase().padEnd(5);
281
284
  switch (label.toLowerCase()) {
@@ -485,14 +488,309 @@ function renderSuccess(message) {
485
488
  return ` ${pc3.green("\u2713")} ${message}`;
486
489
  }
487
490
 
491
+ // src/utils/system-process.ts
492
+ import { readdirSync, readFileSync, existsSync as existsSync2 } from "fs";
493
+ import { execSync } from "child_process";
494
+ import { cpus } from "os";
495
+ function readProcProcesses() {
496
+ const processes = [];
497
+ const currentUid = process.getuid?.();
498
+ const totalMemKb = readTotalMem();
499
+ try {
500
+ const pids = readdirSync("/proc").filter((d) => /^\d+$/.test(d));
501
+ for (const pidStr of pids) {
502
+ try {
503
+ const pid = parseInt(pidStr, 10);
504
+ const proc = readSingleProcProcess(pid, currentUid, totalMemKb);
505
+ if (proc) processes.push(proc);
506
+ } catch {
507
+ continue;
508
+ }
509
+ }
510
+ } catch {
511
+ return [];
512
+ }
513
+ return processes;
514
+ }
515
+ function readSingleProcProcess(pid, currentUid, totalMemKb) {
516
+ const statPath = `/proc/${pid}/stat`;
517
+ const statusPath = `/proc/${pid}/status`;
518
+ const cmdlinePath = `/proc/${pid}/cmdline`;
519
+ if (!existsSync2(statPath)) return null;
520
+ const statusText = readSafe(statusPath);
521
+ if (!statusText) return null;
522
+ const nameMatch = statusText.match(/Name:\s+(.+)/);
523
+ const uidMatch = statusText.match(/Uid:\s+(\d+)/);
524
+ const vmRssMatch = statusText.match(/VmRSS:\s+(\d+)/);
525
+ const stateMatch = statusText.match(/State:\s+(\S)/);
526
+ const name = nameMatch?.[1] ?? `pid_${pid}`;
527
+ const uid = uidMatch ? parseInt(uidMatch[1], 10) : void 0;
528
+ const vmRss = vmRssMatch ? parseInt(vmRssMatch[1], 10) : 0;
529
+ const state = stateMatch?.[1] ?? "?";
530
+ const isUserProcess = uid !== void 0 && currentUid !== void 0 && uid === currentUid;
531
+ const statText = readSafe(statPath);
532
+ let cpuPercent = 0;
533
+ let startedAt = null;
534
+ if (statText) {
535
+ const parenEnd = statText.lastIndexOf(")");
536
+ if (parenEnd > 0) {
537
+ const fields = statText.slice(parenEnd + 2).split(/\s+/);
538
+ const utime = parseInt(fields[11] ?? "0", 10);
539
+ const stime = parseInt(fields[12] ?? "0", 10);
540
+ const starttime = parseInt(fields[19] ?? "0", 10);
541
+ const hertz = 100;
542
+ const uptimeSeconds = readUptime();
543
+ const totalJiffies = utime + stime;
544
+ if (uptimeSeconds > 0) {
545
+ const elapsedJiffies = uptimeSeconds * hertz;
546
+ const processJiffies = totalJiffies;
547
+ cpuPercent = Math.min(100, processJiffies / Math.max(1, elapsedJiffies) * 100 * cpus().length);
548
+ }
549
+ startedAt = formatStartTime(starttime, uptimeSeconds, hertz);
550
+ }
551
+ }
552
+ const cmdlineRaw = readSafe(cmdlinePath);
553
+ const command2 = cmdlineRaw ? cmdlineRaw.split("\0").filter(Boolean).join(" ") : name;
554
+ const ports = detectListeningPorts(pid);
555
+ const memPercent = totalMemKb > 0 ? vmRss / totalMemKb * 100 : 0;
556
+ return {
557
+ pid,
558
+ name,
559
+ command: command2.length > 200 ? command2.slice(0, 200) + "\u2026" : command2,
560
+ cpuPercent: Math.round(cpuPercent * 10) / 10,
561
+ memPercent: Math.round(memPercent * 10) / 10,
562
+ memRss: vmRss,
563
+ state,
564
+ startedAt,
565
+ ports,
566
+ isUserProcess
567
+ };
568
+ }
569
+ function runPsProcesses() {
570
+ try {
571
+ const output = execSync(
572
+ "ps axo pid,comm,pcpu,pmem,rss,state,etime,user --sort=-pcpu 2>/dev/null | head -200",
573
+ { encoding: "utf-8", timeout: 5e3 }
574
+ );
575
+ const lines = output.trim().split("\n").slice(1);
576
+ const currentUser = execSync("whoami", { encoding: "utf-8", timeout: 1e3 }).trim();
577
+ return lines.map((line) => {
578
+ const parts = line.trim().split(/\s+/);
579
+ const pid = parseInt(parts[0] ?? "0", 10);
580
+ const name = parts[1] ?? "";
581
+ const cpu = parseFloat(parts[2] ?? "0");
582
+ const mem = parseFloat(parts[3] ?? "0");
583
+ const rss = parseInt(parts[4] ?? "0", 10);
584
+ const state = parts[5] ?? "?";
585
+ const elapsed = parts[6] ?? "";
586
+ const user = parts[7] ?? "";
587
+ const command2 = parts.slice(1).join(" ");
588
+ return {
589
+ pid,
590
+ name,
591
+ command: command2.length > 200 ? command2.slice(0, 200) + "\u2026" : command2,
592
+ cpuPercent: Math.round(cpu * 10) / 10,
593
+ memPercent: Math.round(mem * 10) / 10,
594
+ memRss: rss,
595
+ state: mapBsdState(state),
596
+ startedAt: elapsed || null,
597
+ ports: [],
598
+ isUserProcess: user === currentUser
599
+ };
600
+ });
601
+ } catch {
602
+ return [];
603
+ }
604
+ }
605
+ function mapBsdState(s) {
606
+ switch (s) {
607
+ case "R":
608
+ return "R";
609
+ case "S":
610
+ return "S";
611
+ case "Z":
612
+ return "Z";
613
+ case "T":
614
+ return "T";
615
+ case "I":
616
+ return "S";
617
+ default:
618
+ return s;
619
+ }
620
+ }
621
+ function detectListeningPorts(_pid) {
622
+ return [];
623
+ }
624
+ function readSafe(path) {
625
+ try {
626
+ return readFileSync(path, "utf-8");
627
+ } catch {
628
+ return null;
629
+ }
630
+ }
631
+ function readTotalMem() {
632
+ try {
633
+ const memInfo = readSafe("/proc/meminfo");
634
+ if (memInfo) {
635
+ const match = memInfo.match(/MemTotal:\s+(\d+)/);
636
+ if (match) return parseInt(match[1], 10);
637
+ }
638
+ } catch {
639
+ }
640
+ return 16e6;
641
+ }
642
+ function readUptime() {
643
+ try {
644
+ const data = readSafe("/proc/uptime");
645
+ if (data) {
646
+ return parseFloat(data.split(/\s+/)[0] ?? "0");
647
+ }
648
+ } catch {
649
+ }
650
+ return 0;
651
+ }
652
+ function formatStartTime(starttime, uptimeSeconds, hertz) {
653
+ try {
654
+ const bootTime = Date.now() / 1e3 - uptimeSeconds;
655
+ const processStart = bootTime + starttime / hertz;
656
+ return new Date(processStart * 1e3).toISOString();
657
+ } catch {
658
+ return null;
659
+ }
660
+ }
661
+ function getSystemProcesses(filter) {
662
+ let processes;
663
+ if (existsSync2("/proc")) {
664
+ processes = readProcProcesses();
665
+ } else {
666
+ processes = runPsProcesses();
667
+ }
668
+ if (filter) {
669
+ if (filter.userOnly) {
670
+ processes = processes.filter((p) => p.isUserProcess);
671
+ }
672
+ if (filter.name) {
673
+ const nameLower = filter.name.toLowerCase();
674
+ processes = processes.filter(
675
+ (p) => p.name.toLowerCase().includes(nameLower) || p.command.toLowerCase().includes(nameLower)
676
+ );
677
+ }
678
+ if (filter.pid) {
679
+ processes = processes.filter((p) => p.pid === filter.pid);
680
+ }
681
+ if (filter.port) {
682
+ processes = processes.filter((p) => p.ports.includes(filter.port));
683
+ }
684
+ if (filter.sortBy) {
685
+ processes.sort((a, b) => {
686
+ switch (filter.sortBy) {
687
+ case "cpu":
688
+ return b.cpuPercent - a.cpuPercent;
689
+ case "mem":
690
+ return b.memPercent - a.memPercent;
691
+ case "pid":
692
+ return a.pid - b.pid;
693
+ case "name":
694
+ return a.name.localeCompare(b.name);
695
+ default:
696
+ return b.cpuPercent - a.cpuPercent;
697
+ }
698
+ });
699
+ }
700
+ if (filter.limit && filter.limit > 0) {
701
+ processes = processes.slice(0, filter.limit);
702
+ }
703
+ }
704
+ return processes;
705
+ }
706
+ function killProcess(pid, signal = "SIGTERM") {
707
+ try {
708
+ process.kill(pid, signal);
709
+ return true;
710
+ } catch {
711
+ return false;
712
+ }
713
+ }
714
+ function isProcessRunning(pid) {
715
+ try {
716
+ process.kill(pid, 0);
717
+ return true;
718
+ } catch {
719
+ return false;
720
+ }
721
+ }
722
+ function formatProcessState(state) {
723
+ switch (state) {
724
+ case "R":
725
+ return "Running";
726
+ case "S":
727
+ return "Sleeping";
728
+ case "D":
729
+ return "Disk Sleep";
730
+ case "Z":
731
+ return "Zombie";
732
+ case "T":
733
+ return "Stopped";
734
+ case "t":
735
+ return "Tracing";
736
+ case "X":
737
+ return "Dead";
738
+ case "I":
739
+ return "Idle";
740
+ default:
741
+ return state;
742
+ }
743
+ }
744
+
488
745
  // src/index.ts
489
746
  var [, , command, ...args] = process.argv;
747
+ function getTrackedPath() {
748
+ const dir = process.env.FENNEC_DATA_DIR ? resolve2(process.env.FENNEC_DATA_DIR) : resolve2(homedir(), ".fennec");
749
+ return resolve2(dir, "tracked.json");
750
+ }
751
+ function readTracked() {
752
+ try {
753
+ const path = getTrackedPath();
754
+ if (!existsSync3(path)) return [];
755
+ return JSON.parse(readFileSync2(path, "utf-8"));
756
+ } catch {
757
+ return [];
758
+ }
759
+ }
760
+ function saveTracked(processes) {
761
+ try {
762
+ const path = getTrackedPath();
763
+ mkdirSync(dirname(path), { recursive: true });
764
+ writeFileSync(path, JSON.stringify(processes, null, 2), "utf-8");
765
+ } catch {
766
+ }
767
+ }
768
+ function addTracked(proc) {
769
+ const tracked = readTracked();
770
+ const filtered = tracked.filter((t) => t.name !== proc.name);
771
+ filtered.push(proc);
772
+ saveTracked(filtered);
773
+ }
774
+ function removeTracked(name) {
775
+ const tracked = readTracked();
776
+ saveTracked(tracked.filter((t) => t.name !== name));
777
+ }
778
+ function removeTrackedByPid(pid) {
779
+ const tracked = readTracked();
780
+ saveTracked(tracked.filter((t) => t.pid !== pid));
781
+ }
490
782
  async function main() {
491
783
  if (!command || command === "start") {
492
- await startServer(args);
784
+ if (args.length === 0 || args[0]?.startsWith("--")) {
785
+ await startServer(args);
786
+ } else {
787
+ await startCommand(args);
788
+ }
493
789
  } else if (command === "run") {
494
790
  await runCommand(args);
495
- } else if (command === "status" || command === "ps") {
791
+ } else if (command === "ps") {
792
+ await psCommand(args);
793
+ } else if (command === "status") {
496
794
  await statusCommand(args);
497
795
  } else if (command === "log") {
498
796
  await logCommand(args);
@@ -521,7 +819,7 @@ async function main() {
521
819
  printBanner();
522
820
  await initCommand();
523
821
  } else if (command === "version" || command === "--version" || command === "-v") {
524
- console.log(` ${symbols.fox} ${pc4.bold("Fennec")} ${pc4.dim("v1.11.0")}`);
822
+ console.log(` ${symbols.fox} ${pc4.bold("Fennec")} ${pc4.dim("v1.11.2")}`);
525
823
  } else if (command === "help" || command === "--help" || command === "-h") {
526
824
  console.log(pc4.dim("\n Use 'fennec help' for more information.\n"));
527
825
  printBanner();
@@ -555,20 +853,26 @@ async function startServer(args2) {
555
853
  process.exit(1);
556
854
  }
557
855
  }
856
+ async function startCommand(args2) {
857
+ printBanner();
858
+ console.error(` ${pc4.dim("Starting app...")}
859
+ `);
860
+ await runCommand(args2);
861
+ }
558
862
  async function runCommand(args2) {
559
863
  const nameIndex = args2.indexOf("--name");
560
864
  const name = nameIndex !== -1 ? args2[nameIndex + 1] : void 0;
865
+ const portIndex = args2.indexOf("--port");
866
+ const port = portIndex !== -1 ? parseInt(args2[portIndex + 1], 10) : void 0;
561
867
  const cwdIndex = args2.indexOf("--cwd");
562
868
  const cwd = cwdIndex !== -1 ? args2[cwdIndex + 1] : void 0;
563
869
  const restartFlag = args2.includes("--restart");
564
- const cmdEnd = Math.min(
565
- nameIndex !== -1 ? nameIndex : Infinity,
566
- cwdIndex !== -1 ? cwdIndex : Infinity
567
- );
870
+ const stopFlags = [nameIndex, portIndex, cwdIndex].filter((i) => i !== -1);
871
+ const cmdEnd = Math.min(...stopFlags, Infinity);
568
872
  const cmdParts = args2.slice(0, cmdEnd);
569
873
  const cmd = cmdParts.join(" ");
570
874
  if (!cmd) {
571
- console.error(renderError("Missing command", "Usage: fennec run <command> --name <name>"));
875
+ console.error(renderError("Missing command", "Usage: fennec start|run <command> --name <name> [--port <port>] [--cwd <dir>] [--restart]"));
572
876
  process.exit(1);
573
877
  }
574
878
  const appName = name ?? cmdParts[0] ?? "app";
@@ -576,6 +880,7 @@ async function runCommand(args2) {
576
880
  ${symbols.fox} ${pc4.bold("Running")} ${renderAppName(appName)}
577
881
  `);
578
882
  console.error(` ${renderKV("Command", cmd)}`);
883
+ if (port) console.error(` ${renderKV("Port", String(port))}`);
579
884
  if (cwd) console.error(` ${renderKV("Directory", cwd)}`);
580
885
  console.error(` ${renderKV("Restart", restartFlag ? "on crash" : "off")}`);
581
886
  console.error(` ${divider()}`);
@@ -587,8 +892,16 @@ async function runCommand(args2) {
587
892
  const pm = new ProcessManager(config);
588
893
  try {
589
894
  const proc = pm.spawn(cmdParts[0], cmdParts.slice(1), cwd, void 0, appName);
895
+ addTracked({
896
+ name: appName,
897
+ pid: proc.pid,
898
+ command: cmd,
899
+ port,
900
+ cwd,
901
+ startedAt: (/* @__PURE__ */ new Date()).toISOString()
902
+ });
590
903
  console.error(`
591
- ${pc4.green("\u25CF")} ${pc4.bold(appName)} ${pc4.dim(`started (PID: ${proc.pid})`)}
904
+ ${pc4.green("\u25CF")} ${pc4.bold(appName)} ${pc4.dim(`started (PID: ${proc.pid})`)}${port ? pc4.dim(` :${port}`) : ""}
592
905
  `);
593
906
  const stdout = proc.child.stdout;
594
907
  const stderr = proc.child.stderr;
@@ -616,6 +929,7 @@ async function runCommand(args2) {
616
929
  });
617
930
  }
618
931
  proc.child.on("exit", (code, signal) => {
932
+ removeTracked(appName);
619
933
  if (restartFlag && code !== 0) {
620
934
  console.error(`
621
935
  ${pc4.yellow("\u26A0")} ${appName} ${pc4.dim(`exited (${code}), restarting...`)}`);
@@ -633,55 +947,166 @@ async function runCommand(args2) {
633
947
  process.exit(1);
634
948
  }
635
949
  }
636
- async function statusCommand(_args) {
637
- const watchFlag = _args.includes("-w") || _args.includes("--watch");
638
- if (watchFlag) {
639
- await watchStatus();
950
+ async function psCommand(args2) {
951
+ const watchFlag = args2.includes("-w") || args2.includes("--watch");
952
+ const systemFlag = args2.includes("--system") || args2.includes("-a") || args2.includes("--all");
953
+ const nameFilter = args2.includes("--name") ? args2[args2.indexOf("--name") + 1] : void 0;
954
+ const sortBy = args2.includes("--sort") ? args2[args2.indexOf("--sort") + 1] : "name";
955
+ if (watchFlag && systemFlag) {
956
+ await watchSystemProcesses(sortBy, 15);
957
+ return;
958
+ }
959
+ if (systemFlag) {
960
+ const spinner = createSpinner("Scanning system processes...");
961
+ try {
962
+ const processes = getSystemProcesses({
963
+ name: nameFilter,
964
+ userOnly: true,
965
+ sortBy,
966
+ limit: 30
967
+ });
968
+ spinner.stop();
969
+ process.stdout.write("\r\x1B[K");
970
+ if (processes.length === 0) {
971
+ console.error(`
972
+ ${pc4.dim("No system processes found.")}
973
+ `);
974
+ return;
975
+ }
976
+ const columns2 = [
977
+ { key: "pid", label: "PID", align: "right", format: (v) => pc4.dim(String(v).padStart(6)) },
978
+ { key: "name", label: "Name", format: (v) => pc4.bold(String(v)) },
979
+ { key: "cpu", label: "CPU%", align: "right", format: (v) => {
980
+ const num = v;
981
+ return num > 10 ? pc4.red(String(num)) : num > 5 ? pc4.yellow(String(num)) : pc4.dim(String(num));
982
+ } },
983
+ { key: "mem", label: "MEM%", align: "right", format: (v) => {
984
+ const num = v;
985
+ return num > 10 ? pc4.red(String(num)) : num > 5 ? pc4.yellow(String(num)) : pc4.dim(String(num));
986
+ } },
987
+ { key: "state", label: "State", format: (v) => {
988
+ const s = String(v);
989
+ if (s === "R" || s === "Running") return pc4.green(s);
990
+ if (s === "Z" || s === "Zombie") return pc4.red(s);
991
+ if (s === "S" || s === "Sleeping") return pc4.cyan(s);
992
+ return pc4.dim(s);
993
+ } }
994
+ ];
995
+ const rows2 = processes.map((p) => ({
996
+ pid: p.pid,
997
+ name: p.name,
998
+ cpu: p.cpuPercent,
999
+ mem: p.memPercent,
1000
+ state: formatProcessState(p.state)
1001
+ }));
1002
+ console.error(`
1003
+ ${symbols.fox} ${pc4.bold("System Processes")} ${pc4.dim(`(top ${processes.length} by ${sortBy})`)}
1004
+ `);
1005
+ console.error(renderTable(columns2, rows2));
1006
+ console.error();
1007
+ } catch (error) {
1008
+ spinner.fail("Failed to scan processes");
1009
+ console.error(renderError("Process scan failed", String(error)));
1010
+ }
1011
+ return;
1012
+ }
1013
+ const tracked = readTracked();
1014
+ if (tracked.length === 0) {
1015
+ console.error(`
1016
+ ${pc4.dim("No tracked processes.")}`);
1017
+ console.error(` ${pc4.dim("Start an app with:")} ${pc4.cyan("fennec start <command> --name <name>")}
1018
+ `);
640
1019
  return;
641
1020
  }
642
1021
  const columns = [
643
- { key: "name", label: "Name", format: (v) => pc4.bold(String(v)) },
644
- { key: "source", label: "Source" },
645
- { key: "status", label: "Status", format: (v) => statusBadge(v) },
646
- { key: "pid", label: "PID" },
647
- { key: "uptime", label: "Uptime" },
648
- { key: "error", label: "Last Error", format: (v) => v ? pc4.red(String(v)) : pc4.dim("-") }
649
- ];
650
- const rows = [
651
- { name: "backend", source: ":3000", status: "running", pid: "12345", uptime: "2h 15m", error: null },
652
- { name: "frontend", source: ":5173", status: "running", pid: "12346", uptime: "2h 15m", error: null },
653
- { name: "database", source: ":5432", status: "running", pid: "docker", uptime: "5h 30m", error: null }
1022
+ { key: "name", label: "App", format: (v) => pc4.bold(String(v)) },
1023
+ { key: "pid", label: "PID", align: "right" },
1024
+ { key: "status", label: "Status", format: (v) => {
1025
+ const s = v;
1026
+ if (s === "running") return pc4.green("\u25CF running");
1027
+ return pc4.red("\u25CB stopped");
1028
+ } },
1029
+ { key: "port", label: "Port", format: (v) => {
1030
+ const p = v;
1031
+ return p ? pc4.yellow(`:${p}`) : pc4.dim("-");
1032
+ } },
1033
+ { key: "command", label: "Command", format: (v) => {
1034
+ const c = String(v);
1035
+ return c.length > 50 ? c.slice(0, 50) + "\u2026" : c;
1036
+ } },
1037
+ { key: "uptime", label: "Uptime", format: (v) => pc4.dim(String(v)) }
654
1038
  ];
1039
+ const rows = tracked.map((t) => {
1040
+ const running = isProcessRunning(t.pid);
1041
+ const uptime = running ? formatUptime(Math.floor((Date.now() - new Date(t.startedAt).getTime()) / 1e3)) : "-";
1042
+ return {
1043
+ name: t.name,
1044
+ pid: running ? String(t.pid) : pc4.dim(String(t.pid)),
1045
+ status: running ? "running" : "stopped",
1046
+ port: t.port ?? null,
1047
+ command: t.command,
1048
+ uptime
1049
+ };
1050
+ });
1051
+ const runningCount = tracked.filter((t) => isProcessRunning(t.pid)).length;
655
1052
  console.error(`
656
- ${symbols.fox} ${pc4.bold("Observed Processes")}
1053
+ ${symbols.fox} ${pc4.bold("Fennec Apps")} ${pc4.dim(`(${runningCount}/${tracked.length} running)`)}
657
1054
  `);
658
1055
  console.error(renderTable(columns, rows));
1056
+ console.error(` ${pc4.dim("Use")} ${pc4.cyan("fennec start <command> --name <name> --port <port>")} ${pc4.dim("to add more apps.")}`);
1057
+ console.error(` ${pc4.dim("Use")} ${pc4.cyan("fennec log <name>")} ${pc4.dim("to view logs.")}`);
1058
+ console.error(` ${pc4.dim("Use")} ${pc4.cyan("fennec kill <name>")} ${pc4.dim("to stop an app.")}`);
659
1059
  console.error();
660
1060
  }
661
- async function watchStatus() {
1061
+ function formatUptime(seconds) {
1062
+ if (seconds < 60) return `${seconds}s`;
1063
+ if (seconds < 3600) return `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
1064
+ const h = Math.floor(seconds / 3600);
1065
+ const m = Math.floor(seconds % 3600 / 60);
1066
+ if (h >= 24) {
1067
+ const d = Math.floor(h / 24);
1068
+ return `${d}d ${h % 24}h ${m}m`;
1069
+ }
1070
+ return `${h}h ${m}m`;
1071
+ }
1072
+ async function watchSystemProcesses(sortBy, limit) {
662
1073
  console.error(`
663
- ${pc4.bold("Watching status")} ${pc4.dim("(Ctrl+C to stop)")}
1074
+ ${pc4.bold("Watching system processes")} ${pc4.dim("(Ctrl+C to stop, refreshes every 3s)")}
664
1075
  `);
665
1076
  const render = () => {
1077
+ const processes = getSystemProcesses({
1078
+ userOnly: true,
1079
+ sortBy,
1080
+ limit
1081
+ });
666
1082
  const columns = [
1083
+ { key: "pid", label: "PID", align: "right", format: (v) => pc4.dim(String(v).padStart(6)) },
667
1084
  { key: "name", label: "Name", format: (v) => pc4.bold(String(v)) },
668
- { key: "status", label: "Status", format: (v) => statusBadge(v) },
669
- { key: "pid", label: "PID" },
670
- { key: "uptime", label: "Uptime" },
671
- { key: "cpu", label: "CPU" },
672
- { key: "mem", label: "Memory" }
673
- ];
674
- const rows = [
675
- { name: "backend", status: "running", pid: "12345", uptime: "2h 15m", cpu: "1.2%", mem: "128MB" },
676
- { name: "frontend", status: "running", pid: "12346", uptime: "2h 15m", cpu: "0.8%", mem: "64MB" }
1085
+ { key: "cpu", label: "CPU%", align: "right", format: (v) => {
1086
+ const num = v;
1087
+ return num > 10 ? pc4.red(String(num)) : num > 5 ? pc4.yellow(String(num)) : pc4.dim(String(num));
1088
+ } },
1089
+ { key: "mem", label: "MEM%", align: "right", format: (v) => {
1090
+ const num = v;
1091
+ return num > 10 ? pc4.red(String(num)) : num > 5 ? pc4.yellow(String(num)) : pc4.dim(String(num));
1092
+ } },
1093
+ { key: "state", label: "S", align: "center" }
677
1094
  ];
678
- return renderTable(columns, rows, { compact: true });
1095
+ const rows = processes.map((p) => ({
1096
+ pid: p.pid,
1097
+ name: p.name,
1098
+ cpu: p.cpuPercent,
1099
+ mem: p.memPercent,
1100
+ state: p.state
1101
+ }));
1102
+ return ` ${timestamp()} ${pc4.dim(`${processes.length} processes`)}
1103
+ ${renderTable(columns, rows, { compact: true })}`;
679
1104
  };
680
1105
  console.error(render());
681
1106
  const interval = setInterval(() => {
682
- process.stdout.write("\x1B[6A");
1107
+ process.stdout.write("\x1B[J");
683
1108
  console.error(render());
684
- }, 2e3);
1109
+ }, 3e3);
685
1110
  process.on("SIGINT", () => {
686
1111
  clearInterval(interval);
687
1112
  process.exit(0);
@@ -689,72 +1114,284 @@ async function watchStatus() {
689
1114
  await new Promise(() => {
690
1115
  });
691
1116
  }
692
- async function logCommand(args2) {
693
- const name = args2[0];
694
- if (!name) {
695
- console.error(renderError("Missing process name", "Usage: fennec log <name> [options]"));
696
- process.exit(1);
697
- }
698
- const linesIndex = args2.indexOf("--lines");
699
- const lines = linesIndex !== -1 ? parseInt(args2[linesIndex + 1], 10) : 50;
700
- const levelIndex = args2.indexOf("--level");
701
- const level = levelIndex !== -1 ? args2[levelIndex + 1] : void 0;
702
- const followFlag = args2.includes("-f") || args2.includes("--follow");
1117
+ async function statusCommand(_args) {
1118
+ const watchFlag = _args.includes("-w") || _args.includes("--watch");
1119
+ const tracked = readTracked();
1120
+ const topSystem = getSystemProcesses({ userOnly: true, sortBy: "cpu", limit: 5 });
1121
+ const totalUserProcs = getSystemProcesses({ userOnly: true }).length;
703
1122
  console.error(`
704
- ${symbols.fox} ${pc4.bold("Logs")} ${renderAppName(name)} ${pc4.dim(`(last ${lines} lines)`)}
705
- `);
706
- const mockLogs = [
707
- { time: "12:00:00", level: "INFO", msg: "Server started on port 3000" },
708
- { time: "12:00:01", level: "INFO", msg: "Database connected" },
709
- { time: "12:00:02", level: "WARN", msg: "Deprecation: use express@5" },
710
- { time: "12:00:05", level: "ERROR", msg: "POST /api/login \u2192 500" },
711
- { time: "12:00:06", level: "ERROR", msg: "JWT_SECRET not set in environment" }
712
- ];
713
- for (const log of mockLogs) {
714
- const time = pc4.dim(`[${log.time}]`);
715
- const lvl = logLevel(log.level);
716
- const msg = log.level === "ERROR" ? pc4.red(log.msg) : log.level === "WARN" ? pc4.yellow(log.msg) : log.msg;
717
- console.error(` ${time} ${lvl} ${msg}`);
718
- }
719
- if (followFlag) {
720
- console.error(`
721
- ${pc4.dim("Following... (Ctrl+C to stop)")}
1123
+ ${symbols.fox} ${pc4.bold("Fennec Status")}
1124
+ `);
1125
+ if (tracked.length > 0) {
1126
+ const runningCount = tracked.filter((t) => isProcessRunning(t.pid)).length;
1127
+ console.error(` ${pc4.bold("Managed Apps")} ${pc4.dim(`(${runningCount}/${tracked.length} running)`)}
1128
+ `);
1129
+ for (const t of tracked) {
1130
+ const running = isProcessRunning(t.pid);
1131
+ const statusIcon = running ? pc4.green("\u25CF") : pc4.red("\u25CB");
1132
+ const portStr = t.port ? ` ${pc4.yellow(`:${t.port}`)}` : "";
1133
+ const uptime = running ? pc4.dim(formatUptime(Math.floor((Date.now() - new Date(t.startedAt).getTime()) / 1e3))) : pc4.red("stopped");
1134
+ console.error(` ${statusIcon} ${pc4.bold(t.name)}${portStr} ${pc4.dim(`(PID ${t.pid})`)} \u2014 ${uptime}`);
1135
+ }
1136
+ console.error();
1137
+ } else {
1138
+ console.error(` ${pc4.dim("No managed apps.")} ${pc4.cyan("fennec start <command> --name <name>")}
722
1139
  `);
723
- await new Promise(() => {
724
- });
1140
+ }
1141
+ console.error(` ${pc4.bold("System")} ${pc4.dim(`(${totalUserProcs} user processes)`)}`);
1142
+ for (const p of topSystem) {
1143
+ const cpuStr = p.cpuPercent > 10 ? pc4.red(`${p.cpuPercent}%`) : p.cpuPercent > 5 ? pc4.yellow(`${p.cpuPercent}%`) : pc4.dim(`${p.cpuPercent}%`);
1144
+ const memStr = p.memPercent > 10 ? pc4.red(`${p.memPercent}%`) : p.memPercent > 5 ? pc4.yellow(`${p.memPercent}%`) : pc4.dim(`${p.memPercent}%`);
1145
+ console.error(` ${pc4.dim(`PID ${p.pid}`)} ${pc4.bold(p.name)} \u2014 CPU: ${cpuStr} MEM: ${memStr}`);
1146
+ }
1147
+ if (watchFlag) {
1148
+ await watchSystemProcesses("cpu", 15);
725
1149
  }
726
1150
  console.error();
727
1151
  }
728
1152
  async function killCommand(args2) {
729
- const name = args2[0];
730
- if (!name) {
731
- console.error(renderError("Missing process name", "Usage: fennec kill <name>"));
732
- process.exit(1);
733
- }
1153
+ const rawTarget = args2[0];
734
1154
  const signalIndex = args2.indexOf("--signal");
735
1155
  const signalRaw = signalIndex !== -1 ? args2[signalIndex + 1] : "SIGTERM";
736
1156
  const signal = signalRaw ?? "SIGTERM";
1157
+ if (rawTarget === "all" || args2.includes("--all") || args2.includes("-a")) {
1158
+ const userProcs = getSystemProcesses({ userOnly: true, sortBy: "cpu", limit: 200 });
1159
+ if (userProcs.length === 0) {
1160
+ console.error(` ${pc4.dim("No user processes to kill.")}`);
1161
+ return;
1162
+ }
1163
+ console.error(`
1164
+ ${pc4.bold(`Kill ${userProcs.length} user processes?`)}`);
1165
+ console.error(` ${pc4.dim("This will stop ALL your running processes.")}`);
1166
+ console.error(` ${pc4.yellow("\u26A0 System processes will not be affected.")}
1167
+ `);
1168
+ const confirmed2 = await confirmPrompt(
1169
+ `${pc4.red("Are you sure?")} ${pc4.dim("This cannot be undone")}`,
1170
+ false
1171
+ );
1172
+ if (!confirmed2) {
1173
+ console.error(` ${pc4.dim("Cancelled.")}`);
1174
+ return;
1175
+ }
1176
+ const spinner2 = createSpinner(`Killing ${userProcs.length} processes...`);
1177
+ let killed = 0;
1178
+ let failed = 0;
1179
+ for (const proc of userProcs) {
1180
+ if (killProcess(proc.pid, signal)) {
1181
+ killed++;
1182
+ removeTrackedByPid(proc.pid);
1183
+ } else {
1184
+ failed++;
1185
+ }
1186
+ if (killed % 10 === 0) await new Promise((r) => setTimeout(r, 50));
1187
+ }
1188
+ await new Promise((r) => setTimeout(r, 500));
1189
+ if (killed > 0) {
1190
+ spinner2.succeed(`${killed} process(es) killed`);
1191
+ } else {
1192
+ spinner2.fail(`Failed to kill processes`);
1193
+ }
1194
+ if (failed > 0) {
1195
+ console.error(` ${pc4.yellow(`${failed} process(es) could not be killed`)} ${pc4.dim("(try with sudo)")}`);
1196
+ }
1197
+ return;
1198
+ }
1199
+ if (!rawTarget) {
1200
+ console.error(renderError("Missing target", "Usage: fennec kill <pid|name|all> [--signal SIGTERM|SIGKILL|SIGINT]"));
1201
+ process.exit(1);
1202
+ }
1203
+ let targetPid;
1204
+ let displayName;
1205
+ const pid = parseInt(rawTarget, 10);
1206
+ if (!isNaN(pid) && String(pid) === rawTarget) {
1207
+ if (!isProcessRunning(pid)) {
1208
+ console.error(renderError("Process not found", `No process with PID ${pid} is running`));
1209
+ process.exit(1);
1210
+ }
1211
+ targetPid = pid;
1212
+ displayName = `PID ${pid}`;
1213
+ } else {
1214
+ const matches = getSystemProcesses({ name: rawTarget, userOnly: true, sortBy: "cpu" });
1215
+ if (matches.length === 0) {
1216
+ console.error(renderError("Process not found", `No running process matching "${rawTarget}"`));
1217
+ process.exit(1);
1218
+ }
1219
+ if (matches.length > 1) {
1220
+ console.error(`
1221
+ ${pc4.bold(`Multiple processes match "${rawTarget}":`)}`);
1222
+ const selected = await selectPrompt(
1223
+ `Select which to kill:`,
1224
+ matches.map((p, i) => ({
1225
+ value: String(i),
1226
+ label: `${p.name} (PID ${p.pid})`,
1227
+ description: `${p.command.slice(0, 80)} \u2014 CPU: ${p.cpuPercent}% MEM: ${p.memPercent}%`
1228
+ }))
1229
+ );
1230
+ if (selected === null) {
1231
+ console.error(` ${pc4.dim("Cancelled")}`);
1232
+ return;
1233
+ }
1234
+ const idx = parseInt(selected, 10);
1235
+ targetPid = matches[idx].pid;
1236
+ displayName = `${matches[idx].name} (PID ${targetPid})`;
1237
+ } else {
1238
+ targetPid = matches[0].pid;
1239
+ displayName = `${matches[0].name} (PID ${targetPid})`;
1240
+ }
1241
+ }
737
1242
  const confirmed = await confirmPrompt(
738
- `Stop ${pc4.bold(name)} with ${pc4.yellow(signal)}?`,
1243
+ `Kill ${pc4.bold(displayName)} with ${pc4.yellow(signal)}?`,
739
1244
  false
740
1245
  );
741
1246
  if (!confirmed) {
742
1247
  console.error(` ${pc4.dim("Cancelled")}`);
743
1248
  return;
744
1249
  }
745
- console.error(`
746
- ${pc4.green("\u2713")} ${renderAppName(name)} ${pc4.dim(`stopped (${signal})`)}
747
- `);
1250
+ const spinner = createSpinner(`Sending ${signal} to ${displayName}...`);
1251
+ const success = killProcess(targetPid, signal);
1252
+ if (success) {
1253
+ await new Promise((r) => setTimeout(r, 200));
1254
+ const stillRunning = isProcessRunning(targetPid);
1255
+ if (stillRunning && signal !== "SIGKILL") {
1256
+ spinner.warn(`${displayName} did not respond to ${signal}`);
1257
+ const forceKill = await confirmPrompt(
1258
+ `Send ${pc4.red("SIGKILL")} to force stop?`,
1259
+ true
1260
+ );
1261
+ if (forceKill) {
1262
+ const forceSpinner = createSpinner(`Sending SIGKILL to ${displayName}...`);
1263
+ killProcess(targetPid, "SIGKILL");
1264
+ await new Promise((r) => setTimeout(r, 300));
1265
+ if (!isProcessRunning(targetPid)) {
1266
+ forceSpinner.succeed(`${displayName} force stopped`);
1267
+ } else {
1268
+ forceSpinner.fail(`${displayName} could not be stopped (permission denied?)`);
1269
+ }
1270
+ } else {
1271
+ console.error(` ${pc4.dim("Retrying with SIGTERM...")}`);
1272
+ killProcess(targetPid, "SIGTERM");
1273
+ console.error(` ${pc4.yellow("\u26A0")} ${displayName} ${pc4.dim("may still be running")}`);
1274
+ }
1275
+ } else {
1276
+ removeTrackedByPid(targetPid);
1277
+ spinner.succeed(`${displayName} stopped`);
1278
+ }
1279
+ } else {
1280
+ spinner.fail(`Failed to kill ${displayName}`);
1281
+ console.error(renderError("Permission denied", `Try running with sudo or use a different signal.`));
1282
+ }
748
1283
  }
749
1284
  async function restartCommand(args2) {
750
1285
  const name = args2[0];
751
1286
  if (!name) {
752
- console.error(renderError("Missing process name", "Usage: fennec restart <name>"));
1287
+ console.error(renderError("Missing process name/pid", "Usage: fennec restart <pid|name>"));
753
1288
  process.exit(1);
754
1289
  }
755
- const spinner = createSpinner(`Restarting ${name}...`);
756
- await new Promise((resolve3) => setTimeout(resolve3, 1e3));
757
- spinner.succeed(`${name} restarted`);
1290
+ let targetPid;
1291
+ const pid = parseInt(name, 10);
1292
+ if (!isNaN(pid) && String(pid) === name) {
1293
+ targetPid = pid;
1294
+ } else {
1295
+ const processes = getSystemProcesses({ name, userOnly: true, limit: 1 });
1296
+ if (processes.length === 0) {
1297
+ console.error(renderError("Process not found", `No process matching "${name}"`));
1298
+ process.exit(1);
1299
+ }
1300
+ targetPid = processes[0].pid;
1301
+ }
1302
+ const confirmed = await confirmPrompt(
1303
+ `Restart ${pc4.bold(`${name} (PID ${targetPid})`)}?`,
1304
+ false
1305
+ );
1306
+ if (!confirmed) {
1307
+ console.error(` ${pc4.dim("Cancelled")}`);
1308
+ return;
1309
+ }
1310
+ const spinner = createSpinner(`Stopping PID ${targetPid}...`);
1311
+ const killed = killProcess(targetPid, "SIGTERM");
1312
+ if (!killed) {
1313
+ spinner.fail(`Failed to stop PID ${targetPid}`);
1314
+ return;
1315
+ }
1316
+ await new Promise((r) => setTimeout(r, 1e3));
1317
+ const stillRunning = isProcessRunning(targetPid);
1318
+ if (stillRunning) {
1319
+ spinner.warn(`Process didn't stop, sending SIGKILL...`);
1320
+ killProcess(targetPid, "SIGKILL");
1321
+ await new Promise((r) => setTimeout(r, 500));
1322
+ }
1323
+ const procInfo = getSystemProcesses({ pid: targetPid, limit: 1 })[0];
1324
+ let suggestion = "";
1325
+ if (procInfo) {
1326
+ suggestion = `
1327
+ ${pc4.dim("Re-run:")} ${renderCommand(procInfo.command)}`;
1328
+ }
1329
+ spinner.succeed(`${name} stopped${suggestion}`);
1330
+ }
1331
+ async function logCommand(args2) {
1332
+ const target = args2[0];
1333
+ if (!target) {
1334
+ console.error(renderError("Missing process name/pid", "Usage: fennec log <pid|name> [--lines N] [--level LEVEL] [-f]"));
1335
+ process.exit(1);
1336
+ }
1337
+ const linesIndex = args2.indexOf("--lines");
1338
+ const lines = linesIndex !== -1 ? parseInt(args2[linesIndex + 1], 10) : 30;
1339
+ const followFlag = args2.includes("-f") || args2.includes("--follow");
1340
+ let targetPid;
1341
+ let displayName;
1342
+ const pid = parseInt(target, 10);
1343
+ if (!isNaN(pid) && String(pid) === target) {
1344
+ targetPid = pid;
1345
+ displayName = `PID ${pid}`;
1346
+ } else {
1347
+ const processes = getSystemProcesses({ name: target, userOnly: true, limit: 1 });
1348
+ if (processes.length === 0) {
1349
+ console.error(renderError("Process not found", `No process matching "${target}"`));
1350
+ process.exit(1);
1351
+ }
1352
+ targetPid = processes[0].pid;
1353
+ displayName = `${processes[0].name} (PID ${targetPid})`;
1354
+ }
1355
+ const spinner = createSpinner(`Fetching logs for ${displayName}...`);
1356
+ try {
1357
+ let logLines = [];
1358
+ try {
1359
+ const { execSync: execSync3 } = await import("child_process");
1360
+ const output = execSync3(
1361
+ `journalctl --no-pager -n ${lines} _PID=${targetPid} 2>/dev/null || tail -n ${lines} /proc/${targetPid}/fd/1 2>/dev/null || echo "No logs available"`,
1362
+ { encoding: "utf-8", timeout: 3e3 }
1363
+ );
1364
+ logLines = output.trim().split("\n").filter(Boolean);
1365
+ } catch {
1366
+ logLines = [` ${pc4.dim("(no log access \u2014 process may not be managed by Fennec)")}`];
1367
+ }
1368
+ spinner.stop();
1369
+ process.stdout.write("\r\x1B[K");
1370
+ console.error(`
1371
+ ${symbols.fox} ${pc4.bold("Logs")} ${renderAppName(displayName)} ${pc4.dim(`(last ${lines} lines)`)}
1372
+ `);
1373
+ for (const line of logLines.slice(-lines)) {
1374
+ const display = line.length > 200 ? line.slice(0, 200) + "\u2026" : line;
1375
+ if (line.toLowerCase().includes("error") || line.toLowerCase().includes("fail")) {
1376
+ console.error(` ${pc4.red(display)}`);
1377
+ } else if (line.toLowerCase().includes("warn")) {
1378
+ console.error(` ${pc4.yellow(display)}`);
1379
+ } else {
1380
+ console.error(` ${display}`);
1381
+ }
1382
+ }
1383
+ if (followFlag) {
1384
+ console.error(`
1385
+ ${pc4.dim("Following... (Ctrl+C to stop)")}
1386
+ `);
1387
+ await new Promise(() => {
1388
+ });
1389
+ }
1390
+ console.error();
1391
+ } catch (error) {
1392
+ spinner.fail(`Failed to fetch logs for ${displayName}`);
1393
+ console.error(renderError("Log fetch failed", String(error)));
1394
+ }
758
1395
  }
759
1396
  async function attachCommand(args2) {
760
1397
  const port = parseInt(args2[0], 10);
@@ -852,7 +1489,7 @@ async function installBrowsersCommand() {
852
1489
  `);
853
1490
  const spinner = createSpinner("Installing Chromium...");
854
1491
  try {
855
- execSync("npx playwright install chromium", {
1492
+ execSync2("npx playwright install chromium", {
856
1493
  stdio: "pipe",
857
1494
  timeout: 12e4
858
1495
  });
@@ -870,7 +1507,7 @@ async function initCommand() {
870
1507
  ${pc4.bold("Initialize Fennec Configuration")}
871
1508
  `);
872
1509
  const configFile = resolve2("./fennec.config.yaml");
873
- if (existsSync2(configFile)) {
1510
+ if (existsSync3(configFile)) {
874
1511
  const overwrite = await confirmPrompt(
875
1512
  `${pc4.yellow("fennec.config.yaml")} already exists. Overwrite?`,
876
1513
  false