@plumpslabs/fennec-cli 1.14.2 → 1.14.4
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 +122 -69
- package/dist/index.js +1600 -436
- 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",
|
|
@@ -1961,6 +1961,14 @@ function renderKV(key, value, options) {
|
|
|
1961
1961
|
function renderKVColor(key, value, color = pc.bold) {
|
|
1962
1962
|
return ` ${pc.dim(key + ":")} ${color(value)}`;
|
|
1963
1963
|
}
|
|
1964
|
+
function renderSection(title, content) {
|
|
1965
|
+
const line = pc.dim("\u2500".repeat(40));
|
|
1966
|
+
return `
|
|
1967
|
+
${pc.bold(title)}
|
|
1968
|
+
${line}
|
|
1969
|
+
${content}
|
|
1970
|
+
`;
|
|
1971
|
+
}
|
|
1964
1972
|
function createSpinner(text) {
|
|
1965
1973
|
const frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
1966
1974
|
let i = 0;
|
|
@@ -2013,10 +2021,10 @@ async function selectPrompt(message, options) {
|
|
|
2013
2021
|
console.log(` ${pc.dim("0) Cancel")}
|
|
2014
2022
|
`);
|
|
2015
2023
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
2016
|
-
const answer = await new Promise((
|
|
2024
|
+
const answer = await new Promise((resolve12) => {
|
|
2017
2025
|
rl.question(` ${pc.bold("Enter number")} ${pc.dim("(0-" + options.length + ")")}: `, (ans) => {
|
|
2018
2026
|
rl.close();
|
|
2019
|
-
|
|
2027
|
+
resolve12(ans.trim());
|
|
2020
2028
|
});
|
|
2021
2029
|
});
|
|
2022
2030
|
const num = parseInt(answer, 10);
|
|
@@ -2031,11 +2039,11 @@ async function selectPrompt(message, options) {
|
|
|
2031
2039
|
async function confirmPrompt(message, defaultValue = false) {
|
|
2032
2040
|
const hint = defaultValue ? pc.bold("Y") + pc.dim("/n") : pc.dim("y/") + pc.bold("N");
|
|
2033
2041
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
2034
|
-
const answer = await new Promise((
|
|
2042
|
+
const answer = await new Promise((resolve12) => {
|
|
2035
2043
|
rl.question(`
|
|
2036
2044
|
${message} ${pc.dim("(" + hint + ")")}: `, (ans) => {
|
|
2037
2045
|
rl.close();
|
|
2038
|
-
|
|
2046
|
+
resolve12(ans.trim().toLowerCase());
|
|
2039
2047
|
});
|
|
2040
2048
|
});
|
|
2041
2049
|
if (!answer) return defaultValue;
|
|
@@ -2091,7 +2099,7 @@ function hexColor(color) {
|
|
|
2091
2099
|
}
|
|
2092
2100
|
var fennecOrange2 = hexColor("#FF6432");
|
|
2093
2101
|
var fennecGold = hexColor("#FFB347");
|
|
2094
|
-
var VERSION = "1.14.
|
|
2102
|
+
var VERSION = "1.14.3";
|
|
2095
2103
|
var cachedBanner = null;
|
|
2096
2104
|
var cachedCompact = null;
|
|
2097
2105
|
function generateBanner() {
|
|
@@ -2158,11 +2166,15 @@ var COMMANDS = {
|
|
|
2158
2166
|
["--name <name>", "App name used for tracking (recommended)"],
|
|
2159
2167
|
["--port <port>", "Port the app listens on \u2014 Fennec waits until it accepts connections"],
|
|
2160
2168
|
["--cwd <dir>", "Working directory to run the command in"],
|
|
2161
|
-
[
|
|
2169
|
+
[
|
|
2170
|
+
"--restart",
|
|
2171
|
+
"Auto-restart if it crashes or its port stops listening (detached supervisor, survives terminal close)"
|
|
2172
|
+
],
|
|
2173
|
+
["--group <group>", "Logical group/namespace for bulk ops (kill/spawn/stop --group <group>)"]
|
|
2162
2174
|
],
|
|
2163
2175
|
examples: [
|
|
2164
2176
|
'start "npm run dev" --name web --port 3000',
|
|
2165
|
-
"start node server.js --name api --cwd ./backend --restart"
|
|
2177
|
+
"start node server.js --name api --cwd ./backend --restart --group backend"
|
|
2166
2178
|
]
|
|
2167
2179
|
},
|
|
2168
2180
|
run: {
|
|
@@ -2175,18 +2187,20 @@ var COMMANDS = {
|
|
|
2175
2187
|
["--name <name>", "App name used for tracking (recommended)"],
|
|
2176
2188
|
["--port <port>", "Port the app listens on"],
|
|
2177
2189
|
["--cwd <dir>", "Working directory"],
|
|
2178
|
-
["--restart", "Auto-restart if it crashes or its port stops listening"]
|
|
2190
|
+
["--restart", "Auto-restart if it crashes or its port stops listening"],
|
|
2191
|
+
["--group <group>", "Logical group for bulk ops"]
|
|
2179
2192
|
],
|
|
2180
|
-
examples: ["run npm start --name web --port 8080"]
|
|
2193
|
+
examples: ["run npm start --name web --port 8080 --group frontend"]
|
|
2181
2194
|
},
|
|
2182
2195
|
ps: {
|
|
2183
2196
|
name: "ps",
|
|
2184
2197
|
usage: "ps [options]",
|
|
2185
2198
|
summary: "List Fennec-tracked apps with live status",
|
|
2186
|
-
description: "Shows tracked apps and whether they are genuinely running (PID + command identity are verified to avoid false positives from PID reuse).",
|
|
2199
|
+
description: "Shows tracked apps and whether they are genuinely running (PID + command identity are verified to avoid false positives from PID reuse). Includes a cross-platform MEM column (resident RSS) so you can spot leaks from long-lived apps.",
|
|
2187
2200
|
options: [
|
|
2188
2201
|
["--system, -a, --all", "Show all system processes instead of tracked apps"],
|
|
2189
2202
|
["--name <name>", "Filter system processes by name"],
|
|
2203
|
+
["--group <g>, -g <g>", "Show only tracked apps in group <g>"],
|
|
2190
2204
|
["--sort <field>", "Sort by cpu | mem | pid | name (default: name)"],
|
|
2191
2205
|
["--json", "Output tracked apps as JSON"],
|
|
2192
2206
|
["-w, --watch", "Watch mode \u2014 refresh every 3s (with --system)"]
|
|
@@ -2219,37 +2233,51 @@ var COMMANDS = {
|
|
|
2219
2233
|
},
|
|
2220
2234
|
spawn: {
|
|
2221
2235
|
name: "spawn",
|
|
2222
|
-
usage: "spawn [name] [--all]",
|
|
2236
|
+
usage: "spawn [name] [name...] [--all] [--group <g>]",
|
|
2223
2237
|
summary: "Re-spawn a stopped tracked app from its saved config",
|
|
2224
2238
|
description: "Revives a previously stopped app using its saved command/args/cwd. With no name, shows an interactive list of stopped apps.",
|
|
2225
|
-
options: [
|
|
2226
|
-
|
|
2239
|
+
options: [
|
|
2240
|
+
["--all, -a", "Re-spawn all stopped apps that have a saved command"],
|
|
2241
|
+
["--group <g>, -g <g>", "Re-spawn only stopped apps in group <g>"]
|
|
2242
|
+
],
|
|
2243
|
+
examples: ["spawn", "spawn web", "spawn be-crm fe-crm", "spawn --all", "spawn --group backend"]
|
|
2227
2244
|
},
|
|
2228
2245
|
stop: {
|
|
2229
2246
|
name: "stop",
|
|
2230
|
-
usage: "stop <name|--all>",
|
|
2247
|
+
usage: "stop <name|--all> [name...] [--group <g>]",
|
|
2231
2248
|
summary: "Stop (pause) a tracked app but keep it in the registry",
|
|
2232
|
-
description: "Sends SIGTERM but keeps the entry in tracked.json so it can be revived later with `fennec spawn`.",
|
|
2233
|
-
options: [
|
|
2234
|
-
|
|
2249
|
+
description: "Sends SIGTERM but keeps the entry in tracked.json so it can be revived later with `fennec spawn`. Stops the ENTIRE process tree so no orphaned children are left. Accepts multiple names at once (e.g. `stop be-crm fe-crm`).",
|
|
2250
|
+
options: [
|
|
2251
|
+
["--all, -a", "Stop all running tracked apps"],
|
|
2252
|
+
["--group <g>, -g <g>", "Stop only running apps in group <g>"]
|
|
2253
|
+
],
|
|
2254
|
+
examples: ["stop web", "stop be-crm fe-crm -y", "stop --all", "stop --group backend"]
|
|
2235
2255
|
},
|
|
2236
2256
|
restart: {
|
|
2237
2257
|
name: "restart",
|
|
2238
|
-
usage: "restart <name|pid>",
|
|
2258
|
+
usage: "restart <name|pid> [name...] [--group <g>]",
|
|
2239
2259
|
summary: "Stop and re-spawn a tracked app from its saved config",
|
|
2240
|
-
|
|
2260
|
+
options: [["--group <g>, -g <g>", "Restart all apps in group <g>"]],
|
|
2261
|
+
examples: ["restart web", "restart be-crm fe-crm -y", "restart --group backend"]
|
|
2241
2262
|
},
|
|
2242
2263
|
kill: {
|
|
2243
2264
|
name: "kill",
|
|
2244
|
-
usage: "kill <pid|name|all>",
|
|
2245
|
-
summary: "Kill a
|
|
2246
|
-
description: "Permanently removes a tracked app (unlike `stop`, which keeps it). By NAME it only matches Fennec-tracked apps \u2014 use an explicit PID to kill a system process. Prompts before killing.",
|
|
2265
|
+
usage: "kill <pid|name|all> [name...] [--group <g>]",
|
|
2266
|
+
summary: "Kill a process and remove it from the registry",
|
|
2267
|
+
description: "Permanently removes a tracked app (unlike `stop`, which keeps it). By NAME it only matches Fennec-tracked apps \u2014 use an explicit PID to kill a system process. Prompts before killing. Kills the ENTIRE process tree (e.g. npm \u2192 vite \u2192 esbuild) so no orphaned children are left behind. Accepts multiple names at once (e.g. `kill be-crm fe-crm`).",
|
|
2247
2268
|
options: [
|
|
2248
2269
|
["--signal <sig>", "Signal to send (default: SIGTERM). e.g. SIGKILL, SIGINT"],
|
|
2249
2270
|
["--all, -a", "Kill ALL tracked apps (asks for confirmation)"],
|
|
2271
|
+
["--group <g>, -g <g>", "Kill only apps in group <g> (other groups untouched)"],
|
|
2250
2272
|
["-y, --yes", "Skip the confirmation prompt"]
|
|
2251
2273
|
],
|
|
2252
|
-
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
|
+
]
|
|
2253
2281
|
},
|
|
2254
2282
|
supervisor: {
|
|
2255
2283
|
name: "supervisor",
|
|
@@ -2317,6 +2345,18 @@ var COMMANDS = {
|
|
|
2317
2345
|
summary: "Rename a tracked app",
|
|
2318
2346
|
examples: ["rename web frontend"]
|
|
2319
2347
|
},
|
|
2348
|
+
group: {
|
|
2349
|
+
name: "group",
|
|
2350
|
+
usage: "group [name] [group]",
|
|
2351
|
+
summary: "Assign a logical group to tracked apps (for scoped bulk ops)",
|
|
2352
|
+
description: "Groups let bulk commands (kill/spawn/stop/ps) target a subset instead of everything. Assign a group to an existing tracked app, clear it with --unset, or run with no args to list every app's group. New apps can also be tagged at start time with `fennec start <cmd> --name <n> --group <g>`. Then `fennec kill --group <g>` / `fennec spawn --group <g>` / `fennec stop --group <g>` / `fennec ps --group <g>` only touch that group.",
|
|
2353
|
+
options: [
|
|
2354
|
+
["<name> <group>", "Assign <group> to the tracked app <name>"],
|
|
2355
|
+
["<name> --unset", "Remove the group from <name>"],
|
|
2356
|
+
["(no args)", "List all tracked apps with their group"]
|
|
2357
|
+
],
|
|
2358
|
+
examples: ["group web backend", "group web --unset", "group", "kill --group backend"]
|
|
2359
|
+
},
|
|
2320
2360
|
attach: {
|
|
2321
2361
|
name: "attach",
|
|
2322
2362
|
usage: "attach <port> --name <name>",
|
|
@@ -2431,7 +2471,27 @@ function findCommandDoc(cmd) {
|
|
|
2431
2471
|
}
|
|
2432
2472
|
var GROUPS = [
|
|
2433
2473
|
{ title: "Server", keys: ["start-server"] },
|
|
2434
|
-
{
|
|
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
|
+
},
|
|
2435
2495
|
{ title: "Observation", keys: ["attach", "attach-pid", "attach-port", "pipe", "watch"] },
|
|
2436
2496
|
{ title: "Data", keys: ["export", "import", "cleanup"] },
|
|
2437
2497
|
{ title: "Configuration", keys: ["init", "setup", "install-browsers", "sessions"] },
|
|
@@ -2445,7 +2505,9 @@ function showHelp() {
|
|
|
2445
2505
|
const lines = [];
|
|
2446
2506
|
lines.push("");
|
|
2447
2507
|
lines.push(` ${pc3.bold("Usage:")} fennec ${pc3.dim("<command>")} ${pc3.dim("[options]")}`);
|
|
2448
|
-
lines.push(
|
|
2508
|
+
lines.push(
|
|
2509
|
+
` ${pc3.dim("Run")} ${pc3.cyan("fennec <command> --help")} ${pc3.dim("for details on any command.")}`
|
|
2510
|
+
);
|
|
2449
2511
|
for (const group of GROUPS) {
|
|
2450
2512
|
lines.push("");
|
|
2451
2513
|
lines.push(` ${sep}`);
|
|
@@ -2470,8 +2532,10 @@ function showCommandHelp(cmd) {
|
|
|
2470
2532
|
if (!doc) {
|
|
2471
2533
|
console.error(`
|
|
2472
2534
|
${pc3.yellow("\u26A0")} Unknown command: ${pc3.bold(cmd)}`);
|
|
2473
|
-
console.error(
|
|
2474
|
-
`)
|
|
2535
|
+
console.error(
|
|
2536
|
+
` ${pc3.dim("Run")} ${pc3.cyan("fennec help")} ${pc3.dim("to see all commands.")}
|
|
2537
|
+
`
|
|
2538
|
+
);
|
|
2475
2539
|
return;
|
|
2476
2540
|
}
|
|
2477
2541
|
const lines = [];
|
|
@@ -2580,7 +2644,9 @@ async function attachPortCommand(args2) {
|
|
|
2580
2644
|
const detector = new PortDetector2();
|
|
2581
2645
|
const info = detector.detectByPort(port);
|
|
2582
2646
|
if (info) {
|
|
2583
|
-
console.log(
|
|
2647
|
+
console.log(
|
|
2648
|
+
`Found process on port ${port}: PID ${info.pid}${info.command ? ` (${info.command})` : ""}`
|
|
2649
|
+
);
|
|
2584
2650
|
} else {
|
|
2585
2651
|
console.error(`No process found listening on port ${port}`);
|
|
2586
2652
|
process.exit(1);
|
|
@@ -2619,7 +2685,18 @@ import { FennecServer } from "@plumpslabs/fennec-core";
|
|
|
2619
2685
|
import pc7 from "picocolors";
|
|
2620
2686
|
|
|
2621
2687
|
// src/commands/tracker.ts
|
|
2622
|
-
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";
|
|
2623
2700
|
import { spawn } from "child_process";
|
|
2624
2701
|
import { resolve as resolve2, dirname, basename } from "path";
|
|
2625
2702
|
import { homedir } from "os";
|
|
@@ -2681,7 +2758,10 @@ function readSingleProcProcess(pid, currentUid, totalMemKb) {
|
|
|
2681
2758
|
if (uptimeSeconds > 0) {
|
|
2682
2759
|
const elapsedJiffies = uptimeSeconds * hertz;
|
|
2683
2760
|
const processJiffies = totalJiffies;
|
|
2684
|
-
cpuPercent = Math.min(
|
|
2761
|
+
cpuPercent = Math.min(
|
|
2762
|
+
100,
|
|
2763
|
+
processJiffies / Math.max(1, elapsedJiffies) * 100 * cpus().length
|
|
2764
|
+
);
|
|
2685
2765
|
}
|
|
2686
2766
|
startedAt = formatStartTime(starttime, uptimeSeconds, hertz);
|
|
2687
2767
|
}
|
|
@@ -2872,12 +2952,32 @@ function getSystemProcesses(filter) {
|
|
|
2872
2952
|
}
|
|
2873
2953
|
return processes;
|
|
2874
2954
|
}
|
|
2875
|
-
function
|
|
2955
|
+
function killTree(pid, signal = "SIGTERM") {
|
|
2956
|
+
if (process.platform === "win32") {
|
|
2957
|
+
try {
|
|
2958
|
+
execSync(`taskkill /pid ${pid} /T /F`, { stdio: "ignore", timeout: 5e3 });
|
|
2959
|
+
return true;
|
|
2960
|
+
} catch {
|
|
2961
|
+
try {
|
|
2962
|
+
process.kill(pid, signal);
|
|
2963
|
+
return true;
|
|
2964
|
+
} catch {
|
|
2965
|
+
return false;
|
|
2966
|
+
}
|
|
2967
|
+
}
|
|
2968
|
+
}
|
|
2876
2969
|
try {
|
|
2877
|
-
process.kill(pid, signal);
|
|
2970
|
+
process.kill(-pid, signal);
|
|
2878
2971
|
return true;
|
|
2879
|
-
} catch {
|
|
2880
|
-
|
|
2972
|
+
} catch (e) {
|
|
2973
|
+
const code = e.code;
|
|
2974
|
+
if (code === "ESRCH") return false;
|
|
2975
|
+
try {
|
|
2976
|
+
process.kill(pid, signal);
|
|
2977
|
+
return true;
|
|
2978
|
+
} catch {
|
|
2979
|
+
return false;
|
|
2980
|
+
}
|
|
2881
2981
|
}
|
|
2882
2982
|
}
|
|
2883
2983
|
function isProcessRunning(pid) {
|
|
@@ -2888,15 +2988,54 @@ function isProcessRunning(pid) {
|
|
|
2888
2988
|
return false;
|
|
2889
2989
|
}
|
|
2890
2990
|
}
|
|
2991
|
+
function getProcessMemRss(pid) {
|
|
2992
|
+
if (process.platform === "win32") {
|
|
2993
|
+
try {
|
|
2994
|
+
const out = execSync(`tasklist /fi "PID eq ${pid}" /fo csv /nh 2>nul`, {
|
|
2995
|
+
encoding: "utf-8",
|
|
2996
|
+
timeout: 3e3
|
|
2997
|
+
});
|
|
2998
|
+
for (const raw of out.split("\n")) {
|
|
2999
|
+
if (!raw.trim()) continue;
|
|
3000
|
+
const parts = raw.split('","').map((p) => p.replace(/^"|"$/g, ""));
|
|
3001
|
+
if (parseInt(parts[1] ?? "", 10) !== pid) continue;
|
|
3002
|
+
const memKb = parseInt((parts[4] ?? "").replace(/[^\d]/g, ""), 10);
|
|
3003
|
+
return isNaN(memKb) ? null : memKb;
|
|
3004
|
+
}
|
|
3005
|
+
return null;
|
|
3006
|
+
} catch {
|
|
3007
|
+
return null;
|
|
3008
|
+
}
|
|
3009
|
+
}
|
|
3010
|
+
if (process.platform === "darwin" || process.platform === "freebsd" || process.platform === "openbsd" || process.platform === "netbsd") {
|
|
3011
|
+
try {
|
|
3012
|
+
const out = execSync(`ps -o rss= -p ${pid} 2>/dev/null`, {
|
|
3013
|
+
encoding: "utf-8",
|
|
3014
|
+
timeout: 3e3
|
|
3015
|
+
}).trim();
|
|
3016
|
+
const kb = parseInt(out, 10);
|
|
3017
|
+
return isNaN(kb) ? null : kb;
|
|
3018
|
+
} catch {
|
|
3019
|
+
return null;
|
|
3020
|
+
}
|
|
3021
|
+
}
|
|
3022
|
+
try {
|
|
3023
|
+
const status = readFileSync(`/proc/${pid}/status`, "utf-8");
|
|
3024
|
+
const m = status.match(/VmRSS:\s+(\d+)/);
|
|
3025
|
+
return m ? parseInt(m[1], 10) : null;
|
|
3026
|
+
} catch {
|
|
3027
|
+
return null;
|
|
3028
|
+
}
|
|
3029
|
+
}
|
|
2891
3030
|
function checkPort(port, host = "127.0.0.1", timeout = 1e3) {
|
|
2892
|
-
const tryHost = (h) => new Promise((
|
|
3031
|
+
const tryHost = (h) => new Promise((resolve12) => {
|
|
2893
3032
|
const socket = new net.Socket();
|
|
2894
3033
|
let settled = false;
|
|
2895
3034
|
const finish = (result) => {
|
|
2896
3035
|
if (settled) return;
|
|
2897
3036
|
settled = true;
|
|
2898
3037
|
socket.destroy();
|
|
2899
|
-
|
|
3038
|
+
resolve12(result);
|
|
2900
3039
|
};
|
|
2901
3040
|
socket.setTimeout(timeout);
|
|
2902
3041
|
socket.once("connect", () => finish(true));
|
|
@@ -2968,7 +3107,10 @@ function findPidOnPortLinux(port) {
|
|
|
2968
3107
|
}
|
|
2969
3108
|
function findPidOnPortMac(port) {
|
|
2970
3109
|
try {
|
|
2971
|
-
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
|
+
});
|
|
2972
3114
|
for (const line of out.split("\n")) {
|
|
2973
3115
|
if (!line.includes("LISTEN")) continue;
|
|
2974
3116
|
const parts = line.trim().split(/\s+/);
|
|
@@ -3023,7 +3165,10 @@ function getProcessCmdlineMac(pid) {
|
|
|
3023
3165
|
}
|
|
3024
3166
|
function getProcessCmdlineWindows(pid) {
|
|
3025
3167
|
try {
|
|
3026
|
-
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
|
+
});
|
|
3027
3172
|
const line = out.split("\n").find((l) => l.includes("="));
|
|
3028
3173
|
const value = line?.split("=")[1]?.trim();
|
|
3029
3174
|
return value ? value : null;
|
|
@@ -3041,7 +3186,10 @@ function getProcessCwd(pid) {
|
|
|
3041
3186
|
}
|
|
3042
3187
|
if (process.platform === "darwin") {
|
|
3043
3188
|
try {
|
|
3044
|
-
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
|
+
});
|
|
3045
3193
|
const line = out.split("\n").find((l) => l.startsWith("n"));
|
|
3046
3194
|
return line ? line.slice(1) : null;
|
|
3047
3195
|
} catch {
|
|
@@ -3089,6 +3237,44 @@ function formatProcessState(state) {
|
|
|
3089
3237
|
}
|
|
3090
3238
|
|
|
3091
3239
|
// src/commands/tracker.ts
|
|
3240
|
+
function extractFlagValue(args2, long, short) {
|
|
3241
|
+
for (let i = 0; i < args2.length; i++) {
|
|
3242
|
+
const a = args2[i];
|
|
3243
|
+
if (a === long || short && a === short) return args2[i + 1];
|
|
3244
|
+
if (a.startsWith(`${long}=`)) return a.slice(long.length + 1);
|
|
3245
|
+
if (short && a.startsWith(`${short}=`)) return a.slice(short.length + 1);
|
|
3246
|
+
}
|
|
3247
|
+
return void 0;
|
|
3248
|
+
}
|
|
3249
|
+
function getGroups() {
|
|
3250
|
+
const tracked = readTracked();
|
|
3251
|
+
const groups = /* @__PURE__ */ new Set();
|
|
3252
|
+
for (const t of tracked) if (t.group) groups.add(t.group);
|
|
3253
|
+
return [...groups].sort();
|
|
3254
|
+
}
|
|
3255
|
+
function resolveTargets(args2) {
|
|
3256
|
+
const groupFlag = extractFlagValue(args2, "--group", "-g");
|
|
3257
|
+
const all = args2.includes("--all") || args2.includes("-a");
|
|
3258
|
+
const positionals = args2.filter((a) => !a.startsWith("-") && !a.includes("="));
|
|
3259
|
+
const groups = getGroups();
|
|
3260
|
+
if (groupFlag) return { kind: "group", group: groupFlag };
|
|
3261
|
+
if (positionals.length === 1 && positionals[0] === "all") return { kind: "all" };
|
|
3262
|
+
if (all) return { kind: "all" };
|
|
3263
|
+
if (positionals.length === 1 && groups.includes(positionals[0])) {
|
|
3264
|
+
return { kind: "group", group: positionals[0] };
|
|
3265
|
+
}
|
|
3266
|
+
if (positionals.length === 1) return { kind: "single", value: positionals[0] };
|
|
3267
|
+
if (positionals.length > 1) return { kind: "names", values: positionals };
|
|
3268
|
+
return { kind: "none" };
|
|
3269
|
+
}
|
|
3270
|
+
function setGroup(name, group) {
|
|
3271
|
+
const tracked = readTracked();
|
|
3272
|
+
const idx = tracked.findIndex((t) => t.name === name);
|
|
3273
|
+
if (idx === -1) return false;
|
|
3274
|
+
tracked[idx] = { ...tracked[idx], group };
|
|
3275
|
+
saveTracked(tracked);
|
|
3276
|
+
return true;
|
|
3277
|
+
}
|
|
3092
3278
|
var RESTART_CAUSE = "fennec:restart";
|
|
3093
3279
|
function getFennecDir() {
|
|
3094
3280
|
return process.env.FENNEC_DATA_DIR ? resolve2(process.env.FENNEC_DATA_DIR) : resolve2(homedir(), ".fennec");
|
|
@@ -3117,7 +3303,10 @@ function saveTracked(processes) {
|
|
|
3117
3303
|
mkdirSync(dirname(path), { recursive: true });
|
|
3118
3304
|
writeFileSync(path, JSON.stringify(processes, null, 2), "utf-8");
|
|
3119
3305
|
} catch (err) {
|
|
3120
|
-
console.error(
|
|
3306
|
+
console.error(
|
|
3307
|
+
"[fennec] Failed to save tracked processes:",
|
|
3308
|
+
err instanceof Error ? err.message : String(err)
|
|
3309
|
+
);
|
|
3121
3310
|
}
|
|
3122
3311
|
}
|
|
3123
3312
|
function addTracked(proc) {
|
|
@@ -3233,7 +3422,10 @@ function rotateLogFile(filePath, maxSize = DEFAULT_MAX_LOG_SIZE, maxFiles = DEFA
|
|
|
3233
3422
|
}
|
|
3234
3423
|
return true;
|
|
3235
3424
|
} catch (err) {
|
|
3236
|
-
console.error(
|
|
3425
|
+
console.error(
|
|
3426
|
+
"[fennec] Log rotation failed:",
|
|
3427
|
+
err instanceof Error ? err.message : String(err)
|
|
3428
|
+
);
|
|
3237
3429
|
return false;
|
|
3238
3430
|
}
|
|
3239
3431
|
}
|
|
@@ -3253,7 +3445,7 @@ function respawnTracked(proc, cause) {
|
|
|
3253
3445
|
if (cause) annotateRestart(logFilePath, cause);
|
|
3254
3446
|
if (isProcessRunning(proc.pid)) {
|
|
3255
3447
|
try {
|
|
3256
|
-
|
|
3448
|
+
killTree(proc.pid, "SIGTERM");
|
|
3257
3449
|
} catch {
|
|
3258
3450
|
}
|
|
3259
3451
|
}
|
|
@@ -3298,15 +3490,11 @@ function spawnDaemon(opts) {
|
|
|
3298
3490
|
return child;
|
|
3299
3491
|
}
|
|
3300
3492
|
function spawnDaemonJsonl(opts, env) {
|
|
3301
|
-
const relay = spawn(
|
|
3302
|
-
process.
|
|
3303
|
-
["
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
stdio: ["pipe", "ignore", "ignore"],
|
|
3307
|
-
detached: true
|
|
3308
|
-
}
|
|
3309
|
-
);
|
|
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
|
+
});
|
|
3310
3498
|
relay.unref();
|
|
3311
3499
|
const child = spawn(opts.cmdParts[0], opts.cmdParts.slice(1), {
|
|
3312
3500
|
cwd: opts.cwd,
|
|
@@ -3362,7 +3550,16 @@ ${RESTART_CAUSE} cause=${cause} at=${(/* @__PURE__ */ new Date()).toISOString()}
|
|
|
3362
3550
|
|
|
3363
3551
|
// src/commands/supervisor.ts
|
|
3364
3552
|
import pc4 from "picocolors";
|
|
3365
|
-
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";
|
|
3366
3563
|
import { spawn as spawn2 } from "child_process";
|
|
3367
3564
|
import { dirname as dirname2 } from "path";
|
|
3368
3565
|
var POLL_INTERVAL_MS = 3e3;
|
|
@@ -3384,6 +3581,38 @@ function ensureSupervisorRunning() {
|
|
|
3384
3581
|
return startSupervisorDaemon();
|
|
3385
3582
|
}
|
|
3386
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
|
+
}
|
|
3387
3616
|
const logFilePath = logFilePathFor("supervisor");
|
|
3388
3617
|
mkdirSync2(dirname2(logFilePath), { recursive: true });
|
|
3389
3618
|
const fd = openSync2(logFilePath, "a");
|
|
@@ -3414,24 +3643,30 @@ async function supervisorCommand(args2) {
|
|
|
3414
3643
|
case "status":
|
|
3415
3644
|
return supervisorStatus();
|
|
3416
3645
|
default:
|
|
3417
|
-
console.error(
|
|
3646
|
+
console.error(
|
|
3647
|
+
renderError("Unknown sub-command", `"${sub}" \u2014 use: start | stop | status | restart`)
|
|
3648
|
+
);
|
|
3418
3649
|
process.exit(1);
|
|
3419
3650
|
}
|
|
3420
3651
|
}
|
|
3421
3652
|
function supervisorStart() {
|
|
3422
3653
|
const existing = getSupervisorPid();
|
|
3423
3654
|
if (existing) {
|
|
3424
|
-
console.error(
|
|
3655
|
+
console.error(
|
|
3656
|
+
`
|
|
3425
3657
|
${pc4.yellow("\u26A0")} Supervisor already running ${pc4.dim(`(PID ${existing})`)}
|
|
3426
|
-
`
|
|
3658
|
+
`
|
|
3659
|
+
);
|
|
3427
3660
|
return;
|
|
3428
3661
|
}
|
|
3429
3662
|
const pid = startSupervisorDaemon();
|
|
3430
3663
|
console.error(`
|
|
3431
3664
|
${pc4.green("\u2713")} ${pc4.bold("Supervisor started")} ${pc4.dim(`(PID ${pid})`)}`);
|
|
3432
3665
|
console.error(` ${renderKV("Logs", pc4.cyan("fennec log supervisor"))}`);
|
|
3433
|
-
console.error(
|
|
3434
|
-
`)
|
|
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
|
+
);
|
|
3435
3670
|
}
|
|
3436
3671
|
function supervisorStop() {
|
|
3437
3672
|
const pid = getSupervisorPid();
|
|
@@ -3442,10 +3677,12 @@ function supervisorStop() {
|
|
|
3442
3677
|
return;
|
|
3443
3678
|
}
|
|
3444
3679
|
try {
|
|
3445
|
-
|
|
3446
|
-
console.error(
|
|
3680
|
+
killTree(pid, "SIGTERM");
|
|
3681
|
+
console.error(
|
|
3682
|
+
`
|
|
3447
3683
|
${pc4.green("\u2713")} ${pc4.bold("Supervisor stopped")} ${pc4.dim(`(PID ${pid})`)}
|
|
3448
|
-
`
|
|
3684
|
+
`
|
|
3685
|
+
);
|
|
3449
3686
|
} catch (err) {
|
|
3450
3687
|
console.error(renderError("Failed to stop supervisor", String(err)));
|
|
3451
3688
|
}
|
|
@@ -3463,12 +3700,18 @@ function supervisorStatus() {
|
|
|
3463
3700
|
if (pid) {
|
|
3464
3701
|
console.error(` ${pc4.green("\u25CF")} running ${pc4.dim(`(PID ${pid})`)}`);
|
|
3465
3702
|
} else {
|
|
3466
|
-
console.error(
|
|
3703
|
+
console.error(
|
|
3704
|
+
` ${pc4.red("\u25CB")} not running ${pc4.dim("\u2014 start with")} ${pc4.cyan("fennec supervisor start")}`
|
|
3705
|
+
);
|
|
3467
3706
|
}
|
|
3468
|
-
console.error(
|
|
3469
|
-
|
|
3707
|
+
console.error(
|
|
3708
|
+
`
|
|
3709
|
+
${pc4.bold("Managed apps")} ${pc4.dim(`(${managed.length} with auto-restart)`)}`
|
|
3710
|
+
);
|
|
3470
3711
|
if (managed.length === 0) {
|
|
3471
|
-
console.error(
|
|
3712
|
+
console.error(
|
|
3713
|
+
` ${pc4.dim("None. Start one with")} ${pc4.cyan("fennec start <cmd> --name <name> --restart")}`
|
|
3714
|
+
);
|
|
3472
3715
|
} else {
|
|
3473
3716
|
for (const t of managed) {
|
|
3474
3717
|
const running = isTrackedRunning(t);
|
|
@@ -3489,6 +3732,10 @@ async function runSupervisor() {
|
|
|
3489
3732
|
return;
|
|
3490
3733
|
}
|
|
3491
3734
|
writeFileSync2(pidPath, String(process.pid), "utf-8");
|
|
3735
|
+
try {
|
|
3736
|
+
unlinkSync(`${pidPath}.lock`);
|
|
3737
|
+
} catch {
|
|
3738
|
+
}
|
|
3492
3739
|
log(`supervisor started (PID ${process.pid}), polling every ${POLL_INTERVAL_MS}ms`);
|
|
3493
3740
|
const crashes = /* @__PURE__ */ new Map();
|
|
3494
3741
|
let stopped = false;
|
|
@@ -3498,6 +3745,10 @@ async function runSupervisor() {
|
|
|
3498
3745
|
unlinkSync(pidPath);
|
|
3499
3746
|
} catch {
|
|
3500
3747
|
}
|
|
3748
|
+
try {
|
|
3749
|
+
unlinkSync(`${pidPath}.lock`);
|
|
3750
|
+
} catch {
|
|
3751
|
+
}
|
|
3501
3752
|
log("supervisor stopping");
|
|
3502
3753
|
process.exit(0);
|
|
3503
3754
|
};
|
|
@@ -3540,7 +3791,9 @@ async function runSupervisor() {
|
|
|
3540
3791
|
rec2.portFails = 0;
|
|
3541
3792
|
rec2.count++;
|
|
3542
3793
|
if (rec2.count >= FLAPPING_THRESHOLD) setFlapping(t, true);
|
|
3543
|
-
log(
|
|
3794
|
+
log(
|
|
3795
|
+
`${t.name}: alive but ${probe} not responding ${PORT_FAIL_THRESHOLD}x \u2014 restarting`
|
|
3796
|
+
);
|
|
3544
3797
|
try {
|
|
3545
3798
|
const newPid = respawnTracked(t, "port-down");
|
|
3546
3799
|
log(`${t.name}: restarted (PID ${newPid}) after ${probe} failure`);
|
|
@@ -3566,14 +3819,18 @@ async function runSupervisor() {
|
|
|
3566
3819
|
if (rec.gaveUp) continue;
|
|
3567
3820
|
if (rec.count >= MAX_RESTARTS_IN_WINDOW) {
|
|
3568
3821
|
rec.gaveUp = true;
|
|
3569
|
-
log(
|
|
3822
|
+
log(
|
|
3823
|
+
`${t.name}: crashed ${rec.count}x in <60s \u2014 giving up (will retry after window resets)`
|
|
3824
|
+
);
|
|
3570
3825
|
continue;
|
|
3571
3826
|
}
|
|
3572
3827
|
rec.count++;
|
|
3573
3828
|
if (rec.count >= FLAPPING_THRESHOLD) setFlapping(t, true);
|
|
3574
3829
|
try {
|
|
3575
3830
|
const newPid = respawnTracked(t, "crash");
|
|
3576
|
-
log(
|
|
3831
|
+
log(
|
|
3832
|
+
`${t.name}: restarted (PID ${newPid}) [${rec.count}/${MAX_RESTARTS_IN_WINDOW} in window]`
|
|
3833
|
+
);
|
|
3577
3834
|
} catch (err) {
|
|
3578
3835
|
log(`${t.name}: restart failed: ${String(err)}`);
|
|
3579
3836
|
}
|
|
@@ -3629,11 +3886,25 @@ function unitPaths(backend) {
|
|
|
3629
3886
|
const fennecDir = getFennecDir();
|
|
3630
3887
|
switch (backend) {
|
|
3631
3888
|
case "systemd":
|
|
3632
|
-
return {
|
|
3889
|
+
return {
|
|
3890
|
+
unit: resolve3(homedir2(), ".config", "systemd", "user", "fennec-supervisor.service")
|
|
3891
|
+
};
|
|
3633
3892
|
case "launchd":
|
|
3634
3893
|
return { unit: resolve3(homedir2(), "Library", "LaunchAgents", "io.fennec.supervisor.plist") };
|
|
3635
3894
|
case "windows":
|
|
3636
|
-
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
|
+
};
|
|
3637
3908
|
default:
|
|
3638
3909
|
return { unit: resolve3(fennecDir, "persist.unit") };
|
|
3639
3910
|
}
|
|
@@ -3714,7 +3985,12 @@ async function persistCommand(args2) {
|
|
|
3714
3985
|
const sub = args2[0] ?? "status";
|
|
3715
3986
|
const backend = detectBackend();
|
|
3716
3987
|
if (backend === "unsupported") {
|
|
3717
|
-
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
|
+
);
|
|
3718
3994
|
process.exit(1);
|
|
3719
3995
|
}
|
|
3720
3996
|
switch (sub) {
|
|
@@ -3725,7 +4001,9 @@ async function persistCommand(args2) {
|
|
|
3725
4001
|
case "status":
|
|
3726
4002
|
return persistStatus(backend);
|
|
3727
4003
|
default:
|
|
3728
|
-
console.error(
|
|
4004
|
+
console.error(
|
|
4005
|
+
renderError("Unknown sub-command", `"${sub}" \u2014 use: enable | disable | status`)
|
|
4006
|
+
);
|
|
3729
4007
|
process.exit(1);
|
|
3730
4008
|
}
|
|
3731
4009
|
}
|
|
@@ -3735,8 +4013,10 @@ function persistEnable(backend) {
|
|
|
3735
4013
|
mkdirSync3(dirname3(unit), { recursive: true });
|
|
3736
4014
|
writeFileSync3(unit, buildUnit(backend, dataDir), "utf-8");
|
|
3737
4015
|
if (backend === "windows") {
|
|
3738
|
-
console.error(
|
|
3739
|
-
|
|
4016
|
+
console.error(
|
|
4017
|
+
`
|
|
4018
|
+
${pc5.green("\u2713")} ${pc5.bold("Boot script installed")} ${pc5.dim(`(runs at next login)`)}`
|
|
4019
|
+
);
|
|
3740
4020
|
console.error(` ${renderKV("Path", pc5.cyan(unit))}`);
|
|
3741
4021
|
console.error(` ${pc5.dim("Supervisor will start automatically when you log in.")}
|
|
3742
4022
|
`);
|
|
@@ -3750,20 +4030,28 @@ function persistEnable(backend) {
|
|
|
3750
4030
|
}
|
|
3751
4031
|
const linger = run("loginctl", ["enable-linger"]);
|
|
3752
4032
|
if (!linger.ok) {
|
|
3753
|
-
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
|
+
);
|
|
3754
4036
|
}
|
|
3755
|
-
console.error(
|
|
3756
|
-
|
|
4037
|
+
console.error(
|
|
4038
|
+
`
|
|
4039
|
+
${pc5.green("\u2713")} ${pc5.bold("Boot persistence enabled")} ${pc5.dim("(systemd user service)")}`
|
|
4040
|
+
);
|
|
3757
4041
|
console.error(` ${renderKV("Unit", pc5.cyan("fennec-supervisor.service"))}`);
|
|
3758
4042
|
console.error(` ${renderKV("Data dir", pc5.cyan(dataDir))}`);
|
|
3759
|
-
console.error(
|
|
3760
|
-
`)
|
|
4043
|
+
console.error(
|
|
4044
|
+
` ${pc5.dim("The supervisor (and all --restart apps) will start at login and keep running after logout.")}
|
|
4045
|
+
`
|
|
4046
|
+
);
|
|
3761
4047
|
return;
|
|
3762
4048
|
}
|
|
3763
4049
|
if (backend === "launchd") {
|
|
3764
4050
|
run("launchctl", ["load", resolve3(unit)]);
|
|
3765
|
-
console.error(
|
|
3766
|
-
|
|
4051
|
+
console.error(
|
|
4052
|
+
`
|
|
4053
|
+
${pc5.green("\u2713")} ${pc5.bold("Boot persistence enabled")} ${pc5.dim("(launchd agent)")}`
|
|
4054
|
+
);
|
|
3767
4055
|
console.error(` ${renderKV("Label", pc5.cyan("io.fennec.supervisor"))}`);
|
|
3768
4056
|
console.error(` ${renderKV("Data dir", pc5.cyan(dataDir))}`);
|
|
3769
4057
|
console.error(` ${pc5.dim("The supervisor will start at login.")}
|
|
@@ -3793,7 +4081,9 @@ function persistStatus(backend) {
|
|
|
3793
4081
|
console.error(`
|
|
3794
4082
|
${pc5.bold("Fennec Boot Persistence")} ${pc5.dim(`(${backend})`)}`);
|
|
3795
4083
|
if (!installed) {
|
|
3796
|
-
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
|
+
);
|
|
3797
4087
|
console.error();
|
|
3798
4088
|
return;
|
|
3799
4089
|
}
|
|
@@ -3822,6 +4112,7 @@ async function psCommand(args2) {
|
|
|
3822
4112
|
const systemFlag = args2.includes("--system") || args2.includes("-a") || args2.includes("--all");
|
|
3823
4113
|
const jsonFlag = args2.includes("--json");
|
|
3824
4114
|
const nameFilter = args2.includes("--name") ? args2[args2.indexOf("--name") + 1] : void 0;
|
|
4115
|
+
const groupFilter = extractFlagValue(args2, "--group", "-g");
|
|
3825
4116
|
const sortBy = args2.includes("--sort") ? args2[args2.indexOf("--sort") + 1] : "name";
|
|
3826
4117
|
if (jsonFlag) {
|
|
3827
4118
|
await psJson();
|
|
@@ -3846,26 +4137,48 @@ async function psCommand(args2) {
|
|
|
3846
4137
|
const columns2 = [
|
|
3847
4138
|
{ key: "pid", label: "PID", align: "right", format: (v) => pc6.dim(String(v).padStart(6)) },
|
|
3848
4139
|
{ key: "name", label: "Name", format: (v) => pc6.bold(String(v)) },
|
|
3849
|
-
{
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
|
|
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
|
+
}
|
|
3864
4169
|
];
|
|
3865
|
-
const rows2 = processes.map((p) => ({
|
|
3866
|
-
|
|
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
|
+
`
|
|
3867
4179
|
${symbols.fox} ${pc6.bold("System Processes")} ${pc6.dim(`(top ${processes.length} by ${sortBy})`)}
|
|
3868
|
-
`
|
|
4180
|
+
`
|
|
4181
|
+
);
|
|
3869
4182
|
console.error(renderTable(columns2, rows2));
|
|
3870
4183
|
console.error();
|
|
3871
4184
|
} catch (error) {
|
|
@@ -3874,46 +4187,102 @@ async function psCommand(args2) {
|
|
|
3874
4187
|
}
|
|
3875
4188
|
return;
|
|
3876
4189
|
}
|
|
3877
|
-
const
|
|
4190
|
+
const trackedAll = readTracked();
|
|
4191
|
+
const tracked = groupFilter ? trackedAll.filter((t) => t.group === groupFilter) : trackedAll;
|
|
3878
4192
|
if (tracked.length === 0) {
|
|
3879
|
-
console.error(
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
|
|
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
|
+
);
|
|
3883
4201
|
return;
|
|
3884
4202
|
}
|
|
3885
4203
|
const columns = [
|
|
3886
4204
|
{ key: "name", label: "App", format: (v) => pc6.bold(String(v)) },
|
|
3887
4205
|
{ key: "pid", label: "PID", align: "right" },
|
|
3888
|
-
{
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
}
|
|
3896
|
-
{
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
|
|
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
|
+
},
|
|
3900
4247
|
{ key: "uptime", label: "Uptime", format: (v) => pc6.dim(String(v)) }
|
|
3901
4248
|
];
|
|
3902
4249
|
const rows = tracked.map((t) => {
|
|
3903
4250
|
const running = isTrackedRunning(t);
|
|
3904
4251
|
const uptime = running ? formatUptime(Math.floor((Date.now() - new Date(t.startedAt).getTime()) / 1e3)) : "-";
|
|
3905
|
-
|
|
4252
|
+
const memKb = running ? getProcessMemRss(t.pid) ?? null : null;
|
|
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
|
+
};
|
|
3906
4263
|
});
|
|
3907
4264
|
const runningCount = tracked.filter((t) => isTrackedRunning(t)).length;
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
`
|
|
4265
|
+
const scope = groupFilter ? ` in group ${pc6.cyan(groupFilter)}` : "";
|
|
4266
|
+
console.error(
|
|
4267
|
+
`
|
|
4268
|
+
${symbols.fox} ${pc6.bold("Fennec Apps")} ${pc6.dim(`(${runningCount}/${tracked.length} running${scope})`)}
|
|
4269
|
+
`
|
|
4270
|
+
);
|
|
3911
4271
|
console.error(renderTable(columns, rows));
|
|
3912
|
-
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
|
+
);
|
|
3913
4275
|
console.error(` ${pc6.dim("Use")} ${pc6.cyan("fennec log <name>")} ${pc6.dim("to view logs.")}`);
|
|
3914
|
-
console.error(
|
|
3915
|
-
|
|
3916
|
-
|
|
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
|
+
);
|
|
4285
|
+
console.error(` ${pc6.dim("Filter by group with:")} ${pc6.cyan("fennec ps --group <group>")}`);
|
|
3917
4286
|
console.error();
|
|
3918
4287
|
}
|
|
3919
4288
|
async function psJson() {
|
|
@@ -3927,6 +4296,11 @@ async function psJson() {
|
|
|
3927
4296
|
port: t.port ?? null,
|
|
3928
4297
|
command: t.command,
|
|
3929
4298
|
cwd: t.cwd ?? null,
|
|
4299
|
+
group: t.group ?? null,
|
|
4300
|
+
memMB: running ? (() => {
|
|
4301
|
+
const kb = getProcessMemRss(t.pid);
|
|
4302
|
+
return kb ? Math.round(kb / 1024) : null;
|
|
4303
|
+
})() : null,
|
|
3930
4304
|
startedAt: t.startedAt,
|
|
3931
4305
|
uptime: running ? Math.floor((Date.now() - new Date(t.startedAt).getTime()) / 1e3) : null
|
|
3932
4306
|
};
|
|
@@ -3943,19 +4317,25 @@ async function statusCommand(_args) {
|
|
|
3943
4317
|
`);
|
|
3944
4318
|
if (tracked.length > 0) {
|
|
3945
4319
|
const runningCount = tracked.filter((t) => isTrackedRunning(t)).length;
|
|
3946
|
-
console.error(
|
|
3947
|
-
`)
|
|
4320
|
+
console.error(
|
|
4321
|
+
` ${pc6.bold("Managed Apps")} ${pc6.dim(`(${runningCount}/${tracked.length} running)`)}
|
|
4322
|
+
`
|
|
4323
|
+
);
|
|
3948
4324
|
for (const t of tracked) {
|
|
3949
4325
|
const running = isTrackedRunning(t);
|
|
3950
4326
|
const statusIcon = running ? pc6.green("\u25CF") : pc6.red("\u25CB");
|
|
3951
4327
|
const portStr = t.port ? ` ${pc6.yellow(`:${t.port}`)}` : "";
|
|
3952
4328
|
const uptime = running ? pc6.dim(formatUptime(Math.floor((Date.now() - new Date(t.startedAt).getTime()) / 1e3))) : pc6.red("stopped");
|
|
3953
|
-
console.error(
|
|
4329
|
+
console.error(
|
|
4330
|
+
` ${statusIcon} ${pc6.bold(t.name)}${portStr} ${pc6.dim(`(PID ${t.pid})`)} \u2014 ${uptime}`
|
|
4331
|
+
);
|
|
3954
4332
|
}
|
|
3955
4333
|
console.error();
|
|
3956
4334
|
} else {
|
|
3957
|
-
console.error(
|
|
3958
|
-
`)
|
|
4335
|
+
console.error(
|
|
4336
|
+
` ${pc6.dim("No managed apps.")} ${pc6.cyan("fennec start <command> --name <name>")}
|
|
4337
|
+
`
|
|
4338
|
+
);
|
|
3959
4339
|
}
|
|
3960
4340
|
console.error(` ${pc6.bold("System")} ${pc6.dim(`(${totalUserProcs} user processes)`)}`);
|
|
3961
4341
|
for (const p of topSystem) {
|
|
@@ -3967,25 +4347,47 @@ async function statusCommand(_args) {
|
|
|
3967
4347
|
console.error();
|
|
3968
4348
|
}
|
|
3969
4349
|
async function watchSystemProcesses(sortBy, limit) {
|
|
3970
|
-
console.error(
|
|
4350
|
+
console.error(
|
|
4351
|
+
`
|
|
3971
4352
|
${pc6.bold("Watching system processes")} ${pc6.dim("(Ctrl+C to stop, refreshes every 3s)")}
|
|
3972
|
-
`
|
|
4353
|
+
`
|
|
4354
|
+
);
|
|
3973
4355
|
const render = () => {
|
|
3974
|
-
const processes = getSystemProcesses({
|
|
4356
|
+
const processes = getSystemProcesses({
|
|
4357
|
+
userOnly: true,
|
|
4358
|
+
sortBy,
|
|
4359
|
+
limit
|
|
4360
|
+
});
|
|
3975
4361
|
const columns = [
|
|
3976
4362
|
{ key: "pid", label: "PID", align: "right", format: (v) => pc6.dim(String(v).padStart(6)) },
|
|
3977
4363
|
{ key: "name", label: "Name", format: (v) => pc6.bold(String(v)) },
|
|
3978
|
-
{
|
|
3979
|
-
|
|
3980
|
-
|
|
3981
|
-
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
|
|
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
|
+
},
|
|
3986
4382
|
{ key: "state", label: "S", align: "center" }
|
|
3987
4383
|
];
|
|
3988
|
-
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
|
+
}));
|
|
3989
4391
|
return ` ${timestamp()} ${pc6.dim(`${processes.length} processes`)}
|
|
3990
4392
|
${renderTable(columns, rows, { compact: true })}`;
|
|
3991
4393
|
};
|
|
@@ -3994,11 +4396,11 @@ ${renderTable(columns, rows, { compact: true })}`;
|
|
|
3994
4396
|
process.stdout.write("\x1B[J");
|
|
3995
4397
|
console.error(render());
|
|
3996
4398
|
}, 3e3);
|
|
3997
|
-
await new Promise((
|
|
4399
|
+
await new Promise((resolve12) => {
|
|
3998
4400
|
process.once("SIGINT", () => {
|
|
3999
4401
|
clearInterval(interval);
|
|
4000
4402
|
process.stdout.write("\x1B[J");
|
|
4001
|
-
|
|
4403
|
+
resolve12();
|
|
4002
4404
|
});
|
|
4003
4405
|
});
|
|
4004
4406
|
}
|
|
@@ -4040,8 +4442,12 @@ async function startServer(args2) {
|
|
|
4040
4442
|
${pc7.green("\u2713")} ${pc7.bold("Fennec server is running")}`);
|
|
4041
4443
|
console.error(` ${renderKV("Transport", pc7.cyan("SSE (HTTP)"))}`);
|
|
4042
4444
|
console.error(` ${renderKV("URL", pc7.cyan(`http://localhost:${port}/sse`))}`);
|
|
4043
|
-
console.error(
|
|
4044
|
-
|
|
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
|
+
);
|
|
4045
4451
|
console.error(`
|
|
4046
4452
|
${pc7.dim("Press Ctrl+C to stop")}
|
|
4047
4453
|
`);
|
|
@@ -4075,20 +4481,28 @@ async function runCommand(args2) {
|
|
|
4075
4481
|
const cwd = cwdIndex !== -1 ? resolve4(args2[cwdIndex + 1]) : process.cwd();
|
|
4076
4482
|
const restartFlag = args2.includes("--restart");
|
|
4077
4483
|
const jsonlFlag = args2.includes("--jsonl");
|
|
4484
|
+
const group = extractFlagValue(args2, "--group", "-g");
|
|
4078
4485
|
const stopFlags = [nameIndex, portIndex, cwdIndex].filter((i) => i !== -1);
|
|
4079
4486
|
const cmdEnd = Math.min(...stopFlags, Infinity);
|
|
4080
4487
|
const cmdParts = args2.slice(0, cmdEnd);
|
|
4081
4488
|
const cmd = cmdParts.join(" ");
|
|
4082
4489
|
if (!cmd) {
|
|
4083
|
-
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
|
+
);
|
|
4084
4496
|
process.exit(1);
|
|
4085
4497
|
}
|
|
4086
4498
|
const appName = name ?? cmdParts[0] ?? "app";
|
|
4087
4499
|
if (port !== void 0) {
|
|
4088
4500
|
const adopted = adoptExternalOnPort(port, appName);
|
|
4089
4501
|
if (adopted) {
|
|
4090
|
-
console.error(
|
|
4091
|
-
|
|
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
|
+
);
|
|
4092
4506
|
console.error(` ${renderKV("Logs", pc7.cyan(`fennec log ${adopted.name}`))}`);
|
|
4093
4507
|
console.error();
|
|
4094
4508
|
return;
|
|
@@ -4098,7 +4512,9 @@ async function runCommand(args2) {
|
|
|
4098
4512
|
const existing = tracked.find((t) => t.name === appName);
|
|
4099
4513
|
if (existing && isTrackedRunning(existing)) {
|
|
4100
4514
|
console.error();
|
|
4101
|
-
console.error(
|
|
4515
|
+
console.error(
|
|
4516
|
+
` ${pc7.yellow("\u26A0")} ${pc7.bold(appName)} ${pc7.dim(`is already running (PID: ${existing.pid})`)}`
|
|
4517
|
+
);
|
|
4102
4518
|
console.error(` ${renderKV("Logs", pc7.cyan(`fennec log ${appName}`))}`);
|
|
4103
4519
|
console.error(` ${renderKV("Stop", pc7.cyan(`fennec kill ${appName}`))}`);
|
|
4104
4520
|
console.error();
|
|
@@ -4107,20 +4523,30 @@ async function runCommand(args2) {
|
|
|
4107
4523
|
const logDir = dirname4(logFilePathFor(appName));
|
|
4108
4524
|
mkdirSync4(logDir, { recursive: true });
|
|
4109
4525
|
const logFilePath = logFilePathFor(appName);
|
|
4110
|
-
console.error(
|
|
4526
|
+
console.error(
|
|
4527
|
+
`
|
|
4111
4528
|
${symbols.fox} ${pc7.bold("Starting")} ${renderAppName(appName)} ${pc7.dim("(daemon)")}
|
|
4112
|
-
`
|
|
4529
|
+
`
|
|
4530
|
+
);
|
|
4113
4531
|
console.error(` ${renderKV("Command", cmd)}`);
|
|
4114
4532
|
if (port) console.error(` ${renderKV("Port", String(port))}`);
|
|
4115
4533
|
if (cwd) console.error(` ${renderKV("Directory", cwd)}`);
|
|
4116
4534
|
if (restartFlag) console.error(` ${renderKV("Auto-restart", pc7.green("enabled"))}`);
|
|
4535
|
+
if (group) console.error(` ${renderKV("Group", group)}`);
|
|
4117
4536
|
console.error(` ${divider()}`);
|
|
4118
4537
|
try {
|
|
4119
4538
|
const startEnv = {};
|
|
4120
4539
|
for (const [k, v] of Object.entries(process.env)) {
|
|
4121
4540
|
if (v !== void 0) startEnv[k] = v;
|
|
4122
4541
|
}
|
|
4123
|
-
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
|
+
});
|
|
4124
4550
|
const pid = currentChild.pid ?? 0;
|
|
4125
4551
|
addTracked({
|
|
4126
4552
|
name: appName,
|
|
@@ -4132,31 +4558,42 @@ async function runCommand(args2) {
|
|
|
4132
4558
|
env: startEnv,
|
|
4133
4559
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4134
4560
|
autoRestart: restartFlag,
|
|
4135
|
-
logMode: jsonlFlag ? "jsonl" : "text"
|
|
4561
|
+
logMode: jsonlFlag ? "jsonl" : "text",
|
|
4562
|
+
group
|
|
4136
4563
|
});
|
|
4137
4564
|
console.error(` ${pc7.green("\u2713")} ${pc7.bold(appName)} ${pc7.dim(`started (PID: ${pid})`)}`);
|
|
4138
|
-
const spinner = createSpinner(
|
|
4565
|
+
const spinner = createSpinner(
|
|
4566
|
+
port ? `Waiting for ${appName} on port ${port}...` : `Confirming ${appName} is running...`
|
|
4567
|
+
);
|
|
4139
4568
|
const ready = await waitForReady(pid, port);
|
|
4140
4569
|
spinner.stop();
|
|
4141
4570
|
process.stdout.write("\r\x1B[K");
|
|
4142
4571
|
if (!ready.running) {
|
|
4143
4572
|
removeTracked(appName);
|
|
4144
|
-
console.error(
|
|
4573
|
+
console.error(
|
|
4574
|
+
` ${pc7.red("\u2717")} ${pc7.bold(appName)} ${pc7.dim("exited immediately after start")}`
|
|
4575
|
+
);
|
|
4145
4576
|
console.error(` ${renderKV("Logs", pc7.cyan(`fennec log ${appName}`))}`);
|
|
4146
4577
|
console.error();
|
|
4147
4578
|
process.exit(1);
|
|
4148
4579
|
}
|
|
4149
4580
|
if (port) {
|
|
4150
4581
|
if (ready.portReady) {
|
|
4151
|
-
console.error(
|
|
4582
|
+
console.error(
|
|
4583
|
+
` ${pc7.green("\u2713")} ${pc7.bold(appName)} ${pc7.dim(`is listening on port ${port}`)}`
|
|
4584
|
+
);
|
|
4152
4585
|
} else {
|
|
4153
|
-
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
|
+
);
|
|
4154
4589
|
}
|
|
4155
4590
|
}
|
|
4156
4591
|
if (restartFlag) {
|
|
4157
4592
|
const supPid = ensureSupervisorRunning();
|
|
4158
4593
|
ensurePersistEnabled();
|
|
4159
|
-
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
|
+
);
|
|
4160
4597
|
console.error(` ${renderKV("Supervisor", pc7.cyan("fennec supervisor status"))}`);
|
|
4161
4598
|
}
|
|
4162
4599
|
await psCommand([]);
|
|
@@ -4181,7 +4618,14 @@ async function resurrectTracked() {
|
|
|
4181
4618
|
try {
|
|
4182
4619
|
const cmdParts = resolveArgs(proc);
|
|
4183
4620
|
const logFilePath = logFilePathFor(proc.name);
|
|
4184
|
-
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
|
+
});
|
|
4185
4629
|
addTracked({
|
|
4186
4630
|
name: proc.name,
|
|
4187
4631
|
pid: child.pid ?? 0,
|
|
@@ -4213,25 +4657,49 @@ async function resurrectTracked() {
|
|
|
4213
4657
|
// src/commands/kill.ts
|
|
4214
4658
|
import pc8 from "picocolors";
|
|
4215
4659
|
async function killCommand(args2) {
|
|
4216
|
-
const rawTarget = args2[0];
|
|
4217
4660
|
const signalIndex = args2.indexOf("--signal");
|
|
4218
4661
|
const signalRaw = signalIndex !== -1 ? args2[signalIndex + 1] : "SIGTERM";
|
|
4219
4662
|
const signal = signalRaw ?? "SIGTERM";
|
|
4220
4663
|
const force = args2.includes("-y") || args2.includes("--yes");
|
|
4221
|
-
|
|
4222
|
-
|
|
4664
|
+
const target = resolveTargets(args2);
|
|
4665
|
+
if (target.kind === "single") {
|
|
4666
|
+
await killOne(target.value, signal, force, false);
|
|
4223
4667
|
return;
|
|
4224
4668
|
}
|
|
4225
|
-
if (
|
|
4226
|
-
|
|
4227
|
-
|
|
4669
|
+
if (target.kind === "names") {
|
|
4670
|
+
for (const name of target.values) {
|
|
4671
|
+
await killOne(name, signal, force, true);
|
|
4672
|
+
}
|
|
4673
|
+
return;
|
|
4228
4674
|
}
|
|
4675
|
+
if (target.kind === "group") {
|
|
4676
|
+
await killGroup(target.group, signal, force);
|
|
4677
|
+
return;
|
|
4678
|
+
}
|
|
4679
|
+
if (target.kind === "all") {
|
|
4680
|
+
await killAll(signal, force);
|
|
4681
|
+
return;
|
|
4682
|
+
}
|
|
4683
|
+
console.error(
|
|
4684
|
+
renderError(
|
|
4685
|
+
"Missing target",
|
|
4686
|
+
"Usage: fennec kill <pid|name|all> [--signal SIGTERM|SIGKILL|SIGINT] [--group <group>] [-y]"
|
|
4687
|
+
)
|
|
4688
|
+
);
|
|
4689
|
+
process.exit(1);
|
|
4690
|
+
}
|
|
4691
|
+
async function killOne(rawTarget, signal, force, multi) {
|
|
4229
4692
|
let targetPid;
|
|
4230
4693
|
let displayName;
|
|
4231
4694
|
const pid = parseInt(rawTarget, 10);
|
|
4232
4695
|
if (!isNaN(pid) && String(pid) === rawTarget) {
|
|
4233
4696
|
if (!isProcessRunning(pid)) {
|
|
4234
|
-
|
|
4697
|
+
const msg = `No process with PID ${pid} is running`;
|
|
4698
|
+
if (multi) {
|
|
4699
|
+
console.error(renderError("Process not found", msg));
|
|
4700
|
+
return;
|
|
4701
|
+
}
|
|
4702
|
+
console.error(renderError("Process not found", msg));
|
|
4235
4703
|
process.exit(1);
|
|
4236
4704
|
}
|
|
4237
4705
|
targetPid = pid;
|
|
@@ -4242,16 +4710,23 @@ async function killCommand(args2) {
|
|
|
4242
4710
|
if (trackedMatch) {
|
|
4243
4711
|
if (!isTrackedRunning(trackedMatch)) {
|
|
4244
4712
|
removeTrackedByPid(trackedMatch.pid);
|
|
4245
|
-
console.error(
|
|
4246
|
-
|
|
4713
|
+
console.error(
|
|
4714
|
+
`
|
|
4715
|
+
${pc8.green("\u2713")} ${pc8.bold(rawTarget)} ${pc8.dim("removed from tracked apps (was already stopped)")}`
|
|
4716
|
+
);
|
|
4247
4717
|
console.error();
|
|
4248
4718
|
return;
|
|
4249
4719
|
}
|
|
4250
4720
|
targetPid = trackedMatch.pid;
|
|
4251
4721
|
displayName = `${trackedMatch.name} (PID ${targetPid})`;
|
|
4252
4722
|
} else {
|
|
4253
|
-
|
|
4254
|
-
Use ${pc8.cyan(
|
|
4723
|
+
const msg = `No tracked process named "${rawTarget}".
|
|
4724
|
+
Use ${pc8.cyan("fennec kill <pid>")} to kill a system process by its PID.`;
|
|
4725
|
+
if (multi) {
|
|
4726
|
+
console.error(renderError("Not tracked", msg));
|
|
4727
|
+
return;
|
|
4728
|
+
}
|
|
4729
|
+
console.error(renderError("Not tracked", msg));
|
|
4255
4730
|
process.exit(1);
|
|
4256
4731
|
}
|
|
4257
4732
|
}
|
|
@@ -4261,7 +4736,7 @@ Use ${pc8.cyan(`fennec kill <pid>`)} to kill a system process by its PID.`));
|
|
|
4261
4736
|
return;
|
|
4262
4737
|
}
|
|
4263
4738
|
const spinner = createSpinner(`Sending ${signal} to ${displayName}...`);
|
|
4264
|
-
const success =
|
|
4739
|
+
const success = killTree(targetPid, signal);
|
|
4265
4740
|
if (success) {
|
|
4266
4741
|
await new Promise((r) => setTimeout(r, 200));
|
|
4267
4742
|
const stillRunning = isProcessRunning(targetPid);
|
|
@@ -4270,12 +4745,12 @@ Use ${pc8.cyan(`fennec kill <pid>`)} to kill a system process by its PID.`));
|
|
|
4270
4745
|
const forceKill = force || await confirmPrompt(`Send ${pc8.red("SIGKILL")} to force stop?`, true);
|
|
4271
4746
|
if (forceKill) {
|
|
4272
4747
|
const forceSpinner = createSpinner(`Sending SIGKILL to ${displayName}...`);
|
|
4273
|
-
|
|
4748
|
+
killTree(targetPid, "SIGKILL");
|
|
4274
4749
|
await new Promise((r) => setTimeout(r, 300));
|
|
4275
4750
|
isProcessRunning(targetPid) ? forceSpinner.fail(`${displayName} could not be stopped (permission denied?)`) : forceSpinner.succeed(`${displayName} force stopped`);
|
|
4276
4751
|
} else {
|
|
4277
4752
|
console.error(` ${pc8.dim("Retrying with SIGTERM...")}`);
|
|
4278
|
-
|
|
4753
|
+
killTree(targetPid, "SIGTERM");
|
|
4279
4754
|
console.error(` ${pc8.yellow("\u26A0")} ${displayName} ${pc8.dim("may still be running")}`);
|
|
4280
4755
|
}
|
|
4281
4756
|
} else {
|
|
@@ -4284,30 +4759,45 @@ Use ${pc8.cyan(`fennec kill <pid>`)} to kill a system process by its PID.`));
|
|
|
4284
4759
|
}
|
|
4285
4760
|
} else {
|
|
4286
4761
|
spinner.fail(`Failed to kill ${displayName}`);
|
|
4287
|
-
console.error(
|
|
4762
|
+
console.error(
|
|
4763
|
+
renderError("Permission denied", "Try running with sudo or use a different signal.")
|
|
4764
|
+
);
|
|
4288
4765
|
}
|
|
4289
4766
|
}
|
|
4290
|
-
async function
|
|
4767
|
+
async function killGroup(group, signal, force) {
|
|
4291
4768
|
const tracked = readTracked();
|
|
4292
|
-
|
|
4293
|
-
|
|
4294
|
-
|
|
4769
|
+
const inGroup = tracked.filter((t) => t.group === group);
|
|
4770
|
+
if (inGroup.length === 0) {
|
|
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
|
+
);
|
|
4778
|
+
process.exit(1);
|
|
4295
4779
|
}
|
|
4296
|
-
const running =
|
|
4297
|
-
const stopped =
|
|
4298
|
-
console.error(
|
|
4299
|
-
|
|
4300
|
-
|
|
4301
|
-
|
|
4780
|
+
const running = inGroup.filter((t) => isTrackedRunning(t));
|
|
4781
|
+
const stopped = inGroup.filter((t) => !isTrackedRunning(t));
|
|
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
|
+
);
|
|
4302
4790
|
const confirmed = force || await confirmPrompt(`${pc8.red("Are you sure?")} ${pc8.dim("This cannot be undone")}`, false);
|
|
4303
4791
|
if (!confirmed) {
|
|
4304
4792
|
console.error(` ${pc8.dim("Cancelled.")}`);
|
|
4305
4793
|
return;
|
|
4306
4794
|
}
|
|
4307
|
-
const spinner = createSpinner(
|
|
4795
|
+
const spinner = createSpinner(
|
|
4796
|
+
`Killing ${running.length} + removing ${stopped.length} tracked app(s) in ${group}...`
|
|
4797
|
+
);
|
|
4308
4798
|
let killed = 0, failed = 0;
|
|
4309
4799
|
for (const t of running) {
|
|
4310
|
-
if (
|
|
4800
|
+
if (killTree(t.pid, signal)) {
|
|
4311
4801
|
killed++;
|
|
4312
4802
|
} else {
|
|
4313
4803
|
failed++;
|
|
@@ -4319,38 +4809,105 @@ async function killAll(signal, force) {
|
|
|
4319
4809
|
}
|
|
4320
4810
|
await new Promise((r) => setTimeout(r, 300));
|
|
4321
4811
|
const total = running.length + stopped.length;
|
|
4322
|
-
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");
|
|
4323
4815
|
if (failed > 0) console.error(` ${pc8.yellow(`${failed} running app(s) could not be killed`)}`);
|
|
4324
4816
|
}
|
|
4325
|
-
|
|
4326
|
-
|
|
4327
|
-
|
|
4328
|
-
|
|
4329
|
-
const stopAll = args2.includes("--all") || args2.includes("-a");
|
|
4330
|
-
if (stopAll) {
|
|
4331
|
-
await stopAllTracked(args2);
|
|
4817
|
+
async function killAll(signal, force) {
|
|
4818
|
+
const tracked = readTracked();
|
|
4819
|
+
if (tracked.length === 0) {
|
|
4820
|
+
console.error(` ${pc8.dim("No tracked apps to kill.")}`);
|
|
4332
4821
|
return;
|
|
4333
4822
|
}
|
|
4334
|
-
const
|
|
4335
|
-
|
|
4336
|
-
|
|
4337
|
-
|
|
4338
|
-
}
|
|
4339
|
-
|
|
4340
|
-
|
|
4823
|
+
const running = tracked.filter((t) => isTrackedRunning(t));
|
|
4824
|
+
const stopped = tracked.filter((t) => !isTrackedRunning(t));
|
|
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
|
+
);
|
|
4833
|
+
const confirmed = force || await confirmPrompt(`${pc8.red("Are you sure?")} ${pc8.dim("This cannot be undone")}`, false);
|
|
4834
|
+
if (!confirmed) {
|
|
4835
|
+
console.error(` ${pc8.dim("Cancelled.")}`);
|
|
4836
|
+
return;
|
|
4837
|
+
}
|
|
4838
|
+
const spinner = createSpinner(
|
|
4839
|
+
`Killing ${running.length} + removing ${stopped.length} tracked app(s)...`
|
|
4840
|
+
);
|
|
4841
|
+
let killed = 0, failed = 0;
|
|
4842
|
+
for (const t of running) {
|
|
4843
|
+
if (killTree(t.pid, signal)) {
|
|
4844
|
+
killed++;
|
|
4845
|
+
} else {
|
|
4846
|
+
failed++;
|
|
4847
|
+
}
|
|
4848
|
+
removeTrackedByPid(t.pid);
|
|
4849
|
+
}
|
|
4850
|
+
for (const t of stopped) {
|
|
4851
|
+
removeTrackedByPid(t.pid);
|
|
4852
|
+
}
|
|
4853
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
4854
|
+
const total = running.length + stopped.length;
|
|
4855
|
+
total > 0 ? spinner.succeed(`${killed} killed, ${stopped.length} stopped entries removed`) : spinner.fail("Failed to kill apps");
|
|
4856
|
+
if (failed > 0) console.error(` ${pc8.yellow(`${failed} running app(s) could not be killed`)}`);
|
|
4857
|
+
}
|
|
4858
|
+
|
|
4859
|
+
// src/commands/stop.ts
|
|
4860
|
+
import pc9 from "picocolors";
|
|
4861
|
+
async function stopCommand(args2) {
|
|
4862
|
+
const target = resolveTargets(args2);
|
|
4863
|
+
if (target.kind === "group") {
|
|
4864
|
+
await stopAllTracked(args2, target.group);
|
|
4865
|
+
return;
|
|
4866
|
+
}
|
|
4867
|
+
if (target.kind === "all") {
|
|
4868
|
+
await stopAllTracked(args2);
|
|
4869
|
+
return;
|
|
4870
|
+
}
|
|
4871
|
+
if (target.kind === "single") {
|
|
4872
|
+
await stopOne(target.value, args2);
|
|
4873
|
+
return;
|
|
4874
|
+
}
|
|
4875
|
+
if (target.kind === "names") {
|
|
4876
|
+
for (const name of target.values) {
|
|
4877
|
+
await stopOne(name, args2, true);
|
|
4878
|
+
}
|
|
4879
|
+
return;
|
|
4880
|
+
}
|
|
4881
|
+
console.error(
|
|
4882
|
+
renderError("Missing name", "Usage: fennec stop <name|--all> [--group <group>] [-y]")
|
|
4883
|
+
);
|
|
4884
|
+
process.exit(1);
|
|
4885
|
+
}
|
|
4886
|
+
async function stopOne(rawTarget, args2, multi = false) {
|
|
4887
|
+
const tracked = readTracked();
|
|
4888
|
+
const trackedMatch = tracked.find((t) => t.name === rawTarget);
|
|
4341
4889
|
if (!trackedMatch) {
|
|
4342
|
-
|
|
4890
|
+
const msg = `No tracked process named "${rawTarget}". Use ${pc9.cyan("fennec kill <pid>")} to kill a system process.`;
|
|
4891
|
+
if (multi) {
|
|
4892
|
+
console.error(renderError("Not found", msg));
|
|
4893
|
+
return;
|
|
4894
|
+
}
|
|
4895
|
+
console.error(renderError("Not found", msg));
|
|
4343
4896
|
process.exit(1);
|
|
4344
4897
|
}
|
|
4345
4898
|
if (!isTrackedRunning(trackedMatch)) {
|
|
4346
4899
|
console.error(`
|
|
4347
4900
|
${pc9.yellow("\u26A0")} ${pc9.bold(rawTarget)} ${pc9.dim("is already stopped")}`);
|
|
4348
4901
|
console.error();
|
|
4349
|
-
process.exit(0);
|
|
4902
|
+
if (!multi) process.exit(0);
|
|
4903
|
+
return;
|
|
4350
4904
|
}
|
|
4351
4905
|
const displayName = `${trackedMatch.name} (PID ${trackedMatch.pid})`;
|
|
4352
4906
|
const force = args2.includes("-y") || args2.includes("--yes");
|
|
4353
|
-
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
|
+
);
|
|
4354
4911
|
if (!confirmed) {
|
|
4355
4912
|
console.error(` ${pc9.dim("Cancelled")}`);
|
|
4356
4913
|
return;
|
|
@@ -4358,18 +4915,23 @@ async function stopCommand(args2) {
|
|
|
4358
4915
|
setAutoRestart(trackedMatch.name, false);
|
|
4359
4916
|
await stopSingleProcess(trackedMatch.pid, trackedMatch.name);
|
|
4360
4917
|
}
|
|
4361
|
-
async function stopAllTracked(args2) {
|
|
4918
|
+
async function stopAllTracked(args2, group) {
|
|
4362
4919
|
const tracked = readTracked();
|
|
4363
|
-
const running = tracked.filter((t) => isTrackedRunning(t));
|
|
4920
|
+
const running = tracked.filter((t) => isTrackedRunning(t) && (!group || t.group === group));
|
|
4364
4921
|
if (running.length === 0) {
|
|
4365
|
-
console.error(
|
|
4366
|
-
|
|
4367
|
-
`)
|
|
4922
|
+
console.error(
|
|
4923
|
+
`
|
|
4924
|
+
${pc9.dim(group ? `No running tracked processes in group "${group}".` : "No running tracked processes to stop.")}
|
|
4925
|
+
`
|
|
4926
|
+
);
|
|
4368
4927
|
return;
|
|
4369
4928
|
}
|
|
4370
|
-
|
|
4371
|
-
|
|
4372
|
-
`
|
|
4929
|
+
const scope = group ? ` group ${pc9.cyan(group)}` : "";
|
|
4930
|
+
console.error(
|
|
4931
|
+
`
|
|
4932
|
+
${pc9.yellow("\u26A0")} ${pc9.bold(`Stop all ${running.length} running process(es)${scope}?`)} ${pc9.dim("(They can be re-spawned later)")}
|
|
4933
|
+
`
|
|
4934
|
+
);
|
|
4373
4935
|
for (const t of running) {
|
|
4374
4936
|
console.error(` ${pc9.green("\u25CF")} ${pc9.bold(t.name)} ${pc9.dim(`(PID ${t.pid})`)}`);
|
|
4375
4937
|
}
|
|
@@ -4386,7 +4948,7 @@ async function stopAllTracked(args2) {
|
|
|
4386
4948
|
let failed = 0;
|
|
4387
4949
|
for (const t of running) {
|
|
4388
4950
|
setAutoRestart(t.name, false);
|
|
4389
|
-
if (
|
|
4951
|
+
if (killTree(t.pid, "SIGTERM")) {
|
|
4390
4952
|
stopped++;
|
|
4391
4953
|
} else {
|
|
4392
4954
|
failed++;
|
|
@@ -4396,7 +4958,9 @@ async function stopAllTracked(args2) {
|
|
|
4396
4958
|
spinner.stop();
|
|
4397
4959
|
process.stdout.write("\r\x1B[K");
|
|
4398
4960
|
if (stopped > 0) {
|
|
4399
|
-
console.error(
|
|
4961
|
+
console.error(
|
|
4962
|
+
` ${pc9.green("\u2713")} ${pc9.bold(`Stopped ${stopped} process(es)`)} ${pc9.dim("(kept in tracked.json)")}`
|
|
4963
|
+
);
|
|
4400
4964
|
}
|
|
4401
4965
|
if (failed > 0) {
|
|
4402
4966
|
console.error(` ${pc9.red("\u2717")} ${pc9.bold(`${failed} process(es) failed to stop`)}`);
|
|
@@ -4407,7 +4971,7 @@ async function stopAllTracked(args2) {
|
|
|
4407
4971
|
async function stopSingleProcess(pid, name) {
|
|
4408
4972
|
const displayName = `${name} (PID ${pid})`;
|
|
4409
4973
|
const spinner = createSpinner(`Stopping ${displayName}...`);
|
|
4410
|
-
const success =
|
|
4974
|
+
const success = killTree(pid, "SIGTERM");
|
|
4411
4975
|
if (success) {
|
|
4412
4976
|
await new Promise((r) => setTimeout(r, 200));
|
|
4413
4977
|
const stillRunning = isProcessRunning(pid);
|
|
@@ -4416,7 +4980,7 @@ async function stopSingleProcess(pid, name) {
|
|
|
4416
4980
|
const forceKill = await confirmPrompt(`Send ${pc9.red("SIGKILL")} to force stop?`, true);
|
|
4417
4981
|
if (forceKill) {
|
|
4418
4982
|
const forceSpinner = createSpinner(`Sending SIGKILL to ${displayName}...`);
|
|
4419
|
-
|
|
4983
|
+
killTree(pid, "SIGKILL");
|
|
4420
4984
|
await new Promise((r) => setTimeout(r, 300));
|
|
4421
4985
|
isProcessRunning(pid) ? forceSpinner.fail(`${displayName} could not be stopped (permission denied?)`) : forceSpinner.succeed(`${displayName} force stopped`);
|
|
4422
4986
|
} else {
|
|
@@ -4439,38 +5003,77 @@ import { mkdirSync as mkdirSync5 } from "fs";
|
|
|
4439
5003
|
import { resolve as resolve5 } from "path";
|
|
4440
5004
|
import { homedir as homedir3 } from "os";
|
|
4441
5005
|
async function spawnCommand(args2) {
|
|
4442
|
-
const
|
|
4443
|
-
|
|
4444
|
-
|
|
4445
|
-
await spawnAllStopped();
|
|
5006
|
+
const target = resolveTargets(args2);
|
|
5007
|
+
if (target.kind === "single") {
|
|
5008
|
+
await spawnOne(target.value, false);
|
|
4446
5009
|
return;
|
|
4447
5010
|
}
|
|
4448
|
-
if (
|
|
4449
|
-
|
|
5011
|
+
if (target.kind === "names") {
|
|
5012
|
+
for (const name of target.values) {
|
|
5013
|
+
await spawnOne(name, true);
|
|
5014
|
+
}
|
|
5015
|
+
return;
|
|
5016
|
+
}
|
|
5017
|
+
if (target.kind === "group") {
|
|
5018
|
+
const group = target.group;
|
|
5019
|
+
const tracked = readTracked();
|
|
5020
|
+
const inGroup = tracked.filter((t) => t.group === group && !isTrackedRunning(t) && t.command);
|
|
5021
|
+
if (inGroup.length === 0) {
|
|
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
|
+
);
|
|
5029
|
+
process.exit(1);
|
|
5030
|
+
}
|
|
5031
|
+
await spawnAllStopped(inGroup);
|
|
5032
|
+
return;
|
|
5033
|
+
}
|
|
5034
|
+
if (target.kind === "all") {
|
|
5035
|
+
await spawnAllStopped(readTracked().filter((t) => !isTrackedRunning(t) && t.command));
|
|
4450
5036
|
return;
|
|
4451
5037
|
}
|
|
5038
|
+
await spawnList();
|
|
5039
|
+
}
|
|
5040
|
+
async function spawnOne(name, multi) {
|
|
4452
5041
|
const tracked = readTracked();
|
|
4453
5042
|
const match = tracked.find((t) => t.name === name);
|
|
4454
5043
|
if (!match) {
|
|
4455
|
-
|
|
5044
|
+
const msg = `No tracked process named "${name}". Use ${pc10.cyan("fennec spawn")} to see available processes, or ${pc10.cyan("fennec start <command> --name <name>")} to create a new one.`;
|
|
5045
|
+
if (multi) {
|
|
5046
|
+
console.error(renderError("Not found", msg));
|
|
5047
|
+
return;
|
|
5048
|
+
}
|
|
5049
|
+
console.error(renderError("Not found", msg));
|
|
4456
5050
|
process.exit(1);
|
|
4457
5051
|
}
|
|
4458
5052
|
if (isTrackedRunning(match)) {
|
|
4459
|
-
console.error(
|
|
4460
|
-
|
|
4461
|
-
|
|
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
|
+
);
|
|
4462
5060
|
console.error();
|
|
4463
|
-
process.exit(0);
|
|
5061
|
+
if (!multi) process.exit(0);
|
|
5062
|
+
return;
|
|
4464
5063
|
}
|
|
4465
5064
|
if (!match.command) {
|
|
4466
|
-
|
|
5065
|
+
const msg = `"${name}" has no saved command and cannot be re-spawned.`;
|
|
5066
|
+
if (multi) {
|
|
5067
|
+
console.error(renderError("No command saved", msg));
|
|
5068
|
+
return;
|
|
5069
|
+
}
|
|
5070
|
+
console.error(renderError("No command saved", msg));
|
|
4467
5071
|
process.exit(1);
|
|
4468
5072
|
}
|
|
4469
5073
|
await respawnProcess(match);
|
|
4470
5074
|
}
|
|
4471
|
-
async function spawnAllStopped() {
|
|
4472
|
-
const
|
|
4473
|
-
const toSpawn = tracked.filter((t) => !isTrackedRunning(t) && t.command);
|
|
5075
|
+
async function spawnAllStopped(procs) {
|
|
5076
|
+
const toSpawn = procs;
|
|
4474
5077
|
if (toSpawn.length === 0) {
|
|
4475
5078
|
console.error(`
|
|
4476
5079
|
${pc10.dim("No stopped processes with saved commands to spawn.")}
|
|
@@ -4493,7 +5096,14 @@ async function spawnAllStopped() {
|
|
|
4493
5096
|
const logDir = resolve5(homedir3(), ".fennec", "logs");
|
|
4494
5097
|
mkdirSync5(logDir, { recursive: true });
|
|
4495
5098
|
const logFilePath = resolve5(logDir, `${proc.name}.log`);
|
|
4496
|
-
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
|
+
});
|
|
4497
5107
|
const pid = child.pid ?? 0;
|
|
4498
5108
|
addTracked({
|
|
4499
5109
|
name: proc.name,
|
|
@@ -4505,7 +5115,10 @@ async function spawnAllStopped() {
|
|
|
4505
5115
|
env: proc.env,
|
|
4506
5116
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4507
5117
|
autoRestart: proc.autoRestart,
|
|
4508
|
-
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
|
|
4509
5122
|
});
|
|
4510
5123
|
if (proc.autoRestart) {
|
|
4511
5124
|
ensureSupervisorRunning();
|
|
@@ -4532,7 +5145,9 @@ async function spawnList() {
|
|
|
4532
5145
|
if (tracked.length === 0) {
|
|
4533
5146
|
console.error(`
|
|
4534
5147
|
${pc10.dim("No tracked processes found.")}`);
|
|
4535
|
-
console.error(
|
|
5148
|
+
console.error(
|
|
5149
|
+
` ${pc10.dim("Start an app first:")} ${pc10.cyan("fennec start <command> --name <name>")}`
|
|
5150
|
+
);
|
|
4536
5151
|
console.error();
|
|
4537
5152
|
return;
|
|
4538
5153
|
}
|
|
@@ -4544,8 +5159,10 @@ async function spawnList() {
|
|
|
4544
5159
|
console.error();
|
|
4545
5160
|
return;
|
|
4546
5161
|
}
|
|
4547
|
-
console.error(
|
|
4548
|
-
|
|
5162
|
+
console.error(
|
|
5163
|
+
`
|
|
5164
|
+
${symbols.fox} ${pc10.bold("Available to spawn")} ${pc10.dim(`(${stopped.length}/${tracked.length} stopped)`)}`
|
|
5165
|
+
);
|
|
4549
5166
|
if (running.length > 0) {
|
|
4550
5167
|
console.error(` ${pc10.dim("Running (use stop first):")}`);
|
|
4551
5168
|
for (const r of running) {
|
|
@@ -4553,11 +5170,14 @@ async function spawnList() {
|
|
|
4553
5170
|
}
|
|
4554
5171
|
console.error();
|
|
4555
5172
|
}
|
|
4556
|
-
const selected = await selectPrompt(
|
|
4557
|
-
|
|
4558
|
-
|
|
4559
|
-
|
|
4560
|
-
|
|
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
|
+
);
|
|
4561
5181
|
if (selected === null) {
|
|
4562
5182
|
console.error(` ${pc10.dim("Cancelled")}`);
|
|
4563
5183
|
return;
|
|
@@ -4572,14 +5192,23 @@ async function respawnProcess(proc) {
|
|
|
4572
5192
|
const logDir = resolve5(homedir3(), ".fennec", "logs");
|
|
4573
5193
|
mkdirSync5(logDir, { recursive: true });
|
|
4574
5194
|
const logFilePath = resolve5(logDir, `${proc.name}.log`);
|
|
4575
|
-
console.error(
|
|
5195
|
+
console.error(
|
|
5196
|
+
`
|
|
4576
5197
|
${symbols.fox} ${pc10.bold("Spawning")} ${renderAppName(proc.name)} ${pc10.dim("(from saved config)")}
|
|
4577
|
-
`
|
|
5198
|
+
`
|
|
5199
|
+
);
|
|
4578
5200
|
console.error(` ${renderKV("Command", proc.command)}`);
|
|
4579
5201
|
if (proc.cwd) console.error(` ${renderKV("Directory", proc.cwd)}`);
|
|
4580
5202
|
if (proc.port) console.error(` ${renderKV("Port", String(proc.port))}`);
|
|
4581
5203
|
try {
|
|
4582
|
-
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
|
+
});
|
|
4583
5212
|
const pid = child.pid ?? 0;
|
|
4584
5213
|
addTracked({
|
|
4585
5214
|
name: proc.name,
|
|
@@ -4591,7 +5220,8 @@ async function respawnProcess(proc) {
|
|
|
4591
5220
|
env: proc.env,
|
|
4592
5221
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4593
5222
|
autoRestart: proc.autoRestart,
|
|
4594
|
-
logMode: proc.logMode
|
|
5223
|
+
logMode: proc.logMode,
|
|
5224
|
+
group: proc.group
|
|
4595
5225
|
});
|
|
4596
5226
|
if (proc.autoRestart) {
|
|
4597
5227
|
ensureSupervisorRunning();
|
|
@@ -4607,32 +5237,51 @@ async function respawnProcess(proc) {
|
|
|
4607
5237
|
|
|
4608
5238
|
// src/commands/restart.ts
|
|
4609
5239
|
import pc11 from "picocolors";
|
|
4610
|
-
import { mkdirSync as mkdirSync6 } from "fs";
|
|
4611
|
-
import { resolve as resolve6 } from "path";
|
|
4612
|
-
import { homedir as homedir4 } from "os";
|
|
4613
5240
|
async function restartCommand(args2) {
|
|
4614
|
-
const
|
|
4615
|
-
|
|
4616
|
-
|
|
4617
|
-
|
|
5241
|
+
const force = args2.includes("-y") || args2.includes("--yes");
|
|
5242
|
+
const target = resolveTargets(args2);
|
|
5243
|
+
if (target.kind === "group") {
|
|
5244
|
+
await restartGroup(target.group, force);
|
|
5245
|
+
return;
|
|
5246
|
+
}
|
|
5247
|
+
if (target.kind === "names") {
|
|
5248
|
+
for (const n of target.values) await restartOne(n, force, true);
|
|
5249
|
+
return;
|
|
4618
5250
|
}
|
|
5251
|
+
if (target.kind === "single") {
|
|
5252
|
+
await restartOne(target.value, force, false);
|
|
5253
|
+
return;
|
|
5254
|
+
}
|
|
5255
|
+
console.error(
|
|
5256
|
+
renderError(
|
|
5257
|
+
"Missing process name/pid",
|
|
5258
|
+
"Usage: fennec restart <name|pid> [-y] [--group <group>]"
|
|
5259
|
+
)
|
|
5260
|
+
);
|
|
5261
|
+
process.exit(1);
|
|
5262
|
+
}
|
|
5263
|
+
async function restartOne(raw, force, multi) {
|
|
4619
5264
|
const tracked = readTracked();
|
|
4620
5265
|
const pidNum = parseInt(raw, 10);
|
|
4621
5266
|
const trackedEntry = tracked.find((t) => t.name === raw) ?? (!isNaN(pidNum) && String(pidNum) === raw ? tracked.find((t) => t.pid === pidNum) : void 0);
|
|
4622
5267
|
if (!trackedEntry) {
|
|
4623
|
-
|
|
4624
|
-
fennec restart only re-spawns Fennec-tracked apps (from their saved config)
|
|
5268
|
+
const msg = `No tracked process named or with PID "${raw}".
|
|
5269
|
+
fennec restart only re-spawns Fennec-tracked apps (from their saved config).`;
|
|
5270
|
+
if (multi) {
|
|
5271
|
+
console.error(renderError("Not tracked", msg));
|
|
5272
|
+
return;
|
|
5273
|
+
}
|
|
5274
|
+
console.error(renderError("Not tracked", msg));
|
|
4625
5275
|
process.exit(1);
|
|
4626
5276
|
}
|
|
4627
5277
|
const targetPid = trackedEntry.pid;
|
|
4628
|
-
const force = args2.includes("-y") || args2.includes("--yes");
|
|
4629
5278
|
const confirmed = force || await confirmPrompt(`Restart ${pc11.bold(`${trackedEntry.name} (PID ${targetPid})`)}?`, false);
|
|
4630
5279
|
if (!confirmed) {
|
|
4631
5280
|
console.error(` ${pc11.dim("Cancelled")}`);
|
|
4632
5281
|
return;
|
|
4633
5282
|
}
|
|
4634
5283
|
const spinner = createSpinner(`Stopping ${trackedEntry.name} (PID ${targetPid})...`);
|
|
4635
|
-
const killed =
|
|
5284
|
+
const killed = killTree(targetPid, "SIGTERM");
|
|
4636
5285
|
if (!killed) {
|
|
4637
5286
|
spinner.fail(`Failed to stop PID ${targetPid}`);
|
|
4638
5287
|
return;
|
|
@@ -4640,17 +5289,22 @@ fennec restart only re-spawns Fennec-tracked apps (from their saved config).`));
|
|
|
4640
5289
|
await new Promise((r) => setTimeout(r, 1e3));
|
|
4641
5290
|
if (isProcessRunning(targetPid)) {
|
|
4642
5291
|
spinner.warn("Process didn't stop, sending SIGKILL...");
|
|
4643
|
-
|
|
5292
|
+
killTree(targetPid, "SIGKILL");
|
|
4644
5293
|
await new Promise((r) => setTimeout(r, 500));
|
|
4645
5294
|
}
|
|
4646
5295
|
spinner.succeed(`${trackedEntry.name} stopped`);
|
|
4647
5296
|
const respawnSpinner = createSpinner(`Re-spawning ${trackedEntry.name}...`);
|
|
4648
5297
|
try {
|
|
4649
5298
|
const cmdParts = resolveArgs(trackedEntry);
|
|
4650
|
-
const
|
|
4651
|
-
|
|
4652
|
-
|
|
4653
|
-
|
|
5299
|
+
const logFilePath = logFilePathFor(trackedEntry.name);
|
|
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
|
+
});
|
|
4654
5308
|
removeTrackedByPid(targetPid);
|
|
4655
5309
|
addTracked({
|
|
4656
5310
|
name: trackedEntry.name,
|
|
@@ -4662,12 +5316,72 @@ fennec restart only re-spawns Fennec-tracked apps (from their saved config).`));
|
|
|
4662
5316
|
env: trackedEntry.env,
|
|
4663
5317
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4664
5318
|
autoRestart: trackedEntry.autoRestart,
|
|
4665
|
-
logMode: trackedEntry.logMode
|
|
5319
|
+
logMode: trackedEntry.logMode,
|
|
5320
|
+
group: trackedEntry.group
|
|
4666
5321
|
});
|
|
4667
5322
|
respawnSpinner.succeed(`${trackedEntry.name} restarted (PID: ${child.pid})`);
|
|
4668
5323
|
} catch (error) {
|
|
4669
5324
|
respawnSpinner.fail(`Failed to re-spawn ${trackedEntry.name}: ${String(error)}`);
|
|
4670
|
-
console.error(
|
|
5325
|
+
console.error(
|
|
5326
|
+
` ${pc11.dim("Config preserved. Re-run manually:")} ${renderCommand(trackedEntry.command)}`
|
|
5327
|
+
);
|
|
5328
|
+
}
|
|
5329
|
+
}
|
|
5330
|
+
async function restartGroup(group, force) {
|
|
5331
|
+
const tracked = readTracked();
|
|
5332
|
+
const inGroup = tracked.filter((t) => t.group === group);
|
|
5333
|
+
if (inGroup.length === 0) {
|
|
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
|
+
);
|
|
5341
|
+
process.exit(1);
|
|
5342
|
+
}
|
|
5343
|
+
const confirmed = force || await confirmPrompt(
|
|
5344
|
+
`Restart ${pc11.bold(`${inGroup.length}`)} process(es) in group ${pc11.cyan(group)}?`,
|
|
5345
|
+
false
|
|
5346
|
+
);
|
|
5347
|
+
if (!confirmed) {
|
|
5348
|
+
console.error(` ${pc11.dim("Cancelled")}`);
|
|
5349
|
+
return;
|
|
5350
|
+
}
|
|
5351
|
+
for (const trackedEntry of inGroup) {
|
|
5352
|
+
const spinner = createSpinner(`Restarting ${trackedEntry.name}...`);
|
|
5353
|
+
try {
|
|
5354
|
+
const cmdParts = resolveArgs(trackedEntry);
|
|
5355
|
+
const logFilePath = logFilePathFor(trackedEntry.name);
|
|
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
|
+
});
|
|
5364
|
+
removeTrackedByPid(trackedEntry.pid);
|
|
5365
|
+
addTracked({
|
|
5366
|
+
name: trackedEntry.name,
|
|
5367
|
+
pid: child.pid ?? 0,
|
|
5368
|
+
command: trackedEntry.command,
|
|
5369
|
+
args: cmdParts,
|
|
5370
|
+
port: trackedEntry.port,
|
|
5371
|
+
group: trackedEntry.group,
|
|
5372
|
+
cwd: trackedEntry.cwd,
|
|
5373
|
+
env: trackedEntry.env,
|
|
5374
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5375
|
+
autoRestart: trackedEntry.autoRestart,
|
|
5376
|
+
logMode: trackedEntry.logMode
|
|
5377
|
+
});
|
|
5378
|
+
spinner.succeed(`${trackedEntry.name} restarted (PID: ${child.pid})`);
|
|
5379
|
+
} catch (error) {
|
|
5380
|
+
spinner.fail(`Failed to re-spawn ${trackedEntry.name}: ${String(error)}`);
|
|
5381
|
+
console.error(
|
|
5382
|
+
` ${pc11.dim("Config preserved. Re-run manually:")} ${renderCommand(trackedEntry.command)}`
|
|
5383
|
+
);
|
|
5384
|
+
}
|
|
4671
5385
|
}
|
|
4672
5386
|
}
|
|
4673
5387
|
|
|
@@ -4676,7 +5390,9 @@ import pc12 from "picocolors";
|
|
|
4676
5390
|
function adoptCommand(args2) {
|
|
4677
5391
|
const pidArg = args2.find((a) => /^\d+$/.test(a));
|
|
4678
5392
|
if (!pidArg) {
|
|
4679
|
-
console.error(
|
|
5393
|
+
console.error(
|
|
5394
|
+
renderError("Missing PID", `Usage: fennec adopt <pid> [--name <name>] [--port <port>]`)
|
|
5395
|
+
);
|
|
4680
5396
|
process.exit(1);
|
|
4681
5397
|
}
|
|
4682
5398
|
const pid = parseInt(pidArg, 10);
|
|
@@ -4689,8 +5405,10 @@ function adoptCommand(args2) {
|
|
|
4689
5405
|
console.error(renderError("Cannot adopt", `No running process with PID ${pid}`));
|
|
4690
5406
|
process.exit(1);
|
|
4691
5407
|
}
|
|
4692
|
-
console.error(
|
|
4693
|
-
|
|
5408
|
+
console.error(
|
|
5409
|
+
`
|
|
5410
|
+
${symbols.fox} ${pc12.green("\u2713")} ${pc12.bold("Adopted")} ${pc12.bold(entry.name)} ${pc12.dim(`(PID ${pid})`)}`
|
|
5411
|
+
);
|
|
4694
5412
|
if (entry.port) console.error(` ${pc12.dim("port")} ${pc12.yellow(`:${entry.port}`)}`);
|
|
4695
5413
|
if (entry.command) console.error(` ${pc12.dim("command")} ${entry.command}`);
|
|
4696
5414
|
console.error(` ${pc12.dim("status")} now tracked + supervised by the supervisor`);
|
|
@@ -4710,7 +5428,10 @@ var SECRET_PATTERNS = [
|
|
|
4710
5428
|
// Authorization: <scheme> <token>
|
|
4711
5429
|
{ name: "authorization", re: /\bAuthorization\s*:\s*\S+/gi },
|
|
4712
5430
|
// Generic API keys
|
|
4713
|
-
{
|
|
5431
|
+
{
|
|
5432
|
+
name: "apikey",
|
|
5433
|
+
re: /\b(api[_-]?key|apikey|access[_-]?key|secret[_-]?key|private[_-]?key)\s*[:=]\s*\S+/gi
|
|
5434
|
+
},
|
|
4714
5435
|
// AWS
|
|
4715
5436
|
{ name: "aws", re: /\b(AKIA|ASIA)[0-9A-Z]{16}\b/g },
|
|
4716
5437
|
// Slack tokens
|
|
@@ -4726,9 +5447,15 @@ var SECRET_PATTERNS = [
|
|
|
4726
5447
|
// Generic long hex/alpha tokens (>=32 chars) in common contexts
|
|
4727
5448
|
{ name: "token", re: /\b(token|secret|password|passwd|pwd|client[_-]?secret)\s*[:=]\s*\S+/gi },
|
|
4728
5449
|
// Connection strings with credentials
|
|
4729
|
-
{
|
|
5450
|
+
{
|
|
5451
|
+
name: "connstr",
|
|
5452
|
+
re: /\b(mongodb(\+srv)?|postgres(ql)?|mysql|redis|amqp|sqlserver):\/\/[^\s:]+:[^\s@]+@/gi
|
|
5453
|
+
},
|
|
4730
5454
|
// Private key blocks
|
|
4731
|
-
{
|
|
5455
|
+
{
|
|
5456
|
+
name: "pem",
|
|
5457
|
+
re: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g
|
|
5458
|
+
}
|
|
4732
5459
|
];
|
|
4733
5460
|
function redactLine(line, opts = {}) {
|
|
4734
5461
|
if (opts.enabled === false) return line;
|
|
@@ -4846,17 +5573,27 @@ async function inspectCommand(args2) {
|
|
|
4846
5573
|
process.exit(1);
|
|
4847
5574
|
}
|
|
4848
5575
|
const tracked = readTracked();
|
|
4849
|
-
const proc = tracked.find(
|
|
5576
|
+
const proc = tracked.find(
|
|
5577
|
+
(t) => t.name === target || parseInt(target, 10) === t.pid && !isNaN(parseInt(target, 10))
|
|
5578
|
+
);
|
|
4850
5579
|
if (!proc) {
|
|
4851
5580
|
console.error(pc13.red(`No tracked app named "${target}".`));
|
|
4852
5581
|
process.exit(1);
|
|
4853
5582
|
}
|
|
4854
5583
|
const running = isTrackedRunning(proc);
|
|
4855
5584
|
const logPath = logFilePathFor(proc.name);
|
|
4856
|
-
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
|
+
});
|
|
4857
5591
|
const recent = lines.slice(-tail);
|
|
4858
5592
|
const errorLines = lines.filter((l) => classifyLevel(l) === "error").slice(-15);
|
|
4859
|
-
const redactedLines = recent.reduce(
|
|
5593
|
+
const redactedLines = recent.reduce(
|
|
5594
|
+
(acc, l) => acc + (redactionCount(stripAnsi(l)) > 0 ? 1 : 0),
|
|
5595
|
+
0
|
|
5596
|
+
);
|
|
4860
5597
|
const metrics = running ? readMetrics(proc.pid) : {};
|
|
4861
5598
|
let portHealthy = null;
|
|
4862
5599
|
if (proc.port) {
|
|
@@ -4871,13 +5608,18 @@ async function inspectCommand(args2) {
|
|
|
4871
5608
|
if (plain) {
|
|
4872
5609
|
const dot = running ? pc13.green("\u25CF") : pc13.red("\u25CB");
|
|
4873
5610
|
const portStr = proc.port ? ` :${proc.port}${portHealthy === false ? pc13.red(" (port down!)") : portHealthy ? pc13.green(" (up)") : ""}` : "";
|
|
4874
|
-
console.error(
|
|
4875
|
-
|
|
4876
|
-
|
|
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
|
+
);
|
|
4877
5618
|
if (errorLines.length) {
|
|
4878
5619
|
console.error(`
|
|
4879
5620
|
${pc13.bold(pc13.red("Recent errors:"))}`);
|
|
4880
|
-
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))}`);
|
|
4881
5623
|
}
|
|
4882
5624
|
console.error();
|
|
4883
5625
|
return;
|
|
@@ -4904,7 +5646,7 @@ async function inspectCommand(args2) {
|
|
|
4904
5646
|
// src/commands/dev.ts
|
|
4905
5647
|
import pc14 from "picocolors";
|
|
4906
5648
|
import { existsSync as existsSync8, readFileSync as readFileSync7 } from "fs";
|
|
4907
|
-
import { resolve as
|
|
5649
|
+
import { resolve as resolve6, dirname as dirname5 } from "path";
|
|
4908
5650
|
import "os";
|
|
4909
5651
|
import yaml from "js-yaml";
|
|
4910
5652
|
function loadConfig(path) {
|
|
@@ -4923,7 +5665,7 @@ function loadConfig(path) {
|
|
|
4923
5665
|
}
|
|
4924
5666
|
function resolveCwd(baseDir, cwd) {
|
|
4925
5667
|
if (!cwd) return baseDir;
|
|
4926
|
-
return
|
|
5668
|
+
return resolve6(baseDir, cwd);
|
|
4927
5669
|
}
|
|
4928
5670
|
async function waitForPort(port, timeoutMs = 3e4) {
|
|
4929
5671
|
const deadline = Date.now() + timeoutMs;
|
|
@@ -4972,7 +5714,7 @@ async function startApp(app, baseDir) {
|
|
|
4972
5714
|
const wasRunning = !!(existing && isTrackedRunning(existing));
|
|
4973
5715
|
if (wasRunning) {
|
|
4974
5716
|
try {
|
|
4975
|
-
|
|
5717
|
+
killTree(existing.pid, "SIGTERM");
|
|
4976
5718
|
} catch {
|
|
4977
5719
|
}
|
|
4978
5720
|
}
|
|
@@ -5014,7 +5756,7 @@ async function startApp(app, baseDir) {
|
|
|
5014
5756
|
async function devCommand(args2) {
|
|
5015
5757
|
const sub = args2[0] ?? "up";
|
|
5016
5758
|
const configIndex = args2.indexOf("--config");
|
|
5017
|
-
const configPath = configIndex !== -1 ?
|
|
5759
|
+
const configPath = configIndex !== -1 ? resolve6(args2[configIndex + 1]) : resolve6(process.cwd(), "fennec.config.yaml");
|
|
5018
5760
|
if (sub === "down") {
|
|
5019
5761
|
return devDown();
|
|
5020
5762
|
}
|
|
@@ -5025,7 +5767,9 @@ async function devCommand(args2) {
|
|
|
5025
5767
|
return devRestart(args2.slice(1));
|
|
5026
5768
|
}
|
|
5027
5769
|
if (sub !== "up") {
|
|
5028
|
-
console.error(
|
|
5770
|
+
console.error(
|
|
5771
|
+
renderError("Unknown sub-command", `"${sub}" \u2014 use: up | down | status | restart`)
|
|
5772
|
+
);
|
|
5029
5773
|
process.exit(1);
|
|
5030
5774
|
}
|
|
5031
5775
|
const config = loadConfig(configPath);
|
|
@@ -5043,14 +5787,18 @@ async function devCommand(args2) {
|
|
|
5043
5787
|
if (p) {
|
|
5044
5788
|
const prev = portOwner.get(p);
|
|
5045
5789
|
if (prev) {
|
|
5046
|
-
console.error(
|
|
5790
|
+
console.error(
|
|
5791
|
+
renderError("Port conflict", `apps "${prev}" and "${a.name}" both declare port :${p}`)
|
|
5792
|
+
);
|
|
5047
5793
|
process.exit(1);
|
|
5048
5794
|
}
|
|
5049
5795
|
portOwner.set(p, a.name);
|
|
5050
5796
|
}
|
|
5051
5797
|
}
|
|
5052
|
-
console.error(
|
|
5053
|
-
|
|
5798
|
+
console.error(
|
|
5799
|
+
`
|
|
5800
|
+
${symbols.fox} ${pc14.bold("fennec dev up")} ${pc14.dim(`\u2014 ${apps.length} app(s) from ${configPath}`)}`
|
|
5801
|
+
);
|
|
5054
5802
|
ensureSupervisorRunning();
|
|
5055
5803
|
ensurePersistEnabled();
|
|
5056
5804
|
for (const app of ordered) {
|
|
@@ -5061,7 +5809,10 @@ async function devCommand(args2) {
|
|
|
5061
5809
|
const sp = createSpinner(`Waiting for ${dep} :${port}...`);
|
|
5062
5810
|
const ok = await waitForPort(port);
|
|
5063
5811
|
sp.stop();
|
|
5064
|
-
if (!ok)
|
|
5812
|
+
if (!ok)
|
|
5813
|
+
console.error(
|
|
5814
|
+
` ${pc14.yellow("\u26A0")} ${pc14.dim(`${dep} :${port} not ready in time (continuing)`)}`
|
|
5815
|
+
);
|
|
5065
5816
|
}
|
|
5066
5817
|
}
|
|
5067
5818
|
const res = await startApp(app, baseDir);
|
|
@@ -5069,8 +5820,10 @@ async function devCommand(args2) {
|
|
|
5069
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})`);
|
|
5070
5821
|
console.error(` ${pc14.green("\u2713")} ${pc14.bold(app.name)} ${tag}`);
|
|
5071
5822
|
}
|
|
5072
|
-
console.error(
|
|
5073
|
-
|
|
5823
|
+
console.error(
|
|
5824
|
+
`
|
|
5825
|
+
${pc14.green("\u2713")} Stack up. Observe with ${pc14.cyan("fennec observe")} or ${pc14.cyan("fennec dev status")}.`
|
|
5826
|
+
);
|
|
5074
5827
|
console.error(` ${pc14.dim("Apps auto-restart (supervisor) + survive reboot (persist).")}
|
|
5075
5828
|
`);
|
|
5076
5829
|
}
|
|
@@ -5082,20 +5835,24 @@ function devDown() {
|
|
|
5082
5835
|
`);
|
|
5083
5836
|
return;
|
|
5084
5837
|
}
|
|
5085
|
-
console.error(
|
|
5086
|
-
|
|
5838
|
+
console.error(
|
|
5839
|
+
`
|
|
5840
|
+
${symbols.fox} ${pc14.bold("fennec dev down")} ${pc14.dim(`\u2014 stopping ${tracked.length} app(s)`)}`
|
|
5841
|
+
);
|
|
5087
5842
|
for (const t of tracked) {
|
|
5088
5843
|
try {
|
|
5089
|
-
if (isTrackedRunning(t))
|
|
5844
|
+
if (isTrackedRunning(t)) killTree(t.pid, "SIGTERM");
|
|
5090
5845
|
} catch {
|
|
5091
5846
|
}
|
|
5092
5847
|
t.autoRestart = false;
|
|
5093
5848
|
addTracked({ ...t, autoRestart: false });
|
|
5094
5849
|
console.error(` ${pc14.red("\u25A0")} ${pc14.bold(t.name)}`);
|
|
5095
5850
|
}
|
|
5096
|
-
console.error(
|
|
5851
|
+
console.error(
|
|
5852
|
+
`
|
|
5097
5853
|
${pc14.green("\u2713")} Stack stopped (auto-restart disabled). Use ${pc14.cyan("fennec dev up")} to bring it back.
|
|
5098
|
-
`
|
|
5854
|
+
`
|
|
5855
|
+
);
|
|
5099
5856
|
}
|
|
5100
5857
|
function devRestart(names) {
|
|
5101
5858
|
ensureSupervisorRunning();
|
|
@@ -5104,7 +5861,12 @@ function devRestart(names) {
|
|
|
5104
5861
|
const targets = names.length ? tracked.filter((t) => names.includes(t.name)) : tracked.filter((t) => t.autoRestart);
|
|
5105
5862
|
const missing = names.filter((n) => !tracked.some((t) => t.name === n));
|
|
5106
5863
|
if (missing.length) {
|
|
5107
|
-
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
|
+
);
|
|
5108
5870
|
}
|
|
5109
5871
|
if (targets.length === 0) {
|
|
5110
5872
|
console.error(`
|
|
@@ -5112,14 +5874,18 @@ function devRestart(names) {
|
|
|
5112
5874
|
`);
|
|
5113
5875
|
return;
|
|
5114
5876
|
}
|
|
5115
|
-
console.error(
|
|
5116
|
-
|
|
5877
|
+
console.error(
|
|
5878
|
+
`
|
|
5879
|
+
${symbols.fox} ${pc14.bold("fennec dev restart")} ${pc14.dim(`\u2014 ${targets.length} app(s)`)}`
|
|
5880
|
+
);
|
|
5117
5881
|
for (const t of targets) {
|
|
5118
5882
|
try {
|
|
5119
5883
|
const newPid = respawnTracked(t, "manual");
|
|
5120
5884
|
console.error(` ${pc14.green("\u2713")} ${pc14.bold(t.name)} ${pc14.dim(`restarted (PID ${newPid})`)}`);
|
|
5121
5885
|
} catch (err) {
|
|
5122
|
-
console.error(
|
|
5886
|
+
console.error(
|
|
5887
|
+
` ${pc14.red("\u2717")} ${pc14.bold(t.name)} ${pc14.dim(`restart failed: ${String(err)}`)}`
|
|
5888
|
+
);
|
|
5123
5889
|
}
|
|
5124
5890
|
}
|
|
5125
5891
|
console.error();
|
|
@@ -5165,7 +5931,15 @@ function topoOrder(apps) {
|
|
|
5165
5931
|
}
|
|
5166
5932
|
|
|
5167
5933
|
// src/commands/log.ts
|
|
5168
|
-
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";
|
|
5169
5943
|
import { execSync as execSync2 } from "child_process";
|
|
5170
5944
|
import pc15 from "picocolors";
|
|
5171
5945
|
var MAX_LOG_LINES = 500;
|
|
@@ -5189,14 +5963,21 @@ async function logCommand(args2) {
|
|
|
5189
5963
|
process.exit(1);
|
|
5190
5964
|
}
|
|
5191
5965
|
if (levelFilter && !["error", "warn", "info", "debug"].includes(levelFilter)) {
|
|
5192
|
-
console.error(
|
|
5966
|
+
console.error(
|
|
5967
|
+
renderError(
|
|
5968
|
+
"Invalid level",
|
|
5969
|
+
`"${levelFilter}" is not a valid level. Use: error, warn, info, debug`
|
|
5970
|
+
)
|
|
5971
|
+
);
|
|
5193
5972
|
process.exit(1);
|
|
5194
5973
|
}
|
|
5195
5974
|
const redact = !noRedact;
|
|
5196
5975
|
let logFilePath = null;
|
|
5197
5976
|
let displayName = target;
|
|
5198
5977
|
const tracked = readTracked();
|
|
5199
|
-
const trackedMatch = tracked.find(
|
|
5978
|
+
const trackedMatch = tracked.find(
|
|
5979
|
+
(t) => t.name === target || parseInt(target, 10) === t.pid && !isNaN(parseInt(target, 10))
|
|
5980
|
+
);
|
|
5200
5981
|
if (trackedMatch) {
|
|
5201
5982
|
displayName = trackedMatch.name;
|
|
5202
5983
|
logFilePath = logFilePathFor(trackedMatch.name);
|
|
@@ -5209,16 +5990,20 @@ async function logCommand(args2) {
|
|
|
5209
5990
|
}
|
|
5210
5991
|
if (clearFlag) {
|
|
5211
5992
|
if (!logFilePath || !existsSync9(logFilePath)) {
|
|
5212
|
-
console.error(
|
|
5993
|
+
console.error(
|
|
5994
|
+
`
|
|
5213
5995
|
${pc15.yellow("\u26A0")} ${pc15.dim("No log file found for")} ${pc15.bold(displayName)}
|
|
5214
|
-
`
|
|
5996
|
+
`
|
|
5997
|
+
);
|
|
5215
5998
|
process.exit(0);
|
|
5216
5999
|
}
|
|
5217
6000
|
try {
|
|
5218
6001
|
unlinkSync3(logFilePath);
|
|
5219
|
-
console.error(
|
|
6002
|
+
console.error(
|
|
6003
|
+
`
|
|
5220
6004
|
${pc15.green("\u2713")} ${pc15.bold("Log cleared")} ${pc15.dim(`\u2014 ${logFilePath}`)}
|
|
5221
|
-
`
|
|
6005
|
+
`
|
|
6006
|
+
);
|
|
5222
6007
|
} catch (err) {
|
|
5223
6008
|
console.error(renderError("Failed to clear log", String(err)));
|
|
5224
6009
|
process.exit(1);
|
|
@@ -5238,7 +6023,10 @@ async function logCommand(args2) {
|
|
|
5238
6023
|
const pid = trackedMatch?.pid ?? parseInt(target, 10);
|
|
5239
6024
|
if (!isNaN(pid)) {
|
|
5240
6025
|
try {
|
|
5241
|
-
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
|
+
);
|
|
5242
6030
|
logLines = output.trim().split("\n").filter(Boolean).map((l) => redact ? redactLine(stripAnsi(l)) : stripAnsi(l));
|
|
5243
6031
|
} catch {
|
|
5244
6032
|
logLines = ["(no logs available)"];
|
|
@@ -5253,7 +6041,10 @@ async function logCommand(args2) {
|
|
|
5253
6041
|
}
|
|
5254
6042
|
const running = trackedMatch ? isTrackedRunning(trackedMatch) : false;
|
|
5255
6043
|
const sliced = logLines.slice(-lineCount);
|
|
5256
|
-
const redactedHits = sliced.reduce(
|
|
6044
|
+
const redactedHits = sliced.reduce(
|
|
6045
|
+
(acc, l) => acc + (redactionCount(stripAnsi(l)) > 0 ? 1 : 0),
|
|
6046
|
+
0
|
|
6047
|
+
);
|
|
5257
6048
|
const payload = {
|
|
5258
6049
|
app: displayName,
|
|
5259
6050
|
running,
|
|
@@ -5275,7 +6066,9 @@ async function logCommand(args2) {
|
|
|
5275
6066
|
console.error(`
|
|
5276
6067
|
${symbols.fox} ${renderAppName(displayName)}${portStr} \u2014 ${statusStr}`);
|
|
5277
6068
|
if (!running) {
|
|
5278
|
-
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
|
+
);
|
|
5279
6072
|
}
|
|
5280
6073
|
}
|
|
5281
6074
|
const spinner = createSpinner(`Reading logs for ${displayName}...`);
|
|
@@ -5292,7 +6085,10 @@ async function logCommand(args2) {
|
|
|
5292
6085
|
const pid = trackedMatch?.pid ?? parseInt(target, 10);
|
|
5293
6086
|
if (!isNaN(pid)) {
|
|
5294
6087
|
try {
|
|
5295
|
-
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
|
+
);
|
|
5296
6092
|
logLines = output.trim().split("\n").filter(Boolean).map((l) => redact ? redactLine(stripAnsi(l)) : stripAnsi(l));
|
|
5297
6093
|
} catch {
|
|
5298
6094
|
logLines = ["(no logs available)"];
|
|
@@ -5307,8 +6103,10 @@ async function logCommand(args2) {
|
|
|
5307
6103
|
}
|
|
5308
6104
|
spinner.stop();
|
|
5309
6105
|
process.stdout.write("\r\x1B[K");
|
|
5310
|
-
console.error(
|
|
5311
|
-
|
|
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
|
+
);
|
|
5312
6110
|
const sliced = logLines.slice(-lineCount);
|
|
5313
6111
|
for (const line of sliced) {
|
|
5314
6112
|
const display = line.length > 300 ? line.slice(0, 300) + "\u2026" : line;
|
|
@@ -5334,7 +6132,7 @@ async function logCommand(args2) {
|
|
|
5334
6132
|
async function followLog(logFilePath, opts) {
|
|
5335
6133
|
let lastSize = 0;
|
|
5336
6134
|
try {
|
|
5337
|
-
lastSize =
|
|
6135
|
+
lastSize = statSync3(logFilePath).size;
|
|
5338
6136
|
} catch {
|
|
5339
6137
|
}
|
|
5340
6138
|
console.error(`
|
|
@@ -5384,10 +6182,10 @@ async function followLog(logFilePath, opts) {
|
|
|
5384
6182
|
closeSync3(fd);
|
|
5385
6183
|
}
|
|
5386
6184
|
}, 400);
|
|
5387
|
-
await new Promise((
|
|
6185
|
+
await new Promise((resolve12) => {
|
|
5388
6186
|
const stop = () => {
|
|
5389
6187
|
clearInterval(timer);
|
|
5390
|
-
|
|
6188
|
+
resolve12();
|
|
5391
6189
|
};
|
|
5392
6190
|
process.once("SIGINT", stop);
|
|
5393
6191
|
process.once("SIGTERM", stop);
|
|
@@ -5398,8 +6196,10 @@ function listAvailableApps() {
|
|
|
5398
6196
|
if (tracked.length === 0) {
|
|
5399
6197
|
console.error(`
|
|
5400
6198
|
${symbols.fox} ${pc15.bold("No tracked apps to show logs for.")}`);
|
|
5401
|
-
console.error(
|
|
5402
|
-
`)
|
|
6199
|
+
console.error(
|
|
6200
|
+
` ${pc15.dim("Start one with:")} ${pc15.cyan("fennec start <command> --name <name>")}
|
|
6201
|
+
`
|
|
6202
|
+
);
|
|
5403
6203
|
return;
|
|
5404
6204
|
}
|
|
5405
6205
|
console.error(`
|
|
@@ -5411,12 +6211,18 @@ function listAvailableApps() {
|
|
|
5411
6211
|
const state = running ? pc15.green(`running (PID ${t.pid})`) : pc15.red("stopped");
|
|
5412
6212
|
const portStr = t.port ? ` ${pc15.yellow(`:${t.port}`)}` : "";
|
|
5413
6213
|
console.error(` ${dot} ${pc15.bold(t.name)}${portStr} ${pc15.dim("\u2014")} ${state}`);
|
|
5414
|
-
console.error(
|
|
6214
|
+
console.error(
|
|
6215
|
+
` ${pc15.cyan(`fennec log ${t.name}`)} ${pc15.dim("\xB7")} ${pc15.cyan(`fennec log ${t.name} -f`)}`
|
|
6216
|
+
);
|
|
5415
6217
|
}
|
|
5416
|
-
console.error(
|
|
5417
|
-
|
|
5418
|
-
|
|
5419
|
-
|
|
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
|
+
);
|
|
5420
6226
|
}
|
|
5421
6227
|
|
|
5422
6228
|
// src/commands/attach.ts
|
|
@@ -5451,37 +6257,253 @@ async function attachCommand(args2) {
|
|
|
5451
6257
|
}
|
|
5452
6258
|
}
|
|
5453
6259
|
|
|
5454
|
-
// src/commands/
|
|
5455
|
-
import {
|
|
6260
|
+
// src/commands/store.ts
|
|
6261
|
+
import { StoreManager, redactSession } from "@plumpslabs/fennec-core";
|
|
6262
|
+
import "@plumpslabs/fennec-core";
|
|
5456
6263
|
import pc17 from "picocolors";
|
|
5457
|
-
|
|
5458
|
-
|
|
5459
|
-
|
|
5460
|
-
if (
|
|
6264
|
+
var KINDS = ["session", "process", "workflow", "plugin", "config", "export"];
|
|
6265
|
+
function formatBytes(n) {
|
|
6266
|
+
if (n < 1024) return `${n} B`;
|
|
6267
|
+
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
|
|
6268
|
+
return `${(n / 1024 / 1024).toFixed(1)} MB`;
|
|
6269
|
+
}
|
|
6270
|
+
function formatAge(ms) {
|
|
6271
|
+
const s = Math.floor(ms / 1e3);
|
|
6272
|
+
if (s < 60) return `${s}s`;
|
|
6273
|
+
const m = Math.floor(s / 60);
|
|
6274
|
+
if (m < 60) return `${m}m`;
|
|
6275
|
+
const h = Math.floor(m / 60);
|
|
6276
|
+
if (h < 24) return `${h}h`;
|
|
6277
|
+
return `${Math.floor(h / 24)}d`;
|
|
6278
|
+
}
|
|
6279
|
+
async function storeCommand(args2) {
|
|
6280
|
+
const local = args2.includes("--local");
|
|
6281
|
+
const showSecrets = args2.includes("--show-secrets");
|
|
6282
|
+
const yes = args2.includes("-y") || args2.includes("--yes");
|
|
6283
|
+
const positional = [];
|
|
6284
|
+
for (const a of args2) {
|
|
6285
|
+
if (a.startsWith("--") || a === "-y") continue;
|
|
6286
|
+
positional.push(a);
|
|
6287
|
+
}
|
|
6288
|
+
const kind = positional[0];
|
|
6289
|
+
const action = positional[1];
|
|
6290
|
+
const name = positional[2];
|
|
6291
|
+
const mgr = new StoreManager(local);
|
|
6292
|
+
mgr.ensure();
|
|
6293
|
+
if (!kind) {
|
|
6294
|
+
const scan = mgr.scan();
|
|
6295
|
+
console.error(
|
|
6296
|
+
`
|
|
6297
|
+
${symbols.fox} ${pc17.bold("Fennec Store")} ${pc17.dim(local ? "(local ./.fennec)" : "(global)")}
|
|
6298
|
+
`
|
|
6299
|
+
);
|
|
6300
|
+
const columns = [
|
|
6301
|
+
{ key: "kind", label: "Kind", format: (v) => pc17.bold(String(v)) },
|
|
6302
|
+
{ key: "count", label: "Items", format: (v) => pc17.cyan(String(v)) },
|
|
6303
|
+
{ key: "size", label: "Size", format: (v) => pc17.dim(formatBytes(Number(v))) },
|
|
6304
|
+
{ key: "oldest", label: "Oldest", format: (v) => pc17.dim(formatAge(Number(v))) }
|
|
6305
|
+
];
|
|
6306
|
+
const rows = scan.map((e) => ({
|
|
6307
|
+
kind: e.kind,
|
|
6308
|
+
count: e.count,
|
|
6309
|
+
size: e.sizeBytes,
|
|
6310
|
+
oldest: e.oldestMs
|
|
6311
|
+
}));
|
|
6312
|
+
console.error(renderTable(columns, rows));
|
|
6313
|
+
console.error(` ${pc17.dim(mgr.base)}
|
|
6314
|
+
`);
|
|
6315
|
+
return;
|
|
6316
|
+
}
|
|
6317
|
+
if (!KINDS.includes(kind)) {
|
|
6318
|
+
console.error(renderError("Unknown kind", `Kind must be one of: ${KINDS.join(", ")}`));
|
|
6319
|
+
process.exit(1);
|
|
6320
|
+
}
|
|
6321
|
+
if (kind !== "session") {
|
|
6322
|
+
const entry = mgr.scan().find((e) => e.kind === kind);
|
|
5461
6323
|
console.error(`
|
|
5462
|
-
${pc17.dim(
|
|
6324
|
+
${pc17.bold(kind)} ${pc17.dim(`(${entry?.count ?? 0} item(s))`)}
|
|
6325
|
+
`);
|
|
6326
|
+
console.error(` ${pc17.dim(mgr.dirFor(kind))}
|
|
6327
|
+
`);
|
|
6328
|
+
const tip = kind === "process" ? "Managed via `fennec ps` / `fennec kill` / `fennec restart`." : kind === "export" ? "Created by `fennec export` / `fennec import`." : "Read-only in this view (Phase 2).";
|
|
6329
|
+
console.error(` ${pc17.dim(tip)}
|
|
5463
6330
|
`);
|
|
5464
6331
|
return;
|
|
5465
6332
|
}
|
|
5466
|
-
const
|
|
5467
|
-
|
|
5468
|
-
|
|
5469
|
-
|
|
5470
|
-
|
|
5471
|
-
|
|
5472
|
-
|
|
6333
|
+
const store = mgr.sessionStore();
|
|
6334
|
+
if (!action || action === "ls" || action === "list") {
|
|
6335
|
+
const sessions = store.list();
|
|
6336
|
+
if (sessions.length === 0) {
|
|
6337
|
+
console.error(
|
|
6338
|
+
`
|
|
6339
|
+
${pc17.dim(`No saved sessions found${local ? " in ./.fennec/sessions" : " in the global store"}.`)}
|
|
6340
|
+
`
|
|
6341
|
+
);
|
|
6342
|
+
return;
|
|
6343
|
+
}
|
|
6344
|
+
const columns = [
|
|
6345
|
+
{ key: "name", label: "Name", format: (v) => pc17.bold(String(v)) },
|
|
6346
|
+
{ key: "origin", label: "Origin" },
|
|
6347
|
+
{ key: "savedAt", label: "Saved", format: (v) => pc17.dim(String(v)) }
|
|
6348
|
+
];
|
|
6349
|
+
const rows = sessions.map((s) => ({
|
|
6350
|
+
name: s.name,
|
|
6351
|
+
origin: s.origin,
|
|
6352
|
+
savedAt: new Date(s.savedAt).toLocaleString()
|
|
6353
|
+
}));
|
|
6354
|
+
console.error(`
|
|
5473
6355
|
${symbols.fox} ${pc17.bold("Saved Sessions")}
|
|
5474
6356
|
`);
|
|
5475
|
-
|
|
5476
|
-
|
|
6357
|
+
console.error(renderTable(columns, rows));
|
|
6358
|
+
console.error(` ${pc17.dim(`${sessions.length} session(s)`)}
|
|
6359
|
+
`);
|
|
6360
|
+
return;
|
|
6361
|
+
}
|
|
6362
|
+
if (action === "info") {
|
|
6363
|
+
if (!name) {
|
|
6364
|
+
console.error(
|
|
6365
|
+
renderError("Missing name", "Usage: fennec store session info <name> [--show-secrets]")
|
|
6366
|
+
);
|
|
6367
|
+
process.exit(1);
|
|
6368
|
+
}
|
|
6369
|
+
const session = store.load(name);
|
|
6370
|
+
if (!session) {
|
|
6371
|
+
console.error(
|
|
6372
|
+
renderError(
|
|
6373
|
+
"Session not found",
|
|
6374
|
+
`No session named "${name}". Use 'fennec store session' to list.`
|
|
6375
|
+
)
|
|
6376
|
+
);
|
|
6377
|
+
process.exit(1);
|
|
6378
|
+
}
|
|
6379
|
+
const shown = showSecrets ? session : redactSession(session);
|
|
6380
|
+
const cookieDomains = shown.cookies.map((c) => String(c.domain ?? c.name ?? "?")).filter((v, i, a) => a.indexOf(v) === i);
|
|
6381
|
+
let body = "";
|
|
6382
|
+
body += renderKV("Name", shown.name);
|
|
6383
|
+
body += renderKV("Origin", shown.origin);
|
|
6384
|
+
body += renderKV("Saved", new Date(shown.savedAt).toLocaleString());
|
|
6385
|
+
if (shown.metadata) {
|
|
6386
|
+
for (const [k, v] of Object.entries(shown.metadata)) body += renderKV(k, String(v));
|
|
6387
|
+
}
|
|
6388
|
+
body += renderKV(
|
|
6389
|
+
"Cookies",
|
|
6390
|
+
`${shown.cookies.length} (domains: ${cookieDomains.join(", ") || "-"})`
|
|
6391
|
+
);
|
|
6392
|
+
body += renderKV("localStorage keys", String(Object.keys(shown.localStorage).length));
|
|
6393
|
+
body += renderKV("Values", showSecrets ? "shown" : pc17.dim("masked \u2014 use --show-secrets"));
|
|
6394
|
+
console.error(`
|
|
6395
|
+
${pc17.bold(`Session: ${shown.name}`)}
|
|
5477
6396
|
`);
|
|
6397
|
+
console.error(renderSection("Details", body));
|
|
6398
|
+
return;
|
|
6399
|
+
}
|
|
6400
|
+
if (action === "rm" || action === "remove" || action === "delete") {
|
|
6401
|
+
if (!name) {
|
|
6402
|
+
console.error(renderError("Missing name", "Usage: fennec store session rm <name>"));
|
|
6403
|
+
process.exit(1);
|
|
6404
|
+
}
|
|
6405
|
+
if (!store.load(name)) {
|
|
6406
|
+
console.error(renderError("Session not found", `No session named "${name}".`));
|
|
6407
|
+
process.exit(1);
|
|
6408
|
+
}
|
|
6409
|
+
const confirmed = yes || await confirmPrompt(`Delete session ${pc17.bold(name)}?`, false);
|
|
6410
|
+
if (!confirmed) {
|
|
6411
|
+
console.error(` ${pc17.dim("Cancelled")}`);
|
|
6412
|
+
return;
|
|
6413
|
+
}
|
|
6414
|
+
const ok = store.delete(name);
|
|
6415
|
+
console.error(
|
|
6416
|
+
ok ? ` ${pc17.green("\u2713")} ${pc17.bold(name)} deleted
|
|
6417
|
+
` : ` ${pc17.red("\u2717")} failed to delete ${pc17.bold(name)}
|
|
6418
|
+
`
|
|
6419
|
+
);
|
|
6420
|
+
return;
|
|
6421
|
+
}
|
|
6422
|
+
console.error(renderError("Unknown action", "Actions: ls | info <name> | rm <name>"));
|
|
6423
|
+
process.exit(1);
|
|
5478
6424
|
}
|
|
5479
6425
|
|
|
5480
|
-
// src/commands/
|
|
6426
|
+
// src/commands/doctor.ts
|
|
6427
|
+
import { existsSync as existsSync10, readFileSync as readFileSync9 } from "fs";
|
|
6428
|
+
import { join } from "path";
|
|
6429
|
+
import { execSync as execSync3 } from "child_process";
|
|
6430
|
+
import { StoreManager as StoreManager2 } from "@plumpslabs/fennec-core";
|
|
5481
6431
|
import pc18 from "picocolors";
|
|
6432
|
+
async function doctorCommand() {
|
|
6433
|
+
const mgr = new StoreManager2(false);
|
|
6434
|
+
const base = mgr.base;
|
|
6435
|
+
const problems = [];
|
|
6436
|
+
const notes = [];
|
|
6437
|
+
if (!mgr.permsSafe()) {
|
|
6438
|
+
problems.push(`Store dir is group/other readable: ${base}
|
|
6439
|
+
Fix: chmod 700 ${base}`);
|
|
6440
|
+
}
|
|
6441
|
+
if (StoreManager2.isSynced(base)) {
|
|
6442
|
+
problems.push(
|
|
6443
|
+
`Store lives under a synced directory: ${base}
|
|
6444
|
+
Auth cookies/tokens in sessions + process launch commands could leak across machines.
|
|
6445
|
+
Prefer a non-synced location (FENNEC_HOME=/non-synced/fennec) or use --local with care.`
|
|
6446
|
+
);
|
|
6447
|
+
}
|
|
6448
|
+
const scan = mgr.scan();
|
|
6449
|
+
const sess = scan.find((s) => s.kind === "session");
|
|
6450
|
+
if (sess && sess.count > 0) {
|
|
6451
|
+
notes.push(
|
|
6452
|
+
`${sess.count} auth session(s) with cookies/localStorage stored on disk.
|
|
6453
|
+
Values are masked by default in 'fennec store session info'; use --show-secrets to reveal.`
|
|
6454
|
+
);
|
|
6455
|
+
}
|
|
6456
|
+
const proc = scan.find((s) => s.kind === "process");
|
|
6457
|
+
if (proc && proc.count > 0) {
|
|
6458
|
+
const tracked = mgr.fileFor("process", "");
|
|
6459
|
+
const cmds = existsSync10(tracked) ? JSON.parse(readFileSync9(tracked, "utf-8")) : [];
|
|
6460
|
+
const secretCmds = cmds.filter(
|
|
6461
|
+
(c) => c.command && /(?:^|[\s;|&])([A-Za-z_][A-Za-z0-9_]*)=/.test(c.command)
|
|
6462
|
+
).length;
|
|
6463
|
+
if (secretCmds > 0) {
|
|
6464
|
+
notes.push(
|
|
6465
|
+
`${secretCmds} tracked process launch command(s) may embed secrets (KEY=value).
|
|
6466
|
+
Stored in ${tracked} \u2014 keep the store out of synced/committed locations.`
|
|
6467
|
+
);
|
|
6468
|
+
}
|
|
6469
|
+
}
|
|
6470
|
+
const localFen = join(process.cwd(), ".fennec");
|
|
6471
|
+
if (existsSync10(localFen)) {
|
|
6472
|
+
let ignored = false;
|
|
6473
|
+
try {
|
|
6474
|
+
execSync3("git check-ignore -q .fennec", { cwd: process.cwd(), stdio: "ignore" });
|
|
6475
|
+
ignored = true;
|
|
6476
|
+
} catch (err) {
|
|
6477
|
+
ignored = err?.code === "ENOENT";
|
|
6478
|
+
}
|
|
6479
|
+
if (!ignored) {
|
|
6480
|
+
problems.push(
|
|
6481
|
+
`Local ./.fennec exists here and is NOT gitignored.
|
|
6482
|
+
It can hold sessions + tracked processes \u2014 add '.fennec/' to .gitignore or remove it.`
|
|
6483
|
+
);
|
|
6484
|
+
}
|
|
6485
|
+
}
|
|
6486
|
+
console.error(`
|
|
6487
|
+
${pc18.bold("Fennec Doctor")}
|
|
6488
|
+
`);
|
|
6489
|
+
console.error(` ${pc18.dim("Store:")} ${base}
|
|
6490
|
+
`);
|
|
6491
|
+
if (problems.length === 0 && notes.length === 0) {
|
|
6492
|
+
console.error(` ${pc18.green("\u2713")} No issues found.
|
|
6493
|
+
`);
|
|
6494
|
+
return;
|
|
6495
|
+
}
|
|
6496
|
+
for (const p of problems) console.error(` ${pc18.red("\u2717")} ${p}
|
|
6497
|
+
`);
|
|
6498
|
+
for (const n of notes) console.error(` ${pc18.yellow("!")} ${n}
|
|
6499
|
+
`);
|
|
6500
|
+
}
|
|
6501
|
+
|
|
6502
|
+
// src/commands/setup.ts
|
|
6503
|
+
import pc19 from "picocolors";
|
|
5482
6504
|
async function setupCommand() {
|
|
5483
6505
|
console.error(`
|
|
5484
|
-
${symbols.fox} ${
|
|
6506
|
+
${symbols.fox} ${pc19.bold("Fennec Setup")}
|
|
5485
6507
|
`);
|
|
5486
6508
|
const mcpClient = await selectPrompt("Which MCP client are you using?", [
|
|
5487
6509
|
{ value: "claude", label: "Claude Desktop", description: "Anthropic's AI desktop app" },
|
|
@@ -5490,12 +6512,12 @@ async function setupCommand() {
|
|
|
5490
6512
|
{ value: "other", label: "Other MCP client", description: "Any MCP-compatible client" }
|
|
5491
6513
|
]);
|
|
5492
6514
|
if (!mcpClient) {
|
|
5493
|
-
console.error(` ${
|
|
6515
|
+
console.error(` ${pc19.dim("Setup cancelled.")}
|
|
5494
6516
|
`);
|
|
5495
6517
|
return;
|
|
5496
6518
|
}
|
|
5497
6519
|
console.error(`
|
|
5498
|
-
${
|
|
6520
|
+
${pc19.green("\u2713")} Selected: ${pc19.bold(mcpClient)}
|
|
5499
6521
|
`);
|
|
5500
6522
|
const configSnippet = `{
|
|
5501
6523
|
"mcpServers": {
|
|
@@ -5505,46 +6527,55 @@ async function setupCommand() {
|
|
|
5505
6527
|
}
|
|
5506
6528
|
}
|
|
5507
6529
|
}`;
|
|
5508
|
-
console.error(` ${
|
|
5509
|
-
`);
|
|
5510
|
-
console.error(` ${pc18.dim("```")}`);
|
|
5511
|
-
console.error(configSnippet.split("\n").map((l) => ` ${l}`).join("\n"));
|
|
5512
|
-
console.error(` ${pc18.dim("```")}
|
|
6530
|
+
console.error(` ${pc19.bold("Add this to your MCP client config:")}
|
|
5513
6531
|
`);
|
|
5514
|
-
console.error(` ${
|
|
6532
|
+
console.error(` ${pc19.dim("```")}`);
|
|
6533
|
+
console.error(
|
|
6534
|
+
configSnippet.split("\n").map((l) => ` ${l}`).join("\n")
|
|
6535
|
+
);
|
|
6536
|
+
console.error(` ${pc19.dim("```")}
|
|
5515
6537
|
`);
|
|
6538
|
+
console.error(
|
|
6539
|
+
` ${renderSuccess("Setup complete!")} ${pc19.dim("Run")} ${renderCommand("fennec start")} ${pc19.dim("to begin.")}
|
|
6540
|
+
`
|
|
6541
|
+
);
|
|
5516
6542
|
}
|
|
5517
6543
|
|
|
5518
6544
|
// src/commands/management.ts
|
|
5519
|
-
import { existsSync as
|
|
5520
|
-
import { resolve as
|
|
5521
|
-
import { execSync as
|
|
5522
|
-
import
|
|
6545
|
+
import { existsSync as existsSync11, writeFileSync as writeFileSync4 } from "fs";
|
|
6546
|
+
import { resolve as resolve7 } from "path";
|
|
6547
|
+
import { execSync as execSync4 } from "child_process";
|
|
6548
|
+
import pc20 from "picocolors";
|
|
5523
6549
|
async function installBrowsersCommand() {
|
|
5524
6550
|
console.error(`
|
|
5525
|
-
${
|
|
6551
|
+
${pc20.bold("Installing Browser Engines")}
|
|
5526
6552
|
`);
|
|
5527
6553
|
const spinner = createSpinner("Installing Chromium...");
|
|
5528
6554
|
try {
|
|
5529
|
-
|
|
6555
|
+
execSync4("npx playwright install chromium", { stdio: "pipe", timeout: 12e4 });
|
|
5530
6556
|
spinner.succeed("Chromium installed successfully");
|
|
5531
6557
|
} catch {
|
|
5532
6558
|
spinner.fail("Failed to install Chromium");
|
|
5533
|
-
console.error(
|
|
6559
|
+
console.error(
|
|
6560
|
+
` ${pc20.yellow("\u2192")} Try running: ${renderCommand("npx playwright install chromium")}`
|
|
6561
|
+
);
|
|
5534
6562
|
}
|
|
5535
6563
|
console.error(`
|
|
5536
|
-
${
|
|
6564
|
+
${pc20.green("\u2713")} Browser installation complete.
|
|
5537
6565
|
`);
|
|
5538
6566
|
}
|
|
5539
6567
|
async function initCommand() {
|
|
5540
6568
|
console.error(`
|
|
5541
|
-
${
|
|
6569
|
+
${pc20.bold("Initialize Fennec Configuration")}
|
|
5542
6570
|
`);
|
|
5543
|
-
const configFile =
|
|
5544
|
-
if (
|
|
5545
|
-
const overwrite = await confirmPrompt(
|
|
6571
|
+
const configFile = resolve7("./fennec.config.yaml");
|
|
6572
|
+
if (existsSync11(configFile)) {
|
|
6573
|
+
const overwrite = await confirmPrompt(
|
|
6574
|
+
`${pc20.yellow("fennec.config.yaml")} already exists. Overwrite?`,
|
|
6575
|
+
false
|
|
6576
|
+
);
|
|
5546
6577
|
if (!overwrite) {
|
|
5547
|
-
console.error(` ${
|
|
6578
|
+
console.error(` ${pc20.dim("Cancelled.")}
|
|
5548
6579
|
`);
|
|
5549
6580
|
return;
|
|
5550
6581
|
}
|
|
@@ -5624,44 +6655,44 @@ logging:
|
|
|
5624
6655
|
file: null
|
|
5625
6656
|
`;
|
|
5626
6657
|
writeFileSync4(configFile, config, "utf-8");
|
|
5627
|
-
spinner.succeed(`Configuration written to ${
|
|
6658
|
+
spinner.succeed(`Configuration written to ${pc20.bold(configFile)}`);
|
|
5628
6659
|
console.error(`
|
|
5629
|
-
${
|
|
6660
|
+
${pc20.dim("Edit the file to customize Fennec behavior.")}
|
|
5630
6661
|
`);
|
|
5631
6662
|
}
|
|
5632
6663
|
|
|
5633
6664
|
// src/commands/health.ts
|
|
5634
|
-
import { existsSync as
|
|
5635
|
-
import { execSync as
|
|
5636
|
-
import { resolve as
|
|
5637
|
-
import { homedir as
|
|
5638
|
-
import
|
|
6665
|
+
import { existsSync as existsSync12, readdirSync as readdirSync2, statSync as statSync4 } from "fs";
|
|
6666
|
+
import { execSync as execSync5 } from "child_process";
|
|
6667
|
+
import { resolve as resolve8 } from "path";
|
|
6668
|
+
import { homedir as homedir5 } from "os";
|
|
6669
|
+
import pc21 from "picocolors";
|
|
5639
6670
|
async function healthCommand() {
|
|
5640
6671
|
printBanner();
|
|
5641
|
-
console.error(` ${
|
|
6672
|
+
console.error(` ${pc21.bold("Fennec Health Check")}
|
|
5642
6673
|
`);
|
|
5643
6674
|
let adbStatus = "unknown";
|
|
5644
6675
|
try {
|
|
5645
|
-
const result =
|
|
6676
|
+
const result = execSync5("adb --version", {
|
|
5646
6677
|
encoding: "utf-8",
|
|
5647
6678
|
timeout: 3e3,
|
|
5648
6679
|
stdio: ["ignore", "pipe", "pipe"]
|
|
5649
6680
|
});
|
|
5650
6681
|
const version = result.split("\n")[0]?.trim() ?? "installed";
|
|
5651
|
-
adbStatus =
|
|
6682
|
+
adbStatus = pc21.green(version);
|
|
5652
6683
|
} catch {
|
|
5653
|
-
adbStatus =
|
|
6684
|
+
adbStatus = pc21.dim("not found") + " (optional)";
|
|
5654
6685
|
}
|
|
5655
6686
|
const tracked = readTracked();
|
|
5656
6687
|
const runningCount = tracked.filter((t) => isProcessRunning(t.pid)).length;
|
|
5657
|
-
const logDir =
|
|
6688
|
+
const logDir = resolve8(homedir5(), ".fennec", "logs");
|
|
5658
6689
|
let logSize = "0 B";
|
|
5659
6690
|
try {
|
|
5660
|
-
if (
|
|
6691
|
+
if (existsSync12(logDir)) {
|
|
5661
6692
|
const files = readdirSync2(logDir);
|
|
5662
6693
|
let totalBytes = 0;
|
|
5663
6694
|
for (const f of files) {
|
|
5664
|
-
const fStat =
|
|
6695
|
+
const fStat = statSync4(resolve8(logDir, f));
|
|
5665
6696
|
totalBytes += fStat.size;
|
|
5666
6697
|
}
|
|
5667
6698
|
logSize = totalBytes > 1024 * 1024 ? `${(totalBytes / (1024 * 1024)).toFixed(1)} MB` : `${(totalBytes / 1024).toFixed(1)} KB`;
|
|
@@ -5674,7 +6705,7 @@ async function healthCommand() {
|
|
|
5674
6705
|
memoryInfo = `${(mem.rss / (1024 * 1024)).toFixed(0)} MB RSS / ${(mem.heapUsed / (1024 * 1024)).toFixed(0)} MB heap`;
|
|
5675
6706
|
} catch {
|
|
5676
6707
|
}
|
|
5677
|
-
console.error(` ${symbols.fox} ${
|
|
6708
|
+
console.error(` ${symbols.fox} ${pc21.bold("System")}
|
|
5678
6709
|
`);
|
|
5679
6710
|
console.error(` ${renderKV("Node.js", process.version)}`);
|
|
5680
6711
|
console.error(` ${renderKV("Platform", `${process.platform} ${process.arch}`)}`);
|
|
@@ -5682,7 +6713,7 @@ async function healthCommand() {
|
|
|
5682
6713
|
console.error(` ${renderKV("ADB", adbStatus)}`);
|
|
5683
6714
|
console.error(` ${renderKV("PID", String(process.pid))}`);
|
|
5684
6715
|
console.error();
|
|
5685
|
-
console.error(` ${symbols.fox} ${
|
|
6716
|
+
console.error(` ${symbols.fox} ${pc21.bold("Processes")}
|
|
5686
6717
|
`);
|
|
5687
6718
|
console.error(` ${renderKV("Tracked", String(tracked.length))}`);
|
|
5688
6719
|
console.error(` ${renderKV("Running", String(runningCount))}`);
|
|
@@ -5690,56 +6721,67 @@ async function healthCommand() {
|
|
|
5690
6721
|
console.error();
|
|
5691
6722
|
const allHealthy = runningCount === tracked.length || tracked.length === 0;
|
|
5692
6723
|
if (allHealthy) {
|
|
5693
|
-
console.error(` ${
|
|
6724
|
+
console.error(` ${pc21.green("\u2713")} ${pc21.bold("All systems healthy")}
|
|
5694
6725
|
`);
|
|
5695
6726
|
} else {
|
|
5696
6727
|
const stopped = tracked.length - runningCount;
|
|
5697
|
-
console.error(
|
|
5698
|
-
`)
|
|
6728
|
+
console.error(
|
|
6729
|
+
` ${pc21.yellow("\u26A0")} ${pc21.bold(`${stopped} process(es) stopped`)} ${pc21.dim("fennec ps")}
|
|
6730
|
+
`
|
|
6731
|
+
);
|
|
5699
6732
|
}
|
|
5700
6733
|
}
|
|
5701
6734
|
|
|
5702
6735
|
// src/commands/cleanup.ts
|
|
5703
|
-
import
|
|
6736
|
+
import pc22 from "picocolors";
|
|
5704
6737
|
async function cleanupCommand() {
|
|
5705
6738
|
const tracked = readTracked();
|
|
5706
6739
|
if (tracked.length === 0) {
|
|
5707
6740
|
console.error(`
|
|
5708
|
-
${
|
|
6741
|
+
${pc22.dim("No tracked processes to clean up.")}
|
|
5709
6742
|
`);
|
|
5710
6743
|
return;
|
|
5711
6744
|
}
|
|
5712
6745
|
const toRemove = tracked.filter((t) => !isProcessRunning(t.pid) && !t.command);
|
|
5713
6746
|
if (toRemove.length === 0) {
|
|
5714
|
-
console.error(
|
|
5715
|
-
|
|
5716
|
-
|
|
6747
|
+
console.error(
|
|
6748
|
+
`
|
|
6749
|
+
${pc22.green("\u2713")} ${pc22.bold("All clean")} ${pc22.dim("\u2014 no dead entries without saved commands.")}
|
|
6750
|
+
`
|
|
6751
|
+
);
|
|
5717
6752
|
return;
|
|
5718
6753
|
}
|
|
5719
|
-
console.error(
|
|
5720
|
-
|
|
5721
|
-
`)
|
|
6754
|
+
console.error(
|
|
6755
|
+
`
|
|
6756
|
+
${pc22.yellow("\u26A0")} ${pc22.bold(`Found ${toRemove.length} dead entr${toRemove.length > 1 ? "ies" : "y"} without saved commands`)}
|
|
6757
|
+
`
|
|
6758
|
+
);
|
|
5722
6759
|
for (const entry of toRemove) {
|
|
5723
|
-
console.error(` ${
|
|
6760
|
+
console.error(` ${pc22.dim("\xB7")} ${pc22.bold(entry.name)} ${pc22.dim(`(PID ${entry.pid})`)}`);
|
|
5724
6761
|
}
|
|
5725
6762
|
console.error();
|
|
5726
|
-
const confirmed = await confirmPrompt(
|
|
6763
|
+
const confirmed = await confirmPrompt(
|
|
6764
|
+
`Remove ${toRemove.length} entr${toRemove.length > 1 ? "ies" : "y"}?`,
|
|
6765
|
+
false
|
|
6766
|
+
);
|
|
5727
6767
|
if (!confirmed) {
|
|
5728
|
-
console.error(` ${
|
|
6768
|
+
console.error(` ${pc22.dim("Cancelled")}
|
|
5729
6769
|
`);
|
|
5730
6770
|
return;
|
|
5731
6771
|
}
|
|
5732
6772
|
const remaining = tracked.filter((t) => !toRemove.includes(t));
|
|
5733
6773
|
saveTracked(remaining);
|
|
5734
|
-
console.error(
|
|
5735
|
-
|
|
5736
|
-
`)
|
|
6774
|
+
console.error(
|
|
6775
|
+
`
|
|
6776
|
+
${pc22.green("\u2713")} ${pc22.bold(`Removed ${toRemove.length} entr${toRemove.length > 1 ? "ies" : "y"}`)} ${pc22.dim(`(${remaining.length} remaining)`)}
|
|
6777
|
+
`
|
|
6778
|
+
);
|
|
5737
6779
|
}
|
|
5738
6780
|
|
|
5739
6781
|
// src/commands/info.ts
|
|
5740
|
-
import
|
|
5741
|
-
import { resolve as
|
|
5742
|
-
import { homedir as
|
|
6782
|
+
import pc23 from "picocolors";
|
|
6783
|
+
import { resolve as resolve9 } from "path";
|
|
6784
|
+
import { homedir as homedir6 } from "os";
|
|
5743
6785
|
async function infoCommand(args2) {
|
|
5744
6786
|
const name = args2[0];
|
|
5745
6787
|
if (!name) {
|
|
@@ -5753,34 +6795,34 @@ async function infoCommand(args2) {
|
|
|
5753
6795
|
process.exit(1);
|
|
5754
6796
|
}
|
|
5755
6797
|
const running = isProcessRunning(match.pid);
|
|
5756
|
-
const statusIcon = running ?
|
|
5757
|
-
const statusText = running ?
|
|
6798
|
+
const statusIcon = running ? pc23.green("\u25CF") : pc23.red("\u25CB");
|
|
6799
|
+
const statusText = running ? pc23.green("running") : pc23.red("stopped");
|
|
5758
6800
|
const uptime = running ? formatUptime(Math.floor((Date.now() - new Date(match.startedAt).getTime()) / 1e3)) : "-";
|
|
5759
|
-
const logPath =
|
|
6801
|
+
const logPath = resolve9(homedir6(), ".fennec", "logs", `${match.name}.log`);
|
|
5760
6802
|
console.error(`
|
|
5761
|
-
${symbols.fox} ${
|
|
6803
|
+
${symbols.fox} ${pc23.bold(match.name)} ${pc23.dim("\u2014 Process Info")}
|
|
5762
6804
|
`);
|
|
5763
|
-
console.error(` ${renderKVColor("Name", match.name,
|
|
6805
|
+
console.error(` ${renderKVColor("Name", match.name, pc23.bold)}`);
|
|
5764
6806
|
console.error(` ${renderKVColor("Status", `${statusIcon} ${statusText}`)}`);
|
|
5765
6807
|
console.error(` ${renderKVColor("PID", String(match.pid))}`);
|
|
5766
6808
|
console.error(` ${renderKVColor("Port", match.port ? String(match.port) : "-")}`);
|
|
5767
|
-
console.error(` ${renderKVColor("Command", match.command ||
|
|
6809
|
+
console.error(` ${renderKVColor("Command", match.command || pc23.dim("(none)"))}`);
|
|
5768
6810
|
if (match.cwd) console.error(` ${renderKVColor("CWD", match.cwd)}`);
|
|
5769
6811
|
console.error(` ${renderKVColor("Started", new Date(match.startedAt).toLocaleString())}`);
|
|
5770
6812
|
console.error(` ${renderKVColor("Uptime", uptime)}`);
|
|
5771
6813
|
console.error(` ${renderKVColor("Log Path", logPath)}`);
|
|
5772
6814
|
if (!running && match.command) {
|
|
5773
6815
|
console.error(`
|
|
5774
|
-
${
|
|
6816
|
+
${pc23.dim("Re-spawn with:")} ${pc23.cyan(`fennec spawn ${match.name}`)}`);
|
|
5775
6817
|
}
|
|
5776
6818
|
console.error();
|
|
5777
6819
|
}
|
|
5778
6820
|
|
|
5779
6821
|
// src/commands/rename.ts
|
|
5780
|
-
import
|
|
5781
|
-
import { existsSync as
|
|
5782
|
-
import { resolve as
|
|
5783
|
-
import { homedir as
|
|
6822
|
+
import pc24 from "picocolors";
|
|
6823
|
+
import { existsSync as existsSync13, renameSync as renameSync2 } from "fs";
|
|
6824
|
+
import { resolve as resolve10 } from "path";
|
|
6825
|
+
import { homedir as homedir7 } from "os";
|
|
5784
6826
|
async function renameCommand(args2) {
|
|
5785
6827
|
const [oldName, newName] = args2;
|
|
5786
6828
|
if (!oldName || !newName) {
|
|
@@ -5789,7 +6831,7 @@ async function renameCommand(args2) {
|
|
|
5789
6831
|
}
|
|
5790
6832
|
if (oldName === newName) {
|
|
5791
6833
|
console.error(`
|
|
5792
|
-
${
|
|
6834
|
+
${pc24.yellow("\u26A0")} ${pc24.dim("Old and new names are the same.")}
|
|
5793
6835
|
`);
|
|
5794
6836
|
process.exit(0);
|
|
5795
6837
|
}
|
|
@@ -5804,25 +6846,30 @@ async function renameCommand(args2) {
|
|
|
5804
6846
|
process.exit(1);
|
|
5805
6847
|
}
|
|
5806
6848
|
console.error(`
|
|
5807
|
-
${symbols.fox} ${
|
|
6849
|
+
${symbols.fox} ${pc24.bold("Rename Process")}
|
|
5808
6850
|
`);
|
|
5809
6851
|
console.error(` ${renderKVColor("From", oldName)}`);
|
|
5810
6852
|
console.error(` ${renderKVColor("To", newName)}`);
|
|
5811
6853
|
if (isProcessRunning(match.pid)) {
|
|
5812
|
-
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
|
+
);
|
|
5813
6857
|
}
|
|
5814
6858
|
console.error();
|
|
5815
|
-
const confirmed = await confirmPrompt(
|
|
6859
|
+
const confirmed = await confirmPrompt(
|
|
6860
|
+
`Rename ${pc24.bold(oldName)} ${pc24.dim("\u2192")} ${pc24.bold(newName)}?`,
|
|
6861
|
+
true
|
|
6862
|
+
);
|
|
5816
6863
|
if (!confirmed) {
|
|
5817
|
-
console.error(` ${
|
|
6864
|
+
console.error(` ${pc24.dim("Cancelled")}
|
|
5818
6865
|
`);
|
|
5819
6866
|
return;
|
|
5820
6867
|
}
|
|
5821
6868
|
const spinner = createSpinner(`Renaming ${oldName} \u2192 ${newName}...`);
|
|
5822
|
-
const logDir =
|
|
5823
|
-
const oldLog =
|
|
5824
|
-
const newLog =
|
|
5825
|
-
if (
|
|
6869
|
+
const logDir = resolve10(homedir7(), ".fennec", "logs");
|
|
6870
|
+
const oldLog = resolve10(logDir, `${oldName}.log`);
|
|
6871
|
+
const newLog = resolve10(logDir, `${newName}.log`);
|
|
6872
|
+
if (existsSync13(oldLog) && !existsSync13(newLog)) {
|
|
5826
6873
|
try {
|
|
5827
6874
|
renameSync2(oldLog, newLog);
|
|
5828
6875
|
} catch {
|
|
@@ -5830,14 +6877,14 @@ async function renameCommand(args2) {
|
|
|
5830
6877
|
}
|
|
5831
6878
|
match.name = newName;
|
|
5832
6879
|
saveTracked(tracked);
|
|
5833
|
-
spinner.succeed(`${
|
|
6880
|
+
spinner.succeed(`${pc24.bold(oldName)} ${pc24.dim("\u2192")} ${pc24.bold(newName)}`);
|
|
5834
6881
|
console.error();
|
|
5835
6882
|
}
|
|
5836
6883
|
|
|
5837
6884
|
// src/commands/export-import.ts
|
|
5838
|
-
import
|
|
5839
|
-
import { existsSync as
|
|
5840
|
-
import { resolve as
|
|
6885
|
+
import pc25 from "picocolors";
|
|
6886
|
+
import { existsSync as existsSync14, writeFileSync as writeFileSync5, readFileSync as readFileSync10 } from "fs";
|
|
6887
|
+
import { resolve as resolve11 } from "path";
|
|
5841
6888
|
async function exportCommand(args2) {
|
|
5842
6889
|
const tracked = readTracked();
|
|
5843
6890
|
const fileIndex = args2.indexOf("--file");
|
|
@@ -5848,17 +6895,19 @@ async function exportCommand(args2) {
|
|
|
5848
6895
|
}
|
|
5849
6896
|
if (tracked.length === 0) {
|
|
5850
6897
|
console.error(`
|
|
5851
|
-
${
|
|
6898
|
+
${pc25.dim("No tracked processes to export.")}
|
|
5852
6899
|
`);
|
|
5853
6900
|
return;
|
|
5854
6901
|
}
|
|
5855
6902
|
const json = JSON.stringify(tracked, null, 2);
|
|
5856
6903
|
if (filePath) {
|
|
5857
6904
|
try {
|
|
5858
|
-
writeFileSync5(
|
|
5859
|
-
console.error(
|
|
5860
|
-
|
|
5861
|
-
`)
|
|
6905
|
+
writeFileSync5(resolve11(filePath), json, "utf-8");
|
|
6906
|
+
console.error(
|
|
6907
|
+
`
|
|
6908
|
+
${pc25.green("\u2713")} ${pc25.bold(`Exported ${tracked.length} process(es)`)} ${pc25.dim(`\u2192 ${filePath}`)}
|
|
6909
|
+
`
|
|
6910
|
+
);
|
|
5862
6911
|
} catch (err) {
|
|
5863
6912
|
console.error(renderError("Export failed", String(err)));
|
|
5864
6913
|
process.exit(1);
|
|
@@ -5873,16 +6922,18 @@ async function importCommand(args2) {
|
|
|
5873
6922
|
console.error(renderError("Missing file", "Usage: fennec import <file>"));
|
|
5874
6923
|
process.exit(1);
|
|
5875
6924
|
}
|
|
5876
|
-
const resolvedPath =
|
|
5877
|
-
if (!
|
|
6925
|
+
const resolvedPath = resolve11(filePath);
|
|
6926
|
+
if (!existsSync14(resolvedPath)) {
|
|
5878
6927
|
console.error(renderError("File not found", `"${filePath}" does not exist.`));
|
|
5879
6928
|
process.exit(1);
|
|
5880
6929
|
}
|
|
5881
6930
|
let imported;
|
|
5882
6931
|
try {
|
|
5883
|
-
imported = JSON.parse(
|
|
6932
|
+
imported = JSON.parse(readFileSync10(resolvedPath, "utf-8"));
|
|
5884
6933
|
if (!Array.isArray(imported) || imported.length === 0) {
|
|
5885
|
-
console.error(
|
|
6934
|
+
console.error(
|
|
6935
|
+
renderError("Invalid file", "File must contain a non-empty JSON array of process objects.")
|
|
6936
|
+
);
|
|
5886
6937
|
process.exit(1);
|
|
5887
6938
|
}
|
|
5888
6939
|
} catch (err) {
|
|
@@ -5891,20 +6942,23 @@ async function importCommand(args2) {
|
|
|
5891
6942
|
}
|
|
5892
6943
|
const existing = readTracked();
|
|
5893
6944
|
console.error(`
|
|
5894
|
-
${symbols.fox} ${
|
|
6945
|
+
${symbols.fox} ${pc25.bold("Import Processes")}
|
|
5895
6946
|
`);
|
|
5896
6947
|
console.error(` ${renderKVColor("File", resolvedPath)}`);
|
|
5897
6948
|
console.error(` ${renderKVColor("Importing", `${imported.length} process(es)`)}`);
|
|
5898
6949
|
console.error(` ${renderKVColor("Existing", `${existing.length} process(es)`)}`);
|
|
5899
6950
|
console.error(`
|
|
5900
|
-
${
|
|
6951
|
+
${pc25.bold("Processes to import:")}`);
|
|
5901
6952
|
for (const p of imported) {
|
|
5902
|
-
console.error(` ${
|
|
6953
|
+
console.error(` ${pc25.green("+")} ${pc25.bold(p.name)} ${pc25.dim(`(${p.command})`)}`);
|
|
5903
6954
|
}
|
|
5904
6955
|
console.error();
|
|
5905
|
-
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
|
+
);
|
|
5906
6960
|
if (!confirmed) {
|
|
5907
|
-
console.error(` ${
|
|
6961
|
+
console.error(` ${pc25.dim("Cancelled")}
|
|
5908
6962
|
`);
|
|
5909
6963
|
return;
|
|
5910
6964
|
}
|
|
@@ -5918,8 +6972,108 @@ async function importCommand(args2) {
|
|
|
5918
6972
|
}
|
|
5919
6973
|
}
|
|
5920
6974
|
saveTracked(merged);
|
|
5921
|
-
console.error(
|
|
6975
|
+
console.error(
|
|
6976
|
+
` ${pc25.green("\u2713")} ${pc25.bold(`Imported ${imported.length} process(es)`)} ${pc25.dim(`(${merged.length} total)`)}
|
|
6977
|
+
`
|
|
6978
|
+
);
|
|
6979
|
+
}
|
|
6980
|
+
|
|
6981
|
+
// src/commands/group.ts
|
|
6982
|
+
import pc26 from "picocolors";
|
|
6983
|
+
async function groupCommand(args2) {
|
|
6984
|
+
const positional = args2.filter((a) => !a.startsWith("-") && !a.includes("="));
|
|
6985
|
+
const unset = args2.includes("--unset");
|
|
6986
|
+
if (positional.length === 0) {
|
|
6987
|
+
const tracked = readTracked();
|
|
6988
|
+
if (tracked.length === 0) {
|
|
6989
|
+
console.error(`
|
|
6990
|
+
${pc26.dim("No tracked processes.")}
|
|
5922
6991
|
`);
|
|
6992
|
+
return;
|
|
6993
|
+
}
|
|
6994
|
+
const columns = [
|
|
6995
|
+
{ key: "name", label: "App", format: (v) => pc26.bold(String(v)) },
|
|
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
|
+
}
|
|
7012
|
+
];
|
|
7013
|
+
const rows = tracked.map((t) => ({
|
|
7014
|
+
name: t.name,
|
|
7015
|
+
group: t.group ?? "-",
|
|
7016
|
+
command: t.command
|
|
7017
|
+
}));
|
|
7018
|
+
console.error(
|
|
7019
|
+
`
|
|
7020
|
+
${pc26.bold("Fennec Apps by Group")} ${pc26.dim(`(${getGroups().length} group(s))`)}
|
|
7021
|
+
`
|
|
7022
|
+
);
|
|
7023
|
+
console.error(renderTable(columns, rows));
|
|
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
|
+
);
|
|
7028
|
+
console.error();
|
|
7029
|
+
return;
|
|
7030
|
+
}
|
|
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
|
+
}
|
|
7038
|
+
console.error(`
|
|
7039
|
+
${pc26.green("\u2713")} ${pc26.bold(name)} ${pc26.dim("removed from its group")}
|
|
7040
|
+
`);
|
|
7041
|
+
return;
|
|
7042
|
+
}
|
|
7043
|
+
let group;
|
|
7044
|
+
let names;
|
|
7045
|
+
if (positional.length >= 3) {
|
|
7046
|
+
group = positional[0];
|
|
7047
|
+
names = positional.slice(1);
|
|
7048
|
+
} else {
|
|
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);
|
|
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();
|
|
5923
7077
|
}
|
|
5924
7078
|
|
|
5925
7079
|
// src/index.ts
|
|
@@ -5977,6 +7131,8 @@ async function main() {
|
|
|
5977
7131
|
await cleanupCommand();
|
|
5978
7132
|
} else if (command === "rename") {
|
|
5979
7133
|
await renameCommand(args);
|
|
7134
|
+
} else if (command === "group") {
|
|
7135
|
+
await groupCommand(args);
|
|
5980
7136
|
} else if (command === "export") {
|
|
5981
7137
|
await exportCommand(args);
|
|
5982
7138
|
} else if (command === "import") {
|
|
@@ -5991,8 +7147,14 @@ async function main() {
|
|
|
5991
7147
|
await attachPortCommand(args);
|
|
5992
7148
|
} else if (command === "watch") {
|
|
5993
7149
|
await watchCommand(args);
|
|
7150
|
+
} else if (command === "store") {
|
|
7151
|
+
printBanner();
|
|
7152
|
+
await storeCommand(args);
|
|
7153
|
+
} else if (command === "doctor") {
|
|
7154
|
+
printBanner();
|
|
7155
|
+
await doctorCommand();
|
|
5994
7156
|
} else if (command === "sessions") {
|
|
5995
|
-
await
|
|
7157
|
+
await storeCommand(["session"]);
|
|
5996
7158
|
} else if (command === "setup") {
|
|
5997
7159
|
await setupCommand();
|
|
5998
7160
|
} else if (command === "install-browsers") {
|
|
@@ -6013,7 +7175,9 @@ async function main() {
|
|
|
6013
7175
|
showHelp();
|
|
6014
7176
|
}
|
|
6015
7177
|
} else {
|
|
6016
|
-
console.error(
|
|
7178
|
+
console.error(
|
|
7179
|
+
renderError(`Unknown command: ${command}`, "Run 'fennec help' for usage information")
|
|
7180
|
+
);
|
|
6017
7181
|
process.exit(1);
|
|
6018
7182
|
}
|
|
6019
7183
|
}
|