@plumpslabs/fennec-cli 1.12.2 → 1.12.3
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 +218 -231
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -2088,7 +2088,7 @@ function hexColor(color) {
|
|
|
2088
2088
|
}
|
|
2089
2089
|
var fennecOrange2 = hexColor("#FF6432");
|
|
2090
2090
|
var fennecGold = hexColor("#FFB347");
|
|
2091
|
-
var VERSION = "1.12.
|
|
2091
|
+
var VERSION = "1.12.3";
|
|
2092
2092
|
var cachedBanner = null;
|
|
2093
2093
|
var cachedCompact = null;
|
|
2094
2094
|
function generateBanner() {
|
|
@@ -2220,6 +2220,7 @@ function showHelp() {
|
|
|
2220
2220
|
}
|
|
2221
2221
|
|
|
2222
2222
|
// src/commands/pipe.ts
|
|
2223
|
+
import { PipeWatcher } from "@plumpslabs/fennec-core";
|
|
2223
2224
|
async function pipeCommand(args2) {
|
|
2224
2225
|
const nameIndex = args2.indexOf("--name");
|
|
2225
2226
|
const name = nameIndex !== -1 ? args2[nameIndex + 1] : "pipe";
|
|
@@ -2227,7 +2228,6 @@ async function pipeCommand(args2) {
|
|
|
2227
2228
|
console.error("Error: --name is required for pipe command");
|
|
2228
2229
|
process.exit(1);
|
|
2229
2230
|
}
|
|
2230
|
-
const { PipeWatcher } = await import("@plumpslabs/fennec-core");
|
|
2231
2231
|
const watcher = new PipeWatcher();
|
|
2232
2232
|
const { write } = watcher.createPipe(name);
|
|
2233
2233
|
console.error(`Pipe watcher '${name}' active. Forwarding stdin...`);
|
|
@@ -2265,13 +2265,13 @@ async function pipeCommand(args2) {
|
|
|
2265
2265
|
}
|
|
2266
2266
|
|
|
2267
2267
|
// src/commands/attach-pid.ts
|
|
2268
|
+
import { PortDetector } from "@plumpslabs/fennec-core";
|
|
2268
2269
|
async function attachPidCommand(args2) {
|
|
2269
2270
|
const pid = parseInt(args2[0], 10);
|
|
2270
2271
|
if (isNaN(pid)) {
|
|
2271
2272
|
console.error("Error: valid PID is required");
|
|
2272
2273
|
process.exit(1);
|
|
2273
2274
|
}
|
|
2274
|
-
const { PortDetector } = await import("@plumpslabs/fennec-core");
|
|
2275
2275
|
const detector = new PortDetector();
|
|
2276
2276
|
const info = detector.detectByPid(pid);
|
|
2277
2277
|
if (info) {
|
|
@@ -2284,14 +2284,14 @@ async function attachPidCommand(args2) {
|
|
|
2284
2284
|
}
|
|
2285
2285
|
|
|
2286
2286
|
// src/commands/attach-port.ts
|
|
2287
|
+
import { PortDetector as PortDetector2 } from "@plumpslabs/fennec-core";
|
|
2287
2288
|
async function attachPortCommand(args2) {
|
|
2288
2289
|
const port = parseInt(args2[0], 10);
|
|
2289
2290
|
if (isNaN(port)) {
|
|
2290
2291
|
console.error("Error: valid port number is required");
|
|
2291
2292
|
process.exit(1);
|
|
2292
2293
|
}
|
|
2293
|
-
const
|
|
2294
|
-
const detector = new PortDetector();
|
|
2294
|
+
const detector = new PortDetector2();
|
|
2295
2295
|
const info = detector.detectByPort(port);
|
|
2296
2296
|
if (info) {
|
|
2297
2297
|
console.log(`Found process on port ${port}: PID ${info.pid}${info.command ? ` (${info.command})` : ""}`);
|
|
@@ -2304,6 +2304,7 @@ async function attachPortCommand(args2) {
|
|
|
2304
2304
|
// src/commands/watch.ts
|
|
2305
2305
|
import { existsSync } from "fs";
|
|
2306
2306
|
import { resolve } from "path";
|
|
2307
|
+
import { LogWatcher } from "@plumpslabs/fennec-core";
|
|
2307
2308
|
async function watchCommand(args2) {
|
|
2308
2309
|
const fileIndex = args2.indexOf("--file");
|
|
2309
2310
|
const filePath = fileIndex !== -1 ? args2[fileIndex + 1] : void 0;
|
|
@@ -2318,7 +2319,6 @@ async function watchCommand(args2) {
|
|
|
2318
2319
|
console.error(`Error: File not found: ${resolvedPath}`);
|
|
2319
2320
|
process.exit(1);
|
|
2320
2321
|
}
|
|
2321
|
-
const { LogWatcher } = await import("@plumpslabs/fennec-core");
|
|
2322
2322
|
const watcher = new LogWatcher();
|
|
2323
2323
|
const watcherId = watcher.watchFile(resolvedPath, name);
|
|
2324
2324
|
console.log(`Watching file: ${resolvedPath}`);
|
|
@@ -2327,12 +2327,12 @@ async function watchCommand(args2) {
|
|
|
2327
2327
|
}
|
|
2328
2328
|
|
|
2329
2329
|
// src/commands/start.ts
|
|
2330
|
-
import { createWriteStream,
|
|
2330
|
+
import { createWriteStream, mkdirSync as mkdirSync2 } from "fs";
|
|
2331
2331
|
import { resolve as resolve3 } from "path";
|
|
2332
2332
|
import { homedir as homedir2 } from "os";
|
|
2333
2333
|
import { spawn } from "child_process";
|
|
2334
2334
|
import { FennecServer } from "@plumpslabs/fennec-core";
|
|
2335
|
-
import
|
|
2335
|
+
import pc5 from "picocolors";
|
|
2336
2336
|
|
|
2337
2337
|
// src/commands/tracker.ts
|
|
2338
2338
|
import { existsSync as existsSync2, writeFileSync, readFileSync, mkdirSync, renameSync, statSync, rmSync } from "fs";
|
|
@@ -2676,10 +2676,174 @@ function formatProcessState(state) {
|
|
|
2676
2676
|
}
|
|
2677
2677
|
}
|
|
2678
2678
|
|
|
2679
|
+
// src/commands/ps.ts
|
|
2680
|
+
import pc4 from "picocolors";
|
|
2681
|
+
async function psCommand(args2) {
|
|
2682
|
+
const watchFlag = args2.includes("-w") || args2.includes("--watch");
|
|
2683
|
+
const systemFlag = args2.includes("--system") || args2.includes("-a") || args2.includes("--all");
|
|
2684
|
+
const nameFilter = args2.includes("--name") ? args2[args2.indexOf("--name") + 1] : void 0;
|
|
2685
|
+
const sortBy = args2.includes("--sort") ? args2[args2.indexOf("--sort") + 1] : "name";
|
|
2686
|
+
if (watchFlag && systemFlag) {
|
|
2687
|
+
await watchSystemProcesses(sortBy, 15);
|
|
2688
|
+
return;
|
|
2689
|
+
}
|
|
2690
|
+
if (systemFlag) {
|
|
2691
|
+
const spinner = createSpinner("Scanning system processes...");
|
|
2692
|
+
try {
|
|
2693
|
+
const processes = getSystemProcesses({ name: nameFilter, userOnly: true, sortBy, limit: 30 });
|
|
2694
|
+
spinner.stop();
|
|
2695
|
+
process.stdout.write("\r\x1B[K");
|
|
2696
|
+
if (processes.length === 0) {
|
|
2697
|
+
console.error(`
|
|
2698
|
+
${pc4.dim("No system processes found.")}
|
|
2699
|
+
`);
|
|
2700
|
+
return;
|
|
2701
|
+
}
|
|
2702
|
+
const columns2 = [
|
|
2703
|
+
{ key: "pid", label: "PID", align: "right", format: (v) => pc4.dim(String(v).padStart(6)) },
|
|
2704
|
+
{ key: "name", label: "Name", format: (v) => pc4.bold(String(v)) },
|
|
2705
|
+
{ key: "cpu", label: "CPU%", align: "right", format: (v) => {
|
|
2706
|
+
const n = v;
|
|
2707
|
+
return n > 10 ? pc4.red(String(n)) : n > 5 ? pc4.yellow(String(n)) : pc4.dim(String(n));
|
|
2708
|
+
} },
|
|
2709
|
+
{ key: "mem", label: "MEM%", align: "right", format: (v) => {
|
|
2710
|
+
const n = v;
|
|
2711
|
+
return n > 10 ? pc4.red(String(n)) : n > 5 ? pc4.yellow(String(n)) : pc4.dim(String(n));
|
|
2712
|
+
} },
|
|
2713
|
+
{ key: "state", label: "State", format: (v) => {
|
|
2714
|
+
const s = String(v);
|
|
2715
|
+
if (s === "R" || s === "Running") return pc4.green(s);
|
|
2716
|
+
if (s === "Z" || s === "Zombie") return pc4.red(s);
|
|
2717
|
+
if (s === "S" || s === "Sleeping") return pc4.cyan(s);
|
|
2718
|
+
return pc4.dim(s);
|
|
2719
|
+
} }
|
|
2720
|
+
];
|
|
2721
|
+
const rows2 = processes.map((p) => ({ pid: p.pid, name: p.name, cpu: p.cpuPercent, mem: p.memPercent, state: formatProcessState(p.state) }));
|
|
2722
|
+
console.error(`
|
|
2723
|
+
${symbols.fox} ${pc4.bold("System Processes")} ${pc4.dim(`(top ${processes.length} by ${sortBy})`)}
|
|
2724
|
+
`);
|
|
2725
|
+
console.error(renderTable(columns2, rows2));
|
|
2726
|
+
console.error();
|
|
2727
|
+
} catch (error) {
|
|
2728
|
+
spinner.fail("Failed to scan processes");
|
|
2729
|
+
console.error(renderError("Process scan failed", String(error)));
|
|
2730
|
+
}
|
|
2731
|
+
return;
|
|
2732
|
+
}
|
|
2733
|
+
const tracked = readTracked();
|
|
2734
|
+
if (tracked.length === 0) {
|
|
2735
|
+
console.error(`
|
|
2736
|
+
${pc4.dim("No tracked processes.")}`);
|
|
2737
|
+
console.error(` ${pc4.dim("Start an app with:")} ${pc4.cyan("fennec start <command> --name <name>")}
|
|
2738
|
+
`);
|
|
2739
|
+
return;
|
|
2740
|
+
}
|
|
2741
|
+
const columns = [
|
|
2742
|
+
{ key: "name", label: "App", format: (v) => pc4.bold(String(v)) },
|
|
2743
|
+
{ key: "pid", label: "PID", align: "right" },
|
|
2744
|
+
{ key: "status", label: "Status", format: (v) => {
|
|
2745
|
+
const s = v;
|
|
2746
|
+
return s === "running" ? pc4.green("\u25CF running") : pc4.red("\u25CB stopped");
|
|
2747
|
+
} },
|
|
2748
|
+
{ key: "port", label: "Port", format: (v) => {
|
|
2749
|
+
const p = v;
|
|
2750
|
+
return p ? pc4.yellow(`:${p}`) : pc4.dim("-");
|
|
2751
|
+
} },
|
|
2752
|
+
{ key: "command", label: "Command", format: (v) => {
|
|
2753
|
+
const c = String(v);
|
|
2754
|
+
return c.length > 50 ? c.slice(0, 50) + "\u2026" : c;
|
|
2755
|
+
} },
|
|
2756
|
+
{ key: "uptime", label: "Uptime", format: (v) => pc4.dim(String(v)) }
|
|
2757
|
+
];
|
|
2758
|
+
const rows = tracked.map((t) => {
|
|
2759
|
+
const running = isProcessRunning(t.pid);
|
|
2760
|
+
const uptime = running ? formatUptime(Math.floor((Date.now() - new Date(t.startedAt).getTime()) / 1e3)) : "-";
|
|
2761
|
+
return { name: t.name, pid: running ? String(t.pid) : pc4.dim(String(t.pid)), status: running ? "running" : "stopped", port: t.port ?? null, command: t.command, uptime };
|
|
2762
|
+
});
|
|
2763
|
+
const runningCount = tracked.filter((t) => isProcessRunning(t.pid)).length;
|
|
2764
|
+
console.error(`
|
|
2765
|
+
${symbols.fox} ${pc4.bold("Fennec Apps")} ${pc4.dim(`(${runningCount}/${tracked.length} running)`)}
|
|
2766
|
+
`);
|
|
2767
|
+
console.error(renderTable(columns, rows));
|
|
2768
|
+
console.error(` ${pc4.dim("Use")} ${pc4.cyan("fennec start <command> --name <name> --port <port>")} ${pc4.dim("to add more apps.")}`);
|
|
2769
|
+
console.error(` ${pc4.dim("Use")} ${pc4.cyan("fennec log <name>")} ${pc4.dim("to view logs.")}`);
|
|
2770
|
+
console.error(` ${pc4.dim("Use")} ${pc4.cyan("fennec kill <name>")} ${pc4.dim("to stop an app.")}`);
|
|
2771
|
+
console.error();
|
|
2772
|
+
}
|
|
2773
|
+
async function statusCommand(_args) {
|
|
2774
|
+
const watchFlag = _args.includes("-w") || _args.includes("--watch");
|
|
2775
|
+
const tracked = readTracked();
|
|
2776
|
+
const topSystem = getSystemProcesses({ userOnly: true, sortBy: "cpu", limit: 5 });
|
|
2777
|
+
const totalUserProcs = getSystemProcesses({ userOnly: true }).length;
|
|
2778
|
+
console.error(`
|
|
2779
|
+
${symbols.fox} ${pc4.bold("Fennec Status")}
|
|
2780
|
+
`);
|
|
2781
|
+
if (tracked.length > 0) {
|
|
2782
|
+
const runningCount = tracked.filter((t) => isProcessRunning(t.pid)).length;
|
|
2783
|
+
console.error(` ${pc4.bold("Managed Apps")} ${pc4.dim(`(${runningCount}/${tracked.length} running)`)}
|
|
2784
|
+
`);
|
|
2785
|
+
for (const t of tracked) {
|
|
2786
|
+
const running = isProcessRunning(t.pid);
|
|
2787
|
+
const statusIcon = running ? pc4.green("\u25CF") : pc4.red("\u25CB");
|
|
2788
|
+
const portStr = t.port ? ` ${pc4.yellow(`:${t.port}`)}` : "";
|
|
2789
|
+
const uptime = running ? pc4.dim(formatUptime(Math.floor((Date.now() - new Date(t.startedAt).getTime()) / 1e3))) : pc4.red("stopped");
|
|
2790
|
+
console.error(` ${statusIcon} ${pc4.bold(t.name)}${portStr} ${pc4.dim(`(PID ${t.pid})`)} \u2014 ${uptime}`);
|
|
2791
|
+
}
|
|
2792
|
+
console.error();
|
|
2793
|
+
} else {
|
|
2794
|
+
console.error(` ${pc4.dim("No managed apps.")} ${pc4.cyan("fennec start <command> --name <name>")}
|
|
2795
|
+
`);
|
|
2796
|
+
}
|
|
2797
|
+
console.error(` ${pc4.bold("System")} ${pc4.dim(`(${totalUserProcs} user processes)`)}`);
|
|
2798
|
+
for (const p of topSystem) {
|
|
2799
|
+
const cpuStr = p.cpuPercent > 10 ? pc4.red(`${p.cpuPercent}%`) : p.cpuPercent > 5 ? pc4.yellow(`${p.cpuPercent}%`) : pc4.dim(`${p.cpuPercent}%`);
|
|
2800
|
+
const memStr = p.memPercent > 10 ? pc4.red(`${p.memPercent}%`) : p.memPercent > 5 ? pc4.yellow(`${p.memPercent}%`) : pc4.dim(`${p.memPercent}%`);
|
|
2801
|
+
console.error(` ${pc4.dim(`PID ${p.pid}`)} ${pc4.bold(p.name)} \u2014 CPU: ${cpuStr} MEM: ${memStr}`);
|
|
2802
|
+
}
|
|
2803
|
+
if (watchFlag) await watchSystemProcesses("cpu", 15);
|
|
2804
|
+
console.error();
|
|
2805
|
+
}
|
|
2806
|
+
async function watchSystemProcesses(sortBy, limit) {
|
|
2807
|
+
console.error(`
|
|
2808
|
+
${pc4.bold("Watching system processes")} ${pc4.dim("(Ctrl+C to stop, refreshes every 3s)")}
|
|
2809
|
+
`);
|
|
2810
|
+
const render = () => {
|
|
2811
|
+
const processes = getSystemProcesses({ userOnly: true, sortBy, limit });
|
|
2812
|
+
const columns = [
|
|
2813
|
+
{ key: "pid", label: "PID", align: "right", format: (v) => pc4.dim(String(v).padStart(6)) },
|
|
2814
|
+
{ key: "name", label: "Name", format: (v) => pc4.bold(String(v)) },
|
|
2815
|
+
{ key: "cpu", label: "CPU%", align: "right", format: (v) => {
|
|
2816
|
+
const n = v;
|
|
2817
|
+
return n > 10 ? pc4.red(String(n)) : n > 5 ? pc4.yellow(String(n)) : pc4.dim(String(n));
|
|
2818
|
+
} },
|
|
2819
|
+
{ key: "mem", label: "MEM%", align: "right", format: (v) => {
|
|
2820
|
+
const n = v;
|
|
2821
|
+
return n > 10 ? pc4.red(String(n)) : n > 5 ? pc4.yellow(String(n)) : pc4.dim(String(n));
|
|
2822
|
+
} },
|
|
2823
|
+
{ key: "state", label: "S", align: "center" }
|
|
2824
|
+
];
|
|
2825
|
+
const rows = processes.map((p) => ({ pid: p.pid, name: p.name, cpu: p.cpuPercent, mem: p.memPercent, state: p.state }));
|
|
2826
|
+
return ` ${timestamp()} ${pc4.dim(`${processes.length} processes`)}
|
|
2827
|
+
${renderTable(columns, rows, { compact: true })}`;
|
|
2828
|
+
};
|
|
2829
|
+
console.error(render());
|
|
2830
|
+
const interval = setInterval(() => {
|
|
2831
|
+
process.stdout.write("\x1B[J");
|
|
2832
|
+
console.error(render());
|
|
2833
|
+
}, 3e3);
|
|
2834
|
+
await new Promise((resolve8) => {
|
|
2835
|
+
process.once("SIGINT", () => {
|
|
2836
|
+
clearInterval(interval);
|
|
2837
|
+
process.stdout.write("\x1B[J");
|
|
2838
|
+
resolve8();
|
|
2839
|
+
});
|
|
2840
|
+
});
|
|
2841
|
+
}
|
|
2842
|
+
|
|
2679
2843
|
// src/commands/start.ts
|
|
2680
2844
|
async function startServer(args2) {
|
|
2681
2845
|
printBanner();
|
|
2682
|
-
console.error(` ${
|
|
2846
|
+
console.error(` ${pc5.dim("Starting Fennec MCP server...")}
|
|
2683
2847
|
`);
|
|
2684
2848
|
const configIndex = args2.indexOf("--config");
|
|
2685
2849
|
const configPath = configIndex !== -1 ? args2[configIndex + 1] : void 0;
|
|
@@ -2688,11 +2852,11 @@ async function startServer(args2) {
|
|
|
2688
2852
|
await resurrectTracked();
|
|
2689
2853
|
await server.start();
|
|
2690
2854
|
console.error(`
|
|
2691
|
-
${
|
|
2855
|
+
${pc5.green("\u2713")} ${pc5.bold("Fennec server is running")}`);
|
|
2692
2856
|
console.error(` ${renderKV("Transport", "stdio")}`);
|
|
2693
2857
|
console.error(` ${renderKV("AI Agent", "Connect via MCP protocol")}`);
|
|
2694
2858
|
console.error(`
|
|
2695
|
-
${
|
|
2859
|
+
${pc5.dim("Press Ctrl+C to stop")}
|
|
2696
2860
|
`);
|
|
2697
2861
|
} catch (error) {
|
|
2698
2862
|
console.error(renderError("Failed to start server", String(error)));
|
|
@@ -2701,7 +2865,7 @@ async function startServer(args2) {
|
|
|
2701
2865
|
}
|
|
2702
2866
|
async function startCommand(args2) {
|
|
2703
2867
|
printBanner();
|
|
2704
|
-
console.error(` ${
|
|
2868
|
+
console.error(` ${pc5.dim("Starting app...")}
|
|
2705
2869
|
`);
|
|
2706
2870
|
await runCommand(args2);
|
|
2707
2871
|
}
|
|
@@ -2725,9 +2889,9 @@ async function runCommand(args2) {
|
|
|
2725
2889
|
const existing = tracked.find((t) => t.name === appName);
|
|
2726
2890
|
if (existing && isProcessRunning(existing.pid)) {
|
|
2727
2891
|
console.error();
|
|
2728
|
-
console.error(` ${
|
|
2729
|
-
console.error(` ${renderKV("Logs",
|
|
2730
|
-
console.error(` ${renderKV("Stop",
|
|
2892
|
+
console.error(` ${pc5.yellow("\u26A0")} ${pc5.bold(appName)} ${pc5.dim(`is already running (PID: ${existing.pid})`)}`);
|
|
2893
|
+
console.error(` ${renderKV("Logs", pc5.cyan(`fennec log ${appName}`))}`);
|
|
2894
|
+
console.error(` ${renderKV("Stop", pc5.cyan(`fennec kill ${appName}`))}`);
|
|
2731
2895
|
console.error();
|
|
2732
2896
|
process.exit(0);
|
|
2733
2897
|
}
|
|
@@ -2735,7 +2899,7 @@ async function runCommand(args2) {
|
|
|
2735
2899
|
mkdirSync2(logDir, { recursive: true });
|
|
2736
2900
|
const logFilePath = resolve3(logDir, `${appName}.log`);
|
|
2737
2901
|
console.error(`
|
|
2738
|
-
${symbols.fox} ${
|
|
2902
|
+
${symbols.fox} ${pc5.bold("Starting")} ${renderAppName(appName)} ${pc5.dim("(daemon)")}
|
|
2739
2903
|
`);
|
|
2740
2904
|
console.error(` ${renderKV("Command", cmd)}`);
|
|
2741
2905
|
if (port) console.error(` ${renderKV("Port", String(port))}`);
|
|
@@ -2762,32 +2926,8 @@ async function runCommand(args2) {
|
|
|
2762
2926
|
cwd,
|
|
2763
2927
|
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2764
2928
|
});
|
|
2765
|
-
console.error(`
|
|
2766
|
-
|
|
2767
|
-
`);
|
|
2768
|
-
await new Promise((r) => setTimeout(r, 1500));
|
|
2769
|
-
try {
|
|
2770
|
-
const initialLogs = readFileSync3(logFilePath, "utf-8").split("\n").filter(Boolean).slice(-8);
|
|
2771
|
-
if (initialLogs.length > 0) {
|
|
2772
|
-
for (const line of initialLogs) {
|
|
2773
|
-
const truncated = line.length > 150 ? line.slice(0, 150) + "\u2026" : line;
|
|
2774
|
-
if (line.toLowerCase().includes("error")) {
|
|
2775
|
-
console.error(` ${pc4.dim("\u2502")} ${pc4.red(truncated)}`);
|
|
2776
|
-
} else if (line.toLowerCase().includes("warn")) {
|
|
2777
|
-
console.error(` ${pc4.dim("\u2502")} ${pc4.yellow(truncated)}`);
|
|
2778
|
-
} else {
|
|
2779
|
-
console.error(` ${pc4.dim("\u2502")} ${truncated}`);
|
|
2780
|
-
}
|
|
2781
|
-
}
|
|
2782
|
-
}
|
|
2783
|
-
} catch {
|
|
2784
|
-
}
|
|
2785
|
-
console.error();
|
|
2786
|
-
console.error(` ${pc4.green("\u2713")} ${pc4.bold(appName)} running in background`);
|
|
2787
|
-
console.error(` ${renderKV("Logs", pc4.cyan(`fennec log ${appName}`))}`);
|
|
2788
|
-
console.error(` ${renderKV("Status", pc4.cyan("fennec ps"))}`);
|
|
2789
|
-
console.error(` ${renderKV("Stop", pc4.cyan(`fennec kill ${appName}`))}`);
|
|
2790
|
-
console.error();
|
|
2929
|
+
console.error(` ${pc5.green("\u2713")} ${pc5.bold(appName)} ${pc5.dim(`started (PID: ${pid})`)}`);
|
|
2930
|
+
await psCommand([]);
|
|
2791
2931
|
} catch (error) {
|
|
2792
2932
|
console.error(renderError(`Failed to start ${appName}`, String(error)));
|
|
2793
2933
|
process.exit(1);
|
|
@@ -2838,177 +2978,13 @@ async function resurrectTracked() {
|
|
|
2838
2978
|
spinner.stop();
|
|
2839
2979
|
process.stdout.write("\r\x1B[K");
|
|
2840
2980
|
if (resurrected > 0) {
|
|
2841
|
-
console.error(` ${
|
|
2981
|
+
console.error(` ${pc5.green("\u25CF")} ${pc5.bold(`Resurrected ${resurrected} process(es)`)}`);
|
|
2842
2982
|
}
|
|
2843
2983
|
if (failed > 0) {
|
|
2844
|
-
console.error(` ${
|
|
2984
|
+
console.error(` ${pc5.red("\u25CF")} ${pc5.bold(`${failed} process(es) failed to resurrect`)}`);
|
|
2845
2985
|
}
|
|
2846
2986
|
}
|
|
2847
2987
|
|
|
2848
|
-
// src/commands/ps.ts
|
|
2849
|
-
import pc5 from "picocolors";
|
|
2850
|
-
async function psCommand(args2) {
|
|
2851
|
-
const watchFlag = args2.includes("-w") || args2.includes("--watch");
|
|
2852
|
-
const systemFlag = args2.includes("--system") || args2.includes("-a") || args2.includes("--all");
|
|
2853
|
-
const nameFilter = args2.includes("--name") ? args2[args2.indexOf("--name") + 1] : void 0;
|
|
2854
|
-
const sortBy = args2.includes("--sort") ? args2[args2.indexOf("--sort") + 1] : "name";
|
|
2855
|
-
if (watchFlag && systemFlag) {
|
|
2856
|
-
await watchSystemProcesses(sortBy, 15);
|
|
2857
|
-
return;
|
|
2858
|
-
}
|
|
2859
|
-
if (systemFlag) {
|
|
2860
|
-
const spinner = createSpinner("Scanning system processes...");
|
|
2861
|
-
try {
|
|
2862
|
-
const processes = getSystemProcesses({ name: nameFilter, userOnly: true, sortBy, limit: 30 });
|
|
2863
|
-
spinner.stop();
|
|
2864
|
-
process.stdout.write("\r\x1B[K");
|
|
2865
|
-
if (processes.length === 0) {
|
|
2866
|
-
console.error(`
|
|
2867
|
-
${pc5.dim("No system processes found.")}
|
|
2868
|
-
`);
|
|
2869
|
-
return;
|
|
2870
|
-
}
|
|
2871
|
-
const columns2 = [
|
|
2872
|
-
{ key: "pid", label: "PID", align: "right", format: (v) => pc5.dim(String(v).padStart(6)) },
|
|
2873
|
-
{ key: "name", label: "Name", format: (v) => pc5.bold(String(v)) },
|
|
2874
|
-
{ key: "cpu", label: "CPU%", align: "right", format: (v) => {
|
|
2875
|
-
const n = v;
|
|
2876
|
-
return n > 10 ? pc5.red(String(n)) : n > 5 ? pc5.yellow(String(n)) : pc5.dim(String(n));
|
|
2877
|
-
} },
|
|
2878
|
-
{ key: "mem", label: "MEM%", align: "right", format: (v) => {
|
|
2879
|
-
const n = v;
|
|
2880
|
-
return n > 10 ? pc5.red(String(n)) : n > 5 ? pc5.yellow(String(n)) : pc5.dim(String(n));
|
|
2881
|
-
} },
|
|
2882
|
-
{ key: "state", label: "State", format: (v) => {
|
|
2883
|
-
const s = String(v);
|
|
2884
|
-
if (s === "R" || s === "Running") return pc5.green(s);
|
|
2885
|
-
if (s === "Z" || s === "Zombie") return pc5.red(s);
|
|
2886
|
-
if (s === "S" || s === "Sleeping") return pc5.cyan(s);
|
|
2887
|
-
return pc5.dim(s);
|
|
2888
|
-
} }
|
|
2889
|
-
];
|
|
2890
|
-
const rows2 = processes.map((p) => ({ pid: p.pid, name: p.name, cpu: p.cpuPercent, mem: p.memPercent, state: formatProcessState(p.state) }));
|
|
2891
|
-
console.error(`
|
|
2892
|
-
${symbols.fox} ${pc5.bold("System Processes")} ${pc5.dim(`(top ${processes.length} by ${sortBy})`)}
|
|
2893
|
-
`);
|
|
2894
|
-
console.error(renderTable(columns2, rows2));
|
|
2895
|
-
console.error();
|
|
2896
|
-
} catch (error) {
|
|
2897
|
-
spinner.fail("Failed to scan processes");
|
|
2898
|
-
console.error(renderError("Process scan failed", String(error)));
|
|
2899
|
-
}
|
|
2900
|
-
return;
|
|
2901
|
-
}
|
|
2902
|
-
const tracked = readTracked();
|
|
2903
|
-
if (tracked.length === 0) {
|
|
2904
|
-
console.error(`
|
|
2905
|
-
${pc5.dim("No tracked processes.")}`);
|
|
2906
|
-
console.error(` ${pc5.dim("Start an app with:")} ${pc5.cyan("fennec start <command> --name <name>")}
|
|
2907
|
-
`);
|
|
2908
|
-
return;
|
|
2909
|
-
}
|
|
2910
|
-
const columns = [
|
|
2911
|
-
{ key: "name", label: "App", format: (v) => pc5.bold(String(v)) },
|
|
2912
|
-
{ key: "pid", label: "PID", align: "right" },
|
|
2913
|
-
{ key: "status", label: "Status", format: (v) => {
|
|
2914
|
-
const s = v;
|
|
2915
|
-
return s === "running" ? pc5.green("\u25CF running") : pc5.red("\u25CB stopped");
|
|
2916
|
-
} },
|
|
2917
|
-
{ key: "port", label: "Port", format: (v) => {
|
|
2918
|
-
const p = v;
|
|
2919
|
-
return p ? pc5.yellow(`:${p}`) : pc5.dim("-");
|
|
2920
|
-
} },
|
|
2921
|
-
{ key: "command", label: "Command", format: (v) => {
|
|
2922
|
-
const c = String(v);
|
|
2923
|
-
return c.length > 50 ? c.slice(0, 50) + "\u2026" : c;
|
|
2924
|
-
} },
|
|
2925
|
-
{ key: "uptime", label: "Uptime", format: (v) => pc5.dim(String(v)) }
|
|
2926
|
-
];
|
|
2927
|
-
const rows = tracked.map((t) => {
|
|
2928
|
-
const running = isProcessRunning(t.pid);
|
|
2929
|
-
const uptime = running ? formatUptime(Math.floor((Date.now() - new Date(t.startedAt).getTime()) / 1e3)) : "-";
|
|
2930
|
-
return { name: t.name, pid: running ? String(t.pid) : pc5.dim(String(t.pid)), status: running ? "running" : "stopped", port: t.port ?? null, command: t.command, uptime };
|
|
2931
|
-
});
|
|
2932
|
-
const runningCount = tracked.filter((t) => isProcessRunning(t.pid)).length;
|
|
2933
|
-
console.error(`
|
|
2934
|
-
${symbols.fox} ${pc5.bold("Fennec Apps")} ${pc5.dim(`(${runningCount}/${tracked.length} running)`)}
|
|
2935
|
-
`);
|
|
2936
|
-
console.error(renderTable(columns, rows));
|
|
2937
|
-
console.error(` ${pc5.dim("Use")} ${pc5.cyan("fennec start <command> --name <name> --port <port>")} ${pc5.dim("to add more apps.")}`);
|
|
2938
|
-
console.error(` ${pc5.dim("Use")} ${pc5.cyan("fennec log <name>")} ${pc5.dim("to view logs.")}`);
|
|
2939
|
-
console.error(` ${pc5.dim("Use")} ${pc5.cyan("fennec kill <name>")} ${pc5.dim("to stop an app.")}`);
|
|
2940
|
-
console.error();
|
|
2941
|
-
}
|
|
2942
|
-
async function statusCommand(_args) {
|
|
2943
|
-
const watchFlag = _args.includes("-w") || _args.includes("--watch");
|
|
2944
|
-
const tracked = readTracked();
|
|
2945
|
-
const topSystem = getSystemProcesses({ userOnly: true, sortBy: "cpu", limit: 5 });
|
|
2946
|
-
const totalUserProcs = getSystemProcesses({ userOnly: true }).length;
|
|
2947
|
-
console.error(`
|
|
2948
|
-
${symbols.fox} ${pc5.bold("Fennec Status")}
|
|
2949
|
-
`);
|
|
2950
|
-
if (tracked.length > 0) {
|
|
2951
|
-
const runningCount = tracked.filter((t) => isProcessRunning(t.pid)).length;
|
|
2952
|
-
console.error(` ${pc5.bold("Managed Apps")} ${pc5.dim(`(${runningCount}/${tracked.length} running)`)}
|
|
2953
|
-
`);
|
|
2954
|
-
for (const t of tracked) {
|
|
2955
|
-
const running = isProcessRunning(t.pid);
|
|
2956
|
-
const statusIcon = running ? pc5.green("\u25CF") : pc5.red("\u25CB");
|
|
2957
|
-
const portStr = t.port ? ` ${pc5.yellow(`:${t.port}`)}` : "";
|
|
2958
|
-
const uptime = running ? pc5.dim(formatUptime(Math.floor((Date.now() - new Date(t.startedAt).getTime()) / 1e3))) : pc5.red("stopped");
|
|
2959
|
-
console.error(` ${statusIcon} ${pc5.bold(t.name)}${portStr} ${pc5.dim(`(PID ${t.pid})`)} \u2014 ${uptime}`);
|
|
2960
|
-
}
|
|
2961
|
-
console.error();
|
|
2962
|
-
} else {
|
|
2963
|
-
console.error(` ${pc5.dim("No managed apps.")} ${pc5.cyan("fennec start <command> --name <name>")}
|
|
2964
|
-
`);
|
|
2965
|
-
}
|
|
2966
|
-
console.error(` ${pc5.bold("System")} ${pc5.dim(`(${totalUserProcs} user processes)`)}`);
|
|
2967
|
-
for (const p of topSystem) {
|
|
2968
|
-
const cpuStr = p.cpuPercent > 10 ? pc5.red(`${p.cpuPercent}%`) : p.cpuPercent > 5 ? pc5.yellow(`${p.cpuPercent}%`) : pc5.dim(`${p.cpuPercent}%`);
|
|
2969
|
-
const memStr = p.memPercent > 10 ? pc5.red(`${p.memPercent}%`) : p.memPercent > 5 ? pc5.yellow(`${p.memPercent}%`) : pc5.dim(`${p.memPercent}%`);
|
|
2970
|
-
console.error(` ${pc5.dim(`PID ${p.pid}`)} ${pc5.bold(p.name)} \u2014 CPU: ${cpuStr} MEM: ${memStr}`);
|
|
2971
|
-
}
|
|
2972
|
-
if (watchFlag) await watchSystemProcesses("cpu", 15);
|
|
2973
|
-
console.error();
|
|
2974
|
-
}
|
|
2975
|
-
async function watchSystemProcesses(sortBy, limit) {
|
|
2976
|
-
console.error(`
|
|
2977
|
-
${pc5.bold("Watching system processes")} ${pc5.dim("(Ctrl+C to stop, refreshes every 3s)")}
|
|
2978
|
-
`);
|
|
2979
|
-
const render = () => {
|
|
2980
|
-
const processes = getSystemProcesses({ userOnly: true, sortBy, limit });
|
|
2981
|
-
const columns = [
|
|
2982
|
-
{ key: "pid", label: "PID", align: "right", format: (v) => pc5.dim(String(v).padStart(6)) },
|
|
2983
|
-
{ key: "name", label: "Name", format: (v) => pc5.bold(String(v)) },
|
|
2984
|
-
{ key: "cpu", label: "CPU%", align: "right", format: (v) => {
|
|
2985
|
-
const n = v;
|
|
2986
|
-
return n > 10 ? pc5.red(String(n)) : n > 5 ? pc5.yellow(String(n)) : pc5.dim(String(n));
|
|
2987
|
-
} },
|
|
2988
|
-
{ key: "mem", label: "MEM%", align: "right", format: (v) => {
|
|
2989
|
-
const n = v;
|
|
2990
|
-
return n > 10 ? pc5.red(String(n)) : n > 5 ? pc5.yellow(String(n)) : pc5.dim(String(n));
|
|
2991
|
-
} },
|
|
2992
|
-
{ key: "state", label: "S", align: "center" }
|
|
2993
|
-
];
|
|
2994
|
-
const rows = processes.map((p) => ({ pid: p.pid, name: p.name, cpu: p.cpuPercent, mem: p.memPercent, state: p.state }));
|
|
2995
|
-
return ` ${timestamp()} ${pc5.dim(`${processes.length} processes`)}
|
|
2996
|
-
${renderTable(columns, rows, { compact: true })}`;
|
|
2997
|
-
};
|
|
2998
|
-
console.error(render());
|
|
2999
|
-
const interval = setInterval(() => {
|
|
3000
|
-
process.stdout.write("\x1B[J");
|
|
3001
|
-
console.error(render());
|
|
3002
|
-
}, 3e3);
|
|
3003
|
-
await new Promise((resolve8) => {
|
|
3004
|
-
process.once("SIGINT", () => {
|
|
3005
|
-
clearInterval(interval);
|
|
3006
|
-
process.stdout.write("\x1B[J");
|
|
3007
|
-
resolve8();
|
|
3008
|
-
});
|
|
3009
|
-
});
|
|
3010
|
-
}
|
|
3011
|
-
|
|
3012
2988
|
// src/commands/kill.ts
|
|
3013
2989
|
import pc6 from "picocolors";
|
|
3014
2990
|
async function killCommand(args2) {
|
|
@@ -3035,25 +3011,32 @@ async function killCommand(args2) {
|
|
|
3035
3011
|
targetPid = pid;
|
|
3036
3012
|
displayName = `PID ${pid}`;
|
|
3037
3013
|
} else {
|
|
3038
|
-
const
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3014
|
+
const tracked = readTracked();
|
|
3015
|
+
const trackedMatch = tracked.find((t) => t.name === rawTarget);
|
|
3016
|
+
if (trackedMatch && isProcessRunning(trackedMatch.pid)) {
|
|
3017
|
+
targetPid = trackedMatch.pid;
|
|
3018
|
+
displayName = `${trackedMatch.name} (PID ${targetPid})`;
|
|
3019
|
+
} else {
|
|
3020
|
+
const matches = getSystemProcesses({ name: rawTarget, userOnly: true, sortBy: "cpu" });
|
|
3021
|
+
if (matches.length === 0) {
|
|
3022
|
+
console.error(renderError("Process not found", `No running process matching "${rawTarget}"`));
|
|
3023
|
+
process.exit(1);
|
|
3024
|
+
}
|
|
3025
|
+
if (matches.length > 1) {
|
|
3026
|
+
console.error(`
|
|
3045
3027
|
${pc6.bold(`Multiple processes match "${rawTarget}":`)}`);
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3028
|
+
const selected = await selectPrompt("Select which to kill:", matches.map((p, i) => ({ value: String(i), label: `${p.name} (PID ${p.pid})`, description: `${p.command.slice(0, 80)} \u2014 CPU: ${p.cpuPercent}% MEM: ${p.memPercent}%` })));
|
|
3029
|
+
if (selected === null) {
|
|
3030
|
+
console.error(` ${pc6.dim("Cancelled")}`);
|
|
3031
|
+
return;
|
|
3032
|
+
}
|
|
3033
|
+
const idx = parseInt(selected, 10);
|
|
3034
|
+
targetPid = matches[idx].pid;
|
|
3035
|
+
displayName = `${matches[idx].name} (PID ${targetPid})`;
|
|
3036
|
+
} else {
|
|
3037
|
+
targetPid = matches[0].pid;
|
|
3038
|
+
displayName = `${matches[0].name} (PID ${targetPid})`;
|
|
3050
3039
|
}
|
|
3051
|
-
const idx = parseInt(selected, 10);
|
|
3052
|
-
targetPid = matches[idx].pid;
|
|
3053
|
-
displayName = `${matches[idx].name} (PID ${targetPid})`;
|
|
3054
|
-
} else {
|
|
3055
|
-
targetPid = matches[0].pid;
|
|
3056
|
-
displayName = `${matches[0].name} (PID ${targetPid})`;
|
|
3057
3040
|
}
|
|
3058
3041
|
}
|
|
3059
3042
|
const confirmed = await confirmPrompt(`Kill ${pc6.bold(displayName)} with ${pc6.yellow(signal)}?`, false);
|
|
@@ -3206,7 +3189,7 @@ async function restartCommand(args2) {
|
|
|
3206
3189
|
}
|
|
3207
3190
|
|
|
3208
3191
|
// src/commands/log.ts
|
|
3209
|
-
import { existsSync as existsSync4, readFileSync as
|
|
3192
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
|
|
3210
3193
|
import { resolve as resolve5 } from "path";
|
|
3211
3194
|
import { homedir as homedir4 } from "os";
|
|
3212
3195
|
import { spawn as spawn3, execSync as execSync2 } from "child_process";
|
|
@@ -3238,7 +3221,7 @@ async function logCommand(args2) {
|
|
|
3238
3221
|
try {
|
|
3239
3222
|
let logLines = [];
|
|
3240
3223
|
if (logFilePath && existsSync4(logFilePath)) {
|
|
3241
|
-
const content =
|
|
3224
|
+
const content = readFileSync3(logFilePath, "utf-8");
|
|
3242
3225
|
logLines = content.split("\n").filter(Boolean);
|
|
3243
3226
|
} else {
|
|
3244
3227
|
const pid = trackedMatch?.pid ?? parseInt(target, 10);
|
|
@@ -3271,8 +3254,12 @@ async function logCommand(args2) {
|
|
|
3271
3254
|
${pc8.dim("Following... (Ctrl+C to stop)")}
|
|
3272
3255
|
`);
|
|
3273
3256
|
const tail = spawn3("tail", ["-n", "0", "-f", logFilePath], { stdio: "inherit" });
|
|
3274
|
-
|
|
3275
|
-
|
|
3257
|
+
await new Promise((resolve8) => {
|
|
3258
|
+
tail.on("exit", () => resolve8());
|
|
3259
|
+
process.once("SIGINT", () => {
|
|
3260
|
+
tail.kill("SIGTERM");
|
|
3261
|
+
resolve8();
|
|
3262
|
+
});
|
|
3276
3263
|
});
|
|
3277
3264
|
}
|
|
3278
3265
|
console.error();
|
|
@@ -3283,6 +3270,7 @@ async function logCommand(args2) {
|
|
|
3283
3270
|
}
|
|
3284
3271
|
|
|
3285
3272
|
// src/commands/attach.ts
|
|
3273
|
+
import { PortDetector as PortDetector3 } from "@plumpslabs/fennec-core";
|
|
3286
3274
|
import pc9 from "picocolors";
|
|
3287
3275
|
async function attachCommand(args2) {
|
|
3288
3276
|
const port = parseInt(args2[0], 10);
|
|
@@ -3295,8 +3283,7 @@ async function attachCommand(args2) {
|
|
|
3295
3283
|
const name = rawName ?? `port-${port}`;
|
|
3296
3284
|
const spinner = createSpinner(`Attaching to :${port}...`);
|
|
3297
3285
|
try {
|
|
3298
|
-
const
|
|
3299
|
-
const detector = new PortDetector();
|
|
3286
|
+
const detector = new PortDetector3();
|
|
3300
3287
|
const info = detector.detectByPort(port);
|
|
3301
3288
|
if (info) {
|
|
3302
3289
|
spinner.succeed(`Attached to :${port}`);
|