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
@@ -4,6 +4,16 @@ export function generateZshCompletions(): string {
4
4
  _pyre() {
5
5
  local -a commands
6
6
  commands=(
7
+ 'menu:Interactive TUI command center and tools navigator'
8
+ 'tui:Alias for menu'
9
+ 'launcher:Alias for menu'
10
+ 'playbook:Execute sequenced multi-step macro recipes'
11
+ 'flight:Forensic flight recorder black box'
12
+ 'run:Performance budgeting runner for CI/CD gates'
13
+ 'bench-git:Cross-branch git performance differ'
14
+ 'statusline:Headless terminal statusline string'
15
+ 'inspect:Process deep-dive inspector'
16
+ 'containers:Container and micro-VM telemetry'
7
17
  'live:Start interactive live monitoring dashboard'
8
18
  'info:Display static system overview'
9
19
  'doctor:Run system permissions and diagnostic checks'
@@ -15,6 +25,7 @@ _pyre() {
15
25
  'ssh:Stream metrics over SSH'
16
26
  'bench:Benchmark command while logging system stats'
17
27
  'completions:Generate shell completion scripts'
28
+ 'optimize:Mac optimizer (quit idle apps, memory, caches, trash, startup, dns)'
18
29
  )
19
30
 
20
31
  _arguments -s \\
@@ -58,8 +69,8 @@ _pyre_completions() {
58
69
  COMPREPLY=()
59
70
  cur="\${COMP_WORDS[COMP_CWORD]}"
60
71
  prev="\${COMP_WORDS[COMP_CWORD-1]}"
61
- commands="live info doctor web ui launch config p2p ssh bench completions"
62
- opts="--json --html --md --csv --tsv --detailed --theme --interval --once --out --export-dir --log --tree --sort --packets --limit --alert-cpu --alert-temp --temp-unit"
72
+ commands="menu tui launcher playbook flight run bench-git statusline inspect containers live info doctor web ui launch config p2p ssh bench completions optimize"
73
+ opts="--json --html --md --csv --tsv --detailed --theme --interval --once --out --export-dir --log --tree --sort --packets --limit --alert-cpu --alert-temp --temp-unit --idle --keep --yes --force --clean --empty --dry-run --auto-quit -m --menu -i --interactive --budget-cpu --budget-mem --budget-time --markdown --nice --format"
63
74
 
64
75
  case "${prev}" in
65
76
  --theme)
@@ -82,6 +93,22 @@ _pyre_completions() {
82
93
  COMPREPLY=( $(compgen -W "server connect" -- ${cur}) )
83
94
  return 0
84
95
  ;;
96
+ optimize)
97
+ COMPREPLY=( $(compgen -W "apps track memory caches trash startup dns protect unprotect help" -- ${cur}) )
98
+ return 0
99
+ ;;
100
+ playbook)
101
+ COMPREPLY=( $(compgen -W "run list audit clean triage bench" -- ${cur}) )
102
+ return 0
103
+ ;;
104
+ flight)
105
+ COMPREPLY=( $(compgen -W "dump status record" -- ${cur}) )
106
+ return 0
107
+ ;;
108
+ statusline)
109
+ COMPREPLY=( $(compgen -W "tmux starship json pango plain" -- ${cur}) )
110
+ return 0
111
+ ;;
85
112
  esac
86
113
 
87
114
  if [[ ${cur} == -* ]] ; then
