pyre-cli 10.3.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.
- package/README.md +41 -0
- package/dist/{alertChannels-TPSQIJ7S.js → alertChannels-MNX4XH6J.js} +1 -0
- package/dist/{anomaliesCmd-25OFKYPK.js → anomaliesCmd-HHTY5LYL.js} +2 -0
- package/dist/benchGitCmd-HIXUC5EK.js +87 -0
- package/dist/{benchmarkCmd-A3ETK6VY.js → benchmarkCmd-B7DLP2VC.js} +2 -0
- package/dist/{blender-FGH3ASEQ.js → blender-6ZX5SPYC.js} +3 -1
- package/dist/{brewHealth-P2GW5Y5C.js → brewHealth-VFW33QB3.js} +1 -0
- package/dist/budgetCmd-TGO3RYLN.js +11 -0
- package/dist/{buildCmd-CDPZ2ZFA.js → buildCmd-E7KUCFYG.js} +3 -1
- package/dist/{checkCmd-WQAHAUWO.js → checkCmd-QLYTP77Y.js} +5 -3
- package/dist/chunk-3RG5ZIWI.js +10 -0
- package/dist/{chunk-VIACN2RU.js → chunk-FYCBGUTW.js} +41 -19
- package/dist/chunk-JH7P6KLN.js +143 -0
- package/dist/{chunk-QM5L43EK.js → chunk-LDJB5V4N.js} +5 -1
- package/dist/chunk-NMGQVRCA.js +111 -0
- package/dist/chunk-OCL7P7N7.js +936 -0
- package/dist/{chunk-23YWCI3O.js → chunk-QEH4LR6P.js} +28 -686
- package/dist/{chunk-KVUMOA6C.js → chunk-RKPGPVTO.js} +77 -4
- package/dist/chunk-UF3XQ3R3.js +3800 -0
- package/dist/{collectors-JDN5Y7EV.js → collectors-V7SCIIDH.js} +5 -3
- package/dist/{completions-ZVTNE52B.js → completions-FTPXDSR2.js} +44 -3
- package/dist/{config-J4YMADEF.js → config-5WW5M63G.js} +2 -1
- package/dist/containersCmd-FSMYOBG2.js +84 -0
- package/dist/{doctor-LHFW4UJP.js → doctor-O4DIAMHK.js} +3 -2
- package/dist/{explainCmd-MF6PIMLW.js → explainCmd-7PBPA7O5.js} +1 -0
- package/dist/{extensions-XJGHG7X3.js → extensions-QQEV7U3D.js} +1 -0
- package/dist/{firebaseSync-ON3RZ6KX.js → firebaseSync-LRN67UTV.js} +2 -0
- package/dist/flightRecorder-GJIAQOXC.js +162 -0
- package/dist/index.js +736 -4125
- package/dist/inspectCmd-5FXJBJTK.js +137 -0
- package/dist/menu-AJFGDLBP.js +1233 -0
- package/dist/monitors/platform/windows-ui.ps1 +31 -29
- package/dist/{netUsageCmd-67IBZZYN.js → netUsageCmd-JP6YKBPF.js} +7 -5
- package/dist/optimizeCmd-WZKLFYZU.js +1110 -0
- package/dist/{pipeCmd-2N4JPKYE.js → pipeCmd-CPK6FVKB.js} +5 -3
- package/dist/playbookCmd-FRROIEXR.js +145 -0
- package/dist/{profileProcCmd-N4H6MCBT.js → profileProcCmd-5Y3733E7.js} +64 -13
- package/dist/{prometheus-5BL5PVLC.js → prometheus-CYNUR3QT.js} +1 -0
- package/dist/{prometheusCmd-LEUM5WCZ.js → prometheusCmd-G2CSEZRQ.js} +5 -3
- package/dist/{replayCmd-Z3PYXY7V.js → replayCmd-ODB5SPOQ.js} +25 -6
- package/dist/{run-HOGQDDTA.js → run-UJQYT2TO.js} +1 -0
- package/dist/{serveCmd-L7LVJOZN.js → serveCmd-KDPWLVQI.js} +5 -3
- package/dist/{service-7GIX3YNC.js → service-LIYYR3UI.js} +1 -0
- package/dist/{smartCmd-ODWKSFNG.js → smartCmd-2BJJ37GV.js} +130 -5
- package/dist/statuslineCmd-EUKYRR2G.js +75 -0
- package/dist/{stressCmd-MFXMFXYX.js → stressCmd-GKHGY26X.js} +2 -0
- package/dist/{syncCmd-42IRF633.js → syncCmd-MIBFKX24.js} +16 -5
- package/dist/topoCmd-RTXQJ6XA.js +189 -0
- package/dist/{updateCheck-52X6LJG5.js → updateCheck-LJHEMG3G.js} +2 -0
- package/dist/{watchdogCmd-6GR4UN6B.js → watchdogCmd-XM6OPTKM.js} +41 -6
- package/dist/{wizard-JQMQWFAW.js → wizard-PRDOJROQ.js} +2 -1
- package/package.json +2 -2
- package/src/benchGitCmd.ts +110 -0
- package/src/budgetCmd.ts +199 -0
- package/src/completions.ts +42 -3
- package/src/containersCmd.ts +106 -0
- package/src/doctor.ts +86 -6
- package/src/easy_features.test.ts +83 -0
- package/src/flightRecorder.ts +214 -0
- package/src/index.ts +181 -8
- package/src/inspectCmd.ts +165 -0
- package/src/live/input.ts +70 -3
- package/src/live/render.ts +1 -1
- package/src/live/state.ts +2 -1
- package/src/menu/commands.ts +754 -0
- package/src/menu/index.ts +358 -0
- package/src/menu/render.ts +272 -0
- package/src/menu/types.ts +63 -0
- package/src/menu.test.ts +110 -0
- package/src/monitors/blender.ts +49 -19
- package/src/monitors/buildTracker.ts +72 -54
- package/src/monitors/platform/windows-ui.ps1 +31 -29
- package/src/monitors/platform/windows.ts +613 -280
- package/src/optimizeCmd.ts +1248 -0
- package/src/platform.test.ts +135 -0
- package/src/playbookCmd.ts +156 -0
- package/src/power_features.test.ts +251 -0
- package/src/profileProcCmd.ts +68 -21
- package/src/replayCmd.ts +15 -6
- package/src/smartCmd.ts +149 -10
- package/src/state/config.ts +13 -0
- package/src/statuslineCmd.ts +87 -0
- package/src/syncCmd.ts +12 -0
- package/src/topoCmd.ts +83 -50
- package/src/watchdogCmd.ts +40 -3
- package/src/xbar.ts +195 -17
- package/dist/chunk-M73HCQD2.js +0 -95
- package/dist/topoCmd-DZ3YS5IL.js +0 -148
package/src/completions.ts
CHANGED
|
@@ -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
|
|
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:
|
|
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.
|
|
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
|
-
//
|
|
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
|
+
}
|