@plumpslabs/fennec-cli 1.14.3 → 1.14.5
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/README.md +79 -78
- package/dist/index.js +894 -298
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1922,7 +1922,7 @@ function renderTable(columns, rows, options) {
|
|
|
1922
1922
|
"top-mid": "\u252C",
|
|
1923
1923
|
"top-left": "\u250C",
|
|
1924
1924
|
"top-right": "\u2510",
|
|
1925
|
-
|
|
1925
|
+
bottom: "\u2500",
|
|
1926
1926
|
"bottom-mid": "\u2534",
|
|
1927
1927
|
"bottom-left": "\u2514",
|
|
1928
1928
|
"bottom-right": "\u2518",
|
|
@@ -2099,7 +2099,7 @@ function hexColor(color) {
|
|
|
2099
2099
|
}
|
|
2100
2100
|
var fennecOrange2 = hexColor("#FF6432");
|
|
2101
2101
|
var fennecGold = hexColor("#FFB347");
|
|
2102
|
-
var VERSION = "1.14.
|
|
2102
|
+
var VERSION = "1.14.5";
|
|
2103
2103
|
var cachedBanner = null;
|
|
2104
2104
|
var cachedCompact = null;
|
|
2105
2105
|
function generateBanner() {
|
|
@@ -2166,7 +2166,10 @@ var COMMANDS = {
|
|
|
2166
2166
|
["--name <name>", "App name used for tracking (recommended)"],
|
|
2167
2167
|
["--port <port>", "Port the app listens on \u2014 Fennec waits until it accepts connections"],
|
|
2168
2168
|
["--cwd <dir>", "Working directory to run the command in"],
|
|
2169
|
-
[
|
|
2169
|
+
[
|
|
2170
|
+
"--restart",
|
|
2171
|
+
"Auto-restart if it crashes or its port stops listening (detached supervisor, survives terminal close)"
|
|
2172
|
+
],
|
|
2170
2173
|
["--group <group>", "Logical group/namespace for bulk ops (kill/spawn/stop --group <group>)"]
|
|
2171
2174
|
],
|
|
2172
2175
|
examples: [
|
|
@@ -2268,7 +2271,13 @@ var COMMANDS = {
|
|
|
2268
2271
|
["--group <g>, -g <g>", "Kill only apps in group <g> (other groups untouched)"],
|
|
2269
2272
|
["-y, --yes", "Skip the confirmation prompt"]
|
|
2270
2273
|
],
|
|
2271
|
-
examples: [
|
|
2274
|
+
examples: [
|
|
2275
|
+
"kill web",
|
|
2276
|
+
"kill 12345 --signal SIGKILL",
|
|
2277
|
+
"kill be-crm fe-crm -y",
|
|
2278
|
+
"kill all -y",
|
|
2279
|
+
"kill --group backend -y"
|
|
2280
|
+
]
|
|
2272
2281
|
},
|
|
2273
2282
|
supervisor: {
|
|
2274
2283
|
name: "supervisor",
|
|
@@ -2462,7 +2471,27 @@ function findCommandDoc(cmd) {
|
|
|
2462
2471
|
}
|
|
2463
2472
|
var GROUPS = [
|
|
2464
2473
|
{ title: "Server", keys: ["start-server"] },
|
|
2465
|
-
{
|
|
2474
|
+
{
|
|
2475
|
+
title: "Apps & Processes",
|
|
2476
|
+
keys: [
|
|
2477
|
+
"start",
|
|
2478
|
+
"run",
|
|
2479
|
+
"ps",
|
|
2480
|
+
"status",
|
|
2481
|
+
"log",
|
|
2482
|
+
"spawn",
|
|
2483
|
+
"stop",
|
|
2484
|
+
"restart",
|
|
2485
|
+
"kill",
|
|
2486
|
+
"group",
|
|
2487
|
+
"supervisor",
|
|
2488
|
+
"persist",
|
|
2489
|
+
"dev",
|
|
2490
|
+
"inspect",
|
|
2491
|
+
"info",
|
|
2492
|
+
"rename"
|
|
2493
|
+
]
|
|
2494
|
+
},
|
|
2466
2495
|
{ title: "Observation", keys: ["attach", "attach-pid", "attach-port", "pipe", "watch"] },
|
|
2467
2496
|
{ title: "Data", keys: ["export", "import", "cleanup"] },
|
|
2468
2497
|
{ title: "Configuration", keys: ["init", "setup", "install-browsers", "sessions"] },
|
|
@@ -2476,7 +2505,9 @@ function showHelp() {
|
|
|
2476
2505
|
const lines = [];
|
|
2477
2506
|
lines.push("");
|
|
2478
2507
|
lines.push(` ${pc3.bold("Usage:")} fennec ${pc3.dim("<command>")} ${pc3.dim("[options]")}`);
|
|
2479
|
-
lines.push(
|
|
2508
|
+
lines.push(
|
|
2509
|
+
` ${pc3.dim("Run")} ${pc3.cyan("fennec <command> --help")} ${pc3.dim("for details on any command.")}`
|
|
2510
|
+
);
|
|
2480
2511
|
for (const group of GROUPS) {
|
|
2481
2512
|
lines.push("");
|
|
2482
2513
|
lines.push(` ${sep}`);
|
|
@@ -2501,8 +2532,10 @@ function showCommandHelp(cmd) {
|
|
|
2501
2532
|
if (!doc) {
|
|
2502
2533
|
console.error(`
|
|
2503
2534
|
${pc3.yellow("\u26A0")} Unknown command: ${pc3.bold(cmd)}`);
|
|
2504
|
-
console.error(
|
|
2505
|
-
`)
|
|
2535
|
+
console.error(
|
|
2536
|
+
` ${pc3.dim("Run")} ${pc3.cyan("fennec help")} ${pc3.dim("to see all commands.")}
|
|
2537
|
+
`
|
|
2538
|
+
);
|
|
2506
2539
|
return;
|
|
2507
2540
|
}
|
|
2508
2541
|
const lines = [];
|
|
@@ -2611,7 +2644,9 @@ async function attachPortCommand(args2) {
|
|
|
2611
2644
|
const detector = new PortDetector2();
|
|
2612
2645
|
const info = detector.detectByPort(port);
|
|
2613
2646
|
if (info) {
|
|
2614
|
-
console.log(
|
|
2647
|
+
console.log(
|
|
2648
|
+
`Found process on port ${port}: PID ${info.pid}${info.command ? ` (${info.command})` : ""}`
|
|
2649
|
+
);
|
|
2615
2650
|
} else {
|
|
2616
2651
|
console.error(`No process found listening on port ${port}`);
|
|
2617
2652
|
process.exit(1);
|
|
@@ -2650,7 +2685,18 @@ import { FennecServer } from "@plumpslabs/fennec-core";
|
|
|
2650
2685
|
import pc7 from "picocolors";
|
|
2651
2686
|
|
|
2652
2687
|
// src/commands/tracker.ts
|
|
2653
|
-
import {
|
|
2688
|
+
import {
|
|
2689
|
+
existsSync as existsSync3,
|
|
2690
|
+
writeFileSync,
|
|
2691
|
+
readFileSync as readFileSync2,
|
|
2692
|
+
mkdirSync,
|
|
2693
|
+
renameSync,
|
|
2694
|
+
statSync,
|
|
2695
|
+
rmSync,
|
|
2696
|
+
openSync,
|
|
2697
|
+
closeSync,
|
|
2698
|
+
writeSync
|
|
2699
|
+
} from "fs";
|
|
2654
2700
|
import { spawn } from "child_process";
|
|
2655
2701
|
import { resolve as resolve2, dirname, basename } from "path";
|
|
2656
2702
|
import { homedir } from "os";
|
|
@@ -2712,7 +2758,10 @@ function readSingleProcProcess(pid, currentUid, totalMemKb) {
|
|
|
2712
2758
|
if (uptimeSeconds > 0) {
|
|
2713
2759
|
const elapsedJiffies = uptimeSeconds * hertz;
|
|
2714
2760
|
const processJiffies = totalJiffies;
|
|
2715
|
-
cpuPercent = Math.min(
|
|
2761
|
+
cpuPercent = Math.min(
|
|
2762
|
+
100,
|
|
2763
|
+
processJiffies / Math.max(1, elapsedJiffies) * 100 * cpus().length
|
|
2764
|
+
);
|
|
2716
2765
|
}
|
|
2717
2766
|
startedAt = formatStartTime(starttime, uptimeSeconds, hertz);
|
|
2718
2767
|
}
|
|
@@ -2942,7 +2991,10 @@ function isProcessRunning(pid) {
|
|
|
2942
2991
|
function getProcessMemRss(pid) {
|
|
2943
2992
|
if (process.platform === "win32") {
|
|
2944
2993
|
try {
|
|
2945
|
-
const out = execSync(`tasklist /fi "PID eq ${pid}" /fo csv /nh 2>nul`, {
|
|
2994
|
+
const out = execSync(`tasklist /fi "PID eq ${pid}" /fo csv /nh 2>nul`, {
|
|
2995
|
+
encoding: "utf-8",
|
|
2996
|
+
timeout: 3e3
|
|
2997
|
+
});
|
|
2946
2998
|
for (const raw of out.split("\n")) {
|
|
2947
2999
|
if (!raw.trim()) continue;
|
|
2948
3000
|
const parts = raw.split('","').map((p) => p.replace(/^"|"$/g, ""));
|
|
@@ -2957,7 +3009,10 @@ function getProcessMemRss(pid) {
|
|
|
2957
3009
|
}
|
|
2958
3010
|
if (process.platform === "darwin" || process.platform === "freebsd" || process.platform === "openbsd" || process.platform === "netbsd") {
|
|
2959
3011
|
try {
|
|
2960
|
-
const out = execSync(`ps -o rss= -p ${pid} 2>/dev/null`, {
|
|
3012
|
+
const out = execSync(`ps -o rss= -p ${pid} 2>/dev/null`, {
|
|
3013
|
+
encoding: "utf-8",
|
|
3014
|
+
timeout: 3e3
|
|
3015
|
+
}).trim();
|
|
2961
3016
|
const kb = parseInt(out, 10);
|
|
2962
3017
|
return isNaN(kb) ? null : kb;
|
|
2963
3018
|
} catch {
|
|
@@ -3052,7 +3107,10 @@ function findPidOnPortLinux(port) {
|
|
|
3052
3107
|
}
|
|
3053
3108
|
function findPidOnPortMac(port) {
|
|
3054
3109
|
try {
|
|
3055
|
-
const out = execSync(`lsof -i :${port} -sTCP:LISTEN -P -n 2>/dev/null`, {
|
|
3110
|
+
const out = execSync(`lsof -i :${port} -sTCP:LISTEN -P -n 2>/dev/null`, {
|
|
3111
|
+
encoding: "utf-8",
|
|
3112
|
+
timeout: 5e3
|
|
3113
|
+
});
|
|
3056
3114
|
for (const line of out.split("\n")) {
|
|
3057
3115
|
if (!line.includes("LISTEN")) continue;
|
|
3058
3116
|
const parts = line.trim().split(/\s+/);
|
|
@@ -3107,7 +3165,10 @@ function getProcessCmdlineMac(pid) {
|
|
|
3107
3165
|
}
|
|
3108
3166
|
function getProcessCmdlineWindows(pid) {
|
|
3109
3167
|
try {
|
|
3110
|
-
const out = execSync(`wmic process where ProcessId=${pid} get CommandLine /value 2>nul`, {
|
|
3168
|
+
const out = execSync(`wmic process where ProcessId=${pid} get CommandLine /value 2>nul`, {
|
|
3169
|
+
encoding: "utf-8",
|
|
3170
|
+
timeout: 3e3
|
|
3171
|
+
});
|
|
3111
3172
|
const line = out.split("\n").find((l) => l.includes("="));
|
|
3112
3173
|
const value = line?.split("=")[1]?.trim();
|
|
3113
3174
|
return value ? value : null;
|
|
@@ -3125,7 +3186,10 @@ function getProcessCwd(pid) {
|
|
|
3125
3186
|
}
|
|
3126
3187
|
if (process.platform === "darwin") {
|
|
3127
3188
|
try {
|
|
3128
|
-
const out = execSync(`lsof -p ${pid} -a -d cwd -F n 2>/dev/null`, {
|
|
3189
|
+
const out = execSync(`lsof -p ${pid} -a -d cwd -F n 2>/dev/null`, {
|
|
3190
|
+
encoding: "utf-8",
|
|
3191
|
+
timeout: 3e3
|
|
3192
|
+
});
|
|
3129
3193
|
const line = out.split("\n").find((l) => l.startsWith("n"));
|
|
3130
3194
|
return line ? line.slice(1) : null;
|
|
3131
3195
|
} catch {
|
|
@@ -3239,7 +3303,10 @@ function saveTracked(processes) {
|
|
|
3239
3303
|
mkdirSync(dirname(path), { recursive: true });
|
|
3240
3304
|
writeFileSync(path, JSON.stringify(processes, null, 2), "utf-8");
|
|
3241
3305
|
} catch (err) {
|
|
3242
|
-
console.error(
|
|
3306
|
+
console.error(
|
|
3307
|
+
"[fennec] Failed to save tracked processes:",
|
|
3308
|
+
err instanceof Error ? err.message : String(err)
|
|
3309
|
+
);
|
|
3243
3310
|
}
|
|
3244
3311
|
}
|
|
3245
3312
|
function addTracked(proc) {
|
|
@@ -3355,7 +3422,10 @@ function rotateLogFile(filePath, maxSize = DEFAULT_MAX_LOG_SIZE, maxFiles = DEFA
|
|
|
3355
3422
|
}
|
|
3356
3423
|
return true;
|
|
3357
3424
|
} catch (err) {
|
|
3358
|
-
console.error(
|
|
3425
|
+
console.error(
|
|
3426
|
+
"[fennec] Log rotation failed:",
|
|
3427
|
+
err instanceof Error ? err.message : String(err)
|
|
3428
|
+
);
|
|
3359
3429
|
return false;
|
|
3360
3430
|
}
|
|
3361
3431
|
}
|
|
@@ -3420,15 +3490,11 @@ function spawnDaemon(opts) {
|
|
|
3420
3490
|
return child;
|
|
3421
3491
|
}
|
|
3422
3492
|
function spawnDaemonJsonl(opts, env) {
|
|
3423
|
-
const relay = spawn(
|
|
3424
|
-
process.
|
|
3425
|
-
["
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
stdio: ["pipe", "ignore", "ignore"],
|
|
3429
|
-
detached: true
|
|
3430
|
-
}
|
|
3431
|
-
);
|
|
3493
|
+
const relay = spawn(process.execPath, ["-e", JSONL_RELAY], {
|
|
3494
|
+
env: { ...process.env, FENNEC_RELAY_LOG: opts.logFilePath },
|
|
3495
|
+
stdio: ["pipe", "ignore", "ignore"],
|
|
3496
|
+
detached: true
|
|
3497
|
+
});
|
|
3432
3498
|
relay.unref();
|
|
3433
3499
|
const child = spawn(opts.cmdParts[0], opts.cmdParts.slice(1), {
|
|
3434
3500
|
cwd: opts.cwd,
|
|
@@ -3484,7 +3550,16 @@ ${RESTART_CAUSE} cause=${cause} at=${(/* @__PURE__ */ new Date()).toISOString()}
|
|
|
3484
3550
|
|
|
3485
3551
|
// src/commands/supervisor.ts
|
|
3486
3552
|
import pc4 from "picocolors";
|
|
3487
|
-
import {
|
|
3553
|
+
import {
|
|
3554
|
+
existsSync as existsSync4,
|
|
3555
|
+
readFileSync as readFileSync3,
|
|
3556
|
+
writeFileSync as writeFileSync2,
|
|
3557
|
+
unlinkSync,
|
|
3558
|
+
mkdirSync as mkdirSync2,
|
|
3559
|
+
openSync as openSync2,
|
|
3560
|
+
closeSync as closeSync2,
|
|
3561
|
+
statSync as statSync2
|
|
3562
|
+
} from "fs";
|
|
3488
3563
|
import { spawn as spawn2 } from "child_process";
|
|
3489
3564
|
import { dirname as dirname2 } from "path";
|
|
3490
3565
|
var POLL_INTERVAL_MS = 3e3;
|
|
@@ -3506,6 +3581,38 @@ function ensureSupervisorRunning() {
|
|
|
3506
3581
|
return startSupervisorDaemon();
|
|
3507
3582
|
}
|
|
3508
3583
|
function startSupervisorDaemon() {
|
|
3584
|
+
const already = getSupervisorPid();
|
|
3585
|
+
if (already) return already;
|
|
3586
|
+
const lockPath = `${getSupervisorPidPath()}.lock`;
|
|
3587
|
+
const SUPERVISOR_LOCK_STALE_MS = 6e4;
|
|
3588
|
+
const acquireLock = () => {
|
|
3589
|
+
try {
|
|
3590
|
+
const lfd = openSync2(lockPath, "wx");
|
|
3591
|
+
closeSync2(lfd);
|
|
3592
|
+
return true;
|
|
3593
|
+
} catch {
|
|
3594
|
+
return false;
|
|
3595
|
+
}
|
|
3596
|
+
};
|
|
3597
|
+
if (!acquireLock()) {
|
|
3598
|
+
const live = getSupervisorPid();
|
|
3599
|
+
if (live) return live;
|
|
3600
|
+
let stale = false;
|
|
3601
|
+
try {
|
|
3602
|
+
stale = Date.now() - statSync2(lockPath).mtimeMs > SUPERVISOR_LOCK_STALE_MS;
|
|
3603
|
+
} catch {
|
|
3604
|
+
stale = false;
|
|
3605
|
+
}
|
|
3606
|
+
if (stale) {
|
|
3607
|
+
try {
|
|
3608
|
+
unlinkSync(lockPath);
|
|
3609
|
+
} catch {
|
|
3610
|
+
}
|
|
3611
|
+
if (!acquireLock()) return getSupervisorPid() ?? 0;
|
|
3612
|
+
} else {
|
|
3613
|
+
return 0;
|
|
3614
|
+
}
|
|
3615
|
+
}
|
|
3509
3616
|
const logFilePath = logFilePathFor("supervisor");
|
|
3510
3617
|
mkdirSync2(dirname2(logFilePath), { recursive: true });
|
|
3511
3618
|
const fd = openSync2(logFilePath, "a");
|
|
@@ -3536,24 +3643,30 @@ async function supervisorCommand(args2) {
|
|
|
3536
3643
|
case "status":
|
|
3537
3644
|
return supervisorStatus();
|
|
3538
3645
|
default:
|
|
3539
|
-
console.error(
|
|
3646
|
+
console.error(
|
|
3647
|
+
renderError("Unknown sub-command", `"${sub}" \u2014 use: start | stop | status | restart`)
|
|
3648
|
+
);
|
|
3540
3649
|
process.exit(1);
|
|
3541
3650
|
}
|
|
3542
3651
|
}
|
|
3543
3652
|
function supervisorStart() {
|
|
3544
3653
|
const existing = getSupervisorPid();
|
|
3545
3654
|
if (existing) {
|
|
3546
|
-
console.error(
|
|
3655
|
+
console.error(
|
|
3656
|
+
`
|
|
3547
3657
|
${pc4.yellow("\u26A0")} Supervisor already running ${pc4.dim(`(PID ${existing})`)}
|
|
3548
|
-
`
|
|
3658
|
+
`
|
|
3659
|
+
);
|
|
3549
3660
|
return;
|
|
3550
3661
|
}
|
|
3551
3662
|
const pid = startSupervisorDaemon();
|
|
3552
3663
|
console.error(`
|
|
3553
3664
|
${pc4.green("\u2713")} ${pc4.bold("Supervisor started")} ${pc4.dim(`(PID ${pid})`)}`);
|
|
3554
3665
|
console.error(` ${renderKV("Logs", pc4.cyan("fennec log supervisor"))}`);
|
|
3555
|
-
console.error(
|
|
3556
|
-
`)
|
|
3666
|
+
console.error(
|
|
3667
|
+
` ${pc4.dim("It will auto-restart apps started with")} ${pc4.cyan("--restart")} ${pc4.dim("even if you close this terminal.")}
|
|
3668
|
+
`
|
|
3669
|
+
);
|
|
3557
3670
|
}
|
|
3558
3671
|
function supervisorStop() {
|
|
3559
3672
|
const pid = getSupervisorPid();
|
|
@@ -3565,9 +3678,11 @@ function supervisorStop() {
|
|
|
3565
3678
|
}
|
|
3566
3679
|
try {
|
|
3567
3680
|
killTree(pid, "SIGTERM");
|
|
3568
|
-
console.error(
|
|
3681
|
+
console.error(
|
|
3682
|
+
`
|
|
3569
3683
|
${pc4.green("\u2713")} ${pc4.bold("Supervisor stopped")} ${pc4.dim(`(PID ${pid})`)}
|
|
3570
|
-
`
|
|
3684
|
+
`
|
|
3685
|
+
);
|
|
3571
3686
|
} catch (err) {
|
|
3572
3687
|
console.error(renderError("Failed to stop supervisor", String(err)));
|
|
3573
3688
|
}
|
|
@@ -3585,12 +3700,18 @@ function supervisorStatus() {
|
|
|
3585
3700
|
if (pid) {
|
|
3586
3701
|
console.error(` ${pc4.green("\u25CF")} running ${pc4.dim(`(PID ${pid})`)}`);
|
|
3587
3702
|
} else {
|
|
3588
|
-
console.error(
|
|
3703
|
+
console.error(
|
|
3704
|
+
` ${pc4.red("\u25CB")} not running ${pc4.dim("\u2014 start with")} ${pc4.cyan("fennec supervisor start")}`
|
|
3705
|
+
);
|
|
3589
3706
|
}
|
|
3590
|
-
console.error(
|
|
3591
|
-
|
|
3707
|
+
console.error(
|
|
3708
|
+
`
|
|
3709
|
+
${pc4.bold("Managed apps")} ${pc4.dim(`(${managed.length} with auto-restart)`)}`
|
|
3710
|
+
);
|
|
3592
3711
|
if (managed.length === 0) {
|
|
3593
|
-
console.error(
|
|
3712
|
+
console.error(
|
|
3713
|
+
` ${pc4.dim("None. Start one with")} ${pc4.cyan("fennec start <cmd> --name <name> --restart")}`
|
|
3714
|
+
);
|
|
3594
3715
|
} else {
|
|
3595
3716
|
for (const t of managed) {
|
|
3596
3717
|
const running = isTrackedRunning(t);
|
|
@@ -3611,6 +3732,10 @@ async function runSupervisor() {
|
|
|
3611
3732
|
return;
|
|
3612
3733
|
}
|
|
3613
3734
|
writeFileSync2(pidPath, String(process.pid), "utf-8");
|
|
3735
|
+
try {
|
|
3736
|
+
unlinkSync(`${pidPath}.lock`);
|
|
3737
|
+
} catch {
|
|
3738
|
+
}
|
|
3614
3739
|
log(`supervisor started (PID ${process.pid}), polling every ${POLL_INTERVAL_MS}ms`);
|
|
3615
3740
|
const crashes = /* @__PURE__ */ new Map();
|
|
3616
3741
|
let stopped = false;
|
|
@@ -3620,6 +3745,10 @@ async function runSupervisor() {
|
|
|
3620
3745
|
unlinkSync(pidPath);
|
|
3621
3746
|
} catch {
|
|
3622
3747
|
}
|
|
3748
|
+
try {
|
|
3749
|
+
unlinkSync(`${pidPath}.lock`);
|
|
3750
|
+
} catch {
|
|
3751
|
+
}
|
|
3623
3752
|
log("supervisor stopping");
|
|
3624
3753
|
process.exit(0);
|
|
3625
3754
|
};
|
|
@@ -3662,7 +3791,9 @@ async function runSupervisor() {
|
|
|
3662
3791
|
rec2.portFails = 0;
|
|
3663
3792
|
rec2.count++;
|
|
3664
3793
|
if (rec2.count >= FLAPPING_THRESHOLD) setFlapping(t, true);
|
|
3665
|
-
log(
|
|
3794
|
+
log(
|
|
3795
|
+
`${t.name}: alive but ${probe} not responding ${PORT_FAIL_THRESHOLD}x \u2014 restarting`
|
|
3796
|
+
);
|
|
3666
3797
|
try {
|
|
3667
3798
|
const newPid = respawnTracked(t, "port-down");
|
|
3668
3799
|
log(`${t.name}: restarted (PID ${newPid}) after ${probe} failure`);
|
|
@@ -3688,14 +3819,18 @@ async function runSupervisor() {
|
|
|
3688
3819
|
if (rec.gaveUp) continue;
|
|
3689
3820
|
if (rec.count >= MAX_RESTARTS_IN_WINDOW) {
|
|
3690
3821
|
rec.gaveUp = true;
|
|
3691
|
-
log(
|
|
3822
|
+
log(
|
|
3823
|
+
`${t.name}: crashed ${rec.count}x in <60s \u2014 giving up (will retry after window resets)`
|
|
3824
|
+
);
|
|
3692
3825
|
continue;
|
|
3693
3826
|
}
|
|
3694
3827
|
rec.count++;
|
|
3695
3828
|
if (rec.count >= FLAPPING_THRESHOLD) setFlapping(t, true);
|
|
3696
3829
|
try {
|
|
3697
3830
|
const newPid = respawnTracked(t, "crash");
|
|
3698
|
-
log(
|
|
3831
|
+
log(
|
|
3832
|
+
`${t.name}: restarted (PID ${newPid}) [${rec.count}/${MAX_RESTARTS_IN_WINDOW} in window]`
|
|
3833
|
+
);
|
|
3699
3834
|
} catch (err) {
|
|
3700
3835
|
log(`${t.name}: restart failed: ${String(err)}`);
|
|
3701
3836
|
}
|
|
@@ -3751,11 +3886,25 @@ function unitPaths(backend) {
|
|
|
3751
3886
|
const fennecDir = getFennecDir();
|
|
3752
3887
|
switch (backend) {
|
|
3753
3888
|
case "systemd":
|
|
3754
|
-
return {
|
|
3889
|
+
return {
|
|
3890
|
+
unit: resolve3(homedir2(), ".config", "systemd", "user", "fennec-supervisor.service")
|
|
3891
|
+
};
|
|
3755
3892
|
case "launchd":
|
|
3756
3893
|
return { unit: resolve3(homedir2(), "Library", "LaunchAgents", "io.fennec.supervisor.plist") };
|
|
3757
3894
|
case "windows":
|
|
3758
|
-
return {
|
|
3895
|
+
return {
|
|
3896
|
+
unit: resolve3(
|
|
3897
|
+
homedir2(),
|
|
3898
|
+
"AppData",
|
|
3899
|
+
"Roaming",
|
|
3900
|
+
"Microsoft",
|
|
3901
|
+
"Windows",
|
|
3902
|
+
"Start Menu",
|
|
3903
|
+
"Programs",
|
|
3904
|
+
"Startup",
|
|
3905
|
+
"fennec-supervisor.bat"
|
|
3906
|
+
)
|
|
3907
|
+
};
|
|
3759
3908
|
default:
|
|
3760
3909
|
return { unit: resolve3(fennecDir, "persist.unit") };
|
|
3761
3910
|
}
|
|
@@ -3836,7 +3985,12 @@ async function persistCommand(args2) {
|
|
|
3836
3985
|
const sub = args2[0] ?? "status";
|
|
3837
3986
|
const backend = detectBackend();
|
|
3838
3987
|
if (backend === "unsupported") {
|
|
3839
|
-
console.error(
|
|
3988
|
+
console.error(
|
|
3989
|
+
renderError(
|
|
3990
|
+
"Unsupported platform",
|
|
3991
|
+
`Boot persistence isn't available on ${platform()}. You can still run "fennec supervisor start" manually.`
|
|
3992
|
+
)
|
|
3993
|
+
);
|
|
3840
3994
|
process.exit(1);
|
|
3841
3995
|
}
|
|
3842
3996
|
switch (sub) {
|
|
@@ -3847,7 +4001,9 @@ async function persistCommand(args2) {
|
|
|
3847
4001
|
case "status":
|
|
3848
4002
|
return persistStatus(backend);
|
|
3849
4003
|
default:
|
|
3850
|
-
console.error(
|
|
4004
|
+
console.error(
|
|
4005
|
+
renderError("Unknown sub-command", `"${sub}" \u2014 use: enable | disable | status`)
|
|
4006
|
+
);
|
|
3851
4007
|
process.exit(1);
|
|
3852
4008
|
}
|
|
3853
4009
|
}
|
|
@@ -3857,8 +4013,10 @@ function persistEnable(backend) {
|
|
|
3857
4013
|
mkdirSync3(dirname3(unit), { recursive: true });
|
|
3858
4014
|
writeFileSync3(unit, buildUnit(backend, dataDir), "utf-8");
|
|
3859
4015
|
if (backend === "windows") {
|
|
3860
|
-
console.error(
|
|
3861
|
-
|
|
4016
|
+
console.error(
|
|
4017
|
+
`
|
|
4018
|
+
${pc5.green("\u2713")} ${pc5.bold("Boot script installed")} ${pc5.dim(`(runs at next login)`)}`
|
|
4019
|
+
);
|
|
3862
4020
|
console.error(` ${renderKV("Path", pc5.cyan(unit))}`);
|
|
3863
4021
|
console.error(` ${pc5.dim("Supervisor will start automatically when you log in.")}
|
|
3864
4022
|
`);
|
|
@@ -3872,20 +4030,28 @@ function persistEnable(backend) {
|
|
|
3872
4030
|
}
|
|
3873
4031
|
const linger = run("loginctl", ["enable-linger"]);
|
|
3874
4032
|
if (!linger.ok) {
|
|
3875
|
-
console.error(
|
|
4033
|
+
console.error(
|
|
4034
|
+
` ${pc5.yellow("\u26A0")} ${pc5.dim("Could not enable user linger \u2014 the supervisor may stop when you log out.")}`
|
|
4035
|
+
);
|
|
3876
4036
|
}
|
|
3877
|
-
console.error(
|
|
3878
|
-
|
|
4037
|
+
console.error(
|
|
4038
|
+
`
|
|
4039
|
+
${pc5.green("\u2713")} ${pc5.bold("Boot persistence enabled")} ${pc5.dim("(systemd user service)")}`
|
|
4040
|
+
);
|
|
3879
4041
|
console.error(` ${renderKV("Unit", pc5.cyan("fennec-supervisor.service"))}`);
|
|
3880
4042
|
console.error(` ${renderKV("Data dir", pc5.cyan(dataDir))}`);
|
|
3881
|
-
console.error(
|
|
3882
|
-
`)
|
|
4043
|
+
console.error(
|
|
4044
|
+
` ${pc5.dim("The supervisor (and all --restart apps) will start at login and keep running after logout.")}
|
|
4045
|
+
`
|
|
4046
|
+
);
|
|
3883
4047
|
return;
|
|
3884
4048
|
}
|
|
3885
4049
|
if (backend === "launchd") {
|
|
3886
4050
|
run("launchctl", ["load", resolve3(unit)]);
|
|
3887
|
-
console.error(
|
|
3888
|
-
|
|
4051
|
+
console.error(
|
|
4052
|
+
`
|
|
4053
|
+
${pc5.green("\u2713")} ${pc5.bold("Boot persistence enabled")} ${pc5.dim("(launchd agent)")}`
|
|
4054
|
+
);
|
|
3889
4055
|
console.error(` ${renderKV("Label", pc5.cyan("io.fennec.supervisor"))}`);
|
|
3890
4056
|
console.error(` ${renderKV("Data dir", pc5.cyan(dataDir))}`);
|
|
3891
4057
|
console.error(` ${pc5.dim("The supervisor will start at login.")}
|
|
@@ -3915,7 +4081,9 @@ function persistStatus(backend) {
|
|
|
3915
4081
|
console.error(`
|
|
3916
4082
|
${pc5.bold("Fennec Boot Persistence")} ${pc5.dim(`(${backend})`)}`);
|
|
3917
4083
|
if (!installed) {
|
|
3918
|
-
console.error(
|
|
4084
|
+
console.error(
|
|
4085
|
+
` ${pc5.red("\u25CB")} ${pc5.dim("not installed \u2014 run ")}${pc5.cyan("fennec persist enable")}${pc5.dim(" to auto-start apps after reboot")}`
|
|
4086
|
+
);
|
|
3919
4087
|
console.error();
|
|
3920
4088
|
return;
|
|
3921
4089
|
}
|
|
@@ -3969,26 +4137,48 @@ async function psCommand(args2) {
|
|
|
3969
4137
|
const columns2 = [
|
|
3970
4138
|
{ key: "pid", label: "PID", align: "right", format: (v) => pc6.dim(String(v).padStart(6)) },
|
|
3971
4139
|
{ key: "name", label: "Name", format: (v) => pc6.bold(String(v)) },
|
|
3972
|
-
{
|
|
3973
|
-
|
|
3974
|
-
|
|
3975
|
-
|
|
3976
|
-
|
|
3977
|
-
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
|
|
3981
|
-
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
|
|
3986
|
-
|
|
4140
|
+
{
|
|
4141
|
+
key: "cpu",
|
|
4142
|
+
label: "CPU%",
|
|
4143
|
+
align: "right",
|
|
4144
|
+
format: (v) => {
|
|
4145
|
+
const n = v;
|
|
4146
|
+
return n > 10 ? pc6.red(String(n)) : n > 5 ? pc6.yellow(String(n)) : pc6.dim(String(n));
|
|
4147
|
+
}
|
|
4148
|
+
},
|
|
4149
|
+
{
|
|
4150
|
+
key: "mem",
|
|
4151
|
+
label: "MEM%",
|
|
4152
|
+
align: "right",
|
|
4153
|
+
format: (v) => {
|
|
4154
|
+
const n = v;
|
|
4155
|
+
return n > 10 ? pc6.red(String(n)) : n > 5 ? pc6.yellow(String(n)) : pc6.dim(String(n));
|
|
4156
|
+
}
|
|
4157
|
+
},
|
|
4158
|
+
{
|
|
4159
|
+
key: "state",
|
|
4160
|
+
label: "State",
|
|
4161
|
+
format: (v) => {
|
|
4162
|
+
const s = String(v);
|
|
4163
|
+
if (s === "R" || s === "Running") return pc6.green(s);
|
|
4164
|
+
if (s === "Z" || s === "Zombie") return pc6.red(s);
|
|
4165
|
+
if (s === "S" || s === "Sleeping") return pc6.cyan(s);
|
|
4166
|
+
return pc6.dim(s);
|
|
4167
|
+
}
|
|
4168
|
+
}
|
|
3987
4169
|
];
|
|
3988
|
-
const rows2 = processes.map((p) => ({
|
|
3989
|
-
|
|
4170
|
+
const rows2 = processes.map((p) => ({
|
|
4171
|
+
pid: p.pid,
|
|
4172
|
+
name: p.name,
|
|
4173
|
+
cpu: p.cpuPercent,
|
|
4174
|
+
mem: p.memPercent,
|
|
4175
|
+
state: formatProcessState(p.state)
|
|
4176
|
+
}));
|
|
4177
|
+
console.error(
|
|
4178
|
+
`
|
|
3990
4179
|
${symbols.fox} ${pc6.bold("System Processes")} ${pc6.dim(`(top ${processes.length} by ${sortBy})`)}
|
|
3991
|
-
`
|
|
4180
|
+
`
|
|
4181
|
+
);
|
|
3992
4182
|
console.error(renderTable(columns2, rows2));
|
|
3993
4183
|
console.error();
|
|
3994
4184
|
} catch (error) {
|
|
@@ -4000,54 +4190,98 @@ async function psCommand(args2) {
|
|
|
4000
4190
|
const trackedAll = readTracked();
|
|
4001
4191
|
const tracked = groupFilter ? trackedAll.filter((t) => t.group === groupFilter) : trackedAll;
|
|
4002
4192
|
if (tracked.length === 0) {
|
|
4003
|
-
console.error(
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
|
|
4193
|
+
console.error(
|
|
4194
|
+
`
|
|
4195
|
+
${pc6.dim(groupFilter ? `No tracked processes in group "${groupFilter}".` : "No tracked processes.")}`
|
|
4196
|
+
);
|
|
4197
|
+
console.error(
|
|
4198
|
+
` ${pc6.dim("Start an app with:")} ${pc6.cyan("fennec start <command> --name <name>")}
|
|
4199
|
+
`
|
|
4200
|
+
);
|
|
4007
4201
|
return;
|
|
4008
4202
|
}
|
|
4009
4203
|
const columns = [
|
|
4010
4204
|
{ key: "name", label: "App", format: (v) => pc6.bold(String(v)) },
|
|
4011
4205
|
{ key: "pid", label: "PID", align: "right" },
|
|
4012
|
-
{
|
|
4013
|
-
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
}
|
|
4020
|
-
{
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
}
|
|
4028
|
-
{
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
4206
|
+
{
|
|
4207
|
+
key: "status",
|
|
4208
|
+
label: "Status",
|
|
4209
|
+
format: (v) => {
|
|
4210
|
+
const s = v;
|
|
4211
|
+
return s === "running" ? pc6.green("\u25CF running") : pc6.red("\u25CB stopped");
|
|
4212
|
+
}
|
|
4213
|
+
},
|
|
4214
|
+
{
|
|
4215
|
+
key: "group",
|
|
4216
|
+
label: "Group",
|
|
4217
|
+
format: (v) => {
|
|
4218
|
+
const g = String(v);
|
|
4219
|
+
return g === "-" ? pc6.dim("-") : pc6.cyan(g);
|
|
4220
|
+
}
|
|
4221
|
+
},
|
|
4222
|
+
{
|
|
4223
|
+
key: "port",
|
|
4224
|
+
label: "Port",
|
|
4225
|
+
format: (v) => {
|
|
4226
|
+
const p = v;
|
|
4227
|
+
return p ? pc6.yellow(`:${p}`) : pc6.dim("-");
|
|
4228
|
+
}
|
|
4229
|
+
},
|
|
4230
|
+
{
|
|
4231
|
+
key: "mem",
|
|
4232
|
+
label: "MEM",
|
|
4233
|
+
align: "right",
|
|
4234
|
+
format: (v) => {
|
|
4235
|
+
const kb = v;
|
|
4236
|
+
return kb && kb > 0 ? pc6.dim(`${(kb / 1024).toFixed(0)}MB`) : pc6.dim("-");
|
|
4237
|
+
}
|
|
4238
|
+
},
|
|
4239
|
+
{
|
|
4240
|
+
key: "command",
|
|
4241
|
+
label: "Command",
|
|
4242
|
+
format: (v) => {
|
|
4243
|
+
const c = String(v);
|
|
4244
|
+
return c.length > 50 ? c.slice(0, 50) + "\u2026" : c;
|
|
4245
|
+
}
|
|
4246
|
+
},
|
|
4032
4247
|
{ key: "uptime", label: "Uptime", format: (v) => pc6.dim(String(v)) }
|
|
4033
4248
|
];
|
|
4034
4249
|
const rows = tracked.map((t) => {
|
|
4035
4250
|
const running = isTrackedRunning(t);
|
|
4036
4251
|
const uptime = running ? formatUptime(Math.floor((Date.now() - new Date(t.startedAt).getTime()) / 1e3)) : "-";
|
|
4037
4252
|
const memKb = running ? getProcessMemRss(t.pid) ?? null : null;
|
|
4038
|
-
return {
|
|
4253
|
+
return {
|
|
4254
|
+
name: t.name,
|
|
4255
|
+
pid: running ? String(t.pid) : pc6.dim(String(t.pid)),
|
|
4256
|
+
status: running ? "running" : "stopped",
|
|
4257
|
+
group: t.group ?? "-",
|
|
4258
|
+
port: t.port ?? null,
|
|
4259
|
+
mem: memKb,
|
|
4260
|
+
command: t.command,
|
|
4261
|
+
uptime
|
|
4262
|
+
};
|
|
4039
4263
|
});
|
|
4040
4264
|
const runningCount = tracked.filter((t) => isTrackedRunning(t)).length;
|
|
4041
4265
|
const scope = groupFilter ? ` in group ${pc6.cyan(groupFilter)}` : "";
|
|
4042
|
-
console.error(
|
|
4266
|
+
console.error(
|
|
4267
|
+
`
|
|
4043
4268
|
${symbols.fox} ${pc6.bold("Fennec Apps")} ${pc6.dim(`(${runningCount}/${tracked.length} running${scope})`)}
|
|
4044
|
-
`
|
|
4269
|
+
`
|
|
4270
|
+
);
|
|
4045
4271
|
console.error(renderTable(columns, rows));
|
|
4046
|
-
console.error(
|
|
4272
|
+
console.error(
|
|
4273
|
+
` ${pc6.dim("Use")} ${pc6.cyan("fennec start <command> --name <name> --port <port>")} ${pc6.dim("to add more apps.")}`
|
|
4274
|
+
);
|
|
4047
4275
|
console.error(` ${pc6.dim("Use")} ${pc6.cyan("fennec log <name>")} ${pc6.dim("to view logs.")}`);
|
|
4048
|
-
console.error(
|
|
4049
|
-
|
|
4050
|
-
|
|
4276
|
+
console.error(
|
|
4277
|
+
` ${pc6.dim("Use")} ${pc6.cyan("fennec stop <name>")} ${pc6.dim("to pause an app.")}`
|
|
4278
|
+
);
|
|
4279
|
+
console.error(
|
|
4280
|
+
` ${pc6.dim("Use")} ${pc6.cyan("fennec spawn <name>")} ${pc6.dim("to resume a paused app.")}`
|
|
4281
|
+
);
|
|
4282
|
+
console.error(
|
|
4283
|
+
` ${pc6.dim("Use")} ${pc6.cyan("fennec kill <name>")} ${pc6.dim("to permanently remove an app.")}`
|
|
4284
|
+
);
|
|
4051
4285
|
console.error(` ${pc6.dim("Filter by group with:")} ${pc6.cyan("fennec ps --group <group>")}`);
|
|
4052
4286
|
console.error();
|
|
4053
4287
|
}
|
|
@@ -4083,19 +4317,25 @@ async function statusCommand(_args) {
|
|
|
4083
4317
|
`);
|
|
4084
4318
|
if (tracked.length > 0) {
|
|
4085
4319
|
const runningCount = tracked.filter((t) => isTrackedRunning(t)).length;
|
|
4086
|
-
console.error(
|
|
4087
|
-
`)
|
|
4320
|
+
console.error(
|
|
4321
|
+
` ${pc6.bold("Managed Apps")} ${pc6.dim(`(${runningCount}/${tracked.length} running)`)}
|
|
4322
|
+
`
|
|
4323
|
+
);
|
|
4088
4324
|
for (const t of tracked) {
|
|
4089
4325
|
const running = isTrackedRunning(t);
|
|
4090
4326
|
const statusIcon = running ? pc6.green("\u25CF") : pc6.red("\u25CB");
|
|
4091
4327
|
const portStr = t.port ? ` ${pc6.yellow(`:${t.port}`)}` : "";
|
|
4092
4328
|
const uptime = running ? pc6.dim(formatUptime(Math.floor((Date.now() - new Date(t.startedAt).getTime()) / 1e3))) : pc6.red("stopped");
|
|
4093
|
-
console.error(
|
|
4329
|
+
console.error(
|
|
4330
|
+
` ${statusIcon} ${pc6.bold(t.name)}${portStr} ${pc6.dim(`(PID ${t.pid})`)} \u2014 ${uptime}`
|
|
4331
|
+
);
|
|
4094
4332
|
}
|
|
4095
4333
|
console.error();
|
|
4096
4334
|
} else {
|
|
4097
|
-
console.error(
|
|
4098
|
-
`)
|
|
4335
|
+
console.error(
|
|
4336
|
+
` ${pc6.dim("No managed apps.")} ${pc6.cyan("fennec start <command> --name <name>")}
|
|
4337
|
+
`
|
|
4338
|
+
);
|
|
4099
4339
|
}
|
|
4100
4340
|
console.error(` ${pc6.bold("System")} ${pc6.dim(`(${totalUserProcs} user processes)`)}`);
|
|
4101
4341
|
for (const p of topSystem) {
|
|
@@ -4107,25 +4347,47 @@ async function statusCommand(_args) {
|
|
|
4107
4347
|
console.error();
|
|
4108
4348
|
}
|
|
4109
4349
|
async function watchSystemProcesses(sortBy, limit) {
|
|
4110
|
-
console.error(
|
|
4350
|
+
console.error(
|
|
4351
|
+
`
|
|
4111
4352
|
${pc6.bold("Watching system processes")} ${pc6.dim("(Ctrl+C to stop, refreshes every 3s)")}
|
|
4112
|
-
`
|
|
4353
|
+
`
|
|
4354
|
+
);
|
|
4113
4355
|
const render = () => {
|
|
4114
|
-
const processes = getSystemProcesses({
|
|
4356
|
+
const processes = getSystemProcesses({
|
|
4357
|
+
userOnly: true,
|
|
4358
|
+
sortBy,
|
|
4359
|
+
limit
|
|
4360
|
+
});
|
|
4115
4361
|
const columns = [
|
|
4116
4362
|
{ key: "pid", label: "PID", align: "right", format: (v) => pc6.dim(String(v).padStart(6)) },
|
|
4117
4363
|
{ key: "name", label: "Name", format: (v) => pc6.bold(String(v)) },
|
|
4118
|
-
{
|
|
4119
|
-
|
|
4120
|
-
|
|
4121
|
-
|
|
4122
|
-
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
|
|
4364
|
+
{
|
|
4365
|
+
key: "cpu",
|
|
4366
|
+
label: "CPU%",
|
|
4367
|
+
align: "right",
|
|
4368
|
+
format: (v) => {
|
|
4369
|
+
const n = v;
|
|
4370
|
+
return n > 10 ? pc6.red(String(n)) : n > 5 ? pc6.yellow(String(n)) : pc6.dim(String(n));
|
|
4371
|
+
}
|
|
4372
|
+
},
|
|
4373
|
+
{
|
|
4374
|
+
key: "mem",
|
|
4375
|
+
label: "MEM%",
|
|
4376
|
+
align: "right",
|
|
4377
|
+
format: (v) => {
|
|
4378
|
+
const n = v;
|
|
4379
|
+
return n > 10 ? pc6.red(String(n)) : n > 5 ? pc6.yellow(String(n)) : pc6.dim(String(n));
|
|
4380
|
+
}
|
|
4381
|
+
},
|
|
4126
4382
|
{ key: "state", label: "S", align: "center" }
|
|
4127
4383
|
];
|
|
4128
|
-
const rows = processes.map((p) => ({
|
|
4384
|
+
const rows = processes.map((p) => ({
|
|
4385
|
+
pid: p.pid,
|
|
4386
|
+
name: p.name,
|
|
4387
|
+
cpu: p.cpuPercent,
|
|
4388
|
+
mem: p.memPercent,
|
|
4389
|
+
state: p.state
|
|
4390
|
+
}));
|
|
4129
4391
|
return ` ${timestamp()} ${pc6.dim(`${processes.length} processes`)}
|
|
4130
4392
|
${renderTable(columns, rows, { compact: true })}`;
|
|
4131
4393
|
};
|
|
@@ -4180,8 +4442,12 @@ async function startServer(args2) {
|
|
|
4180
4442
|
${pc7.green("\u2713")} ${pc7.bold("Fennec server is running")}`);
|
|
4181
4443
|
console.error(` ${renderKV("Transport", pc7.cyan("SSE (HTTP)"))}`);
|
|
4182
4444
|
console.error(` ${renderKV("URL", pc7.cyan(`http://localhost:${port}/sse`))}`);
|
|
4183
|
-
console.error(
|
|
4184
|
-
|
|
4445
|
+
console.error(
|
|
4446
|
+
` ${renderKV("MCP Config", pc7.cyan(`{ "type": "remote", "url": "http://localhost:${port}/sse" }`))}`
|
|
4447
|
+
);
|
|
4448
|
+
console.error(
|
|
4449
|
+
` ${renderKV("OpenCode", pc7.cyan(`type: remote, url: http://localhost:${port}/sse`))}`
|
|
4450
|
+
);
|
|
4185
4451
|
console.error(`
|
|
4186
4452
|
${pc7.dim("Press Ctrl+C to stop")}
|
|
4187
4453
|
`);
|
|
@@ -4221,15 +4487,22 @@ async function runCommand(args2) {
|
|
|
4221
4487
|
const cmdParts = args2.slice(0, cmdEnd);
|
|
4222
4488
|
const cmd = cmdParts.join(" ");
|
|
4223
4489
|
if (!cmd) {
|
|
4224
|
-
console.error(
|
|
4490
|
+
console.error(
|
|
4491
|
+
renderError(
|
|
4492
|
+
"Missing command",
|
|
4493
|
+
"Usage: fennec start|run <command> --name <name> [--port <port>] [--cwd <dir>] [--restart]"
|
|
4494
|
+
)
|
|
4495
|
+
);
|
|
4225
4496
|
process.exit(1);
|
|
4226
4497
|
}
|
|
4227
4498
|
const appName = name ?? cmdParts[0] ?? "app";
|
|
4228
4499
|
if (port !== void 0) {
|
|
4229
4500
|
const adopted = adoptExternalOnPort(port, appName);
|
|
4230
4501
|
if (adopted) {
|
|
4231
|
-
console.error(
|
|
4232
|
-
|
|
4502
|
+
console.error(
|
|
4503
|
+
`
|
|
4504
|
+
${pc7.green("\u2713")} ${pc7.bold("Adopted")} ${pc7.bold(adopted.name)} ${pc7.dim(`(PID ${adopted.pid}) \u2014 already listening on :${port}`)}`
|
|
4505
|
+
);
|
|
4233
4506
|
console.error(` ${renderKV("Logs", pc7.cyan(`fennec log ${adopted.name}`))}`);
|
|
4234
4507
|
console.error();
|
|
4235
4508
|
return;
|
|
@@ -4239,7 +4512,9 @@ async function runCommand(args2) {
|
|
|
4239
4512
|
const existing = tracked.find((t) => t.name === appName);
|
|
4240
4513
|
if (existing && isTrackedRunning(existing)) {
|
|
4241
4514
|
console.error();
|
|
4242
|
-
console.error(
|
|
4515
|
+
console.error(
|
|
4516
|
+
` ${pc7.yellow("\u26A0")} ${pc7.bold(appName)} ${pc7.dim(`is already running (PID: ${existing.pid})`)}`
|
|
4517
|
+
);
|
|
4243
4518
|
console.error(` ${renderKV("Logs", pc7.cyan(`fennec log ${appName}`))}`);
|
|
4244
4519
|
console.error(` ${renderKV("Stop", pc7.cyan(`fennec kill ${appName}`))}`);
|
|
4245
4520
|
console.error();
|
|
@@ -4248,9 +4523,11 @@ async function runCommand(args2) {
|
|
|
4248
4523
|
const logDir = dirname4(logFilePathFor(appName));
|
|
4249
4524
|
mkdirSync4(logDir, { recursive: true });
|
|
4250
4525
|
const logFilePath = logFilePathFor(appName);
|
|
4251
|
-
console.error(
|
|
4526
|
+
console.error(
|
|
4527
|
+
`
|
|
4252
4528
|
${symbols.fox} ${pc7.bold("Starting")} ${renderAppName(appName)} ${pc7.dim("(daemon)")}
|
|
4253
|
-
`
|
|
4529
|
+
`
|
|
4530
|
+
);
|
|
4254
4531
|
console.error(` ${renderKV("Command", cmd)}`);
|
|
4255
4532
|
if (port) console.error(` ${renderKV("Port", String(port))}`);
|
|
4256
4533
|
if (cwd) console.error(` ${renderKV("Directory", cwd)}`);
|
|
@@ -4262,7 +4539,14 @@ async function runCommand(args2) {
|
|
|
4262
4539
|
for (const [k, v] of Object.entries(process.env)) {
|
|
4263
4540
|
if (v !== void 0) startEnv[k] = v;
|
|
4264
4541
|
}
|
|
4265
|
-
const currentChild = spawnDaemon({
|
|
4542
|
+
const currentChild = spawnDaemon({
|
|
4543
|
+
cmdParts,
|
|
4544
|
+
name: appName,
|
|
4545
|
+
cwd,
|
|
4546
|
+
logFilePath,
|
|
4547
|
+
env: buildSpawnEnv(startEnv),
|
|
4548
|
+
logMode: jsonlFlag ? "jsonl" : "text"
|
|
4549
|
+
});
|
|
4266
4550
|
const pid = currentChild.pid ?? 0;
|
|
4267
4551
|
addTracked({
|
|
4268
4552
|
name: appName,
|
|
@@ -4278,28 +4562,38 @@ async function runCommand(args2) {
|
|
|
4278
4562
|
group
|
|
4279
4563
|
});
|
|
4280
4564
|
console.error(` ${pc7.green("\u2713")} ${pc7.bold(appName)} ${pc7.dim(`started (PID: ${pid})`)}`);
|
|
4281
|
-
const spinner = createSpinner(
|
|
4565
|
+
const spinner = createSpinner(
|
|
4566
|
+
port ? `Waiting for ${appName} on port ${port}...` : `Confirming ${appName} is running...`
|
|
4567
|
+
);
|
|
4282
4568
|
const ready = await waitForReady(pid, port);
|
|
4283
4569
|
spinner.stop();
|
|
4284
4570
|
process.stdout.write("\r\x1B[K");
|
|
4285
4571
|
if (!ready.running) {
|
|
4286
4572
|
removeTracked(appName);
|
|
4287
|
-
console.error(
|
|
4573
|
+
console.error(
|
|
4574
|
+
` ${pc7.red("\u2717")} ${pc7.bold(appName)} ${pc7.dim("exited immediately after start")}`
|
|
4575
|
+
);
|
|
4288
4576
|
console.error(` ${renderKV("Logs", pc7.cyan(`fennec log ${appName}`))}`);
|
|
4289
4577
|
console.error();
|
|
4290
4578
|
process.exit(1);
|
|
4291
4579
|
}
|
|
4292
4580
|
if (port) {
|
|
4293
4581
|
if (ready.portReady) {
|
|
4294
|
-
console.error(
|
|
4582
|
+
console.error(
|
|
4583
|
+
` ${pc7.green("\u2713")} ${pc7.bold(appName)} ${pc7.dim(`is listening on port ${port}`)}`
|
|
4584
|
+
);
|
|
4295
4585
|
} else {
|
|
4296
|
-
console.error(
|
|
4586
|
+
console.error(
|
|
4587
|
+
` ${pc7.yellow("\u26A0")} ${pc7.bold(appName)} ${pc7.dim(`is running but port ${port} is not accepting connections yet`)}`
|
|
4588
|
+
);
|
|
4297
4589
|
}
|
|
4298
4590
|
}
|
|
4299
4591
|
if (restartFlag) {
|
|
4300
4592
|
const supPid = ensureSupervisorRunning();
|
|
4301
4593
|
ensurePersistEnabled();
|
|
4302
|
-
console.error(
|
|
4594
|
+
console.error(
|
|
4595
|
+
` ${pc7.green("\u2713")} ${pc7.bold("Auto-restart")} ${pc7.dim(`managed by supervisor (PID ${supPid}) \u2014 survives terminal close`)}`
|
|
4596
|
+
);
|
|
4303
4597
|
console.error(` ${renderKV("Supervisor", pc7.cyan("fennec supervisor status"))}`);
|
|
4304
4598
|
}
|
|
4305
4599
|
await psCommand([]);
|
|
@@ -4324,7 +4618,14 @@ async function resurrectTracked() {
|
|
|
4324
4618
|
try {
|
|
4325
4619
|
const cmdParts = resolveArgs(proc);
|
|
4326
4620
|
const logFilePath = logFilePathFor(proc.name);
|
|
4327
|
-
const child = spawnDaemon({
|
|
4621
|
+
const child = spawnDaemon({
|
|
4622
|
+
cmdParts,
|
|
4623
|
+
name: proc.name,
|
|
4624
|
+
cwd: proc.cwd,
|
|
4625
|
+
logFilePath,
|
|
4626
|
+
env: buildSpawnEnv(proc.env),
|
|
4627
|
+
logMode: proc.logMode
|
|
4628
|
+
});
|
|
4328
4629
|
addTracked({
|
|
4329
4630
|
name: proc.name,
|
|
4330
4631
|
pid: child.pid ?? 0,
|
|
@@ -4379,7 +4680,12 @@ async function killCommand(args2) {
|
|
|
4379
4680
|
await killAll(signal, force);
|
|
4380
4681
|
return;
|
|
4381
4682
|
}
|
|
4382
|
-
console.error(
|
|
4683
|
+
console.error(
|
|
4684
|
+
renderError(
|
|
4685
|
+
"Missing target",
|
|
4686
|
+
"Usage: fennec kill <pid|name|all> [--signal SIGTERM|SIGKILL|SIGINT] [--group <group>] [-y]"
|
|
4687
|
+
)
|
|
4688
|
+
);
|
|
4383
4689
|
process.exit(1);
|
|
4384
4690
|
}
|
|
4385
4691
|
async function killOne(rawTarget, signal, force, multi) {
|
|
@@ -4404,8 +4710,10 @@ async function killOne(rawTarget, signal, force, multi) {
|
|
|
4404
4710
|
if (trackedMatch) {
|
|
4405
4711
|
if (!isTrackedRunning(trackedMatch)) {
|
|
4406
4712
|
removeTrackedByPid(trackedMatch.pid);
|
|
4407
|
-
console.error(
|
|
4408
|
-
|
|
4713
|
+
console.error(
|
|
4714
|
+
`
|
|
4715
|
+
${pc8.green("\u2713")} ${pc8.bold(rawTarget)} ${pc8.dim("removed from tracked apps (was already stopped)")}`
|
|
4716
|
+
);
|
|
4409
4717
|
console.error();
|
|
4410
4718
|
return;
|
|
4411
4719
|
}
|
|
@@ -4451,29 +4759,42 @@ Use ${pc8.cyan("fennec kill <pid>")} to kill a system process by its PID.`;
|
|
|
4451
4759
|
}
|
|
4452
4760
|
} else {
|
|
4453
4761
|
spinner.fail(`Failed to kill ${displayName}`);
|
|
4454
|
-
console.error(
|
|
4762
|
+
console.error(
|
|
4763
|
+
renderError("Permission denied", "Try running with sudo or use a different signal.")
|
|
4764
|
+
);
|
|
4455
4765
|
}
|
|
4456
4766
|
}
|
|
4457
4767
|
async function killGroup(group, signal, force) {
|
|
4458
4768
|
const tracked = readTracked();
|
|
4459
4769
|
const inGroup = tracked.filter((t) => t.group === group);
|
|
4460
4770
|
if (inGroup.length === 0) {
|
|
4461
|
-
console.error(
|
|
4462
|
-
|
|
4771
|
+
console.error(
|
|
4772
|
+
renderError(
|
|
4773
|
+
"Empty group",
|
|
4774
|
+
`No tracked entries in group "${group}".
|
|
4775
|
+
Known groups: ${pc8.cyan(getGroups().join(", ") || "(none)")}`
|
|
4776
|
+
)
|
|
4777
|
+
);
|
|
4463
4778
|
process.exit(1);
|
|
4464
4779
|
}
|
|
4465
4780
|
const running = inGroup.filter((t) => isTrackedRunning(t));
|
|
4466
4781
|
const stopped = inGroup.filter((t) => !isTrackedRunning(t));
|
|
4467
|
-
console.error(
|
|
4468
|
-
|
|
4469
|
-
|
|
4470
|
-
|
|
4782
|
+
console.error(
|
|
4783
|
+
`
|
|
4784
|
+
${pc8.bold(`Kill group ${pc8.cyan(group)}: ${running.length} running + remove ${stopped.length} stopped?`)}`
|
|
4785
|
+
);
|
|
4786
|
+
console.error(
|
|
4787
|
+
` ${pc8.dim("Permanently removes only apps in this group \u2014 other groups are untouched.")}
|
|
4788
|
+
`
|
|
4789
|
+
);
|
|
4471
4790
|
const confirmed = force || await confirmPrompt(`${pc8.red("Are you sure?")} ${pc8.dim("This cannot be undone")}`, false);
|
|
4472
4791
|
if (!confirmed) {
|
|
4473
4792
|
console.error(` ${pc8.dim("Cancelled.")}`);
|
|
4474
4793
|
return;
|
|
4475
4794
|
}
|
|
4476
|
-
const spinner = createSpinner(
|
|
4795
|
+
const spinner = createSpinner(
|
|
4796
|
+
`Killing ${running.length} + removing ${stopped.length} tracked app(s) in ${group}...`
|
|
4797
|
+
);
|
|
4477
4798
|
let killed = 0, failed = 0;
|
|
4478
4799
|
for (const t of running) {
|
|
4479
4800
|
if (killTree(t.pid, signal)) {
|
|
@@ -4488,7 +4809,9 @@ Known groups: ${pc8.cyan(getGroups().join(", ") || "(none)")}`));
|
|
|
4488
4809
|
}
|
|
4489
4810
|
await new Promise((r) => setTimeout(r, 300));
|
|
4490
4811
|
const total = running.length + stopped.length;
|
|
4491
|
-
total > 0 ? spinner.succeed(
|
|
4812
|
+
total > 0 ? spinner.succeed(
|
|
4813
|
+
`${killed} killed, ${stopped.length} stopped entries removed from group ${group}`
|
|
4814
|
+
) : spinner.fail("Failed to kill apps");
|
|
4492
4815
|
if (failed > 0) console.error(` ${pc8.yellow(`${failed} running app(s) could not be killed`)}`);
|
|
4493
4816
|
}
|
|
4494
4817
|
async function killAll(signal, force) {
|
|
@@ -4499,16 +4822,22 @@ async function killAll(signal, force) {
|
|
|
4499
4822
|
}
|
|
4500
4823
|
const running = tracked.filter((t) => isTrackedRunning(t));
|
|
4501
4824
|
const stopped = tracked.filter((t) => !isTrackedRunning(t));
|
|
4502
|
-
console.error(
|
|
4503
|
-
|
|
4504
|
-
|
|
4505
|
-
|
|
4825
|
+
console.error(
|
|
4826
|
+
`
|
|
4827
|
+
${pc8.bold(`Kill ${running.length} running + remove ${stopped.length} stopped tracked app(s)?`)}`
|
|
4828
|
+
);
|
|
4829
|
+
console.error(
|
|
4830
|
+
` ${pc8.dim("Permanently removes every Fennec-tracked app \u2014 other processes are untouched.")}
|
|
4831
|
+
`
|
|
4832
|
+
);
|
|
4506
4833
|
const confirmed = force || await confirmPrompt(`${pc8.red("Are you sure?")} ${pc8.dim("This cannot be undone")}`, false);
|
|
4507
4834
|
if (!confirmed) {
|
|
4508
4835
|
console.error(` ${pc8.dim("Cancelled.")}`);
|
|
4509
4836
|
return;
|
|
4510
4837
|
}
|
|
4511
|
-
const spinner = createSpinner(
|
|
4838
|
+
const spinner = createSpinner(
|
|
4839
|
+
`Killing ${running.length} + removing ${stopped.length} tracked app(s)...`
|
|
4840
|
+
);
|
|
4512
4841
|
let killed = 0, failed = 0;
|
|
4513
4842
|
for (const t of running) {
|
|
4514
4843
|
if (killTree(t.pid, signal)) {
|
|
@@ -4549,7 +4878,9 @@ async function stopCommand(args2) {
|
|
|
4549
4878
|
}
|
|
4550
4879
|
return;
|
|
4551
4880
|
}
|
|
4552
|
-
console.error(
|
|
4881
|
+
console.error(
|
|
4882
|
+
renderError("Missing name", "Usage: fennec stop <name|--all> [--group <group>] [-y]")
|
|
4883
|
+
);
|
|
4553
4884
|
process.exit(1);
|
|
4554
4885
|
}
|
|
4555
4886
|
async function stopOne(rawTarget, args2, multi = false) {
|
|
@@ -4573,7 +4904,10 @@ async function stopOne(rawTarget, args2, multi = false) {
|
|
|
4573
4904
|
}
|
|
4574
4905
|
const displayName = `${trackedMatch.name} (PID ${trackedMatch.pid})`;
|
|
4575
4906
|
const force = args2.includes("-y") || args2.includes("--yes");
|
|
4576
|
-
const confirmed = force || await confirmPrompt(
|
|
4907
|
+
const confirmed = force || await confirmPrompt(
|
|
4908
|
+
`Stop ${pc9.bold(displayName)}? ${pc9.dim("It can be re-spawned later via fennec spawn")}`,
|
|
4909
|
+
false
|
|
4910
|
+
);
|
|
4577
4911
|
if (!confirmed) {
|
|
4578
4912
|
console.error(` ${pc9.dim("Cancelled")}`);
|
|
4579
4913
|
return;
|
|
@@ -4585,15 +4919,19 @@ async function stopAllTracked(args2, group) {
|
|
|
4585
4919
|
const tracked = readTracked();
|
|
4586
4920
|
const running = tracked.filter((t) => isTrackedRunning(t) && (!group || t.group === group));
|
|
4587
4921
|
if (running.length === 0) {
|
|
4588
|
-
console.error(
|
|
4922
|
+
console.error(
|
|
4923
|
+
`
|
|
4589
4924
|
${pc9.dim(group ? `No running tracked processes in group "${group}".` : "No running tracked processes to stop.")}
|
|
4590
|
-
`
|
|
4925
|
+
`
|
|
4926
|
+
);
|
|
4591
4927
|
return;
|
|
4592
4928
|
}
|
|
4593
4929
|
const scope = group ? ` group ${pc9.cyan(group)}` : "";
|
|
4594
|
-
console.error(
|
|
4930
|
+
console.error(
|
|
4931
|
+
`
|
|
4595
4932
|
${pc9.yellow("\u26A0")} ${pc9.bold(`Stop all ${running.length} running process(es)${scope}?`)} ${pc9.dim("(They can be re-spawned later)")}
|
|
4596
|
-
`
|
|
4933
|
+
`
|
|
4934
|
+
);
|
|
4597
4935
|
for (const t of running) {
|
|
4598
4936
|
console.error(` ${pc9.green("\u25CF")} ${pc9.bold(t.name)} ${pc9.dim(`(PID ${t.pid})`)}`);
|
|
4599
4937
|
}
|
|
@@ -4620,7 +4958,9 @@ async function stopAllTracked(args2, group) {
|
|
|
4620
4958
|
spinner.stop();
|
|
4621
4959
|
process.stdout.write("\r\x1B[K");
|
|
4622
4960
|
if (stopped > 0) {
|
|
4623
|
-
console.error(
|
|
4961
|
+
console.error(
|
|
4962
|
+
` ${pc9.green("\u2713")} ${pc9.bold(`Stopped ${stopped} process(es)`)} ${pc9.dim("(kept in tracked.json)")}`
|
|
4963
|
+
);
|
|
4624
4964
|
}
|
|
4625
4965
|
if (failed > 0) {
|
|
4626
4966
|
console.error(` ${pc9.red("\u2717")} ${pc9.bold(`${failed} process(es) failed to stop`)}`);
|
|
@@ -4679,8 +5019,13 @@ async function spawnCommand(args2) {
|
|
|
4679
5019
|
const tracked = readTracked();
|
|
4680
5020
|
const inGroup = tracked.filter((t) => t.group === group && !isTrackedRunning(t) && t.command);
|
|
4681
5021
|
if (inGroup.length === 0) {
|
|
4682
|
-
console.error(
|
|
4683
|
-
|
|
5022
|
+
console.error(
|
|
5023
|
+
renderError(
|
|
5024
|
+
"Empty group",
|
|
5025
|
+
`No stopped entries with a saved command in group "${group}".
|
|
5026
|
+
Known groups: ${pc10.cyan(getGroups().join(", ") || "(none)")}`
|
|
5027
|
+
)
|
|
5028
|
+
);
|
|
4684
5029
|
process.exit(1);
|
|
4685
5030
|
}
|
|
4686
5031
|
await spawnAllStopped(inGroup);
|
|
@@ -4705,9 +5050,13 @@ async function spawnOne(name, multi) {
|
|
|
4705
5050
|
process.exit(1);
|
|
4706
5051
|
}
|
|
4707
5052
|
if (isTrackedRunning(match)) {
|
|
4708
|
-
console.error(
|
|
4709
|
-
|
|
4710
|
-
|
|
5053
|
+
console.error(
|
|
5054
|
+
`
|
|
5055
|
+
${pc10.yellow("\u26A0")} ${pc10.bold(name)} ${pc10.dim("is already running (PID:")} ${match.pid}${pc10.dim(")")}`
|
|
5056
|
+
);
|
|
5057
|
+
console.error(
|
|
5058
|
+
` ${pc10.dim("Use")} ${pc10.cyan(`fennec stop ${name}`)} ${pc10.dim("to stop it first.")}`
|
|
5059
|
+
);
|
|
4711
5060
|
console.error();
|
|
4712
5061
|
if (!multi) process.exit(0);
|
|
4713
5062
|
return;
|
|
@@ -4747,7 +5096,14 @@ async function spawnAllStopped(procs) {
|
|
|
4747
5096
|
const logDir = resolve5(homedir3(), ".fennec", "logs");
|
|
4748
5097
|
mkdirSync5(logDir, { recursive: true });
|
|
4749
5098
|
const logFilePath = resolve5(logDir, `${proc.name}.log`);
|
|
4750
|
-
const child = spawnDaemon({
|
|
5099
|
+
const child = spawnDaemon({
|
|
5100
|
+
cmdParts,
|
|
5101
|
+
name: proc.name,
|
|
5102
|
+
cwd: proc.cwd,
|
|
5103
|
+
logFilePath,
|
|
5104
|
+
env: buildSpawnEnv(proc.env),
|
|
5105
|
+
logMode: proc.logMode
|
|
5106
|
+
});
|
|
4751
5107
|
const pid = child.pid ?? 0;
|
|
4752
5108
|
addTracked({
|
|
4753
5109
|
name: proc.name,
|
|
@@ -4759,7 +5115,10 @@ async function spawnAllStopped(procs) {
|
|
|
4759
5115
|
env: proc.env,
|
|
4760
5116
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4761
5117
|
autoRestart: proc.autoRestart,
|
|
4762
|
-
logMode: proc.logMode
|
|
5118
|
+
logMode: proc.logMode,
|
|
5119
|
+
// Preserve the logical group across stop -> spawn (addTracked REPLACES
|
|
5120
|
+
// the entry by name, so group must be carried over explicitly).
|
|
5121
|
+
group: proc.group
|
|
4763
5122
|
});
|
|
4764
5123
|
if (proc.autoRestart) {
|
|
4765
5124
|
ensureSupervisorRunning();
|
|
@@ -4786,7 +5145,9 @@ async function spawnList() {
|
|
|
4786
5145
|
if (tracked.length === 0) {
|
|
4787
5146
|
console.error(`
|
|
4788
5147
|
${pc10.dim("No tracked processes found.")}`);
|
|
4789
|
-
console.error(
|
|
5148
|
+
console.error(
|
|
5149
|
+
` ${pc10.dim("Start an app first:")} ${pc10.cyan("fennec start <command> --name <name>")}`
|
|
5150
|
+
);
|
|
4790
5151
|
console.error();
|
|
4791
5152
|
return;
|
|
4792
5153
|
}
|
|
@@ -4798,8 +5159,10 @@ async function spawnList() {
|
|
|
4798
5159
|
console.error();
|
|
4799
5160
|
return;
|
|
4800
5161
|
}
|
|
4801
|
-
console.error(
|
|
4802
|
-
|
|
5162
|
+
console.error(
|
|
5163
|
+
`
|
|
5164
|
+
${symbols.fox} ${pc10.bold("Available to spawn")} ${pc10.dim(`(${stopped.length}/${tracked.length} stopped)`)}`
|
|
5165
|
+
);
|
|
4803
5166
|
if (running.length > 0) {
|
|
4804
5167
|
console.error(` ${pc10.dim("Running (use stop first):")}`);
|
|
4805
5168
|
for (const r of running) {
|
|
@@ -4807,11 +5170,14 @@ async function spawnList() {
|
|
|
4807
5170
|
}
|
|
4808
5171
|
console.error();
|
|
4809
5172
|
}
|
|
4810
|
-
const selected = await selectPrompt(
|
|
4811
|
-
|
|
4812
|
-
|
|
4813
|
-
|
|
4814
|
-
|
|
5173
|
+
const selected = await selectPrompt(
|
|
5174
|
+
"Select a process to spawn:",
|
|
5175
|
+
stopped.map((t) => ({
|
|
5176
|
+
value: t.name,
|
|
5177
|
+
label: t.name,
|
|
5178
|
+
description: t.command.length > 80 ? t.command.slice(0, 80) + "..." : t.command
|
|
5179
|
+
}))
|
|
5180
|
+
);
|
|
4815
5181
|
if (selected === null) {
|
|
4816
5182
|
console.error(` ${pc10.dim("Cancelled")}`);
|
|
4817
5183
|
return;
|
|
@@ -4826,14 +5192,23 @@ async function respawnProcess(proc) {
|
|
|
4826
5192
|
const logDir = resolve5(homedir3(), ".fennec", "logs");
|
|
4827
5193
|
mkdirSync5(logDir, { recursive: true });
|
|
4828
5194
|
const logFilePath = resolve5(logDir, `${proc.name}.log`);
|
|
4829
|
-
console.error(
|
|
5195
|
+
console.error(
|
|
5196
|
+
`
|
|
4830
5197
|
${symbols.fox} ${pc10.bold("Spawning")} ${renderAppName(proc.name)} ${pc10.dim("(from saved config)")}
|
|
4831
|
-
`
|
|
5198
|
+
`
|
|
5199
|
+
);
|
|
4832
5200
|
console.error(` ${renderKV("Command", proc.command)}`);
|
|
4833
5201
|
if (proc.cwd) console.error(` ${renderKV("Directory", proc.cwd)}`);
|
|
4834
5202
|
if (proc.port) console.error(` ${renderKV("Port", String(proc.port))}`);
|
|
4835
5203
|
try {
|
|
4836
|
-
const child = spawnDaemon({
|
|
5204
|
+
const child = spawnDaemon({
|
|
5205
|
+
cmdParts,
|
|
5206
|
+
name: proc.name,
|
|
5207
|
+
cwd: proc.cwd,
|
|
5208
|
+
logFilePath,
|
|
5209
|
+
env: buildSpawnEnv(proc.env),
|
|
5210
|
+
logMode: proc.logMode
|
|
5211
|
+
});
|
|
4837
5212
|
const pid = child.pid ?? 0;
|
|
4838
5213
|
addTracked({
|
|
4839
5214
|
name: proc.name,
|
|
@@ -4845,7 +5220,8 @@ async function respawnProcess(proc) {
|
|
|
4845
5220
|
env: proc.env,
|
|
4846
5221
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4847
5222
|
autoRestart: proc.autoRestart,
|
|
4848
|
-
logMode: proc.logMode
|
|
5223
|
+
logMode: proc.logMode,
|
|
5224
|
+
group: proc.group
|
|
4849
5225
|
});
|
|
4850
5226
|
if (proc.autoRestart) {
|
|
4851
5227
|
ensureSupervisorRunning();
|
|
@@ -4876,7 +5252,12 @@ async function restartCommand(args2) {
|
|
|
4876
5252
|
await restartOne(target.value, force, false);
|
|
4877
5253
|
return;
|
|
4878
5254
|
}
|
|
4879
|
-
console.error(
|
|
5255
|
+
console.error(
|
|
5256
|
+
renderError(
|
|
5257
|
+
"Missing process name/pid",
|
|
5258
|
+
"Usage: fennec restart <name|pid> [-y] [--group <group>]"
|
|
5259
|
+
)
|
|
5260
|
+
);
|
|
4880
5261
|
process.exit(1);
|
|
4881
5262
|
}
|
|
4882
5263
|
async function restartOne(raw, force, multi) {
|
|
@@ -4916,7 +5297,14 @@ fennec restart only re-spawns Fennec-tracked apps (from their saved config).`;
|
|
|
4916
5297
|
try {
|
|
4917
5298
|
const cmdParts = resolveArgs(trackedEntry);
|
|
4918
5299
|
const logFilePath = logFilePathFor(trackedEntry.name);
|
|
4919
|
-
const child = spawnDaemon({
|
|
5300
|
+
const child = spawnDaemon({
|
|
5301
|
+
cmdParts,
|
|
5302
|
+
name: trackedEntry.name,
|
|
5303
|
+
cwd: trackedEntry.cwd,
|
|
5304
|
+
logFilePath,
|
|
5305
|
+
env: buildSpawnEnv(trackedEntry.env),
|
|
5306
|
+
logMode: trackedEntry.logMode
|
|
5307
|
+
});
|
|
4920
5308
|
removeTrackedByPid(targetPid);
|
|
4921
5309
|
addTracked({
|
|
4922
5310
|
name: trackedEntry.name,
|
|
@@ -4928,23 +5316,34 @@ fennec restart only re-spawns Fennec-tracked apps (from their saved config).`;
|
|
|
4928
5316
|
env: trackedEntry.env,
|
|
4929
5317
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4930
5318
|
autoRestart: trackedEntry.autoRestart,
|
|
4931
|
-
logMode: trackedEntry.logMode
|
|
5319
|
+
logMode: trackedEntry.logMode,
|
|
5320
|
+
group: trackedEntry.group
|
|
4932
5321
|
});
|
|
4933
5322
|
respawnSpinner.succeed(`${trackedEntry.name} restarted (PID: ${child.pid})`);
|
|
4934
5323
|
} catch (error) {
|
|
4935
5324
|
respawnSpinner.fail(`Failed to re-spawn ${trackedEntry.name}: ${String(error)}`);
|
|
4936
|
-
console.error(
|
|
5325
|
+
console.error(
|
|
5326
|
+
` ${pc11.dim("Config preserved. Re-run manually:")} ${renderCommand(trackedEntry.command)}`
|
|
5327
|
+
);
|
|
4937
5328
|
}
|
|
4938
5329
|
}
|
|
4939
5330
|
async function restartGroup(group, force) {
|
|
4940
5331
|
const tracked = readTracked();
|
|
4941
5332
|
const inGroup = tracked.filter((t) => t.group === group);
|
|
4942
5333
|
if (inGroup.length === 0) {
|
|
4943
|
-
console.error(
|
|
4944
|
-
|
|
5334
|
+
console.error(
|
|
5335
|
+
renderError(
|
|
5336
|
+
"Empty group",
|
|
5337
|
+
`No tracked entries in group "${group}".
|
|
5338
|
+
Known groups: ${pc11.cyan(getGroups().join(", ") || "(none)")}`
|
|
5339
|
+
)
|
|
5340
|
+
);
|
|
4945
5341
|
process.exit(1);
|
|
4946
5342
|
}
|
|
4947
|
-
const confirmed = force || await confirmPrompt(
|
|
5343
|
+
const confirmed = force || await confirmPrompt(
|
|
5344
|
+
`Restart ${pc11.bold(`${inGroup.length}`)} process(es) in group ${pc11.cyan(group)}?`,
|
|
5345
|
+
false
|
|
5346
|
+
);
|
|
4948
5347
|
if (!confirmed) {
|
|
4949
5348
|
console.error(` ${pc11.dim("Cancelled")}`);
|
|
4950
5349
|
return;
|
|
@@ -4954,7 +5353,14 @@ Known groups: ${pc11.cyan(getGroups().join(", ") || "(none)")}`));
|
|
|
4954
5353
|
try {
|
|
4955
5354
|
const cmdParts = resolveArgs(trackedEntry);
|
|
4956
5355
|
const logFilePath = logFilePathFor(trackedEntry.name);
|
|
4957
|
-
const child = spawnDaemon({
|
|
5356
|
+
const child = spawnDaemon({
|
|
5357
|
+
cmdParts,
|
|
5358
|
+
name: trackedEntry.name,
|
|
5359
|
+
cwd: trackedEntry.cwd,
|
|
5360
|
+
logFilePath,
|
|
5361
|
+
env: buildSpawnEnv(trackedEntry.env),
|
|
5362
|
+
logMode: trackedEntry.logMode
|
|
5363
|
+
});
|
|
4958
5364
|
removeTrackedByPid(trackedEntry.pid);
|
|
4959
5365
|
addTracked({
|
|
4960
5366
|
name: trackedEntry.name,
|
|
@@ -4962,6 +5368,7 @@ Known groups: ${pc11.cyan(getGroups().join(", ") || "(none)")}`));
|
|
|
4962
5368
|
command: trackedEntry.command,
|
|
4963
5369
|
args: cmdParts,
|
|
4964
5370
|
port: trackedEntry.port,
|
|
5371
|
+
group: trackedEntry.group,
|
|
4965
5372
|
cwd: trackedEntry.cwd,
|
|
4966
5373
|
env: trackedEntry.env,
|
|
4967
5374
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -4971,7 +5378,9 @@ Known groups: ${pc11.cyan(getGroups().join(", ") || "(none)")}`));
|
|
|
4971
5378
|
spinner.succeed(`${trackedEntry.name} restarted (PID: ${child.pid})`);
|
|
4972
5379
|
} catch (error) {
|
|
4973
5380
|
spinner.fail(`Failed to re-spawn ${trackedEntry.name}: ${String(error)}`);
|
|
4974
|
-
console.error(
|
|
5381
|
+
console.error(
|
|
5382
|
+
` ${pc11.dim("Config preserved. Re-run manually:")} ${renderCommand(trackedEntry.command)}`
|
|
5383
|
+
);
|
|
4975
5384
|
}
|
|
4976
5385
|
}
|
|
4977
5386
|
}
|
|
@@ -4981,7 +5390,9 @@ import pc12 from "picocolors";
|
|
|
4981
5390
|
function adoptCommand(args2) {
|
|
4982
5391
|
const pidArg = args2.find((a) => /^\d+$/.test(a));
|
|
4983
5392
|
if (!pidArg) {
|
|
4984
|
-
console.error(
|
|
5393
|
+
console.error(
|
|
5394
|
+
renderError("Missing PID", `Usage: fennec adopt <pid> [--name <name>] [--port <port>]`)
|
|
5395
|
+
);
|
|
4985
5396
|
process.exit(1);
|
|
4986
5397
|
}
|
|
4987
5398
|
const pid = parseInt(pidArg, 10);
|
|
@@ -4994,8 +5405,10 @@ function adoptCommand(args2) {
|
|
|
4994
5405
|
console.error(renderError("Cannot adopt", `No running process with PID ${pid}`));
|
|
4995
5406
|
process.exit(1);
|
|
4996
5407
|
}
|
|
4997
|
-
console.error(
|
|
4998
|
-
|
|
5408
|
+
console.error(
|
|
5409
|
+
`
|
|
5410
|
+
${symbols.fox} ${pc12.green("\u2713")} ${pc12.bold("Adopted")} ${pc12.bold(entry.name)} ${pc12.dim(`(PID ${pid})`)}`
|
|
5411
|
+
);
|
|
4999
5412
|
if (entry.port) console.error(` ${pc12.dim("port")} ${pc12.yellow(`:${entry.port}`)}`);
|
|
5000
5413
|
if (entry.command) console.error(` ${pc12.dim("command")} ${entry.command}`);
|
|
5001
5414
|
console.error(` ${pc12.dim("status")} now tracked + supervised by the supervisor`);
|
|
@@ -5015,7 +5428,10 @@ var SECRET_PATTERNS = [
|
|
|
5015
5428
|
// Authorization: <scheme> <token>
|
|
5016
5429
|
{ name: "authorization", re: /\bAuthorization\s*:\s*\S+/gi },
|
|
5017
5430
|
// Generic API keys
|
|
5018
|
-
{
|
|
5431
|
+
{
|
|
5432
|
+
name: "apikey",
|
|
5433
|
+
re: /\b(api[_-]?key|apikey|access[_-]?key|secret[_-]?key|private[_-]?key)\s*[:=]\s*\S+/gi
|
|
5434
|
+
},
|
|
5019
5435
|
// AWS
|
|
5020
5436
|
{ name: "aws", re: /\b(AKIA|ASIA)[0-9A-Z]{16}\b/g },
|
|
5021
5437
|
// Slack tokens
|
|
@@ -5031,9 +5447,15 @@ var SECRET_PATTERNS = [
|
|
|
5031
5447
|
// Generic long hex/alpha tokens (>=32 chars) in common contexts
|
|
5032
5448
|
{ name: "token", re: /\b(token|secret|password|passwd|pwd|client[_-]?secret)\s*[:=]\s*\S+/gi },
|
|
5033
5449
|
// Connection strings with credentials
|
|
5034
|
-
{
|
|
5450
|
+
{
|
|
5451
|
+
name: "connstr",
|
|
5452
|
+
re: /\b(mongodb(\+srv)?|postgres(ql)?|mysql|redis|amqp|sqlserver):\/\/[^\s:]+:[^\s@]+@/gi
|
|
5453
|
+
},
|
|
5035
5454
|
// Private key blocks
|
|
5036
|
-
{
|
|
5455
|
+
{
|
|
5456
|
+
name: "pem",
|
|
5457
|
+
re: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g
|
|
5458
|
+
}
|
|
5037
5459
|
];
|
|
5038
5460
|
function redactLine(line, opts = {}) {
|
|
5039
5461
|
if (opts.enabled === false) return line;
|
|
@@ -5151,17 +5573,27 @@ async function inspectCommand(args2) {
|
|
|
5151
5573
|
process.exit(1);
|
|
5152
5574
|
}
|
|
5153
5575
|
const tracked = readTracked();
|
|
5154
|
-
const proc = tracked.find(
|
|
5576
|
+
const proc = tracked.find(
|
|
5577
|
+
(t) => t.name === target || parseInt(target, 10) === t.pid && !isNaN(parseInt(target, 10))
|
|
5578
|
+
);
|
|
5155
5579
|
if (!proc) {
|
|
5156
5580
|
console.error(pc13.red(`No tracked app named "${target}".`));
|
|
5157
5581
|
process.exit(1);
|
|
5158
5582
|
}
|
|
5159
5583
|
const running = isTrackedRunning(proc);
|
|
5160
5584
|
const logPath = logFilePathFor(proc.name);
|
|
5161
|
-
const lines = readLogLines(logPath, {
|
|
5585
|
+
const lines = readLogLines(logPath, {
|
|
5586
|
+
tail: Math.max(tail, sinceMs ? MAX_TAIL : 0),
|
|
5587
|
+
sinceMs: sinceMs ?? void 0,
|
|
5588
|
+
redact: true,
|
|
5589
|
+
parseTimestamp: true
|
|
5590
|
+
});
|
|
5162
5591
|
const recent = lines.slice(-tail);
|
|
5163
5592
|
const errorLines = lines.filter((l) => classifyLevel(l) === "error").slice(-15);
|
|
5164
|
-
const redactedLines = recent.reduce(
|
|
5593
|
+
const redactedLines = recent.reduce(
|
|
5594
|
+
(acc, l) => acc + (redactionCount(stripAnsi(l)) > 0 ? 1 : 0),
|
|
5595
|
+
0
|
|
5596
|
+
);
|
|
5165
5597
|
const metrics = running ? readMetrics(proc.pid) : {};
|
|
5166
5598
|
let portHealthy = null;
|
|
5167
5599
|
if (proc.port) {
|
|
@@ -5176,13 +5608,18 @@ async function inspectCommand(args2) {
|
|
|
5176
5608
|
if (plain) {
|
|
5177
5609
|
const dot = running ? pc13.green("\u25CF") : pc13.red("\u25CB");
|
|
5178
5610
|
const portStr = proc.port ? ` :${proc.port}${portHealthy === false ? pc13.red(" (port down!)") : portHealthy ? pc13.green(" (up)") : ""}` : "";
|
|
5179
|
-
console.error(
|
|
5180
|
-
|
|
5181
|
-
|
|
5611
|
+
console.error(
|
|
5612
|
+
`
|
|
5613
|
+
${symbols.fox} ${renderAppName(proc.name)}${portStr} ${dot} ${running ? pc13.green(`running PID ${proc.pid}`) : pc13.red("stopped")}`
|
|
5614
|
+
);
|
|
5615
|
+
console.error(
|
|
5616
|
+
` ${pc13.dim("uptime")} ${pc13.cyan(uptimeSec ? formatUptime(uptimeSec) : "\u2014")} ${pc13.dim("rss")} ${pc13.cyan(metrics.rssMb ? `${metrics.rssMb}MB` : "\u2014")} ${pc13.dim("errors(last)")} ${pc13.cyan(String(errorLines.length))} ${pc13.dim("redacted")} ${pc13.cyan(String(redactedLines))}`
|
|
5617
|
+
);
|
|
5182
5618
|
if (errorLines.length) {
|
|
5183
5619
|
console.error(`
|
|
5184
5620
|
${pc13.bold(pc13.red("Recent errors:"))}`);
|
|
5185
|
-
for (const l of errorLines.slice(-8))
|
|
5621
|
+
for (const l of errorLines.slice(-8))
|
|
5622
|
+
console.error(` ${pc13.red(stripAnsi(l).slice(0, 300))}`);
|
|
5186
5623
|
}
|
|
5187
5624
|
console.error();
|
|
5188
5625
|
return;
|
|
@@ -5330,7 +5767,9 @@ async function devCommand(args2) {
|
|
|
5330
5767
|
return devRestart(args2.slice(1));
|
|
5331
5768
|
}
|
|
5332
5769
|
if (sub !== "up") {
|
|
5333
|
-
console.error(
|
|
5770
|
+
console.error(
|
|
5771
|
+
renderError("Unknown sub-command", `"${sub}" \u2014 use: up | down | status | restart`)
|
|
5772
|
+
);
|
|
5334
5773
|
process.exit(1);
|
|
5335
5774
|
}
|
|
5336
5775
|
const config = loadConfig(configPath);
|
|
@@ -5348,14 +5787,18 @@ async function devCommand(args2) {
|
|
|
5348
5787
|
if (p) {
|
|
5349
5788
|
const prev = portOwner.get(p);
|
|
5350
5789
|
if (prev) {
|
|
5351
|
-
console.error(
|
|
5790
|
+
console.error(
|
|
5791
|
+
renderError("Port conflict", `apps "${prev}" and "${a.name}" both declare port :${p}`)
|
|
5792
|
+
);
|
|
5352
5793
|
process.exit(1);
|
|
5353
5794
|
}
|
|
5354
5795
|
portOwner.set(p, a.name);
|
|
5355
5796
|
}
|
|
5356
5797
|
}
|
|
5357
|
-
console.error(
|
|
5358
|
-
|
|
5798
|
+
console.error(
|
|
5799
|
+
`
|
|
5800
|
+
${symbols.fox} ${pc14.bold("fennec dev up")} ${pc14.dim(`\u2014 ${apps.length} app(s) from ${configPath}`)}`
|
|
5801
|
+
);
|
|
5359
5802
|
ensureSupervisorRunning();
|
|
5360
5803
|
ensurePersistEnabled();
|
|
5361
5804
|
for (const app of ordered) {
|
|
@@ -5366,7 +5809,10 @@ async function devCommand(args2) {
|
|
|
5366
5809
|
const sp = createSpinner(`Waiting for ${dep} :${port}...`);
|
|
5367
5810
|
const ok = await waitForPort(port);
|
|
5368
5811
|
sp.stop();
|
|
5369
|
-
if (!ok)
|
|
5812
|
+
if (!ok)
|
|
5813
|
+
console.error(
|
|
5814
|
+
` ${pc14.yellow("\u26A0")} ${pc14.dim(`${dep} :${port} not ready in time (continuing)`)}`
|
|
5815
|
+
);
|
|
5370
5816
|
}
|
|
5371
5817
|
}
|
|
5372
5818
|
const res = await startApp(app, baseDir);
|
|
@@ -5374,8 +5820,10 @@ async function devCommand(args2) {
|
|
|
5374
5820
|
const tag = res === "skipped" ? pc14.dim("already running (skipped)") : res === "restarted" ? pc14.yellow(`restarted (PID ${pid})`) : res === "adopted" ? pc14.cyan(`adopted external process on :${app.port ?? app.waitForPort}`) : pc14.dim(`started (PID ${pid})`);
|
|
5375
5821
|
console.error(` ${pc14.green("\u2713")} ${pc14.bold(app.name)} ${tag}`);
|
|
5376
5822
|
}
|
|
5377
|
-
console.error(
|
|
5378
|
-
|
|
5823
|
+
console.error(
|
|
5824
|
+
`
|
|
5825
|
+
${pc14.green("\u2713")} Stack up. Observe with ${pc14.cyan("fennec observe")} or ${pc14.cyan("fennec dev status")}.`
|
|
5826
|
+
);
|
|
5379
5827
|
console.error(` ${pc14.dim("Apps auto-restart (supervisor) + survive reboot (persist).")}
|
|
5380
5828
|
`);
|
|
5381
5829
|
}
|
|
@@ -5387,8 +5835,10 @@ function devDown() {
|
|
|
5387
5835
|
`);
|
|
5388
5836
|
return;
|
|
5389
5837
|
}
|
|
5390
|
-
console.error(
|
|
5391
|
-
|
|
5838
|
+
console.error(
|
|
5839
|
+
`
|
|
5840
|
+
${symbols.fox} ${pc14.bold("fennec dev down")} ${pc14.dim(`\u2014 stopping ${tracked.length} app(s)`)}`
|
|
5841
|
+
);
|
|
5392
5842
|
for (const t of tracked) {
|
|
5393
5843
|
try {
|
|
5394
5844
|
if (isTrackedRunning(t)) killTree(t.pid, "SIGTERM");
|
|
@@ -5398,9 +5848,11 @@ function devDown() {
|
|
|
5398
5848
|
addTracked({ ...t, autoRestart: false });
|
|
5399
5849
|
console.error(` ${pc14.red("\u25A0")} ${pc14.bold(t.name)}`);
|
|
5400
5850
|
}
|
|
5401
|
-
console.error(
|
|
5851
|
+
console.error(
|
|
5852
|
+
`
|
|
5402
5853
|
${pc14.green("\u2713")} Stack stopped (auto-restart disabled). Use ${pc14.cyan("fennec dev up")} to bring it back.
|
|
5403
|
-
`
|
|
5854
|
+
`
|
|
5855
|
+
);
|
|
5404
5856
|
}
|
|
5405
5857
|
function devRestart(names) {
|
|
5406
5858
|
ensureSupervisorRunning();
|
|
@@ -5409,7 +5861,12 @@ function devRestart(names) {
|
|
|
5409
5861
|
const targets = names.length ? tracked.filter((t) => names.includes(t.name)) : tracked.filter((t) => t.autoRestart);
|
|
5410
5862
|
const missing = names.filter((n) => !tracked.some((t) => t.name === n));
|
|
5411
5863
|
if (missing.length) {
|
|
5412
|
-
console.error(
|
|
5864
|
+
console.error(
|
|
5865
|
+
renderError(
|
|
5866
|
+
"Not tracked",
|
|
5867
|
+
`No such app(s): ${missing.join(", ")} \u2014 run "fennec dev status" to list tracked apps.`
|
|
5868
|
+
)
|
|
5869
|
+
);
|
|
5413
5870
|
}
|
|
5414
5871
|
if (targets.length === 0) {
|
|
5415
5872
|
console.error(`
|
|
@@ -5417,14 +5874,18 @@ function devRestart(names) {
|
|
|
5417
5874
|
`);
|
|
5418
5875
|
return;
|
|
5419
5876
|
}
|
|
5420
|
-
console.error(
|
|
5421
|
-
|
|
5877
|
+
console.error(
|
|
5878
|
+
`
|
|
5879
|
+
${symbols.fox} ${pc14.bold("fennec dev restart")} ${pc14.dim(`\u2014 ${targets.length} app(s)`)}`
|
|
5880
|
+
);
|
|
5422
5881
|
for (const t of targets) {
|
|
5423
5882
|
try {
|
|
5424
5883
|
const newPid = respawnTracked(t, "manual");
|
|
5425
5884
|
console.error(` ${pc14.green("\u2713")} ${pc14.bold(t.name)} ${pc14.dim(`restarted (PID ${newPid})`)}`);
|
|
5426
5885
|
} catch (err) {
|
|
5427
|
-
console.error(
|
|
5886
|
+
console.error(
|
|
5887
|
+
` ${pc14.red("\u2717")} ${pc14.bold(t.name)} ${pc14.dim(`restart failed: ${String(err)}`)}`
|
|
5888
|
+
);
|
|
5428
5889
|
}
|
|
5429
5890
|
}
|
|
5430
5891
|
console.error();
|
|
@@ -5470,7 +5931,15 @@ function topoOrder(apps) {
|
|
|
5470
5931
|
}
|
|
5471
5932
|
|
|
5472
5933
|
// src/commands/log.ts
|
|
5473
|
-
import {
|
|
5934
|
+
import {
|
|
5935
|
+
existsSync as existsSync9,
|
|
5936
|
+
unlinkSync as unlinkSync3,
|
|
5937
|
+
statSync as statSync3,
|
|
5938
|
+
openSync as openSync3,
|
|
5939
|
+
fstatSync,
|
|
5940
|
+
readSync,
|
|
5941
|
+
closeSync as closeSync3
|
|
5942
|
+
} from "fs";
|
|
5474
5943
|
import { execSync as execSync2 } from "child_process";
|
|
5475
5944
|
import pc15 from "picocolors";
|
|
5476
5945
|
var MAX_LOG_LINES = 500;
|
|
@@ -5494,14 +5963,21 @@ async function logCommand(args2) {
|
|
|
5494
5963
|
process.exit(1);
|
|
5495
5964
|
}
|
|
5496
5965
|
if (levelFilter && !["error", "warn", "info", "debug"].includes(levelFilter)) {
|
|
5497
|
-
console.error(
|
|
5966
|
+
console.error(
|
|
5967
|
+
renderError(
|
|
5968
|
+
"Invalid level",
|
|
5969
|
+
`"${levelFilter}" is not a valid level. Use: error, warn, info, debug`
|
|
5970
|
+
)
|
|
5971
|
+
);
|
|
5498
5972
|
process.exit(1);
|
|
5499
5973
|
}
|
|
5500
5974
|
const redact = !noRedact;
|
|
5501
5975
|
let logFilePath = null;
|
|
5502
5976
|
let displayName = target;
|
|
5503
5977
|
const tracked = readTracked();
|
|
5504
|
-
const trackedMatch = tracked.find(
|
|
5978
|
+
const trackedMatch = tracked.find(
|
|
5979
|
+
(t) => t.name === target || parseInt(target, 10) === t.pid && !isNaN(parseInt(target, 10))
|
|
5980
|
+
);
|
|
5505
5981
|
if (trackedMatch) {
|
|
5506
5982
|
displayName = trackedMatch.name;
|
|
5507
5983
|
logFilePath = logFilePathFor(trackedMatch.name);
|
|
@@ -5514,16 +5990,20 @@ async function logCommand(args2) {
|
|
|
5514
5990
|
}
|
|
5515
5991
|
if (clearFlag) {
|
|
5516
5992
|
if (!logFilePath || !existsSync9(logFilePath)) {
|
|
5517
|
-
console.error(
|
|
5993
|
+
console.error(
|
|
5994
|
+
`
|
|
5518
5995
|
${pc15.yellow("\u26A0")} ${pc15.dim("No log file found for")} ${pc15.bold(displayName)}
|
|
5519
|
-
`
|
|
5996
|
+
`
|
|
5997
|
+
);
|
|
5520
5998
|
process.exit(0);
|
|
5521
5999
|
}
|
|
5522
6000
|
try {
|
|
5523
6001
|
unlinkSync3(logFilePath);
|
|
5524
|
-
console.error(
|
|
6002
|
+
console.error(
|
|
6003
|
+
`
|
|
5525
6004
|
${pc15.green("\u2713")} ${pc15.bold("Log cleared")} ${pc15.dim(`\u2014 ${logFilePath}`)}
|
|
5526
|
-
`
|
|
6005
|
+
`
|
|
6006
|
+
);
|
|
5527
6007
|
} catch (err) {
|
|
5528
6008
|
console.error(renderError("Failed to clear log", String(err)));
|
|
5529
6009
|
process.exit(1);
|
|
@@ -5543,7 +6023,10 @@ async function logCommand(args2) {
|
|
|
5543
6023
|
const pid = trackedMatch?.pid ?? parseInt(target, 10);
|
|
5544
6024
|
if (!isNaN(pid)) {
|
|
5545
6025
|
try {
|
|
5546
|
-
const output = execSync2(
|
|
6026
|
+
const output = execSync2(
|
|
6027
|
+
`journalctl --no-pager -n ${Math.max(lineCount, 500)} _PID=${pid} 2>/dev/null || echo ""`,
|
|
6028
|
+
{ encoding: "utf-8", timeout: 3e3 }
|
|
6029
|
+
);
|
|
5547
6030
|
logLines = output.trim().split("\n").filter(Boolean).map((l) => redact ? redactLine(stripAnsi(l)) : stripAnsi(l));
|
|
5548
6031
|
} catch {
|
|
5549
6032
|
logLines = ["(no logs available)"];
|
|
@@ -5558,7 +6041,10 @@ async function logCommand(args2) {
|
|
|
5558
6041
|
}
|
|
5559
6042
|
const running = trackedMatch ? isTrackedRunning(trackedMatch) : false;
|
|
5560
6043
|
const sliced = logLines.slice(-lineCount);
|
|
5561
|
-
const redactedHits = sliced.reduce(
|
|
6044
|
+
const redactedHits = sliced.reduce(
|
|
6045
|
+
(acc, l) => acc + (redactionCount(stripAnsi(l)) > 0 ? 1 : 0),
|
|
6046
|
+
0
|
|
6047
|
+
);
|
|
5562
6048
|
const payload = {
|
|
5563
6049
|
app: displayName,
|
|
5564
6050
|
running,
|
|
@@ -5580,7 +6066,9 @@ async function logCommand(args2) {
|
|
|
5580
6066
|
console.error(`
|
|
5581
6067
|
${symbols.fox} ${renderAppName(displayName)}${portStr} \u2014 ${statusStr}`);
|
|
5582
6068
|
if (!running) {
|
|
5583
|
-
console.error(
|
|
6069
|
+
console.error(
|
|
6070
|
+
` ${pc15.dim("This app is not running. Showing last captured logs.")} ${pc15.cyan(`fennec spawn ${displayName}`)} ${pc15.dim("to restart.")}`
|
|
6071
|
+
);
|
|
5584
6072
|
}
|
|
5585
6073
|
}
|
|
5586
6074
|
const spinner = createSpinner(`Reading logs for ${displayName}...`);
|
|
@@ -5597,7 +6085,10 @@ async function logCommand(args2) {
|
|
|
5597
6085
|
const pid = trackedMatch?.pid ?? parseInt(target, 10);
|
|
5598
6086
|
if (!isNaN(pid)) {
|
|
5599
6087
|
try {
|
|
5600
|
-
const output = execSync2(
|
|
6088
|
+
const output = execSync2(
|
|
6089
|
+
`journalctl --no-pager -n ${Math.max(lineCount, 500)} _PID=${pid} 2>/dev/null || echo ""`,
|
|
6090
|
+
{ encoding: "utf-8", timeout: 3e3 }
|
|
6091
|
+
);
|
|
5601
6092
|
logLines = output.trim().split("\n").filter(Boolean).map((l) => redact ? redactLine(stripAnsi(l)) : stripAnsi(l));
|
|
5602
6093
|
} catch {
|
|
5603
6094
|
logLines = ["(no logs available)"];
|
|
@@ -5612,8 +6103,10 @@ async function logCommand(args2) {
|
|
|
5612
6103
|
}
|
|
5613
6104
|
spinner.stop();
|
|
5614
6105
|
process.stdout.write("\r\x1B[K");
|
|
5615
|
-
console.error(
|
|
5616
|
-
|
|
6106
|
+
console.error(
|
|
6107
|
+
`
|
|
6108
|
+
${symbols.fox} ${pc15.bold("Logs")} ${renderAppName(displayName)} ${pc15.dim(`(last ${logLines.length} line${logLines.length !== 1 ? "s" : ""})${redact ? pc15.dim(" \xB7 secrets redacted") : ""})`)}`
|
|
6109
|
+
);
|
|
5617
6110
|
const sliced = logLines.slice(-lineCount);
|
|
5618
6111
|
for (const line of sliced) {
|
|
5619
6112
|
const display = line.length > 300 ? line.slice(0, 300) + "\u2026" : line;
|
|
@@ -5639,7 +6132,7 @@ async function logCommand(args2) {
|
|
|
5639
6132
|
async function followLog(logFilePath, opts) {
|
|
5640
6133
|
let lastSize = 0;
|
|
5641
6134
|
try {
|
|
5642
|
-
lastSize =
|
|
6135
|
+
lastSize = statSync3(logFilePath).size;
|
|
5643
6136
|
} catch {
|
|
5644
6137
|
}
|
|
5645
6138
|
console.error(`
|
|
@@ -5703,8 +6196,10 @@ function listAvailableApps() {
|
|
|
5703
6196
|
if (tracked.length === 0) {
|
|
5704
6197
|
console.error(`
|
|
5705
6198
|
${symbols.fox} ${pc15.bold("No tracked apps to show logs for.")}`);
|
|
5706
|
-
console.error(
|
|
5707
|
-
`)
|
|
6199
|
+
console.error(
|
|
6200
|
+
` ${pc15.dim("Start one with:")} ${pc15.cyan("fennec start <command> --name <name>")}
|
|
6201
|
+
`
|
|
6202
|
+
);
|
|
5708
6203
|
return;
|
|
5709
6204
|
}
|
|
5710
6205
|
console.error(`
|
|
@@ -5716,12 +6211,18 @@ function listAvailableApps() {
|
|
|
5716
6211
|
const state = running ? pc15.green(`running (PID ${t.pid})`) : pc15.red("stopped");
|
|
5717
6212
|
const portStr = t.port ? ` ${pc15.yellow(`:${t.port}`)}` : "";
|
|
5718
6213
|
console.error(` ${dot} ${pc15.bold(t.name)}${portStr} ${pc15.dim("\u2014")} ${state}`);
|
|
5719
|
-
console.error(
|
|
6214
|
+
console.error(
|
|
6215
|
+
` ${pc15.cyan(`fennec log ${t.name}`)} ${pc15.dim("\xB7")} ${pc15.cyan(`fennec log ${t.name} -f`)}`
|
|
6216
|
+
);
|
|
5720
6217
|
}
|
|
5721
|
-
console.error(
|
|
5722
|
-
|
|
5723
|
-
|
|
5724
|
-
|
|
6218
|
+
console.error(
|
|
6219
|
+
`
|
|
6220
|
+
${pc15.dim("Flags:")} ${pc15.cyan("-f")} ${pc15.dim("follow")} ${pc15.dim("\xB7")} ${pc15.cyan("--lines N")} ${pc15.dim("\xB7")} ${pc15.cyan("--since 10m|1h")} ${pc15.dim("\xB7")} ${pc15.cyan("--level error|warn|info|debug")} ${pc15.dim("\xB7")} ${pc15.cyan("--json")} ${pc15.dim("(AI)")} ${pc15.dim("\xB7")} ${pc15.cyan("--no-redact")} ${pc15.dim("\xB7")} ${pc15.cyan("--clear")}`
|
|
6221
|
+
);
|
|
6222
|
+
console.error(
|
|
6223
|
+
` ${pc15.dim("AI mode:")} ${pc15.cyan("fennec log <app> --json --since 10m")} ${pc15.dim("\u2014 bounded, redacted, machine-readable. Secrets are redacted by default.")}
|
|
6224
|
+
`
|
|
6225
|
+
);
|
|
5725
6226
|
}
|
|
5726
6227
|
|
|
5727
6228
|
// src/commands/attach.ts
|
|
@@ -5791,9 +6292,11 @@ async function storeCommand(args2) {
|
|
|
5791
6292
|
mgr.ensure();
|
|
5792
6293
|
if (!kind) {
|
|
5793
6294
|
const scan = mgr.scan();
|
|
5794
|
-
console.error(
|
|
6295
|
+
console.error(
|
|
6296
|
+
`
|
|
5795
6297
|
${symbols.fox} ${pc17.bold("Fennec Store")} ${pc17.dim(local ? "(local ./.fennec)" : "(global)")}
|
|
5796
|
-
`
|
|
6298
|
+
`
|
|
6299
|
+
);
|
|
5797
6300
|
const columns = [
|
|
5798
6301
|
{ key: "kind", label: "Kind", format: (v) => pc17.bold(String(v)) },
|
|
5799
6302
|
{ key: "count", label: "Items", format: (v) => pc17.cyan(String(v)) },
|
|
@@ -5831,9 +6334,11 @@ async function storeCommand(args2) {
|
|
|
5831
6334
|
if (!action || action === "ls" || action === "list") {
|
|
5832
6335
|
const sessions = store.list();
|
|
5833
6336
|
if (sessions.length === 0) {
|
|
5834
|
-
console.error(
|
|
6337
|
+
console.error(
|
|
6338
|
+
`
|
|
5835
6339
|
${pc17.dim(`No saved sessions found${local ? " in ./.fennec/sessions" : " in the global store"}.`)}
|
|
5836
|
-
`
|
|
6340
|
+
`
|
|
6341
|
+
);
|
|
5837
6342
|
return;
|
|
5838
6343
|
}
|
|
5839
6344
|
const columns = [
|
|
@@ -5856,12 +6361,19 @@ async function storeCommand(args2) {
|
|
|
5856
6361
|
}
|
|
5857
6362
|
if (action === "info") {
|
|
5858
6363
|
if (!name) {
|
|
5859
|
-
console.error(
|
|
6364
|
+
console.error(
|
|
6365
|
+
renderError("Missing name", "Usage: fennec store session info <name> [--show-secrets]")
|
|
6366
|
+
);
|
|
5860
6367
|
process.exit(1);
|
|
5861
6368
|
}
|
|
5862
6369
|
const session = store.load(name);
|
|
5863
6370
|
if (!session) {
|
|
5864
|
-
console.error(
|
|
6371
|
+
console.error(
|
|
6372
|
+
renderError(
|
|
6373
|
+
"Session not found",
|
|
6374
|
+
`No session named "${name}". Use 'fennec store session' to list.`
|
|
6375
|
+
)
|
|
6376
|
+
);
|
|
5865
6377
|
process.exit(1);
|
|
5866
6378
|
}
|
|
5867
6379
|
const shown = showSecrets ? session : redactSession(session);
|
|
@@ -5873,7 +6385,10 @@ async function storeCommand(args2) {
|
|
|
5873
6385
|
if (shown.metadata) {
|
|
5874
6386
|
for (const [k, v] of Object.entries(shown.metadata)) body += renderKV(k, String(v));
|
|
5875
6387
|
}
|
|
5876
|
-
body += renderKV(
|
|
6388
|
+
body += renderKV(
|
|
6389
|
+
"Cookies",
|
|
6390
|
+
`${shown.cookies.length} (domains: ${cookieDomains.join(", ") || "-"})`
|
|
6391
|
+
);
|
|
5877
6392
|
body += renderKV("localStorage keys", String(Object.keys(shown.localStorage).length));
|
|
5878
6393
|
body += renderKV("Values", showSecrets ? "shown" : pc17.dim("masked \u2014 use --show-secrets"));
|
|
5879
6394
|
console.error(`
|
|
@@ -5897,9 +6412,11 @@ async function storeCommand(args2) {
|
|
|
5897
6412
|
return;
|
|
5898
6413
|
}
|
|
5899
6414
|
const ok = store.delete(name);
|
|
5900
|
-
console.error(
|
|
6415
|
+
console.error(
|
|
6416
|
+
ok ? ` ${pc17.green("\u2713")} ${pc17.bold(name)} deleted
|
|
5901
6417
|
` : ` ${pc17.red("\u2717")} failed to delete ${pc17.bold(name)}
|
|
5902
|
-
`
|
|
6418
|
+
`
|
|
6419
|
+
);
|
|
5903
6420
|
return;
|
|
5904
6421
|
}
|
|
5905
6422
|
console.error(renderError("Unknown action", "Actions: ls | info <name> | rm <name>"));
|
|
@@ -6013,11 +6530,15 @@ async function setupCommand() {
|
|
|
6013
6530
|
console.error(` ${pc19.bold("Add this to your MCP client config:")}
|
|
6014
6531
|
`);
|
|
6015
6532
|
console.error(` ${pc19.dim("```")}`);
|
|
6016
|
-
console.error(
|
|
6533
|
+
console.error(
|
|
6534
|
+
configSnippet.split("\n").map((l) => ` ${l}`).join("\n")
|
|
6535
|
+
);
|
|
6017
6536
|
console.error(` ${pc19.dim("```")}
|
|
6018
6537
|
`);
|
|
6019
|
-
console.error(
|
|
6020
|
-
`)
|
|
6538
|
+
console.error(
|
|
6539
|
+
` ${renderSuccess("Setup complete!")} ${pc19.dim("Run")} ${renderCommand("fennec start")} ${pc19.dim("to begin.")}
|
|
6540
|
+
`
|
|
6541
|
+
);
|
|
6021
6542
|
}
|
|
6022
6543
|
|
|
6023
6544
|
// src/commands/management.ts
|
|
@@ -6035,7 +6556,9 @@ async function installBrowsersCommand() {
|
|
|
6035
6556
|
spinner.succeed("Chromium installed successfully");
|
|
6036
6557
|
} catch {
|
|
6037
6558
|
spinner.fail("Failed to install Chromium");
|
|
6038
|
-
console.error(
|
|
6559
|
+
console.error(
|
|
6560
|
+
` ${pc20.yellow("\u2192")} Try running: ${renderCommand("npx playwright install chromium")}`
|
|
6561
|
+
);
|
|
6039
6562
|
}
|
|
6040
6563
|
console.error(`
|
|
6041
6564
|
${pc20.green("\u2713")} Browser installation complete.
|
|
@@ -6047,7 +6570,10 @@ async function initCommand() {
|
|
|
6047
6570
|
`);
|
|
6048
6571
|
const configFile = resolve7("./fennec.config.yaml");
|
|
6049
6572
|
if (existsSync11(configFile)) {
|
|
6050
|
-
const overwrite = await confirmPrompt(
|
|
6573
|
+
const overwrite = await confirmPrompt(
|
|
6574
|
+
`${pc20.yellow("fennec.config.yaml")} already exists. Overwrite?`,
|
|
6575
|
+
false
|
|
6576
|
+
);
|
|
6051
6577
|
if (!overwrite) {
|
|
6052
6578
|
console.error(` ${pc20.dim("Cancelled.")}
|
|
6053
6579
|
`);
|
|
@@ -6136,7 +6662,7 @@ logging:
|
|
|
6136
6662
|
}
|
|
6137
6663
|
|
|
6138
6664
|
// src/commands/health.ts
|
|
6139
|
-
import { existsSync as existsSync12, readdirSync as readdirSync2, statSync as
|
|
6665
|
+
import { existsSync as existsSync12, readdirSync as readdirSync2, statSync as statSync4 } from "fs";
|
|
6140
6666
|
import { execSync as execSync5 } from "child_process";
|
|
6141
6667
|
import { resolve as resolve8 } from "path";
|
|
6142
6668
|
import { homedir as homedir5 } from "os";
|
|
@@ -6166,7 +6692,7 @@ async function healthCommand() {
|
|
|
6166
6692
|
const files = readdirSync2(logDir);
|
|
6167
6693
|
let totalBytes = 0;
|
|
6168
6694
|
for (const f of files) {
|
|
6169
|
-
const fStat =
|
|
6695
|
+
const fStat = statSync4(resolve8(logDir, f));
|
|
6170
6696
|
totalBytes += fStat.size;
|
|
6171
6697
|
}
|
|
6172
6698
|
logSize = totalBytes > 1024 * 1024 ? `${(totalBytes / (1024 * 1024)).toFixed(1)} MB` : `${(totalBytes / 1024).toFixed(1)} KB`;
|
|
@@ -6199,8 +6725,10 @@ async function healthCommand() {
|
|
|
6199
6725
|
`);
|
|
6200
6726
|
} else {
|
|
6201
6727
|
const stopped = tracked.length - runningCount;
|
|
6202
|
-
console.error(
|
|
6203
|
-
`)
|
|
6728
|
+
console.error(
|
|
6729
|
+
` ${pc21.yellow("\u26A0")} ${pc21.bold(`${stopped} process(es) stopped`)} ${pc21.dim("fennec ps")}
|
|
6730
|
+
`
|
|
6731
|
+
);
|
|
6204
6732
|
}
|
|
6205
6733
|
}
|
|
6206
6734
|
|
|
@@ -6216,19 +6744,26 @@ async function cleanupCommand() {
|
|
|
6216
6744
|
}
|
|
6217
6745
|
const toRemove = tracked.filter((t) => !isProcessRunning(t.pid) && !t.command);
|
|
6218
6746
|
if (toRemove.length === 0) {
|
|
6219
|
-
console.error(
|
|
6747
|
+
console.error(
|
|
6748
|
+
`
|
|
6220
6749
|
${pc22.green("\u2713")} ${pc22.bold("All clean")} ${pc22.dim("\u2014 no dead entries without saved commands.")}
|
|
6221
|
-
`
|
|
6750
|
+
`
|
|
6751
|
+
);
|
|
6222
6752
|
return;
|
|
6223
6753
|
}
|
|
6224
|
-
console.error(
|
|
6754
|
+
console.error(
|
|
6755
|
+
`
|
|
6225
6756
|
${pc22.yellow("\u26A0")} ${pc22.bold(`Found ${toRemove.length} dead entr${toRemove.length > 1 ? "ies" : "y"} without saved commands`)}
|
|
6226
|
-
`
|
|
6757
|
+
`
|
|
6758
|
+
);
|
|
6227
6759
|
for (const entry of toRemove) {
|
|
6228
6760
|
console.error(` ${pc22.dim("\xB7")} ${pc22.bold(entry.name)} ${pc22.dim(`(PID ${entry.pid})`)}`);
|
|
6229
6761
|
}
|
|
6230
6762
|
console.error();
|
|
6231
|
-
const confirmed = await confirmPrompt(
|
|
6763
|
+
const confirmed = await confirmPrompt(
|
|
6764
|
+
`Remove ${toRemove.length} entr${toRemove.length > 1 ? "ies" : "y"}?`,
|
|
6765
|
+
false
|
|
6766
|
+
);
|
|
6232
6767
|
if (!confirmed) {
|
|
6233
6768
|
console.error(` ${pc22.dim("Cancelled")}
|
|
6234
6769
|
`);
|
|
@@ -6236,9 +6771,11 @@ async function cleanupCommand() {
|
|
|
6236
6771
|
}
|
|
6237
6772
|
const remaining = tracked.filter((t) => !toRemove.includes(t));
|
|
6238
6773
|
saveTracked(remaining);
|
|
6239
|
-
console.error(
|
|
6774
|
+
console.error(
|
|
6775
|
+
`
|
|
6240
6776
|
${pc22.green("\u2713")} ${pc22.bold(`Removed ${toRemove.length} entr${toRemove.length > 1 ? "ies" : "y"}`)} ${pc22.dim(`(${remaining.length} remaining)`)}
|
|
6241
|
-
`
|
|
6777
|
+
`
|
|
6778
|
+
);
|
|
6242
6779
|
}
|
|
6243
6780
|
|
|
6244
6781
|
// src/commands/info.ts
|
|
@@ -6314,10 +6851,15 @@ async function renameCommand(args2) {
|
|
|
6314
6851
|
console.error(` ${renderKVColor("From", oldName)}`);
|
|
6315
6852
|
console.error(` ${renderKVColor("To", newName)}`);
|
|
6316
6853
|
if (isProcessRunning(match.pid)) {
|
|
6317
|
-
console.error(
|
|
6854
|
+
console.error(
|
|
6855
|
+
` ${pc24.yellow("\u26A0")} ${pc24.dim("Process is still running \u2014 log entries will go to the old file until stopped.")}`
|
|
6856
|
+
);
|
|
6318
6857
|
}
|
|
6319
6858
|
console.error();
|
|
6320
|
-
const confirmed = await confirmPrompt(
|
|
6859
|
+
const confirmed = await confirmPrompt(
|
|
6860
|
+
`Rename ${pc24.bold(oldName)} ${pc24.dim("\u2192")} ${pc24.bold(newName)}?`,
|
|
6861
|
+
true
|
|
6862
|
+
);
|
|
6321
6863
|
if (!confirmed) {
|
|
6322
6864
|
console.error(` ${pc24.dim("Cancelled")}
|
|
6323
6865
|
`);
|
|
@@ -6361,9 +6903,11 @@ async function exportCommand(args2) {
|
|
|
6361
6903
|
if (filePath) {
|
|
6362
6904
|
try {
|
|
6363
6905
|
writeFileSync5(resolve11(filePath), json, "utf-8");
|
|
6364
|
-
console.error(
|
|
6906
|
+
console.error(
|
|
6907
|
+
`
|
|
6365
6908
|
${pc25.green("\u2713")} ${pc25.bold(`Exported ${tracked.length} process(es)`)} ${pc25.dim(`\u2192 ${filePath}`)}
|
|
6366
|
-
`
|
|
6909
|
+
`
|
|
6910
|
+
);
|
|
6367
6911
|
} catch (err) {
|
|
6368
6912
|
console.error(renderError("Export failed", String(err)));
|
|
6369
6913
|
process.exit(1);
|
|
@@ -6387,7 +6931,9 @@ async function importCommand(args2) {
|
|
|
6387
6931
|
try {
|
|
6388
6932
|
imported = JSON.parse(readFileSync10(resolvedPath, "utf-8"));
|
|
6389
6933
|
if (!Array.isArray(imported) || imported.length === 0) {
|
|
6390
|
-
console.error(
|
|
6934
|
+
console.error(
|
|
6935
|
+
renderError("Invalid file", "File must contain a non-empty JSON array of process objects.")
|
|
6936
|
+
);
|
|
6391
6937
|
process.exit(1);
|
|
6392
6938
|
}
|
|
6393
6939
|
} catch (err) {
|
|
@@ -6407,7 +6953,10 @@ async function importCommand(args2) {
|
|
|
6407
6953
|
console.error(` ${pc25.green("+")} ${pc25.bold(p.name)} ${pc25.dim(`(${p.command})`)}`);
|
|
6408
6954
|
}
|
|
6409
6955
|
console.error();
|
|
6410
|
-
const confirmed = await confirmPrompt(
|
|
6956
|
+
const confirmed = await confirmPrompt(
|
|
6957
|
+
"Merge into tracked.json? Existing processes with the same name will be overwritten.",
|
|
6958
|
+
true
|
|
6959
|
+
);
|
|
6411
6960
|
if (!confirmed) {
|
|
6412
6961
|
console.error(` ${pc25.dim("Cancelled")}
|
|
6413
6962
|
`);
|
|
@@ -6423,8 +6972,10 @@ async function importCommand(args2) {
|
|
|
6423
6972
|
}
|
|
6424
6973
|
}
|
|
6425
6974
|
saveTracked(merged);
|
|
6426
|
-
console.error(
|
|
6427
|
-
`)
|
|
6975
|
+
console.error(
|
|
6976
|
+
` ${pc25.green("\u2713")} ${pc25.bold(`Imported ${imported.length} process(es)`)} ${pc25.dim(`(${merged.length} total)`)}
|
|
6977
|
+
`
|
|
6978
|
+
);
|
|
6428
6979
|
}
|
|
6429
6980
|
|
|
6430
6981
|
// src/commands/group.ts
|
|
@@ -6442,44 +6993,87 @@ async function groupCommand(args2) {
|
|
|
6442
6993
|
}
|
|
6443
6994
|
const columns = [
|
|
6444
6995
|
{ key: "name", label: "App", format: (v) => pc26.bold(String(v)) },
|
|
6445
|
-
{
|
|
6446
|
-
|
|
6447
|
-
|
|
6448
|
-
|
|
6449
|
-
|
|
6450
|
-
|
|
6451
|
-
|
|
6452
|
-
}
|
|
6996
|
+
{
|
|
6997
|
+
key: "group",
|
|
6998
|
+
label: "Group",
|
|
6999
|
+
format: (v) => {
|
|
7000
|
+
const g = String(v);
|
|
7001
|
+
return g === "-" ? pc26.dim("-") : pc26.cyan(g);
|
|
7002
|
+
}
|
|
7003
|
+
},
|
|
7004
|
+
{
|
|
7005
|
+
key: "command",
|
|
7006
|
+
label: "Command",
|
|
7007
|
+
format: (v) => {
|
|
7008
|
+
const c = String(v);
|
|
7009
|
+
return c.length > 50 ? c.slice(0, 50) + "\u2026" : c;
|
|
7010
|
+
}
|
|
7011
|
+
}
|
|
6453
7012
|
];
|
|
6454
|
-
const rows = tracked.map((t) => ({
|
|
6455
|
-
|
|
7013
|
+
const rows = tracked.map((t) => ({
|
|
7014
|
+
name: t.name,
|
|
7015
|
+
group: t.group ?? "-",
|
|
7016
|
+
command: t.command
|
|
7017
|
+
}));
|
|
7018
|
+
console.error(
|
|
7019
|
+
`
|
|
6456
7020
|
${pc26.bold("Fennec Apps by Group")} ${pc26.dim(`(${getGroups().length} group(s))`)}
|
|
6457
|
-
`
|
|
7021
|
+
`
|
|
7022
|
+
);
|
|
6458
7023
|
console.error(renderTable(columns, rows));
|
|
6459
|
-
console.error(` ${pc26.dim("Assign:")} ${pc26.cyan("fennec group <name> <group>")}
|
|
7024
|
+
console.error(` ${pc26.dim("Assign one:")} ${pc26.cyan("fennec group <name> <group>")}`);
|
|
7025
|
+
console.error(
|
|
7026
|
+
` ${pc26.dim("Assign bulk:")} ${pc26.cyan("fennec group <group> <name...>")} ${pc26.dim("Clear:")} ${pc26.cyan("fennec group <name> --unset")}`
|
|
7027
|
+
);
|
|
6460
7028
|
console.error();
|
|
6461
7029
|
return;
|
|
6462
7030
|
}
|
|
6463
|
-
const name = positional[0];
|
|
6464
|
-
const newGroup = positional[1];
|
|
6465
|
-
if (!newGroup && !unset) {
|
|
6466
|
-
console.error(renderError("Missing group", "Usage: fennec group <name> <group> (or: fennec group <name> --unset)"));
|
|
6467
|
-
process.exit(1);
|
|
6468
|
-
}
|
|
6469
|
-
const ok = setGroup(name, unset ? void 0 : newGroup);
|
|
6470
|
-
if (!ok) {
|
|
6471
|
-
console.error(renderError("Not found", `No tracked process named "${name}".`));
|
|
6472
|
-
process.exit(1);
|
|
6473
|
-
}
|
|
6474
7031
|
if (unset) {
|
|
7032
|
+
const name = positional[0];
|
|
7033
|
+
const ok = setGroup(name, void 0);
|
|
7034
|
+
if (!ok) {
|
|
7035
|
+
console.error(renderError("Not found", `No tracked process named "${name}".`));
|
|
7036
|
+
process.exit(1);
|
|
7037
|
+
}
|
|
6475
7038
|
console.error(`
|
|
6476
7039
|
${pc26.green("\u2713")} ${pc26.bold(name)} ${pc26.dim("removed from its group")}
|
|
6477
7040
|
`);
|
|
7041
|
+
return;
|
|
7042
|
+
}
|
|
7043
|
+
let group;
|
|
7044
|
+
let names;
|
|
7045
|
+
if (positional.length >= 3) {
|
|
7046
|
+
group = positional[0];
|
|
7047
|
+
names = positional.slice(1);
|
|
6478
7048
|
} else {
|
|
6479
|
-
|
|
6480
|
-
|
|
6481
|
-
|
|
7049
|
+
const [a, b] = positional;
|
|
7050
|
+
const tracked = readTracked();
|
|
7051
|
+
const aIsApp = tracked.some((t) => t.name === a);
|
|
7052
|
+
if (aIsApp) {
|
|
7053
|
+
names = [a];
|
|
7054
|
+
group = b;
|
|
7055
|
+
} else {
|
|
7056
|
+
group = a;
|
|
7057
|
+
names = [b];
|
|
7058
|
+
}
|
|
7059
|
+
}
|
|
7060
|
+
const okNames = [];
|
|
7061
|
+
const failed = [];
|
|
7062
|
+
for (const n of names) {
|
|
7063
|
+
if (setGroup(n, group)) okNames.push(n);
|
|
7064
|
+
else failed.push(n);
|
|
6482
7065
|
}
|
|
7066
|
+
if (okNames.length > 0) {
|
|
7067
|
+
console.error(
|
|
7068
|
+
`
|
|
7069
|
+
${pc26.green("\u2713")} ${pc26.bold(okNames.join(", "))} ${pc26.dim(`assigned to group`)} ${pc26.cyan(group)}`
|
|
7070
|
+
);
|
|
7071
|
+
}
|
|
7072
|
+
if (failed.length > 0) {
|
|
7073
|
+
console.error(` ${pc26.red("\u2717")} ${pc26.dim(`not found: ${failed.join(", ")}`)}`);
|
|
7074
|
+
}
|
|
7075
|
+
if (okNames.length === 0 && failed.length > 0) process.exit(1);
|
|
7076
|
+
console.error();
|
|
6483
7077
|
}
|
|
6484
7078
|
|
|
6485
7079
|
// src/index.ts
|
|
@@ -6581,7 +7175,9 @@ async function main() {
|
|
|
6581
7175
|
showHelp();
|
|
6582
7176
|
}
|
|
6583
7177
|
} else {
|
|
6584
|
-
console.error(
|
|
7178
|
+
console.error(
|
|
7179
|
+
renderError(`Unknown command: ${command}`, "Run 'fennec help' for usage information")
|
|
7180
|
+
);
|
|
6585
7181
|
process.exit(1);
|
|
6586
7182
|
}
|
|
6587
7183
|
}
|