pyre-cli 10.2.0 → 10.4.0

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.
Files changed (88) hide show
  1. package/README.md +75 -18
  2. package/dist/{alertChannels-TPSQIJ7S.js → alertChannels-MNX4XH6J.js} +1 -0
  3. package/dist/{anomaliesCmd-25OFKYPK.js → anomaliesCmd-HHTY5LYL.js} +2 -0
  4. package/dist/benchGitCmd-HIXUC5EK.js +87 -0
  5. package/dist/{benchmarkCmd-A3ETK6VY.js → benchmarkCmd-B7DLP2VC.js} +2 -0
  6. package/dist/{blender-FGH3ASEQ.js → blender-6ZX5SPYC.js} +3 -1
  7. package/dist/{brewHealth-P2GW5Y5C.js → brewHealth-VFW33QB3.js} +1 -0
  8. package/dist/budgetCmd-TGO3RYLN.js +11 -0
  9. package/dist/{buildCmd-CDPZ2ZFA.js → buildCmd-E7KUCFYG.js} +3 -1
  10. package/dist/{checkCmd-WQAHAUWO.js → checkCmd-QLYTP77Y.js} +5 -3
  11. package/dist/chunk-3RG5ZIWI.js +10 -0
  12. package/dist/{chunk-VIACN2RU.js → chunk-FYCBGUTW.js} +41 -19
  13. package/dist/chunk-JH7P6KLN.js +143 -0
  14. package/dist/{chunk-QM5L43EK.js → chunk-LDJB5V4N.js} +5 -1
  15. package/dist/chunk-NMGQVRCA.js +111 -0
  16. package/dist/chunk-OCL7P7N7.js +936 -0
  17. package/dist/{chunk-23YWCI3O.js → chunk-QEH4LR6P.js} +28 -686
  18. package/dist/{chunk-KVUMOA6C.js → chunk-RKPGPVTO.js} +77 -4
  19. package/dist/chunk-UF3XQ3R3.js +3800 -0
  20. package/dist/{collectors-JDN5Y7EV.js → collectors-V7SCIIDH.js} +5 -3
  21. package/dist/{completions-ZVTNE52B.js → completions-FTPXDSR2.js} +44 -3
  22. package/dist/{config-J4YMADEF.js → config-5WW5M63G.js} +2 -1
  23. package/dist/containersCmd-FSMYOBG2.js +84 -0
  24. package/dist/{doctor-LHFW4UJP.js → doctor-O4DIAMHK.js} +3 -2
  25. package/dist/{explainCmd-MF6PIMLW.js → explainCmd-7PBPA7O5.js} +1 -0
  26. package/dist/{extensions-XJGHG7X3.js → extensions-QQEV7U3D.js} +1 -0
  27. package/dist/{firebaseSync-ON3RZ6KX.js → firebaseSync-LRN67UTV.js} +2 -0
  28. package/dist/flightRecorder-GJIAQOXC.js +162 -0
  29. package/dist/index.js +737 -4125
  30. package/dist/inspectCmd-5FXJBJTK.js +137 -0
  31. package/dist/menu-AJFGDLBP.js +1233 -0
  32. package/dist/monitors/platform/windows-ui.ps1 +31 -29
  33. package/dist/{netUsageCmd-67IBZZYN.js → netUsageCmd-JP6YKBPF.js} +7 -5
  34. package/dist/optimizeCmd-WZKLFYZU.js +1110 -0
  35. package/dist/{pipeCmd-2N4JPKYE.js → pipeCmd-CPK6FVKB.js} +5 -3
  36. package/dist/playbookCmd-FRROIEXR.js +145 -0
  37. package/dist/{profileProcCmd-N4H6MCBT.js → profileProcCmd-5Y3733E7.js} +64 -13
  38. package/dist/{prometheus-5BL5PVLC.js → prometheus-CYNUR3QT.js} +1 -0
  39. package/dist/{prometheusCmd-LEUM5WCZ.js → prometheusCmd-G2CSEZRQ.js} +5 -3
  40. package/dist/{replayCmd-Z3PYXY7V.js → replayCmd-ODB5SPOQ.js} +25 -6
  41. package/dist/{run-HOGQDDTA.js → run-UJQYT2TO.js} +1 -0
  42. package/dist/{serveCmd-L7LVJOZN.js → serveCmd-KDPWLVQI.js} +5 -3
  43. package/dist/{service-7GIX3YNC.js → service-LIYYR3UI.js} +1 -0
  44. package/dist/{smartCmd-ODWKSFNG.js → smartCmd-2BJJ37GV.js} +130 -5
  45. package/dist/statuslineCmd-EUKYRR2G.js +75 -0
  46. package/dist/{stressCmd-MFXMFXYX.js → stressCmd-GKHGY26X.js} +2 -0
  47. package/dist/{syncCmd-42IRF633.js → syncCmd-MIBFKX24.js} +16 -5
  48. package/dist/topoCmd-RTXQJ6XA.js +189 -0
  49. package/dist/{updateCheck-52X6LJG5.js → updateCheck-LJHEMG3G.js} +2 -0
  50. package/dist/{watchdogCmd-6GR4UN6B.js → watchdogCmd-XM6OPTKM.js} +41 -6
  51. package/dist/{wizard-JQMQWFAW.js → wizard-PRDOJROQ.js} +2 -1
  52. package/package.json +2 -2
  53. package/src/benchGitCmd.ts +110 -0
  54. package/src/budgetCmd.ts +199 -0
  55. package/src/completions.ts +42 -3
  56. package/src/containersCmd.ts +106 -0
  57. package/src/doctor.ts +86 -6
  58. package/src/easy_features.test.ts +83 -0
  59. package/src/flightRecorder.ts +214 -0
  60. package/src/index.ts +183 -9
  61. package/src/inspectCmd.ts +165 -0
  62. package/src/live/input.ts +70 -3
  63. package/src/live/render.ts +1 -1
  64. package/src/live/state.ts +2 -1
  65. package/src/menu/commands.ts +754 -0
  66. package/src/menu/index.ts +358 -0
  67. package/src/menu/render.ts +272 -0
  68. package/src/menu/types.ts +63 -0
  69. package/src/menu.test.ts +110 -0
  70. package/src/monitors/blender.ts +49 -19
  71. package/src/monitors/buildTracker.ts +72 -54
  72. package/src/monitors/platform/windows-ui.ps1 +31 -29
  73. package/src/monitors/platform/windows.ts +613 -280
  74. package/src/optimizeCmd.ts +1248 -0
  75. package/src/platform.test.ts +135 -0
  76. package/src/playbookCmd.ts +156 -0
  77. package/src/power_features.test.ts +251 -0
  78. package/src/profileProcCmd.ts +68 -21
  79. package/src/replayCmd.ts +15 -6
  80. package/src/smartCmd.ts +149 -10
  81. package/src/state/config.ts +13 -0
  82. package/src/statuslineCmd.ts +87 -0
  83. package/src/syncCmd.ts +12 -0
  84. package/src/topoCmd.ts +83 -50
  85. package/src/watchdogCmd.ts +40 -3
  86. package/src/xbar.ts +195 -17
  87. package/dist/chunk-M73HCQD2.js +0 -95
  88. package/dist/topoCmd-DZ3YS5IL.js +0 -148
