@plumpslabs/fennec-cli 1.14.1 → 1.14.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +59 -7
- package/dist/index.js +771 -203
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -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,12 @@ 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
|
-
["--restart", "Auto-restart if it crashes or its port stops listening (detached supervisor, survives terminal close)"]
|
|
2169
|
+
["--restart", "Auto-restart if it crashes or its port stops listening (detached supervisor, survives terminal close)"],
|
|
2170
|
+
["--group <group>", "Logical group/namespace for bulk ops (kill/spawn/stop --group <group>)"]
|
|
2162
2171
|
],
|
|
2163
2172
|
examples: [
|
|
2164
2173
|
'start "npm run dev" --name web --port 3000',
|
|
2165
|
-
"start node server.js --name api --cwd ./backend --restart"
|
|
2174
|
+
"start node server.js --name api --cwd ./backend --restart --group backend"
|
|
2166
2175
|
]
|
|
2167
2176
|
},
|
|
2168
2177
|
run: {
|
|
@@ -2175,18 +2184,20 @@ var COMMANDS = {
|
|
|
2175
2184
|
["--name <name>", "App name used for tracking (recommended)"],
|
|
2176
2185
|
["--port <port>", "Port the app listens on"],
|
|
2177
2186
|
["--cwd <dir>", "Working directory"],
|
|
2178
|
-
["--restart", "Auto-restart if it crashes or its port stops listening"]
|
|
2187
|
+
["--restart", "Auto-restart if it crashes or its port stops listening"],
|
|
2188
|
+
["--group <group>", "Logical group for bulk ops"]
|
|
2179
2189
|
],
|
|
2180
|
-
examples: ["run npm start --name web --port 8080"]
|
|
2190
|
+
examples: ["run npm start --name web --port 8080 --group frontend"]
|
|
2181
2191
|
},
|
|
2182
2192
|
ps: {
|
|
2183
2193
|
name: "ps",
|
|
2184
2194
|
usage: "ps [options]",
|
|
2185
2195
|
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).",
|
|
2196
|
+
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
2197
|
options: [
|
|
2188
2198
|
["--system, -a, --all", "Show all system processes instead of tracked apps"],
|
|
2189
2199
|
["--name <name>", "Filter system processes by name"],
|
|
2200
|
+
["--group <g>, -g <g>", "Show only tracked apps in group <g>"],
|
|
2190
2201
|
["--sort <field>", "Sort by cpu | mem | pid | name (default: name)"],
|
|
2191
2202
|
["--json", "Output tracked apps as JSON"],
|
|
2192
2203
|
["-w, --watch", "Watch mode \u2014 refresh every 3s (with --system)"]
|
|
@@ -2219,37 +2230,45 @@ var COMMANDS = {
|
|
|
2219
2230
|
},
|
|
2220
2231
|
spawn: {
|
|
2221
2232
|
name: "spawn",
|
|
2222
|
-
usage: "spawn [name] [--all]",
|
|
2233
|
+
usage: "spawn [name] [name...] [--all] [--group <g>]",
|
|
2223
2234
|
summary: "Re-spawn a stopped tracked app from its saved config",
|
|
2224
2235
|
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
|
-
|
|
2236
|
+
options: [
|
|
2237
|
+
["--all, -a", "Re-spawn all stopped apps that have a saved command"],
|
|
2238
|
+
["--group <g>, -g <g>", "Re-spawn only stopped apps in group <g>"]
|
|
2239
|
+
],
|
|
2240
|
+
examples: ["spawn", "spawn web", "spawn be-crm fe-crm", "spawn --all", "spawn --group backend"]
|
|
2227
2241
|
},
|
|
2228
2242
|
stop: {
|
|
2229
2243
|
name: "stop",
|
|
2230
|
-
usage: "stop <name|--all>",
|
|
2244
|
+
usage: "stop <name|--all> [name...] [--group <g>]",
|
|
2231
2245
|
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
|
-
|
|
2246
|
+
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`).",
|
|
2247
|
+
options: [
|
|
2248
|
+
["--all, -a", "Stop all running tracked apps"],
|
|
2249
|
+
["--group <g>, -g <g>", "Stop only running apps in group <g>"]
|
|
2250
|
+
],
|
|
2251
|
+
examples: ["stop web", "stop be-crm fe-crm -y", "stop --all", "stop --group backend"]
|
|
2235
2252
|
},
|
|
2236
2253
|
restart: {
|
|
2237
2254
|
name: "restart",
|
|
2238
|
-
usage: "restart <name|pid>",
|
|
2255
|
+
usage: "restart <name|pid> [name...] [--group <g>]",
|
|
2239
2256
|
summary: "Stop and re-spawn a tracked app from its saved config",
|
|
2240
|
-
|
|
2257
|
+
options: [["--group <g>, -g <g>", "Restart all apps in group <g>"]],
|
|
2258
|
+
examples: ["restart web", "restart be-crm fe-crm -y", "restart --group backend"]
|
|
2241
2259
|
},
|
|
2242
2260
|
kill: {
|
|
2243
2261
|
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.",
|
|
2262
|
+
usage: "kill <pid|name|all> [name...] [--group <g>]",
|
|
2263
|
+
summary: "Kill a process and remove it from the registry",
|
|
2264
|
+
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
2265
|
options: [
|
|
2248
2266
|
["--signal <sig>", "Signal to send (default: SIGTERM). e.g. SIGKILL, SIGINT"],
|
|
2249
2267
|
["--all, -a", "Kill ALL tracked apps (asks for confirmation)"],
|
|
2268
|
+
["--group <g>, -g <g>", "Kill only apps in group <g> (other groups untouched)"],
|
|
2250
2269
|
["-y, --yes", "Skip the confirmation prompt"]
|
|
2251
2270
|
],
|
|
2252
|
-
examples: ["kill web", "kill 12345 --signal SIGKILL", "kill all -y"]
|
|
2271
|
+
examples: ["kill web", "kill 12345 --signal SIGKILL", "kill be-crm fe-crm -y", "kill all -y", "kill --group backend -y"]
|
|
2253
2272
|
},
|
|
2254
2273
|
supervisor: {
|
|
2255
2274
|
name: "supervisor",
|
|
@@ -2317,6 +2336,18 @@ var COMMANDS = {
|
|
|
2317
2336
|
summary: "Rename a tracked app",
|
|
2318
2337
|
examples: ["rename web frontend"]
|
|
2319
2338
|
},
|
|
2339
|
+
group: {
|
|
2340
|
+
name: "group",
|
|
2341
|
+
usage: "group [name] [group]",
|
|
2342
|
+
summary: "Assign a logical group to tracked apps (for scoped bulk ops)",
|
|
2343
|
+
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.",
|
|
2344
|
+
options: [
|
|
2345
|
+
["<name> <group>", "Assign <group> to the tracked app <name>"],
|
|
2346
|
+
["<name> --unset", "Remove the group from <name>"],
|
|
2347
|
+
["(no args)", "List all tracked apps with their group"]
|
|
2348
|
+
],
|
|
2349
|
+
examples: ["group web backend", "group web --unset", "group", "kill --group backend"]
|
|
2350
|
+
},
|
|
2320
2351
|
attach: {
|
|
2321
2352
|
name: "attach",
|
|
2322
2353
|
usage: "attach <port> --name <name>",
|
|
@@ -2431,7 +2462,7 @@ function findCommandDoc(cmd) {
|
|
|
2431
2462
|
}
|
|
2432
2463
|
var GROUPS = [
|
|
2433
2464
|
{ title: "Server", keys: ["start-server"] },
|
|
2434
|
-
{ title: "Apps & Processes", keys: ["start", "run", "ps", "status", "log", "spawn", "stop", "restart", "kill", "supervisor", "persist", "dev", "inspect", "info", "rename"] },
|
|
2465
|
+
{ title: "Apps & Processes", keys: ["start", "run", "ps", "status", "log", "spawn", "stop", "restart", "kill", "group", "supervisor", "persist", "dev", "inspect", "info", "rename"] },
|
|
2435
2466
|
{ title: "Observation", keys: ["attach", "attach-pid", "attach-port", "pipe", "watch"] },
|
|
2436
2467
|
{ title: "Data", keys: ["export", "import", "cleanup"] },
|
|
2437
2468
|
{ title: "Configuration", keys: ["init", "setup", "install-browsers", "sessions"] },
|
|
@@ -2872,12 +2903,32 @@ function getSystemProcesses(filter) {
|
|
|
2872
2903
|
}
|
|
2873
2904
|
return processes;
|
|
2874
2905
|
}
|
|
2875
|
-
function
|
|
2906
|
+
function killTree(pid, signal = "SIGTERM") {
|
|
2907
|
+
if (process.platform === "win32") {
|
|
2908
|
+
try {
|
|
2909
|
+
execSync(`taskkill /pid ${pid} /T /F`, { stdio: "ignore", timeout: 5e3 });
|
|
2910
|
+
return true;
|
|
2911
|
+
} catch {
|
|
2912
|
+
try {
|
|
2913
|
+
process.kill(pid, signal);
|
|
2914
|
+
return true;
|
|
2915
|
+
} catch {
|
|
2916
|
+
return false;
|
|
2917
|
+
}
|
|
2918
|
+
}
|
|
2919
|
+
}
|
|
2876
2920
|
try {
|
|
2877
|
-
process.kill(pid, signal);
|
|
2921
|
+
process.kill(-pid, signal);
|
|
2878
2922
|
return true;
|
|
2879
|
-
} catch {
|
|
2880
|
-
|
|
2923
|
+
} catch (e) {
|
|
2924
|
+
const code = e.code;
|
|
2925
|
+
if (code === "ESRCH") return false;
|
|
2926
|
+
try {
|
|
2927
|
+
process.kill(pid, signal);
|
|
2928
|
+
return true;
|
|
2929
|
+
} catch {
|
|
2930
|
+
return false;
|
|
2931
|
+
}
|
|
2881
2932
|
}
|
|
2882
2933
|
}
|
|
2883
2934
|
function isProcessRunning(pid) {
|
|
@@ -2888,15 +2939,48 @@ function isProcessRunning(pid) {
|
|
|
2888
2939
|
return false;
|
|
2889
2940
|
}
|
|
2890
2941
|
}
|
|
2942
|
+
function getProcessMemRss(pid) {
|
|
2943
|
+
if (process.platform === "win32") {
|
|
2944
|
+
try {
|
|
2945
|
+
const out = execSync(`tasklist /fi "PID eq ${pid}" /fo csv /nh 2>nul`, { encoding: "utf-8", timeout: 3e3 });
|
|
2946
|
+
for (const raw of out.split("\n")) {
|
|
2947
|
+
if (!raw.trim()) continue;
|
|
2948
|
+
const parts = raw.split('","').map((p) => p.replace(/^"|"$/g, ""));
|
|
2949
|
+
if (parseInt(parts[1] ?? "", 10) !== pid) continue;
|
|
2950
|
+
const memKb = parseInt((parts[4] ?? "").replace(/[^\d]/g, ""), 10);
|
|
2951
|
+
return isNaN(memKb) ? null : memKb;
|
|
2952
|
+
}
|
|
2953
|
+
return null;
|
|
2954
|
+
} catch {
|
|
2955
|
+
return null;
|
|
2956
|
+
}
|
|
2957
|
+
}
|
|
2958
|
+
if (process.platform === "darwin" || process.platform === "freebsd" || process.platform === "openbsd" || process.platform === "netbsd") {
|
|
2959
|
+
try {
|
|
2960
|
+
const out = execSync(`ps -o rss= -p ${pid} 2>/dev/null`, { encoding: "utf-8", timeout: 3e3 }).trim();
|
|
2961
|
+
const kb = parseInt(out, 10);
|
|
2962
|
+
return isNaN(kb) ? null : kb;
|
|
2963
|
+
} catch {
|
|
2964
|
+
return null;
|
|
2965
|
+
}
|
|
2966
|
+
}
|
|
2967
|
+
try {
|
|
2968
|
+
const status = readFileSync(`/proc/${pid}/status`, "utf-8");
|
|
2969
|
+
const m = status.match(/VmRSS:\s+(\d+)/);
|
|
2970
|
+
return m ? parseInt(m[1], 10) : null;
|
|
2971
|
+
} catch {
|
|
2972
|
+
return null;
|
|
2973
|
+
}
|
|
2974
|
+
}
|
|
2891
2975
|
function checkPort(port, host = "127.0.0.1", timeout = 1e3) {
|
|
2892
|
-
const tryHost = (h) => new Promise((
|
|
2976
|
+
const tryHost = (h) => new Promise((resolve12) => {
|
|
2893
2977
|
const socket = new net.Socket();
|
|
2894
2978
|
let settled = false;
|
|
2895
2979
|
const finish = (result) => {
|
|
2896
2980
|
if (settled) return;
|
|
2897
2981
|
settled = true;
|
|
2898
2982
|
socket.destroy();
|
|
2899
|
-
|
|
2983
|
+
resolve12(result);
|
|
2900
2984
|
};
|
|
2901
2985
|
socket.setTimeout(timeout);
|
|
2902
2986
|
socket.once("connect", () => finish(true));
|
|
@@ -3089,6 +3173,44 @@ function formatProcessState(state) {
|
|
|
3089
3173
|
}
|
|
3090
3174
|
|
|
3091
3175
|
// src/commands/tracker.ts
|
|
3176
|
+
function extractFlagValue(args2, long, short) {
|
|
3177
|
+
for (let i = 0; i < args2.length; i++) {
|
|
3178
|
+
const a = args2[i];
|
|
3179
|
+
if (a === long || short && a === short) return args2[i + 1];
|
|
3180
|
+
if (a.startsWith(`${long}=`)) return a.slice(long.length + 1);
|
|
3181
|
+
if (short && a.startsWith(`${short}=`)) return a.slice(short.length + 1);
|
|
3182
|
+
}
|
|
3183
|
+
return void 0;
|
|
3184
|
+
}
|
|
3185
|
+
function getGroups() {
|
|
3186
|
+
const tracked = readTracked();
|
|
3187
|
+
const groups = /* @__PURE__ */ new Set();
|
|
3188
|
+
for (const t of tracked) if (t.group) groups.add(t.group);
|
|
3189
|
+
return [...groups].sort();
|
|
3190
|
+
}
|
|
3191
|
+
function resolveTargets(args2) {
|
|
3192
|
+
const groupFlag = extractFlagValue(args2, "--group", "-g");
|
|
3193
|
+
const all = args2.includes("--all") || args2.includes("-a");
|
|
3194
|
+
const positionals = args2.filter((a) => !a.startsWith("-") && !a.includes("="));
|
|
3195
|
+
const groups = getGroups();
|
|
3196
|
+
if (groupFlag) return { kind: "group", group: groupFlag };
|
|
3197
|
+
if (positionals.length === 1 && positionals[0] === "all") return { kind: "all" };
|
|
3198
|
+
if (all) return { kind: "all" };
|
|
3199
|
+
if (positionals.length === 1 && groups.includes(positionals[0])) {
|
|
3200
|
+
return { kind: "group", group: positionals[0] };
|
|
3201
|
+
}
|
|
3202
|
+
if (positionals.length === 1) return { kind: "single", value: positionals[0] };
|
|
3203
|
+
if (positionals.length > 1) return { kind: "names", values: positionals };
|
|
3204
|
+
return { kind: "none" };
|
|
3205
|
+
}
|
|
3206
|
+
function setGroup(name, group) {
|
|
3207
|
+
const tracked = readTracked();
|
|
3208
|
+
const idx = tracked.findIndex((t) => t.name === name);
|
|
3209
|
+
if (idx === -1) return false;
|
|
3210
|
+
tracked[idx] = { ...tracked[idx], group };
|
|
3211
|
+
saveTracked(tracked);
|
|
3212
|
+
return true;
|
|
3213
|
+
}
|
|
3092
3214
|
var RESTART_CAUSE = "fennec:restart";
|
|
3093
3215
|
function getFennecDir() {
|
|
3094
3216
|
return process.env.FENNEC_DATA_DIR ? resolve2(process.env.FENNEC_DATA_DIR) : resolve2(homedir(), ".fennec");
|
|
@@ -3253,7 +3375,7 @@ function respawnTracked(proc, cause) {
|
|
|
3253
3375
|
if (cause) annotateRestart(logFilePath, cause);
|
|
3254
3376
|
if (isProcessRunning(proc.pid)) {
|
|
3255
3377
|
try {
|
|
3256
|
-
|
|
3378
|
+
killTree(proc.pid, "SIGTERM");
|
|
3257
3379
|
} catch {
|
|
3258
3380
|
}
|
|
3259
3381
|
}
|
|
@@ -3442,7 +3564,7 @@ function supervisorStop() {
|
|
|
3442
3564
|
return;
|
|
3443
3565
|
}
|
|
3444
3566
|
try {
|
|
3445
|
-
|
|
3567
|
+
killTree(pid, "SIGTERM");
|
|
3446
3568
|
console.error(`
|
|
3447
3569
|
${pc4.green("\u2713")} ${pc4.bold("Supervisor stopped")} ${pc4.dim(`(PID ${pid})`)}
|
|
3448
3570
|
`);
|
|
@@ -3822,6 +3944,7 @@ async function psCommand(args2) {
|
|
|
3822
3944
|
const systemFlag = args2.includes("--system") || args2.includes("-a") || args2.includes("--all");
|
|
3823
3945
|
const jsonFlag = args2.includes("--json");
|
|
3824
3946
|
const nameFilter = args2.includes("--name") ? args2[args2.indexOf("--name") + 1] : void 0;
|
|
3947
|
+
const groupFilter = extractFlagValue(args2, "--group", "-g");
|
|
3825
3948
|
const sortBy = args2.includes("--sort") ? args2[args2.indexOf("--sort") + 1] : "name";
|
|
3826
3949
|
if (jsonFlag) {
|
|
3827
3950
|
await psJson();
|
|
@@ -3874,10 +3997,11 @@ async function psCommand(args2) {
|
|
|
3874
3997
|
}
|
|
3875
3998
|
return;
|
|
3876
3999
|
}
|
|
3877
|
-
const
|
|
4000
|
+
const trackedAll = readTracked();
|
|
4001
|
+
const tracked = groupFilter ? trackedAll.filter((t) => t.group === groupFilter) : trackedAll;
|
|
3878
4002
|
if (tracked.length === 0) {
|
|
3879
4003
|
console.error(`
|
|
3880
|
-
${pc6.dim("No tracked processes.")}`);
|
|
4004
|
+
${pc6.dim(groupFilter ? `No tracked processes in group "${groupFilter}".` : "No tracked processes.")}`);
|
|
3881
4005
|
console.error(` ${pc6.dim("Start an app with:")} ${pc6.cyan("fennec start <command> --name <name>")}
|
|
3882
4006
|
`);
|
|
3883
4007
|
return;
|
|
@@ -3889,10 +4013,18 @@ async function psCommand(args2) {
|
|
|
3889
4013
|
const s = v;
|
|
3890
4014
|
return s === "running" ? pc6.green("\u25CF running") : pc6.red("\u25CB stopped");
|
|
3891
4015
|
} },
|
|
4016
|
+
{ key: "group", label: "Group", format: (v) => {
|
|
4017
|
+
const g = String(v);
|
|
4018
|
+
return g === "-" ? pc6.dim("-") : pc6.cyan(g);
|
|
4019
|
+
} },
|
|
3892
4020
|
{ key: "port", label: "Port", format: (v) => {
|
|
3893
4021
|
const p = v;
|
|
3894
4022
|
return p ? pc6.yellow(`:${p}`) : pc6.dim("-");
|
|
3895
4023
|
} },
|
|
4024
|
+
{ key: "mem", label: "MEM", align: "right", format: (v) => {
|
|
4025
|
+
const kb = v;
|
|
4026
|
+
return kb && kb > 0 ? pc6.dim(`${(kb / 1024).toFixed(0)}MB`) : pc6.dim("-");
|
|
4027
|
+
} },
|
|
3896
4028
|
{ key: "command", label: "Command", format: (v) => {
|
|
3897
4029
|
const c = String(v);
|
|
3898
4030
|
return c.length > 50 ? c.slice(0, 50) + "\u2026" : c;
|
|
@@ -3902,11 +4034,13 @@ async function psCommand(args2) {
|
|
|
3902
4034
|
const rows = tracked.map((t) => {
|
|
3903
4035
|
const running = isTrackedRunning(t);
|
|
3904
4036
|
const uptime = running ? formatUptime(Math.floor((Date.now() - new Date(t.startedAt).getTime()) / 1e3)) : "-";
|
|
3905
|
-
|
|
4037
|
+
const memKb = running ? getProcessMemRss(t.pid) ?? null : null;
|
|
4038
|
+
return { name: t.name, pid: running ? String(t.pid) : pc6.dim(String(t.pid)), status: running ? "running" : "stopped", group: t.group ?? "-", port: t.port ?? null, mem: memKb, command: t.command, uptime };
|
|
3906
4039
|
});
|
|
3907
4040
|
const runningCount = tracked.filter((t) => isTrackedRunning(t)).length;
|
|
4041
|
+
const scope = groupFilter ? ` in group ${pc6.cyan(groupFilter)}` : "";
|
|
3908
4042
|
console.error(`
|
|
3909
|
-
${symbols.fox} ${pc6.bold("Fennec Apps")} ${pc6.dim(`(${runningCount}/${tracked.length} running)`)}
|
|
4043
|
+
${symbols.fox} ${pc6.bold("Fennec Apps")} ${pc6.dim(`(${runningCount}/${tracked.length} running${scope})`)}
|
|
3910
4044
|
`);
|
|
3911
4045
|
console.error(renderTable(columns, rows));
|
|
3912
4046
|
console.error(` ${pc6.dim("Use")} ${pc6.cyan("fennec start <command> --name <name> --port <port>")} ${pc6.dim("to add more apps.")}`);
|
|
@@ -3914,6 +4048,7 @@ async function psCommand(args2) {
|
|
|
3914
4048
|
console.error(` ${pc6.dim("Use")} ${pc6.cyan("fennec stop <name>")} ${pc6.dim("to pause an app.")}`);
|
|
3915
4049
|
console.error(` ${pc6.dim("Use")} ${pc6.cyan("fennec spawn <name>")} ${pc6.dim("to resume a paused app.")}`);
|
|
3916
4050
|
console.error(` ${pc6.dim("Use")} ${pc6.cyan("fennec kill <name>")} ${pc6.dim("to permanently remove an app.")}`);
|
|
4051
|
+
console.error(` ${pc6.dim("Filter by group with:")} ${pc6.cyan("fennec ps --group <group>")}`);
|
|
3917
4052
|
console.error();
|
|
3918
4053
|
}
|
|
3919
4054
|
async function psJson() {
|
|
@@ -3927,6 +4062,11 @@ async function psJson() {
|
|
|
3927
4062
|
port: t.port ?? null,
|
|
3928
4063
|
command: t.command,
|
|
3929
4064
|
cwd: t.cwd ?? null,
|
|
4065
|
+
group: t.group ?? null,
|
|
4066
|
+
memMB: running ? (() => {
|
|
4067
|
+
const kb = getProcessMemRss(t.pid);
|
|
4068
|
+
return kb ? Math.round(kb / 1024) : null;
|
|
4069
|
+
})() : null,
|
|
3930
4070
|
startedAt: t.startedAt,
|
|
3931
4071
|
uptime: running ? Math.floor((Date.now() - new Date(t.startedAt).getTime()) / 1e3) : null
|
|
3932
4072
|
};
|
|
@@ -3994,11 +4134,11 @@ ${renderTable(columns, rows, { compact: true })}`;
|
|
|
3994
4134
|
process.stdout.write("\x1B[J");
|
|
3995
4135
|
console.error(render());
|
|
3996
4136
|
}, 3e3);
|
|
3997
|
-
await new Promise((
|
|
4137
|
+
await new Promise((resolve12) => {
|
|
3998
4138
|
process.once("SIGINT", () => {
|
|
3999
4139
|
clearInterval(interval);
|
|
4000
4140
|
process.stdout.write("\x1B[J");
|
|
4001
|
-
|
|
4141
|
+
resolve12();
|
|
4002
4142
|
});
|
|
4003
4143
|
});
|
|
4004
4144
|
}
|
|
@@ -4075,6 +4215,7 @@ async function runCommand(args2) {
|
|
|
4075
4215
|
const cwd = cwdIndex !== -1 ? resolve4(args2[cwdIndex + 1]) : process.cwd();
|
|
4076
4216
|
const restartFlag = args2.includes("--restart");
|
|
4077
4217
|
const jsonlFlag = args2.includes("--jsonl");
|
|
4218
|
+
const group = extractFlagValue(args2, "--group", "-g");
|
|
4078
4219
|
const stopFlags = [nameIndex, portIndex, cwdIndex].filter((i) => i !== -1);
|
|
4079
4220
|
const cmdEnd = Math.min(...stopFlags, Infinity);
|
|
4080
4221
|
const cmdParts = args2.slice(0, cmdEnd);
|
|
@@ -4114,6 +4255,7 @@ async function runCommand(args2) {
|
|
|
4114
4255
|
if (port) console.error(` ${renderKV("Port", String(port))}`);
|
|
4115
4256
|
if (cwd) console.error(` ${renderKV("Directory", cwd)}`);
|
|
4116
4257
|
if (restartFlag) console.error(` ${renderKV("Auto-restart", pc7.green("enabled"))}`);
|
|
4258
|
+
if (group) console.error(` ${renderKV("Group", group)}`);
|
|
4117
4259
|
console.error(` ${divider()}`);
|
|
4118
4260
|
try {
|
|
4119
4261
|
const startEnv = {};
|
|
@@ -4132,7 +4274,8 @@ async function runCommand(args2) {
|
|
|
4132
4274
|
env: startEnv,
|
|
4133
4275
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4134
4276
|
autoRestart: restartFlag,
|
|
4135
|
-
logMode: jsonlFlag ? "jsonl" : "text"
|
|
4277
|
+
logMode: jsonlFlag ? "jsonl" : "text",
|
|
4278
|
+
group
|
|
4136
4279
|
});
|
|
4137
4280
|
console.error(` ${pc7.green("\u2713")} ${pc7.bold(appName)} ${pc7.dim(`started (PID: ${pid})`)}`);
|
|
4138
4281
|
const spinner = createSpinner(port ? `Waiting for ${appName} on port ${port}...` : `Confirming ${appName} is running...`);
|
|
@@ -4213,25 +4356,44 @@ async function resurrectTracked() {
|
|
|
4213
4356
|
// src/commands/kill.ts
|
|
4214
4357
|
import pc8 from "picocolors";
|
|
4215
4358
|
async function killCommand(args2) {
|
|
4216
|
-
const rawTarget = args2[0];
|
|
4217
4359
|
const signalIndex = args2.indexOf("--signal");
|
|
4218
4360
|
const signalRaw = signalIndex !== -1 ? args2[signalIndex + 1] : "SIGTERM";
|
|
4219
4361
|
const signal = signalRaw ?? "SIGTERM";
|
|
4220
4362
|
const force = args2.includes("-y") || args2.includes("--yes");
|
|
4221
|
-
|
|
4222
|
-
|
|
4363
|
+
const target = resolveTargets(args2);
|
|
4364
|
+
if (target.kind === "single") {
|
|
4365
|
+
await killOne(target.value, signal, force, false);
|
|
4223
4366
|
return;
|
|
4224
4367
|
}
|
|
4225
|
-
if (
|
|
4226
|
-
|
|
4227
|
-
|
|
4368
|
+
if (target.kind === "names") {
|
|
4369
|
+
for (const name of target.values) {
|
|
4370
|
+
await killOne(name, signal, force, true);
|
|
4371
|
+
}
|
|
4372
|
+
return;
|
|
4373
|
+
}
|
|
4374
|
+
if (target.kind === "group") {
|
|
4375
|
+
await killGroup(target.group, signal, force);
|
|
4376
|
+
return;
|
|
4377
|
+
}
|
|
4378
|
+
if (target.kind === "all") {
|
|
4379
|
+
await killAll(signal, force);
|
|
4380
|
+
return;
|
|
4228
4381
|
}
|
|
4382
|
+
console.error(renderError("Missing target", "Usage: fennec kill <pid|name|all> [--signal SIGTERM|SIGKILL|SIGINT] [--group <group>] [-y]"));
|
|
4383
|
+
process.exit(1);
|
|
4384
|
+
}
|
|
4385
|
+
async function killOne(rawTarget, signal, force, multi) {
|
|
4229
4386
|
let targetPid;
|
|
4230
4387
|
let displayName;
|
|
4231
4388
|
const pid = parseInt(rawTarget, 10);
|
|
4232
4389
|
if (!isNaN(pid) && String(pid) === rawTarget) {
|
|
4233
4390
|
if (!isProcessRunning(pid)) {
|
|
4234
|
-
|
|
4391
|
+
const msg = `No process with PID ${pid} is running`;
|
|
4392
|
+
if (multi) {
|
|
4393
|
+
console.error(renderError("Process not found", msg));
|
|
4394
|
+
return;
|
|
4395
|
+
}
|
|
4396
|
+
console.error(renderError("Process not found", msg));
|
|
4235
4397
|
process.exit(1);
|
|
4236
4398
|
}
|
|
4237
4399
|
targetPid = pid;
|
|
@@ -4250,8 +4412,13 @@ async function killCommand(args2) {
|
|
|
4250
4412
|
targetPid = trackedMatch.pid;
|
|
4251
4413
|
displayName = `${trackedMatch.name} (PID ${targetPid})`;
|
|
4252
4414
|
} else {
|
|
4253
|
-
|
|
4254
|
-
Use ${pc8.cyan(
|
|
4415
|
+
const msg = `No tracked process named "${rawTarget}".
|
|
4416
|
+
Use ${pc8.cyan("fennec kill <pid>")} to kill a system process by its PID.`;
|
|
4417
|
+
if (multi) {
|
|
4418
|
+
console.error(renderError("Not tracked", msg));
|
|
4419
|
+
return;
|
|
4420
|
+
}
|
|
4421
|
+
console.error(renderError("Not tracked", msg));
|
|
4255
4422
|
process.exit(1);
|
|
4256
4423
|
}
|
|
4257
4424
|
}
|
|
@@ -4261,7 +4428,7 @@ Use ${pc8.cyan(`fennec kill <pid>`)} to kill a system process by its PID.`));
|
|
|
4261
4428
|
return;
|
|
4262
4429
|
}
|
|
4263
4430
|
const spinner = createSpinner(`Sending ${signal} to ${displayName}...`);
|
|
4264
|
-
const success =
|
|
4431
|
+
const success = killTree(targetPid, signal);
|
|
4265
4432
|
if (success) {
|
|
4266
4433
|
await new Promise((r) => setTimeout(r, 200));
|
|
4267
4434
|
const stillRunning = isProcessRunning(targetPid);
|
|
@@ -4270,12 +4437,12 @@ Use ${pc8.cyan(`fennec kill <pid>`)} to kill a system process by its PID.`));
|
|
|
4270
4437
|
const forceKill = force || await confirmPrompt(`Send ${pc8.red("SIGKILL")} to force stop?`, true);
|
|
4271
4438
|
if (forceKill) {
|
|
4272
4439
|
const forceSpinner = createSpinner(`Sending SIGKILL to ${displayName}...`);
|
|
4273
|
-
|
|
4440
|
+
killTree(targetPid, "SIGKILL");
|
|
4274
4441
|
await new Promise((r) => setTimeout(r, 300));
|
|
4275
4442
|
isProcessRunning(targetPid) ? forceSpinner.fail(`${displayName} could not be stopped (permission denied?)`) : forceSpinner.succeed(`${displayName} force stopped`);
|
|
4276
4443
|
} else {
|
|
4277
4444
|
console.error(` ${pc8.dim("Retrying with SIGTERM...")}`);
|
|
4278
|
-
|
|
4445
|
+
killTree(targetPid, "SIGTERM");
|
|
4279
4446
|
console.error(` ${pc8.yellow("\u26A0")} ${displayName} ${pc8.dim("may still be running")}`);
|
|
4280
4447
|
}
|
|
4281
4448
|
} else {
|
|
@@ -4287,6 +4454,43 @@ Use ${pc8.cyan(`fennec kill <pid>`)} to kill a system process by its PID.`));
|
|
|
4287
4454
|
console.error(renderError("Permission denied", "Try running with sudo or use a different signal."));
|
|
4288
4455
|
}
|
|
4289
4456
|
}
|
|
4457
|
+
async function killGroup(group, signal, force) {
|
|
4458
|
+
const tracked = readTracked();
|
|
4459
|
+
const inGroup = tracked.filter((t) => t.group === group);
|
|
4460
|
+
if (inGroup.length === 0) {
|
|
4461
|
+
console.error(renderError("Empty group", `No tracked entries in group "${group}".
|
|
4462
|
+
Known groups: ${pc8.cyan(getGroups().join(", ") || "(none)")}`));
|
|
4463
|
+
process.exit(1);
|
|
4464
|
+
}
|
|
4465
|
+
const running = inGroup.filter((t) => isTrackedRunning(t));
|
|
4466
|
+
const stopped = inGroup.filter((t) => !isTrackedRunning(t));
|
|
4467
|
+
console.error(`
|
|
4468
|
+
${pc8.bold(`Kill group ${pc8.cyan(group)}: ${running.length} running + remove ${stopped.length} stopped?`)}`);
|
|
4469
|
+
console.error(` ${pc8.dim("Permanently removes only apps in this group \u2014 other groups are untouched.")}
|
|
4470
|
+
`);
|
|
4471
|
+
const confirmed = force || await confirmPrompt(`${pc8.red("Are you sure?")} ${pc8.dim("This cannot be undone")}`, false);
|
|
4472
|
+
if (!confirmed) {
|
|
4473
|
+
console.error(` ${pc8.dim("Cancelled.")}`);
|
|
4474
|
+
return;
|
|
4475
|
+
}
|
|
4476
|
+
const spinner = createSpinner(`Killing ${running.length} + removing ${stopped.length} tracked app(s) in ${group}...`);
|
|
4477
|
+
let killed = 0, failed = 0;
|
|
4478
|
+
for (const t of running) {
|
|
4479
|
+
if (killTree(t.pid, signal)) {
|
|
4480
|
+
killed++;
|
|
4481
|
+
} else {
|
|
4482
|
+
failed++;
|
|
4483
|
+
}
|
|
4484
|
+
removeTrackedByPid(t.pid);
|
|
4485
|
+
}
|
|
4486
|
+
for (const t of stopped) {
|
|
4487
|
+
removeTrackedByPid(t.pid);
|
|
4488
|
+
}
|
|
4489
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
4490
|
+
const total = running.length + stopped.length;
|
|
4491
|
+
total > 0 ? spinner.succeed(`${killed} killed, ${stopped.length} stopped entries removed from group ${group}`) : spinner.fail("Failed to kill apps");
|
|
4492
|
+
if (failed > 0) console.error(` ${pc8.yellow(`${failed} running app(s) could not be killed`)}`);
|
|
4493
|
+
}
|
|
4290
4494
|
async function killAll(signal, force) {
|
|
4291
4495
|
const tracked = readTracked();
|
|
4292
4496
|
if (tracked.length === 0) {
|
|
@@ -4307,7 +4511,7 @@ async function killAll(signal, force) {
|
|
|
4307
4511
|
const spinner = createSpinner(`Killing ${running.length} + removing ${stopped.length} tracked app(s)...`);
|
|
4308
4512
|
let killed = 0, failed = 0;
|
|
4309
4513
|
for (const t of running) {
|
|
4310
|
-
if (
|
|
4514
|
+
if (killTree(t.pid, signal)) {
|
|
4311
4515
|
killed++;
|
|
4312
4516
|
} else {
|
|
4313
4517
|
failed++;
|
|
@@ -4319,34 +4523,53 @@ async function killAll(signal, force) {
|
|
|
4319
4523
|
}
|
|
4320
4524
|
await new Promise((r) => setTimeout(r, 300));
|
|
4321
4525
|
const total = running.length + stopped.length;
|
|
4322
|
-
total > 0 ? spinner.succeed(`${killed} killed, ${stopped.length} stopped
|
|
4526
|
+
total > 0 ? spinner.succeed(`${killed} killed, ${stopped.length} stopped entries removed`) : spinner.fail("Failed to kill apps");
|
|
4323
4527
|
if (failed > 0) console.error(` ${pc8.yellow(`${failed} running app(s) could not be killed`)}`);
|
|
4324
4528
|
}
|
|
4325
4529
|
|
|
4326
4530
|
// src/commands/stop.ts
|
|
4327
4531
|
import pc9 from "picocolors";
|
|
4328
4532
|
async function stopCommand(args2) {
|
|
4329
|
-
const
|
|
4330
|
-
if (
|
|
4533
|
+
const target = resolveTargets(args2);
|
|
4534
|
+
if (target.kind === "group") {
|
|
4535
|
+
await stopAllTracked(args2, target.group);
|
|
4536
|
+
return;
|
|
4537
|
+
}
|
|
4538
|
+
if (target.kind === "all") {
|
|
4331
4539
|
await stopAllTracked(args2);
|
|
4332
4540
|
return;
|
|
4333
4541
|
}
|
|
4334
|
-
|
|
4335
|
-
|
|
4336
|
-
|
|
4337
|
-
process.exit(1);
|
|
4542
|
+
if (target.kind === "single") {
|
|
4543
|
+
await stopOne(target.value, args2);
|
|
4544
|
+
return;
|
|
4338
4545
|
}
|
|
4546
|
+
if (target.kind === "names") {
|
|
4547
|
+
for (const name of target.values) {
|
|
4548
|
+
await stopOne(name, args2, true);
|
|
4549
|
+
}
|
|
4550
|
+
return;
|
|
4551
|
+
}
|
|
4552
|
+
console.error(renderError("Missing name", "Usage: fennec stop <name|--all> [--group <group>] [-y]"));
|
|
4553
|
+
process.exit(1);
|
|
4554
|
+
}
|
|
4555
|
+
async function stopOne(rawTarget, args2, multi = false) {
|
|
4339
4556
|
const tracked = readTracked();
|
|
4340
4557
|
const trackedMatch = tracked.find((t) => t.name === rawTarget);
|
|
4341
4558
|
if (!trackedMatch) {
|
|
4342
|
-
|
|
4559
|
+
const msg = `No tracked process named "${rawTarget}". Use ${pc9.cyan("fennec kill <pid>")} to kill a system process.`;
|
|
4560
|
+
if (multi) {
|
|
4561
|
+
console.error(renderError("Not found", msg));
|
|
4562
|
+
return;
|
|
4563
|
+
}
|
|
4564
|
+
console.error(renderError("Not found", msg));
|
|
4343
4565
|
process.exit(1);
|
|
4344
4566
|
}
|
|
4345
4567
|
if (!isTrackedRunning(trackedMatch)) {
|
|
4346
4568
|
console.error(`
|
|
4347
4569
|
${pc9.yellow("\u26A0")} ${pc9.bold(rawTarget)} ${pc9.dim("is already stopped")}`);
|
|
4348
4570
|
console.error();
|
|
4349
|
-
process.exit(0);
|
|
4571
|
+
if (!multi) process.exit(0);
|
|
4572
|
+
return;
|
|
4350
4573
|
}
|
|
4351
4574
|
const displayName = `${trackedMatch.name} (PID ${trackedMatch.pid})`;
|
|
4352
4575
|
const force = args2.includes("-y") || args2.includes("--yes");
|
|
@@ -4358,17 +4581,18 @@ async function stopCommand(args2) {
|
|
|
4358
4581
|
setAutoRestart(trackedMatch.name, false);
|
|
4359
4582
|
await stopSingleProcess(trackedMatch.pid, trackedMatch.name);
|
|
4360
4583
|
}
|
|
4361
|
-
async function stopAllTracked(args2) {
|
|
4584
|
+
async function stopAllTracked(args2, group) {
|
|
4362
4585
|
const tracked = readTracked();
|
|
4363
|
-
const running = tracked.filter((t) => isTrackedRunning(t));
|
|
4586
|
+
const running = tracked.filter((t) => isTrackedRunning(t) && (!group || t.group === group));
|
|
4364
4587
|
if (running.length === 0) {
|
|
4365
4588
|
console.error(`
|
|
4366
|
-
${pc9.dim("No running tracked processes to stop.")}
|
|
4589
|
+
${pc9.dim(group ? `No running tracked processes in group "${group}".` : "No running tracked processes to stop.")}
|
|
4367
4590
|
`);
|
|
4368
4591
|
return;
|
|
4369
4592
|
}
|
|
4593
|
+
const scope = group ? ` group ${pc9.cyan(group)}` : "";
|
|
4370
4594
|
console.error(`
|
|
4371
|
-
${pc9.yellow("\u26A0")} ${pc9.bold(`Stop all ${running.length} running process(es)?`)} ${pc9.dim("(They can be re-spawned later)")}
|
|
4595
|
+
${pc9.yellow("\u26A0")} ${pc9.bold(`Stop all ${running.length} running process(es)${scope}?`)} ${pc9.dim("(They can be re-spawned later)")}
|
|
4372
4596
|
`);
|
|
4373
4597
|
for (const t of running) {
|
|
4374
4598
|
console.error(` ${pc9.green("\u25CF")} ${pc9.bold(t.name)} ${pc9.dim(`(PID ${t.pid})`)}`);
|
|
@@ -4386,7 +4610,7 @@ async function stopAllTracked(args2) {
|
|
|
4386
4610
|
let failed = 0;
|
|
4387
4611
|
for (const t of running) {
|
|
4388
4612
|
setAutoRestart(t.name, false);
|
|
4389
|
-
if (
|
|
4613
|
+
if (killTree(t.pid, "SIGTERM")) {
|
|
4390
4614
|
stopped++;
|
|
4391
4615
|
} else {
|
|
4392
4616
|
failed++;
|
|
@@ -4407,7 +4631,7 @@ async function stopAllTracked(args2) {
|
|
|
4407
4631
|
async function stopSingleProcess(pid, name) {
|
|
4408
4632
|
const displayName = `${name} (PID ${pid})`;
|
|
4409
4633
|
const spinner = createSpinner(`Stopping ${displayName}...`);
|
|
4410
|
-
const success =
|
|
4634
|
+
const success = killTree(pid, "SIGTERM");
|
|
4411
4635
|
if (success) {
|
|
4412
4636
|
await new Promise((r) => setTimeout(r, 200));
|
|
4413
4637
|
const stillRunning = isProcessRunning(pid);
|
|
@@ -4416,7 +4640,7 @@ async function stopSingleProcess(pid, name) {
|
|
|
4416
4640
|
const forceKill = await confirmPrompt(`Send ${pc9.red("SIGKILL")} to force stop?`, true);
|
|
4417
4641
|
if (forceKill) {
|
|
4418
4642
|
const forceSpinner = createSpinner(`Sending SIGKILL to ${displayName}...`);
|
|
4419
|
-
|
|
4643
|
+
killTree(pid, "SIGKILL");
|
|
4420
4644
|
await new Promise((r) => setTimeout(r, 300));
|
|
4421
4645
|
isProcessRunning(pid) ? forceSpinner.fail(`${displayName} could not be stopped (permission denied?)`) : forceSpinner.succeed(`${displayName} force stopped`);
|
|
4422
4646
|
} else {
|
|
@@ -4439,20 +4663,45 @@ import { mkdirSync as mkdirSync5 } from "fs";
|
|
|
4439
4663
|
import { resolve as resolve5 } from "path";
|
|
4440
4664
|
import { homedir as homedir3 } from "os";
|
|
4441
4665
|
async function spawnCommand(args2) {
|
|
4442
|
-
const
|
|
4443
|
-
|
|
4444
|
-
|
|
4445
|
-
await spawnAllStopped();
|
|
4666
|
+
const target = resolveTargets(args2);
|
|
4667
|
+
if (target.kind === "single") {
|
|
4668
|
+
await spawnOne(target.value, false);
|
|
4446
4669
|
return;
|
|
4447
4670
|
}
|
|
4448
|
-
if (
|
|
4449
|
-
|
|
4671
|
+
if (target.kind === "names") {
|
|
4672
|
+
for (const name of target.values) {
|
|
4673
|
+
await spawnOne(name, true);
|
|
4674
|
+
}
|
|
4675
|
+
return;
|
|
4676
|
+
}
|
|
4677
|
+
if (target.kind === "group") {
|
|
4678
|
+
const group = target.group;
|
|
4679
|
+
const tracked = readTracked();
|
|
4680
|
+
const inGroup = tracked.filter((t) => t.group === group && !isTrackedRunning(t) && t.command);
|
|
4681
|
+
if (inGroup.length === 0) {
|
|
4682
|
+
console.error(renderError("Empty group", `No stopped entries with a saved command in group "${group}".
|
|
4683
|
+
Known groups: ${pc10.cyan(getGroups().join(", ") || "(none)")}`));
|
|
4684
|
+
process.exit(1);
|
|
4685
|
+
}
|
|
4686
|
+
await spawnAllStopped(inGroup);
|
|
4687
|
+
return;
|
|
4688
|
+
}
|
|
4689
|
+
if (target.kind === "all") {
|
|
4690
|
+
await spawnAllStopped(readTracked().filter((t) => !isTrackedRunning(t) && t.command));
|
|
4450
4691
|
return;
|
|
4451
4692
|
}
|
|
4693
|
+
await spawnList();
|
|
4694
|
+
}
|
|
4695
|
+
async function spawnOne(name, multi) {
|
|
4452
4696
|
const tracked = readTracked();
|
|
4453
4697
|
const match = tracked.find((t) => t.name === name);
|
|
4454
4698
|
if (!match) {
|
|
4455
|
-
|
|
4699
|
+
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.`;
|
|
4700
|
+
if (multi) {
|
|
4701
|
+
console.error(renderError("Not found", msg));
|
|
4702
|
+
return;
|
|
4703
|
+
}
|
|
4704
|
+
console.error(renderError("Not found", msg));
|
|
4456
4705
|
process.exit(1);
|
|
4457
4706
|
}
|
|
4458
4707
|
if (isTrackedRunning(match)) {
|
|
@@ -4460,17 +4709,22 @@ async function spawnCommand(args2) {
|
|
|
4460
4709
|
${pc10.yellow("\u26A0")} ${pc10.bold(name)} ${pc10.dim("is already running (PID:")} ${match.pid}${pc10.dim(")")}`);
|
|
4461
4710
|
console.error(` ${pc10.dim("Use")} ${pc10.cyan(`fennec stop ${name}`)} ${pc10.dim("to stop it first.")}`);
|
|
4462
4711
|
console.error();
|
|
4463
|
-
process.exit(0);
|
|
4712
|
+
if (!multi) process.exit(0);
|
|
4713
|
+
return;
|
|
4464
4714
|
}
|
|
4465
4715
|
if (!match.command) {
|
|
4466
|
-
|
|
4716
|
+
const msg = `"${name}" has no saved command and cannot be re-spawned.`;
|
|
4717
|
+
if (multi) {
|
|
4718
|
+
console.error(renderError("No command saved", msg));
|
|
4719
|
+
return;
|
|
4720
|
+
}
|
|
4721
|
+
console.error(renderError("No command saved", msg));
|
|
4467
4722
|
process.exit(1);
|
|
4468
4723
|
}
|
|
4469
4724
|
await respawnProcess(match);
|
|
4470
4725
|
}
|
|
4471
|
-
async function spawnAllStopped() {
|
|
4472
|
-
const
|
|
4473
|
-
const toSpawn = tracked.filter((t) => !isTrackedRunning(t) && t.command);
|
|
4726
|
+
async function spawnAllStopped(procs) {
|
|
4727
|
+
const toSpawn = procs;
|
|
4474
4728
|
if (toSpawn.length === 0) {
|
|
4475
4729
|
console.error(`
|
|
4476
4730
|
${pc10.dim("No stopped processes with saved commands to spawn.")}
|
|
@@ -4607,32 +4861,46 @@ async function respawnProcess(proc) {
|
|
|
4607
4861
|
|
|
4608
4862
|
// src/commands/restart.ts
|
|
4609
4863
|
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
4864
|
async function restartCommand(args2) {
|
|
4614
|
-
const
|
|
4615
|
-
|
|
4616
|
-
|
|
4617
|
-
|
|
4865
|
+
const force = args2.includes("-y") || args2.includes("--yes");
|
|
4866
|
+
const target = resolveTargets(args2);
|
|
4867
|
+
if (target.kind === "group") {
|
|
4868
|
+
await restartGroup(target.group, force);
|
|
4869
|
+
return;
|
|
4870
|
+
}
|
|
4871
|
+
if (target.kind === "names") {
|
|
4872
|
+
for (const n of target.values) await restartOne(n, force, true);
|
|
4873
|
+
return;
|
|
4874
|
+
}
|
|
4875
|
+
if (target.kind === "single") {
|
|
4876
|
+
await restartOne(target.value, force, false);
|
|
4877
|
+
return;
|
|
4618
4878
|
}
|
|
4879
|
+
console.error(renderError("Missing process name/pid", "Usage: fennec restart <name|pid> [-y] [--group <group>]"));
|
|
4880
|
+
process.exit(1);
|
|
4881
|
+
}
|
|
4882
|
+
async function restartOne(raw, force, multi) {
|
|
4619
4883
|
const tracked = readTracked();
|
|
4620
4884
|
const pidNum = parseInt(raw, 10);
|
|
4621
4885
|
const trackedEntry = tracked.find((t) => t.name === raw) ?? (!isNaN(pidNum) && String(pidNum) === raw ? tracked.find((t) => t.pid === pidNum) : void 0);
|
|
4622
4886
|
if (!trackedEntry) {
|
|
4623
|
-
|
|
4624
|
-
fennec restart only re-spawns Fennec-tracked apps (from their saved config)
|
|
4887
|
+
const msg = `No tracked process named or with PID "${raw}".
|
|
4888
|
+
fennec restart only re-spawns Fennec-tracked apps (from their saved config).`;
|
|
4889
|
+
if (multi) {
|
|
4890
|
+
console.error(renderError("Not tracked", msg));
|
|
4891
|
+
return;
|
|
4892
|
+
}
|
|
4893
|
+
console.error(renderError("Not tracked", msg));
|
|
4625
4894
|
process.exit(1);
|
|
4626
4895
|
}
|
|
4627
4896
|
const targetPid = trackedEntry.pid;
|
|
4628
|
-
const force = args2.includes("-y") || args2.includes("--yes");
|
|
4629
4897
|
const confirmed = force || await confirmPrompt(`Restart ${pc11.bold(`${trackedEntry.name} (PID ${targetPid})`)}?`, false);
|
|
4630
4898
|
if (!confirmed) {
|
|
4631
4899
|
console.error(` ${pc11.dim("Cancelled")}`);
|
|
4632
4900
|
return;
|
|
4633
4901
|
}
|
|
4634
4902
|
const spinner = createSpinner(`Stopping ${trackedEntry.name} (PID ${targetPid})...`);
|
|
4635
|
-
const killed =
|
|
4903
|
+
const killed = killTree(targetPid, "SIGTERM");
|
|
4636
4904
|
if (!killed) {
|
|
4637
4905
|
spinner.fail(`Failed to stop PID ${targetPid}`);
|
|
4638
4906
|
return;
|
|
@@ -4640,16 +4908,14 @@ fennec restart only re-spawns Fennec-tracked apps (from their saved config).`));
|
|
|
4640
4908
|
await new Promise((r) => setTimeout(r, 1e3));
|
|
4641
4909
|
if (isProcessRunning(targetPid)) {
|
|
4642
4910
|
spinner.warn("Process didn't stop, sending SIGKILL...");
|
|
4643
|
-
|
|
4911
|
+
killTree(targetPid, "SIGKILL");
|
|
4644
4912
|
await new Promise((r) => setTimeout(r, 500));
|
|
4645
4913
|
}
|
|
4646
4914
|
spinner.succeed(`${trackedEntry.name} stopped`);
|
|
4647
4915
|
const respawnSpinner = createSpinner(`Re-spawning ${trackedEntry.name}...`);
|
|
4648
4916
|
try {
|
|
4649
4917
|
const cmdParts = resolveArgs(trackedEntry);
|
|
4650
|
-
const
|
|
4651
|
-
mkdirSync6(logDir, { recursive: true });
|
|
4652
|
-
const logFilePath = resolve6(logDir, `${trackedEntry.name}.log`);
|
|
4918
|
+
const logFilePath = logFilePathFor(trackedEntry.name);
|
|
4653
4919
|
const child = spawnDaemon({ cmdParts, name: trackedEntry.name, cwd: trackedEntry.cwd, logFilePath, env: buildSpawnEnv(trackedEntry.env), logMode: trackedEntry.logMode });
|
|
4654
4920
|
removeTrackedByPid(targetPid);
|
|
4655
4921
|
addTracked({
|
|
@@ -4670,6 +4936,45 @@ fennec restart only re-spawns Fennec-tracked apps (from their saved config).`));
|
|
|
4670
4936
|
console.error(` ${pc11.dim("Config preserved. Re-run manually:")} ${renderCommand(trackedEntry.command)}`);
|
|
4671
4937
|
}
|
|
4672
4938
|
}
|
|
4939
|
+
async function restartGroup(group, force) {
|
|
4940
|
+
const tracked = readTracked();
|
|
4941
|
+
const inGroup = tracked.filter((t) => t.group === group);
|
|
4942
|
+
if (inGroup.length === 0) {
|
|
4943
|
+
console.error(renderError("Empty group", `No tracked entries in group "${group}".
|
|
4944
|
+
Known groups: ${pc11.cyan(getGroups().join(", ") || "(none)")}`));
|
|
4945
|
+
process.exit(1);
|
|
4946
|
+
}
|
|
4947
|
+
const confirmed = force || await confirmPrompt(`Restart ${pc11.bold(`${inGroup.length}`)} process(es) in group ${pc11.cyan(group)}?`, false);
|
|
4948
|
+
if (!confirmed) {
|
|
4949
|
+
console.error(` ${pc11.dim("Cancelled")}`);
|
|
4950
|
+
return;
|
|
4951
|
+
}
|
|
4952
|
+
for (const trackedEntry of inGroup) {
|
|
4953
|
+
const spinner = createSpinner(`Restarting ${trackedEntry.name}...`);
|
|
4954
|
+
try {
|
|
4955
|
+
const cmdParts = resolveArgs(trackedEntry);
|
|
4956
|
+
const logFilePath = logFilePathFor(trackedEntry.name);
|
|
4957
|
+
const child = spawnDaemon({ cmdParts, name: trackedEntry.name, cwd: trackedEntry.cwd, logFilePath, env: buildSpawnEnv(trackedEntry.env), logMode: trackedEntry.logMode });
|
|
4958
|
+
removeTrackedByPid(trackedEntry.pid);
|
|
4959
|
+
addTracked({
|
|
4960
|
+
name: trackedEntry.name,
|
|
4961
|
+
pid: child.pid ?? 0,
|
|
4962
|
+
command: trackedEntry.command,
|
|
4963
|
+
args: cmdParts,
|
|
4964
|
+
port: trackedEntry.port,
|
|
4965
|
+
cwd: trackedEntry.cwd,
|
|
4966
|
+
env: trackedEntry.env,
|
|
4967
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4968
|
+
autoRestart: trackedEntry.autoRestart,
|
|
4969
|
+
logMode: trackedEntry.logMode
|
|
4970
|
+
});
|
|
4971
|
+
spinner.succeed(`${trackedEntry.name} restarted (PID: ${child.pid})`);
|
|
4972
|
+
} catch (error) {
|
|
4973
|
+
spinner.fail(`Failed to re-spawn ${trackedEntry.name}: ${String(error)}`);
|
|
4974
|
+
console.error(` ${pc11.dim("Config preserved. Re-run manually:")} ${renderCommand(trackedEntry.command)}`);
|
|
4975
|
+
}
|
|
4976
|
+
}
|
|
4977
|
+
}
|
|
4673
4978
|
|
|
4674
4979
|
// src/commands/adopt.ts
|
|
4675
4980
|
import pc12 from "picocolors";
|
|
@@ -4904,7 +5209,7 @@ async function inspectCommand(args2) {
|
|
|
4904
5209
|
// src/commands/dev.ts
|
|
4905
5210
|
import pc14 from "picocolors";
|
|
4906
5211
|
import { existsSync as existsSync8, readFileSync as readFileSync7 } from "fs";
|
|
4907
|
-
import { resolve as
|
|
5212
|
+
import { resolve as resolve6, dirname as dirname5 } from "path";
|
|
4908
5213
|
import "os";
|
|
4909
5214
|
import yaml from "js-yaml";
|
|
4910
5215
|
function loadConfig(path) {
|
|
@@ -4923,7 +5228,7 @@ function loadConfig(path) {
|
|
|
4923
5228
|
}
|
|
4924
5229
|
function resolveCwd(baseDir, cwd) {
|
|
4925
5230
|
if (!cwd) return baseDir;
|
|
4926
|
-
return
|
|
5231
|
+
return resolve6(baseDir, cwd);
|
|
4927
5232
|
}
|
|
4928
5233
|
async function waitForPort(port, timeoutMs = 3e4) {
|
|
4929
5234
|
const deadline = Date.now() + timeoutMs;
|
|
@@ -4972,7 +5277,7 @@ async function startApp(app, baseDir) {
|
|
|
4972
5277
|
const wasRunning = !!(existing && isTrackedRunning(existing));
|
|
4973
5278
|
if (wasRunning) {
|
|
4974
5279
|
try {
|
|
4975
|
-
|
|
5280
|
+
killTree(existing.pid, "SIGTERM");
|
|
4976
5281
|
} catch {
|
|
4977
5282
|
}
|
|
4978
5283
|
}
|
|
@@ -5014,7 +5319,7 @@ async function startApp(app, baseDir) {
|
|
|
5014
5319
|
async function devCommand(args2) {
|
|
5015
5320
|
const sub = args2[0] ?? "up";
|
|
5016
5321
|
const configIndex = args2.indexOf("--config");
|
|
5017
|
-
const configPath = configIndex !== -1 ?
|
|
5322
|
+
const configPath = configIndex !== -1 ? resolve6(args2[configIndex + 1]) : resolve6(process.cwd(), "fennec.config.yaml");
|
|
5018
5323
|
if (sub === "down") {
|
|
5019
5324
|
return devDown();
|
|
5020
5325
|
}
|
|
@@ -5086,7 +5391,7 @@ function devDown() {
|
|
|
5086
5391
|
${symbols.fox} ${pc14.bold("fennec dev down")} ${pc14.dim(`\u2014 stopping ${tracked.length} app(s)`)}`);
|
|
5087
5392
|
for (const t of tracked) {
|
|
5088
5393
|
try {
|
|
5089
|
-
if (isTrackedRunning(t))
|
|
5394
|
+
if (isTrackedRunning(t)) killTree(t.pid, "SIGTERM");
|
|
5090
5395
|
} catch {
|
|
5091
5396
|
}
|
|
5092
5397
|
t.autoRestart = false;
|
|
@@ -5384,10 +5689,10 @@ async function followLog(logFilePath, opts) {
|
|
|
5384
5689
|
closeSync3(fd);
|
|
5385
5690
|
}
|
|
5386
5691
|
}, 400);
|
|
5387
|
-
await new Promise((
|
|
5692
|
+
await new Promise((resolve12) => {
|
|
5388
5693
|
const stop = () => {
|
|
5389
5694
|
clearInterval(timer);
|
|
5390
|
-
|
|
5695
|
+
resolve12();
|
|
5391
5696
|
};
|
|
5392
5697
|
process.once("SIGINT", stop);
|
|
5393
5698
|
process.once("SIGTERM", stop);
|
|
@@ -5451,37 +5756,237 @@ async function attachCommand(args2) {
|
|
|
5451
5756
|
}
|
|
5452
5757
|
}
|
|
5453
5758
|
|
|
5454
|
-
// src/commands/
|
|
5455
|
-
import {
|
|
5759
|
+
// src/commands/store.ts
|
|
5760
|
+
import { StoreManager, redactSession } from "@plumpslabs/fennec-core";
|
|
5761
|
+
import "@plumpslabs/fennec-core";
|
|
5456
5762
|
import pc17 from "picocolors";
|
|
5457
|
-
|
|
5458
|
-
|
|
5459
|
-
|
|
5460
|
-
if (
|
|
5763
|
+
var KINDS = ["session", "process", "workflow", "plugin", "config", "export"];
|
|
5764
|
+
function formatBytes(n) {
|
|
5765
|
+
if (n < 1024) return `${n} B`;
|
|
5766
|
+
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
|
|
5767
|
+
return `${(n / 1024 / 1024).toFixed(1)} MB`;
|
|
5768
|
+
}
|
|
5769
|
+
function formatAge(ms) {
|
|
5770
|
+
const s = Math.floor(ms / 1e3);
|
|
5771
|
+
if (s < 60) return `${s}s`;
|
|
5772
|
+
const m = Math.floor(s / 60);
|
|
5773
|
+
if (m < 60) return `${m}m`;
|
|
5774
|
+
const h = Math.floor(m / 60);
|
|
5775
|
+
if (h < 24) return `${h}h`;
|
|
5776
|
+
return `${Math.floor(h / 24)}d`;
|
|
5777
|
+
}
|
|
5778
|
+
async function storeCommand(args2) {
|
|
5779
|
+
const local = args2.includes("--local");
|
|
5780
|
+
const showSecrets = args2.includes("--show-secrets");
|
|
5781
|
+
const yes = args2.includes("-y") || args2.includes("--yes");
|
|
5782
|
+
const positional = [];
|
|
5783
|
+
for (const a of args2) {
|
|
5784
|
+
if (a.startsWith("--") || a === "-y") continue;
|
|
5785
|
+
positional.push(a);
|
|
5786
|
+
}
|
|
5787
|
+
const kind = positional[0];
|
|
5788
|
+
const action = positional[1];
|
|
5789
|
+
const name = positional[2];
|
|
5790
|
+
const mgr = new StoreManager(local);
|
|
5791
|
+
mgr.ensure();
|
|
5792
|
+
if (!kind) {
|
|
5793
|
+
const scan = mgr.scan();
|
|
5461
5794
|
console.error(`
|
|
5462
|
-
${pc17.dim("
|
|
5795
|
+
${symbols.fox} ${pc17.bold("Fennec Store")} ${pc17.dim(local ? "(local ./.fennec)" : "(global)")}
|
|
5796
|
+
`);
|
|
5797
|
+
const columns = [
|
|
5798
|
+
{ key: "kind", label: "Kind", format: (v) => pc17.bold(String(v)) },
|
|
5799
|
+
{ key: "count", label: "Items", format: (v) => pc17.cyan(String(v)) },
|
|
5800
|
+
{ key: "size", label: "Size", format: (v) => pc17.dim(formatBytes(Number(v))) },
|
|
5801
|
+
{ key: "oldest", label: "Oldest", format: (v) => pc17.dim(formatAge(Number(v))) }
|
|
5802
|
+
];
|
|
5803
|
+
const rows = scan.map((e) => ({
|
|
5804
|
+
kind: e.kind,
|
|
5805
|
+
count: e.count,
|
|
5806
|
+
size: e.sizeBytes,
|
|
5807
|
+
oldest: e.oldestMs
|
|
5808
|
+
}));
|
|
5809
|
+
console.error(renderTable(columns, rows));
|
|
5810
|
+
console.error(` ${pc17.dim(mgr.base)}
|
|
5463
5811
|
`);
|
|
5464
5812
|
return;
|
|
5465
5813
|
}
|
|
5466
|
-
|
|
5467
|
-
|
|
5468
|
-
|
|
5469
|
-
|
|
5470
|
-
|
|
5471
|
-
|
|
5472
|
-
|
|
5814
|
+
if (!KINDS.includes(kind)) {
|
|
5815
|
+
console.error(renderError("Unknown kind", `Kind must be one of: ${KINDS.join(", ")}`));
|
|
5816
|
+
process.exit(1);
|
|
5817
|
+
}
|
|
5818
|
+
if (kind !== "session") {
|
|
5819
|
+
const entry = mgr.scan().find((e) => e.kind === kind);
|
|
5820
|
+
console.error(`
|
|
5821
|
+
${pc17.bold(kind)} ${pc17.dim(`(${entry?.count ?? 0} item(s))`)}
|
|
5822
|
+
`);
|
|
5823
|
+
console.error(` ${pc17.dim(mgr.dirFor(kind))}
|
|
5824
|
+
`);
|
|
5825
|
+
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).";
|
|
5826
|
+
console.error(` ${pc17.dim(tip)}
|
|
5827
|
+
`);
|
|
5828
|
+
return;
|
|
5829
|
+
}
|
|
5830
|
+
const store = mgr.sessionStore();
|
|
5831
|
+
if (!action || action === "ls" || action === "list") {
|
|
5832
|
+
const sessions = store.list();
|
|
5833
|
+
if (sessions.length === 0) {
|
|
5834
|
+
console.error(`
|
|
5835
|
+
${pc17.dim(`No saved sessions found${local ? " in ./.fennec/sessions" : " in the global store"}.`)}
|
|
5836
|
+
`);
|
|
5837
|
+
return;
|
|
5838
|
+
}
|
|
5839
|
+
const columns = [
|
|
5840
|
+
{ key: "name", label: "Name", format: (v) => pc17.bold(String(v)) },
|
|
5841
|
+
{ key: "origin", label: "Origin" },
|
|
5842
|
+
{ key: "savedAt", label: "Saved", format: (v) => pc17.dim(String(v)) }
|
|
5843
|
+
];
|
|
5844
|
+
const rows = sessions.map((s) => ({
|
|
5845
|
+
name: s.name,
|
|
5846
|
+
origin: s.origin,
|
|
5847
|
+
savedAt: new Date(s.savedAt).toLocaleString()
|
|
5848
|
+
}));
|
|
5849
|
+
console.error(`
|
|
5473
5850
|
${symbols.fox} ${pc17.bold("Saved Sessions")}
|
|
5474
5851
|
`);
|
|
5475
|
-
|
|
5476
|
-
|
|
5852
|
+
console.error(renderTable(columns, rows));
|
|
5853
|
+
console.error(` ${pc17.dim(`${sessions.length} session(s)`)}
|
|
5854
|
+
`);
|
|
5855
|
+
return;
|
|
5856
|
+
}
|
|
5857
|
+
if (action === "info") {
|
|
5858
|
+
if (!name) {
|
|
5859
|
+
console.error(renderError("Missing name", "Usage: fennec store session info <name> [--show-secrets]"));
|
|
5860
|
+
process.exit(1);
|
|
5861
|
+
}
|
|
5862
|
+
const session = store.load(name);
|
|
5863
|
+
if (!session) {
|
|
5864
|
+
console.error(renderError("Session not found", `No session named "${name}". Use 'fennec store session' to list.`));
|
|
5865
|
+
process.exit(1);
|
|
5866
|
+
}
|
|
5867
|
+
const shown = showSecrets ? session : redactSession(session);
|
|
5868
|
+
const cookieDomains = shown.cookies.map((c) => String(c.domain ?? c.name ?? "?")).filter((v, i, a) => a.indexOf(v) === i);
|
|
5869
|
+
let body = "";
|
|
5870
|
+
body += renderKV("Name", shown.name);
|
|
5871
|
+
body += renderKV("Origin", shown.origin);
|
|
5872
|
+
body += renderKV("Saved", new Date(shown.savedAt).toLocaleString());
|
|
5873
|
+
if (shown.metadata) {
|
|
5874
|
+
for (const [k, v] of Object.entries(shown.metadata)) body += renderKV(k, String(v));
|
|
5875
|
+
}
|
|
5876
|
+
body += renderKV("Cookies", `${shown.cookies.length} (domains: ${cookieDomains.join(", ") || "-"})`);
|
|
5877
|
+
body += renderKV("localStorage keys", String(Object.keys(shown.localStorage).length));
|
|
5878
|
+
body += renderKV("Values", showSecrets ? "shown" : pc17.dim("masked \u2014 use --show-secrets"));
|
|
5879
|
+
console.error(`
|
|
5880
|
+
${pc17.bold(`Session: ${shown.name}`)}
|
|
5881
|
+
`);
|
|
5882
|
+
console.error(renderSection("Details", body));
|
|
5883
|
+
return;
|
|
5884
|
+
}
|
|
5885
|
+
if (action === "rm" || action === "remove" || action === "delete") {
|
|
5886
|
+
if (!name) {
|
|
5887
|
+
console.error(renderError("Missing name", "Usage: fennec store session rm <name>"));
|
|
5888
|
+
process.exit(1);
|
|
5889
|
+
}
|
|
5890
|
+
if (!store.load(name)) {
|
|
5891
|
+
console.error(renderError("Session not found", `No session named "${name}".`));
|
|
5892
|
+
process.exit(1);
|
|
5893
|
+
}
|
|
5894
|
+
const confirmed = yes || await confirmPrompt(`Delete session ${pc17.bold(name)}?`, false);
|
|
5895
|
+
if (!confirmed) {
|
|
5896
|
+
console.error(` ${pc17.dim("Cancelled")}`);
|
|
5897
|
+
return;
|
|
5898
|
+
}
|
|
5899
|
+
const ok = store.delete(name);
|
|
5900
|
+
console.error(ok ? ` ${pc17.green("\u2713")} ${pc17.bold(name)} deleted
|
|
5901
|
+
` : ` ${pc17.red("\u2717")} failed to delete ${pc17.bold(name)}
|
|
5477
5902
|
`);
|
|
5903
|
+
return;
|
|
5904
|
+
}
|
|
5905
|
+
console.error(renderError("Unknown action", "Actions: ls | info <name> | rm <name>"));
|
|
5906
|
+
process.exit(1);
|
|
5478
5907
|
}
|
|
5479
5908
|
|
|
5480
|
-
// src/commands/
|
|
5909
|
+
// src/commands/doctor.ts
|
|
5910
|
+
import { existsSync as existsSync10, readFileSync as readFileSync9 } from "fs";
|
|
5911
|
+
import { join } from "path";
|
|
5912
|
+
import { execSync as execSync3 } from "child_process";
|
|
5913
|
+
import { StoreManager as StoreManager2 } from "@plumpslabs/fennec-core";
|
|
5481
5914
|
import pc18 from "picocolors";
|
|
5915
|
+
async function doctorCommand() {
|
|
5916
|
+
const mgr = new StoreManager2(false);
|
|
5917
|
+
const base = mgr.base;
|
|
5918
|
+
const problems = [];
|
|
5919
|
+
const notes = [];
|
|
5920
|
+
if (!mgr.permsSafe()) {
|
|
5921
|
+
problems.push(`Store dir is group/other readable: ${base}
|
|
5922
|
+
Fix: chmod 700 ${base}`);
|
|
5923
|
+
}
|
|
5924
|
+
if (StoreManager2.isSynced(base)) {
|
|
5925
|
+
problems.push(
|
|
5926
|
+
`Store lives under a synced directory: ${base}
|
|
5927
|
+
Auth cookies/tokens in sessions + process launch commands could leak across machines.
|
|
5928
|
+
Prefer a non-synced location (FENNEC_HOME=/non-synced/fennec) or use --local with care.`
|
|
5929
|
+
);
|
|
5930
|
+
}
|
|
5931
|
+
const scan = mgr.scan();
|
|
5932
|
+
const sess = scan.find((s) => s.kind === "session");
|
|
5933
|
+
if (sess && sess.count > 0) {
|
|
5934
|
+
notes.push(
|
|
5935
|
+
`${sess.count} auth session(s) with cookies/localStorage stored on disk.
|
|
5936
|
+
Values are masked by default in 'fennec store session info'; use --show-secrets to reveal.`
|
|
5937
|
+
);
|
|
5938
|
+
}
|
|
5939
|
+
const proc = scan.find((s) => s.kind === "process");
|
|
5940
|
+
if (proc && proc.count > 0) {
|
|
5941
|
+
const tracked = mgr.fileFor("process", "");
|
|
5942
|
+
const cmds = existsSync10(tracked) ? JSON.parse(readFileSync9(tracked, "utf-8")) : [];
|
|
5943
|
+
const secretCmds = cmds.filter(
|
|
5944
|
+
(c) => c.command && /(?:^|[\s;|&])([A-Za-z_][A-Za-z0-9_]*)=/.test(c.command)
|
|
5945
|
+
).length;
|
|
5946
|
+
if (secretCmds > 0) {
|
|
5947
|
+
notes.push(
|
|
5948
|
+
`${secretCmds} tracked process launch command(s) may embed secrets (KEY=value).
|
|
5949
|
+
Stored in ${tracked} \u2014 keep the store out of synced/committed locations.`
|
|
5950
|
+
);
|
|
5951
|
+
}
|
|
5952
|
+
}
|
|
5953
|
+
const localFen = join(process.cwd(), ".fennec");
|
|
5954
|
+
if (existsSync10(localFen)) {
|
|
5955
|
+
let ignored = false;
|
|
5956
|
+
try {
|
|
5957
|
+
execSync3("git check-ignore -q .fennec", { cwd: process.cwd(), stdio: "ignore" });
|
|
5958
|
+
ignored = true;
|
|
5959
|
+
} catch (err) {
|
|
5960
|
+
ignored = err?.code === "ENOENT";
|
|
5961
|
+
}
|
|
5962
|
+
if (!ignored) {
|
|
5963
|
+
problems.push(
|
|
5964
|
+
`Local ./.fennec exists here and is NOT gitignored.
|
|
5965
|
+
It can hold sessions + tracked processes \u2014 add '.fennec/' to .gitignore or remove it.`
|
|
5966
|
+
);
|
|
5967
|
+
}
|
|
5968
|
+
}
|
|
5969
|
+
console.error(`
|
|
5970
|
+
${pc18.bold("Fennec Doctor")}
|
|
5971
|
+
`);
|
|
5972
|
+
console.error(` ${pc18.dim("Store:")} ${base}
|
|
5973
|
+
`);
|
|
5974
|
+
if (problems.length === 0 && notes.length === 0) {
|
|
5975
|
+
console.error(` ${pc18.green("\u2713")} No issues found.
|
|
5976
|
+
`);
|
|
5977
|
+
return;
|
|
5978
|
+
}
|
|
5979
|
+
for (const p of problems) console.error(` ${pc18.red("\u2717")} ${p}
|
|
5980
|
+
`);
|
|
5981
|
+
for (const n of notes) console.error(` ${pc18.yellow("!")} ${n}
|
|
5982
|
+
`);
|
|
5983
|
+
}
|
|
5984
|
+
|
|
5985
|
+
// src/commands/setup.ts
|
|
5986
|
+
import pc19 from "picocolors";
|
|
5482
5987
|
async function setupCommand() {
|
|
5483
5988
|
console.error(`
|
|
5484
|
-
${symbols.fox} ${
|
|
5989
|
+
${symbols.fox} ${pc19.bold("Fennec Setup")}
|
|
5485
5990
|
`);
|
|
5486
5991
|
const mcpClient = await selectPrompt("Which MCP client are you using?", [
|
|
5487
5992
|
{ value: "claude", label: "Claude Desktop", description: "Anthropic's AI desktop app" },
|
|
@@ -5490,12 +5995,12 @@ async function setupCommand() {
|
|
|
5490
5995
|
{ value: "other", label: "Other MCP client", description: "Any MCP-compatible client" }
|
|
5491
5996
|
]);
|
|
5492
5997
|
if (!mcpClient) {
|
|
5493
|
-
console.error(` ${
|
|
5998
|
+
console.error(` ${pc19.dim("Setup cancelled.")}
|
|
5494
5999
|
`);
|
|
5495
6000
|
return;
|
|
5496
6001
|
}
|
|
5497
6002
|
console.error(`
|
|
5498
|
-
${
|
|
6003
|
+
${pc19.green("\u2713")} Selected: ${pc19.bold(mcpClient)}
|
|
5499
6004
|
`);
|
|
5500
6005
|
const configSnippet = `{
|
|
5501
6006
|
"mcpServers": {
|
|
@@ -5505,46 +6010,46 @@ async function setupCommand() {
|
|
|
5505
6010
|
}
|
|
5506
6011
|
}
|
|
5507
6012
|
}`;
|
|
5508
|
-
console.error(` ${
|
|
6013
|
+
console.error(` ${pc19.bold("Add this to your MCP client config:")}
|
|
5509
6014
|
`);
|
|
5510
|
-
console.error(` ${
|
|
6015
|
+
console.error(` ${pc19.dim("```")}`);
|
|
5511
6016
|
console.error(configSnippet.split("\n").map((l) => ` ${l}`).join("\n"));
|
|
5512
|
-
console.error(` ${
|
|
6017
|
+
console.error(` ${pc19.dim("```")}
|
|
5513
6018
|
`);
|
|
5514
|
-
console.error(` ${renderSuccess("Setup complete!")} ${
|
|
6019
|
+
console.error(` ${renderSuccess("Setup complete!")} ${pc19.dim("Run")} ${renderCommand("fennec start")} ${pc19.dim("to begin.")}
|
|
5515
6020
|
`);
|
|
5516
6021
|
}
|
|
5517
6022
|
|
|
5518
6023
|
// src/commands/management.ts
|
|
5519
|
-
import { existsSync as
|
|
5520
|
-
import { resolve as
|
|
5521
|
-
import { execSync as
|
|
5522
|
-
import
|
|
6024
|
+
import { existsSync as existsSync11, writeFileSync as writeFileSync4 } from "fs";
|
|
6025
|
+
import { resolve as resolve7 } from "path";
|
|
6026
|
+
import { execSync as execSync4 } from "child_process";
|
|
6027
|
+
import pc20 from "picocolors";
|
|
5523
6028
|
async function installBrowsersCommand() {
|
|
5524
6029
|
console.error(`
|
|
5525
|
-
${
|
|
6030
|
+
${pc20.bold("Installing Browser Engines")}
|
|
5526
6031
|
`);
|
|
5527
6032
|
const spinner = createSpinner("Installing Chromium...");
|
|
5528
6033
|
try {
|
|
5529
|
-
|
|
6034
|
+
execSync4("npx playwright install chromium", { stdio: "pipe", timeout: 12e4 });
|
|
5530
6035
|
spinner.succeed("Chromium installed successfully");
|
|
5531
6036
|
} catch {
|
|
5532
6037
|
spinner.fail("Failed to install Chromium");
|
|
5533
|
-
console.error(` ${
|
|
6038
|
+
console.error(` ${pc20.yellow("\u2192")} Try running: ${renderCommand("npx playwright install chromium")}`);
|
|
5534
6039
|
}
|
|
5535
6040
|
console.error(`
|
|
5536
|
-
${
|
|
6041
|
+
${pc20.green("\u2713")} Browser installation complete.
|
|
5537
6042
|
`);
|
|
5538
6043
|
}
|
|
5539
6044
|
async function initCommand() {
|
|
5540
6045
|
console.error(`
|
|
5541
|
-
${
|
|
6046
|
+
${pc20.bold("Initialize Fennec Configuration")}
|
|
5542
6047
|
`);
|
|
5543
|
-
const configFile =
|
|
5544
|
-
if (
|
|
5545
|
-
const overwrite = await confirmPrompt(`${
|
|
6048
|
+
const configFile = resolve7("./fennec.config.yaml");
|
|
6049
|
+
if (existsSync11(configFile)) {
|
|
6050
|
+
const overwrite = await confirmPrompt(`${pc20.yellow("fennec.config.yaml")} already exists. Overwrite?`, false);
|
|
5546
6051
|
if (!overwrite) {
|
|
5547
|
-
console.error(` ${
|
|
6052
|
+
console.error(` ${pc20.dim("Cancelled.")}
|
|
5548
6053
|
`);
|
|
5549
6054
|
return;
|
|
5550
6055
|
}
|
|
@@ -5624,44 +6129,44 @@ logging:
|
|
|
5624
6129
|
file: null
|
|
5625
6130
|
`;
|
|
5626
6131
|
writeFileSync4(configFile, config, "utf-8");
|
|
5627
|
-
spinner.succeed(`Configuration written to ${
|
|
6132
|
+
spinner.succeed(`Configuration written to ${pc20.bold(configFile)}`);
|
|
5628
6133
|
console.error(`
|
|
5629
|
-
${
|
|
6134
|
+
${pc20.dim("Edit the file to customize Fennec behavior.")}
|
|
5630
6135
|
`);
|
|
5631
6136
|
}
|
|
5632
6137
|
|
|
5633
6138
|
// src/commands/health.ts
|
|
5634
|
-
import { existsSync as
|
|
5635
|
-
import { execSync as
|
|
5636
|
-
import { resolve as
|
|
5637
|
-
import { homedir as
|
|
5638
|
-
import
|
|
6139
|
+
import { existsSync as existsSync12, readdirSync as readdirSync2, statSync as statSync3 } from "fs";
|
|
6140
|
+
import { execSync as execSync5 } from "child_process";
|
|
6141
|
+
import { resolve as resolve8 } from "path";
|
|
6142
|
+
import { homedir as homedir5 } from "os";
|
|
6143
|
+
import pc21 from "picocolors";
|
|
5639
6144
|
async function healthCommand() {
|
|
5640
6145
|
printBanner();
|
|
5641
|
-
console.error(` ${
|
|
6146
|
+
console.error(` ${pc21.bold("Fennec Health Check")}
|
|
5642
6147
|
`);
|
|
5643
6148
|
let adbStatus = "unknown";
|
|
5644
6149
|
try {
|
|
5645
|
-
const result =
|
|
6150
|
+
const result = execSync5("adb --version", {
|
|
5646
6151
|
encoding: "utf-8",
|
|
5647
6152
|
timeout: 3e3,
|
|
5648
6153
|
stdio: ["ignore", "pipe", "pipe"]
|
|
5649
6154
|
});
|
|
5650
6155
|
const version = result.split("\n")[0]?.trim() ?? "installed";
|
|
5651
|
-
adbStatus =
|
|
6156
|
+
adbStatus = pc21.green(version);
|
|
5652
6157
|
} catch {
|
|
5653
|
-
adbStatus =
|
|
6158
|
+
adbStatus = pc21.dim("not found") + " (optional)";
|
|
5654
6159
|
}
|
|
5655
6160
|
const tracked = readTracked();
|
|
5656
6161
|
const runningCount = tracked.filter((t) => isProcessRunning(t.pid)).length;
|
|
5657
|
-
const logDir =
|
|
6162
|
+
const logDir = resolve8(homedir5(), ".fennec", "logs");
|
|
5658
6163
|
let logSize = "0 B";
|
|
5659
6164
|
try {
|
|
5660
|
-
if (
|
|
6165
|
+
if (existsSync12(logDir)) {
|
|
5661
6166
|
const files = readdirSync2(logDir);
|
|
5662
6167
|
let totalBytes = 0;
|
|
5663
6168
|
for (const f of files) {
|
|
5664
|
-
const fStat = statSync3(
|
|
6169
|
+
const fStat = statSync3(resolve8(logDir, f));
|
|
5665
6170
|
totalBytes += fStat.size;
|
|
5666
6171
|
}
|
|
5667
6172
|
logSize = totalBytes > 1024 * 1024 ? `${(totalBytes / (1024 * 1024)).toFixed(1)} MB` : `${(totalBytes / 1024).toFixed(1)} KB`;
|
|
@@ -5674,7 +6179,7 @@ async function healthCommand() {
|
|
|
5674
6179
|
memoryInfo = `${(mem.rss / (1024 * 1024)).toFixed(0)} MB RSS / ${(mem.heapUsed / (1024 * 1024)).toFixed(0)} MB heap`;
|
|
5675
6180
|
} catch {
|
|
5676
6181
|
}
|
|
5677
|
-
console.error(` ${symbols.fox} ${
|
|
6182
|
+
console.error(` ${symbols.fox} ${pc21.bold("System")}
|
|
5678
6183
|
`);
|
|
5679
6184
|
console.error(` ${renderKV("Node.js", process.version)}`);
|
|
5680
6185
|
console.error(` ${renderKV("Platform", `${process.platform} ${process.arch}`)}`);
|
|
@@ -5682,7 +6187,7 @@ async function healthCommand() {
|
|
|
5682
6187
|
console.error(` ${renderKV("ADB", adbStatus)}`);
|
|
5683
6188
|
console.error(` ${renderKV("PID", String(process.pid))}`);
|
|
5684
6189
|
console.error();
|
|
5685
|
-
console.error(` ${symbols.fox} ${
|
|
6190
|
+
console.error(` ${symbols.fox} ${pc21.bold("Processes")}
|
|
5686
6191
|
`);
|
|
5687
6192
|
console.error(` ${renderKV("Tracked", String(tracked.length))}`);
|
|
5688
6193
|
console.error(` ${renderKV("Running", String(runningCount))}`);
|
|
@@ -5690,56 +6195,56 @@ async function healthCommand() {
|
|
|
5690
6195
|
console.error();
|
|
5691
6196
|
const allHealthy = runningCount === tracked.length || tracked.length === 0;
|
|
5692
6197
|
if (allHealthy) {
|
|
5693
|
-
console.error(` ${
|
|
6198
|
+
console.error(` ${pc21.green("\u2713")} ${pc21.bold("All systems healthy")}
|
|
5694
6199
|
`);
|
|
5695
6200
|
} else {
|
|
5696
6201
|
const stopped = tracked.length - runningCount;
|
|
5697
|
-
console.error(` ${
|
|
6202
|
+
console.error(` ${pc21.yellow("\u26A0")} ${pc21.bold(`${stopped} process(es) stopped`)} ${pc21.dim("fennec ps")}
|
|
5698
6203
|
`);
|
|
5699
6204
|
}
|
|
5700
6205
|
}
|
|
5701
6206
|
|
|
5702
6207
|
// src/commands/cleanup.ts
|
|
5703
|
-
import
|
|
6208
|
+
import pc22 from "picocolors";
|
|
5704
6209
|
async function cleanupCommand() {
|
|
5705
6210
|
const tracked = readTracked();
|
|
5706
6211
|
if (tracked.length === 0) {
|
|
5707
6212
|
console.error(`
|
|
5708
|
-
${
|
|
6213
|
+
${pc22.dim("No tracked processes to clean up.")}
|
|
5709
6214
|
`);
|
|
5710
6215
|
return;
|
|
5711
6216
|
}
|
|
5712
6217
|
const toRemove = tracked.filter((t) => !isProcessRunning(t.pid) && !t.command);
|
|
5713
6218
|
if (toRemove.length === 0) {
|
|
5714
6219
|
console.error(`
|
|
5715
|
-
${
|
|
6220
|
+
${pc22.green("\u2713")} ${pc22.bold("All clean")} ${pc22.dim("\u2014 no dead entries without saved commands.")}
|
|
5716
6221
|
`);
|
|
5717
6222
|
return;
|
|
5718
6223
|
}
|
|
5719
6224
|
console.error(`
|
|
5720
|
-
${
|
|
6225
|
+
${pc22.yellow("\u26A0")} ${pc22.bold(`Found ${toRemove.length} dead entr${toRemove.length > 1 ? "ies" : "y"} without saved commands`)}
|
|
5721
6226
|
`);
|
|
5722
6227
|
for (const entry of toRemove) {
|
|
5723
|
-
console.error(` ${
|
|
6228
|
+
console.error(` ${pc22.dim("\xB7")} ${pc22.bold(entry.name)} ${pc22.dim(`(PID ${entry.pid})`)}`);
|
|
5724
6229
|
}
|
|
5725
6230
|
console.error();
|
|
5726
6231
|
const confirmed = await confirmPrompt(`Remove ${toRemove.length} entr${toRemove.length > 1 ? "ies" : "y"}?`, false);
|
|
5727
6232
|
if (!confirmed) {
|
|
5728
|
-
console.error(` ${
|
|
6233
|
+
console.error(` ${pc22.dim("Cancelled")}
|
|
5729
6234
|
`);
|
|
5730
6235
|
return;
|
|
5731
6236
|
}
|
|
5732
6237
|
const remaining = tracked.filter((t) => !toRemove.includes(t));
|
|
5733
6238
|
saveTracked(remaining);
|
|
5734
6239
|
console.error(`
|
|
5735
|
-
${
|
|
6240
|
+
${pc22.green("\u2713")} ${pc22.bold(`Removed ${toRemove.length} entr${toRemove.length > 1 ? "ies" : "y"}`)} ${pc22.dim(`(${remaining.length} remaining)`)}
|
|
5736
6241
|
`);
|
|
5737
6242
|
}
|
|
5738
6243
|
|
|
5739
6244
|
// src/commands/info.ts
|
|
5740
|
-
import
|
|
5741
|
-
import { resolve as
|
|
5742
|
-
import { homedir as
|
|
6245
|
+
import pc23 from "picocolors";
|
|
6246
|
+
import { resolve as resolve9 } from "path";
|
|
6247
|
+
import { homedir as homedir6 } from "os";
|
|
5743
6248
|
async function infoCommand(args2) {
|
|
5744
6249
|
const name = args2[0];
|
|
5745
6250
|
if (!name) {
|
|
@@ -5753,34 +6258,34 @@ async function infoCommand(args2) {
|
|
|
5753
6258
|
process.exit(1);
|
|
5754
6259
|
}
|
|
5755
6260
|
const running = isProcessRunning(match.pid);
|
|
5756
|
-
const statusIcon = running ?
|
|
5757
|
-
const statusText = running ?
|
|
6261
|
+
const statusIcon = running ? pc23.green("\u25CF") : pc23.red("\u25CB");
|
|
6262
|
+
const statusText = running ? pc23.green("running") : pc23.red("stopped");
|
|
5758
6263
|
const uptime = running ? formatUptime(Math.floor((Date.now() - new Date(match.startedAt).getTime()) / 1e3)) : "-";
|
|
5759
|
-
const logPath =
|
|
6264
|
+
const logPath = resolve9(homedir6(), ".fennec", "logs", `${match.name}.log`);
|
|
5760
6265
|
console.error(`
|
|
5761
|
-
${symbols.fox} ${
|
|
6266
|
+
${symbols.fox} ${pc23.bold(match.name)} ${pc23.dim("\u2014 Process Info")}
|
|
5762
6267
|
`);
|
|
5763
|
-
console.error(` ${renderKVColor("Name", match.name,
|
|
6268
|
+
console.error(` ${renderKVColor("Name", match.name, pc23.bold)}`);
|
|
5764
6269
|
console.error(` ${renderKVColor("Status", `${statusIcon} ${statusText}`)}`);
|
|
5765
6270
|
console.error(` ${renderKVColor("PID", String(match.pid))}`);
|
|
5766
6271
|
console.error(` ${renderKVColor("Port", match.port ? String(match.port) : "-")}`);
|
|
5767
|
-
console.error(` ${renderKVColor("Command", match.command ||
|
|
6272
|
+
console.error(` ${renderKVColor("Command", match.command || pc23.dim("(none)"))}`);
|
|
5768
6273
|
if (match.cwd) console.error(` ${renderKVColor("CWD", match.cwd)}`);
|
|
5769
6274
|
console.error(` ${renderKVColor("Started", new Date(match.startedAt).toLocaleString())}`);
|
|
5770
6275
|
console.error(` ${renderKVColor("Uptime", uptime)}`);
|
|
5771
6276
|
console.error(` ${renderKVColor("Log Path", logPath)}`);
|
|
5772
6277
|
if (!running && match.command) {
|
|
5773
6278
|
console.error(`
|
|
5774
|
-
${
|
|
6279
|
+
${pc23.dim("Re-spawn with:")} ${pc23.cyan(`fennec spawn ${match.name}`)}`);
|
|
5775
6280
|
}
|
|
5776
6281
|
console.error();
|
|
5777
6282
|
}
|
|
5778
6283
|
|
|
5779
6284
|
// src/commands/rename.ts
|
|
5780
|
-
import
|
|
5781
|
-
import { existsSync as
|
|
5782
|
-
import { resolve as
|
|
5783
|
-
import { homedir as
|
|
6285
|
+
import pc24 from "picocolors";
|
|
6286
|
+
import { existsSync as existsSync13, renameSync as renameSync2 } from "fs";
|
|
6287
|
+
import { resolve as resolve10 } from "path";
|
|
6288
|
+
import { homedir as homedir7 } from "os";
|
|
5784
6289
|
async function renameCommand(args2) {
|
|
5785
6290
|
const [oldName, newName] = args2;
|
|
5786
6291
|
if (!oldName || !newName) {
|
|
@@ -5789,7 +6294,7 @@ async function renameCommand(args2) {
|
|
|
5789
6294
|
}
|
|
5790
6295
|
if (oldName === newName) {
|
|
5791
6296
|
console.error(`
|
|
5792
|
-
${
|
|
6297
|
+
${pc24.yellow("\u26A0")} ${pc24.dim("Old and new names are the same.")}
|
|
5793
6298
|
`);
|
|
5794
6299
|
process.exit(0);
|
|
5795
6300
|
}
|
|
@@ -5804,25 +6309,25 @@ async function renameCommand(args2) {
|
|
|
5804
6309
|
process.exit(1);
|
|
5805
6310
|
}
|
|
5806
6311
|
console.error(`
|
|
5807
|
-
${symbols.fox} ${
|
|
6312
|
+
${symbols.fox} ${pc24.bold("Rename Process")}
|
|
5808
6313
|
`);
|
|
5809
6314
|
console.error(` ${renderKVColor("From", oldName)}`);
|
|
5810
6315
|
console.error(` ${renderKVColor("To", newName)}`);
|
|
5811
6316
|
if (isProcessRunning(match.pid)) {
|
|
5812
|
-
console.error(` ${
|
|
6317
|
+
console.error(` ${pc24.yellow("\u26A0")} ${pc24.dim("Process is still running \u2014 log entries will go to the old file until stopped.")}`);
|
|
5813
6318
|
}
|
|
5814
6319
|
console.error();
|
|
5815
|
-
const confirmed = await confirmPrompt(`Rename ${
|
|
6320
|
+
const confirmed = await confirmPrompt(`Rename ${pc24.bold(oldName)} ${pc24.dim("\u2192")} ${pc24.bold(newName)}?`, true);
|
|
5816
6321
|
if (!confirmed) {
|
|
5817
|
-
console.error(` ${
|
|
6322
|
+
console.error(` ${pc24.dim("Cancelled")}
|
|
5818
6323
|
`);
|
|
5819
6324
|
return;
|
|
5820
6325
|
}
|
|
5821
6326
|
const spinner = createSpinner(`Renaming ${oldName} \u2192 ${newName}...`);
|
|
5822
|
-
const logDir =
|
|
5823
|
-
const oldLog =
|
|
5824
|
-
const newLog =
|
|
5825
|
-
if (
|
|
6327
|
+
const logDir = resolve10(homedir7(), ".fennec", "logs");
|
|
6328
|
+
const oldLog = resolve10(logDir, `${oldName}.log`);
|
|
6329
|
+
const newLog = resolve10(logDir, `${newName}.log`);
|
|
6330
|
+
if (existsSync13(oldLog) && !existsSync13(newLog)) {
|
|
5826
6331
|
try {
|
|
5827
6332
|
renameSync2(oldLog, newLog);
|
|
5828
6333
|
} catch {
|
|
@@ -5830,14 +6335,14 @@ async function renameCommand(args2) {
|
|
|
5830
6335
|
}
|
|
5831
6336
|
match.name = newName;
|
|
5832
6337
|
saveTracked(tracked);
|
|
5833
|
-
spinner.succeed(`${
|
|
6338
|
+
spinner.succeed(`${pc24.bold(oldName)} ${pc24.dim("\u2192")} ${pc24.bold(newName)}`);
|
|
5834
6339
|
console.error();
|
|
5835
6340
|
}
|
|
5836
6341
|
|
|
5837
6342
|
// src/commands/export-import.ts
|
|
5838
|
-
import
|
|
5839
|
-
import { existsSync as
|
|
5840
|
-
import { resolve as
|
|
6343
|
+
import pc25 from "picocolors";
|
|
6344
|
+
import { existsSync as existsSync14, writeFileSync as writeFileSync5, readFileSync as readFileSync10 } from "fs";
|
|
6345
|
+
import { resolve as resolve11 } from "path";
|
|
5841
6346
|
async function exportCommand(args2) {
|
|
5842
6347
|
const tracked = readTracked();
|
|
5843
6348
|
const fileIndex = args2.indexOf("--file");
|
|
@@ -5848,16 +6353,16 @@ async function exportCommand(args2) {
|
|
|
5848
6353
|
}
|
|
5849
6354
|
if (tracked.length === 0) {
|
|
5850
6355
|
console.error(`
|
|
5851
|
-
${
|
|
6356
|
+
${pc25.dim("No tracked processes to export.")}
|
|
5852
6357
|
`);
|
|
5853
6358
|
return;
|
|
5854
6359
|
}
|
|
5855
6360
|
const json = JSON.stringify(tracked, null, 2);
|
|
5856
6361
|
if (filePath) {
|
|
5857
6362
|
try {
|
|
5858
|
-
writeFileSync5(
|
|
6363
|
+
writeFileSync5(resolve11(filePath), json, "utf-8");
|
|
5859
6364
|
console.error(`
|
|
5860
|
-
${
|
|
6365
|
+
${pc25.green("\u2713")} ${pc25.bold(`Exported ${tracked.length} process(es)`)} ${pc25.dim(`\u2192 ${filePath}`)}
|
|
5861
6366
|
`);
|
|
5862
6367
|
} catch (err) {
|
|
5863
6368
|
console.error(renderError("Export failed", String(err)));
|
|
@@ -5873,14 +6378,14 @@ async function importCommand(args2) {
|
|
|
5873
6378
|
console.error(renderError("Missing file", "Usage: fennec import <file>"));
|
|
5874
6379
|
process.exit(1);
|
|
5875
6380
|
}
|
|
5876
|
-
const resolvedPath =
|
|
5877
|
-
if (!
|
|
6381
|
+
const resolvedPath = resolve11(filePath);
|
|
6382
|
+
if (!existsSync14(resolvedPath)) {
|
|
5878
6383
|
console.error(renderError("File not found", `"${filePath}" does not exist.`));
|
|
5879
6384
|
process.exit(1);
|
|
5880
6385
|
}
|
|
5881
6386
|
let imported;
|
|
5882
6387
|
try {
|
|
5883
|
-
imported = JSON.parse(
|
|
6388
|
+
imported = JSON.parse(readFileSync10(resolvedPath, "utf-8"));
|
|
5884
6389
|
if (!Array.isArray(imported) || imported.length === 0) {
|
|
5885
6390
|
console.error(renderError("Invalid file", "File must contain a non-empty JSON array of process objects."));
|
|
5886
6391
|
process.exit(1);
|
|
@@ -5891,20 +6396,20 @@ async function importCommand(args2) {
|
|
|
5891
6396
|
}
|
|
5892
6397
|
const existing = readTracked();
|
|
5893
6398
|
console.error(`
|
|
5894
|
-
${symbols.fox} ${
|
|
6399
|
+
${symbols.fox} ${pc25.bold("Import Processes")}
|
|
5895
6400
|
`);
|
|
5896
6401
|
console.error(` ${renderKVColor("File", resolvedPath)}`);
|
|
5897
6402
|
console.error(` ${renderKVColor("Importing", `${imported.length} process(es)`)}`);
|
|
5898
6403
|
console.error(` ${renderKVColor("Existing", `${existing.length} process(es)`)}`);
|
|
5899
6404
|
console.error(`
|
|
5900
|
-
${
|
|
6405
|
+
${pc25.bold("Processes to import:")}`);
|
|
5901
6406
|
for (const p of imported) {
|
|
5902
|
-
console.error(` ${
|
|
6407
|
+
console.error(` ${pc25.green("+")} ${pc25.bold(p.name)} ${pc25.dim(`(${p.command})`)}`);
|
|
5903
6408
|
}
|
|
5904
6409
|
console.error();
|
|
5905
6410
|
const confirmed = await confirmPrompt("Merge into tracked.json? Existing processes with the same name will be overwritten.", true);
|
|
5906
6411
|
if (!confirmed) {
|
|
5907
|
-
console.error(` ${
|
|
6412
|
+
console.error(` ${pc25.dim("Cancelled")}
|
|
5908
6413
|
`);
|
|
5909
6414
|
return;
|
|
5910
6415
|
}
|
|
@@ -5918,8 +6423,63 @@ async function importCommand(args2) {
|
|
|
5918
6423
|
}
|
|
5919
6424
|
}
|
|
5920
6425
|
saveTracked(merged);
|
|
5921
|
-
console.error(` ${
|
|
6426
|
+
console.error(` ${pc25.green("\u2713")} ${pc25.bold(`Imported ${imported.length} process(es)`)} ${pc25.dim(`(${merged.length} total)`)}
|
|
6427
|
+
`);
|
|
6428
|
+
}
|
|
6429
|
+
|
|
6430
|
+
// src/commands/group.ts
|
|
6431
|
+
import pc26 from "picocolors";
|
|
6432
|
+
async function groupCommand(args2) {
|
|
6433
|
+
const positional = args2.filter((a) => !a.startsWith("-") && !a.includes("="));
|
|
6434
|
+
const unset = args2.includes("--unset");
|
|
6435
|
+
if (positional.length === 0) {
|
|
6436
|
+
const tracked = readTracked();
|
|
6437
|
+
if (tracked.length === 0) {
|
|
6438
|
+
console.error(`
|
|
6439
|
+
${pc26.dim("No tracked processes.")}
|
|
6440
|
+
`);
|
|
6441
|
+
return;
|
|
6442
|
+
}
|
|
6443
|
+
const columns = [
|
|
6444
|
+
{ key: "name", label: "App", format: (v) => pc26.bold(String(v)) },
|
|
6445
|
+
{ key: "group", label: "Group", format: (v) => {
|
|
6446
|
+
const g = String(v);
|
|
6447
|
+
return g === "-" ? pc26.dim("-") : pc26.cyan(g);
|
|
6448
|
+
} },
|
|
6449
|
+
{ key: "command", label: "Command", format: (v) => {
|
|
6450
|
+
const c = String(v);
|
|
6451
|
+
return c.length > 50 ? c.slice(0, 50) + "\u2026" : c;
|
|
6452
|
+
} }
|
|
6453
|
+
];
|
|
6454
|
+
const rows = tracked.map((t) => ({ name: t.name, group: t.group ?? "-", command: t.command }));
|
|
6455
|
+
console.error(`
|
|
6456
|
+
${pc26.bold("Fennec Apps by Group")} ${pc26.dim(`(${getGroups().length} group(s))`)}
|
|
5922
6457
|
`);
|
|
6458
|
+
console.error(renderTable(columns, rows));
|
|
6459
|
+
console.error(` ${pc26.dim("Assign:")} ${pc26.cyan("fennec group <name> <group>")} ${pc26.dim("Clear:")} ${pc26.cyan("fennec group <name> --unset")}`);
|
|
6460
|
+
console.error();
|
|
6461
|
+
return;
|
|
6462
|
+
}
|
|
6463
|
+
const name = positional[0];
|
|
6464
|
+
const newGroup = positional[1];
|
|
6465
|
+
if (!newGroup && !unset) {
|
|
6466
|
+
console.error(renderError("Missing group", "Usage: fennec group <name> <group> (or: fennec group <name> --unset)"));
|
|
6467
|
+
process.exit(1);
|
|
6468
|
+
}
|
|
6469
|
+
const ok = setGroup(name, unset ? void 0 : newGroup);
|
|
6470
|
+
if (!ok) {
|
|
6471
|
+
console.error(renderError("Not found", `No tracked process named "${name}".`));
|
|
6472
|
+
process.exit(1);
|
|
6473
|
+
}
|
|
6474
|
+
if (unset) {
|
|
6475
|
+
console.error(`
|
|
6476
|
+
${pc26.green("\u2713")} ${pc26.bold(name)} ${pc26.dim("removed from its group")}
|
|
6477
|
+
`);
|
|
6478
|
+
} else {
|
|
6479
|
+
console.error(`
|
|
6480
|
+
${pc26.green("\u2713")} ${pc26.bold(name)} ${pc26.dim("assigned to group")} ${pc26.cyan(newGroup)}
|
|
6481
|
+
`);
|
|
6482
|
+
}
|
|
5923
6483
|
}
|
|
5924
6484
|
|
|
5925
6485
|
// src/index.ts
|
|
@@ -5977,6 +6537,8 @@ async function main() {
|
|
|
5977
6537
|
await cleanupCommand();
|
|
5978
6538
|
} else if (command === "rename") {
|
|
5979
6539
|
await renameCommand(args);
|
|
6540
|
+
} else if (command === "group") {
|
|
6541
|
+
await groupCommand(args);
|
|
5980
6542
|
} else if (command === "export") {
|
|
5981
6543
|
await exportCommand(args);
|
|
5982
6544
|
} else if (command === "import") {
|
|
@@ -5991,8 +6553,14 @@ async function main() {
|
|
|
5991
6553
|
await attachPortCommand(args);
|
|
5992
6554
|
} else if (command === "watch") {
|
|
5993
6555
|
await watchCommand(args);
|
|
6556
|
+
} else if (command === "store") {
|
|
6557
|
+
printBanner();
|
|
6558
|
+
await storeCommand(args);
|
|
6559
|
+
} else if (command === "doctor") {
|
|
6560
|
+
printBanner();
|
|
6561
|
+
await doctorCommand();
|
|
5994
6562
|
} else if (command === "sessions") {
|
|
5995
|
-
await
|
|
6563
|
+
await storeCommand(["session"]);
|
|
5996
6564
|
} else if (command === "setup") {
|
|
5997
6565
|
await setupCommand();
|
|
5998
6566
|
} else if (command === "install-browsers") {
|