@plumpslabs/fennec-cli 1.10.0 → 1.11.1
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 +44 -1
- package/dist/index.js +1348 -108
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -1,54 +1,128 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { existsSync as
|
|
5
|
-
import { resolve as resolve2 } from "path";
|
|
6
|
-
import {
|
|
7
|
-
import { execSync } from "child_process";
|
|
8
|
-
import { FennecServer, SessionStore } from "@plumpslabs/fennec-core";
|
|
4
|
+
import { existsSync as existsSync3, writeFileSync, readFileSync as readFileSync2, mkdirSync } from "fs";
|
|
5
|
+
import { resolve as resolve2, dirname } from "path";
|
|
6
|
+
import { homedir } from "os";
|
|
7
|
+
import { execSync as execSync2 } from "child_process";
|
|
8
|
+
import { FennecServer, SessionStore, ProcessManager } from "@plumpslabs/fennec-core";
|
|
9
|
+
import pc4 from "picocolors";
|
|
9
10
|
|
|
10
11
|
// src/utils/banner.ts
|
|
12
|
+
import pc from "picocolors";
|
|
13
|
+
function hexColor(color) {
|
|
14
|
+
if (typeof pc.hex === "function") {
|
|
15
|
+
return pc.hex(color);
|
|
16
|
+
}
|
|
17
|
+
return (s) => s;
|
|
18
|
+
}
|
|
19
|
+
var fennecOrange = hexColor("#FF6432");
|
|
20
|
+
var fennecYellow = hexColor("#FFB347");
|
|
21
|
+
var FOX = `
|
|
22
|
+
${fennecOrange("/\\\\ /\\\\")}
|
|
23
|
+
${fennecOrange("(")} ${fennecYellow("o o")} ${fennecOrange(") ") + pc.bold("Fennec")} ${pc.dim("v1.11.1")}
|
|
24
|
+
${fennecOrange("=(")} ${fennecYellow(" Y ")} ${fennecOrange(")=")} ${pc.dim("ears everywhere in your stack.")}
|
|
25
|
+
${fennecOrange(") (")}
|
|
26
|
+
`;
|
|
27
|
+
var FOX_COMPACT = `${pc.redBright(" \u{1F98A}")} ${pc.bold("Fennec")} ${pc.dim("v1.11.1")}`;
|
|
11
28
|
function printBanner() {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
`);
|
|
29
|
+
if (process.stdout.isTTY) {
|
|
30
|
+
console.error(FOX);
|
|
31
|
+
} else {
|
|
32
|
+
console.error(FOX_COMPACT);
|
|
33
|
+
}
|
|
18
34
|
}
|
|
19
35
|
|
|
20
36
|
// src/utils/help.ts
|
|
37
|
+
import pc2 from "picocolors";
|
|
38
|
+
var fennecOrange2 = hexColor("#FF6432");
|
|
21
39
|
function showHelp() {
|
|
40
|
+
const sep = fennecOrange2("\u2500".repeat(50));
|
|
22
41
|
console.error(`
|
|
23
|
-
Usage: fennec <command> [options]
|
|
24
42
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
43
|
+
${pc2.bold("Usage:")} fennec ${pc2.dim("<command>")} ${pc2.dim("[options]")}
|
|
44
|
+
|
|
45
|
+
${sep}
|
|
46
|
+
${pc2.bold("Server")}
|
|
47
|
+
${sep}
|
|
48
|
+
|
|
49
|
+
${pc2.cyan("start")} ${pc2.dim("Start Fennec MCP server (default)")}
|
|
50
|
+
${pc2.dim("--config")} Path to config file
|
|
51
|
+
${pc2.dim("--transport")} Transport type ${pc2.dim("(stdio | sse) [default: stdio]")}
|
|
52
|
+
${pc2.dim("--api-port")} Management API port ${pc2.dim("[default: 3456]")}
|
|
53
|
+
|
|
54
|
+
${pc2.cyan("start")} ${pc2.dim("<command>")} ${pc2.dim("Start an app under Fennec observation (new!)")}
|
|
55
|
+
${pc2.dim("--name")} App name ${pc2.dim("(required for tracking)")}
|
|
56
|
+
${pc2.dim("--port")} Port the app listens on ${pc2.dim("(optional)")}
|
|
57
|
+
${pc2.dim("--cwd")} Working directory
|
|
58
|
+
${pc2.dim("--restart")} Auto-restart on crash
|
|
59
|
+
|
|
60
|
+
${sep}
|
|
61
|
+
${pc2.bold("Process Management")}
|
|
62
|
+
${sep}
|
|
63
|
+
|
|
64
|
+
${pc2.cyan("ps")} ${pc2.dim("[options]")} ${pc2.dim("List real system processes")}
|
|
65
|
+
${pc2.dim("--name")} Filter by process name
|
|
66
|
+
${pc2.dim("--port")} Filter by port
|
|
67
|
+
${pc2.dim("--sort")} Sort by ${pc2.dim("(cpu | mem | pid | name) [default: cpu]")}
|
|
68
|
+
${pc2.dim("-n")} Max results ${pc2.dim("[default: 30]")}
|
|
69
|
+
${pc2.dim("-w, --watch")} Watch mode ${pc2.dim("(refresh every 3s)")}
|
|
70
|
+
${pc2.dim("-a, --all")} Show all processes ${pc2.dim("(not just user)")}
|
|
71
|
+
|
|
72
|
+
${pc2.cyan("run")} ${pc2.dim("<command>")} ${pc2.dim("Run a command under Fennec observation")}
|
|
73
|
+
${pc2.dim("--name")} Process name ${pc2.dim("(required)")}
|
|
74
|
+
${pc2.dim("--cwd")} Working directory
|
|
75
|
+
${pc2.dim("--restart")} Auto-restart on crash
|
|
76
|
+
|
|
77
|
+
${pc2.cyan("status")} ${pc2.dim("[name]")} ${pc2.dim("Show system overview & top processes")}
|
|
78
|
+
${pc2.dim("-w, --watch")} Watch mode ${pc2.dim("(refresh every 3s)")}
|
|
79
|
+
|
|
80
|
+
${pc2.cyan("log")} ${pc2.dim("<pid|name>")} ${pc2.dim("Show logs for a process")}
|
|
81
|
+
${pc2.dim("--lines")} Number of lines ${pc2.dim("[default: 30]")}
|
|
82
|
+
${pc2.dim("-f, --follow")} Follow mode ${pc2.dim("(journalctl --follow)")}
|
|
83
|
+
|
|
84
|
+
${pc2.cyan("kill")} ${pc2.dim("<pid|name|all>")} ${pc2.dim("Kill process(es) by PID, name, or all")}
|
|
85
|
+
${pc2.dim("--signal")} Signal to send ${pc2.dim("[default: SIGTERM]")}
|
|
86
|
+
${pc2.dim("--all")} Kill all user processes
|
|
87
|
+
|
|
88
|
+
${pc2.cyan("restart")} ${pc2.dim("<pid|name>")} ${pc2.dim("Kill + show re-run command")}
|
|
89
|
+
|
|
90
|
+
${sep}
|
|
91
|
+
${pc2.bold("Observation")}
|
|
92
|
+
${sep}
|
|
93
|
+
|
|
94
|
+
${pc2.cyan("attach")} ${pc2.dim("<port>")} ${pc2.dim("Observe a process by port")}
|
|
95
|
+
${pc2.dim("--name")} Process name
|
|
30
96
|
|
|
31
|
-
|
|
32
|
-
|
|
97
|
+
${pc2.cyan("pipe")} ${pc2.dim("Pipe stdin to log watcher")}
|
|
98
|
+
${pc2.dim("--name")} Watcher name ${pc2.dim("(required)")}
|
|
33
99
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
100
|
+
${pc2.cyan("watch")} ${pc2.dim("Watch a log file")}
|
|
101
|
+
${pc2.dim("--file")} File path ${pc2.dim("(required)")}
|
|
102
|
+
${pc2.dim("--name")} Watcher name
|
|
37
103
|
|
|
38
|
-
|
|
39
|
-
<port>
|
|
40
|
-
--name Name for the process
|
|
104
|
+
${pc2.cyan("attach-pid")} ${pc2.dim("<pid>")} ${pc2.dim("Attach to process by PID")}
|
|
105
|
+
${pc2.cyan("attach-port")} ${pc2.dim("<port>")} ${pc2.dim("Attach to process by port")}
|
|
41
106
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
107
|
+
${sep}
|
|
108
|
+
${pc2.bold("Configuration")}
|
|
109
|
+
${sep}
|
|
45
110
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
111
|
+
${pc2.cyan("init")} ${pc2.dim("Generate fennec.config.yaml")}
|
|
112
|
+
${pc2.cyan("setup")} ${pc2.dim("Configure MCP client")}
|
|
113
|
+
${pc2.cyan("install-browsers")} ${pc2.dim("Install Playwright browser engines")}
|
|
114
|
+
${pc2.cyan("sessions")} ${pc2.dim("List saved auth sessions")}
|
|
50
115
|
|
|
51
|
-
|
|
116
|
+
${sep}
|
|
117
|
+
${pc2.bold("Other")}
|
|
118
|
+
${sep}
|
|
119
|
+
|
|
120
|
+
${pc2.cyan("help")} ${pc2.dim("Show this help message")}
|
|
121
|
+
${pc2.cyan("version")} ${pc2.dim("Show version")}
|
|
122
|
+
|
|
123
|
+
${pc2.dim("\u2500".repeat(50))}
|
|
124
|
+
|
|
125
|
+
${pc2.dim("Learn more:")} ${pc2.cyan("https://github.com/plumpslabs/fennec")}
|
|
52
126
|
`);
|
|
53
127
|
}
|
|
54
128
|
|
|
@@ -159,62 +233,614 @@ async function watchCommand(args2) {
|
|
|
159
233
|
process.stdin.resume();
|
|
160
234
|
}
|
|
161
235
|
|
|
162
|
-
// src/
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
236
|
+
// src/utils/format.ts
|
|
237
|
+
import { createInterface } from "readline";
|
|
238
|
+
import pc3 from "picocolors";
|
|
239
|
+
function hex(color) {
|
|
240
|
+
if (typeof pc3.hex === "function") {
|
|
241
|
+
return pc3.hex(color);
|
|
242
|
+
}
|
|
243
|
+
return (s) => s;
|
|
244
|
+
}
|
|
245
|
+
var fennecOrange3 = hex("#FF6432");
|
|
246
|
+
var blueAccent = hex("#4A90D9");
|
|
247
|
+
var redAccent = hex("#E94560");
|
|
248
|
+
var colors = {
|
|
249
|
+
// Brand colors
|
|
250
|
+
primary: fennecOrange3,
|
|
251
|
+
secondary: blueAccent,
|
|
252
|
+
accent: redAccent,
|
|
253
|
+
// Semantic colors
|
|
254
|
+
success: pc3.green,
|
|
255
|
+
error: pc3.red,
|
|
256
|
+
warning: pc3.yellow,
|
|
257
|
+
info: pc3.cyan,
|
|
258
|
+
muted: pc3.dim,
|
|
259
|
+
highlight: pc3.bold,
|
|
260
|
+
// Backgrounds
|
|
261
|
+
bgSuccess: (s) => pc3.bgGreen(pc3.black(` ${s} `)),
|
|
262
|
+
bgError: (s) => pc3.bgRed(pc3.white(` ${s} `)),
|
|
263
|
+
bgWarning: (s) => pc3.bgYellow(pc3.black(` ${s} `)),
|
|
264
|
+
bgInfo: (s) => pc3.bgCyan(pc3.black(` ${s} `))
|
|
265
|
+
};
|
|
266
|
+
var symbols = {
|
|
267
|
+
// Status
|
|
268
|
+
active: pc3.green("\u25CF"),
|
|
269
|
+
inactive: pc3.dim("\u25CB"),
|
|
270
|
+
error: pc3.red("\u2717"),
|
|
271
|
+
warning: pc3.yellow("\u26A0"),
|
|
272
|
+
success: pc3.green("\u2713"),
|
|
273
|
+
info: pc3.cyan("\u2139"),
|
|
274
|
+
pending: pc3.yellow("\u25CC"),
|
|
275
|
+
// UI
|
|
276
|
+
bullet: pc3.dim("\u2502"),
|
|
277
|
+
dot: pc3.dim("\xB7"),
|
|
278
|
+
arrow: pc3.cyan("\u2192"),
|
|
279
|
+
pointer: fennecOrange3("\u25B6"),
|
|
280
|
+
separator: pc3.dim("\u2500"),
|
|
281
|
+
// Fennec
|
|
282
|
+
fox: "\u{1F98A}",
|
|
283
|
+
ears: "\u23DC"
|
|
284
|
+
};
|
|
285
|
+
function logLevel(label) {
|
|
286
|
+
const upper = label.toUpperCase().padEnd(5);
|
|
287
|
+
switch (label.toLowerCase()) {
|
|
288
|
+
case "error":
|
|
289
|
+
return pc3.bgRed(pc3.white(` ${upper} `));
|
|
290
|
+
case "warn":
|
|
291
|
+
return pc3.bgYellow(pc3.black(` ${upper} `));
|
|
292
|
+
case "info":
|
|
293
|
+
return pc3.bgCyan(pc3.black(` ${upper} `));
|
|
294
|
+
case "debug":
|
|
295
|
+
return pc3.dim(pc3.italic(upper));
|
|
296
|
+
case "verbose":
|
|
297
|
+
return pc3.dim(pc3.italic(upper));
|
|
298
|
+
default:
|
|
299
|
+
return pc3.dim(upper);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
function renderTable(columns, rows, options) {
|
|
303
|
+
const pad = options?.padding ?? 1;
|
|
304
|
+
const isCompact = options?.compact ?? false;
|
|
305
|
+
if (rows.length === 0) {
|
|
306
|
+
return pc3.dim(" (no data)");
|
|
307
|
+
}
|
|
308
|
+
const widths = columns.map((col) => {
|
|
309
|
+
const headerLen = col.label.length;
|
|
310
|
+
const maxDataLen = rows.reduce((max, row) => {
|
|
311
|
+
const val = String(row[col.key] ?? "-");
|
|
312
|
+
return Math.max(max, val.length);
|
|
313
|
+
}, 0);
|
|
314
|
+
return Math.max(headerLen, maxDataLen, col.width ?? 0) + pad * 2;
|
|
315
|
+
});
|
|
316
|
+
const hLine = (left, mid, right, fill = "\u2500") => {
|
|
317
|
+
return pc3.dim(
|
|
318
|
+
left + widths.map((w) => fill.repeat(w)).join(mid) + right
|
|
319
|
+
);
|
|
320
|
+
};
|
|
321
|
+
const formatCell = (text, colIdx) => {
|
|
322
|
+
const col = columns[colIdx];
|
|
323
|
+
const w = widths[colIdx];
|
|
324
|
+
const display = text.length > w ? text.slice(0, w - 1) + "\u2026" : text;
|
|
325
|
+
const padded = display.padEnd(w);
|
|
326
|
+
const align = col.align ?? "left";
|
|
327
|
+
if (align === "right") return padded.padStart(w);
|
|
328
|
+
if (align === "center") {
|
|
329
|
+
const leftPad = Math.floor((w - display.length) / 2);
|
|
330
|
+
return " ".repeat(leftPad) + display + " ".repeat(w - display.length - leftPad);
|
|
331
|
+
}
|
|
332
|
+
return padded;
|
|
333
|
+
};
|
|
334
|
+
const renderRow = (row) => {
|
|
335
|
+
const cells = columns.map((col, i) => {
|
|
336
|
+
const raw = row[col.key];
|
|
337
|
+
const formatted = col.format ? col.format(raw) : String(raw ?? "-");
|
|
338
|
+
return formatCell(formatted, i);
|
|
170
339
|
});
|
|
340
|
+
return pc3.dim("\u2502") + cells.join(pc3.dim("\u2502")) + pc3.dim("\u2502");
|
|
341
|
+
};
|
|
342
|
+
const lines = [];
|
|
343
|
+
if (!isCompact) {
|
|
344
|
+
lines.push(hLine("\u250C", "\u252C", "\u2510"));
|
|
345
|
+
}
|
|
346
|
+
const headerCells = columns.map((col, i) => {
|
|
347
|
+
return formatCell(pc3.bold(col.label), i);
|
|
171
348
|
});
|
|
349
|
+
lines.push(pc3.dim("\u2502") + headerCells.join(pc3.dim("\u2502")) + pc3.dim("\u2502"));
|
|
350
|
+
if (!isCompact) {
|
|
351
|
+
lines.push(hLine("\u251C", "\u253C", "\u2524"));
|
|
352
|
+
} else {
|
|
353
|
+
lines.push(pc3.dim("\u251C") + widths.map((w) => pc3.dim("\u2500").repeat(w)).join(pc3.dim("\u253C")) + pc3.dim("\u2524"));
|
|
354
|
+
}
|
|
355
|
+
for (const row of rows) {
|
|
356
|
+
const rendered = renderRow(row);
|
|
357
|
+
if (row._error) {
|
|
358
|
+
lines.push(colors.error(rendered));
|
|
359
|
+
} else {
|
|
360
|
+
lines.push(rendered);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
if (!isCompact) {
|
|
364
|
+
lines.push(hLine("\u2514", "\u2534", "\u2518"));
|
|
365
|
+
}
|
|
366
|
+
return lines.join("\n");
|
|
172
367
|
}
|
|
173
|
-
|
|
174
|
-
const
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
368
|
+
function renderKV(key, value, options) {
|
|
369
|
+
const indent = options?.indent ?? 2;
|
|
370
|
+
const sep = options?.separator ?? ":";
|
|
371
|
+
const pad = " ".repeat(indent);
|
|
372
|
+
return `${pad}${pc3.dim(key + sep)} ${value}`;
|
|
373
|
+
}
|
|
374
|
+
function createSpinner(text) {
|
|
375
|
+
const frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
376
|
+
let i = 0;
|
|
377
|
+
let running = true;
|
|
378
|
+
const interval = setInterval(() => {
|
|
379
|
+
if (!running) return;
|
|
380
|
+
process.stdout.write(`\r${pc3.cyan(frames[i])} ${text}`);
|
|
381
|
+
i = (i + 1) % frames.length;
|
|
382
|
+
}, 80);
|
|
383
|
+
return {
|
|
384
|
+
update(newText) {
|
|
385
|
+
if (!running) return;
|
|
386
|
+
process.stdout.write(`\r${pc3.cyan(frames[i])} ${newText}`);
|
|
387
|
+
},
|
|
388
|
+
succeed(msg) {
|
|
389
|
+
running = false;
|
|
390
|
+
clearInterval(interval);
|
|
391
|
+
process.stdout.write(`\r${pc3.green("\u2713")} ${msg}
|
|
392
|
+
`);
|
|
393
|
+
},
|
|
394
|
+
fail(msg) {
|
|
395
|
+
running = false;
|
|
396
|
+
clearInterval(interval);
|
|
397
|
+
process.stdout.write(`\r${pc3.red("\u2717")} ${msg}
|
|
398
|
+
`);
|
|
399
|
+
},
|
|
400
|
+
warn(msg) {
|
|
401
|
+
running = false;
|
|
402
|
+
clearInterval(interval);
|
|
403
|
+
process.stdout.write(`\r${pc3.yellow("\u26A0")} ${msg}
|
|
404
|
+
`);
|
|
405
|
+
},
|
|
406
|
+
stop() {
|
|
407
|
+
running = false;
|
|
408
|
+
clearInterval(interval);
|
|
409
|
+
}
|
|
410
|
+
};
|
|
179
411
|
}
|
|
180
412
|
async function selectPrompt(message, options) {
|
|
181
413
|
console.log(`
|
|
182
|
-
${message}
|
|
414
|
+
${pc3.bold(message)}
|
|
183
415
|
`);
|
|
184
416
|
options.forEach((opt, i) => {
|
|
185
|
-
|
|
417
|
+
const num2 = pc3.cyan(` ${i + 1}`);
|
|
418
|
+
console.log(`${num2} ${opt.label}`);
|
|
419
|
+
if (opt.description) {
|
|
420
|
+
console.log(` ${pc3.dim(opt.description)}`);
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
console.log(` ${pc3.dim("0) Cancel")}
|
|
424
|
+
`);
|
|
425
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
426
|
+
const answer = await new Promise((resolve3) => {
|
|
427
|
+
rl.question(` ${pc3.bold("Enter number")} ${pc3.dim("(0-" + options.length + ")")}: `, (ans) => {
|
|
428
|
+
rl.close();
|
|
429
|
+
resolve3(ans.trim());
|
|
430
|
+
});
|
|
186
431
|
});
|
|
187
|
-
console.log(` 0) Cancel`);
|
|
188
|
-
const answer = await rlQuestion(`
|
|
189
|
-
Enter number (0-${options.length}): `);
|
|
190
432
|
const num = parseInt(answer, 10);
|
|
191
|
-
if (isNaN(num) || num === 0) return
|
|
433
|
+
if (isNaN(num) || num === 0) return null;
|
|
192
434
|
const selected = options[num - 1];
|
|
193
435
|
if (!selected) {
|
|
194
|
-
console.log("
|
|
195
|
-
return
|
|
436
|
+
console.log(` ${pc3.red("Invalid selection.")}`);
|
|
437
|
+
return null;
|
|
196
438
|
}
|
|
197
439
|
return selected.value;
|
|
198
440
|
}
|
|
199
|
-
function
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
441
|
+
async function confirmPrompt(message, defaultValue = false) {
|
|
442
|
+
const hint = defaultValue ? pc3.bold("Y") + pc3.dim("/n") : pc3.dim("y/") + pc3.bold("N");
|
|
443
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
444
|
+
const answer = await new Promise((resolve3) => {
|
|
445
|
+
rl.question(`
|
|
446
|
+
${message} ${pc3.dim("(" + hint + ")")}: `, (ans) => {
|
|
447
|
+
rl.close();
|
|
448
|
+
resolve3(ans.trim().toLowerCase());
|
|
449
|
+
});
|
|
450
|
+
});
|
|
451
|
+
if (!answer) return defaultValue;
|
|
452
|
+
return answer === "y" || answer === "yes";
|
|
453
|
+
}
|
|
454
|
+
function divider(text) {
|
|
455
|
+
if (text) {
|
|
456
|
+
const len = 50 - text.length - 2;
|
|
457
|
+
const left = Math.floor(len / 2);
|
|
458
|
+
const right = len - left;
|
|
459
|
+
return pc3.dim("\u2500".repeat(left) + ` ${text} ` + "\u2500".repeat(right));
|
|
460
|
+
}
|
|
461
|
+
return pc3.dim("\u2500".repeat(50));
|
|
462
|
+
}
|
|
463
|
+
function timestamp(date) {
|
|
464
|
+
const d = date ?? /* @__PURE__ */ new Date();
|
|
465
|
+
const time = d.toLocaleTimeString("en-US", { hour12: false });
|
|
466
|
+
return pc3.dim(`[${time}]`);
|
|
467
|
+
}
|
|
468
|
+
function renderCommand(cmd) {
|
|
469
|
+
return fennecOrange3(`$ ${cmd}`);
|
|
470
|
+
}
|
|
471
|
+
function renderAppName(name) {
|
|
472
|
+
return fennecOrange3(pc3.bold(name));
|
|
473
|
+
}
|
|
474
|
+
function renderError(title, details, suggestions) {
|
|
475
|
+
const lines = [];
|
|
476
|
+
lines.push(`
|
|
477
|
+
${pc3.bgRed(pc3.white(" ERROR "))} ${pc3.bold(title)}`);
|
|
478
|
+
if (details) {
|
|
479
|
+
lines.push(` ${pc3.dim(details)}`);
|
|
480
|
+
}
|
|
481
|
+
if (suggestions && suggestions.length > 0) {
|
|
482
|
+
lines.push(`
|
|
483
|
+
${pc3.bold("Suggestions:")}`);
|
|
484
|
+
for (const s of suggestions) {
|
|
485
|
+
lines.push(` ${pc3.cyan("\u2192")} ${s}`);
|
|
211
486
|
}
|
|
487
|
+
}
|
|
488
|
+
return lines.join("\n") + "\n";
|
|
489
|
+
}
|
|
490
|
+
function renderSuccess(message) {
|
|
491
|
+
return ` ${pc3.green("\u2713")} ${message}`;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// src/utils/system-process.ts
|
|
495
|
+
import { readdirSync, readFileSync, existsSync as existsSync2 } from "fs";
|
|
496
|
+
import { execSync } from "child_process";
|
|
497
|
+
import { cpus } from "os";
|
|
498
|
+
function readProcProcesses() {
|
|
499
|
+
const processes = [];
|
|
500
|
+
const currentUid = process.getuid?.();
|
|
501
|
+
const totalMemKb = readTotalMem();
|
|
502
|
+
try {
|
|
503
|
+
const pids = readdirSync("/proc").filter((d) => /^\d+$/.test(d));
|
|
504
|
+
for (const pidStr of pids) {
|
|
505
|
+
try {
|
|
506
|
+
const pid = parseInt(pidStr, 10);
|
|
507
|
+
const proc = readSingleProcProcess(pid, currentUid, totalMemKb);
|
|
508
|
+
if (proc) processes.push(proc);
|
|
509
|
+
} catch {
|
|
510
|
+
continue;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
} catch {
|
|
514
|
+
return [];
|
|
515
|
+
}
|
|
516
|
+
return processes;
|
|
517
|
+
}
|
|
518
|
+
function readSingleProcProcess(pid, currentUid, totalMemKb) {
|
|
519
|
+
const statPath = `/proc/${pid}/stat`;
|
|
520
|
+
const statusPath = `/proc/${pid}/status`;
|
|
521
|
+
const cmdlinePath = `/proc/${pid}/cmdline`;
|
|
522
|
+
if (!existsSync2(statPath)) return null;
|
|
523
|
+
const statusText = readSafe(statusPath);
|
|
524
|
+
if (!statusText) return null;
|
|
525
|
+
const nameMatch = statusText.match(/Name:\s+(.+)/);
|
|
526
|
+
const uidMatch = statusText.match(/Uid:\s+(\d+)/);
|
|
527
|
+
const vmRssMatch = statusText.match(/VmRSS:\s+(\d+)/);
|
|
528
|
+
const stateMatch = statusText.match(/State:\s+(\S)/);
|
|
529
|
+
const name = nameMatch?.[1] ?? `pid_${pid}`;
|
|
530
|
+
const uid = uidMatch ? parseInt(uidMatch[1], 10) : void 0;
|
|
531
|
+
const vmRss = vmRssMatch ? parseInt(vmRssMatch[1], 10) : 0;
|
|
532
|
+
const state = stateMatch?.[1] ?? "?";
|
|
533
|
+
const isUserProcess = uid !== void 0 && currentUid !== void 0 && uid === currentUid;
|
|
534
|
+
const statText = readSafe(statPath);
|
|
535
|
+
let cpuPercent = 0;
|
|
536
|
+
let startedAt = null;
|
|
537
|
+
if (statText) {
|
|
538
|
+
const parenEnd = statText.lastIndexOf(")");
|
|
539
|
+
if (parenEnd > 0) {
|
|
540
|
+
const fields = statText.slice(parenEnd + 2).split(/\s+/);
|
|
541
|
+
const utime = parseInt(fields[11] ?? "0", 10);
|
|
542
|
+
const stime = parseInt(fields[12] ?? "0", 10);
|
|
543
|
+
const starttime = parseInt(fields[19] ?? "0", 10);
|
|
544
|
+
const hertz = 100;
|
|
545
|
+
const uptimeSeconds = readUptime();
|
|
546
|
+
const totalJiffies = utime + stime;
|
|
547
|
+
if (uptimeSeconds > 0) {
|
|
548
|
+
const elapsedJiffies = uptimeSeconds * hertz;
|
|
549
|
+
const processJiffies = totalJiffies;
|
|
550
|
+
cpuPercent = Math.min(100, processJiffies / Math.max(1, elapsedJiffies) * 100 * cpus().length);
|
|
551
|
+
}
|
|
552
|
+
startedAt = formatStartTime(starttime, uptimeSeconds, hertz);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
const cmdlineRaw = readSafe(cmdlinePath);
|
|
556
|
+
const command2 = cmdlineRaw ? cmdlineRaw.split("\0").filter(Boolean).join(" ") : name;
|
|
557
|
+
const ports = detectListeningPorts(pid);
|
|
558
|
+
const memPercent = totalMemKb > 0 ? vmRss / totalMemKb * 100 : 0;
|
|
559
|
+
return {
|
|
560
|
+
pid,
|
|
561
|
+
name,
|
|
562
|
+
command: command2.length > 200 ? command2.slice(0, 200) + "\u2026" : command2,
|
|
563
|
+
cpuPercent: Math.round(cpuPercent * 10) / 10,
|
|
564
|
+
memPercent: Math.round(memPercent * 10) / 10,
|
|
565
|
+
memRss: vmRss,
|
|
566
|
+
state,
|
|
567
|
+
startedAt,
|
|
568
|
+
ports,
|
|
569
|
+
isUserProcess
|
|
212
570
|
};
|
|
213
571
|
}
|
|
214
|
-
|
|
572
|
+
function runPsProcesses() {
|
|
573
|
+
try {
|
|
574
|
+
const output = execSync(
|
|
575
|
+
"ps axo pid,comm,pcpu,pmem,rss,state,etime,user --sort=-pcpu 2>/dev/null | head -200",
|
|
576
|
+
{ encoding: "utf-8", timeout: 5e3 }
|
|
577
|
+
);
|
|
578
|
+
const lines = output.trim().split("\n").slice(1);
|
|
579
|
+
const currentUser = execSync("whoami", { encoding: "utf-8", timeout: 1e3 }).trim();
|
|
580
|
+
return lines.map((line) => {
|
|
581
|
+
const parts = line.trim().split(/\s+/);
|
|
582
|
+
const pid = parseInt(parts[0] ?? "0", 10);
|
|
583
|
+
const name = parts[1] ?? "";
|
|
584
|
+
const cpu = parseFloat(parts[2] ?? "0");
|
|
585
|
+
const mem = parseFloat(parts[3] ?? "0");
|
|
586
|
+
const rss = parseInt(parts[4] ?? "0", 10);
|
|
587
|
+
const state = parts[5] ?? "?";
|
|
588
|
+
const elapsed = parts[6] ?? "";
|
|
589
|
+
const user = parts[7] ?? "";
|
|
590
|
+
const command2 = parts.slice(1).join(" ");
|
|
591
|
+
return {
|
|
592
|
+
pid,
|
|
593
|
+
name,
|
|
594
|
+
command: command2.length > 200 ? command2.slice(0, 200) + "\u2026" : command2,
|
|
595
|
+
cpuPercent: Math.round(cpu * 10) / 10,
|
|
596
|
+
memPercent: Math.round(mem * 10) / 10,
|
|
597
|
+
memRss: rss,
|
|
598
|
+
state: mapBsdState(state),
|
|
599
|
+
startedAt: elapsed || null,
|
|
600
|
+
ports: [],
|
|
601
|
+
isUserProcess: user === currentUser
|
|
602
|
+
};
|
|
603
|
+
});
|
|
604
|
+
} catch {
|
|
605
|
+
return [];
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
function mapBsdState(s) {
|
|
609
|
+
switch (s) {
|
|
610
|
+
case "R":
|
|
611
|
+
return "R";
|
|
612
|
+
case "S":
|
|
613
|
+
return "S";
|
|
614
|
+
case "Z":
|
|
615
|
+
return "Z";
|
|
616
|
+
case "T":
|
|
617
|
+
return "T";
|
|
618
|
+
case "I":
|
|
619
|
+
return "S";
|
|
620
|
+
default:
|
|
621
|
+
return s;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
function detectListeningPorts(pid) {
|
|
625
|
+
try {
|
|
626
|
+
const tcpFile = `/proc/${pid}/net/tcp`;
|
|
627
|
+
const tcp6File = `/proc/${pid}/net/tcp6`;
|
|
628
|
+
const ports = [];
|
|
629
|
+
for (const f of [tcpFile, tcp6File]) {
|
|
630
|
+
try {
|
|
631
|
+
const data = readSafe(f);
|
|
632
|
+
if (data) {
|
|
633
|
+
for (const port of parseProcNetTcp(data)) {
|
|
634
|
+
if (!ports.includes(port)) ports.push(port);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
} catch {
|
|
638
|
+
continue;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
return ports.sort((a, b) => a - b);
|
|
642
|
+
} catch {
|
|
643
|
+
return [];
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
function parseProcNetTcp(data) {
|
|
647
|
+
const ports = [];
|
|
648
|
+
const lines = data.split("\n").slice(1);
|
|
649
|
+
for (const line of lines) {
|
|
650
|
+
const parts = line.trim().split(/\s+/);
|
|
651
|
+
if (parts.length < 3) continue;
|
|
652
|
+
const localAddr = parts[1] ?? "";
|
|
653
|
+
const state = parts[3] ?? "";
|
|
654
|
+
if (state !== "0A") continue;
|
|
655
|
+
const portMatch = localAddr.match(/:([0-9a-fA-F]+)$/);
|
|
656
|
+
if (portMatch) {
|
|
657
|
+
const port = parseInt(portMatch[1], 16);
|
|
658
|
+
if (port > 0 && port <= 65535) {
|
|
659
|
+
ports.push(port);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
return ports;
|
|
664
|
+
}
|
|
665
|
+
function readSafe(path) {
|
|
666
|
+
try {
|
|
667
|
+
return readFileSync(path, "utf-8");
|
|
668
|
+
} catch {
|
|
669
|
+
return null;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
function readTotalMem() {
|
|
673
|
+
try {
|
|
674
|
+
const memInfo = readSafe("/proc/meminfo");
|
|
675
|
+
if (memInfo) {
|
|
676
|
+
const match = memInfo.match(/MemTotal:\s+(\d+)/);
|
|
677
|
+
if (match) return parseInt(match[1], 10);
|
|
678
|
+
}
|
|
679
|
+
} catch {
|
|
680
|
+
}
|
|
681
|
+
return 16e6;
|
|
682
|
+
}
|
|
683
|
+
function readUptime() {
|
|
684
|
+
try {
|
|
685
|
+
const data = readSafe("/proc/uptime");
|
|
686
|
+
if (data) {
|
|
687
|
+
return parseFloat(data.split(/\s+/)[0] ?? "0");
|
|
688
|
+
}
|
|
689
|
+
} catch {
|
|
690
|
+
}
|
|
691
|
+
return 0;
|
|
692
|
+
}
|
|
693
|
+
function formatStartTime(starttime, uptimeSeconds, hertz) {
|
|
694
|
+
try {
|
|
695
|
+
const bootTime = Date.now() / 1e3 - uptimeSeconds;
|
|
696
|
+
const processStart = bootTime + starttime / hertz;
|
|
697
|
+
return new Date(processStart * 1e3).toISOString();
|
|
698
|
+
} catch {
|
|
699
|
+
return null;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
function getSystemProcesses(filter) {
|
|
703
|
+
let processes;
|
|
704
|
+
if (existsSync2("/proc")) {
|
|
705
|
+
processes = readProcProcesses();
|
|
706
|
+
} else {
|
|
707
|
+
processes = runPsProcesses();
|
|
708
|
+
}
|
|
709
|
+
if (filter) {
|
|
710
|
+
if (filter.userOnly) {
|
|
711
|
+
processes = processes.filter((p) => p.isUserProcess);
|
|
712
|
+
}
|
|
713
|
+
if (filter.name) {
|
|
714
|
+
const nameLower = filter.name.toLowerCase();
|
|
715
|
+
processes = processes.filter(
|
|
716
|
+
(p) => p.name.toLowerCase().includes(nameLower) || p.command.toLowerCase().includes(nameLower)
|
|
717
|
+
);
|
|
718
|
+
}
|
|
719
|
+
if (filter.pid) {
|
|
720
|
+
processes = processes.filter((p) => p.pid === filter.pid);
|
|
721
|
+
}
|
|
722
|
+
if (filter.port) {
|
|
723
|
+
processes = processes.filter((p) => p.ports.includes(filter.port));
|
|
724
|
+
}
|
|
725
|
+
if (filter.sortBy) {
|
|
726
|
+
processes.sort((a, b) => {
|
|
727
|
+
switch (filter.sortBy) {
|
|
728
|
+
case "cpu":
|
|
729
|
+
return b.cpuPercent - a.cpuPercent;
|
|
730
|
+
case "mem":
|
|
731
|
+
return b.memPercent - a.memPercent;
|
|
732
|
+
case "pid":
|
|
733
|
+
return a.pid - b.pid;
|
|
734
|
+
case "name":
|
|
735
|
+
return a.name.localeCompare(b.name);
|
|
736
|
+
default:
|
|
737
|
+
return b.cpuPercent - a.cpuPercent;
|
|
738
|
+
}
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
if (filter.limit && filter.limit > 0) {
|
|
742
|
+
processes = processes.slice(0, filter.limit);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
return processes;
|
|
746
|
+
}
|
|
747
|
+
function killProcess(pid, signal = "SIGTERM") {
|
|
748
|
+
try {
|
|
749
|
+
process.kill(pid, signal);
|
|
750
|
+
return true;
|
|
751
|
+
} catch {
|
|
752
|
+
return false;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
function isProcessRunning(pid) {
|
|
756
|
+
try {
|
|
757
|
+
process.kill(pid, 0);
|
|
758
|
+
return true;
|
|
759
|
+
} catch {
|
|
760
|
+
return false;
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
function formatProcessState(state) {
|
|
764
|
+
switch (state) {
|
|
765
|
+
case "R":
|
|
766
|
+
return "Running";
|
|
767
|
+
case "S":
|
|
768
|
+
return "Sleeping";
|
|
769
|
+
case "D":
|
|
770
|
+
return "Disk Sleep";
|
|
771
|
+
case "Z":
|
|
772
|
+
return "Zombie";
|
|
773
|
+
case "T":
|
|
774
|
+
return "Stopped";
|
|
775
|
+
case "t":
|
|
776
|
+
return "Tracing";
|
|
777
|
+
case "X":
|
|
778
|
+
return "Dead";
|
|
779
|
+
case "I":
|
|
780
|
+
return "Idle";
|
|
781
|
+
default:
|
|
782
|
+
return state;
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// src/index.ts
|
|
787
|
+
var [, , command, ...args] = process.argv;
|
|
788
|
+
function getTrackedPath() {
|
|
789
|
+
const dir = process.env.FENNEC_DATA_DIR ? resolve2(process.env.FENNEC_DATA_DIR) : resolve2(homedir(), ".fennec");
|
|
790
|
+
return resolve2(dir, "tracked.json");
|
|
791
|
+
}
|
|
792
|
+
function readTracked() {
|
|
793
|
+
try {
|
|
794
|
+
const path = getTrackedPath();
|
|
795
|
+
if (!existsSync3(path)) return [];
|
|
796
|
+
return JSON.parse(readFileSync2(path, "utf-8"));
|
|
797
|
+
} catch {
|
|
798
|
+
return [];
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
function saveTracked(processes) {
|
|
802
|
+
try {
|
|
803
|
+
const path = getTrackedPath();
|
|
804
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
805
|
+
writeFileSync(path, JSON.stringify(processes, null, 2), "utf-8");
|
|
806
|
+
} catch {
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
function addTracked(proc) {
|
|
810
|
+
const tracked = readTracked();
|
|
811
|
+
const filtered = tracked.filter((t) => t.name !== proc.name);
|
|
812
|
+
filtered.push(proc);
|
|
813
|
+
saveTracked(filtered);
|
|
814
|
+
}
|
|
815
|
+
function removeTracked(name) {
|
|
816
|
+
const tracked = readTracked();
|
|
817
|
+
saveTracked(tracked.filter((t) => t.name !== name));
|
|
818
|
+
}
|
|
819
|
+
function removeTrackedByPid(pid) {
|
|
820
|
+
const tracked = readTracked();
|
|
821
|
+
saveTracked(tracked.filter((t) => t.pid !== pid));
|
|
822
|
+
}
|
|
215
823
|
async function main() {
|
|
216
824
|
if (!command || command === "start") {
|
|
217
|
-
|
|
825
|
+
if (args.length === 0 || args[0]?.startsWith("--")) {
|
|
826
|
+
await startServer(args);
|
|
827
|
+
} else {
|
|
828
|
+
await startCommand(args);
|
|
829
|
+
}
|
|
830
|
+
} else if (command === "run") {
|
|
831
|
+
await runCommand(args);
|
|
832
|
+
} else if (command === "ps") {
|
|
833
|
+
await psCommand(args);
|
|
834
|
+
} else if (command === "status") {
|
|
835
|
+
await statusCommand(args);
|
|
836
|
+
} else if (command === "log") {
|
|
837
|
+
await logCommand(args);
|
|
838
|
+
} else if (command === "kill") {
|
|
839
|
+
await killCommand(args);
|
|
840
|
+
} else if (command === "restart") {
|
|
841
|
+
await restartCommand(args);
|
|
842
|
+
} else if (command === "attach") {
|
|
843
|
+
await attachCommand(args);
|
|
218
844
|
} else if (command === "pipe") {
|
|
219
845
|
await pipeCommand(args);
|
|
220
846
|
} else if (command === "attach-pid") {
|
|
@@ -233,89 +859,701 @@ async function main() {
|
|
|
233
859
|
} else if (command === "init") {
|
|
234
860
|
printBanner();
|
|
235
861
|
await initCommand();
|
|
862
|
+
} else if (command === "version" || command === "--version" || command === "-v") {
|
|
863
|
+
console.log(` ${symbols.fox} ${pc4.bold("Fennec")} ${pc4.dim("v1.11.1")}`);
|
|
236
864
|
} else if (command === "help" || command === "--help" || command === "-h") {
|
|
865
|
+
console.log(pc4.dim("\n Use 'fennec help' for more information.\n"));
|
|
237
866
|
printBanner();
|
|
238
867
|
showHelp();
|
|
239
868
|
} else {
|
|
240
|
-
console.error(`Unknown command: ${command}
|
|
241
|
-
console.error("Run 'fennec help' for usage information");
|
|
869
|
+
console.error(renderError(`Unknown command: ${command}`, "Run 'fennec help' for usage information"));
|
|
242
870
|
process.exit(1);
|
|
243
871
|
}
|
|
244
872
|
}
|
|
245
873
|
async function startServer(args2) {
|
|
874
|
+
printBanner();
|
|
875
|
+
console.error(` ${pc4.dim("Starting Fennec MCP server...")}
|
|
876
|
+
`);
|
|
246
877
|
const configIndex = args2.indexOf("--config");
|
|
247
878
|
const configPath = configIndex !== -1 ? args2[configIndex + 1] : void 0;
|
|
248
|
-
const
|
|
249
|
-
|
|
879
|
+
const apiPortIndex = args2.indexOf("--api-port");
|
|
880
|
+
const apiPort = apiPortIndex !== -1 ? parseInt(args2[apiPortIndex + 1], 10) : 3456;
|
|
881
|
+
try {
|
|
882
|
+
const server = new FennecServer(configPath);
|
|
883
|
+
await server.start();
|
|
884
|
+
console.error(`
|
|
885
|
+
${pc4.green("\u2713")} ${pc4.bold("Fennec server is running")}`);
|
|
886
|
+
console.error(` ${renderKV("Transport", "stdio")}`);
|
|
887
|
+
console.error(` ${renderKV("Management API", `http://localhost:${apiPort}`)}`);
|
|
888
|
+
console.error(` ${renderKV("AI Agent", "Connect via MCP protocol")}`);
|
|
889
|
+
console.error(`
|
|
890
|
+
${pc4.dim("Press Ctrl+C to stop")}
|
|
891
|
+
`);
|
|
892
|
+
} catch (error) {
|
|
893
|
+
console.error(renderError("Failed to start server", String(error)));
|
|
894
|
+
process.exit(1);
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
async function startCommand(args2) {
|
|
898
|
+
printBanner();
|
|
899
|
+
console.error(` ${pc4.dim("Starting app...")}
|
|
900
|
+
`);
|
|
901
|
+
await runCommand(args2);
|
|
902
|
+
}
|
|
903
|
+
async function runCommand(args2) {
|
|
904
|
+
const nameIndex = args2.indexOf("--name");
|
|
905
|
+
const name = nameIndex !== -1 ? args2[nameIndex + 1] : void 0;
|
|
906
|
+
const portIndex = args2.indexOf("--port");
|
|
907
|
+
const port = portIndex !== -1 ? parseInt(args2[portIndex + 1], 10) : void 0;
|
|
908
|
+
const cwdIndex = args2.indexOf("--cwd");
|
|
909
|
+
const cwd = cwdIndex !== -1 ? args2[cwdIndex + 1] : void 0;
|
|
910
|
+
const restartFlag = args2.includes("--restart");
|
|
911
|
+
const stopFlags = [nameIndex, portIndex, cwdIndex].filter((i) => i !== -1);
|
|
912
|
+
const cmdEnd = Math.min(...stopFlags, Infinity);
|
|
913
|
+
const cmdParts = args2.slice(0, cmdEnd);
|
|
914
|
+
const cmd = cmdParts.join(" ");
|
|
915
|
+
if (!cmd) {
|
|
916
|
+
console.error(renderError("Missing command", "Usage: fennec start|run <command> --name <name> [--port <port>] [--cwd <dir>] [--restart]"));
|
|
917
|
+
process.exit(1);
|
|
918
|
+
}
|
|
919
|
+
const appName = name ?? cmdParts[0] ?? "app";
|
|
920
|
+
console.error(`
|
|
921
|
+
${symbols.fox} ${pc4.bold("Running")} ${renderAppName(appName)}
|
|
922
|
+
`);
|
|
923
|
+
console.error(` ${renderKV("Command", cmd)}`);
|
|
924
|
+
if (port) console.error(` ${renderKV("Port", String(port))}`);
|
|
925
|
+
if (cwd) console.error(` ${renderKV("Directory", cwd)}`);
|
|
926
|
+
console.error(` ${renderKV("Restart", restartFlag ? "on crash" : "off")}`);
|
|
927
|
+
console.error(` ${divider()}`);
|
|
928
|
+
const config = {
|
|
929
|
+
maxProcesses: 10,
|
|
930
|
+
logBufferLines: 2e3,
|
|
931
|
+
spawnAllowlist: []
|
|
932
|
+
};
|
|
933
|
+
const pm = new ProcessManager(config);
|
|
934
|
+
try {
|
|
935
|
+
const proc = pm.spawn(cmdParts[0], cmdParts.slice(1), cwd, void 0, appName);
|
|
936
|
+
addTracked({
|
|
937
|
+
name: appName,
|
|
938
|
+
pid: proc.pid,
|
|
939
|
+
command: cmd,
|
|
940
|
+
port,
|
|
941
|
+
cwd,
|
|
942
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
943
|
+
});
|
|
944
|
+
console.error(`
|
|
945
|
+
${pc4.green("\u25CF")} ${pc4.bold(appName)} ${pc4.dim(`started (PID: ${proc.pid})`)}${port ? pc4.dim(` :${port}`) : ""}
|
|
946
|
+
`);
|
|
947
|
+
const stdout = proc.child.stdout;
|
|
948
|
+
const stderr = proc.child.stderr;
|
|
949
|
+
if (stdout) {
|
|
950
|
+
stdout.on("data", (data) => {
|
|
951
|
+
const lines = data.toString().split("\n").filter(Boolean);
|
|
952
|
+
for (const line of lines) {
|
|
953
|
+
const level = line.includes("error") || line.includes("Error") ? "error" : line.includes("warn") || line.includes("WARN") ? "warn" : "info";
|
|
954
|
+
const time = timestamp();
|
|
955
|
+
const lvl = logLevel(level);
|
|
956
|
+
process.stdout.write(` ${time} ${lvl} ${line}
|
|
957
|
+
`);
|
|
958
|
+
}
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
if (stderr) {
|
|
962
|
+
stderr.on("data", (data) => {
|
|
963
|
+
const lines = data.toString().split("\n").filter(Boolean);
|
|
964
|
+
for (const line of lines) {
|
|
965
|
+
const time = timestamp();
|
|
966
|
+
const lvl = logLevel("error");
|
|
967
|
+
process.stderr.write(` ${time} ${lvl} ${pc4.red(line)}
|
|
968
|
+
`);
|
|
969
|
+
}
|
|
970
|
+
});
|
|
971
|
+
}
|
|
972
|
+
proc.child.on("exit", (code, signal) => {
|
|
973
|
+
removeTracked(appName);
|
|
974
|
+
if (restartFlag && code !== 0) {
|
|
975
|
+
console.error(`
|
|
976
|
+
${pc4.yellow("\u26A0")} ${appName} ${pc4.dim(`exited (${code}), restarting...`)}`);
|
|
977
|
+
pm.restart(appName).catch(() => {
|
|
978
|
+
});
|
|
979
|
+
} else {
|
|
980
|
+
console.error(`
|
|
981
|
+
${pc4.dim("\u25CB")} ${pc4.bold(appName)} ${pc4.dim(`exited (${code})`)}`);
|
|
982
|
+
}
|
|
983
|
+
});
|
|
984
|
+
await new Promise(() => {
|
|
985
|
+
});
|
|
986
|
+
} catch (error) {
|
|
987
|
+
console.error(renderError(`Failed to run ${appName}`, String(error)));
|
|
988
|
+
process.exit(1);
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
async function psCommand(args2) {
|
|
992
|
+
const watchFlag = args2.includes("-w") || args2.includes("--watch");
|
|
993
|
+
const nameFilter = args2.includes("--name") ? args2[args2.indexOf("--name") + 1] : void 0;
|
|
994
|
+
const portFilter = args2.includes("--port") ? parseInt(args2[args2.indexOf("--port") + 1], 10) : void 0;
|
|
995
|
+
const sortBy = args2.includes("--sort") ? args2[args2.indexOf("--sort") + 1] : args2.includes("-m") ? "mem" : args2.includes("-c") ? "cpu" : "cpu";
|
|
996
|
+
const limit = args2.includes("-n") ? parseInt(args2[args2.indexOf("-n") + 1], 10) : 30;
|
|
997
|
+
const allFlag = args2.includes("-a") || args2.includes("--all");
|
|
998
|
+
if (watchFlag) {
|
|
999
|
+
await watchProcesses(sortBy, limit);
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1002
|
+
const spinner = createSpinner("Scanning system processes...");
|
|
1003
|
+
try {
|
|
1004
|
+
const processes = getSystemProcesses({
|
|
1005
|
+
name: nameFilter,
|
|
1006
|
+
port: portFilter,
|
|
1007
|
+
userOnly: !allFlag,
|
|
1008
|
+
sortBy,
|
|
1009
|
+
limit
|
|
1010
|
+
});
|
|
1011
|
+
spinner.stop();
|
|
1012
|
+
process.stdout.write("\r\x1B[K");
|
|
1013
|
+
if (processes.length === 0) {
|
|
1014
|
+
console.error(`
|
|
1015
|
+
${pc4.dim("No processes found.")}
|
|
1016
|
+
`);
|
|
1017
|
+
return;
|
|
1018
|
+
}
|
|
1019
|
+
const columns = [
|
|
1020
|
+
{ key: "pid", label: "PID", align: "right", format: (v) => pc4.dim(String(v).padStart(6)) },
|
|
1021
|
+
{ key: "name", label: "Name", format: (v) => pc4.bold(String(v)) },
|
|
1022
|
+
{ key: "cpu", label: "CPU%", align: "right", format: (v) => {
|
|
1023
|
+
const num = v;
|
|
1024
|
+
return num > 10 ? pc4.red(String(num)) : num > 5 ? pc4.yellow(String(num)) : pc4.dim(String(num));
|
|
1025
|
+
} },
|
|
1026
|
+
{ key: "mem", label: "MEM%", align: "right", format: (v) => {
|
|
1027
|
+
const num = v;
|
|
1028
|
+
return num > 10 ? pc4.red(String(num)) : num > 5 ? pc4.yellow(String(num)) : pc4.dim(String(num));
|
|
1029
|
+
} },
|
|
1030
|
+
{ key: "state", label: "State", format: (v) => {
|
|
1031
|
+
const s = String(v);
|
|
1032
|
+
if (s === "R" || s === "Running") return pc4.green(s);
|
|
1033
|
+
if (s === "Z" || s === "Zombie") return pc4.red(s);
|
|
1034
|
+
if (s === "S" || s === "Sleeping") return pc4.cyan(s);
|
|
1035
|
+
return pc4.dim(s);
|
|
1036
|
+
} },
|
|
1037
|
+
{ key: "ports", label: "Ports", format: (v) => {
|
|
1038
|
+
const ports = v;
|
|
1039
|
+
return ports.length > 0 ? ports.map((p) => pc4.yellow(`:${p}`)).join(", ") : pc4.dim("-");
|
|
1040
|
+
} }
|
|
1041
|
+
];
|
|
1042
|
+
const rows = processes.map((p) => ({
|
|
1043
|
+
pid: p.pid,
|
|
1044
|
+
name: p.name,
|
|
1045
|
+
cpu: p.cpuPercent,
|
|
1046
|
+
mem: p.memPercent,
|
|
1047
|
+
state: formatProcessState(p.state),
|
|
1048
|
+
ports: p.ports
|
|
1049
|
+
}));
|
|
1050
|
+
const tracked = readTracked();
|
|
1051
|
+
const managedPids = new Set(tracked.map((t) => t.pid));
|
|
1052
|
+
const managedRows = processes.map((p) => {
|
|
1053
|
+
const isManaged = managedPids.has(p.pid);
|
|
1054
|
+
const trackedInfo = tracked.find((t) => t.pid === p.pid);
|
|
1055
|
+
return {
|
|
1056
|
+
...{
|
|
1057
|
+
pid: p.pid,
|
|
1058
|
+
name: p.name,
|
|
1059
|
+
cpu: p.cpuPercent,
|
|
1060
|
+
mem: p.memPercent,
|
|
1061
|
+
state: formatProcessState(p.state),
|
|
1062
|
+
ports: p.ports
|
|
1063
|
+
},
|
|
1064
|
+
_managed: isManaged,
|
|
1065
|
+
_trackedPort: trackedInfo?.port
|
|
1066
|
+
};
|
|
1067
|
+
});
|
|
1068
|
+
if (tracked.length > 0) {
|
|
1069
|
+
console.error(` ${pc4.green("\u25CF")} ${pc4.bold("Managed:")} ${tracked.map((t) => {
|
|
1070
|
+
const running = isProcessRunning(t.pid);
|
|
1071
|
+
const status = running ? pc4.green("running") : pc4.red("stopped");
|
|
1072
|
+
const portStr = t.port ? ` ${pc4.yellow(`:${t.port}`)}` : "";
|
|
1073
|
+
return `${pc4.bold(t.name)}${portStr} (${status})`;
|
|
1074
|
+
}).join(", ")}
|
|
1075
|
+
`);
|
|
1076
|
+
}
|
|
1077
|
+
console.error(` ${symbols.fox} ${pc4.bold("System Processes")} ${pc4.dim(`(${processes.length} shown, sorted by ${sortBy})`)}
|
|
1078
|
+
`);
|
|
1079
|
+
console.error(renderTable(columns, managedRows));
|
|
1080
|
+
console.error();
|
|
1081
|
+
} catch (error) {
|
|
1082
|
+
spinner.fail("Failed to scan processes");
|
|
1083
|
+
console.error(renderError("Process scan failed", String(error)));
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
async function watchProcesses(sortBy, limit) {
|
|
1087
|
+
console.error(`
|
|
1088
|
+
${pc4.bold("Watching processes")} ${pc4.dim("(Ctrl+C to stop, refreshes every 3s)")}
|
|
1089
|
+
`);
|
|
1090
|
+
const render = () => {
|
|
1091
|
+
const processes = getSystemProcesses({
|
|
1092
|
+
userOnly: true,
|
|
1093
|
+
sortBy,
|
|
1094
|
+
limit
|
|
1095
|
+
});
|
|
1096
|
+
const columns = [
|
|
1097
|
+
{ key: "pid", label: "PID", align: "right", format: (v) => pc4.dim(String(v).padStart(6)) },
|
|
1098
|
+
{ key: "name", label: "Name", format: (v) => pc4.bold(String(v)) },
|
|
1099
|
+
{ key: "cpu", label: "CPU%", align: "right", format: (v) => {
|
|
1100
|
+
const num = v;
|
|
1101
|
+
return num > 10 ? pc4.red(String(num)) : num > 5 ? pc4.yellow(String(num)) : pc4.dim(String(num));
|
|
1102
|
+
} },
|
|
1103
|
+
{ key: "mem", label: "MEM%", align: "right", format: (v) => {
|
|
1104
|
+
const num = v;
|
|
1105
|
+
return num > 10 ? pc4.red(String(num)) : num > 5 ? pc4.yellow(String(num)) : pc4.dim(String(num));
|
|
1106
|
+
} },
|
|
1107
|
+
{ key: "state", label: "S", align: "center" }
|
|
1108
|
+
];
|
|
1109
|
+
const rows = processes.map((p) => ({
|
|
1110
|
+
pid: p.pid,
|
|
1111
|
+
name: p.name,
|
|
1112
|
+
cpu: p.cpuPercent,
|
|
1113
|
+
mem: p.memPercent,
|
|
1114
|
+
state: p.state
|
|
1115
|
+
}));
|
|
1116
|
+
return ` ${timestamp()} ${pc4.dim(`${processes.length} processes`)}
|
|
1117
|
+
${renderTable(columns, rows, { compact: true })}`;
|
|
1118
|
+
};
|
|
1119
|
+
console.error(render());
|
|
1120
|
+
const interval = setInterval(() => {
|
|
1121
|
+
process.stdout.write("\x1B[J");
|
|
1122
|
+
console.error(render());
|
|
1123
|
+
}, 3e3);
|
|
1124
|
+
process.on("SIGINT", () => {
|
|
1125
|
+
clearInterval(interval);
|
|
1126
|
+
process.exit(0);
|
|
1127
|
+
});
|
|
1128
|
+
await new Promise(() => {
|
|
1129
|
+
});
|
|
1130
|
+
}
|
|
1131
|
+
async function statusCommand(_args) {
|
|
1132
|
+
const watchFlag = _args.includes("-w") || _args.includes("--watch");
|
|
1133
|
+
const spinner = createSpinner("Gathering system status...");
|
|
1134
|
+
try {
|
|
1135
|
+
const topProcesses = getSystemProcesses({ userOnly: true, sortBy: "cpu", limit: 10 });
|
|
1136
|
+
const totalProcesses = getSystemProcesses({ userOnly: true }).length;
|
|
1137
|
+
const services = [
|
|
1138
|
+
{ name: "node", label: "Node.js" },
|
|
1139
|
+
{ name: "npm", label: "npm" },
|
|
1140
|
+
{ name: "pnpm", label: "pnpm" },
|
|
1141
|
+
{ name: "vite", label: "Vite" },
|
|
1142
|
+
{ name: "next", label: "Next.js" },
|
|
1143
|
+
{ name: "webpack", label: "Webpack" },
|
|
1144
|
+
{ name: "tsc", label: "TypeScript" },
|
|
1145
|
+
{ name: "docker", label: "Docker" },
|
|
1146
|
+
{ name: "mysqld", label: "MySQL" },
|
|
1147
|
+
{ name: "postgres", label: "PostgreSQL" },
|
|
1148
|
+
{ name: "redis", label: "Redis" },
|
|
1149
|
+
{ name: "ollama", label: "Ollama" }
|
|
1150
|
+
];
|
|
1151
|
+
spinner.stop();
|
|
1152
|
+
process.stdout.write("\r\x1B[K");
|
|
1153
|
+
console.error(`
|
|
1154
|
+
${symbols.fox} ${pc4.bold("Fennec System Overview")}
|
|
1155
|
+
`);
|
|
1156
|
+
console.error(` ${renderKV("User Processes", pc4.bold(String(totalProcesses)))}`);
|
|
1157
|
+
console.error(` ${renderKV("Fennec Version", pc4.dim("v1.11.1"))}`);
|
|
1158
|
+
const activeServices = services.filter((s) => topProcesses.some((p) => p.name.toLowerCase().includes(s.name))).slice(0, 5);
|
|
1159
|
+
if (activeServices.length > 0) {
|
|
1160
|
+
console.error(` ${renderKV("Detected", activeServices.map((s) => pc4.green(s.label)).join(", "))}`);
|
|
1161
|
+
}
|
|
1162
|
+
console.error();
|
|
1163
|
+
const columns = [
|
|
1164
|
+
{ key: "pid", label: "PID", align: "right", format: (v) => pc4.dim(String(v).padStart(6)) },
|
|
1165
|
+
{ key: "name", label: "Process", format: (v) => pc4.bold(String(v)) },
|
|
1166
|
+
{ key: "cpu", label: "CPU%", align: "right" },
|
|
1167
|
+
{ key: "mem", label: "MEM%", align: "right" }
|
|
1168
|
+
];
|
|
1169
|
+
const rows = topProcesses.map((p) => ({
|
|
1170
|
+
pid: p.pid,
|
|
1171
|
+
name: p.name,
|
|
1172
|
+
cpu: p.cpuPercent,
|
|
1173
|
+
mem: p.memPercent
|
|
1174
|
+
}));
|
|
1175
|
+
console.error(` ${pc4.bold("Top CPU Processes")}
|
|
1176
|
+
`);
|
|
1177
|
+
console.error(renderTable(columns, rows));
|
|
1178
|
+
if (watchFlag) {
|
|
1179
|
+
await watchProcesses("cpu", 15);
|
|
1180
|
+
}
|
|
1181
|
+
console.error();
|
|
1182
|
+
} catch (error) {
|
|
1183
|
+
spinner.fail("Failed to gather system status");
|
|
1184
|
+
console.error(renderError("Status check failed", String(error)));
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
async function killCommand(args2) {
|
|
1188
|
+
const rawTarget = args2[0];
|
|
1189
|
+
const signalIndex = args2.indexOf("--signal");
|
|
1190
|
+
const signalRaw = signalIndex !== -1 ? args2[signalIndex + 1] : "SIGTERM";
|
|
1191
|
+
const signal = signalRaw ?? "SIGTERM";
|
|
1192
|
+
if (rawTarget === "all" || args2.includes("--all") || args2.includes("-a")) {
|
|
1193
|
+
const userProcs = getSystemProcesses({ userOnly: true, sortBy: "cpu", limit: 200 });
|
|
1194
|
+
if (userProcs.length === 0) {
|
|
1195
|
+
console.error(` ${pc4.dim("No user processes to kill.")}`);
|
|
1196
|
+
return;
|
|
1197
|
+
}
|
|
1198
|
+
console.error(`
|
|
1199
|
+
${pc4.bold(`Kill ${userProcs.length} user processes?`)}`);
|
|
1200
|
+
console.error(` ${pc4.dim("This will stop ALL your running processes.")}`);
|
|
1201
|
+
console.error(` ${pc4.yellow("\u26A0 System processes will not be affected.")}
|
|
1202
|
+
`);
|
|
1203
|
+
const confirmed2 = await confirmPrompt(
|
|
1204
|
+
`${pc4.red("Are you sure?")} ${pc4.dim("This cannot be undone")}`,
|
|
1205
|
+
false
|
|
1206
|
+
);
|
|
1207
|
+
if (!confirmed2) {
|
|
1208
|
+
console.error(` ${pc4.dim("Cancelled.")}`);
|
|
1209
|
+
return;
|
|
1210
|
+
}
|
|
1211
|
+
const spinner2 = createSpinner(`Killing ${userProcs.length} processes...`);
|
|
1212
|
+
let killed = 0;
|
|
1213
|
+
let failed = 0;
|
|
1214
|
+
for (const proc of userProcs) {
|
|
1215
|
+
if (killProcess(proc.pid, signal)) {
|
|
1216
|
+
killed++;
|
|
1217
|
+
removeTrackedByPid(proc.pid);
|
|
1218
|
+
} else {
|
|
1219
|
+
failed++;
|
|
1220
|
+
}
|
|
1221
|
+
if (killed % 10 === 0) await new Promise((r) => setTimeout(r, 50));
|
|
1222
|
+
}
|
|
1223
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
1224
|
+
if (killed > 0) {
|
|
1225
|
+
spinner2.succeed(`${killed} process(es) killed`);
|
|
1226
|
+
} else {
|
|
1227
|
+
spinner2.fail(`Failed to kill processes`);
|
|
1228
|
+
}
|
|
1229
|
+
if (failed > 0) {
|
|
1230
|
+
console.error(` ${pc4.yellow(`${failed} process(es) could not be killed`)} ${pc4.dim("(try with sudo)")}`);
|
|
1231
|
+
}
|
|
1232
|
+
return;
|
|
1233
|
+
}
|
|
1234
|
+
if (!rawTarget) {
|
|
1235
|
+
console.error(renderError("Missing target", "Usage: fennec kill <pid|name|all> [--signal SIGTERM|SIGKILL|SIGINT]"));
|
|
1236
|
+
process.exit(1);
|
|
1237
|
+
}
|
|
1238
|
+
let targetPid;
|
|
1239
|
+
let displayName;
|
|
1240
|
+
const pid = parseInt(rawTarget, 10);
|
|
1241
|
+
if (!isNaN(pid) && String(pid) === rawTarget) {
|
|
1242
|
+
if (!isProcessRunning(pid)) {
|
|
1243
|
+
console.error(renderError("Process not found", `No process with PID ${pid} is running`));
|
|
1244
|
+
process.exit(1);
|
|
1245
|
+
}
|
|
1246
|
+
targetPid = pid;
|
|
1247
|
+
displayName = `PID ${pid}`;
|
|
1248
|
+
} else {
|
|
1249
|
+
const matches = getSystemProcesses({ name: rawTarget, userOnly: true, sortBy: "cpu" });
|
|
1250
|
+
if (matches.length === 0) {
|
|
1251
|
+
console.error(renderError("Process not found", `No running process matching "${rawTarget}"`));
|
|
1252
|
+
process.exit(1);
|
|
1253
|
+
}
|
|
1254
|
+
if (matches.length > 1) {
|
|
1255
|
+
console.error(`
|
|
1256
|
+
${pc4.bold(`Multiple processes match "${rawTarget}":`)}`);
|
|
1257
|
+
const selected = await selectPrompt(
|
|
1258
|
+
`Select which to kill:`,
|
|
1259
|
+
matches.map((p, i) => ({
|
|
1260
|
+
value: String(i),
|
|
1261
|
+
label: `${p.name} (PID ${p.pid})`,
|
|
1262
|
+
description: `${p.command.slice(0, 80)} \u2014 CPU: ${p.cpuPercent}% MEM: ${p.memPercent}%`
|
|
1263
|
+
}))
|
|
1264
|
+
);
|
|
1265
|
+
if (selected === null) {
|
|
1266
|
+
console.error(` ${pc4.dim("Cancelled")}`);
|
|
1267
|
+
return;
|
|
1268
|
+
}
|
|
1269
|
+
const idx = parseInt(selected, 10);
|
|
1270
|
+
targetPid = matches[idx].pid;
|
|
1271
|
+
displayName = `${matches[idx].name} (PID ${targetPid})`;
|
|
1272
|
+
} else {
|
|
1273
|
+
targetPid = matches[0].pid;
|
|
1274
|
+
displayName = `${matches[0].name} (PID ${targetPid})`;
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
const confirmed = await confirmPrompt(
|
|
1278
|
+
`Kill ${pc4.bold(displayName)} with ${pc4.yellow(signal)}?`,
|
|
1279
|
+
false
|
|
1280
|
+
);
|
|
1281
|
+
if (!confirmed) {
|
|
1282
|
+
console.error(` ${pc4.dim("Cancelled")}`);
|
|
1283
|
+
return;
|
|
1284
|
+
}
|
|
1285
|
+
const spinner = createSpinner(`Sending ${signal} to ${displayName}...`);
|
|
1286
|
+
const success = killProcess(targetPid, signal);
|
|
1287
|
+
if (success) {
|
|
1288
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
1289
|
+
const stillRunning = isProcessRunning(targetPid);
|
|
1290
|
+
if (stillRunning && signal !== "SIGKILL") {
|
|
1291
|
+
spinner.warn(`${displayName} did not respond to ${signal}`);
|
|
1292
|
+
const forceKill = await confirmPrompt(
|
|
1293
|
+
`Send ${pc4.red("SIGKILL")} to force stop?`,
|
|
1294
|
+
true
|
|
1295
|
+
);
|
|
1296
|
+
if (forceKill) {
|
|
1297
|
+
const forceSpinner = createSpinner(`Sending SIGKILL to ${displayName}...`);
|
|
1298
|
+
killProcess(targetPid, "SIGKILL");
|
|
1299
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
1300
|
+
if (!isProcessRunning(targetPid)) {
|
|
1301
|
+
forceSpinner.succeed(`${displayName} force stopped`);
|
|
1302
|
+
} else {
|
|
1303
|
+
forceSpinner.fail(`${displayName} could not be stopped (permission denied?)`);
|
|
1304
|
+
}
|
|
1305
|
+
} else {
|
|
1306
|
+
console.error(` ${pc4.dim("Retrying with SIGTERM...")}`);
|
|
1307
|
+
killProcess(targetPid, "SIGTERM");
|
|
1308
|
+
console.error(` ${pc4.yellow("\u26A0")} ${displayName} ${pc4.dim("may still be running")}`);
|
|
1309
|
+
}
|
|
1310
|
+
} else {
|
|
1311
|
+
removeTrackedByPid(targetPid);
|
|
1312
|
+
spinner.succeed(`${displayName} stopped`);
|
|
1313
|
+
}
|
|
1314
|
+
} else {
|
|
1315
|
+
spinner.fail(`Failed to kill ${displayName}`);
|
|
1316
|
+
console.error(renderError("Permission denied", `Try running with sudo or use a different signal.`));
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
async function restartCommand(args2) {
|
|
1320
|
+
const name = args2[0];
|
|
1321
|
+
if (!name) {
|
|
1322
|
+
console.error(renderError("Missing process name/pid", "Usage: fennec restart <pid|name>"));
|
|
1323
|
+
process.exit(1);
|
|
1324
|
+
}
|
|
1325
|
+
let targetPid;
|
|
1326
|
+
const pid = parseInt(name, 10);
|
|
1327
|
+
if (!isNaN(pid) && String(pid) === name) {
|
|
1328
|
+
targetPid = pid;
|
|
1329
|
+
} else {
|
|
1330
|
+
const processes = getSystemProcesses({ name, userOnly: true, limit: 1 });
|
|
1331
|
+
if (processes.length === 0) {
|
|
1332
|
+
console.error(renderError("Process not found", `No process matching "${name}"`));
|
|
1333
|
+
process.exit(1);
|
|
1334
|
+
}
|
|
1335
|
+
targetPid = processes[0].pid;
|
|
1336
|
+
}
|
|
1337
|
+
const confirmed = await confirmPrompt(
|
|
1338
|
+
`Restart ${pc4.bold(`${name} (PID ${targetPid})`)}?`,
|
|
1339
|
+
false
|
|
1340
|
+
);
|
|
1341
|
+
if (!confirmed) {
|
|
1342
|
+
console.error(` ${pc4.dim("Cancelled")}`);
|
|
1343
|
+
return;
|
|
1344
|
+
}
|
|
1345
|
+
const spinner = createSpinner(`Stopping PID ${targetPid}...`);
|
|
1346
|
+
const killed = killProcess(targetPid, "SIGTERM");
|
|
1347
|
+
if (!killed) {
|
|
1348
|
+
spinner.fail(`Failed to stop PID ${targetPid}`);
|
|
1349
|
+
return;
|
|
1350
|
+
}
|
|
1351
|
+
await new Promise((r) => setTimeout(r, 1e3));
|
|
1352
|
+
const stillRunning = isProcessRunning(targetPid);
|
|
1353
|
+
if (stillRunning) {
|
|
1354
|
+
spinner.warn(`Process didn't stop, sending SIGKILL...`);
|
|
1355
|
+
killProcess(targetPid, "SIGKILL");
|
|
1356
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
1357
|
+
}
|
|
1358
|
+
const procInfo = getSystemProcesses({ pid: targetPid, limit: 1 })[0];
|
|
1359
|
+
let suggestion = "";
|
|
1360
|
+
if (procInfo) {
|
|
1361
|
+
suggestion = `
|
|
1362
|
+
${pc4.dim("Re-run:")} ${renderCommand(procInfo.command)}`;
|
|
1363
|
+
}
|
|
1364
|
+
spinner.succeed(`${name} stopped${suggestion}`);
|
|
1365
|
+
}
|
|
1366
|
+
async function logCommand(args2) {
|
|
1367
|
+
const target = args2[0];
|
|
1368
|
+
if (!target) {
|
|
1369
|
+
console.error(renderError("Missing process name/pid", "Usage: fennec log <pid|name> [--lines N] [--level LEVEL] [-f]"));
|
|
1370
|
+
process.exit(1);
|
|
1371
|
+
}
|
|
1372
|
+
const linesIndex = args2.indexOf("--lines");
|
|
1373
|
+
const lines = linesIndex !== -1 ? parseInt(args2[linesIndex + 1], 10) : 30;
|
|
1374
|
+
const followFlag = args2.includes("-f") || args2.includes("--follow");
|
|
1375
|
+
let targetPid;
|
|
1376
|
+
let displayName;
|
|
1377
|
+
const pid = parseInt(target, 10);
|
|
1378
|
+
if (!isNaN(pid) && String(pid) === target) {
|
|
1379
|
+
targetPid = pid;
|
|
1380
|
+
displayName = `PID ${pid}`;
|
|
1381
|
+
} else {
|
|
1382
|
+
const processes = getSystemProcesses({ name: target, userOnly: true, limit: 1 });
|
|
1383
|
+
if (processes.length === 0) {
|
|
1384
|
+
console.error(renderError("Process not found", `No process matching "${target}"`));
|
|
1385
|
+
process.exit(1);
|
|
1386
|
+
}
|
|
1387
|
+
targetPid = processes[0].pid;
|
|
1388
|
+
displayName = `${processes[0].name} (PID ${targetPid})`;
|
|
1389
|
+
}
|
|
1390
|
+
const spinner = createSpinner(`Fetching logs for ${displayName}...`);
|
|
1391
|
+
try {
|
|
1392
|
+
let logLines = [];
|
|
1393
|
+
try {
|
|
1394
|
+
const { execSync: execSync3 } = await import("child_process");
|
|
1395
|
+
const output = execSync3(
|
|
1396
|
+
`journalctl --no-pager -n ${lines} _PID=${targetPid} 2>/dev/null || tail -n ${lines} /proc/${targetPid}/fd/1 2>/dev/null || echo "No logs available"`,
|
|
1397
|
+
{ encoding: "utf-8", timeout: 3e3 }
|
|
1398
|
+
);
|
|
1399
|
+
logLines = output.trim().split("\n").filter(Boolean);
|
|
1400
|
+
} catch {
|
|
1401
|
+
logLines = [` ${pc4.dim("(no log access \u2014 process may not be managed by Fennec)")}`];
|
|
1402
|
+
}
|
|
1403
|
+
spinner.stop();
|
|
1404
|
+
process.stdout.write("\r\x1B[K");
|
|
1405
|
+
console.error(`
|
|
1406
|
+
${symbols.fox} ${pc4.bold("Logs")} ${renderAppName(displayName)} ${pc4.dim(`(last ${lines} lines)`)}
|
|
1407
|
+
`);
|
|
1408
|
+
for (const line of logLines.slice(-lines)) {
|
|
1409
|
+
const display = line.length > 200 ? line.slice(0, 200) + "\u2026" : line;
|
|
1410
|
+
if (line.toLowerCase().includes("error") || line.toLowerCase().includes("fail")) {
|
|
1411
|
+
console.error(` ${pc4.red(display)}`);
|
|
1412
|
+
} else if (line.toLowerCase().includes("warn")) {
|
|
1413
|
+
console.error(` ${pc4.yellow(display)}`);
|
|
1414
|
+
} else {
|
|
1415
|
+
console.error(` ${display}`);
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
if (followFlag) {
|
|
1419
|
+
console.error(`
|
|
1420
|
+
${pc4.dim("Following... (Ctrl+C to stop)")}
|
|
1421
|
+
`);
|
|
1422
|
+
await new Promise(() => {
|
|
1423
|
+
});
|
|
1424
|
+
}
|
|
1425
|
+
console.error();
|
|
1426
|
+
} catch (error) {
|
|
1427
|
+
spinner.fail(`Failed to fetch logs for ${displayName}`);
|
|
1428
|
+
console.error(renderError("Log fetch failed", String(error)));
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
async function attachCommand(args2) {
|
|
1432
|
+
const port = parseInt(args2[0], 10);
|
|
1433
|
+
if (isNaN(port)) {
|
|
1434
|
+
console.error(renderError("Invalid port", "Usage: fennec attach <port> --name <name>"));
|
|
1435
|
+
process.exit(1);
|
|
1436
|
+
}
|
|
1437
|
+
const nameIndex = args2.indexOf("--name");
|
|
1438
|
+
const rawName = nameIndex !== -1 ? args2[nameIndex + 1] : void 0;
|
|
1439
|
+
const name = rawName ?? `port-${port}`;
|
|
1440
|
+
const spinner = createSpinner(`Attaching to :${port}...`);
|
|
1441
|
+
try {
|
|
1442
|
+
const { PortDetector } = await import("@plumpslabs/fennec-core");
|
|
1443
|
+
const detector = new PortDetector();
|
|
1444
|
+
const info = detector.detectByPort(port);
|
|
1445
|
+
if (info) {
|
|
1446
|
+
spinner.succeed(`Attached to :${port}`);
|
|
1447
|
+
console.error(` ${renderKV("Name", renderAppName(name))}`);
|
|
1448
|
+
console.error(` ${renderKV("PID", String(info.pid))}`);
|
|
1449
|
+
console.error(` ${renderKV("Command", info.command || pc4.dim("unknown"))}`);
|
|
1450
|
+
} else {
|
|
1451
|
+
spinner.fail(`No process found on port ${port}`);
|
|
1452
|
+
process.exit(1);
|
|
1453
|
+
}
|
|
1454
|
+
} catch (error) {
|
|
1455
|
+
spinner.fail(`Failed to attach to :${port}`);
|
|
1456
|
+
console.error(renderError("Error", String(error)));
|
|
1457
|
+
process.exit(1);
|
|
1458
|
+
}
|
|
250
1459
|
}
|
|
251
1460
|
async function sessionsCommand() {
|
|
252
1461
|
const store = new SessionStore("./.fennec/sessions");
|
|
253
1462
|
const sessions = store.list();
|
|
254
1463
|
if (sessions.length === 0) {
|
|
255
|
-
console.
|
|
1464
|
+
console.error(`
|
|
1465
|
+
${pc4.dim("No saved sessions found.")}
|
|
1466
|
+
`);
|
|
256
1467
|
return;
|
|
257
1468
|
}
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
1469
|
+
const columns = [
|
|
1470
|
+
{ key: "name", label: "Name", format: (v) => pc4.bold(String(v)) },
|
|
1471
|
+
{ key: "origin", label: "Origin" },
|
|
1472
|
+
{ key: "savedAt", label: "Saved", format: (v) => pc4.dim(String(v)) }
|
|
1473
|
+
];
|
|
1474
|
+
const rows = sessions.map((s) => ({
|
|
1475
|
+
name: s.name,
|
|
1476
|
+
origin: s.origin,
|
|
1477
|
+
savedAt: new Date(s.savedAt).toLocaleString()
|
|
1478
|
+
}));
|
|
1479
|
+
console.error(`
|
|
1480
|
+
${symbols.fox} ${pc4.bold("Saved Sessions")}
|
|
1481
|
+
`);
|
|
1482
|
+
console.error(renderTable(columns, rows));
|
|
1483
|
+
console.error(` ${pc4.dim(`${sessions.length} session(s)`)}
|
|
1484
|
+
`);
|
|
264
1485
|
}
|
|
265
1486
|
async function setupCommand() {
|
|
266
|
-
console.
|
|
1487
|
+
console.error(`
|
|
1488
|
+
${symbols.fox} ${pc4.bold("Fennec Setup")}
|
|
1489
|
+
`);
|
|
267
1490
|
const mcpClient = await selectPrompt("Which MCP client are you using?", [
|
|
268
|
-
{ value: "claude", label: "Claude Desktop" },
|
|
269
|
-
{ value: "
|
|
1491
|
+
{ value: "claude", label: "Claude Desktop", description: "Anthropic's AI desktop app" },
|
|
1492
|
+
{ value: "cursor", label: "Cursor", description: "AI-powered code editor" },
|
|
1493
|
+
{ value: "cline", label: "Cline", description: "VS Code MCP client" },
|
|
1494
|
+
{ value: "other", label: "Other MCP client", description: "Any MCP-compatible client" }
|
|
270
1495
|
]);
|
|
271
|
-
if (
|
|
272
|
-
console.
|
|
1496
|
+
if (!mcpClient) {
|
|
1497
|
+
console.error(` ${pc4.dim("Setup cancelled.")}
|
|
1498
|
+
`);
|
|
273
1499
|
return;
|
|
274
1500
|
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
{
|
|
1501
|
+
console.error(`
|
|
1502
|
+
${pc4.green("\u2713")} Selected: ${pc4.bold(mcpClient)}
|
|
1503
|
+
`);
|
|
1504
|
+
const configSnippet = `{
|
|
280
1505
|
"mcpServers": {
|
|
281
1506
|
"fennec": {
|
|
282
1507
|
"command": "fennec",
|
|
283
1508
|
"args": ["start"]
|
|
284
1509
|
}
|
|
285
1510
|
}
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
console.
|
|
1511
|
+
}`;
|
|
1512
|
+
console.error(` ${pc4.bold("Add this to your MCP client config:")}
|
|
1513
|
+
`);
|
|
1514
|
+
console.error(` ${pc4.dim("```")}`);
|
|
1515
|
+
console.error(configSnippet.split("\n").map((l) => ` ${l}`).join("\n"));
|
|
1516
|
+
console.error(` ${pc4.dim("```")}
|
|
1517
|
+
`);
|
|
1518
|
+
console.error(` ${renderSuccess("Setup complete!")} ${pc4.dim("Run")} ${renderCommand("fennec start")} ${pc4.dim("to begin.")}
|
|
1519
|
+
`);
|
|
290
1520
|
}
|
|
291
1521
|
async function installBrowsersCommand() {
|
|
292
|
-
console.
|
|
293
|
-
|
|
1522
|
+
console.error(`
|
|
1523
|
+
${pc4.bold("Installing Browser Engines")}
|
|
1524
|
+
`);
|
|
1525
|
+
const spinner = createSpinner("Installing Chromium...");
|
|
294
1526
|
try {
|
|
295
|
-
|
|
296
|
-
stdio: "
|
|
1527
|
+
execSync2("npx playwright install chromium", {
|
|
1528
|
+
stdio: "pipe",
|
|
297
1529
|
timeout: 12e4
|
|
298
1530
|
});
|
|
299
|
-
|
|
1531
|
+
spinner.succeed("Chromium installed successfully");
|
|
300
1532
|
} catch {
|
|
301
|
-
|
|
302
|
-
console.error("Try running
|
|
1533
|
+
spinner.fail("Failed to install Chromium");
|
|
1534
|
+
console.error(` ${pc4.yellow("\u2192")} Try running: ${renderCommand("npx playwright install chromium")}`);
|
|
303
1535
|
}
|
|
304
|
-
console.
|
|
1536
|
+
console.error(`
|
|
1537
|
+
${pc4.green("\u2713")} Browser installation complete.
|
|
1538
|
+
`);
|
|
305
1539
|
}
|
|
306
1540
|
async function initCommand() {
|
|
307
|
-
console.
|
|
1541
|
+
console.error(`
|
|
1542
|
+
${pc4.bold("Initialize Fennec Configuration")}
|
|
1543
|
+
`);
|
|
308
1544
|
const configFile = resolve2("./fennec.config.yaml");
|
|
309
|
-
if (
|
|
1545
|
+
if (existsSync3(configFile)) {
|
|
310
1546
|
const overwrite = await confirmPrompt(
|
|
311
|
-
"fennec.config.yaml already exists. Overwrite
|
|
1547
|
+
`${pc4.yellow("fennec.config.yaml")} already exists. Overwrite?`,
|
|
312
1548
|
false
|
|
313
1549
|
);
|
|
314
1550
|
if (!overwrite) {
|
|
315
|
-
console.
|
|
1551
|
+
console.error(` ${pc4.dim("Cancelled.")}
|
|
1552
|
+
`);
|
|
316
1553
|
return;
|
|
317
1554
|
}
|
|
318
1555
|
}
|
|
1556
|
+
const spinner = createSpinner("Generating configuration...");
|
|
319
1557
|
const config = `# Fennec Configuration
|
|
320
1558
|
# Generated by 'fennec init'
|
|
321
1559
|
|
|
@@ -390,11 +1628,13 @@ logging:
|
|
|
390
1628
|
file: null
|
|
391
1629
|
`;
|
|
392
1630
|
writeFileSync(configFile, config, "utf-8");
|
|
393
|
-
|
|
1631
|
+
spinner.succeed(`Configuration written to ${pc4.bold(configFile)}`);
|
|
1632
|
+
console.error(`
|
|
1633
|
+
${pc4.dim("Edit the file to customize Fennec behavior.")}
|
|
394
1634
|
`);
|
|
395
1635
|
}
|
|
396
1636
|
main().catch((error) => {
|
|
397
|
-
console.error("Fatal error
|
|
1637
|
+
console.error(renderError("Fatal error", String(error)));
|
|
398
1638
|
process.exit(1);
|
|
399
1639
|
});
|
|
400
1640
|
//# sourceMappingURL=index.js.map
|