package/src/index.ts CHANGED
@@ -9,6 +9,7 @@
9
9
  import fs from 'node:fs';
10
10
  import os from 'node:os';
11
11
  import path from 'node:path';
12
+ import crypto from 'node:crypto';
12
13
  import { fileURLToPath } from 'node:url';
13
14
  import readline from 'node:readline';
14
15
  import { Command } from 'commander';
@@ -101,10 +102,34 @@ program
101
102
  .option('--top <n>', 'Number of items to display in netusage command (default: 15)')
102
103
  .option('--raw', 'Print unparsed output for profiler')
103
104
  .option('--sync-dir <dir>', 'Custom directory path for cloud profile/config sync')
104
- .option('--scan', 'Perform active network sweep on LAN neighbors for topology map');
105
+ .option('--scan', 'Perform active network sweep on LAN neighbors for topology map')
106
+ .option('--idle <duration>', 'Idle threshold for optimize apps (e.g. 30m, 2h)')
107
+ .option('--keep <n>', 'Always keep the N most recently used apps open (optimize)')
108
+ .option('-y, --yes', 'Skip confirmation prompts (optimize)')
109
+ .option('--force', 'Terminate apps that do not quit gracefully (optimize apps)')
110
+ .option('--clean', 'Delete measured caches after confirmation (optimize caches)')
111
+ .option('--empty', 'Empty the Trash after confirmation (optimize trash)')
112
+ .option('--auto-quit <duration>', 'Let the optimize tracker automatically quit apps idle this long')
113
+ .option('-m, --menu', 'Launch interactive TUI command center & tools navigator')
114
+ .option('-i, --interactive', 'Alias for --menu')
115
+ .option('--budget-cpu <pct>', 'Maximum allowable peak CPU % for pyre run')
116
+ .option('--budget-mem <bytes>', 'Maximum allowable peak memory for pyre run (e.g. 500MB, 2GB)')
117
+ .option('--budget-time <sec>', 'Maximum allowable duration for pyre run in seconds')
118
+ .option('--nice <val>', 'Nice value for pyre inspect --nice (-20 to 19)')
119
+ .option('--format <format>', 'Output format for statusline (tmux, starship, json, pango, plain)');
105
120
 
