pyre-cli 10.3.0 → 10.5.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 +41 -0
  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 +736 -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 +181 -8
  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 +636 -280
  74. package/src/optimizeCmd.ts +1248 -0
  75. package/src/platform.test.ts +146 -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
@@ -102,10 +102,34 @@ program
102
102
  .option('--top <n>', 'Number of items to display in netusage command (default: 15)')
103
103
  .option('--raw', 'Print unparsed output for profiler')
104
104
  .option('--sync-dir <dir>', 'Custom directory path for cloud profile/config sync')
105
- .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)');
106
120
 
107
121
  program.addHelpText('after', `
122
+ 💡 Tip: Can't remember all commands? Run 'pyre menu' to browse and run all tools interactively!
123
+
108
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)
109
133
  live Interactive live dashboard
110
134
  check One-line plain-English health summary
111
135
  pipe Continuous newline-delimited JSON stream for scripting
@@ -143,6 +167,8 @@ program.addHelpText('after', `
143
167
  build Active compiler and build system tracker
144
168
  topo Network topology mapper and pyre peer auto-discovery
145
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
146
172
  `);
147
173
 
148
174
  program.parse(process.argv);
@@ -226,6 +252,106 @@ async function main() {
226
252
 
227
253
  const cmd = program.args[0];
228
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
+
229
355
  if (cmd === 'check') {
230
356
  const { runCheckCommand } = await import('./checkCmd.js');
231
357
  await runCheckCommand({
@@ -309,6 +435,23 @@ async function main() {
309
435
  return;
310
436
  }
311
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
+
312
455
  if (cmd === 'replay') {
313
456
  const file = program.args[1];
314
457
  if (!file) {
@@ -580,7 +723,12 @@ async function main() {
580
723
  }
581
724
 
582
725
  if (cmd === 'xbar') {
583
- 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
+ });
584
732
  return;
585
733
  }
586
734
 
@@ -624,9 +772,13 @@ async function main() {
624
772
  if (opts.alertPushoverToken) config.pushoverToken = opts.alertPushoverToken;
625
773
  if (opts.alertPushoverUser) config.pushoverUser = opts.alertPushoverUser;
626
774
  if (opts.alertNtfy) config.ntfyUrl = opts.alertNtfy;
627
- 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
+ }
628
780
 
629
- if (cmd === 'live' || (!isExportMode() && !opts.once && cmd !== 'p2p')) {
781
+ if (cmd === 'live' || (!cmd && !isExportMode() && !opts.once)) {
630
782
  if (!process.stdout.isTTY && cmd !== 'live') {
631
783
  await runWebCommand();
632
784
  return;
@@ -640,6 +792,7 @@ async function main() {
640
792
  animation: config.splashAnimation as any,
641
793
  });
642
794
  const interval = parseFloat(opts.interval) || 2;
795
+ const tempUnit: 'c' | 'f' = (opts.tempUnit?.toLowerCase() === 'f' || config.tempUnit === 'f') ? 'f' : 'c';
643
796
  await startLive({
644
797
  interval,
645
798
  detailed: opts.detailed ?? config.detailed,
@@ -674,13 +827,14 @@ async function main() {
674
827
 
675
828
  const data = await collectAll({ detailed: opts.detailed, processLimit });
676
829
 
830
+ const tempUnit: 'c' | 'f' = (opts.tempUnit?.toLowerCase() === 'f' || config.tempUnit === 'f') ? 'f' : 'c';
677
831
  let output: string;
678
832
  if (opts.json) output = formatJson(data);
679
833
  else if (opts.html) output = formatHtml(data);
680
834
  else if (opts.md) output = formatMarkdown(data);
681
835
  else if (opts.csv) output = formatCsv(data);
682
836
  else if (opts.tsv) output = formatTsv(data);
683
- 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 });
684
838
 
685
839
  console.log(output);
686
840
 
@@ -932,6 +1086,25 @@ async function runInfoCommand(): Promise<void> {
932
1086
  }
933
1087
  }
934
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
+
935
1108
  console.log(lines.join('\n'));
936
1109
  }
937
1110
 
@@ -1729,10 +1902,10 @@ async function runBenchCommand(benchCmd: string): Promise<void> {
1729
1902
  let powerSamplesCount = 0;
1730
1903
 
1731
1904
  const child = process.platform === 'win32'
1732
- ? spawn('cmd.exe', ['/c', benchCmd], { stdio: 'inherit' })
1905
+ ? spawn('cmd.exe', ['/c', benchCmd])
1733
1906
  : spawn('sh', ['-c', benchCmd]);
1734
- child.stdout.on('data', d => process.stdout.write(d));
1735
- 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));
1736
1909
 
1737
1910
  const interval = parseFloat(opts.interval) || 2;
1738
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' },