@@ -99,6 +126,16 @@ complete -F _pyre_completions pyre
99
126
  export function generateFishCompletions(): string {
100
127
  return `# fish completion for pyre
101
128
 
129
+ complete -c pyre -n "__fish_use_subcommand" -a menu -d "Interactive TUI command center and tools navigator"
130
+ complete -c pyre -n "__fish_use_subcommand" -a tui -d "Alias for menu"
131
+ complete -c pyre -n "__fish_use_subcommand" -a launcher -d "Alias for menu"
132
+ complete -c pyre -n "__fish_use_subcommand" -a playbook -d "Execute sequenced multi-step macro recipes"
133
+ complete -c pyre -n "__fish_use_subcommand" -a flight -d "Forensic flight recorder black box"
134
+ complete -c pyre -n "__fish_use_subcommand" -a run -d "Performance budgeting runner for CI/CD gates"
135
+ complete -c pyre -n "__fish_use_subcommand" -a bench-git -d "Cross-branch git performance differ"
136
+ complete -c pyre -n "__fish_use_subcommand" -a statusline -d "Headless terminal statusline string"
137
+ complete -c pyre -n "__fish_use_subcommand" -a inspect -d "Process deep-dive inspector"
138
+ complete -c pyre -n "__fish_use_subcommand" -a containers -d "Container and micro-VM telemetry"
102
139
  complete -c pyre -n "__fish_use_subcommand" -a live -d "Start interactive live monitoring dashboard"
103
140
  complete -c pyre -n "__fish_use_subcommand" -a info -d "Display static system overview"
104
141
  complete -c pyre -n "__fish_use_subcommand" -a doctor -d "Run system diagnostics"
@@ -108,6 +145,8 @@ complete -c pyre -n "__fish_use_subcommand" -a launch -d "Launch Activity Monito
108
145
  complete -c pyre -n "__fish_use_subcommand" -a config -d "Show or reset configuration options"
109
146
  complete -c pyre -n "__fish_use_subcommand" -a p2p -d "P2P streaming mode"
110
147
  complete -c pyre -n "__fish_use_subcommand" -a completions -d "Generate shell completions"
148
+ complete -c pyre -n "__fish_use_subcommand" -a optimize -d "Mac optimizer: quit idle apps, clean caches"
149
+ complete -c pyre -n "__fish_seen_subcommand_from optimize" -a "apps track memory caches trash startup dns protect unprotect help"
111
150
 
112
151
  complete -c pyre -l json -s j -d "Output snapshot as JSON"
113
152
  complete -c pyre -l html -d "Output snapshot as HTML"
@@ -125,7 +164,7 @@ export function generatePowerShellCompletions(): string {
125
164
  return `# PowerShell completion for pyre
126
165
  Register-ArgumentCompleter -Native -CommandName pyre -ScriptBlock {
127
166
  param($wordToComplete, $commandAst, $cursorPosition)
128
- $commands = @('live', 'check', 'pipe', 'stress', 'ui', 'launch', 'web', 'ssh', 'fleet', 'bench', 'benchmark', 'anomalies', 'doctor', 'extensions', 'brew', 'update', 'profile', 'config', 'history', 'diff', 'info', 'completions', 'p2p', 'blender', 'prometheus', 'smart', 'replay', 'explain')
167
+ $commands = @('menu', 'tui', 'launcher', 'playbook', 'flight', 'run', 'bench-git', 'statusline', 'inspect', 'containers', 'live', 'check', 'pipe', 'stress', 'ui', 'launch', 'web', 'ssh', 'fleet', 'bench', 'benchmark', 'anomalies', 'doctor', 'extensions', 'brew', 'update', 'profile', 'config', 'history', 'diff', 'info', 'completions', 'p2p', 'blender', 'prometheus', 'smart', 'replay', 'explain', 'optimize')
129
168
  $options = @('--json', '--html', '--md', '--csv', '--tsv', '--detailed', '--theme', '--interval', '--once', '--out', '--export-dir', '--log', '--tree', '--sort', '--packets', '--limit', '--alert-cpu', '--alert-temp', '--temp-unit', '--plain', '--help', '--version')
130
169
 
131
170
  $elements = $commandAst.CommandElements
@@ -0,0 +1,106 @@
1
+ /**
2
+ * pyre containers — Container & Micro-VM Telemetry.
3
+ *
4
+ * Discovers running containers across Docker, Podman, and OrbStack,
5
+ * reporting container names, CPU load, memory consumption, and network I/O.
6
+ */
7
+ import { execSync } from 'node:child_process';
8
+ import chalk from 'chalk';
9
+
10
+ export interface ContainerStats {
11
+ id: string;
12
+ name: string;
13
+ engine: 'docker' | 'podman' | 'orbstack';
14
+ cpuPct: string;
15
+ memUsage: string;
16
+ memPct: string;
17
+ netIO: string;
18
+ }
19
+
20
+ export function detectContainerEngine(): 'docker' | 'podman' | null {
21
+ try {
22
+ execSync('docker --version', { stdio: 'ignore' });
23
+ return 'docker';
24
+ } catch {
25
+ try {
26
+ execSync('podman --version', { stdio: 'ignore' });
27
+ return 'podman';
28
+ } catch {
29
+ return null;
30
+ }
31
+ }
32
+ }
33
+
34
+ export function collectContainerStats(): ContainerStats[] {
35
+ const engine = detectContainerEngine();
36
+ if (!engine) return [];
37
+
38
+ try {
39
+ const raw = execSync(`${engine} stats --no-stream --format "{{.ID}}\t{{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.MemPerc}}\t{{.NetIO}}" 2>/dev/null`, {
40
+ encoding: 'utf-8',
41
+ timeout: 3000,
42
+ });
43
+
44
+ const lines = raw.trim().split('\n').filter(Boolean);
45
+ const stats: ContainerStats[] = [];
46
+
47
+ for (const l of lines) {
48
+ const parts = l.split('\t');
49
+ if (parts.length >= 6) {
50
+ stats.push({
51
+ id: parts[0]?.trim() || '',
52
+ name: parts[1]?.trim() || '',
53
+ engine,
54
+ cpuPct: parts[2]?.trim() || '0%',
55
+ memUsage: parts[3]?.trim() || '0B',
56
+ memPct: parts[4]?.trim() || '0%',
57
+ netIO: parts[5]?.trim() || '0B/0B',
58
+ });
59
+ }
60
+ }
61
+
62
+ return stats;
63
+ } catch {
64
+ // Engine daemon might not be running
65
+ return [];
66
+ }
67
+ }
68
+
69
+ export async function runContainersCommand(opts?: { json?: boolean }): Promise<void> {
70
+ const stats = collectContainerStats();
71
+ const engine = detectContainerEngine();
72
+
73
+ if (opts?.json) {
74
+ console.log(JSON.stringify({ engine: engine || 'none', containers: stats }, null, 2));
75
+ return;
76
+ }
77
+
78
+ console.log(chalk.bold('\n 🔥 pyre containers — Container & Micro-VM Telemetry\n'));
79
+
80
+ if (!engine) {
81
+ console.log(chalk.yellow(' No container engine (Docker / Podman) detected in PATH.'));
82
+ console.log(chalk.dim(' Install Docker or Podman to enable container performance telemetry.\n'));
83
+ return;
84
+ }
85
+
86
+ if (stats.length === 0) {
87
+ console.log(chalk.dim(` Engine: ${engine} (Active, 0 running containers detected)\n`));
88
+ return;
89
+ }
90
+
91
+ console.log(chalk.dim(` Engine: ${engine} (${stats.length} active containers)\n`));
92
+
93
+ console.log(` ${'ID'.padEnd(14)} ${'Name'.padEnd(24)} ${'CPU %'.padEnd(10)} ${'Memory Usage'.padEnd(22)} ${'Net I/O'}`);
94
+ console.log(chalk.dim(' ─'.repeat(40)));
95
+
96
+ for (const c of stats) {
97
+ const idCol = chalk.dim(c.id.padEnd(14));
98
+ const nameCol = chalk.bold.cyan(c.name.slice(0, 22).padEnd(24));
99
+ const cpuCol = chalk.yellow(c.cpuPct.padEnd(10));
100
+ const memCol = chalk.magenta(`${c.memUsage} (${c.memPct})`.padEnd(22));
101
+ const netCol = chalk.dim(c.netIO);
102
+ console.log(` ${idCol} ${nameCol} ${cpuCol} ${memCol} ${netCol}`);
103
+ }
104
+
105
+ console.log();
106
+ }
package/src/doctor.ts CHANGED
@@ -336,14 +336,15 @@ async function runWindowsDoctor(): Promise<DoctorCheck[]> {
336
336
  });
337
337
  }