106
121
  program.addHelpText('after', `
122
+ 💡 Tip: Can't remember all commands? Run 'pyre menu' to browse and run all tools interactively!
123
+
107
124
  Commands:
125
+ menu, tui, launcher Interactive TUI command center & tools navigator
126
+ playbook [run|list] Execute sequenced multi-step macro recipes (audit, clean, triage)
127
+ flight [dump|status|record] Forensic flight recorder black box in RAM
128
+ run [options] -- <cmd...> Performance budgeting runner for CI/CD gates
129
+ bench-git <b1> <b2> <cmd> Cross-branch git performance differ
130
+ statusline Headless terminal statusline string (tmux, starship, neovim)
131
+ inspect <pid> Process deep-dive inspector (lsof, sockets, nice)
132
+ containers Container and micro-VM telemetry (Docker, Podman, OrbStack)
108
133
  live Interactive live dashboard
109
134
  check One-line plain-English health summary
110
135
  pipe Continuous newline-delimited JSON stream for scripting
@@ -142,6 +167,8 @@ program.addHelpText('after', `
142
167
  build Active compiler and build system tracker
143
168
  topo Network topology mapper and pyre peer auto-discovery
144
169
  sync <push|pull|status> Synchronize config profiles across Macs via iCloud/Drive
170
+ optimize [apps|track|memory|caches|trash|startup|dns]
171
+ Mac optimizer: quit idle apps, free memory, clean caches
145
172
  `);
146
173
 
147
174
  program.parse(process.argv);
@@ -202,7 +229,7 @@ async function runServerCommand(): Promise<void> {
202
229
  }
203
230
 
204
231
  const port = 9876;
205
- const password = 'mysecret';
232
+ const password = crypto.randomBytes(12).toString('hex');
206
233
 
207
234
  console.log(chalk.bold(`\n pyre server`));
208
235
  console.log(chalk.dim(` Detected IP: ${ip}`));
@@ -225,6 +252,106 @@ async function main() {
225
252
 
226
253
  const cmd = program.args[0];
227
254
 
255
+ if (cmd === 'menu' || cmd === 'tui' || cmd === 'launcher' || cmd === 'interactive' || opts.menu || opts.interactive) {
256
+ const { runMenu } = await import('./menu/index.js');
257
+ await runMenu({
258
+ theme: opts.theme || config.theme,
259
+ });
260
+ return;
261
+ }
262
+
263
+ if (!cmd && config.defaultAction === 'menu' && !isExportMode() && !opts.once && process.stdout.isTTY) {
264
+ const { runMenu } = await import('./menu/index.js');
265
+ await runMenu({
266
+ theme: opts.theme || config.theme,
267
+ });
268
+ return;
269
+ }
270
+
271
+ if (cmd === 'playbook') {
272
+ const sub = program.args[1];
273
+ const { runPlaybook, listPlaybooks } = await import('./playbookCmd.js');
274
+ if (sub === 'list' || !sub) {
275
+ listPlaybooks();
276
+ } else if (sub === 'run') {
277
+ const id = program.args[2] || 'audit';
278
+ await runPlaybook(id);
279
+ } else {
280
+ await runPlaybook(sub);
281
+ }
282
+ return;
283
+ }
284
+
285
+ if (cmd === 'flight') {
286
+ const sub = program.args[1] || 'dump';
287
+ const { runFlightCommand } = await import('./flightRecorder.js');
288
+ await runFlightCommand(sub, {
289
+ out: opts.out,
290
+ duration: opts.duration ? parseInt(opts.duration, 10) : undefined,
291
+ });
292
+ return;
293
+ }
294
+
295
+ if (cmd === 'run') {
296
+ const rawArgs = process.argv;
297
+ const dashDashIdx = rawArgs.indexOf('--');
298
+ const targetCommand = dashDashIdx !== -1 ? rawArgs.slice(dashDashIdx + 1) : program.args.slice(1);
299
+ const { runWithBudget, parseBytesString } = await import('./budgetCmd.js');
300
+ const res = await runWithBudget(targetCommand, {
301
+ maxCpuPct: opts.budgetCpu ? parseFloat(opts.budgetCpu) : undefined,
302
+ maxMemoryBytes: opts.budgetMem ? parseBytesString(opts.budgetMem) : undefined,
303
+ maxTimeSec: opts.budgetTime ? parseFloat(opts.budgetTime) : undefined,
304
+ }, {
305
+ markdown: !!opts.markdown || !!opts.md,
306
+ outFile: opts.out,
307
+ });
308
+ if (res.exitCode !== 0) {
309
+ process.exit(res.exitCode);
310
+ }
311
+ if (res.violations.length > 0) {
312
+ process.exit(124);
313
+ }
314
+ return;
315
+ }
316
+
317
+ if (cmd === 'bench-git') {
318
+ const branchA = program.args[1];
319
+ const branchB = program.args[2];
320
+ const commandStr = program.args.slice(3).join(' ');
321
+ if (!branchA || !branchB || !commandStr) {
322
+ console.log(chalk.red(' ✖ Usage: pyre bench-git <branchA> <branchB> "<command>"'));
323
+ process.exit(1);
324
+ }
325
+ const { runBenchGitCommand } = await import('./benchGitCmd.js');
326
+ await runBenchGitCommand(branchA, branchB, commandStr);
327
+ return;
328
+ }
329
+
330
+ if (cmd === 'statusline') {
331
+ const { runStatuslineCommand } = await import('./statuslineCmd.js');
332
+ await runStatuslineCommand(opts.format || 'plain');
333
+ return;
334
+ }
335
+
336
+ if (cmd === 'inspect') {
337
+ const targetPid = parseInt(program.args[1], 10);
338
+ const { runInspectCommand } = await import('./inspectCmd.js');
339
+ const niceVal = opts.nice !== undefined ? parseInt(opts.nice, 10) : undefined;
340
+ await runInspectCommand(targetPid, {
341
+ nice: niceVal,
342
+ json: !!opts.json,
343
+ });
344
+ return;
345
+ }
346
+
347
+ if (cmd === 'containers') {
348
+ const { runContainersCommand } = await import('./containersCmd.js');
349
+ await runContainersCommand({
350
+ json: !!opts.json,
351
+ });
352
+ return;
353
+ }
354
+
228
355
  if (cmd === 'check') {
229
356
  const { runCheckCommand } = await import('./checkCmd.js');
230
357
  await runCheckCommand({
@@ -308,6 +435,23 @@ async function main() {
308
435
  return;
309
436
  }
310
437
 
438
+ if (cmd === 'optimize') {
439
+ const { runOptimizeCommand } = await import('./optimizeCmd.js');
440
+ await runOptimizeCommand(program.args[1], program.args.slice(2), {
441
+ idle: opts.idle,
442
+ keep: opts.keep,
443
+ yes: !!opts.yes,
444
+ force: !!opts.force,
445
+ clean: !!opts.clean,
446
+ empty: !!opts.empty,
447
+ dryRun: !!opts.dryRun,
448
+ install: !!opts.install,
449
+ uninstall: !!opts.uninstall,
450
+ autoQuit: opts.autoQuit,
451
+ });
452
+ return;
453
+ }
454
+
311
455
  if (cmd === 'replay') {
312
456
  const file = program.args[1];
313
457
  if (!file) {
@@ -579,7 +723,12 @@ async function main() {
579
723
  }
580
724
 
581
725
  if (cmd === 'xbar') {
582
- generateXbarPlugin();
726
+ const sub = program.args[1];
727
+ await generateXbarPlugin({
728
+ install: !!opts.install || sub === 'install',
729
+ yes: !!opts.yes,
730
+ dir: opts.dir,
731
+ });
583
732
  return;
584
733
  }
585
734
 
@@ -623,9 +772,13 @@ async function main() {
623
772
  if (opts.alertPushoverToken) config.pushoverToken = opts.alertPushoverToken;
624
773
  if (opts.alertPushoverUser) config.pushoverUser = opts.alertPushoverUser;
625
774
  if (opts.alertNtfy) config.ntfyUrl = opts.alertNtfy;
626
- const tempUnit = opts.tempUnit === 'f' ? 'f' : 'c';
775
+ if (cmd && cmd !== 'live' && cmd !== 'p2p') {
776
+ console.error(chalk.red(`\n ✖ Unknown command: '${cmd}'`));
777
+ console.log(chalk.yellow(` 💡 Tip: Can't remember a command? Run 'pyre menu' to browse and run all tools interactively!\n`));
778
+ process.exit(1);
779
+ }
627
780
 
