@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 +793 -121
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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
|
|
5
|
-
import { resolve as resolve2 } from "path";
|
|
6
|
-
import {
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
61
|
-
${pc2.dim("-w, --watch")} Watch mode ${pc2.dim("(refresh every
|
|
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>")}
|
|
64
|
-
${pc2.dim("--lines")} Number of lines ${pc2.dim("[default:
|
|
65
|
-
${pc2.dim("--
|
|
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>")}
|
|
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>")}
|
|
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
|
-
|
|
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
|
-
|
|
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 === "
|
|
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.
|
|
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
|
|
565
|
-
|
|
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
|
|
637
|
-
const watchFlag =
|
|
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
|
|
999
|
+
await watchProcesses(sortBy, limit);
|
|
640
1000
|
return;
|
|
641
1001
|
}
|
|
642
|
-
const
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
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
|
-
|
|
659
|
-
|
|
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
|
|
1086
|
+
async function watchProcesses(sortBy, limit) {
|
|
662
1087
|
console.error(`
|
|
663
|
-
${pc4.bold("Watching
|
|
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: "
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
{ key: "mem", label: "
|
|
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
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
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[
|
|
1121
|
+
process.stdout.write("\x1B[J");
|
|
683
1122
|
console.error(render());
|
|
684
|
-
},
|
|
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
|
|
693
|
-
const
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
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.
|
|
1154
|
+
${symbols.fox} ${pc4.bold("Fennec System Overview")}
|
|
722
1155
|
`);
|
|
723
|
-
|
|
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
|
|
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
|
-
`
|
|
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
|
-
|
|
746
|
-
|
|
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
|
-
|
|
756
|
-
|
|
757
|
-
|
|
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
|
-
|
|
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 (
|
|
1545
|
+
if (existsSync3(configFile)) {
|
|
874
1546
|
const overwrite = await confirmPrompt(
|
|
875
1547
|
`${pc4.yellow("fennec.config.yaml")} already exists. Overwrite?`,
|
|
876
1548
|
false
|