@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 +753 -116
- 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.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.
|
|
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
|
|
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
|
|
61
|
-
${pc2.dim("-w, --watch")} Watch mode ${pc2.dim("(refresh every
|
|
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>")}
|
|
64
|
-
${pc2.dim("--lines")} Number of lines ${pc2.dim("[default:
|
|
65
|
-
${pc2.dim("--
|
|
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>")}
|
|
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>")}
|
|
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
|
-
|
|
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
|
-
|
|
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 === "
|
|
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.
|
|
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
|
|
565
|
-
|
|
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
|
|
637
|
-
const watchFlag =
|
|
638
|
-
|
|
639
|
-
|
|
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: "
|
|
644
|
-
{ key: "
|
|
645
|
-
{ key: "status", label: "Status", format: (v) =>
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
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("
|
|
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
|
-
|
|
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
|
|
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: "
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
{ key: "mem", label: "
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
{
|
|
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
|
-
|
|
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[
|
|
1107
|
+
process.stdout.write("\x1B[J");
|
|
683
1108
|
console.error(render());
|
|
684
|
-
},
|
|
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
|
|
693
|
-
const
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
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("
|
|
705
|
-
`);
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
{
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
console.error(
|
|
718
|
-
}
|
|
719
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
`
|
|
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
|
-
|
|
746
|
-
|
|
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
|
-
|
|
756
|
-
|
|
757
|
-
|
|
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
|
-
|
|
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 (
|
|
1510
|
+
if (existsSync3(configFile)) {
|
|
874
1511
|
const overwrite = await confirmPrompt(
|
|
875
1512
|
`${pc4.yellow("fennec.config.yaml")} already exists. Overwrite?`,
|
|
876
1513
|
false
|