@plumpslabs/fennec-cli 1.11.0 → 1.11.1

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.1")}
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.1")}`;
24
28
  function printBanner() {
25
29
  if (process.stdout.isTTY) {
26
30
  console.error(FOX);
@@ -42,33 +46,46 @@ 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 real system processes")}
65
+ ${pc2.dim("--name")} Filter by process name
66
+ ${pc2.dim("--port")} Filter by port
67
+ ${pc2.dim("--sort")} Sort by ${pc2.dim("(cpu | mem | pid | name) [default: cpu]")}
68
+ ${pc2.dim("-n")} Max results ${pc2.dim("[default: 30]")}
69
+ ${pc2.dim("-w, --watch")} Watch mode ${pc2.dim("(refresh every 3s)")}
70
+ ${pc2.dim("-a, --all")} Show all processes ${pc2.dim("(not just user)")}
71
+
55
72
  ${pc2.cyan("run")} ${pc2.dim("<command>")} ${pc2.dim("Run a command under Fennec observation")}
56
73
  ${pc2.dim("--name")} Process name ${pc2.dim("(required)")}
57
74
  ${pc2.dim("--cwd")} Working directory
58
75
  ${pc2.dim("--restart")} Auto-restart on crash
59
76
 
60
- ${pc2.cyan("status")} ${pc2.dim("[name]")} ${pc2.dim("Show observed processes")}
61
- ${pc2.dim("-w, --watch")} Watch mode ${pc2.dim("(refresh every 2s)")}
77
+ ${pc2.cyan("status")} ${pc2.dim("[name]")} ${pc2.dim("Show system overview & top processes")}
78
+ ${pc2.dim("-w, --watch")} Watch mode ${pc2.dim("(refresh every 3s)")}
62
79
 
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)")}
80
+ ${pc2.cyan("log")} ${pc2.dim("<pid|name>")} ${pc2.dim("Show logs for a process")}
81
+ ${pc2.dim("--lines")} Number of lines ${pc2.dim("[default: 30]")}
82
+ ${pc2.dim("-f, --follow")} Follow mode ${pc2.dim("(journalctl --follow)")}
67
83
 
68
- ${pc2.cyan("kill")} ${pc2.dim("<name>")} ${pc2.dim("Stop a process")}
84
+ ${pc2.cyan("kill")} ${pc2.dim("<pid|name|all>")} ${pc2.dim("Kill process(es) by PID, name, or all")}
69
85
  ${pc2.dim("--signal")} Signal to send ${pc2.dim("[default: SIGTERM]")}
86
+ ${pc2.dim("--all")} Kill all user processes
70
87
 
71
- ${pc2.cyan("restart")} ${pc2.dim("<name>")} ${pc2.dim("Restart a process")}
88
+ ${pc2.cyan("restart")} ${pc2.dim("<pid|name>")} ${pc2.dim("Kill + show re-run command")}
72
89
 
73
90
  ${sep}
74
91
  ${pc2.bold("Observation")}
@@ -220,7 +237,10 @@ async function watchCommand(args2) {
220
237
  import { createInterface } from "readline";
221
238
  import pc3 from "picocolors";
222
239
  function hex(color) {
223
- return pc3.hex(color);
240
+ if (typeof pc3.hex === "function") {
241
+ return pc3.hex(color);
242
+ }
243
+ return (s) => s;
224
244
  }
225
245
  var fennecOrange3 = hex("#FF6432");
226
246
  var blueAccent = hex("#4A90D9");
@@ -262,20 +282,6 @@ var symbols = {
262
282
  fox: "\u{1F98A}",
263
283
  ears: "\u23DC"
264
284
  };
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
285
  function logLevel(label) {
280
286
  const upper = label.toUpperCase().padEnd(5);
281
287
  switch (label.toLowerCase()) {
@@ -485,14 +491,347 @@ function renderSuccess(message) {
485
491
  return ` ${pc3.green("\u2713")} ${message}`;
486
492
  }
487
493
 
494
+ // src/utils/system-process.ts
495
+ import { readdirSync, readFileSync, existsSync as existsSync2 } from "fs";
496
+ import { execSync } from "child_process";
497
+ import { cpus } from "os";
498
+ function readProcProcesses() {
499
+ const processes = [];
500
+ const currentUid = process.getuid?.();
501
+ const totalMemKb = readTotalMem();
502
+ try {
503
+ const pids = readdirSync("/proc").filter((d) => /^\d+$/.test(d));
504
+ for (const pidStr of pids) {
505
+ try {
506
+ const pid = parseInt(pidStr, 10);
507
+ const proc = readSingleProcProcess(pid, currentUid, totalMemKb);
508
+ if (proc) processes.push(proc);
509
+ } catch {
510
+ continue;
511
+ }
512
+ }
513
+ } catch {
514
+ return [];
515
+ }
516
+ return processes;
517
+ }
518
+ function readSingleProcProcess(pid, currentUid, totalMemKb) {
519
+ const statPath = `/proc/${pid}/stat`;
520
+ const statusPath = `/proc/${pid}/status`;
521
+ const cmdlinePath = `/proc/${pid}/cmdline`;
522
+ if (!existsSync2(statPath)) return null;
523
+ const statusText = readSafe(statusPath);
524
+ if (!statusText) return null;
525
+ const nameMatch = statusText.match(/Name:\s+(.+)/);
526
+ const uidMatch = statusText.match(/Uid:\s+(\d+)/);
527
+ const vmRssMatch = statusText.match(/VmRSS:\s+(\d+)/);
528
+ const stateMatch = statusText.match(/State:\s+(\S)/);
529
+ const name = nameMatch?.[1] ?? `pid_${pid}`;
530
+ const uid = uidMatch ? parseInt(uidMatch[1], 10) : void 0;
531
+ const vmRss = vmRssMatch ? parseInt(vmRssMatch[1], 10) : 0;
532
+ const state = stateMatch?.[1] ?? "?";
533
+ const isUserProcess = uid !== void 0 && currentUid !== void 0 && uid === currentUid;
534
+ const statText = readSafe(statPath);
535
+ let cpuPercent = 0;
536
+ let startedAt = null;
537
+ if (statText) {
538
+ const parenEnd = statText.lastIndexOf(")");
539
+ if (parenEnd > 0) {
540
+ const fields = statText.slice(parenEnd + 2).split(/\s+/);
541
+ const utime = parseInt(fields[11] ?? "0", 10);
542
+ const stime = parseInt(fields[12] ?? "0", 10);
543
+ const starttime = parseInt(fields[19] ?? "0", 10);
544
+ const hertz = 100;
545
+ const uptimeSeconds = readUptime();
546
+ const totalJiffies = utime + stime;
547
+ if (uptimeSeconds > 0) {
548
+ const elapsedJiffies = uptimeSeconds * hertz;
549
+ const processJiffies = totalJiffies;
550
+ cpuPercent = Math.min(100, processJiffies / Math.max(1, elapsedJiffies) * 100 * cpus().length);
551
+ }
552
+ startedAt = formatStartTime(starttime, uptimeSeconds, hertz);
553
+ }
554
+ }
555
+ const cmdlineRaw = readSafe(cmdlinePath);
556
+ const command2 = cmdlineRaw ? cmdlineRaw.split("\0").filter(Boolean).join(" ") : name;
557
+ const ports = detectListeningPorts(pid);
558
+ const memPercent = totalMemKb > 0 ? vmRss / totalMemKb * 100 : 0;
559
+ return {
560
+ pid,
561
+ name,
562
+ command: command2.length > 200 ? command2.slice(0, 200) + "\u2026" : command2,
563
+ cpuPercent: Math.round(cpuPercent * 10) / 10,
564
+ memPercent: Math.round(memPercent * 10) / 10,
565
+ memRss: vmRss,
566
+ state,
567
+ startedAt,
568
+ ports,
569
+ isUserProcess
570
+ };
571
+ }
572
+ function runPsProcesses() {
573
+ try {
574
+ const output = execSync(
575
+ "ps axo pid,comm,pcpu,pmem,rss,state,etime,user --sort=-pcpu 2>/dev/null | head -200",
576
+ { encoding: "utf-8", timeout: 5e3 }
577
+ );
578
+ const lines = output.trim().split("\n").slice(1);
579
+ const currentUser = execSync("whoami", { encoding: "utf-8", timeout: 1e3 }).trim();
580
+ return lines.map((line) => {
581
+ const parts = line.trim().split(/\s+/);
582
+ const pid = parseInt(parts[0] ?? "0", 10);
583
+ const name = parts[1] ?? "";
584
+ const cpu = parseFloat(parts[2] ?? "0");
585
+ const mem = parseFloat(parts[3] ?? "0");
586
+ const rss = parseInt(parts[4] ?? "0", 10);
587
+ const state = parts[5] ?? "?";
588
+ const elapsed = parts[6] ?? "";
589
+ const user = parts[7] ?? "";
590
+ const command2 = parts.slice(1).join(" ");
591
+ return {
592
+ pid,
593
+ name,
594
+ command: command2.length > 200 ? command2.slice(0, 200) + "\u2026" : command2,
595
+ cpuPercent: Math.round(cpu * 10) / 10,
596
+ memPercent: Math.round(mem * 10) / 10,
597
+ memRss: rss,
598
+ state: mapBsdState(state),
599
+ startedAt: elapsed || null,
600
+ ports: [],
601
+ isUserProcess: user === currentUser
602
+ };
603
+ });
604
+ } catch {
605
+ return [];
606
+ }
607
+ }
608
+ function mapBsdState(s) {
609
+ switch (s) {
610
+ case "R":
611
+ return "R";
612
+ case "S":
613
+ return "S";
614
+ case "Z":
615
+ return "Z";
616
+ case "T":
617
+ return "T";
618
+ case "I":
619
+ return "S";
620
+ default:
621
+ return s;
622
+ }
623
+ }
624
+ function detectListeningPorts(pid) {
625
+ try {
626
+ const tcpFile = `/proc/${pid}/net/tcp`;
627
+ const tcp6File = `/proc/${pid}/net/tcp6`;
628
+ const ports = [];
629
+ for (const f of [tcpFile, tcp6File]) {
630
+ try {
631
+ const data = readSafe(f);
632
+ if (data) {
633
+ for (const port of parseProcNetTcp(data)) {
634
+ if (!ports.includes(port)) ports.push(port);
635
+ }
636
+ }
637
+ } catch {
638
+ continue;
639
+ }
640
+ }
641
+ return ports.sort((a, b) => a - b);
642
+ } catch {
643
+ return [];
644
+ }
645
+ }
646
+ function parseProcNetTcp(data) {
647
+ const ports = [];
648
+ const lines = data.split("\n").slice(1);
649
+ for (const line of lines) {
650
+ const parts = line.trim().split(/\s+/);
651
+ if (parts.length < 3) continue;
652
+ const localAddr = parts[1] ?? "";
653
+ const state = parts[3] ?? "";
654
+ if (state !== "0A") continue;
655
+ const portMatch = localAddr.match(/:([0-9a-fA-F]+)$/);
656
+ if (portMatch) {
657
+ const port = parseInt(portMatch[1], 16);
658
+ if (port > 0 && port <= 65535) {
659
+ ports.push(port);
660
+ }
661
+ }
662
+ }
663
+ return ports;
664
+ }
665
+ function readSafe(path) {
666
+ try {
667
+ return readFileSync(path, "utf-8");
668
+ } catch {
669
+ return null;
670
+ }
671
+ }
672
+ function readTotalMem() {
673
+ try {
674
+ const memInfo = readSafe("/proc/meminfo");
675
+ if (memInfo) {
676
+ const match = memInfo.match(/MemTotal:\s+(\d+)/);
677
+ if (match) return parseInt(match[1], 10);
678
+ }
679
+ } catch {
680
+ }
681
+ return 16e6;
682
+ }
683
+ function readUptime() {
684
+ try {
685
+ const data = readSafe("/proc/uptime");
686
+ if (data) {
687
+ return parseFloat(data.split(/\s+/)[0] ?? "0");
688
+ }
689
+ } catch {
690
+ }
691
+ return 0;
692
+ }
693
+ function formatStartTime(starttime, uptimeSeconds, hertz) {
694
+ try {
695
+ const bootTime = Date.now() / 1e3 - uptimeSeconds;
696
+ const processStart = bootTime + starttime / hertz;
697
+ return new Date(processStart * 1e3).toISOString();
698
+ } catch {
699
+ return null;
700
+ }
701
+ }
702
+ function getSystemProcesses(filter) {
703
+ let processes;
704
+ if (existsSync2("/proc")) {
705
+ processes = readProcProcesses();
706
+ } else {
707
+ processes = runPsProcesses();
708
+ }
709
+ if (filter) {
710
+ if (filter.userOnly) {
711
+ processes = processes.filter((p) => p.isUserProcess);
712
+ }
713
+ if (filter.name) {
714
+ const nameLower = filter.name.toLowerCase();
715
+ processes = processes.filter(
716
+ (p) => p.name.toLowerCase().includes(nameLower) || p.command.toLowerCase().includes(nameLower)
717
+ );
718
+ }
719
+ if (filter.pid) {
720
+ processes = processes.filter((p) => p.pid === filter.pid);
721
+ }
722
+ if (filter.port) {
723
+ processes = processes.filter((p) => p.ports.includes(filter.port));
724
+ }
725
+ if (filter.sortBy) {
726
+ processes.sort((a, b) => {
727
+ switch (filter.sortBy) {
728
+ case "cpu":
729
+ return b.cpuPercent - a.cpuPercent;
730
+ case "mem":
731
+ return b.memPercent - a.memPercent;
732
+ case "pid":
733
+ return a.pid - b.pid;
734
+ case "name":
735
+ return a.name.localeCompare(b.name);
736
+ default:
737
+ return b.cpuPercent - a.cpuPercent;
738
+ }
739
+ });
740
+ }
741
+ if (filter.limit && filter.limit > 0) {
742
+ processes = processes.slice(0, filter.limit);
743
+ }
744
+ }
745
+ return processes;
746
+ }
747
+ function killProcess(pid, signal = "SIGTERM") {
748
+ try {
749
+ process.kill(pid, signal);
750
+ return true;
751
+ } catch {
752
+ return false;
753
+ }
754
+ }
755
+ function isProcessRunning(pid) {
756
+ try {
757
+ process.kill(pid, 0);
758
+ return true;
759
+ } catch {
760
+ return false;
761
+ }
762
+ }
763
+ function formatProcessState(state) {
764
+ switch (state) {
765
+ case "R":
766
+ return "Running";
767
+ case "S":
768
+ return "Sleeping";
769
+ case "D":
770
+ return "Disk Sleep";
771
+ case "Z":
772
+ return "Zombie";
773
+ case "T":
774
+ return "Stopped";
775
+ case "t":
776
+ return "Tracing";
777
+ case "X":
778
+ return "Dead";
779
+ case "I":
780
+ return "Idle";
781
+ default:
782
+ return state;
783
+ }
784
+ }
785
+
488
786
  // src/index.ts
489
787
  var [, , command, ...args] = process.argv;
788
+ function getTrackedPath() {
789
+ const dir = process.env.FENNEC_DATA_DIR ? resolve2(process.env.FENNEC_DATA_DIR) : resolve2(homedir(), ".fennec");
790
+ return resolve2(dir, "tracked.json");
791
+ }
792
+ function readTracked() {
793
+ try {
794
+ const path = getTrackedPath();
795
+ if (!existsSync3(path)) return [];
796
+ return JSON.parse(readFileSync2(path, "utf-8"));
797
+ } catch {
798
+ return [];
799
+ }
800
+ }
801
+ function saveTracked(processes) {
802
+ try {
803
+ const path = getTrackedPath();
804
+ mkdirSync(dirname(path), { recursive: true });
805
+ writeFileSync(path, JSON.stringify(processes, null, 2), "utf-8");
806
+ } catch {
807
+ }
808
+ }
809
+ function addTracked(proc) {
810
+ const tracked = readTracked();
811
+ const filtered = tracked.filter((t) => t.name !== proc.name);
812
+ filtered.push(proc);
813
+ saveTracked(filtered);
814
+ }
815
+ function removeTracked(name) {
816
+ const tracked = readTracked();
817
+ saveTracked(tracked.filter((t) => t.name !== name));
818
+ }
819
+ function removeTrackedByPid(pid) {
820
+ const tracked = readTracked();
821
+ saveTracked(tracked.filter((t) => t.pid !== pid));
822
+ }
490
823
  async function main() {
491
824
  if (!command || command === "start") {
492
- await startServer(args);
825
+ if (args.length === 0 || args[0]?.startsWith("--")) {
826
+ await startServer(args);
827
+ } else {
828
+ await startCommand(args);
829
+ }
493
830
  } else if (command === "run") {
494
831
  await runCommand(args);
495
- } else if (command === "status" || command === "ps") {
832
+ } else if (command === "ps") {
833
+ await psCommand(args);
834
+ } else if (command === "status") {
496
835
  await statusCommand(args);
497
836
  } else if (command === "log") {
498
837
  await logCommand(args);
@@ -521,7 +860,7 @@ async function main() {
521
860
  printBanner();
522
861
  await initCommand();
523
862
  } else if (command === "version" || command === "--version" || command === "-v") {
524
- console.log(` ${symbols.fox} ${pc4.bold("Fennec")} ${pc4.dim("v1.11.0")}`);
863
+ console.log(` ${symbols.fox} ${pc4.bold("Fennec")} ${pc4.dim("v1.11.1")}`);
525
864
  } else if (command === "help" || command === "--help" || command === "-h") {
526
865
  console.log(pc4.dim("\n Use 'fennec help' for more information.\n"));
527
866
  printBanner();
@@ -555,20 +894,26 @@ async function startServer(args2) {
555
894
  process.exit(1);
556
895
  }
557
896
  }
897
+ async function startCommand(args2) {
898
+ printBanner();
899
+ console.error(` ${pc4.dim("Starting app...")}
900
+ `);
901
+ await runCommand(args2);
902
+ }
558
903
  async function runCommand(args2) {
559
904
  const nameIndex = args2.indexOf("--name");
560
905
  const name = nameIndex !== -1 ? args2[nameIndex + 1] : void 0;
906
+ const portIndex = args2.indexOf("--port");
907
+ const port = portIndex !== -1 ? parseInt(args2[portIndex + 1], 10) : void 0;
561
908
  const cwdIndex = args2.indexOf("--cwd");
562
909
  const cwd = cwdIndex !== -1 ? args2[cwdIndex + 1] : void 0;
563
910
  const restartFlag = args2.includes("--restart");
564
- const cmdEnd = Math.min(
565
- nameIndex !== -1 ? nameIndex : Infinity,
566
- cwdIndex !== -1 ? cwdIndex : Infinity
567
- );
911
+ const stopFlags = [nameIndex, portIndex, cwdIndex].filter((i) => i !== -1);
912
+ const cmdEnd = Math.min(...stopFlags, Infinity);
568
913
  const cmdParts = args2.slice(0, cmdEnd);
569
914
  const cmd = cmdParts.join(" ");
570
915
  if (!cmd) {
571
- console.error(renderError("Missing command", "Usage: fennec run <command> --name <name>"));
916
+ console.error(renderError("Missing command", "Usage: fennec start|run <command> --name <name> [--port <port>] [--cwd <dir>] [--restart]"));
572
917
  process.exit(1);
573
918
  }
574
919
  const appName = name ?? cmdParts[0] ?? "app";
@@ -576,6 +921,7 @@ async function runCommand(args2) {
576
921
  ${symbols.fox} ${pc4.bold("Running")} ${renderAppName(appName)}
577
922
  `);
578
923
  console.error(` ${renderKV("Command", cmd)}`);
924
+ if (port) console.error(` ${renderKV("Port", String(port))}`);
579
925
  if (cwd) console.error(` ${renderKV("Directory", cwd)}`);
580
926
  console.error(` ${renderKV("Restart", restartFlag ? "on crash" : "off")}`);
581
927
  console.error(` ${divider()}`);
@@ -587,8 +933,16 @@ async function runCommand(args2) {
587
933
  const pm = new ProcessManager(config);
588
934
  try {
589
935
  const proc = pm.spawn(cmdParts[0], cmdParts.slice(1), cwd, void 0, appName);
936
+ addTracked({
937
+ name: appName,
938
+ pid: proc.pid,
939
+ command: cmd,
940
+ port,
941
+ cwd,
942
+ startedAt: (/* @__PURE__ */ new Date()).toISOString()
943
+ });
590
944
  console.error(`
591
- ${pc4.green("\u25CF")} ${pc4.bold(appName)} ${pc4.dim(`started (PID: ${proc.pid})`)}
945
+ ${pc4.green("\u25CF")} ${pc4.bold(appName)} ${pc4.dim(`started (PID: ${proc.pid})`)}${port ? pc4.dim(` :${port}`) : ""}
592
946
  `);
593
947
  const stdout = proc.child.stdout;
594
948
  const stderr = proc.child.stderr;
@@ -616,6 +970,7 @@ async function runCommand(args2) {
616
970
  });
617
971
  }
618
972
  proc.child.on("exit", (code, signal) => {
973
+ removeTracked(appName);
619
974
  if (restartFlag && code !== 0) {
620
975
  console.error(`
621
976
  ${pc4.yellow("\u26A0")} ${appName} ${pc4.dim(`exited (${code}), restarting...`)}`);
@@ -633,55 +988,139 @@ async function runCommand(args2) {
633
988
  process.exit(1);
634
989
  }
635
990
  }
636
- async function statusCommand(_args) {
637
- const watchFlag = _args.includes("-w") || _args.includes("--watch");
991
+ async function psCommand(args2) {
992
+ const watchFlag = args2.includes("-w") || args2.includes("--watch");
993
+ const nameFilter = args2.includes("--name") ? args2[args2.indexOf("--name") + 1] : void 0;
994
+ const portFilter = args2.includes("--port") ? parseInt(args2[args2.indexOf("--port") + 1], 10) : void 0;
995
+ const sortBy = args2.includes("--sort") ? args2[args2.indexOf("--sort") + 1] : args2.includes("-m") ? "mem" : args2.includes("-c") ? "cpu" : "cpu";
996
+ const limit = args2.includes("-n") ? parseInt(args2[args2.indexOf("-n") + 1], 10) : 30;
997
+ const allFlag = args2.includes("-a") || args2.includes("--all");
638
998
  if (watchFlag) {
639
- await watchStatus();
999
+ await watchProcesses(sortBy, limit);
640
1000
  return;
641
1001
  }
642
- 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 }
654
- ];
655
- console.error(`
656
- ${symbols.fox} ${pc4.bold("Observed Processes")}
1002
+ const spinner = createSpinner("Scanning system processes...");
1003
+ try {
1004
+ const processes = getSystemProcesses({
1005
+ name: nameFilter,
1006
+ port: portFilter,
1007
+ userOnly: !allFlag,
1008
+ sortBy,
1009
+ limit
1010
+ });
1011
+ spinner.stop();
1012
+ process.stdout.write("\r\x1B[K");
1013
+ if (processes.length === 0) {
1014
+ console.error(`
1015
+ ${pc4.dim("No processes found.")}
657
1016
  `);
658
- console.error(renderTable(columns, rows));
659
- console.error();
1017
+ return;
1018
+ }
1019
+ const columns = [
1020
+ { key: "pid", label: "PID", align: "right", format: (v) => pc4.dim(String(v).padStart(6)) },
1021
+ { key: "name", label: "Name", format: (v) => pc4.bold(String(v)) },
1022
+ { key: "cpu", label: "CPU%", align: "right", format: (v) => {
1023
+ const num = v;
1024
+ return num > 10 ? pc4.red(String(num)) : num > 5 ? pc4.yellow(String(num)) : pc4.dim(String(num));
1025
+ } },
1026
+ { key: "mem", label: "MEM%", align: "right", format: (v) => {
1027
+ const num = v;
1028
+ return num > 10 ? pc4.red(String(num)) : num > 5 ? pc4.yellow(String(num)) : pc4.dim(String(num));
1029
+ } },
1030
+ { key: "state", label: "State", format: (v) => {
1031
+ const s = String(v);
1032
+ if (s === "R" || s === "Running") return pc4.green(s);
1033
+ if (s === "Z" || s === "Zombie") return pc4.red(s);
1034
+ if (s === "S" || s === "Sleeping") return pc4.cyan(s);
1035
+ return pc4.dim(s);
1036
+ } },
1037
+ { key: "ports", label: "Ports", format: (v) => {
1038
+ const ports = v;
1039
+ return ports.length > 0 ? ports.map((p) => pc4.yellow(`:${p}`)).join(", ") : pc4.dim("-");
1040
+ } }
1041
+ ];
1042
+ const rows = processes.map((p) => ({
1043
+ pid: p.pid,
1044
+ name: p.name,
1045
+ cpu: p.cpuPercent,
1046
+ mem: p.memPercent,
1047
+ state: formatProcessState(p.state),
1048
+ ports: p.ports
1049
+ }));
1050
+ const tracked = readTracked();
1051
+ const managedPids = new Set(tracked.map((t) => t.pid));
1052
+ const managedRows = processes.map((p) => {
1053
+ const isManaged = managedPids.has(p.pid);
1054
+ const trackedInfo = tracked.find((t) => t.pid === p.pid);
1055
+ return {
1056
+ ...{
1057
+ pid: p.pid,
1058
+ name: p.name,
1059
+ cpu: p.cpuPercent,
1060
+ mem: p.memPercent,
1061
+ state: formatProcessState(p.state),
1062
+ ports: p.ports
1063
+ },
1064
+ _managed: isManaged,
1065
+ _trackedPort: trackedInfo?.port
1066
+ };
1067
+ });
1068
+ if (tracked.length > 0) {
1069
+ console.error(` ${pc4.green("\u25CF")} ${pc4.bold("Managed:")} ${tracked.map((t) => {
1070
+ const running = isProcessRunning(t.pid);
1071
+ const status = running ? pc4.green("running") : pc4.red("stopped");
1072
+ const portStr = t.port ? ` ${pc4.yellow(`:${t.port}`)}` : "";
1073
+ return `${pc4.bold(t.name)}${portStr} (${status})`;
1074
+ }).join(", ")}
1075
+ `);
1076
+ }
1077
+ console.error(` ${symbols.fox} ${pc4.bold("System Processes")} ${pc4.dim(`(${processes.length} shown, sorted by ${sortBy})`)}
1078
+ `);
1079
+ console.error(renderTable(columns, managedRows));
1080
+ console.error();
1081
+ } catch (error) {
1082
+ spinner.fail("Failed to scan processes");
1083
+ console.error(renderError("Process scan failed", String(error)));
1084
+ }
660
1085
  }
661
- async function watchStatus() {
1086
+ async function watchProcesses(sortBy, limit) {
662
1087
  console.error(`
663
- ${pc4.bold("Watching status")} ${pc4.dim("(Ctrl+C to stop)")}
1088
+ ${pc4.bold("Watching processes")} ${pc4.dim("(Ctrl+C to stop, refreshes every 3s)")}
664
1089
  `);
665
1090
  const render = () => {
1091
+ const processes = getSystemProcesses({
1092
+ userOnly: true,
1093
+ sortBy,
1094
+ limit
1095
+ });
666
1096
  const columns = [
1097
+ { key: "pid", label: "PID", align: "right", format: (v) => pc4.dim(String(v).padStart(6)) },
667
1098
  { 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" }
1099
+ { key: "cpu", label: "CPU%", align: "right", format: (v) => {
1100
+ const num = v;
1101
+ return num > 10 ? pc4.red(String(num)) : num > 5 ? pc4.yellow(String(num)) : pc4.dim(String(num));
1102
+ } },
1103
+ { key: "mem", label: "MEM%", align: "right", format: (v) => {
1104
+ const num = v;
1105
+ return num > 10 ? pc4.red(String(num)) : num > 5 ? pc4.yellow(String(num)) : pc4.dim(String(num));
1106
+ } },
1107
+ { key: "state", label: "S", align: "center" }
673
1108
  ];
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" }
677
- ];
678
- return renderTable(columns, rows, { compact: true });
1109
+ const rows = processes.map((p) => ({
1110
+ pid: p.pid,
1111
+ name: p.name,
1112
+ cpu: p.cpuPercent,
1113
+ mem: p.memPercent,
1114
+ state: p.state
1115
+ }));
1116
+ return ` ${timestamp()} ${pc4.dim(`${processes.length} processes`)}
1117
+ ${renderTable(columns, rows, { compact: true })}`;
679
1118
  };
680
1119
  console.error(render());
681
1120
  const interval = setInterval(() => {
682
- process.stdout.write("\x1B[6A");
1121
+ process.stdout.write("\x1B[J");
683
1122
  console.error(render());
684
- }, 2e3);
1123
+ }, 3e3);
685
1124
  process.on("SIGINT", () => {
686
1125
  clearInterval(interval);
687
1126
  process.exit(0);
@@ -689,72 +1128,305 @@ async function watchStatus() {
689
1128
  await new Promise(() => {
690
1129
  });
691
1130
  }
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");
703
- 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) {
1131
+ async function statusCommand(_args) {
1132
+ const watchFlag = _args.includes("-w") || _args.includes("--watch");
1133
+ const spinner = createSpinner("Gathering system status...");
1134
+ try {
1135
+ const topProcesses = getSystemProcesses({ userOnly: true, sortBy: "cpu", limit: 10 });
1136
+ const totalProcesses = getSystemProcesses({ userOnly: true }).length;
1137
+ const services = [
1138
+ { name: "node", label: "Node.js" },
1139
+ { name: "npm", label: "npm" },
1140
+ { name: "pnpm", label: "pnpm" },
1141
+ { name: "vite", label: "Vite" },
1142
+ { name: "next", label: "Next.js" },
1143
+ { name: "webpack", label: "Webpack" },
1144
+ { name: "tsc", label: "TypeScript" },
1145
+ { name: "docker", label: "Docker" },
1146
+ { name: "mysqld", label: "MySQL" },
1147
+ { name: "postgres", label: "PostgreSQL" },
1148
+ { name: "redis", label: "Redis" },
1149
+ { name: "ollama", label: "Ollama" }
1150
+ ];
1151
+ spinner.stop();
1152
+ process.stdout.write("\r\x1B[K");
720
1153
  console.error(`
721
- ${pc4.dim("Following... (Ctrl+C to stop)")}
1154
+ ${symbols.fox} ${pc4.bold("Fennec System Overview")}
722
1155
  `);
723
- await new Promise(() => {
724
- });
1156
+ console.error(` ${renderKV("User Processes", pc4.bold(String(totalProcesses)))}`);
1157
+ console.error(` ${renderKV("Fennec Version", pc4.dim("v1.11.1"))}`);
1158
+ const activeServices = services.filter((s) => topProcesses.some((p) => p.name.toLowerCase().includes(s.name))).slice(0, 5);
1159
+ if (activeServices.length > 0) {
1160
+ console.error(` ${renderKV("Detected", activeServices.map((s) => pc4.green(s.label)).join(", "))}`);
1161
+ }
1162
+ console.error();
1163
+ const columns = [
1164
+ { key: "pid", label: "PID", align: "right", format: (v) => pc4.dim(String(v).padStart(6)) },
1165
+ { key: "name", label: "Process", format: (v) => pc4.bold(String(v)) },
1166
+ { key: "cpu", label: "CPU%", align: "right" },
1167
+ { key: "mem", label: "MEM%", align: "right" }
1168
+ ];
1169
+ const rows = topProcesses.map((p) => ({
1170
+ pid: p.pid,
1171
+ name: p.name,
1172
+ cpu: p.cpuPercent,
1173
+ mem: p.memPercent
1174
+ }));
1175
+ console.error(` ${pc4.bold("Top CPU Processes")}
1176
+ `);
1177
+ console.error(renderTable(columns, rows));
1178
+ if (watchFlag) {
1179
+ await watchProcesses("cpu", 15);
1180
+ }
1181
+ console.error();
1182
+ } catch (error) {
1183
+ spinner.fail("Failed to gather system status");
1184
+ console.error(renderError("Status check failed", String(error)));
725
1185
  }
726
- console.error();
727
1186
  }
728
1187
  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
- }
1188
+ const rawTarget = args2[0];
734
1189
  const signalIndex = args2.indexOf("--signal");
735
1190
  const signalRaw = signalIndex !== -1 ? args2[signalIndex + 1] : "SIGTERM";
736
1191
  const signal = signalRaw ?? "SIGTERM";
1192
+ if (rawTarget === "all" || args2.includes("--all") || args2.includes("-a")) {
1193
+ const userProcs = getSystemProcesses({ userOnly: true, sortBy: "cpu", limit: 200 });
1194
+ if (userProcs.length === 0) {
1195
+ console.error(` ${pc4.dim("No user processes to kill.")}`);
1196
+ return;
1197
+ }
1198
+ console.error(`
1199
+ ${pc4.bold(`Kill ${userProcs.length} user processes?`)}`);
1200
+ console.error(` ${pc4.dim("This will stop ALL your running processes.")}`);
1201
+ console.error(` ${pc4.yellow("\u26A0 System processes will not be affected.")}
1202
+ `);
1203
+ const confirmed2 = await confirmPrompt(
1204
+ `${pc4.red("Are you sure?")} ${pc4.dim("This cannot be undone")}`,
1205
+ false
1206
+ );
1207
+ if (!confirmed2) {
1208
+ console.error(` ${pc4.dim("Cancelled.")}`);
1209
+ return;
1210
+ }
1211
+ const spinner2 = createSpinner(`Killing ${userProcs.length} processes...`);
1212
+ let killed = 0;
1213
+ let failed = 0;
1214
+ for (const proc of userProcs) {
1215
+ if (killProcess(proc.pid, signal)) {
1216
+ killed++;
1217
+ removeTrackedByPid(proc.pid);
1218
+ } else {
1219
+ failed++;
1220
+ }
1221
+ if (killed % 10 === 0) await new Promise((r) => setTimeout(r, 50));
1222
+ }
1223
+ await new Promise((r) => setTimeout(r, 500));
1224
+ if (killed > 0) {
1225
+ spinner2.succeed(`${killed} process(es) killed`);
1226
+ } else {
1227
+ spinner2.fail(`Failed to kill processes`);
1228
+ }
1229
+ if (failed > 0) {
1230
+ console.error(` ${pc4.yellow(`${failed} process(es) could not be killed`)} ${pc4.dim("(try with sudo)")}`);
1231
+ }
1232
+ return;
1233
+ }
1234
+ if (!rawTarget) {
1235
+ console.error(renderError("Missing target", "Usage: fennec kill <pid|name|all> [--signal SIGTERM|SIGKILL|SIGINT]"));
1236
+ process.exit(1);
1237
+ }
1238
+ let targetPid;
1239
+ let displayName;
1240
+ const pid = parseInt(rawTarget, 10);
1241
+ if (!isNaN(pid) && String(pid) === rawTarget) {
1242
+ if (!isProcessRunning(pid)) {
1243
+ console.error(renderError("Process not found", `No process with PID ${pid} is running`));
1244
+ process.exit(1);
1245
+ }
1246
+ targetPid = pid;
1247
+ displayName = `PID ${pid}`;
1248
+ } else {
1249
+ const matches = getSystemProcesses({ name: rawTarget, userOnly: true, sortBy: "cpu" });
1250
+ if (matches.length === 0) {
1251
+ console.error(renderError("Process not found", `No running process matching "${rawTarget}"`));
1252
+ process.exit(1);
1253
+ }
1254
+ if (matches.length > 1) {
1255
+ console.error(`
1256
+ ${pc4.bold(`Multiple processes match "${rawTarget}":`)}`);
1257
+ const selected = await selectPrompt(
1258
+ `Select which to kill:`,
1259
+ matches.map((p, i) => ({
1260
+ value: String(i),
1261
+ label: `${p.name} (PID ${p.pid})`,
1262
+ description: `${p.command.slice(0, 80)} \u2014 CPU: ${p.cpuPercent}% MEM: ${p.memPercent}%`
1263
+ }))
1264
+ );
1265
+ if (selected === null) {
1266
+ console.error(` ${pc4.dim("Cancelled")}`);
1267
+ return;
1268
+ }
1269
+ const idx = parseInt(selected, 10);
1270
+ targetPid = matches[idx].pid;
1271
+ displayName = `${matches[idx].name} (PID ${targetPid})`;
1272
+ } else {
1273
+ targetPid = matches[0].pid;
1274
+ displayName = `${matches[0].name} (PID ${targetPid})`;
1275
+ }
1276
+ }
737
1277
  const confirmed = await confirmPrompt(
738
- `Stop ${pc4.bold(name)} with ${pc4.yellow(signal)}?`,
1278
+ `Kill ${pc4.bold(displayName)} with ${pc4.yellow(signal)}?`,
739
1279
  false
740
1280
  );
741
1281
  if (!confirmed) {
742
1282
  console.error(` ${pc4.dim("Cancelled")}`);
743
1283
  return;
744
1284
  }
745
- console.error(`
746
- ${pc4.green("\u2713")} ${renderAppName(name)} ${pc4.dim(`stopped (${signal})`)}
747
- `);
1285
+ const spinner = createSpinner(`Sending ${signal} to ${displayName}...`);
1286
+ const success = killProcess(targetPid, signal);
1287
+ if (success) {
1288
+ await new Promise((r) => setTimeout(r, 200));
1289
+ const stillRunning = isProcessRunning(targetPid);
1290
+ if (stillRunning && signal !== "SIGKILL") {
1291
+ spinner.warn(`${displayName} did not respond to ${signal}`);
1292
+ const forceKill = await confirmPrompt(
1293
+ `Send ${pc4.red("SIGKILL")} to force stop?`,
1294
+ true
1295
+ );
1296
+ if (forceKill) {
1297
+ const forceSpinner = createSpinner(`Sending SIGKILL to ${displayName}...`);
1298
+ killProcess(targetPid, "SIGKILL");
1299
+ await new Promise((r) => setTimeout(r, 300));
1300
+ if (!isProcessRunning(targetPid)) {
1301
+ forceSpinner.succeed(`${displayName} force stopped`);
1302
+ } else {
1303
+ forceSpinner.fail(`${displayName} could not be stopped (permission denied?)`);
1304
+ }
1305
+ } else {
1306
+ console.error(` ${pc4.dim("Retrying with SIGTERM...")}`);
1307
+ killProcess(targetPid, "SIGTERM");
1308
+ console.error(` ${pc4.yellow("\u26A0")} ${displayName} ${pc4.dim("may still be running")}`);
1309
+ }
1310
+ } else {
1311
+ removeTrackedByPid(targetPid);
1312
+ spinner.succeed(`${displayName} stopped`);
1313
+ }
1314
+ } else {
1315
+ spinner.fail(`Failed to kill ${displayName}`);
1316
+ console.error(renderError("Permission denied", `Try running with sudo or use a different signal.`));
1317
+ }
748
1318
  }
749
1319
  async function restartCommand(args2) {
750
1320
  const name = args2[0];
751
1321
  if (!name) {
752
- console.error(renderError("Missing process name", "Usage: fennec restart <name>"));
1322
+ console.error(renderError("Missing process name/pid", "Usage: fennec restart <pid|name>"));
753
1323
  process.exit(1);
754
1324
  }
755
- const spinner = createSpinner(`Restarting ${name}...`);
756
- await new Promise((resolve3) => setTimeout(resolve3, 1e3));
757
- spinner.succeed(`${name} restarted`);
1325
+ let targetPid;
1326
+ const pid = parseInt(name, 10);
1327
+ if (!isNaN(pid) && String(pid) === name) {
1328
+ targetPid = pid;
1329
+ } else {
1330
+ const processes = getSystemProcesses({ name, userOnly: true, limit: 1 });
1331
+ if (processes.length === 0) {
1332
+ console.error(renderError("Process not found", `No process matching "${name}"`));
1333
+ process.exit(1);
1334
+ }
1335
+ targetPid = processes[0].pid;
1336
+ }
1337
+ const confirmed = await confirmPrompt(
1338
+ `Restart ${pc4.bold(`${name} (PID ${targetPid})`)}?`,
1339
+ false
1340
+ );
1341
+ if (!confirmed) {
1342
+ console.error(` ${pc4.dim("Cancelled")}`);
1343
+ return;
1344
+ }
1345
+ const spinner = createSpinner(`Stopping PID ${targetPid}...`);
1346
+ const killed = killProcess(targetPid, "SIGTERM");
1347
+ if (!killed) {
1348
+ spinner.fail(`Failed to stop PID ${targetPid}`);
1349
+ return;
1350
+ }
1351
+ await new Promise((r) => setTimeout(r, 1e3));
1352
+ const stillRunning = isProcessRunning(targetPid);
1353
+ if (stillRunning) {
1354
+ spinner.warn(`Process didn't stop, sending SIGKILL...`);
1355
+ killProcess(targetPid, "SIGKILL");
1356
+ await new Promise((r) => setTimeout(r, 500));
1357
+ }
1358
+ const procInfo = getSystemProcesses({ pid: targetPid, limit: 1 })[0];
1359
+ let suggestion = "";
1360
+ if (procInfo) {
1361
+ suggestion = `
1362
+ ${pc4.dim("Re-run:")} ${renderCommand(procInfo.command)}`;
1363
+ }
1364
+ spinner.succeed(`${name} stopped${suggestion}`);
1365
+ }
1366
+ async function logCommand(args2) {
1367
+ const target = args2[0];
1368
+ if (!target) {
1369
+ console.error(renderError("Missing process name/pid", "Usage: fennec log <pid|name> [--lines N] [--level LEVEL] [-f]"));
1370
+ process.exit(1);
1371
+ }
1372
+ const linesIndex = args2.indexOf("--lines");
1373
+ const lines = linesIndex !== -1 ? parseInt(args2[linesIndex + 1], 10) : 30;
1374
+ const followFlag = args2.includes("-f") || args2.includes("--follow");
1375
+ let targetPid;
1376
+ let displayName;
1377
+ const pid = parseInt(target, 10);
1378
+ if (!isNaN(pid) && String(pid) === target) {
1379
+ targetPid = pid;
1380
+ displayName = `PID ${pid}`;
1381
+ } else {
1382
+ const processes = getSystemProcesses({ name: target, userOnly: true, limit: 1 });
1383
+ if (processes.length === 0) {
1384
+ console.error(renderError("Process not found", `No process matching "${target}"`));
1385
+ process.exit(1);
1386
+ }
1387
+ targetPid = processes[0].pid;
1388
+ displayName = `${processes[0].name} (PID ${targetPid})`;
1389
+ }
1390
+ const spinner = createSpinner(`Fetching logs for ${displayName}...`);
1391
+ try {
1392
+ let logLines = [];
1393
+ try {
1394
+ const { execSync: execSync3 } = await import("child_process");
1395
+ const output = execSync3(
1396
+ `journalctl --no-pager -n ${lines} _PID=${targetPid} 2>/dev/null || tail -n ${lines} /proc/${targetPid}/fd/1 2>/dev/null || echo "No logs available"`,
1397
+ { encoding: "utf-8", timeout: 3e3 }
1398
+ );
1399
+ logLines = output.trim().split("\n").filter(Boolean);
1400
+ } catch {
1401
+ logLines = [` ${pc4.dim("(no log access \u2014 process may not be managed by Fennec)")}`];
1402
+ }
1403
+ spinner.stop();
1404
+ process.stdout.write("\r\x1B[K");
1405
+ console.error(`
1406
+ ${symbols.fox} ${pc4.bold("Logs")} ${renderAppName(displayName)} ${pc4.dim(`(last ${lines} lines)`)}
1407
+ `);
1408
+ for (const line of logLines.slice(-lines)) {
1409
+ const display = line.length > 200 ? line.slice(0, 200) + "\u2026" : line;
1410
+ if (line.toLowerCase().includes("error") || line.toLowerCase().includes("fail")) {
1411
+ console.error(` ${pc4.red(display)}`);
1412
+ } else if (line.toLowerCase().includes("warn")) {
1413
+ console.error(` ${pc4.yellow(display)}`);
1414
+ } else {
1415
+ console.error(` ${display}`);
1416
+ }
1417
+ }
1418
+ if (followFlag) {
1419
+ console.error(`
1420
+ ${pc4.dim("Following... (Ctrl+C to stop)")}
1421
+ `);
1422
+ await new Promise(() => {
1423
+ });
1424
+ }
1425
+ console.error();
1426
+ } catch (error) {
1427
+ spinner.fail(`Failed to fetch logs for ${displayName}`);
1428
+ console.error(renderError("Log fetch failed", String(error)));
1429
+ }
758
1430
  }
759
1431
  async function attachCommand(args2) {
760
1432
  const port = parseInt(args2[0], 10);
@@ -852,7 +1524,7 @@ async function installBrowsersCommand() {
852
1524
  `);
853
1525
  const spinner = createSpinner("Installing Chromium...");
854
1526
  try {
855
- execSync("npx playwright install chromium", {
1527
+ execSync2("npx playwright install chromium", {
856
1528
  stdio: "pipe",
857
1529
  timeout: 12e4
858
1530
  });
@@ -870,7 +1542,7 @@ async function initCommand() {
870
1542
  ${pc4.bold("Initialize Fennec Configuration")}
871
1543
  `);
872
1544
  const configFile = resolve2("./fennec.config.yaml");
873
- if (existsSync2(configFile)) {
1545
+ if (existsSync3(configFile)) {
874
1546
  const overwrite = await confirmPrompt(
875
1547
  `${pc4.yellow("fennec.config.yaml")} already exists. Overwrite?`,
876
1548
  false