@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/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 existsSync2, writeFileSync } from "fs";
5
- import { resolve as resolve2 } from "path";
6
- import { createInterface } from "readline";
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
- console.error(`
13
- /\\ /\\
14
- ( o o ) fennec v1.9.0
15
- =( Y )= ears everywhere in your stack.
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
- Commands:
26
- start Start the MCP server (default)
27
- --transport Transport type (stdio | sse) [default: stdio]
28
- --port SSE port [default: 3333]
29
- --config Path to config file
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
- pipe Pipe stdin to log watcher
32
- --name Watcher name (required)
97
+ ${pc2.cyan("pipe")} ${pc2.dim("Pipe stdin to log watcher")}
98
+ ${pc2.dim("--name")} Watcher name ${pc2.dim("(required)")}
33
99
 
34
- attach-pid Attach to a running process by PID
35
- <pid> Process ID (required)
36
- --name Name for the process
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
- attach-port Attach to a process by port
39
- <port> Port number (required)
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
- watch Watch a log file
43
- --file File path (required)
44
- --name Watcher name
107
+ ${sep}
108
+ ${pc2.bold("Configuration")}
109
+ ${sep}
45
110
 
46
- sessions List saved auth sessions
47
- setup Configure MCP client (Claude Desktop)
48
- install-browsers Install Playwright browser engines
49
- init Generate fennec.config.yaml
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
- help Show this help message
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/index.ts
163
- var [, , command, ...args] = process.argv;
164
- function rlQuestion(query) {
165
- const rl = createInterface({ input: process.stdin, output: process.stdout });
166
- return new Promise((resolve3) => {
167
- rl.question(query, (answer) => {
168
- rl.close();
169
- resolve3(answer.trim());
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
- async function confirmPrompt(message, defaultValue = false) {
174
- const hint = defaultValue ? "Y/n" : "y/N";
175
- const answer = await rlQuestion(`
176
- ${message} (${hint}): `);
177
- if (!answer) return defaultValue;
178
- return answer.toLowerCase() === "y" || answer.toLowerCase() === "yes";
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
- console.log(` ${i + 1}) ${opt.label}`);
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 /* @__PURE__ */ Symbol("cancel");
433
+ if (isNaN(num) || num === 0) return null;
192
434
  const selected = options[num - 1];
193
435
  if (!selected) {
194
- console.log(" Invalid selection. Cancelled.");
195
- return /* @__PURE__ */ Symbol("cancel");
436
+ console.log(` ${pc3.red("Invalid selection.")}`);
437
+ return null;
196
438
  }
197
439
  return selected.value;
198
440
  }
199
- function simpleSpinner(text) {
200
- const frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
201
- let i = 0;
202
- const interval = setInterval(() => {
203
- process.stdout.write(`\r${frames[i]} ${text}`);
204
- i = (i + 1) % frames.length;
205
- }, 80);
206
- return {
207
- stop(message) {
208
- clearInterval(interval);
209
- process.stdout.write(`\r${message ? "\u2713" : " "} ${message ?? text}
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
- var isCancel = (v) => v === /* @__PURE__ */ Symbol("cancel");
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
- await startServer(args);
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 server = new FennecServer(configPath);
249
- await server.start();
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.log("No saved sessions found.");
1464
+ console.error(`
1465
+ ${pc4.dim("No saved sessions found.")}
1466
+ `);
256
1467
  return;
257
1468
  }
258
- console.log("Saved sessions:");
259
- for (const s of sessions) {
260
- console.log(` ${s.name} at ${s.origin} (saved: ${new Date(s.savedAt).toLocaleString()})`);
261
- }
262
- console.log(`
263
- Total: ${sessions.length} session(s)`);
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.log("\n Fennec Setup\n");
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: "other", label: "Other MCP client" }
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 (isCancel(mcpClient)) {
272
- console.log("Setup cancelled");
1496
+ if (!mcpClient) {
1497
+ console.error(` ${pc4.dim("Setup cancelled.")}
1498
+ `);
273
1499
  return;
274
1500
  }
275
- if (mcpClient === "claude") {
276
- console.log(`
277
- To configure Claude Desktop for Fennec, add to your config:
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.log("Setup complete! Run 'fennec start' to begin.\n");
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.log("\n Installing browser engines\n");
293
- const s = simpleSpinner("Installing Chromium...");
1522
+ console.error(`
1523
+ ${pc4.bold("Installing Browser Engines")}
1524
+ `);
1525
+ const spinner = createSpinner("Installing Chromium...");
294
1526
  try {
295
- execSync("npx playwright install chromium", {
296
- stdio: "inherit",
1527
+ execSync2("npx playwright install chromium", {
1528
+ stdio: "pipe",
297
1529
  timeout: 12e4
298
1530
  });
299
- s.stop("Chromium installed successfully");
1531
+ spinner.succeed("Chromium installed successfully");
300
1532
  } catch {
301
- s.stop("Failed to install Chromium");
302
- console.error("Try running manually: npx playwright install chromium");
1533
+ spinner.fail("Failed to install Chromium");
1534
+ console.error(` ${pc4.yellow("\u2192")} Try running: ${renderCommand("npx playwright install chromium")}`);
303
1535
  }
304
- console.log("\nBrowser installation complete.\n");
1536
+ console.error(`
1537
+ ${pc4.green("\u2713")} Browser installation complete.
1538
+ `);
305
1539
  }
306
1540
  async function initCommand() {
307
- console.log("\n Initialize Fennec Configuration\n");
1541
+ console.error(`
1542
+ ${pc4.bold("Initialize Fennec Configuration")}
1543
+ `);
308
1544
  const configFile = resolve2("./fennec.config.yaml");
309
- if (existsSync2(configFile)) {
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.log("Cancelled");
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
- console.log(`Configuration written to ${configFile}
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:", error);
1637
+ console.error(renderError("Fatal error", String(error)));
398
1638
  process.exit(1);
399
1639
  });
400
1640
  //# sourceMappingURL=index.js.map