@plumpslabs/fennec-cli 1.12.2 → 1.13.0
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 +955 -332
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1958,6 +1958,9 @@ function renderKV(key, value, options) {
|
|
|
1958
1958
|
const pad = " ".repeat(indent);
|
|
1959
1959
|
return `${pad}${pc.dim(key + sep)} ${value}`;
|
|
1960
1960
|
}
|
|
1961
|
+
function renderKVColor(key, value, color = pc.bold) {
|
|
1962
|
+
return ` ${pc.dim(key + ":")} ${color(value)}`;
|
|
1963
|
+
}
|
|
1961
1964
|
function createSpinner(text) {
|
|
1962
1965
|
const frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
1963
1966
|
let i = 0;
|
|
@@ -2010,10 +2013,10 @@ async function selectPrompt(message, options) {
|
|
|
2010
2013
|
console.log(` ${pc.dim("0) Cancel")}
|
|
2011
2014
|
`);
|
|
2012
2015
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
2013
|
-
const answer = await new Promise((
|
|
2016
|
+
const answer = await new Promise((resolve12) => {
|
|
2014
2017
|
rl.question(` ${pc.bold("Enter number")} ${pc.dim("(0-" + options.length + ")")}: `, (ans) => {
|
|
2015
2018
|
rl.close();
|
|
2016
|
-
|
|
2019
|
+
resolve12(ans.trim());
|
|
2017
2020
|
});
|
|
2018
2021
|
});
|
|
2019
2022
|
const num = parseInt(answer, 10);
|
|
@@ -2028,11 +2031,11 @@ async function selectPrompt(message, options) {
|
|
|
2028
2031
|
async function confirmPrompt(message, defaultValue = false) {
|
|
2029
2032
|
const hint = defaultValue ? pc.bold("Y") + pc.dim("/n") : pc.dim("y/") + pc.bold("N");
|
|
2030
2033
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
2031
|
-
const answer = await new Promise((
|
|
2034
|
+
const answer = await new Promise((resolve12) => {
|
|
2032
2035
|
rl.question(`
|
|
2033
2036
|
${message} ${pc.dim("(" + hint + ")")}: `, (ans) => {
|
|
2034
2037
|
rl.close();
|
|
2035
|
-
|
|
2038
|
+
resolve12(ans.trim().toLowerCase());
|
|
2036
2039
|
});
|
|
2037
2040
|
});
|
|
2038
2041
|
if (!answer) return defaultValue;
|
|
@@ -2088,7 +2091,7 @@ function hexColor(color) {
|
|
|
2088
2091
|
}
|
|
2089
2092
|
var fennecOrange2 = hexColor("#FF6432");
|
|
2090
2093
|
var fennecGold = hexColor("#FFB347");
|
|
2091
|
-
var VERSION = "1.
|
|
2094
|
+
var VERSION = "1.13.0";
|
|
2092
2095
|
var cachedBanner = null;
|
|
2093
2096
|
var cachedCompact = null;
|
|
2094
2097
|
function generateBanner() {
|
|
@@ -2220,6 +2223,7 @@ function showHelp() {
|
|
|
2220
2223
|
}
|
|
2221
2224
|
|
|
2222
2225
|
// src/commands/pipe.ts
|
|
2226
|
+
import { PipeWatcher } from "@plumpslabs/fennec-core";
|
|
2223
2227
|
async function pipeCommand(args2) {
|
|
2224
2228
|
const nameIndex = args2.indexOf("--name");
|
|
2225
2229
|
const name = nameIndex !== -1 ? args2[nameIndex + 1] : "pipe";
|
|
@@ -2227,7 +2231,6 @@ async function pipeCommand(args2) {
|
|
|
2227
2231
|
console.error("Error: --name is required for pipe command");
|
|
2228
2232
|
process.exit(1);
|
|
2229
2233
|
}
|
|
2230
|
-
const { PipeWatcher } = await import("@plumpslabs/fennec-core");
|
|
2231
2234
|
const watcher = new PipeWatcher();
|
|
2232
2235
|
const { write } = watcher.createPipe(name);
|
|
2233
2236
|
console.error(`Pipe watcher '${name}' active. Forwarding stdin...`);
|
|
@@ -2265,13 +2268,13 @@ async function pipeCommand(args2) {
|
|
|
2265
2268
|
}
|
|
2266
2269
|
|
|
2267
2270
|
// src/commands/attach-pid.ts
|
|
2271
|
+
import { PortDetector } from "@plumpslabs/fennec-core";
|
|
2268
2272
|
async function attachPidCommand(args2) {
|
|
2269
2273
|
const pid = parseInt(args2[0], 10);
|
|
2270
2274
|
if (isNaN(pid)) {
|
|
2271
2275
|
console.error("Error: valid PID is required");
|
|
2272
2276
|
process.exit(1);
|
|
2273
2277
|
}
|
|
2274
|
-
const { PortDetector } = await import("@plumpslabs/fennec-core");
|
|
2275
2278
|
const detector = new PortDetector();
|
|
2276
2279
|
const info = detector.detectByPid(pid);
|
|
2277
2280
|
if (info) {
|
|
@@ -2284,14 +2287,14 @@ async function attachPidCommand(args2) {
|
|
|
2284
2287
|
}
|
|
2285
2288
|
|
|
2286
2289
|
// src/commands/attach-port.ts
|
|
2290
|
+
import { PortDetector as PortDetector2 } from "@plumpslabs/fennec-core";
|
|
2287
2291
|
async function attachPortCommand(args2) {
|
|
2288
2292
|
const port = parseInt(args2[0], 10);
|
|
2289
2293
|
if (isNaN(port)) {
|
|
2290
2294
|
console.error("Error: valid port number is required");
|
|
2291
2295
|
process.exit(1);
|
|
2292
2296
|
}
|
|
2293
|
-
const
|
|
2294
|
-
const detector = new PortDetector();
|
|
2297
|
+
const detector = new PortDetector2();
|
|
2295
2298
|
const info = detector.detectByPort(port);
|
|
2296
2299
|
if (info) {
|
|
2297
2300
|
console.log(`Found process on port ${port}: PID ${info.pid}${info.command ? ` (${info.command})` : ""}`);
|
|
@@ -2304,6 +2307,7 @@ async function attachPortCommand(args2) {
|
|
|
2304
2307
|
// src/commands/watch.ts
|
|
2305
2308
|
import { existsSync } from "fs";
|
|
2306
2309
|
import { resolve } from "path";
|
|
2310
|
+
import { LogWatcher } from "@plumpslabs/fennec-core";
|
|
2307
2311
|
async function watchCommand(args2) {
|
|
2308
2312
|
const fileIndex = args2.indexOf("--file");
|
|
2309
2313
|
const filePath = fileIndex !== -1 ? args2[fileIndex + 1] : void 0;
|
|
@@ -2318,7 +2322,6 @@ async function watchCommand(args2) {
|
|
|
2318
2322
|
console.error(`Error: File not found: ${resolvedPath}`);
|
|
2319
2323
|
process.exit(1);
|
|
2320
2324
|
}
|
|
2321
|
-
const { LogWatcher } = await import("@plumpslabs/fennec-core");
|
|
2322
2325
|
const watcher = new LogWatcher();
|
|
2323
2326
|
const watcherId = watcher.watchFile(resolvedPath, name);
|
|
2324
2327
|
console.log(`Watching file: ${resolvedPath}`);
|
|
@@ -2327,12 +2330,12 @@ async function watchCommand(args2) {
|
|
|
2327
2330
|
}
|
|
2328
2331
|
|
|
2329
2332
|
// src/commands/start.ts
|
|
2330
|
-
import { createWriteStream,
|
|
2333
|
+
import { createWriteStream, mkdirSync as mkdirSync2 } from "fs";
|
|
2331
2334
|
import { resolve as resolve3 } from "path";
|
|
2332
2335
|
import { homedir as homedir2 } from "os";
|
|
2333
2336
|
import { spawn } from "child_process";
|
|
2334
2337
|
import { FennecServer } from "@plumpslabs/fennec-core";
|
|
2335
|
-
import
|
|
2338
|
+
import pc5 from "picocolors";
|
|
2336
2339
|
|
|
2337
2340
|
// src/commands/tracker.ts
|
|
2338
2341
|
import { existsSync as existsSync2, writeFileSync, readFileSync, mkdirSync, renameSync, statSync, rmSync } from "fs";
|
|
@@ -2676,10 +2679,198 @@ function formatProcessState(state) {
|
|
|
2676
2679
|
}
|
|
2677
2680
|
}
|
|
2678
2681
|
|
|
2682
|
+
// src/commands/ps.ts
|
|
2683
|
+
import pc4 from "picocolors";
|
|
2684
|
+
async function psCommand(args2) {
|
|
2685
|
+
const watchFlag = args2.includes("-w") || args2.includes("--watch");
|
|
2686
|
+
const systemFlag = args2.includes("--system") || args2.includes("-a") || args2.includes("--all");
|
|
2687
|
+
const jsonFlag = args2.includes("--json");
|
|
2688
|
+
const nameFilter = args2.includes("--name") ? args2[args2.indexOf("--name") + 1] : void 0;
|
|
2689
|
+
const sortBy = args2.includes("--sort") ? args2[args2.indexOf("--sort") + 1] : "name";
|
|
2690
|
+
if (jsonFlag) {
|
|
2691
|
+
await psJson();
|
|
2692
|
+
return;
|
|
2693
|
+
}
|
|
2694
|
+
if (watchFlag && systemFlag) {
|
|
2695
|
+
await watchSystemProcesses(sortBy, 15);
|
|
2696
|
+
return;
|
|
2697
|
+
}
|
|
2698
|
+
if (systemFlag) {
|
|
2699
|
+
const spinner = createSpinner("Scanning system processes...");
|
|
2700
|
+
try {
|
|
2701
|
+
const processes = getSystemProcesses({ name: nameFilter, userOnly: true, sortBy, limit: 30 });
|
|
2702
|
+
spinner.stop();
|
|
2703
|
+
process.stdout.write("\r\x1B[K");
|
|
2704
|
+
if (processes.length === 0) {
|
|
2705
|
+
console.error(`
|
|
2706
|
+
${pc4.dim("No system processes found.")}
|
|
2707
|
+
`);
|
|
2708
|
+
return;
|
|
2709
|
+
}
|
|
2710
|
+
const columns2 = [
|
|
2711
|
+
{ key: "pid", label: "PID", align: "right", format: (v) => pc4.dim(String(v).padStart(6)) },
|
|
2712
|
+
{ key: "name", label: "Name", format: (v) => pc4.bold(String(v)) },
|
|
2713
|
+
{ key: "cpu", label: "CPU%", align: "right", format: (v) => {
|
|
2714
|
+
const n = v;
|
|
2715
|
+
return n > 10 ? pc4.red(String(n)) : n > 5 ? pc4.yellow(String(n)) : pc4.dim(String(n));
|
|
2716
|
+
} },
|
|
2717
|
+
{ key: "mem", label: "MEM%", align: "right", format: (v) => {
|
|
2718
|
+
const n = v;
|
|
2719
|
+
return n > 10 ? pc4.red(String(n)) : n > 5 ? pc4.yellow(String(n)) : pc4.dim(String(n));
|
|
2720
|
+
} },
|
|
2721
|
+
{ key: "state", label: "State", format: (v) => {
|
|
2722
|
+
const s = String(v);
|
|
2723
|
+
if (s === "R" || s === "Running") return pc4.green(s);
|
|
2724
|
+
if (s === "Z" || s === "Zombie") return pc4.red(s);
|
|
2725
|
+
if (s === "S" || s === "Sleeping") return pc4.cyan(s);
|
|
2726
|
+
return pc4.dim(s);
|
|
2727
|
+
} }
|
|
2728
|
+
];
|
|
2729
|
+
const rows2 = processes.map((p) => ({ pid: p.pid, name: p.name, cpu: p.cpuPercent, mem: p.memPercent, state: formatProcessState(p.state) }));
|
|
2730
|
+
console.error(`
|
|
2731
|
+
${symbols.fox} ${pc4.bold("System Processes")} ${pc4.dim(`(top ${processes.length} by ${sortBy})`)}
|
|
2732
|
+
`);
|
|
2733
|
+
console.error(renderTable(columns2, rows2));
|
|
2734
|
+
console.error();
|
|
2735
|
+
} catch (error) {
|
|
2736
|
+
spinner.fail("Failed to scan processes");
|
|
2737
|
+
console.error(renderError("Process scan failed", String(error)));
|
|
2738
|
+
}
|
|
2739
|
+
return;
|
|
2740
|
+
}
|
|
2741
|
+
const tracked = readTracked();
|
|
2742
|
+
if (tracked.length === 0) {
|
|
2743
|
+
console.error(`
|
|
2744
|
+
${pc4.dim("No tracked processes.")}`);
|
|
2745
|
+
console.error(` ${pc4.dim("Start an app with:")} ${pc4.cyan("fennec start <command> --name <name>")}
|
|
2746
|
+
`);
|
|
2747
|
+
return;
|
|
2748
|
+
}
|
|
2749
|
+
const columns = [
|
|
2750
|
+
{ key: "name", label: "App", format: (v) => pc4.bold(String(v)) },
|
|
2751
|
+
{ key: "pid", label: "PID", align: "right" },
|
|
2752
|
+
{ key: "status", label: "Status", format: (v) => {
|
|
2753
|
+
const s = v;
|
|
2754
|
+
return s === "running" ? pc4.green("\u25CF running") : pc4.red("\u25CB stopped");
|
|
2755
|
+
} },
|
|
2756
|
+
{ key: "port", label: "Port", format: (v) => {
|
|
2757
|
+
const p = v;
|
|
2758
|
+
return p ? pc4.yellow(`:${p}`) : pc4.dim("-");
|
|
2759
|
+
} },
|
|
2760
|
+
{ key: "command", label: "Command", format: (v) => {
|
|
2761
|
+
const c = String(v);
|
|
2762
|
+
return c.length > 50 ? c.slice(0, 50) + "\u2026" : c;
|
|
2763
|
+
} },
|
|
2764
|
+
{ key: "uptime", label: "Uptime", format: (v) => pc4.dim(String(v)) }
|
|
2765
|
+
];
|
|
2766
|
+
const rows = tracked.map((t) => {
|
|
2767
|
+
const running = isProcessRunning(t.pid);
|
|
2768
|
+
const uptime = running ? formatUptime(Math.floor((Date.now() - new Date(t.startedAt).getTime()) / 1e3)) : "-";
|
|
2769
|
+
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 };
|
|
2770
|
+
});
|
|
2771
|
+
const runningCount = tracked.filter((t) => isProcessRunning(t.pid)).length;
|
|
2772
|
+
console.error(`
|
|
2773
|
+
${symbols.fox} ${pc4.bold("Fennec Apps")} ${pc4.dim(`(${runningCount}/${tracked.length} running)`)}
|
|
2774
|
+
`);
|
|
2775
|
+
console.error(renderTable(columns, rows));
|
|
2776
|
+
console.error(` ${pc4.dim("Use")} ${pc4.cyan("fennec start <command> --name <name> --port <port>")} ${pc4.dim("to add more apps.")}`);
|
|
2777
|
+
console.error(` ${pc4.dim("Use")} ${pc4.cyan("fennec log <name>")} ${pc4.dim("to view logs.")}`);
|
|
2778
|
+
console.error(` ${pc4.dim("Use")} ${pc4.cyan("fennec stop <name>")} ${pc4.dim("to pause an app.")}`);
|
|
2779
|
+
console.error(` ${pc4.dim("Use")} ${pc4.cyan("fennec spawn <name>")} ${pc4.dim("to resume a paused app.")}`);
|
|
2780
|
+
console.error(` ${pc4.dim("Use")} ${pc4.cyan("fennec kill <name>")} ${pc4.dim("to permanently remove an app.")}`);
|
|
2781
|
+
console.error();
|
|
2782
|
+
}
|
|
2783
|
+
async function psJson() {
|
|
2784
|
+
const tracked = readTracked();
|
|
2785
|
+
const data = tracked.map((t) => {
|
|
2786
|
+
const running = isProcessRunning(t.pid);
|
|
2787
|
+
return {
|
|
2788
|
+
name: t.name,
|
|
2789
|
+
pid: t.pid,
|
|
2790
|
+
status: running ? "running" : "stopped",
|
|
2791
|
+
port: t.port ?? null,
|
|
2792
|
+
command: t.command,
|
|
2793
|
+
cwd: t.cwd ?? null,
|
|
2794
|
+
startedAt: t.startedAt,
|
|
2795
|
+
uptime: running ? Math.floor((Date.now() - new Date(t.startedAt).getTime()) / 1e3) : null
|
|
2796
|
+
};
|
|
2797
|
+
});
|
|
2798
|
+
console.log(JSON.stringify(data, null, 2));
|
|
2799
|
+
}
|
|
2800
|
+
async function statusCommand(_args) {
|
|
2801
|
+
const watchFlag = _args.includes("-w") || _args.includes("--watch");
|
|
2802
|
+
const tracked = readTracked();
|
|
2803
|
+
const topSystem = getSystemProcesses({ userOnly: true, sortBy: "cpu", limit: 5 });
|
|
2804
|
+
const totalUserProcs = getSystemProcesses({ userOnly: true }).length;
|
|
2805
|
+
console.error(`
|
|
2806
|
+
${symbols.fox} ${pc4.bold("Fennec Status")}
|
|
2807
|
+
`);
|
|
2808
|
+
if (tracked.length > 0) {
|
|
2809
|
+
const runningCount = tracked.filter((t) => isProcessRunning(t.pid)).length;
|
|
2810
|
+
console.error(` ${pc4.bold("Managed Apps")} ${pc4.dim(`(${runningCount}/${tracked.length} running)`)}
|
|
2811
|
+
`);
|
|
2812
|
+
for (const t of tracked) {
|
|
2813
|
+
const running = isProcessRunning(t.pid);
|
|
2814
|
+
const statusIcon = running ? pc4.green("\u25CF") : pc4.red("\u25CB");
|
|
2815
|
+
const portStr = t.port ? ` ${pc4.yellow(`:${t.port}`)}` : "";
|
|
2816
|
+
const uptime = running ? pc4.dim(formatUptime(Math.floor((Date.now() - new Date(t.startedAt).getTime()) / 1e3))) : pc4.red("stopped");
|
|
2817
|
+
console.error(` ${statusIcon} ${pc4.bold(t.name)}${portStr} ${pc4.dim(`(PID ${t.pid})`)} \u2014 ${uptime}`);
|
|
2818
|
+
}
|
|
2819
|
+
console.error();
|
|
2820
|
+
} else {
|
|
2821
|
+
console.error(` ${pc4.dim("No managed apps.")} ${pc4.cyan("fennec start <command> --name <name>")}
|
|
2822
|
+
`);
|
|
2823
|
+
}
|
|
2824
|
+
console.error(` ${pc4.bold("System")} ${pc4.dim(`(${totalUserProcs} user processes)`)}`);
|
|
2825
|
+
for (const p of topSystem) {
|
|
2826
|
+
const cpuStr = p.cpuPercent > 10 ? pc4.red(`${p.cpuPercent}%`) : p.cpuPercent > 5 ? pc4.yellow(`${p.cpuPercent}%`) : pc4.dim(`${p.cpuPercent}%`);
|
|
2827
|
+
const memStr = p.memPercent > 10 ? pc4.red(`${p.memPercent}%`) : p.memPercent > 5 ? pc4.yellow(`${p.memPercent}%`) : pc4.dim(`${p.memPercent}%`);
|
|
2828
|
+
console.error(` ${pc4.dim(`PID ${p.pid}`)} ${pc4.bold(p.name)} \u2014 CPU: ${cpuStr} MEM: ${memStr}`);
|
|
2829
|
+
}
|
|
2830
|
+
if (watchFlag) await watchSystemProcesses("cpu", 15);
|
|
2831
|
+
console.error();
|
|
2832
|
+
}
|
|
2833
|
+
async function watchSystemProcesses(sortBy, limit) {
|
|
2834
|
+
console.error(`
|
|
2835
|
+
${pc4.bold("Watching system processes")} ${pc4.dim("(Ctrl+C to stop, refreshes every 3s)")}
|
|
2836
|
+
`);
|
|
2837
|
+
const render = () => {
|
|
2838
|
+
const processes = getSystemProcesses({ userOnly: true, sortBy, limit });
|
|
2839
|
+
const columns = [
|
|
2840
|
+
{ key: "pid", label: "PID", align: "right", format: (v) => pc4.dim(String(v).padStart(6)) },
|
|
2841
|
+
{ key: "name", label: "Name", format: (v) => pc4.bold(String(v)) },
|
|
2842
|
+
{ key: "cpu", label: "CPU%", align: "right", format: (v) => {
|
|
2843
|
+
const n = v;
|
|
2844
|
+
return n > 10 ? pc4.red(String(n)) : n > 5 ? pc4.yellow(String(n)) : pc4.dim(String(n));
|
|
2845
|
+
} },
|
|
2846
|
+
{ key: "mem", label: "MEM%", align: "right", format: (v) => {
|
|
2847
|
+
const n = v;
|
|
2848
|
+
return n > 10 ? pc4.red(String(n)) : n > 5 ? pc4.yellow(String(n)) : pc4.dim(String(n));
|
|
2849
|
+
} },
|
|
2850
|
+
{ key: "state", label: "S", align: "center" }
|
|
2851
|
+
];
|
|
2852
|
+
const rows = processes.map((p) => ({ pid: p.pid, name: p.name, cpu: p.cpuPercent, mem: p.memPercent, state: p.state }));
|
|
2853
|
+
return ` ${timestamp()} ${pc4.dim(`${processes.length} processes`)}
|
|
2854
|
+
${renderTable(columns, rows, { compact: true })}`;
|
|
2855
|
+
};
|
|
2856
|
+
console.error(render());
|
|
2857
|
+
const interval = setInterval(() => {
|
|
2858
|
+
process.stdout.write("\x1B[J");
|
|
2859
|
+
console.error(render());
|
|
2860
|
+
}, 3e3);
|
|
2861
|
+
await new Promise((resolve12) => {
|
|
2862
|
+
process.once("SIGINT", () => {
|
|
2863
|
+
clearInterval(interval);
|
|
2864
|
+
process.stdout.write("\x1B[J");
|
|
2865
|
+
resolve12();
|
|
2866
|
+
});
|
|
2867
|
+
});
|
|
2868
|
+
}
|
|
2869
|
+
|
|
2679
2870
|
// src/commands/start.ts
|
|
2680
2871
|
async function startServer(args2) {
|
|
2681
2872
|
printBanner();
|
|
2682
|
-
console.error(` ${
|
|
2873
|
+
console.error(` ${pc5.dim("Starting Fennec MCP server...")}
|
|
2683
2874
|
`);
|
|
2684
2875
|
const configIndex = args2.indexOf("--config");
|
|
2685
2876
|
const configPath = configIndex !== -1 ? args2[configIndex + 1] : void 0;
|
|
@@ -2688,11 +2879,11 @@ async function startServer(args2) {
|
|
|
2688
2879
|
await resurrectTracked();
|
|
2689
2880
|
await server.start();
|
|
2690
2881
|
console.error(`
|
|
2691
|
-
${
|
|
2882
|
+
${pc5.green("\u2713")} ${pc5.bold("Fennec server is running")}`);
|
|
2692
2883
|
console.error(` ${renderKV("Transport", "stdio")}`);
|
|
2693
2884
|
console.error(` ${renderKV("AI Agent", "Connect via MCP protocol")}`);
|
|
2694
2885
|
console.error(`
|
|
2695
|
-
${
|
|
2886
|
+
${pc5.dim("Press Ctrl+C to stop")}
|
|
2696
2887
|
`);
|
|
2697
2888
|
} catch (error) {
|
|
2698
2889
|
console.error(renderError("Failed to start server", String(error)));
|
|
@@ -2701,7 +2892,7 @@ async function startServer(args2) {
|
|
|
2701
2892
|
}
|
|
2702
2893
|
async function startCommand(args2) {
|
|
2703
2894
|
printBanner();
|
|
2704
|
-
console.error(` ${
|
|
2895
|
+
console.error(` ${pc5.dim("Starting app...")}
|
|
2705
2896
|
`);
|
|
2706
2897
|
await runCommand(args2);
|
|
2707
2898
|
}
|
|
@@ -2712,12 +2903,13 @@ async function runCommand(args2) {
|
|
|
2712
2903
|
const port = portIndex !== -1 ? parseInt(args2[portIndex + 1], 10) : void 0;
|
|
2713
2904
|
const cwdIndex = args2.indexOf("--cwd");
|
|
2714
2905
|
const cwd = cwdIndex !== -1 ? args2[cwdIndex + 1] : void 0;
|
|
2906
|
+
const restartFlag = args2.includes("--restart");
|
|
2715
2907
|
const stopFlags = [nameIndex, portIndex, cwdIndex].filter((i) => i !== -1);
|
|
2716
2908
|
const cmdEnd = Math.min(...stopFlags, Infinity);
|
|
2717
2909
|
const cmdParts = args2.slice(0, cmdEnd);
|
|
2718
2910
|
const cmd = cmdParts.join(" ");
|
|
2719
2911
|
if (!cmd) {
|
|
2720
|
-
console.error(renderError("Missing command", "Usage: fennec start|run <command> --name <name> [--port <port>] [--cwd <dir>]"));
|
|
2912
|
+
console.error(renderError("Missing command", "Usage: fennec start|run <command> --name <name> [--port <port>] [--cwd <dir>] [--restart]"));
|
|
2721
2913
|
process.exit(1);
|
|
2722
2914
|
}
|
|
2723
2915
|
const appName = name ?? cmdParts[0] ?? "app";
|
|
@@ -2725,9 +2917,9 @@ async function runCommand(args2) {
|
|
|
2725
2917
|
const existing = tracked.find((t) => t.name === appName);
|
|
2726
2918
|
if (existing && isProcessRunning(existing.pid)) {
|
|
2727
2919
|
console.error();
|
|
2728
|
-
console.error(` ${
|
|
2729
|
-
console.error(` ${renderKV("Logs",
|
|
2730
|
-
console.error(` ${renderKV("Stop",
|
|
2920
|
+
console.error(` ${pc5.yellow("\u26A0")} ${pc5.bold(appName)} ${pc5.dim(`is already running (PID: ${existing.pid})`)}`);
|
|
2921
|
+
console.error(` ${renderKV("Logs", pc5.cyan(`fennec log ${appName}`))}`);
|
|
2922
|
+
console.error(` ${renderKV("Stop", pc5.cyan(`fennec kill ${appName}`))}`);
|
|
2731
2923
|
console.error();
|
|
2732
2924
|
process.exit(0);
|
|
2733
2925
|
}
|
|
@@ -2735,25 +2927,26 @@ async function runCommand(args2) {
|
|
|
2735
2927
|
mkdirSync2(logDir, { recursive: true });
|
|
2736
2928
|
const logFilePath = resolve3(logDir, `${appName}.log`);
|
|
2737
2929
|
console.error(`
|
|
2738
|
-
${symbols.fox} ${
|
|
2930
|
+
${symbols.fox} ${pc5.bold("Starting")} ${renderAppName(appName)} ${pc5.dim("(daemon)")}
|
|
2739
2931
|
`);
|
|
2740
2932
|
console.error(` ${renderKV("Command", cmd)}`);
|
|
2741
2933
|
if (port) console.error(` ${renderKV("Port", String(port))}`);
|
|
2742
2934
|
if (cwd) console.error(` ${renderKV("Directory", cwd)}`);
|
|
2935
|
+
if (restartFlag) console.error(` ${renderKV("Auto-restart", pc5.green("enabled"))}`);
|
|
2743
2936
|
console.error(` ${divider()}`);
|
|
2744
2937
|
try {
|
|
2745
|
-
|
|
2938
|
+
let currentChild = spawn(cmdParts[0], cmdParts.slice(1), {
|
|
2746
2939
|
cwd,
|
|
2747
2940
|
env: { ...process.env },
|
|
2748
2941
|
stdio: ["ignore", "pipe", "pipe"],
|
|
2749
2942
|
detached: true
|
|
2750
2943
|
});
|
|
2751
|
-
const pid =
|
|
2944
|
+
const pid = currentChild.pid ?? 0;
|
|
2752
2945
|
rotateLogFile(logFilePath);
|
|
2753
2946
|
const logStream = createWriteStream(logFilePath, { flags: "a" });
|
|
2754
|
-
if (
|
|
2755
|
-
if (
|
|
2756
|
-
|
|
2947
|
+
if (currentChild.stdout) currentChild.stdout.pipe(logStream);
|
|
2948
|
+
if (currentChild.stderr) currentChild.stderr.pipe(logStream);
|
|
2949
|
+
currentChild.unref();
|
|
2757
2950
|
addTracked({
|
|
2758
2951
|
name: appName,
|
|
2759
2952
|
pid,
|
|
@@ -2762,32 +2955,45 @@ async function runCommand(args2) {
|
|
|
2762
2955
|
cwd,
|
|
2763
2956
|
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2764
2957
|
});
|
|
2765
|
-
console.error(`
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2958
|
+
console.error(` ${pc5.green("\u2713")} ${pc5.bold(appName)} ${pc5.dim(`started (PID: ${pid})`)}`);
|
|
2959
|
+
if (restartFlag) {
|
|
2960
|
+
let setupRestartWatch2 = function(c) {
|
|
2961
|
+
c.on("exit", (code, signal) => {
|
|
2962
|
+
if (code !== 0 && signal !== "SIGTERM" && signal !== "SIGKILL") {
|
|
2963
|
+
console.error(` ${pc5.yellow("\u26A0")} ${pc5.bold(appName)} ${pc5.dim(`exited with code ${code}, restarting...`)}`);
|
|
2964
|
+
const restarted = spawn(cmdParts[0], cmdParts.slice(1), {
|
|
2965
|
+
cwd,
|
|
2966
|
+
env: { ...process.env },
|
|
2967
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
2968
|
+
detached: true
|
|
2969
|
+
});
|
|
2970
|
+
const newPid = restarted.pid ?? 0;
|
|
2971
|
+
rotateLogFile(logFilePath);
|
|
2972
|
+
const rs = createWriteStream(logFilePath, { flags: "a" });
|
|
2973
|
+
if (restarted.stdout) restarted.stdout.pipe(rs);
|
|
2974
|
+
if (restarted.stderr) restarted.stderr.pipe(rs);
|
|
2975
|
+
restarted.unref();
|
|
2976
|
+
addTracked({ name: appName, pid: newPid, command: cmd, port, cwd, startedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
2977
|
+
console.error(` ${pc5.green("\u2713")} ${pc5.bold(appName)} ${pc5.dim(`restarted (PID: ${newPid})`)}`);
|
|
2978
|
+
currentChild = restarted;
|
|
2979
|
+
setupRestartWatch2(restarted);
|
|
2780
2980
|
}
|
|
2781
|
-
}
|
|
2782
|
-
}
|
|
2783
|
-
|
|
2981
|
+
});
|
|
2982
|
+
};
|
|
2983
|
+
var setupRestartWatch = setupRestartWatch2;
|
|
2984
|
+
console.error(` ${pc5.dim("Auto-restart enabled \u2014 watching for crashes...")}
|
|
2985
|
+
`);
|
|
2986
|
+
setupRestartWatch2(currentChild);
|
|
2987
|
+
await new Promise((resolve12) => {
|
|
2988
|
+
process.once("SIGINT", () => {
|
|
2989
|
+
currentChild.kill("SIGTERM");
|
|
2990
|
+
resolve12();
|
|
2991
|
+
});
|
|
2992
|
+
});
|
|
2993
|
+
}
|
|
2994
|
+
if (!restartFlag) {
|
|
2995
|
+
await psCommand([]);
|
|
2784
2996
|
}
|
|
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();
|
|
2791
2997
|
} catch (error) {
|
|
2792
2998
|
console.error(renderError(`Failed to start ${appName}`, String(error)));
|
|
2793
2999
|
process.exit(1);
|
|
@@ -2838,227 +3044,70 @@ async function resurrectTracked() {
|
|
|
2838
3044
|
spinner.stop();
|
|
2839
3045
|
process.stdout.write("\r\x1B[K");
|
|
2840
3046
|
if (resurrected > 0) {
|
|
2841
|
-
console.error(` ${
|
|
3047
|
+
console.error(` ${pc5.green("\u25CF")} ${pc5.bold(`Resurrected ${resurrected} process(es)`)}`);
|
|
2842
3048
|
}
|
|
2843
3049
|
if (failed > 0) {
|
|
2844
|
-
console.error(` ${
|
|
3050
|
+
console.error(` ${pc5.red("\u25CF")} ${pc5.bold(`${failed} process(es) failed to resurrect`)}`);
|
|
2845
3051
|
}
|
|
2846
3052
|
}
|
|
2847
3053
|
|
|
2848
|
-
// src/commands/
|
|
2849
|
-
import
|
|
2850
|
-
async function
|
|
2851
|
-
const
|
|
2852
|
-
const
|
|
2853
|
-
const
|
|
2854
|
-
const
|
|
2855
|
-
if (
|
|
2856
|
-
await
|
|
3054
|
+
// src/commands/kill.ts
|
|
3055
|
+
import pc6 from "picocolors";
|
|
3056
|
+
async function killCommand(args2) {
|
|
3057
|
+
const rawTarget = args2[0];
|
|
3058
|
+
const signalIndex = args2.indexOf("--signal");
|
|
3059
|
+
const signalRaw = signalIndex !== -1 ? args2[signalIndex + 1] : "SIGTERM";
|
|
3060
|
+
const signal = signalRaw ?? "SIGTERM";
|
|
3061
|
+
if (rawTarget === "all" || args2.includes("--all") || args2.includes("-a")) {
|
|
3062
|
+
await killAll(signal);
|
|
2857
3063
|
return;
|
|
2858
3064
|
}
|
|
2859
|
-
if (
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
3065
|
+
if (!rawTarget) {
|
|
3066
|
+
console.error(renderError("Missing target", "Usage: fennec kill <pid|name|all> [--signal SIGTERM|SIGKILL|SIGINT]"));
|
|
3067
|
+
process.exit(1);
|
|
3068
|
+
}
|
|
3069
|
+
let targetPid;
|
|
3070
|
+
let displayName;
|
|
3071
|
+
const pid = parseInt(rawTarget, 10);
|
|
3072
|
+
if (!isNaN(pid) && String(pid) === rawTarget) {
|
|
3073
|
+
if (!isProcessRunning(pid)) {
|
|
3074
|
+
console.error(renderError("Process not found", `No process with PID ${pid} is running`));
|
|
3075
|
+
process.exit(1);
|
|
3076
|
+
}
|
|
3077
|
+
targetPid = pid;
|
|
3078
|
+
displayName = `PID ${pid}`;
|
|
3079
|
+
} else {
|
|
3080
|
+
const tracked = readTracked();
|
|
3081
|
+
const trackedMatch = tracked.find((t) => t.name === rawTarget);
|
|
3082
|
+
if (trackedMatch && isProcessRunning(trackedMatch.pid)) {
|
|
3083
|
+
targetPid = trackedMatch.pid;
|
|
3084
|
+
displayName = `${trackedMatch.name} (PID ${targetPid})`;
|
|
3085
|
+
} else {
|
|
3086
|
+
const matches = getSystemProcesses({ name: rawTarget, userOnly: true, sortBy: "cpu" });
|
|
3087
|
+
if (matches.length === 0) {
|
|
3088
|
+
console.error(renderError("Process not found", `No running process matching "${rawTarget}"`));
|
|
3089
|
+
process.exit(1);
|
|
3090
|
+
}
|
|
3091
|
+
if (matches.length > 1) {
|
|
2866
3092
|
console.error(`
|
|
2867
|
-
${
|
|
2868
|
-
|
|
2869
|
-
|
|
3093
|
+
${pc6.bold(`Multiple processes match "${rawTarget}":`)}`);
|
|
3094
|
+
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}%` })));
|
|
3095
|
+
if (selected === null) {
|
|
3096
|
+
console.error(` ${pc6.dim("Cancelled")}`);
|
|
3097
|
+
return;
|
|
3098
|
+
}
|
|
3099
|
+
const idx = parseInt(selected, 10);
|
|
3100
|
+
targetPid = matches[idx].pid;
|
|
3101
|
+
displayName = `${matches[idx].name} (PID ${targetPid})`;
|
|
3102
|
+
} else {
|
|
3103
|
+
targetPid = matches[0].pid;
|
|
3104
|
+
displayName = `${matches[0].name} (PID ${targetPid})`;
|
|
2870
3105
|
}
|
|
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
3106
|
}
|
|
2900
|
-
return;
|
|
2901
3107
|
}
|
|
2902
|
-
const
|
|
2903
|
-
if (
|
|
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
|
-
// src/commands/kill.ts
|
|
3013
|
-
import pc6 from "picocolors";
|
|
3014
|
-
async function killCommand(args2) {
|
|
3015
|
-
const rawTarget = args2[0];
|
|
3016
|
-
const signalIndex = args2.indexOf("--signal");
|
|
3017
|
-
const signalRaw = signalIndex !== -1 ? args2[signalIndex + 1] : "SIGTERM";
|
|
3018
|
-
const signal = signalRaw ?? "SIGTERM";
|
|
3019
|
-
if (rawTarget === "all" || args2.includes("--all") || args2.includes("-a")) {
|
|
3020
|
-
await killAll(signal);
|
|
3021
|
-
return;
|
|
3022
|
-
}
|
|
3023
|
-
if (!rawTarget) {
|
|
3024
|
-
console.error(renderError("Missing target", "Usage: fennec kill <pid|name|all> [--signal SIGTERM|SIGKILL|SIGINT]"));
|
|
3025
|
-
process.exit(1);
|
|
3026
|
-
}
|
|
3027
|
-
let targetPid;
|
|
3028
|
-
let displayName;
|
|
3029
|
-
const pid = parseInt(rawTarget, 10);
|
|
3030
|
-
if (!isNaN(pid) && String(pid) === rawTarget) {
|
|
3031
|
-
if (!isProcessRunning(pid)) {
|
|
3032
|
-
console.error(renderError("Process not found", `No process with PID ${pid} is running`));
|
|
3033
|
-
process.exit(1);
|
|
3034
|
-
}
|
|
3035
|
-
targetPid = pid;
|
|
3036
|
-
displayName = `PID ${pid}`;
|
|
3037
|
-
} else {
|
|
3038
|
-
const matches = getSystemProcesses({ name: rawTarget, userOnly: true, sortBy: "cpu" });
|
|
3039
|
-
if (matches.length === 0) {
|
|
3040
|
-
console.error(renderError("Process not found", `No running process matching "${rawTarget}"`));
|
|
3041
|
-
process.exit(1);
|
|
3042
|
-
}
|
|
3043
|
-
if (matches.length > 1) {
|
|
3044
|
-
console.error(`
|
|
3045
|
-
${pc6.bold(`Multiple processes match "${rawTarget}":`)}`);
|
|
3046
|
-
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}%` })));
|
|
3047
|
-
if (selected === null) {
|
|
3048
|
-
console.error(` ${pc6.dim("Cancelled")}`);
|
|
3049
|
-
return;
|
|
3050
|
-
}
|
|
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
|
-
}
|
|
3058
|
-
}
|
|
3059
|
-
const confirmed = await confirmPrompt(`Kill ${pc6.bold(displayName)} with ${pc6.yellow(signal)}?`, false);
|
|
3060
|
-
if (!confirmed) {
|
|
3061
|
-
console.error(` ${pc6.dim("Cancelled")}`);
|
|
3108
|
+
const confirmed = await confirmPrompt(`Kill ${pc6.bold(displayName)} with ${pc6.yellow(signal)}?`, false);
|
|
3109
|
+
if (!confirmed) {
|
|
3110
|
+
console.error(` ${pc6.dim("Cancelled")}`);
|
|
3062
3111
|
return;
|
|
3063
3112
|
}
|
|
3064
3113
|
const spinner = createSpinner(`Sending ${signal} to ${displayName}...`);
|
|
@@ -3120,12 +3169,295 @@ async function killAll(signal) {
|
|
|
3120
3169
|
if (failed > 0) console.error(` ${pc6.yellow(`${failed} process(es) could not be killed`)} ${pc6.dim("(try with sudo)")}`);
|
|
3121
3170
|
}
|
|
3122
3171
|
|
|
3123
|
-
// src/commands/
|
|
3172
|
+
// src/commands/stop.ts
|
|
3124
3173
|
import pc7 from "picocolors";
|
|
3125
|
-
|
|
3174
|
+
async function stopCommand(args2) {
|
|
3175
|
+
const stopAll = args2.includes("--all") || args2.includes("-a");
|
|
3176
|
+
if (stopAll) {
|
|
3177
|
+
await stopAllTracked();
|
|
3178
|
+
return;
|
|
3179
|
+
}
|
|
3180
|
+
const rawTarget = args2[0];
|
|
3181
|
+
if (!rawTarget) {
|
|
3182
|
+
console.error(renderError("Missing name", "Usage: fennec stop <name|--all>"));
|
|
3183
|
+
process.exit(1);
|
|
3184
|
+
}
|
|
3185
|
+
const tracked = readTracked();
|
|
3186
|
+
const trackedMatch = tracked.find((t) => t.name === rawTarget);
|
|
3187
|
+
if (!trackedMatch) {
|
|
3188
|
+
console.error(renderError("Not found", `No tracked process named "${rawTarget}". Use ${pc7.cyan("fennec kill <pid>")} to kill a system process.`));
|
|
3189
|
+
process.exit(1);
|
|
3190
|
+
}
|
|
3191
|
+
if (!isProcessRunning(trackedMatch.pid)) {
|
|
3192
|
+
console.error(`
|
|
3193
|
+
${pc7.yellow("\u26A0")} ${pc7.bold(rawTarget)} ${pc7.dim("is already stopped")}`);
|
|
3194
|
+
console.error();
|
|
3195
|
+
process.exit(0);
|
|
3196
|
+
}
|
|
3197
|
+
const displayName = `${trackedMatch.name} (PID ${trackedMatch.pid})`;
|
|
3198
|
+
const confirmed = await confirmPrompt(`Stop ${pc7.bold(displayName)}? ${pc7.dim("It can be re-spawned later via fennec spawn")}`, false);
|
|
3199
|
+
if (!confirmed) {
|
|
3200
|
+
console.error(` ${pc7.dim("Cancelled")}`);
|
|
3201
|
+
return;
|
|
3202
|
+
}
|
|
3203
|
+
await stopSingleProcess(trackedMatch.pid, trackedMatch.name);
|
|
3204
|
+
}
|
|
3205
|
+
async function stopAllTracked() {
|
|
3206
|
+
const tracked = readTracked();
|
|
3207
|
+
const running = tracked.filter((t) => isProcessRunning(t.pid));
|
|
3208
|
+
if (running.length === 0) {
|
|
3209
|
+
console.error(`
|
|
3210
|
+
${pc7.dim("No running tracked processes to stop.")}
|
|
3211
|
+
`);
|
|
3212
|
+
return;
|
|
3213
|
+
}
|
|
3214
|
+
console.error(`
|
|
3215
|
+
${pc7.yellow("\u26A0")} ${pc7.bold(`Stop all ${running.length} running process(es)?`)} ${pc7.dim("(They can be re-spawned later)")}
|
|
3216
|
+
`);
|
|
3217
|
+
for (const t of running) {
|
|
3218
|
+
console.error(` ${pc7.green("\u25CF")} ${pc7.bold(t.name)} ${pc7.dim(`(PID ${t.pid})`)}`);
|
|
3219
|
+
}
|
|
3220
|
+
console.error();
|
|
3221
|
+
const confirmed = await confirmPrompt("Stop all?", false);
|
|
3222
|
+
if (!confirmed) {
|
|
3223
|
+
console.error(` ${pc7.dim("Cancelled")}
|
|
3224
|
+
`);
|
|
3225
|
+
return;
|
|
3226
|
+
}
|
|
3227
|
+
const spinner = createSpinner(`Stopping ${running.length} process(es)...`);
|
|
3228
|
+
let stopped = 0;
|
|
3229
|
+
let failed = 0;
|
|
3230
|
+
for (const t of running) {
|
|
3231
|
+
if (killProcess(t.pid, "SIGTERM")) {
|
|
3232
|
+
stopped++;
|
|
3233
|
+
} else {
|
|
3234
|
+
failed++;
|
|
3235
|
+
}
|
|
3236
|
+
}
|
|
3237
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
3238
|
+
spinner.stop();
|
|
3239
|
+
process.stdout.write("\r\x1B[K");
|
|
3240
|
+
if (stopped > 0) {
|
|
3241
|
+
console.error(` ${pc7.green("\u2713")} ${pc7.bold(`Stopped ${stopped} process(es)`)} ${pc7.dim("(kept in tracked.json)")}`);
|
|
3242
|
+
}
|
|
3243
|
+
if (failed > 0) {
|
|
3244
|
+
console.error(` ${pc7.red("\u2717")} ${pc7.bold(`${failed} process(es) failed to stop`)}`);
|
|
3245
|
+
}
|
|
3246
|
+
console.error();
|
|
3247
|
+
await psCommand([]);
|
|
3248
|
+
}
|
|
3249
|
+
async function stopSingleProcess(pid, name) {
|
|
3250
|
+
const displayName = `${name} (PID ${pid})`;
|
|
3251
|
+
const spinner = createSpinner(`Stopping ${displayName}...`);
|
|
3252
|
+
const success = killProcess(pid, "SIGTERM");
|
|
3253
|
+
if (success) {
|
|
3254
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
3255
|
+
const stillRunning = isProcessRunning(pid);
|
|
3256
|
+
if (stillRunning) {
|
|
3257
|
+
spinner.warn(`${displayName} did not respond to SIGTERM`);
|
|
3258
|
+
const forceKill = await confirmPrompt(`Send ${pc7.red("SIGKILL")} to force stop?`, true);
|
|
3259
|
+
if (forceKill) {
|
|
3260
|
+
const forceSpinner = createSpinner(`Sending SIGKILL to ${displayName}...`);
|
|
3261
|
+
killProcess(pid, "SIGKILL");
|
|
3262
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
3263
|
+
isProcessRunning(pid) ? forceSpinner.fail(`${displayName} could not be stopped (permission denied?)`) : forceSpinner.succeed(`${displayName} force stopped`);
|
|
3264
|
+
} else {
|
|
3265
|
+
console.error(` ${pc7.yellow("\u26A0")} ${displayName} ${pc7.dim("may still be running")}`);
|
|
3266
|
+
}
|
|
3267
|
+
} else {
|
|
3268
|
+
spinner.succeed(`${displayName} stopped`);
|
|
3269
|
+
}
|
|
3270
|
+
console.error();
|
|
3271
|
+
await psCommand([]);
|
|
3272
|
+
} else {
|
|
3273
|
+
spinner.fail(`Failed to stop ${displayName}`);
|
|
3274
|
+
console.error(renderError("Permission denied", "Try running with sudo or use fennec kill."));
|
|
3275
|
+
}
|
|
3276
|
+
}
|
|
3277
|
+
|
|
3278
|
+
// src/commands/spawn.ts
|
|
3279
|
+
import pc8 from "picocolors";
|
|
3280
|
+
import { createWriteStream as createWriteStream2, mkdirSync as mkdirSync3 } from "fs";
|
|
3126
3281
|
import { resolve as resolve4 } from "path";
|
|
3127
3282
|
import { homedir as homedir3 } from "os";
|
|
3128
3283
|
import { spawn as spawn2 } from "child_process";
|
|
3284
|
+
async function spawnCommand(args2) {
|
|
3285
|
+
const spawnAll = args2.includes("--all") || args2.includes("-a");
|
|
3286
|
+
const name = args2[0];
|
|
3287
|
+
if (spawnAll) {
|
|
3288
|
+
await spawnAllStopped();
|
|
3289
|
+
return;
|
|
3290
|
+
}
|
|
3291
|
+
if (!name) {
|
|
3292
|
+
await spawnList();
|
|
3293
|
+
return;
|
|
3294
|
+
}
|
|
3295
|
+
const tracked = readTracked();
|
|
3296
|
+
const match = tracked.find((t) => t.name === name);
|
|
3297
|
+
if (!match) {
|
|
3298
|
+
console.error(renderError("Not found", `No tracked process named "${name}". Use ${pc8.cyan("fennec spawn")} to see available processes, or ${pc8.cyan("fennec start <command> --name <name>")} to create a new one.`));
|
|
3299
|
+
process.exit(1);
|
|
3300
|
+
}
|
|
3301
|
+
if (isProcessRunning(match.pid)) {
|
|
3302
|
+
console.error(`
|
|
3303
|
+
${pc8.yellow("\u26A0")} ${pc8.bold(name)} ${pc8.dim("is already running (PID:")} ${match.pid}${pc8.dim(")")}`);
|
|
3304
|
+
console.error(` ${pc8.dim("Use")} ${pc8.cyan(`fennec stop ${name}`)} ${pc8.dim("to stop it first.")}`);
|
|
3305
|
+
console.error();
|
|
3306
|
+
process.exit(0);
|
|
3307
|
+
}
|
|
3308
|
+
if (!match.command) {
|
|
3309
|
+
console.error(renderError("No command saved", `"${name}" has no saved command and cannot be re-spawned.`));
|
|
3310
|
+
process.exit(1);
|
|
3311
|
+
}
|
|
3312
|
+
await respawnProcess(match);
|
|
3313
|
+
}
|
|
3314
|
+
async function spawnAllStopped() {
|
|
3315
|
+
const tracked = readTracked();
|
|
3316
|
+
const toSpawn = tracked.filter((t) => !isProcessRunning(t.pid) && t.command);
|
|
3317
|
+
if (toSpawn.length === 0) {
|
|
3318
|
+
console.error(`
|
|
3319
|
+
${pc8.dim("No stopped processes with saved commands to spawn.")}
|
|
3320
|
+
`);
|
|
3321
|
+
return;
|
|
3322
|
+
}
|
|
3323
|
+
console.error(`
|
|
3324
|
+
${symbols.fox} ${pc8.bold(`Spawning ${toSpawn.length} process(es)...`)}
|
|
3325
|
+
`);
|
|
3326
|
+
for (const t of toSpawn) {
|
|
3327
|
+
console.error(` ${pc8.dim("\xB7")} ${pc8.bold(t.name)} ${pc8.dim(`(${t.command})`)}`);
|
|
3328
|
+
}
|
|
3329
|
+
console.error();
|
|
3330
|
+
const spinner = createSpinner("Spawning...");
|
|
3331
|
+
let spawned = 0;
|
|
3332
|
+
let failed = 0;
|
|
3333
|
+
for (const proc of toSpawn) {
|
|
3334
|
+
try {
|
|
3335
|
+
const cmdParts = proc.command.split(/\s+/);
|
|
3336
|
+
const logDir = resolve4(homedir3(), ".fennec", "logs");
|
|
3337
|
+
mkdirSync3(logDir, { recursive: true });
|
|
3338
|
+
const logFilePath = resolve4(logDir, `${proc.name}.log`);
|
|
3339
|
+
const child = spawn2(cmdParts[0], cmdParts.slice(1), {
|
|
3340
|
+
cwd: proc.cwd,
|
|
3341
|
+
env: { ...process.env },
|
|
3342
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
3343
|
+
detached: true
|
|
3344
|
+
});
|
|
3345
|
+
const pid = child.pid ?? 0;
|
|
3346
|
+
rotateLogFile(logFilePath);
|
|
3347
|
+
const logStream = createWriteStream2(logFilePath, { flags: "a" });
|
|
3348
|
+
if (child.stdout) child.stdout.pipe(logStream);
|
|
3349
|
+
if (child.stderr) child.stderr.pipe(logStream);
|
|
3350
|
+
child.unref();
|
|
3351
|
+
addTracked({
|
|
3352
|
+
name: proc.name,
|
|
3353
|
+
pid,
|
|
3354
|
+
command: proc.command,
|
|
3355
|
+
port: proc.port,
|
|
3356
|
+
cwd: proc.cwd,
|
|
3357
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3358
|
+
});
|
|
3359
|
+
spawned++;
|
|
3360
|
+
} catch {
|
|
3361
|
+
failed++;
|
|
3362
|
+
}
|
|
3363
|
+
}
|
|
3364
|
+
spinner.stop();
|
|
3365
|
+
process.stdout.write("\r\x1B[K");
|
|
3366
|
+
if (spawned > 0) {
|
|
3367
|
+
console.error(` ${pc8.green("\u2713")} ${pc8.bold(`Spawned ${spawned} process(es)`)}`);
|
|
3368
|
+
}
|
|
3369
|
+
if (failed > 0) {
|
|
3370
|
+
console.error(` ${pc8.red("\u2717")} ${pc8.bold(`${failed} process(es) failed to spawn`)}`);
|
|
3371
|
+
}
|
|
3372
|
+
console.error();
|
|
3373
|
+
await psCommand([]);
|
|
3374
|
+
}
|
|
3375
|
+
async function spawnList() {
|
|
3376
|
+
const tracked = readTracked();
|
|
3377
|
+
if (tracked.length === 0) {
|
|
3378
|
+
console.error(`
|
|
3379
|
+
${pc8.dim("No tracked processes found.")}`);
|
|
3380
|
+
console.error(` ${pc8.dim("Start an app first:")} ${pc8.cyan("fennec start <command> --name <name>")}`);
|
|
3381
|
+
console.error();
|
|
3382
|
+
return;
|
|
3383
|
+
}
|
|
3384
|
+
const stopped = tracked.filter((t) => !isProcessRunning(t.pid));
|
|
3385
|
+
const running = tracked.filter((t) => isProcessRunning(t.pid));
|
|
3386
|
+
if (stopped.length === 0) {
|
|
3387
|
+
console.error(`
|
|
3388
|
+
${pc8.dim("All tracked processes are running. Nothing to spawn.")}`);
|
|
3389
|
+
console.error();
|
|
3390
|
+
return;
|
|
3391
|
+
}
|
|
3392
|
+
console.error(`
|
|
3393
|
+
${symbols.fox} ${pc8.bold("Available to spawn")} ${pc8.dim(`(${stopped.length}/${tracked.length} stopped)`)}`);
|
|
3394
|
+
if (running.length > 0) {
|
|
3395
|
+
console.error(` ${pc8.dim("Running (use stop first):")}`);
|
|
3396
|
+
for (const r of running) {
|
|
3397
|
+
console.error(` ${pc8.green("\u25CF")} ${pc8.bold(r.name)} ${pc8.dim(`(PID ${r.pid})`)}`);
|
|
3398
|
+
}
|
|
3399
|
+
console.error();
|
|
3400
|
+
}
|
|
3401
|
+
const selected = await selectPrompt("Select a process to spawn:", stopped.map((t) => ({
|
|
3402
|
+
value: t.name,
|
|
3403
|
+
label: t.name,
|
|
3404
|
+
description: t.command.length > 80 ? t.command.slice(0, 80) + "..." : t.command
|
|
3405
|
+
})));
|
|
3406
|
+
if (selected === null) {
|
|
3407
|
+
console.error(` ${pc8.dim("Cancelled")}`);
|
|
3408
|
+
return;
|
|
3409
|
+
}
|
|
3410
|
+
const match = stopped.find((t) => t.name === selected);
|
|
3411
|
+
if (match) {
|
|
3412
|
+
await respawnProcess(match);
|
|
3413
|
+
}
|
|
3414
|
+
}
|
|
3415
|
+
async function respawnProcess(proc) {
|
|
3416
|
+
const cmdParts = proc.command.split(/\s+/);
|
|
3417
|
+
const logDir = resolve4(homedir3(), ".fennec", "logs");
|
|
3418
|
+
mkdirSync3(logDir, { recursive: true });
|
|
3419
|
+
const logFilePath = resolve4(logDir, `${proc.name}.log`);
|
|
3420
|
+
console.error(`
|
|
3421
|
+
${symbols.fox} ${pc8.bold("Spawning")} ${renderAppName(proc.name)} ${pc8.dim("(from saved config)")}
|
|
3422
|
+
`);
|
|
3423
|
+
console.error(` ${renderKV("Command", proc.command)}`);
|
|
3424
|
+
if (proc.cwd) console.error(` ${renderKV("Directory", proc.cwd)}`);
|
|
3425
|
+
if (proc.port) console.error(` ${renderKV("Port", String(proc.port))}`);
|
|
3426
|
+
try {
|
|
3427
|
+
const child = spawn2(cmdParts[0], cmdParts.slice(1), {
|
|
3428
|
+
cwd: proc.cwd,
|
|
3429
|
+
env: { ...process.env },
|
|
3430
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
3431
|
+
detached: true
|
|
3432
|
+
});
|
|
3433
|
+
const pid = child.pid ?? 0;
|
|
3434
|
+
rotateLogFile(logFilePath);
|
|
3435
|
+
const logStream = createWriteStream2(logFilePath, { flags: "a" });
|
|
3436
|
+
if (child.stdout) child.stdout.pipe(logStream);
|
|
3437
|
+
if (child.stderr) child.stderr.pipe(logStream);
|
|
3438
|
+
child.unref();
|
|
3439
|
+
addTracked({
|
|
3440
|
+
name: proc.name,
|
|
3441
|
+
pid,
|
|
3442
|
+
command: proc.command,
|
|
3443
|
+
port: proc.port,
|
|
3444
|
+
cwd: proc.cwd,
|
|
3445
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3446
|
+
});
|
|
3447
|
+
console.error(` ${pc8.green("\u2713")} ${pc8.bold(proc.name)} ${pc8.dim(`spawned (PID: ${pid})`)}`);
|
|
3448
|
+
await psCommand([]);
|
|
3449
|
+
} catch (error) {
|
|
3450
|
+
console.error(renderError(`Failed to spawn ${proc.name}`, String(error)));
|
|
3451
|
+
process.exit(1);
|
|
3452
|
+
}
|
|
3453
|
+
}
|
|
3454
|
+
|
|
3455
|
+
// src/commands/restart.ts
|
|
3456
|
+
import pc9 from "picocolors";
|
|
3457
|
+
import { mkdirSync as mkdirSync4, createWriteStream as createWriteStream3 } from "fs";
|
|
3458
|
+
import { resolve as resolve5 } from "path";
|
|
3459
|
+
import { homedir as homedir4 } from "os";
|
|
3460
|
+
import { spawn as spawn3 } from "child_process";
|
|
3129
3461
|
async function restartCommand(args2) {
|
|
3130
3462
|
const name = args2[0];
|
|
3131
3463
|
if (!name) {
|
|
@@ -3150,9 +3482,9 @@ async function restartCommand(args2) {
|
|
|
3150
3482
|
targetPid = processes[0].pid;
|
|
3151
3483
|
}
|
|
3152
3484
|
}
|
|
3153
|
-
const confirmed = await confirmPrompt(`Restart ${
|
|
3485
|
+
const confirmed = await confirmPrompt(`Restart ${pc9.bold(`${name} (PID ${targetPid})`)}?`, false);
|
|
3154
3486
|
if (!confirmed) {
|
|
3155
|
-
console.error(` ${
|
|
3487
|
+
console.error(` ${pc9.dim("Cancelled")}`);
|
|
3156
3488
|
return;
|
|
3157
3489
|
}
|
|
3158
3490
|
const spinner = createSpinner(`Stopping PID ${targetPid}...`);
|
|
@@ -3172,17 +3504,17 @@ async function restartCommand(args2) {
|
|
|
3172
3504
|
const respawnSpinner = createSpinner(`Re-spawning ${trackedEntry.name}...`);
|
|
3173
3505
|
try {
|
|
3174
3506
|
const cmdParts = trackedEntry.command.split(" ");
|
|
3175
|
-
const logDir =
|
|
3176
|
-
|
|
3177
|
-
const logFilePath =
|
|
3178
|
-
const child =
|
|
3507
|
+
const logDir = resolve5(homedir4(), ".fennec", "logs");
|
|
3508
|
+
mkdirSync4(logDir, { recursive: true });
|
|
3509
|
+
const logFilePath = resolve5(logDir, `${trackedEntry.name}.log`);
|
|
3510
|
+
const child = spawn3(cmdParts[0], cmdParts.slice(1), {
|
|
3179
3511
|
cwd: trackedEntry.cwd,
|
|
3180
3512
|
env: { ...process.env },
|
|
3181
3513
|
stdio: ["ignore", "pipe", "pipe"],
|
|
3182
3514
|
detached: true
|
|
3183
3515
|
});
|
|
3184
3516
|
rotateLogFile(logFilePath);
|
|
3185
|
-
const logStream =
|
|
3517
|
+
const logStream = createWriteStream3(logFilePath, { flags: "a" });
|
|
3186
3518
|
if (child.stdout) child.stdout.pipe(logStream);
|
|
3187
3519
|
if (child.stderr) child.stderr.pipe(logStream);
|
|
3188
3520
|
child.unref();
|
|
@@ -3198,35 +3530,41 @@ async function restartCommand(args2) {
|
|
|
3198
3530
|
respawnSpinner.succeed(`${trackedEntry.name} restarted (PID: ${child.pid})`);
|
|
3199
3531
|
} catch (error) {
|
|
3200
3532
|
respawnSpinner.fail(`Failed to re-spawn ${trackedEntry.name}: ${String(error)}`);
|
|
3201
|
-
console.error(` ${
|
|
3533
|
+
console.error(` ${pc9.dim("Config preserved. Re-run manually:")} ${renderCommand(trackedEntry.command)}`);
|
|
3202
3534
|
}
|
|
3203
3535
|
} else {
|
|
3204
|
-
console.error(` ${
|
|
3536
|
+
console.error(` ${pc9.dim("No tracked config found. Re-run manually:")} ${renderCommand(name)}`);
|
|
3205
3537
|
}
|
|
3206
3538
|
}
|
|
3207
3539
|
|
|
3208
3540
|
// src/commands/log.ts
|
|
3209
|
-
import { existsSync as existsSync4, readFileSync as
|
|
3210
|
-
import { resolve as
|
|
3211
|
-
import { homedir as
|
|
3212
|
-
import { spawn as
|
|
3213
|
-
import
|
|
3541
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3, unlinkSync } from "fs";
|
|
3542
|
+
import { resolve as resolve6 } from "path";
|
|
3543
|
+
import { homedir as homedir5 } from "os";
|
|
3544
|
+
import { spawn as spawn4, execSync as execSync2 } from "child_process";
|
|
3545
|
+
import pc10 from "picocolors";
|
|
3214
3546
|
async function logCommand(args2) {
|
|
3215
3547
|
const target = args2[0];
|
|
3216
3548
|
if (!target) {
|
|
3217
|
-
console.error(renderError("Missing process name/pid", "Usage: fennec log <name|pid> [--lines N] [-f]"));
|
|
3549
|
+
console.error(renderError("Missing process name/pid", "Usage: fennec log <name|pid> [--lines N] [-f] [--clear] [--level error|warn|info|debug]"));
|
|
3218
3550
|
process.exit(1);
|
|
3219
3551
|
}
|
|
3552
|
+
const clearFlag = args2.includes("--clear");
|
|
3220
3553
|
const linesIndex = args2.indexOf("--lines");
|
|
3221
3554
|
const lineCount = linesIndex !== -1 ? parseInt(args2[linesIndex + 1], 10) : 30;
|
|
3222
3555
|
const followFlag = args2.includes("-f") || args2.includes("--follow");
|
|
3556
|
+
const levelFilter = args2.includes("--level") ? args2[args2.indexOf("--level") + 1]?.toLowerCase() : void 0;
|
|
3557
|
+
if (levelFilter && !["error", "warn", "info", "debug"].includes(levelFilter)) {
|
|
3558
|
+
console.error(renderError("Invalid level", `"${levelFilter}" is not a valid level. Use: error, warn, info, debug`));
|
|
3559
|
+
process.exit(1);
|
|
3560
|
+
}
|
|
3223
3561
|
let logFilePath = null;
|
|
3224
3562
|
let displayName = target;
|
|
3225
3563
|
const tracked = readTracked();
|
|
3226
3564
|
const trackedMatch = tracked.find((t) => t.name === target || parseInt(target, 10) === t.pid && !isNaN(parseInt(target, 10)));
|
|
3227
3565
|
if (trackedMatch) {
|
|
3228
3566
|
displayName = trackedMatch.name;
|
|
3229
|
-
logFilePath =
|
|
3567
|
+
logFilePath = resolve6(homedir5(), ".fennec", "logs", `${trackedMatch.name}.log`);
|
|
3230
3568
|
} else {
|
|
3231
3569
|
const pid = parseInt(target, 10);
|
|
3232
3570
|
if (!isNaN(pid) && String(pid) === target) {
|
|
@@ -3234,11 +3572,29 @@ async function logCommand(args2) {
|
|
|
3234
3572
|
if (existsSync4(procPath)) logFilePath = procPath;
|
|
3235
3573
|
}
|
|
3236
3574
|
}
|
|
3575
|
+
if (clearFlag) {
|
|
3576
|
+
if (!logFilePath || !existsSync4(logFilePath)) {
|
|
3577
|
+
console.error(`
|
|
3578
|
+
${pc10.yellow("\u26A0")} ${pc10.dim("No log file found for")} ${pc10.bold(displayName)}
|
|
3579
|
+
`);
|
|
3580
|
+
process.exit(0);
|
|
3581
|
+
}
|
|
3582
|
+
try {
|
|
3583
|
+
unlinkSync(logFilePath);
|
|
3584
|
+
console.error(`
|
|
3585
|
+
${pc10.green("\u2713")} ${pc10.bold("Log cleared")} ${pc10.dim(`\u2014 ${logFilePath}`)}
|
|
3586
|
+
`);
|
|
3587
|
+
} catch (err) {
|
|
3588
|
+
console.error(renderError("Failed to clear log", String(err)));
|
|
3589
|
+
process.exit(1);
|
|
3590
|
+
}
|
|
3591
|
+
return;
|
|
3592
|
+
}
|
|
3237
3593
|
const spinner = createSpinner(`Reading logs for ${displayName}...`);
|
|
3238
3594
|
try {
|
|
3239
3595
|
let logLines = [];
|
|
3240
3596
|
if (logFilePath && existsSync4(logFilePath)) {
|
|
3241
|
-
const content =
|
|
3597
|
+
const content = readFileSync3(logFilePath, "utf-8");
|
|
3242
3598
|
logLines = content.split("\n").filter(Boolean);
|
|
3243
3599
|
} else {
|
|
3244
3600
|
const pid = trackedMatch?.pid ?? parseInt(target, 10);
|
|
@@ -3253,27 +3609,55 @@ async function logCommand(args2) {
|
|
|
3253
3609
|
logLines = ["(no logs available)"];
|
|
3254
3610
|
}
|
|
3255
3611
|
}
|
|
3612
|
+
if (levelFilter) {
|
|
3613
|
+
const levelRegex = new RegExp(`\\b(${levelFilter.toUpperCase()})\\b`, "i");
|
|
3614
|
+
logLines = logLines.filter((line) => levelRegex.test(line));
|
|
3615
|
+
}
|
|
3256
3616
|
spinner.stop();
|
|
3257
3617
|
process.stdout.write("\r\x1B[K");
|
|
3258
3618
|
console.error(`
|
|
3259
|
-
${symbols.fox} ${
|
|
3260
|
-
`);
|
|
3619
|
+
${symbols.fox} ${pc10.bold("Logs")} ${renderAppName(displayName)} ${pc10.dim(`(last ${logLines.length} line${logLines.length !== 1 ? "s" : ""})`)}`);
|
|
3261
3620
|
const sliced = logLines.slice(-lineCount);
|
|
3262
3621
|
for (const line of sliced) {
|
|
3263
3622
|
const display = line.length > 300 ? line.slice(0, 300) + "\u2026" : line;
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
else
|
|
3623
|
+
const prefix = followFlag ? `${timestamp()} ` : "";
|
|
3624
|
+
if (line.toLowerCase().includes("error") || line.toLowerCase().includes("fail") || line.toLowerCase().includes("fatal")) {
|
|
3625
|
+
console.error(` ${pc10.red(prefix + display)}`);
|
|
3626
|
+
} else if (line.toLowerCase().includes("warn")) {
|
|
3627
|
+
console.error(` ${pc10.yellow(prefix + display)}`);
|
|
3628
|
+
} else if (line.toLowerCase().includes("info") || line.includes("[")) {
|
|
3629
|
+
console.error(` ${pc10.cyan(prefix + display)}`);
|
|
3630
|
+
} else {
|
|
3631
|
+
console.error(` ${prefix}${display}`);
|
|
3632
|
+
}
|
|
3268
3633
|
}
|
|
3269
3634
|
if (followFlag && logFilePath) {
|
|
3270
|
-
|
|
3271
|
-
|
|
3635
|
+
const tailArgs = ["-n", "0", "-f"];
|
|
3636
|
+
if (levelFilter) {
|
|
3637
|
+
const tail = spawn4("tail", ["-n", "0", "-f", logFilePath], { stdio: ["ignore", "pipe", "pipe"] });
|
|
3638
|
+
const grep = spawn4("grep", ["--line-buffered", "-i", levelFilter], { stdio: ["pipe", "inherit", "inherit"] });
|
|
3639
|
+
if (tail.stdout) tail.stdout.pipe(grep.stdin);
|
|
3640
|
+
await new Promise((resolve12) => {
|
|
3641
|
+
tail.on("exit", () => resolve12());
|
|
3642
|
+
process.once("SIGINT", () => {
|
|
3643
|
+
tail.kill("SIGTERM");
|
|
3644
|
+
grep.kill("SIGTERM");
|
|
3645
|
+
resolve12();
|
|
3646
|
+
});
|
|
3647
|
+
});
|
|
3648
|
+
} else {
|
|
3649
|
+
console.error(`
|
|
3650
|
+
${pc10.dim("Following... (Ctrl+C to stop)")}
|
|
3272
3651
|
`);
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3652
|
+
const tail = spawn4("tail", tailArgs.concat([logFilePath]), { stdio: "inherit" });
|
|
3653
|
+
await new Promise((resolve12) => {
|
|
3654
|
+
tail.on("exit", () => resolve12());
|
|
3655
|
+
process.once("SIGINT", () => {
|
|
3656
|
+
tail.kill("SIGTERM");
|
|
3657
|
+
resolve12();
|
|
3658
|
+
});
|
|
3659
|
+
});
|
|
3660
|
+
}
|
|
3277
3661
|
}
|
|
3278
3662
|
console.error();
|
|
3279
3663
|
} catch (error) {
|
|
@@ -3283,7 +3667,8 @@ async function logCommand(args2) {
|
|
|
3283
3667
|
}
|
|
3284
3668
|
|
|
3285
3669
|
// src/commands/attach.ts
|
|
3286
|
-
import
|
|
3670
|
+
import { PortDetector as PortDetector3 } from "@plumpslabs/fennec-core";
|
|
3671
|
+
import pc11 from "picocolors";
|
|
3287
3672
|
async function attachCommand(args2) {
|
|
3288
3673
|
const port = parseInt(args2[0], 10);
|
|
3289
3674
|
if (isNaN(port)) {
|
|
@@ -3295,14 +3680,13 @@ async function attachCommand(args2) {
|
|
|
3295
3680
|
const name = rawName ?? `port-${port}`;
|
|
3296
3681
|
const spinner = createSpinner(`Attaching to :${port}...`);
|
|
3297
3682
|
try {
|
|
3298
|
-
const
|
|
3299
|
-
const detector = new PortDetector();
|
|
3683
|
+
const detector = new PortDetector3();
|
|
3300
3684
|
const info = detector.detectByPort(port);
|
|
3301
3685
|
if (info) {
|
|
3302
3686
|
spinner.succeed(`Attached to :${port}`);
|
|
3303
3687
|
console.error(` ${renderKV("Name", renderAppName(name))}`);
|
|
3304
3688
|
console.error(` ${renderKV("PID", String(info.pid))}`);
|
|
3305
|
-
console.error(` ${renderKV("Command", info.command ||
|
|
3689
|
+
console.error(` ${renderKV("Command", info.command || pc11.dim("unknown"))}`);
|
|
3306
3690
|
} else {
|
|
3307
3691
|
spinner.fail(`No process found on port ${port}`);
|
|
3308
3692
|
process.exit(1);
|
|
@@ -3316,35 +3700,35 @@ async function attachCommand(args2) {
|
|
|
3316
3700
|
|
|
3317
3701
|
// src/commands/sessions.ts
|
|
3318
3702
|
import { SessionStore } from "@plumpslabs/fennec-core";
|
|
3319
|
-
import
|
|
3703
|
+
import pc12 from "picocolors";
|
|
3320
3704
|
async function sessionsCommand() {
|
|
3321
3705
|
const store = new SessionStore("./.fennec/sessions");
|
|
3322
3706
|
const sessions = store.list();
|
|
3323
3707
|
if (sessions.length === 0) {
|
|
3324
3708
|
console.error(`
|
|
3325
|
-
${
|
|
3709
|
+
${pc12.dim("No saved sessions found.")}
|
|
3326
3710
|
`);
|
|
3327
3711
|
return;
|
|
3328
3712
|
}
|
|
3329
3713
|
const columns = [
|
|
3330
|
-
{ key: "name", label: "Name", format: (v) =>
|
|
3714
|
+
{ key: "name", label: "Name", format: (v) => pc12.bold(String(v)) },
|
|
3331
3715
|
{ key: "origin", label: "Origin" },
|
|
3332
|
-
{ key: "savedAt", label: "Saved", format: (v) =>
|
|
3716
|
+
{ key: "savedAt", label: "Saved", format: (v) => pc12.dim(String(v)) }
|
|
3333
3717
|
];
|
|
3334
3718
|
const rows = sessions.map((s) => ({ name: s.name, origin: s.origin, savedAt: new Date(s.savedAt).toLocaleString() }));
|
|
3335
3719
|
console.error(`
|
|
3336
|
-
${symbols.fox} ${
|
|
3720
|
+
${symbols.fox} ${pc12.bold("Saved Sessions")}
|
|
3337
3721
|
`);
|
|
3338
3722
|
console.error(renderTable(columns, rows));
|
|
3339
|
-
console.error(` ${
|
|
3723
|
+
console.error(` ${pc12.dim(`${sessions.length} session(s)`)}
|
|
3340
3724
|
`);
|
|
3341
3725
|
}
|
|
3342
3726
|
|
|
3343
3727
|
// src/commands/setup.ts
|
|
3344
|
-
import
|
|
3728
|
+
import pc13 from "picocolors";
|
|
3345
3729
|
async function setupCommand() {
|
|
3346
3730
|
console.error(`
|
|
3347
|
-
${symbols.fox} ${
|
|
3731
|
+
${symbols.fox} ${pc13.bold("Fennec Setup")}
|
|
3348
3732
|
`);
|
|
3349
3733
|
const mcpClient = await selectPrompt("Which MCP client are you using?", [
|
|
3350
3734
|
{ value: "claude", label: "Claude Desktop", description: "Anthropic's AI desktop app" },
|
|
@@ -3353,12 +3737,12 @@ async function setupCommand() {
|
|
|
3353
3737
|
{ value: "other", label: "Other MCP client", description: "Any MCP-compatible client" }
|
|
3354
3738
|
]);
|
|
3355
3739
|
if (!mcpClient) {
|
|
3356
|
-
console.error(` ${
|
|
3740
|
+
console.error(` ${pc13.dim("Setup cancelled.")}
|
|
3357
3741
|
`);
|
|
3358
3742
|
return;
|
|
3359
3743
|
}
|
|
3360
3744
|
console.error(`
|
|
3361
|
-
${
|
|
3745
|
+
${pc13.green("\u2713")} Selected: ${pc13.bold(mcpClient)}
|
|
3362
3746
|
`);
|
|
3363
3747
|
const configSnippet = `{
|
|
3364
3748
|
"mcpServers": {
|
|
@@ -3368,24 +3752,24 @@ async function setupCommand() {
|
|
|
3368
3752
|
}
|
|
3369
3753
|
}
|
|
3370
3754
|
}`;
|
|
3371
|
-
console.error(` ${
|
|
3755
|
+
console.error(` ${pc13.bold("Add this to your MCP client config:")}
|
|
3372
3756
|
`);
|
|
3373
|
-
console.error(` ${
|
|
3757
|
+
console.error(` ${pc13.dim("```")}`);
|
|
3374
3758
|
console.error(configSnippet.split("\n").map((l) => ` ${l}`).join("\n"));
|
|
3375
|
-
console.error(` ${
|
|
3759
|
+
console.error(` ${pc13.dim("```")}
|
|
3376
3760
|
`);
|
|
3377
|
-
console.error(` ${renderSuccess("Setup complete!")} ${
|
|
3761
|
+
console.error(` ${renderSuccess("Setup complete!")} ${pc13.dim("Run")} ${renderCommand("fennec start")} ${pc13.dim("to begin.")}
|
|
3378
3762
|
`);
|
|
3379
3763
|
}
|
|
3380
3764
|
|
|
3381
3765
|
// src/commands/management.ts
|
|
3382
3766
|
import { existsSync as existsSync5, writeFileSync as writeFileSync2 } from "fs";
|
|
3383
|
-
import { resolve as
|
|
3767
|
+
import { resolve as resolve7 } from "path";
|
|
3384
3768
|
import { execSync as execSync3 } from "child_process";
|
|
3385
|
-
import
|
|
3769
|
+
import pc14 from "picocolors";
|
|
3386
3770
|
async function installBrowsersCommand() {
|
|
3387
3771
|
console.error(`
|
|
3388
|
-
${
|
|
3772
|
+
${pc14.bold("Installing Browser Engines")}
|
|
3389
3773
|
`);
|
|
3390
3774
|
const spinner = createSpinner("Installing Chromium...");
|
|
3391
3775
|
try {
|
|
@@ -3393,21 +3777,21 @@ async function installBrowsersCommand() {
|
|
|
3393
3777
|
spinner.succeed("Chromium installed successfully");
|
|
3394
3778
|
} catch {
|
|
3395
3779
|
spinner.fail("Failed to install Chromium");
|
|
3396
|
-
console.error(` ${
|
|
3780
|
+
console.error(` ${pc14.yellow("\u2192")} Try running: ${renderCommand("npx playwright install chromium")}`);
|
|
3397
3781
|
}
|
|
3398
3782
|
console.error(`
|
|
3399
|
-
${
|
|
3783
|
+
${pc14.green("\u2713")} Browser installation complete.
|
|
3400
3784
|
`);
|
|
3401
3785
|
}
|
|
3402
3786
|
async function initCommand() {
|
|
3403
3787
|
console.error(`
|
|
3404
|
-
${
|
|
3788
|
+
${pc14.bold("Initialize Fennec Configuration")}
|
|
3405
3789
|
`);
|
|
3406
|
-
const configFile =
|
|
3790
|
+
const configFile = resolve7("./fennec.config.yaml");
|
|
3407
3791
|
if (existsSync5(configFile)) {
|
|
3408
|
-
const overwrite = await confirmPrompt(`${
|
|
3792
|
+
const overwrite = await confirmPrompt(`${pc14.yellow("fennec.config.yaml")} already exists. Overwrite?`, false);
|
|
3409
3793
|
if (!overwrite) {
|
|
3410
|
-
console.error(` ${
|
|
3794
|
+
console.error(` ${pc14.dim("Cancelled.")}
|
|
3411
3795
|
`);
|
|
3412
3796
|
return;
|
|
3413
3797
|
}
|
|
@@ -3487,21 +3871,21 @@ logging:
|
|
|
3487
3871
|
file: null
|
|
3488
3872
|
`;
|
|
3489
3873
|
writeFileSync2(configFile, config, "utf-8");
|
|
3490
|
-
spinner.succeed(`Configuration written to ${
|
|
3874
|
+
spinner.succeed(`Configuration written to ${pc14.bold(configFile)}`);
|
|
3491
3875
|
console.error(`
|
|
3492
|
-
${
|
|
3876
|
+
${pc14.dim("Edit the file to customize Fennec behavior.")}
|
|
3493
3877
|
`);
|
|
3494
3878
|
}
|
|
3495
3879
|
|
|
3496
3880
|
// src/commands/health.ts
|
|
3497
3881
|
import { existsSync as existsSync6, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
|
|
3498
3882
|
import { execSync as execSync4 } from "child_process";
|
|
3499
|
-
import { resolve as
|
|
3500
|
-
import { homedir as
|
|
3501
|
-
import
|
|
3883
|
+
import { resolve as resolve8 } from "path";
|
|
3884
|
+
import { homedir as homedir6 } from "os";
|
|
3885
|
+
import pc15 from "picocolors";
|
|
3502
3886
|
async function healthCommand() {
|
|
3503
3887
|
printBanner();
|
|
3504
|
-
console.error(` ${
|
|
3888
|
+
console.error(` ${pc15.bold("Fennec Health Check")}
|
|
3505
3889
|
`);
|
|
3506
3890
|
let adbStatus = "unknown";
|
|
3507
3891
|
try {
|
|
@@ -3511,20 +3895,20 @@ async function healthCommand() {
|
|
|
3511
3895
|
stdio: ["ignore", "pipe", "pipe"]
|
|
3512
3896
|
});
|
|
3513
3897
|
const version = result.split("\n")[0]?.trim() ?? "installed";
|
|
3514
|
-
adbStatus =
|
|
3898
|
+
adbStatus = pc15.green(version);
|
|
3515
3899
|
} catch {
|
|
3516
|
-
adbStatus =
|
|
3900
|
+
adbStatus = pc15.dim("not found") + " (optional)";
|
|
3517
3901
|
}
|
|
3518
3902
|
const tracked = readTracked();
|
|
3519
3903
|
const runningCount = tracked.filter((t) => isProcessRunning(t.pid)).length;
|
|
3520
|
-
const logDir =
|
|
3904
|
+
const logDir = resolve8(homedir6(), ".fennec", "logs");
|
|
3521
3905
|
let logSize = "0 B";
|
|
3522
3906
|
try {
|
|
3523
3907
|
if (existsSync6(logDir)) {
|
|
3524
3908
|
const files = readdirSync2(logDir);
|
|
3525
3909
|
let totalBytes = 0;
|
|
3526
3910
|
for (const f of files) {
|
|
3527
|
-
const fStat = statSync2(
|
|
3911
|
+
const fStat = statSync2(resolve8(logDir, f));
|
|
3528
3912
|
totalBytes += fStat.size;
|
|
3529
3913
|
}
|
|
3530
3914
|
logSize = totalBytes > 1024 * 1024 ? `${(totalBytes / (1024 * 1024)).toFixed(1)} MB` : `${(totalBytes / 1024).toFixed(1)} KB`;
|
|
@@ -3537,7 +3921,7 @@ async function healthCommand() {
|
|
|
3537
3921
|
memoryInfo = `${(mem.rss / (1024 * 1024)).toFixed(0)} MB RSS / ${(mem.heapUsed / (1024 * 1024)).toFixed(0)} MB heap`;
|
|
3538
3922
|
} catch {
|
|
3539
3923
|
}
|
|
3540
|
-
console.error(` ${symbols.fox} ${
|
|
3924
|
+
console.error(` ${symbols.fox} ${pc15.bold("System")}
|
|
3541
3925
|
`);
|
|
3542
3926
|
console.error(` ${renderKV("Node.js", process.version)}`);
|
|
3543
3927
|
console.error(` ${renderKV("Platform", `${process.platform} ${process.arch}`)}`);
|
|
@@ -3545,7 +3929,7 @@ async function healthCommand() {
|
|
|
3545
3929
|
console.error(` ${renderKV("ADB", adbStatus)}`);
|
|
3546
3930
|
console.error(` ${renderKV("PID", String(process.pid))}`);
|
|
3547
3931
|
console.error();
|
|
3548
|
-
console.error(` ${symbols.fox} ${
|
|
3932
|
+
console.error(` ${symbols.fox} ${pc15.bold("Processes")}
|
|
3549
3933
|
`);
|
|
3550
3934
|
console.error(` ${renderKV("Tracked", String(tracked.length))}`);
|
|
3551
3935
|
console.error(` ${renderKV("Running", String(runningCount))}`);
|
|
@@ -3553,15 +3937,238 @@ async function healthCommand() {
|
|
|
3553
3937
|
console.error();
|
|
3554
3938
|
const allHealthy = runningCount === tracked.length || tracked.length === 0;
|
|
3555
3939
|
if (allHealthy) {
|
|
3556
|
-
console.error(` ${
|
|
3940
|
+
console.error(` ${pc15.green("\u2713")} ${pc15.bold("All systems healthy")}
|
|
3557
3941
|
`);
|
|
3558
3942
|
} else {
|
|
3559
3943
|
const stopped = tracked.length - runningCount;
|
|
3560
|
-
console.error(` ${
|
|
3944
|
+
console.error(` ${pc15.yellow("\u26A0")} ${pc15.bold(`${stopped} process(es) stopped`)} ${pc15.dim("fennec ps")}
|
|
3561
3945
|
`);
|
|
3562
3946
|
}
|
|
3563
3947
|
}
|
|
3564
3948
|
|
|
3949
|
+
// src/commands/cleanup.ts
|
|
3950
|
+
import pc16 from "picocolors";
|
|
3951
|
+
async function cleanupCommand() {
|
|
3952
|
+
const tracked = readTracked();
|
|
3953
|
+
if (tracked.length === 0) {
|
|
3954
|
+
console.error(`
|
|
3955
|
+
${pc16.dim("No tracked processes to clean up.")}
|
|
3956
|
+
`);
|
|
3957
|
+
return;
|
|
3958
|
+
}
|
|
3959
|
+
const toRemove = tracked.filter((t) => !isProcessRunning(t.pid) && !t.command);
|
|
3960
|
+
if (toRemove.length === 0) {
|
|
3961
|
+
console.error(`
|
|
3962
|
+
${pc16.green("\u2713")} ${pc16.bold("All clean")} ${pc16.dim("\u2014 no dead entries without saved commands.")}
|
|
3963
|
+
`);
|
|
3964
|
+
return;
|
|
3965
|
+
}
|
|
3966
|
+
console.error(`
|
|
3967
|
+
${pc16.yellow("\u26A0")} ${pc16.bold(`Found ${toRemove.length} dead entr${toRemove.length > 1 ? "ies" : "y"} without saved commands`)}
|
|
3968
|
+
`);
|
|
3969
|
+
for (const entry of toRemove) {
|
|
3970
|
+
console.error(` ${pc16.dim("\xB7")} ${pc16.bold(entry.name)} ${pc16.dim(`(PID ${entry.pid})`)}`);
|
|
3971
|
+
}
|
|
3972
|
+
console.error();
|
|
3973
|
+
const confirmed = await confirmPrompt(`Remove ${toRemove.length} entr${toRemove.length > 1 ? "ies" : "y"}?`, false);
|
|
3974
|
+
if (!confirmed) {
|
|
3975
|
+
console.error(` ${pc16.dim("Cancelled")}
|
|
3976
|
+
`);
|
|
3977
|
+
return;
|
|
3978
|
+
}
|
|
3979
|
+
const remaining = tracked.filter((t) => !toRemove.includes(t));
|
|
3980
|
+
saveTracked(remaining);
|
|
3981
|
+
console.error(`
|
|
3982
|
+
${pc16.green("\u2713")} ${pc16.bold(`Removed ${toRemove.length} entr${toRemove.length > 1 ? "ies" : "y"}`)} ${pc16.dim(`(${remaining.length} remaining)`)}
|
|
3983
|
+
`);
|
|
3984
|
+
}
|
|
3985
|
+
|
|
3986
|
+
// src/commands/info.ts
|
|
3987
|
+
import pc17 from "picocolors";
|
|
3988
|
+
import { resolve as resolve9 } from "path";
|
|
3989
|
+
import { homedir as homedir7 } from "os";
|
|
3990
|
+
async function infoCommand(args2) {
|
|
3991
|
+
const name = args2[0];
|
|
3992
|
+
if (!name) {
|
|
3993
|
+
console.error(renderError("Missing name", "Usage: fennec info <name>"));
|
|
3994
|
+
process.exit(1);
|
|
3995
|
+
}
|
|
3996
|
+
const tracked = readTracked();
|
|
3997
|
+
const match = tracked.find((t) => t.name === name);
|
|
3998
|
+
if (!match) {
|
|
3999
|
+
console.error(renderError("Not found", `No tracked process named "${name}".`));
|
|
4000
|
+
process.exit(1);
|
|
4001
|
+
}
|
|
4002
|
+
const running = isProcessRunning(match.pid);
|
|
4003
|
+
const statusIcon = running ? pc17.green("\u25CF") : pc17.red("\u25CB");
|
|
4004
|
+
const statusText = running ? pc17.green("running") : pc17.red("stopped");
|
|
4005
|
+
const uptime = running ? formatUptime(Math.floor((Date.now() - new Date(match.startedAt).getTime()) / 1e3)) : "-";
|
|
4006
|
+
const logPath = resolve9(homedir7(), ".fennec", "logs", `${match.name}.log`);
|
|
4007
|
+
console.error(`
|
|
4008
|
+
${symbols.fox} ${pc17.bold(match.name)} ${pc17.dim("\u2014 Process Info")}
|
|
4009
|
+
`);
|
|
4010
|
+
console.error(` ${renderKVColor("Name", match.name, pc17.bold)}`);
|
|
4011
|
+
console.error(` ${renderKVColor("Status", `${statusIcon} ${statusText}`)}`);
|
|
4012
|
+
console.error(` ${renderKVColor("PID", String(match.pid))}`);
|
|
4013
|
+
console.error(` ${renderKVColor("Port", match.port ? String(match.port) : "-")}`);
|
|
4014
|
+
console.error(` ${renderKVColor("Command", match.command || pc17.dim("(none)"))}`);
|
|
4015
|
+
if (match.cwd) console.error(` ${renderKVColor("CWD", match.cwd)}`);
|
|
4016
|
+
console.error(` ${renderKVColor("Started", new Date(match.startedAt).toLocaleString())}`);
|
|
4017
|
+
console.error(` ${renderKVColor("Uptime", uptime)}`);
|
|
4018
|
+
console.error(` ${renderKVColor("Log Path", logPath)}`);
|
|
4019
|
+
if (!running && match.command) {
|
|
4020
|
+
console.error(`
|
|
4021
|
+
${pc17.dim("Re-spawn with:")} ${pc17.cyan(`fennec spawn ${match.name}`)}`);
|
|
4022
|
+
}
|
|
4023
|
+
console.error();
|
|
4024
|
+
}
|
|
4025
|
+
|
|
4026
|
+
// src/commands/rename.ts
|
|
4027
|
+
import pc18 from "picocolors";
|
|
4028
|
+
import { existsSync as existsSync7, renameSync as renameSync2 } from "fs";
|
|
4029
|
+
import { resolve as resolve10 } from "path";
|
|
4030
|
+
import { homedir as homedir8 } from "os";
|
|
4031
|
+
async function renameCommand(args2) {
|
|
4032
|
+
const [oldName, newName] = args2;
|
|
4033
|
+
if (!oldName || !newName) {
|
|
4034
|
+
console.error(renderError("Missing arguments", "Usage: fennec rename <old-name> <new-name>"));
|
|
4035
|
+
process.exit(1);
|
|
4036
|
+
}
|
|
4037
|
+
if (oldName === newName) {
|
|
4038
|
+
console.error(`
|
|
4039
|
+
${pc18.yellow("\u26A0")} ${pc18.dim("Old and new names are the same.")}
|
|
4040
|
+
`);
|
|
4041
|
+
process.exit(0);
|
|
4042
|
+
}
|
|
4043
|
+
const tracked = readTracked();
|
|
4044
|
+
const match = tracked.find((t) => t.name === oldName);
|
|
4045
|
+
if (!match) {
|
|
4046
|
+
console.error(renderError("Not found", `No tracked process named "${oldName}".`));
|
|
4047
|
+
process.exit(1);
|
|
4048
|
+
}
|
|
4049
|
+
if (tracked.some((t) => t.name === newName)) {
|
|
4050
|
+
console.error(renderError("Name taken", `A process named "${newName}" already exists.`));
|
|
4051
|
+
process.exit(1);
|
|
4052
|
+
}
|
|
4053
|
+
console.error(`
|
|
4054
|
+
${symbols.fox} ${pc18.bold("Rename Process")}
|
|
4055
|
+
`);
|
|
4056
|
+
console.error(` ${renderKVColor("From", oldName)}`);
|
|
4057
|
+
console.error(` ${renderKVColor("To", newName)}`);
|
|
4058
|
+
if (isProcessRunning(match.pid)) {
|
|
4059
|
+
console.error(` ${pc18.yellow("\u26A0")} ${pc18.dim("Process is still running \u2014 log entries will go to the old file until stopped.")}`);
|
|
4060
|
+
}
|
|
4061
|
+
console.error();
|
|
4062
|
+
const confirmed = await confirmPrompt(`Rename ${pc18.bold(oldName)} ${pc18.dim("\u2192")} ${pc18.bold(newName)}?`, true);
|
|
4063
|
+
if (!confirmed) {
|
|
4064
|
+
console.error(` ${pc18.dim("Cancelled")}
|
|
4065
|
+
`);
|
|
4066
|
+
return;
|
|
4067
|
+
}
|
|
4068
|
+
const spinner = createSpinner(`Renaming ${oldName} \u2192 ${newName}...`);
|
|
4069
|
+
const logDir = resolve10(homedir8(), ".fennec", "logs");
|
|
4070
|
+
const oldLog = resolve10(logDir, `${oldName}.log`);
|
|
4071
|
+
const newLog = resolve10(logDir, `${newName}.log`);
|
|
4072
|
+
if (existsSync7(oldLog) && !existsSync7(newLog)) {
|
|
4073
|
+
try {
|
|
4074
|
+
renameSync2(oldLog, newLog);
|
|
4075
|
+
} catch {
|
|
4076
|
+
}
|
|
4077
|
+
}
|
|
4078
|
+
match.name = newName;
|
|
4079
|
+
saveTracked(tracked);
|
|
4080
|
+
spinner.succeed(`${pc18.bold(oldName)} ${pc18.dim("\u2192")} ${pc18.bold(newName)}`);
|
|
4081
|
+
console.error();
|
|
4082
|
+
}
|
|
4083
|
+
|
|
4084
|
+
// src/commands/export-import.ts
|
|
4085
|
+
import pc19 from "picocolors";
|
|
4086
|
+
import { existsSync as existsSync8, writeFileSync as writeFileSync3, readFileSync as readFileSync4 } from "fs";
|
|
4087
|
+
import { resolve as resolve11 } from "path";
|
|
4088
|
+
async function exportCommand(args2) {
|
|
4089
|
+
const tracked = readTracked();
|
|
4090
|
+
const fileIndex = args2.indexOf("--file");
|
|
4091
|
+
const filePath = fileIndex !== -1 ? args2[fileIndex + 1] : void 0;
|
|
4092
|
+
if (fileIndex !== -1 && !filePath) {
|
|
4093
|
+
console.error(renderError("Missing file path", "Usage: fennec export --file <path>"));
|
|
4094
|
+
process.exit(1);
|
|
4095
|
+
}
|
|
4096
|
+
if (tracked.length === 0) {
|
|
4097
|
+
console.error(`
|
|
4098
|
+
${pc19.dim("No tracked processes to export.")}
|
|
4099
|
+
`);
|
|
4100
|
+
return;
|
|
4101
|
+
}
|
|
4102
|
+
const json = JSON.stringify(tracked, null, 2);
|
|
4103
|
+
if (filePath) {
|
|
4104
|
+
try {
|
|
4105
|
+
writeFileSync3(resolve11(filePath), json, "utf-8");
|
|
4106
|
+
console.error(`
|
|
4107
|
+
${pc19.green("\u2713")} ${pc19.bold(`Exported ${tracked.length} process(es)`)} ${pc19.dim(`\u2192 ${filePath}`)}
|
|
4108
|
+
`);
|
|
4109
|
+
} catch (err) {
|
|
4110
|
+
console.error(renderError("Export failed", String(err)));
|
|
4111
|
+
process.exit(1);
|
|
4112
|
+
}
|
|
4113
|
+
} else {
|
|
4114
|
+
console.log(json);
|
|
4115
|
+
}
|
|
4116
|
+
}
|
|
4117
|
+
async function importCommand(args2) {
|
|
4118
|
+
const filePath = args2[0];
|
|
4119
|
+
if (!filePath) {
|
|
4120
|
+
console.error(renderError("Missing file", "Usage: fennec import <file>"));
|
|
4121
|
+
process.exit(1);
|
|
4122
|
+
}
|
|
4123
|
+
const resolvedPath = resolve11(filePath);
|
|
4124
|
+
if (!existsSync8(resolvedPath)) {
|
|
4125
|
+
console.error(renderError("File not found", `"${filePath}" does not exist.`));
|
|
4126
|
+
process.exit(1);
|
|
4127
|
+
}
|
|
4128
|
+
let imported;
|
|
4129
|
+
try {
|
|
4130
|
+
imported = JSON.parse(readFileSync4(resolvedPath, "utf-8"));
|
|
4131
|
+
if (!Array.isArray(imported) || imported.length === 0) {
|
|
4132
|
+
console.error(renderError("Invalid file", "File must contain a non-empty JSON array of process objects."));
|
|
4133
|
+
process.exit(1);
|
|
4134
|
+
}
|
|
4135
|
+
} catch (err) {
|
|
4136
|
+
console.error(renderError("Invalid JSON", String(err)));
|
|
4137
|
+
process.exit(1);
|
|
4138
|
+
}
|
|
4139
|
+
const existing = readTracked();
|
|
4140
|
+
console.error(`
|
|
4141
|
+
${symbols.fox} ${pc19.bold("Import Processes")}
|
|
4142
|
+
`);
|
|
4143
|
+
console.error(` ${renderKVColor("File", resolvedPath)}`);
|
|
4144
|
+
console.error(` ${renderKVColor("Importing", `${imported.length} process(es)`)}`);
|
|
4145
|
+
console.error(` ${renderKVColor("Existing", `${existing.length} process(es)`)}`);
|
|
4146
|
+
console.error(`
|
|
4147
|
+
${pc19.bold("Processes to import:")}`);
|
|
4148
|
+
for (const p of imported) {
|
|
4149
|
+
console.error(` ${pc19.green("+")} ${pc19.bold(p.name)} ${pc19.dim(`(${p.command})`)}`);
|
|
4150
|
+
}
|
|
4151
|
+
console.error();
|
|
4152
|
+
const confirmed = await confirmPrompt("Merge into tracked.json? Existing processes with the same name will be overwritten.", true);
|
|
4153
|
+
if (!confirmed) {
|
|
4154
|
+
console.error(` ${pc19.dim("Cancelled")}
|
|
4155
|
+
`);
|
|
4156
|
+
return;
|
|
4157
|
+
}
|
|
4158
|
+
const merged = [...existing];
|
|
4159
|
+
for (const proc of imported) {
|
|
4160
|
+
const idx = merged.findIndex((t) => t.name === proc.name);
|
|
4161
|
+
if (idx !== -1) {
|
|
4162
|
+
merged[idx] = { ...merged[idx], ...proc };
|
|
4163
|
+
} else {
|
|
4164
|
+
merged.push(proc);
|
|
4165
|
+
}
|
|
4166
|
+
}
|
|
4167
|
+
saveTracked(merged);
|
|
4168
|
+
console.error(` ${pc19.green("\u2713")} ${pc19.bold(`Imported ${imported.length} process(es)`)} ${pc19.dim(`(${merged.length} total)`)}
|
|
4169
|
+
`);
|
|
4170
|
+
}
|
|
4171
|
+
|
|
3565
4172
|
// src/index.ts
|
|
3566
4173
|
var [, , command, ...args] = process.argv;
|
|
3567
4174
|
async function main() {
|
|
@@ -3581,10 +4188,26 @@ async function main() {
|
|
|
3581
4188
|
await statusCommand(args);
|
|
3582
4189
|
} else if (command === "log") {
|
|
3583
4190
|
await logCommand(args);
|
|
4191
|
+
} else if (command === "spawn") {
|
|
4192
|
+
await spawnCommand(args);
|
|
4193
|
+
} else if (command === "stop") {
|
|
4194
|
+
await stopCommand(args);
|
|
3584
4195
|
} else if (command === "kill") {
|
|
3585
4196
|
await killCommand(args);
|
|
3586
4197
|
} else if (command === "restart") {
|
|
3587
4198
|
await restartCommand(args);
|
|
4199
|
+
} else if (command === "info") {
|
|
4200
|
+
printBanner();
|
|
4201
|
+
await infoCommand(args);
|
|
4202
|
+
} else if (command === "cleanup") {
|
|
4203
|
+
printBanner();
|
|
4204
|
+
await cleanupCommand();
|
|
4205
|
+
} else if (command === "rename") {
|
|
4206
|
+
await renameCommand(args);
|
|
4207
|
+
} else if (command === "export") {
|
|
4208
|
+
await exportCommand(args);
|
|
4209
|
+
} else if (command === "import") {
|
|
4210
|
+
await importCommand(args);
|
|
3588
4211
|
} else if (command === "attach") {
|
|
3589
4212
|
await attachCommand(args);
|
|
3590
4213
|
} else if (command === "pipe") {
|