338
338
 
339
- // 2. PowerShell / WMI Interface
339
+ // 2. PowerShell / WMI Interface (modern CIM check)
340
340
  try {
341
- const wmiCheck = await run('wmic os get Caption 2>nul || powershell -Command "(Get-CimInstance Win32_OperatingSystem).Caption"', '');
341
+ const { runPowerShell } = await import('./monitors/run.js');
342
+ const wmiCheck = await runPowerShell('(Get-CimInstance Win32_OperatingSystem -ErrorAction SilentlyContinue).Caption', '');
342
343
  if (wmiCheck.toLowerCase().includes('windows')) {
343
344
  checks.push({
344
345
  name: 'WMI / CIM Subsystem',
345
346
  status: 'ok',
346
- message: 'WMI/CIM management queries responsive.',
347
+ message: `WMI/CIM management queries responsive (${wmiCheck}).`,
347
348
  });
348
349
  } else {
349
350
  checks.push({
@@ -357,11 +358,90 @@ async function runWindowsDoctor(): Promise<DoctorCheck[]> {
357
358
  checks.push({
358
359
  name: 'WMI / CIM Subsystem',
359
360
  status: 'warn',
360
- message: 'Could not execute WMI query.',
361
+ message: 'Could not execute WMI/CIM query.',
361
362
  });
362
363
  }
363
364
 
364
- // 3. Network Reachability
365
+ // 3. Windows Defender / Antivirus Protection
366
+ try {
367
+ const { runPowerShell } = await import('./monitors/run.js');
368
+ const avRaw = await runPowerShell(
369
+ `$av = Get-CimInstance -Namespace root/SecurityCenter2 -ClassName AntiVirusProduct -ErrorAction SilentlyContinue; if ($av) { ($av | Select-Object -ExpandProperty displayName) -join ', ' } else { '' }`,
370
+ ''
371
+ );
372
+ if (avRaw.trim()) {
373
+ checks.push({
374
+ name: 'Antivirus Protection',
375
+ status: 'ok',
376
+ message: `Antivirus active (${avRaw.trim()}).`,
377
+ });
378
+ } else {
379
+ checks.push({
380
+ name: 'Antivirus Protection',
381
+ status: 'warn',
382
+ message: 'No third-party Antivirus reported via SecurityCenter2; Windows Defender active.',
383
+ });
384
+ }
385
+ } catch {
386
+ checks.push({
387
+ name: 'Antivirus Protection',
388
+ status: 'warn',
389
+ message: 'Could not query SecurityCenter2 status.',
390
+ });
391
+ }
392
+
393
+ // 4. User Account Control (UAC) Status
394
+ try {
395
+ const { runPowerShell } = await import('./monitors/run.js');
396
+ const uacRaw = await runPowerShell(
397
+ `(Get-ItemProperty -Path 'HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Policies\\System' -Name EnableLUA -ErrorAction SilentlyContinue).EnableLUA`,
398
+ ''
399
+ );
400
+ if (uacRaw.trim() === '1') {
401
+ checks.push({
402
+ name: 'User Account Control',
403
+ status: 'ok',
404
+ message: 'UAC is enabled.',
405
+ });
406
+ } else {
407
+ checks.push({
408
+ name: 'User Account Control',
409
+ status: 'warn',
410
+ message: 'UAC is disabled or restricted.',
411
+ details: 'Running with UAC disabled allows processes elevated privileges without prompts.',
412
+ });
413
+ }
414
+ } catch {
415
+ checks.push({
416
+ name: 'User Account Control',
417
+ status: 'warn',
418
+ message: 'Could not query UAC registry status.',
419
+ });
420
+ }
421
+
422
+ // 5. Hardware Inventory (Intel / AMD CPU, NVIDIA / AMD / Intel GPU)
423
+ try {
424
+ const { runPowerShell } = await import('./monitors/run.js');
425
+ const cpuName = await runPowerShell('(Get-CimInstance Win32_Processor -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty Name)', '');
426
+ const gpuName = await runPowerShell('(Get-CimInstance Win32_VideoController -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty Name)', '');
427
+ const cpuVendor = cpuName.toLowerCase().includes('amd') ? 'AMD' : cpuName.toLowerCase().includes('intel') ? 'Intel' : 'x86/x64';
428
+ const gpuVendor = gpuName.toLowerCase().includes('nvidia') ? 'NVIDIA' : gpuName.toLowerCase().includes('amd') || gpuName.toLowerCase().includes('radeon') ? 'AMD' : gpuName.toLowerCase().includes('intel') ? 'Intel' : 'Standard';
429
+
430
+ checks.push({
431
+ name: 'Hardware Telemetry',
432
+ status: 'ok',
433
+ message: `${cpuVendor} CPU & ${gpuVendor} GPU recognized.`,
434
+ details: `CPU: ${cpuName.trim() || 'Generic'} | GPU: ${gpuName.trim() || 'Generic'}`,
435
+ });
436
+ } catch {
437
+ checks.push({
438
+ name: 'Hardware Telemetry',
439
+ status: 'warn',
440
+ message: 'Hardware inspection incomplete.',
441
+ });
442
+ }
443
+
444
+ // 6. Network Reachability
365
445
  try {
366
446
  const netCheck = await run('netstat -rn 2>nul', '');
367
447
  if (netCheck.includes('0.0.0.0')) {
@@ -385,7 +465,7 @@ async function runWindowsDoctor(): Promise<DoctorCheck[]> {
385
465
  });
386
466
  }
387
467
 
388
- // 4. Config file integrity
468
+ // 7. Config file integrity
389
469
  checks.push(checkConfigIntegrity());
390
470
 
391
471
  return checks;
@@ -230,3 +230,86 @@ describe('Pyre Preferences, Autostart Service, and Settings UI', () => {
230
230
 
231
231
 
232
232
 
233
+ describe('Mac Optimizer (pyre optimize)', async () => {
234
+ const opt = await import('./optimizeCmd.js');
235
+
236
+ it('parses durations', () => {
237
+ assert.strictEqual(opt.parseDuration('2h', 0), 7_200_000);
238
+ assert.strictEqual(opt.parseDuration('30m', 0), 1_800_000);
239
+ assert.strictEqual(opt.parseDuration('90s', 0), 90_000);
240
+ assert.strictEqual(opt.parseDuration('1d', 0), 86_400_000);
241
+ assert.strictEqual(opt.parseDuration('15', 0), 900_000);
242
+ assert.strictEqual(opt.parseDuration(undefined, 42), 42);
243
+ assert.ok(Number.isNaN(opt.parseDuration('soon', 0)));
244
+ });
245
+
246
+ it('parses lsappinfo list output (Foreground apps only)', () => {
247
+ const raw = ` 1) "loginwindow" ASN:0x0-0x3003:
248
+ bundleID="com.apple.loginwindow"
249
+ pid = 414 type="UIElement" flavor=3
250
+ 4) "Terminal" ASN:0x0-0xa00a:
251
+ bundleID="com.apple.Terminal"
252
+ pid = 615 type="Foreground" flavor=3 Version="470.2"
253
+ checkin time = 2026/10/10 09:07:09 ( 2 days ago )
254
+ 5) "Safari" ASN:0x0-0xb00b:
255
+ bundleID="com.apple.Safari"
256
+ pid = 619 type="Foreground" flavor=3
257
+ `;
258
+ const apps = opt.parseLsappinfoList(raw);
259
+ assert.strictEqual(apps.length, 2);
260
+ assert.deepStrictEqual(apps.map(a => a.bundleId), ['com.apple.Terminal', 'com.apple.Safari']);
261
+ assert.strictEqual(apps[0].pid, 615);
262
+ assert.strictEqual(apps[0].asn, 'ASN:0x0-0xa00a');
263
+ assert.strictEqual(apps[0].launchedAt, new Date(2026, 9, 10, 9, 7, 9).getTime());
264
+ });
265
+
266
+ it('parses visible process order', () => {
267
+ const order = opt.parseAsnList('ASN:0x0-0xb00b-"Safari": ASN:0x0-0x598598-"Antigravity": ASN:0x0-0xb00b-"Safari":');
268
+ assert.deepStrictEqual(order, ['ASN:0x0-0xb00b', 'ASN:0x0-0x598598']);
269
+ });
270
+
271
+ const now = new Date(2026, 9, 12, 12, 0, 0).getTime();
272
+ const H = 3_600_000;
273
+ const mk = (name: string, rank: number, extra: Partial<import('./optimizeCmd.js').RunningApp> = {}) => ({
274
+ asn: `ASN:0x0-0x${rank}`, name, bundleId: `com.test.${name.toLowerCase()}`, pid: 1000 + rank,
275
+ launchedAt: now - 24 * H, rank, cpu: 0, rssKb: 100_000, frontmost: false, ...extra,
276
+ });
277
+ const baseCtx = { now, idleThresholdMs: 2 * H, keepRecent: 2, protectedApps: [] as string[], selfPids: new Set<number>() };
278
+
279
+ it('selects by rank when no tracking data, honoring keep/protected/busy/self', () => {
280
+ const apps = [
281
+ mk('Front', 0, { frontmost: true }),
282
+ mk('Recent', 1),
283
+ mk('Finder', 2, { bundleId: 'com.apple.finder' }),
284
+ mk('Busy', 3, { cpu: 30 }),
285
+ mk('Mine', 4, { pid: 777 }),
286
+ mk('Fresh', 5, { launchedAt: now - 10 * 60_000 }),
287
+ mk('Music', 6, { bundleId: 'com.apple.Music', cpu: 2 }),
288
+ mk('Old', 7),
289
+ mk('Custom', 8),
290
+ ];
291
+ const { candidates, skipped } = opt.selectIdleApps(apps, { ...baseCtx, protectedApps: ['custom'], selfPids: new Set([777]) });
292
+ assert.deepStrictEqual(candidates.map(c => c.app.name), ['Old']);
293
+ assert.strictEqual(candidates[0].idleMs, undefined);
294
+ assert.strictEqual(skipped.length, 8);
295
+ });
296
+
297
+ it('uses fresh tracking data for real idle times', () => {
298
+ const usage = {
299
+ version: 1 as const, trackingSince: now - 10 * H, updatedAt: now - 5000,
300
+ apps: { 'com.test.used': { name: 'Used', lastActive: now - 30 * 60_000 }, 'com.test.stale': { name: 'Stale', lastActive: now - 5 * H } },
301
+ };
302
+ const apps = [mk('A', 0), mk('B', 1), mk('Used', 2), mk('Stale', 3), mk('Never', 4)];
303
+ const { candidates } = opt.selectIdleApps(apps, { ...baseCtx, usage });
304
+ assert.deepStrictEqual(candidates.map(c => c.app.name), ['Stale', 'Never']);
305
+ assert.strictEqual(candidates[0].idleMs, 5 * H);
306
+ // Never seen since tracking began 10h ago → idle bounded by trackingSince
307
+ assert.strictEqual(candidates[1].idleMs, 10 * H);
308
+ });
309
+
310
+ it('ignores stale tracking data (tracker not running)', () => {
311
+ const usage = { version: 1 as const, trackingSince: now - 10 * H, updatedAt: now - 3 * H, apps: {} };
312
+ assert.strictEqual(opt.isTrackingFresh(usage, now), false);
313
+ assert.strictEqual(opt.computeIdleMs(mk('X', 5), usage, now), undefined);
314
+ });
315
+ });
@@ -0,0 +1,214 @@
1
+ /**
2
+ * pyre flight — In-Memory Forensic Flight Recorder (Black Box).
3
+ *
4
+ * Maintains a circular memory buffer of recent high-resolution telemetry frames.
5
+ * Dumps structured post-mortem forensic reports when freezes, thermal throttles,
6
+ * or runaway process spikes occur.
7
+ */
8
+ import fs from 'node:fs';
9
+ import path from 'node:path';
10
+ import chalk from 'chalk';
11
+ import { collectAll } from './monitors/collectors.js';
12
+ import { getConfigDir } from './state/config.js';
13
+
14
+ export interface FlightFrame {
15
+ timestamp: string;
16
+ cpu: {
17
+ usage: number;
18
+ temp?: number;
19
+ loadAvg: number[];
20
+ };
21
+ memory: {
22
+ usagePercent: number;
23
+ usedBytes: number;
24
+ totalBytes: number;
25
+ };
26
+ network: {
27
+ rxPackets: number;
28
+ txPackets: number;
29
+ };
30
+ topProcesses: Array<{
31
+ pid: number;
32
+ name: string;
33
+ cpu: number;
34
+ mem: number;
35
+ }>;
36
+ }
37
+
38
+ export interface FlightReport {
39
+ generatedAt: string;
40
+ frameCount: number;
41
+ timeSpanSec: number;
42
+ summary: {
43
+ peakCpuUsage: number;
44
+ peakMemUsage: number;
45
+ peakTempC?: number;
46
+ highestCpuProcess?: { name: string; pid: number; cpu: number };
47
+ highestMemProcess?: { name: string; pid: number; mem: number };
48
+ };
49
+ frames: FlightFrame[];
50
+ }
51
+
52
+ export class FlightRecorder {
53
+ private buffer: FlightFrame[] = [];
54
+ private maxFrames: number;
55
+
56
+ constructor(maxFrames = 600) {
57
+ this.maxFrames = maxFrames;
58
+ }
59
+
60
+ public push(frame: FlightFrame): void {
61
+ if (this.buffer.length >= this.maxFrames) {
62
+ this.buffer.shift();
63
+ }
64
+ this.buffer.push(frame);
65
+ }
66
+
67
+ public size(): number {
68
+ return this.buffer.length;
69
+ }
70
+
71
+ public getFrames(): FlightFrame[] {
72
+ return [...this.buffer];
73
+ }
74
+
75
+ public clear(): void {
76
+ this.buffer = [];
77
+ }
78
+
79
+ public generateReport(): FlightReport {
80
+ const frames = this.getFrames();
81
+ let peakCpu = 0;
82
+ let peakMem = 0;
83
+ let peakTemp: number | undefined = undefined;
84
+ let highestCpuProc: { name: string; pid: number; cpu: number } | undefined = undefined;
85
+ let highestMemProc: { name: string; pid: number; mem: number } | undefined = undefined;
86
+
87
+ for (const f of frames) {
88
+ if (f.cpu.usage > peakCpu) peakCpu = f.cpu.usage;
89
+ if (f.memory.usagePercent > peakMem) peakMem = f.memory.usagePercent;
90
+ if (f.cpu.temp !== undefined) {
91
+ if (peakTemp === undefined || f.cpu.temp > peakTemp) peakTemp = f.cpu.temp;
92
+ }
93
+ for (const p of f.topProcesses) {
94
+ if (!highestCpuProc || p.cpu > highestCpuProc.cpu) {
95
+ highestCpuProc = { name: p.name, pid: p.pid, cpu: p.cpu };
96
+ }
97
+ if (!highestMemProc || p.mem > highestMemProc.mem) {
98
+ highestMemProc = { name: p.name, pid: p.pid, mem: p.mem };
99
+ }
100
+ }
101
+ }
102
+
103
+ const timeSpanSec = frames.length > 1
104
+ ? Math.round((new Date(frames[frames.length - 1].timestamp).getTime() - new Date(frames[0].timestamp).getTime()) / 1000)
105
+ : 0;
106
+
107
+ return {
108
+ generatedAt: new Date().toISOString(),
109
+ frameCount: frames.length,
110
+ timeSpanSec,
111
+ summary: {
112
+ peakCpuUsage: peakCpu,
113
+ peakMemUsage: peakMem,
114
+ peakTempC: peakTemp,
115
+ highestCpuProcess: highestCpuProc,
116
+ highestMemProcess: highestMemProc,
117
+ },
118
+ frames,
119
+ };
120
+ }
121
+
122
+ public dump(outDir?: string): string {
123
+ const report = this.generateReport();
124
+ const targetDir = outDir || path.join(process.cwd(), 'pyre-exports');
125
+ if (!fs.existsSync(targetDir)) {
126
+ fs.mkdirSync(targetDir, { recursive: true });
127
+ }
128
+ const filename = `pyre-flight-${Date.now()}.json`;
129
+ const fullPath = path.join(targetDir, filename);
130
+ fs.writeFileSync(fullPath, JSON.stringify(report, null, 2), 'utf-8');
131
+ return fullPath;
132
+ }
133
+ }
134
+
135
+ // Global shared recorder instance
136
+ export const globalFlightRecorder = new FlightRecorder(600);
137
+
138
+ export async function captureCurrentFlightFrame(): Promise<FlightFrame> {
139
+ const data = await collectAll();
140
+ const sortedProcs = [...(data.processes || [])].sort((a, b) => b.cpu - a.cpu).slice(0, 5);
141
+
142
+ return {
143
+ timestamp: new Date().toISOString(),
144
+ cpu: {
145
+ usage: data.cpu.usage,
146
+ temp: data.cpu.temperature,
147
+ loadAvg: data.cpu.loadAvg || [],
148
+ },
149
+ memory: {
150
+ usagePercent: data.memory.usagePercent,
151
+ usedBytes: data.memory.used,
152
+ totalBytes: data.memory.total,
153
+ },
154
+ network: {
155
+ rxPackets: data.network.rxPackets,
156
+ txPackets: data.network.txPackets,
157
+ },
158
+ topProcesses: sortedProcs.map((p) => ({
159
+ pid: p.pid,
160
+ name: p.command,
161
+ cpu: p.cpu,
162
+ mem: p.mem,
163
+ })),
164
+ };
165
+ }
166
+
167
+ export async function runFlightCommand(subcommand = 'dump', opts?: { out?: string; duration?: number }): Promise<void> {
168
+ if (subcommand === 'status') {
169
+ const size = globalFlightRecorder.size();
170
+ console.log(chalk.bold('\n 🔥 pyre Flight Recorder Status\n'));
171
+ console.log(` Buffer Frames: ${chalk.green(size)} / 600`);
172
+ console.log(` RAM Footprint: ~${((size * 1.5) / 1024).toFixed(2)} MB`);
173
+ console.log(chalk.dim(' Run `pyre flight dump` to generate a post-mortem forensic file.\n'));
174
+ return;
175
+ }
176
+
177
+ if (subcommand === 'record') {
178
+ const durationSec = opts?.duration || 10;
179
+ console.log(chalk.bold(`\n 🔥 Sampling Flight Recorder for ${durationSec}s...\n`));
180
+ for (let i = 0; i < durationSec; i++) {
181
+ const frame = await captureCurrentFlightFrame();
182
+ globalFlightRecorder.push(frame);
183
+ process.stdout.write(`\r Sampled frame ${i + 1}/${durationSec}...`);
184
+ await new Promise((r) => setTimeout(r, 1000));
185
+ }
186
+ console.log('\n');
187
+ const file = globalFlightRecorder.dump(opts?.out);
188
+ console.log(chalk.green(` ✔ Flight recording saved to: ${file}\n`));
189
+ return;
190
+ }
191
+
192
+ // Default: dump
193
+ // Capture at least one fresh frame if buffer empty
194
+ if (globalFlightRecorder.size() === 0) {
195
+ const frame = await captureCurrentFlightFrame();
196
+ globalFlightRecorder.push(frame);
197
+ }
198
+
199
+ const file = globalFlightRecorder.dump(opts?.out);
200
+ const report = globalFlightRecorder.generateReport();
201
+
202
+ console.log(chalk.bold('\n 🔥 pyre Flight Recorder — Forensic Dump\n'));
203
+ console.log(chalk.dim(` Output File: ${file}`));
204
+ console.log(` Frames: ${chalk.bold(report.frameCount)}`);
205
+ console.log(` Peak CPU: ${chalk.yellow(report.summary.peakCpuUsage.toFixed(1) + '%')}`);
206
+ console.log(` Peak Memory: ${chalk.magenta(report.summary.peakMemUsage.toFixed(1) + '%')}`);
207
+ if (report.summary.peakTempC !== undefined) {
208
+ console.log(` Peak Temp: ${chalk.red(report.summary.peakTempC.toFixed(1) + '°C')}`);
209
+ }
210
+ if (report.summary.highestCpuProcess) {
211
+ console.log(` Top CPU Proc: ${chalk.cyan(report.summary.highestCpuProcess.name)} (PID: ${report.summary.highestCpuProcess.pid}, ${report.summary.highestCpuProcess.cpu.toFixed(1)}%)`);
212
+ }
213
+ console.log();
214
+ }