628
- if (cmd === 'live' || (!isExportMode() && !opts.once && cmd !== 'p2p')) {
781
+ if (cmd === 'live' || (!cmd && !isExportMode() && !opts.once)) {
629
782
  if (!process.stdout.isTTY && cmd !== 'live') {
630
783
  await runWebCommand();
631
784
  return;
@@ -639,6 +792,7 @@ async function main() {
639
792
  animation: config.splashAnimation as any,
640
793
  });
641
794
  const interval = parseFloat(opts.interval) || 2;
795
+ const tempUnit: 'c' | 'f' = (opts.tempUnit?.toLowerCase() === 'f' || config.tempUnit === 'f') ? 'f' : 'c';
642
796
  await startLive({
643
797
  interval,
644
798
  detailed: opts.detailed ?? config.detailed,
@@ -673,13 +827,14 @@ async function main() {
673
827
 
674
828
  const data = await collectAll({ detailed: opts.detailed, processLimit });
675
829
 
830
+ const tempUnit: 'c' | 'f' = (opts.tempUnit?.toLowerCase() === 'f' || config.tempUnit === 'f') ? 'f' : 'c';
676
831
  let output: string;
677
832
  if (opts.json) output = formatJson(data);
678
833
  else if (opts.html) output = formatHtml(data);
679
834
  else if (opts.md) output = formatMarkdown(data);
680
835
  else if (opts.csv) output = formatCsv(data);
681
836
  else if (opts.tsv) output = formatTsv(data);
682
- else output = formatTable(data, { width: process.stdout.columns || 80, sortBy: opts.sort, treeView: opts.tree ?? config.treeView, minimal: !!opts.minimal, visible: { packets: opts.packets ? true : undefined } });
837
+ else output = formatTable(data, { width: process.stdout.columns || 80, sortBy: opts.sort, treeView: opts.tree ?? config.treeView, minimal: !!opts.minimal, visible: { packets: opts.packets ? true : undefined }, tempUnit });
683
838
 
684
839
  console.log(output);
685
840
 
@@ -931,6 +1086,25 @@ async function runInfoCommand(): Promise<void> {
931
1086
  }
932
1087
  }
933
1088
 
1089
+ if (process.platform === 'win32') {
1090
+ lines.push('');
1091
+ lines.push(chalk.cyan.bold(' ⏱ System Restore & Recovery:'));
1092
+ try {
1093
+ const { runPowerShell } = await import('./monitors/run.js');
1094
+ const srRaw = await runPowerShell(
1095
+ `(Get-CimInstance -Namespace root/default -ClassName SystemRestore -ErrorAction SilentlyContinue | Select-Object -Last 1 -ExpandProperty Description) -join ''`,
1096
+ ''
1097
+ );
1098
+ if (srRaw.trim()) {
1099
+ lines.push(` • Latest Restore Point: ${chalk.green(srRaw.trim())}`);
1100
+ } else {
1101
+ lines.push(' • System Restore active (no custom checkpoints)');
1102
+ }
1103
+ } catch {
1104
+ lines.push(' • System Restore available');
1105
+ }
1106
+ }
1107
+
934
1108
  console.log(lines.join('\n'));
935
1109
  }
936
1110
 
@@ -1728,10 +1902,10 @@ async function runBenchCommand(benchCmd: string): Promise<void> {
1728
1902
  let powerSamplesCount = 0;
1729
1903
 
1730
1904
  const child = process.platform === 'win32'
1731
- ? spawn('cmd.exe', ['/c', benchCmd], { stdio: 'inherit' })
1905
+ ? spawn('cmd.exe', ['/c', benchCmd])
1732
1906
  : spawn('sh', ['-c', benchCmd]);
1733
- child.stdout.on('data', d => process.stdout.write(d));
1734
- child.stderr.on('data', d => process.stdout.write(d));
1907
+ if (child.stdout) child.stdout.on('data', d => process.stdout.write(d));
1908
+ if (child.stderr) child.stderr.on('data', d => process.stdout.write(d));
1735
1909
 
1736
1910
  const interval = parseFloat(opts.interval) || 2;
1737
1911
  const handle = setInterval(async () => {
@@ -0,0 +1,165 @@
1
+ /**
2
+ * pyre inspect — Deep-Dive Process Inspector.
3
+ *
4
+ * Inspects low-level attributes for a running PID:
5
+ * open file handles (lsof), active sockets, thread count,
6
+ * nice priority, and memory regions.
7
+ */
8
+ import os from 'node:os';
9
+ import { execSync } from 'node:child_process';
10
+ import chalk from 'chalk';
11
+ import { formatBytes } from './budgetCmd.js';
12
+
13
+ export interface ProcessDetails {
14
+ pid: number;
15
+ name: string;
16
+ user: string;
17
+ ppid: number;
18
+ threads: number;
19
+ nice: number;
20
+ cpuPct: number;
21
+ memRssBytes: number;
22
+ openFiles: string[];
23
+ sockets: string[];
24
+ }
25
+
26
+ export function inspectProcess(pid: number): ProcessDetails {
27
+ // Check if process exists
28
+ try {
29
+ process.kill(pid, 0);
30
+ } catch (err: any) {
31
+ if (err.code === 'ESRCH') {
32
+ throw new Error(`Process with PID ${pid} not found or permission denied.`);
33
+ }
34
+ }
35
+
36
+ // Query ps metadata
37
+ let name = 'unknown';
38
+ let user = 'unknown';
39
+ let ppid = 0;
40
+ let threads = 1;
41
+ let nice = 0;
42
+ let cpuPct = 0;
43
+ let memRssBytes = 0;
44
+
45
+ try {
46
+ const psOut = execSync(`ps -p ${pid} -o user,ppid,nice,%cpu,rss,comm 2>/dev/null`, { encoding: 'utf-8' });
47
+ const lines = psOut.trim().split('\n');
48
+ if (lines.length > 1) {
49
+ const parts = lines[1].trim().split(/\s+/);
50
+ user = parts[0] || 'unknown';
51
+ ppid = parseInt(parts[1], 10) || 0;
52
+ nice = parseInt(parts[2], 10) || 0;
53
+ cpuPct = parseFloat(parts[3]) || 0;
54
+ memRssBytes = (parseInt(parts[4], 10) || 0) * 1024;
55
+ name = parts.slice(5).join(' ') || 'unknown';
56
+ }
57
+ } catch {
58
+ // Fallback if ps is restricted or unavailable
59
+ if (pid === process.pid) {
60
+ try {
61
+ user = os.userInfo().username || 'current';
62
+ } catch {
63
+ user = process.env.USER || 'current';
64
+ }
65
+ ppid = process.ppid || 0;
66
+ name = process.title || 'node';
67
+ memRssBytes = process.memoryUsage().rss;
68
+ threads = 1;
69
+ }
70
+ }
71
+
72
+ // Query lsof for open files and network sockets
73
+ const openFiles: string[] = [];
74
+ const sockets: string[] = [];
75
+
76
+ try {
77
+ const lsofOut = execSync(`lsof -p ${pid} -F tn 2>/dev/null`, { encoding: 'utf-8' });
78
+ const lines = lsofOut.split('\n');
79
+ let curType = '';
80
+
81
+ for (const l of lines) {
82
+ if (l.startsWith('t')) {
83
+ curType = l.slice(1).trim();
84
+ } else if (l.startsWith('n')) {
85
+ const nameVal = l.slice(1).trim();
86
+ if (curType === 'IPv4' || curType === 'IPv6' || nameVal.includes(':') || nameVal.includes('->')) {
87
+ sockets.push(nameVal);
88
+ } else if (nameVal.startsWith('/') && !openFiles.includes(nameVal)) {
89
+ openFiles.push(nameVal);
90
+ }
91
+ }
92
+ }
93
+ } catch {
94
+ // lsof may not have permissions for system PIDs without sudo
95
+ }
96
+
97
+ return {
98
+ pid,
99
+ name,
100
+ user,
101
+ ppid,
102
+ threads,
103
+ nice,
104
+ cpuPct,
105
+ memRssBytes,
106
+ openFiles: openFiles.slice(0, 15),
107
+ sockets: sockets.slice(0, 15),
108
+ };
109
+ }
110
+
111
+ export async function runInspectCommand(pid: number, opts?: { nice?: number; json?: boolean }): Promise<void> {
112
+ if (isNaN(pid) || pid <= 0) {
113
+ console.error(chalk.red(' ✖ Invalid PID. Usage: pyre inspect <pid>'));
114
+ return;
115
+ }
116
+
117
+ // If renicing requested
118
+ if (opts?.nice !== undefined) {
119
+ try {
120
+ execSync(`renice ${opts.nice} -p ${pid}`, { stdio: 'inherit' });
121
+ console.log(chalk.green(` ✔ Successfully set nice priority for PID ${pid} to ${opts.nice}\n`));
122
+ } catch (err: any) {
123
+ console.error(chalk.red(` ✖ Failed to renice PID ${pid}: ${err.message}`));
124
+ }
125
+ }
126
+
127
+ try {
128
+ const details = inspectProcess(pid);
129
+
130
+ if (opts?.json) {
131
+ console.log(JSON.stringify(details, null, 2));
132
+ return;
133
+ }
134
+
135
+ console.log(chalk.bold(`\n 🔥 pyre inspect — Process Deep Dive (PID ${details.pid})\n`));
136
+ console.log(` Process: ${chalk.bold.hex('#ff6a39')(details.name)}`);
137
+ console.log(` User: ${details.user}`);
138
+ console.log(` PPID: ${details.ppid}`);
139
+ console.log(` CPU Usage: ${details.cpuPct.toFixed(1)}%`);
140
+ console.log(` Memory RSS: ${formatBytes(details.memRssBytes)}`);
141
+ console.log(` Nice Value: ${details.nice} (Priority: ${details.nice < 0 ? 'High' : details.nice > 0 ? 'Low' : 'Normal'})`);
142
+ console.log(chalk.dim(' ─────────────────────────────────────────────────────────────'));
143
+
144
+ // Open sockets
145
+ console.log(chalk.bold.cyan('\n Active Network Sockets:'));
146
+ if (details.sockets.length === 0) {
147
+ console.log(chalk.dim(' No open sockets detected (or permission denied).'));
148
+ } else {
149
+ details.sockets.forEach((s) => console.log(` • ${s}`));
150
+ }
151
+
152
+ // Open file descriptors
153
+ console.log(chalk.bold.yellow('\n Open File Descriptors:'));
154
+ if (details.openFiles.length === 0) {
155
+ console.log(chalk.dim(' No open file handles detected.'));
156
+ } else {
157
+ details.openFiles.forEach((f) => console.log(` • ${chalk.dim(f)}`));
158
+ }
159
+
160
+ console.log(chalk.dim('\n ─────────────────────────────────────────────────────────────'));
161
+ console.log(chalk.dim(` Adjust priority: pyre inspect ${pid} --nice <value -20 to 19>\n`));
162
+ } catch (err: any) {
163
+ console.error(chalk.red(`\n ✖ ${err.message}\n`));
164
+ }
165
+ }
package/src/live/input.ts CHANGED
@@ -7,6 +7,7 @@
7
7
  * Imported by `index.ts` as the public API of the live module.
8
8
  */
9
9
  import readline from 'node:readline';
10
+ import { execSync } from 'node:child_process';
10
11
  import chalk from 'chalk';
11
12
  import { collectAll, StatsData } from '../monitors/index.js';
12
13
  import { THEMES, type ThemeName, type VisibleItems, getTabHitboxes, TAB_BAR_ROW } from '../formatters/index.js';
@@ -218,6 +219,9 @@ function isEscKey(key: readline.Key, str?: string): boolean {
218
219
  case 'Resume Dashboard':
219
220
  state.inputMode = null;
220
221
  break;
222
+ case 'Command Center (All Tools)':
223
+ openCommandCenter();
224
+ return;
221
225
  case 'Settings (Customizer)':
222
226
  state.inputMode = 'customizer';
223
227
  state.customizerIndex = 0;
@@ -437,20 +441,31 @@ function killProcess(pidStr: string, signal: string = 'SIGTERM') {
437
441
  return;
438
442
  }
439
443
 
440
- const isProtected = pid === 1 || pid === process.pid || pid === process.ppid;
444
+ const isProtected = pid === 1 || pid === 0 || pid === 4 || pid === process.pid || pid === process.ppid;
441
445
  if (isProtected && confirmKillPid !== pid) {
442
446
  confirmKillPid = pid;
443
- const name = pid === 1 ? 'launchd (system init)' : (pid === process.pid ? 'pyre itself' : 'parent process');
447
+ const name = (pid === 0 || pid === 4) ? 'Windows System Kernel' : (pid === 1 ? 'init/launchd' : (pid === process.pid ? 'pyre itself' : 'parent process'));
444
448
  setStatus(`⚠️ PROTECTED PID ${pid} (${name})! Re-enter PID ${pid} to confirm termination.`, 8000);
445
449
  return;
446
450
  }
447
451
 
448
452
  confirmKillPid = null;
453
+
454
+ if (process.platform === 'win32') {
455
+ try {
456
+ execSync(`taskkill /F /PID ${pid} /T`, { stdio: 'pipe' });
457
+ setStatus(`Terminated PID ${pid} (taskkill /F /T)`);
458
+ return;
459
+ } catch (err: any) {
460
+ // fallback to process.kill
461
+ }
462
+ }
463
+
449
464
  try {
450
465
  process.kill(pid, signal as NodeJS.Signals);
451
466
  setStatus(`Sent ${signal} to PID ${pid}`);
452
467
  } catch (err: any) {
453
- setStatus(`Failed to send ${signal} to ${pid}: ${err.message}`);
468
+ setStatus(`Failed to terminate PID ${pid}: ${err.message}`);
454
469
  }
455
470
  }
456
471
 
@@ -576,6 +591,51 @@ function killProcess(pidStr: string, signal: string = 'SIGTERM') {
576
591
  }
577
592
  }
578
593
 
594
+ async function openCommandCenter() {
595
+ state.paused = true;
596
+ if (state.intervalHandle) {
597
+ clearInterval(state.intervalHandle);
598
+ state.intervalHandle = null;
599
+ }
600
+ if (state.uiIntervalHandle) {
601
+ clearInterval(state.uiIntervalHandle);
602
+ state.uiIntervalHandle = null;
603
+ }
604
+ if (state.keypressHandler) {
605
+ process.stdin.removeListener('keypress', state.keypressHandler);
606
+ }
607
+ process.stdout.removeListener('resize', onResize);
608
+ if (process.stdin.isTTY) process.stdin.setRawMode(false);
609
+ process.stdin.pause();
610
+ process.stdout.write('\x1b[?1000l\x1b[?25h\x1b[?1049l\x1b[2J\x1b[H');
611
+
612
+ const { runMenu } = await import('../menu/index.js');
613
+ await runMenu({
614
+ theme: state.currentTheme,
615
+ onExit: () => {},
616
+ });
617
+
618
+ process.stdout.write('\x1b[?1049h\x1b[?25l\x1b[2J\x1b[H');
619
+ if (state.mouseEnabled) {
620
+ process.stdout.write('\x1b[?1000h');
621
+ }
622
+ readline.emitKeypressEvents(process.stdin);
623
+ if (process.stdin.isTTY) process.stdin.setRawMode(true);
624
+ process.stdin.resume();
625
+ if (state.keypressHandler) {
626
+ process.stdin.on('keypress', state.keypressHandler);
627
+ }
628
+ process.stdout.on('resize', onResize);
629
+ state.paused = false;
630
+ state.inputMode = null;
631
+ restartTicker();
632
+ if (state.uiIntervalHandle) clearInterval(state.uiIntervalHandle);
633
+ state.uiIntervalHandle = setInterval(() => {
634
+ if (state.running && !state.paused) render();
635
+ }, 250);
636
+ render();
637
+ }
638
+
579
639
  /**
580
640
  * Start the interactive live dashboard.
581
641
  *
@@ -766,6 +826,10 @@ function killProcess(pidStr: string, signal: string = 'SIGTERM') {
766
826
  case 'right':
767
827
  cycleTab(1);
768
828
  break;
829
+ case 'o':
830
+ case 'O':
831
+ openCommandCenter();
832
+ return;
769
833
  case 'c':
770
834
  state.inputMode = 'customizer';
771
835
  state.customizerIndex = 0;
@@ -1047,6 +1111,9 @@ function killProcess(pidStr: string, signal: string = 'SIGTERM') {
1047
1111
 
1048
1112
  // Execute the command
1049
1113
  switch (cmd.id) {
1114
+ case 'command-center':
1115
+ openCommandCenter();
1116
+ break;
1050
1117
  case 'cpu': case 'mem': case 'gpu': case 'power': case 'battery':
1051
1118
  case 'thermal': case 'network': case 'packets': case 'tasks': case 'disk':
1052
1119
  case 'process': case 'containers': case 'p2p': case 'anomalies': case 'blender':
@@ -502,6 +502,7 @@ function render() {
502
502
  function footerLine(): string[] {
503
503
  const row1: [string, string][] = [
504
504
  ['q', 'quit'],
505
+ ['o', 'tools menu'],
505
506
  ['c', 'customize UI'],
506
507
  ['←/→', 'tab'],
507
508
  ['1-9', 'panels'],
@@ -509,7 +510,6 @@ function footerLine(): string[] {
509
510
  ['b', `graph:${state.graphMode}`],
510
511
  ['t', state.treeView ? 'flat' : 'tree'],
511
512
  ['d', state.detailed ? 'basic' : 'detailed'],
512
- ['T', `temp:${state.tempUnit}`],
513
513
  ['p', state.paused ? 'resume' : 'pause'],
514
514
  ];
515
515
  const row2: [string, string][] = [
package/src/live/state.ts CHANGED
@@ -19,9 +19,10 @@ import { type SplashColorScheme, type SplashAnimation } from '../splash.js';
19
19
 
20
20
  export { type SplashColorScheme, type SplashAnimation };
21
21
 
22
- export const MENU_OPTIONS = ['Resume Dashboard', 'Settings (Customizer)', 'Readme', 'Credits', 'Quit pyre'] as const;
22
+ export const MENU_OPTIONS = ['Resume Dashboard', 'Command Center (All Tools)', 'Settings (Customizer)', 'Readme', 'Credits', 'Quit pyre'] as const;
23
23
 
24
24
  const PALETTE_COMMANDS = [
25
+ { id: 'command-center', label: 'Command Center (Browse all tools)', keys: 'o', category: 'Navigation' },
25
26
  { id: 'cpu', label: 'Switch to CPU panel', keys: '1', category: 'Navigation' },
26
27
  { id: 'mem', label: 'Switch to Memory panel', keys: '2', category: 'Navigation' },
27
28
  { id: 'gpu', label: 'Switch to GPU panel', keys: '3', category: 'Navigation' },