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.
- 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 +636 -280
- package/src/optimizeCmd.ts +1248 -0
- package/src/platform.test.ts +146 -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/profileProcCmd.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* pyre profile-proc — Process CPU Profiler.
|
|
3
3
|
*
|
|
4
|
-
* Samples a process for a given duration (default 5 seconds) using macOS `sample
|
|
5
|
-
*
|
|
6
|
-
* hot frames summary.
|
|
4
|
+
* Samples a process for a given duration (default 5 seconds) using macOS `sample`,
|
|
5
|
+
* Windows PowerShell thread & module profiling, or fallback thread analysis,
|
|
6
|
+
* presenting an annotated call-tree, thread states, and hot frames summary.
|
|
7
7
|
*/
|
|
8
|
-
import { execFile } from 'node:child_process';
|
|
8
|
+
import { execFile, execSync } from 'node:child_process';
|
|
9
9
|
import { promisify } from 'node:util';
|
|
10
10
|
import chalk from 'chalk';
|
|
11
|
+
import { runPowerShell } from './monitors/run.js';
|
|
11
12
|
|
|
12
13
|
const execFileAsync = promisify(execFile);
|
|
13
14
|
|
|
@@ -25,10 +26,21 @@ export async function runProfileProcCommand(opts: ProfileProcOptions): Promise<v
|
|
|
25
26
|
process.exit(1);
|
|
26
27
|
}
|
|
27
28
|
|
|
28
|
-
// 1. Verify process exists
|
|
29
|
+
// 1. Verify process exists (cross-platform)
|
|
30
|
+
let processAlive = false;
|
|
29
31
|
try {
|
|
30
32
|
process.kill(pid, 0);
|
|
33
|
+
processAlive = true;
|
|
31
34
|
} catch {
|
|
35
|
+
if (process.platform === 'win32') {
|
|
36
|
+
try {
|
|
37
|
+
const out = execSync(`tasklist /fi "PID eq ${pid}" /nh`, { stdio: 'pipe' }).toString();
|
|
38
|
+
processAlive = out.includes(String(pid));
|
|
39
|
+
} catch {}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (!processAlive) {
|
|
32
44
|
console.error(chalk.red(`\n ✖ Error: Process with PID ${pid} is not running or accessible.\n`));
|
|
33
45
|
process.exit(1);
|
|
34
46
|
}
|
|
@@ -38,8 +50,40 @@ export async function runProfileProcCommand(opts: ProfileProcOptions): Promise<v
|
|
|
38
50
|
let sampleOutput = '';
|
|
39
51
|
let usedFallback = false;
|
|
40
52
|
|
|
41
|
-
//
|
|
42
|
-
if (process.platform === '
|
|
53
|
+
// 1. Windows Native Profiler
|
|
54
|
+
if (process.platform === 'win32') {
|
|
55
|
+
try {
|
|
56
|
+
const psScript = `
|
|
57
|
+
$p1 = Get-Process -Id ${pid} -ErrorAction SilentlyContinue
|
|
58
|
+
if (-not $p1) { exit 1 }
|
|
59
|
+
$cpu1 = $p1.TotalProcessorTime.TotalSeconds
|
|
60
|
+
$start = Get-Date
|
|
61
|
+
Start-Sleep -Seconds ${durationSec}
|
|
62
|
+
$p2 = Get-Process -Id ${pid} -ErrorAction SilentlyContinue
|
|
63
|
+
$cpu2 = if ($p2) { $p2.TotalProcessorTime.TotalSeconds } else { $cpu1 }
|
|
64
|
+
$elapsed = ((Get-Date) - $start).TotalSeconds
|
|
65
|
+
$cpuPct = if ($elapsed -gt 0) { [math]::Round((($cpu2 - $cpu1) / $elapsed) * 100, 1) } else { 0 }
|
|
66
|
+
|
|
67
|
+
Write-Output "Process: $($p1.ProcessName) (PID $($p1.Id))"
|
|
68
|
+
Write-Output "Path: $($p1.Path)"
|
|
69
|
+
Write-Output "Average CPU Utilization: $cpuPct%"
|
|
70
|
+
Write-Output "Working Set (RAM): $([math]::Round($p1.WorkingSet64 / 1MB, 1)) MB"
|
|
71
|
+
Write-Output "Threads Total: $($p1.Threads.Count)"
|
|
72
|
+
Write-Output ""
|
|
73
|
+
Write-Output "Call graph / Active Threads:"
|
|
74
|
+
$p1.Threads | Select-Object -First 25 | ForEach-Object {
|
|
75
|
+
$state = $_.ThreadState
|
|
76
|
+
$wait = $_.WaitReason
|
|
77
|
+
$time = [math]::Round($_.TotalProcessorTime.TotalSeconds, 3)
|
|
78
|
+
Write-Output " + [TID $($_.Id)] CPU: $($time)s | State: $state | WaitReason: $wait"
|
|
79
|
+
}
|
|
80
|
+
`;
|
|
81
|
+
sampleOutput = await runPowerShell(psScript, '', (durationSec + 8) * 1000);
|
|
82
|
+
} catch {}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// 2. macOS native `sample` command
|
|
86
|
+
if (!sampleOutput && process.platform === 'darwin') {
|
|
43
87
|
try {
|
|
44
88
|
const { stdout } = await execFileAsync('sample', [String(pid), String(durationSec), '-file', '/dev/stdout'], {
|
|
45
89
|
timeout: (durationSec + 5) * 1000,
|
|
@@ -47,19 +91,22 @@ export async function runProfileProcCommand(opts: ProfileProcOptions): Promise<v
|
|
|
47
91
|
});
|
|
48
92
|
sampleOutput = stdout;
|
|
49
93
|
} catch (err: any) {
|
|
50
|
-
// Sample might fail if SIP restricts it on system processes or timeout
|
|
51
94
|
sampleOutput = err.stdout || '';
|
|
52
95
|
}
|
|
53
96
|
}
|
|
54
97
|
|
|
55
|
-
//
|
|
56
|
-
if (!sampleOutput || sampleOutput.length <
|
|
98
|
+
// 3. POSIX Fallback (Linux / fallback macOS)
|
|
99
|
+
if (!sampleOutput || sampleOutput.length < 50) {
|
|
57
100
|
usedFallback = true;
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
101
|
+
if (process.platform !== 'win32') {
|
|
102
|
+
try {
|
|
103
|
+
const { stdout } = await execFileAsync('ps', ['-M', '-p', String(pid), '-o', 'pid,thread,pcpu,pmem,state,time,command']);
|
|
104
|
+
sampleOutput = stdout;
|
|
105
|
+
} catch {
|
|
106
|
+
sampleOutput = `Process PID ${pid} active. Deep stack sampling requires elevated permissions or user-owned process.`;
|
|
107
|
+
}
|
|
108
|
+
} else {
|
|
109
|
+
sampleOutput = `Process PID ${pid} active. Profiling finished.`;
|
|
63
110
|
}
|
|
64
111
|
}
|
|
65
112
|
|
|
@@ -70,17 +117,17 @@ export async function runProfileProcCommand(opts: ProfileProcOptions): Promise<v
|
|
|
70
117
|
|
|
71
118
|
// Parse and display clean call-tree analysis
|
|
72
119
|
if (!usedFallback) {
|
|
73
|
-
// Extract Call graph section
|
|
74
120
|
const lines = sampleOutput.split('\n');
|
|
75
121
|
const headerLines: string[] = [];
|
|
76
122
|
const stackLines: string[] = [];
|
|
77
123
|
let inCallGraph = false;
|
|
78
124
|
|
|
79
125
|
for (const line of lines) {
|
|
80
|
-
if (line.includes('Analysis of sampling') || line.includes('Process:') || line.includes('Path:') ||
|
|
126
|
+
if (line.includes('Analysis of sampling') || line.includes('Process:') || line.includes('Path:') ||
|
|
127
|
+
line.includes('Identifier:') || line.includes('Average CPU Utilization:') || line.includes('Working Set') || line.includes('Threads Total:')) {
|
|
81
128
|
headerLines.push(line.trim());
|
|
82
129
|
}
|
|
83
|
-
if (line.includes('Call graph:') || line.includes('+ Line Coverage')) {
|
|
130
|
+
if (line.includes('Call graph:') || line.includes('Call graph / Active Threads:') || line.includes('+ Line Coverage')) {
|
|
84
131
|
inCallGraph = true;
|
|
85
132
|
continue;
|
|
86
133
|
}
|
|
@@ -95,13 +142,13 @@ export async function runProfileProcCommand(opts: ProfileProcOptions): Promise<v
|
|
|
95
142
|
|
|
96
143
|
if (headerLines.length > 0) {
|
|
97
144
|
console.log(chalk.dim(' Process Info:'));
|
|
98
|
-
for (const h of headerLines.slice(0,
|
|
145
|
+
for (const h of headerLines.slice(0, 6)) {
|
|
99
146
|
console.log(` ${chalk.cyan(h)}`);
|
|
100
147
|
}
|
|
101
148
|
console.log();
|
|
102
149
|
}
|
|
103
150
|
|
|
104
|
-
console.log(chalk.bold(' Top Call Stacks & Hot
|
|
151
|
+
console.log(chalk.bold(' Top Call Stacks & Hot Threads:'));
|
|
105
152
|
console.log(chalk.dim(' ' + '─'.repeat(70)));
|
|
106
153
|
|
|
107
154
|
if (stackLines.length === 0) {
|
|
@@ -120,7 +167,7 @@ export async function runProfileProcCommand(opts: ProfileProcOptions): Promise<v
|
|
|
120
167
|
}
|
|
121
168
|
}
|
|
122
169
|
} else {
|
|
123
|
-
console.log(chalk.yellow(' Note:
|
|
170
|
+
console.log(chalk.yellow(' Note: Operating system permissions prevented deep call-graph unwind.'));
|
|
124
171
|
console.log(chalk.dim(' Showing process thread snapshot:\n'));
|
|
125
172
|
for (const l of sampleOutput.split('\n')) {
|
|
126
173
|
if (l.trim()) console.log(` ${l}`);
|
package/src/replayCmd.ts
CHANGED
|
@@ -100,14 +100,15 @@ function renderFrame(row: LogRow, idx: number, total: number, speed: number, pau
|
|
|
100
100
|
lines.push(' ' + divider);
|
|
101
101
|
|
|
102
102
|
// Scrub bar
|
|
103
|
-
const progWidth = 30;
|
|
103
|
+
const progWidth = Math.min(40, Math.max(20, cols - 30));
|
|
104
|
+
const pct = total > 1 ? (idx / (total - 1)) * 100 : 0;
|
|
104
105
|
const progress = total > 1 ? Math.round((idx / (total - 1)) * progWidth) : 0;
|
|
105
|
-
const progressBar = chalk.cyan('
|
|
106
|
+
const progressBar = chalk.cyan('━'.repeat(progress)) + chalk.bold.yellow('◆') + chalk.dim('─'.repeat(Math.max(0, progWidth - progress)));
|
|
106
107
|
const icon = paused ? chalk.yellow('⏸') : chalk.green('▶');
|
|
107
108
|
const spStr = speed === 1 ? '1×' : `${speed}×`;
|
|
108
|
-
lines.push(` ${icon} ${progressBar}
|
|
109
|
+
lines.push(` ${icon} [${progressBar}] ${chalk.bold(`${pct.toFixed(0)}%`)} (${idx + 1}/${total}) ${chalk.dim(spStr)}`);
|
|
109
110
|
lines.push('');
|
|
110
|
-
lines.push(chalk.dim(' Space/p=pause ]/[=speed ←/→=step q=quit'));
|
|
111
|
+
lines.push(chalk.dim(' Space/p=pause ]/[=speed ←/→=step 0-9=jump % Home/End=seek q=quit'));
|
|
111
112
|
|
|
112
113
|
return lines;
|
|
113
114
|
}
|
|
@@ -166,8 +167,16 @@ export async function runReplayCommand(opts: ReplayOptions): Promise<void> {
|
|
|
166
167
|
if (key.name === 'space' || key.name === 'p') { paused = !paused; renderCurrent(); return; }
|
|
167
168
|
if (str === ']') { speed = Math.min(speed * 2, 32); renderCurrent(); return; }
|
|
168
169
|
if (str === '[') { speed = Math.max(speed / 2, 0.125); renderCurrent(); return; }
|
|
169
|
-
if (key.name === 'right') { frameIdx = Math.min(frameIdx + 1, rows.length - 1); renderCurrent(); return; }
|
|
170
|
-
if (key.name === 'left') { frameIdx = Math.max(frameIdx - 1, 0); renderCurrent(); return; }
|
|
170
|
+
if (key.name === 'right') { frameIdx = Math.min(frameIdx + (key.shift ? 10 : 1), rows.length - 1); renderCurrent(); return; }
|
|
171
|
+
if (key.name === 'left') { frameIdx = Math.max(frameIdx - (key.shift ? 10 : 1), 0); renderCurrent(); return; }
|
|
172
|
+
if (key.name === 'home' || str === '0') { frameIdx = 0; renderCurrent(); return; }
|
|
173
|
+
if (key.name === 'end') { frameIdx = rows.length - 1; renderCurrent(); return; }
|
|
174
|
+
if (str >= '1' && str <= '9') {
|
|
175
|
+
const frac = parseInt(str, 10) / 10;
|
|
176
|
+
frameIdx = Math.min(rows.length - 1, Math.round(frac * (rows.length - 1)));
|
|
177
|
+
renderCurrent();
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
171
180
|
});
|
|
172
181
|
|
|
173
182
|
const BASE_MS = 2000;
|
package/src/smartCmd.ts
CHANGED
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* pyre smart — S.M.A.R.T. disk health inspector.
|
|
3
3
|
*
|
|
4
|
-
* Uses smartctl (from smartmontools,
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Uses smartctl (from smartmontools) for detailed SMART data across macOS, Linux, and Windows.
|
|
5
|
+
* Falls back to native platform inspectors when smartctl is not installed:
|
|
6
|
+
* - macOS: diskutil
|
|
7
|
+
* - Windows: Get-PhysicalDisk & Get-StorageReliabilityCounter & Win32_DiskDrive
|
|
8
|
+
* - Linux: lsblk & hdparm
|
|
7
9
|
*/
|
|
8
10
|
import { execFile } from 'node:child_process';
|
|
9
11
|
import { promisify } from 'node:util';
|
|
10
12
|
import chalk from 'chalk';
|
|
13
|
+
import { runPowerShell } from './monitors/run.js';
|
|
11
14
|
|
|
12
15
|
const execFileAsync = promisify(execFile);
|
|
13
16
|
|
|
14
|
-
interface DiskInfo {
|
|
17
|
+
export interface DiskInfo {
|
|
15
18
|
device: string;
|
|
16
19
|
model: string;
|
|
17
20
|
serialNumber: string;
|
|
@@ -35,6 +38,74 @@ async function runCmd(cmd: string, args: string[], timeoutMs = 5000): Promise<st
|
|
|
35
38
|
}
|
|
36
39
|
}
|
|
37
40
|
|
|
41
|
+
export function parseWindowsPhysicalDiskCsv(csvRaw: string): DiskInfo[] {
|
|
42
|
+
const lines = csvRaw.split('\n').filter(l => l.trim().length > 0);
|
|
43
|
+
if (lines.length < 2) return [];
|
|
44
|
+
|
|
45
|
+
const headers = lines[0].split(',').map(h => h.trim().replace(/^"|"$/g, ''));
|
|
46
|
+
const disks: DiskInfo[] = [];
|
|
47
|
+
|
|
48
|
+
for (const line of lines.slice(1)) {
|
|
49
|
+
const values = line.split(',').map(v => v.trim().replace(/^"|"$/g, ''));
|
|
50
|
+
const row: Record<string, string> = {};
|
|
51
|
+
headers.forEach((h, i) => { row[h] = values[i] || ''; });
|
|
52
|
+
|
|
53
|
+
const deviceId = row.DeviceId || row.DeviceID || '0';
|
|
54
|
+
const friendlyName = row.FriendlyName || row.Model || 'Physical Disk';
|
|
55
|
+
const serialNumber = row.SerialNumber || '';
|
|
56
|
+
const mediaType = row.MediaType || row.InterfaceType || '';
|
|
57
|
+
const healthStatus = (row.HealthStatus || row.Status || '').toUpperCase();
|
|
58
|
+
const sizeBytes = parseFloat(row.Size) || 0;
|
|
59
|
+
|
|
60
|
+
let health: DiskInfo['health'] = 'Unknown';
|
|
61
|
+
if (healthStatus.includes('HEALTHY') || healthStatus.includes('OK') || healthStatus.includes('PASSED')) {
|
|
62
|
+
health = 'PASSED';
|
|
63
|
+
} else if (healthStatus.includes('UNHEALTHY') || healthStatus.includes('ERROR') || healthStatus.includes('FAILED')) {
|
|
64
|
+
health = 'FAILED';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const capacity = sizeBytes > 0
|
|
68
|
+
? sizeBytes >= 1e12 ? `${(sizeBytes / 1e12).toFixed(1)} TB`
|
|
69
|
+
: sizeBytes >= 1e9 ? `${(sizeBytes / 1e9).toFixed(0)} GB`
|
|
70
|
+
: `${(sizeBytes / 1e6).toFixed(0)} MB`
|
|
71
|
+
: undefined;
|
|
72
|
+
|
|
73
|
+
disks.push({
|
|
74
|
+
device: `\\\\.\\PhysicalDrive${deviceId}`,
|
|
75
|
+
model: friendlyName,
|
|
76
|
+
serialNumber,
|
|
77
|
+
health,
|
|
78
|
+
protocol: mediaType,
|
|
79
|
+
capacity,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return disks;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function parseWindowsReliabilityCsv(csvRaw: string): Partial<DiskInfo> {
|
|
87
|
+
const lines = csvRaw.split('\n').filter(l => l.trim().length > 0);
|
|
88
|
+
if (lines.length < 2) return {};
|
|
89
|
+
|
|
90
|
+
const headers = lines[0].split(',').map(h => h.trim().replace(/^"|"$/g, ''));
|
|
91
|
+
const values = lines[1].split(',').map(v => v.trim().replace(/^"|"$/g, ''));
|
|
92
|
+
const row: Record<string, string> = {};
|
|
93
|
+
headers.forEach((h, i) => { row[h] = values[i] || ''; });
|
|
94
|
+
|
|
95
|
+
const temp = parseFloat(row.Temperature);
|
|
96
|
+
const wear = parseFloat(row.Wear);
|
|
97
|
+
const poh = parseFloat(row.PowerOnHours);
|
|
98
|
+
const readErrors = parseFloat(row.ReadErrorsTotal);
|
|
99
|
+
const writeErrors = parseFloat(row.WriteErrorsTotal);
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
tempC: Number.isFinite(temp) && temp > 0 && temp < 120 ? temp : undefined,
|
|
103
|
+
percentUsed: Number.isFinite(wear) && wear >= 0 && wear <= 100 ? wear : undefined,
|
|
104
|
+
powerOnHours: Number.isFinite(poh) && poh >= 0 ? poh : undefined,
|
|
105
|
+
reallocatedSectors: (Number.isFinite(readErrors) ? readErrors : 0) + (Number.isFinite(writeErrors) ? writeErrors : 0),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
38
109
|
async function getPhysicalDisks(): Promise<string[]> {
|
|
39
110
|
// 1. First try smartctl --scan --json on all platforms
|
|
40
111
|
const scanRaw = await runCmd('smartctl', ['--scan', '--json']);
|
|
@@ -59,9 +130,19 @@ async function getPhysicalDisks(): Promise<string[]> {
|
|
|
59
130
|
return ['/dev/sda'];
|
|
60
131
|
}
|
|
61
132
|
|
|
62
|
-
// 3. Windows
|
|
133
|
+
// 3. Windows native query
|
|
63
134
|
if (process.platform === 'win32') {
|
|
64
|
-
|
|
135
|
+
try {
|
|
136
|
+
const raw = await runPowerShell(
|
|
137
|
+
`Get-PhysicalDisk -ErrorAction SilentlyContinue | Select-Object -ExpandProperty DeviceId`,
|
|
138
|
+
''
|
|
139
|
+
);
|
|
140
|
+
const ids = raw.split('\n').map(s => s.trim()).filter(Boolean);
|
|
141
|
+
if (ids.length > 0) {
|
|
142
|
+
return ids.map(id => `\\\\.\\PhysicalDrive${id}`);
|
|
143
|
+
}
|
|
144
|
+
} catch {}
|
|
145
|
+
return ['\\\\.\\PhysicalDrive0'];
|
|
65
146
|
}
|
|
66
147
|
|
|
67
148
|
// 4. macOS fallback: diskutil
|
|
@@ -121,6 +202,60 @@ async function collectSmartctlDisk(device: string): Promise<DiskInfo | null> {
|
|
|
121
202
|
}
|
|
122
203
|
}
|
|
123
204
|
|
|
205
|
+
async function collectWindowsDiskFallback(device: string): Promise<DiskInfo | null> {
|
|
206
|
+
const match = device.match(/PhysicalDrive(\d+)/i);
|
|
207
|
+
const diskNumber = match ? match[1] : '0';
|
|
208
|
+
|
|
209
|
+
try {
|
|
210
|
+
const rawDisk = await runPowerShell(
|
|
211
|
+
`Get-PhysicalDisk -DeviceId '${diskNumber}' -ErrorAction SilentlyContinue | Select-Object DeviceId, FriendlyName, SerialNumber, MediaType, HealthStatus, Size | ConvertTo-Csv -NoTypeInformation`,
|
|
212
|
+
''
|
|
213
|
+
);
|
|
214
|
+
const parsed = parseWindowsPhysicalDiskCsv(rawDisk);
|
|
215
|
+
if (parsed.length > 0) {
|
|
216
|
+
const info = parsed[0];
|
|
217
|
+
info.device = device;
|
|
218
|
+
|
|
219
|
+
// Query storage reliability counters for wear, temp, and power on hours
|
|
220
|
+
try {
|
|
221
|
+
const rawReliability = await runPowerShell(
|
|
222
|
+
`Get-StorageReliabilityCounter -Disk (Get-Disk -Number ${diskNumber} -ErrorAction SilentlyContinue) -ErrorAction SilentlyContinue | Select-Object Temperature, Wear, PowerOnHours, ReadErrorsTotal, WriteErrorsTotal | ConvertTo-Csv -NoTypeInformation`,
|
|
223
|
+
''
|
|
224
|
+
);
|
|
225
|
+
const rel = parseWindowsReliabilityCsv(rawReliability);
|
|
226
|
+
if (rel.tempC !== undefined) info.tempC = rel.tempC;
|
|
227
|
+
if (rel.percentUsed !== undefined) info.percentUsed = rel.percentUsed;
|
|
228
|
+
if (rel.powerOnHours !== undefined) info.powerOnHours = rel.powerOnHours;
|
|
229
|
+
if (rel.reallocatedSectors !== undefined) info.reallocatedSectors = rel.reallocatedSectors;
|
|
230
|
+
} catch {}
|
|
231
|
+
|
|
232
|
+
return info;
|
|
233
|
+
}
|
|
234
|
+
} catch {}
|
|
235
|
+
|
|
236
|
+
// Fallback to Win32_DiskDrive
|
|
237
|
+
try {
|
|
238
|
+
const rawWmi = await runPowerShell(
|
|
239
|
+
`Get-CimInstance Win32_DiskDrive -Filter "Index = ${diskNumber}" -ErrorAction SilentlyContinue | Select-Object Model, SerialNumber, Status, Size | ConvertTo-Csv -NoTypeInformation`,
|
|
240
|
+
''
|
|
241
|
+
);
|
|
242
|
+
const lines = rawWmi.split('\n').filter(l => l.trim());
|
|
243
|
+
if (lines.length >= 2) {
|
|
244
|
+
const values = lines[1].split(',').map(s => s.trim().replace(/^"|"$/g, ''));
|
|
245
|
+
const sizeBytes = parseFloat(values[3]) || 0;
|
|
246
|
+
return {
|
|
247
|
+
device,
|
|
248
|
+
model: values[0] || 'Physical Disk',
|
|
249
|
+
serialNumber: values[1] || '',
|
|
250
|
+
health: values[2]?.toUpperCase().includes('OK') ? 'PASSED' : 'Unknown',
|
|
251
|
+
capacity: sizeBytes >= 1e12 ? `${(sizeBytes / 1e12).toFixed(1)} TB` : `${(sizeBytes / 1e9).toFixed(0)} GB`,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
} catch {}
|
|
255
|
+
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
|
|
124
259
|
async function collectDiskutilFallback(device: string): Promise<DiskInfo | null> {
|
|
125
260
|
const raw = await runCmd('diskutil', ['info', device]);
|
|
126
261
|
if (!raw) return null;
|
|
@@ -156,8 +291,11 @@ export async function runSmartCommand(): Promise<void> {
|
|
|
156
291
|
const hasSmartctl = smartctlVersion.includes('smartmontools');
|
|
157
292
|
|
|
158
293
|
if (!hasSmartctl) {
|
|
159
|
-
console.log(chalk.yellow(' ⚠ smartctl not found. Showing
|
|
160
|
-
|
|
294
|
+
console.log(chalk.yellow(' ⚠ smartctl not found. Showing native storage health metrics.'));
|
|
295
|
+
const installHint = process.platform === 'win32'
|
|
296
|
+
? 'winget install smartmontools'
|
|
297
|
+
: (process.platform === 'linux' ? 'sudo apt install smartmontools' : 'brew install smartmontools');
|
|
298
|
+
console.log(chalk.dim(' For full SMART data: ') + chalk.cyan(installHint) + '\n');
|
|
161
299
|
}
|
|
162
300
|
|
|
163
301
|
const devices = await getPhysicalDisks();
|
|
@@ -169,7 +307,8 @@ export async function runSmartCommand(): Promise<void> {
|
|
|
169
307
|
for (const device of devices) {
|
|
170
308
|
let info: DiskInfo | null = null;
|
|
171
309
|
if (hasSmartctl) info = await collectSmartctlDisk(device);
|
|
172
|
-
if (!info) info = await
|
|
310
|
+
if (!info && process.platform === 'win32') info = await collectWindowsDiskFallback(device);
|
|
311
|
+
if (!info && process.platform === 'darwin') info = await collectDiskutilFallback(device);
|
|
173
312
|
if (!info) continue;
|
|
174
313
|
|
|
175
314
|
console.log(chalk.bold(` ${info.device}`));
|
|
@@ -200,6 +339,6 @@ export async function runSmartCommand(): Promise<void> {
|
|
|
200
339
|
}
|
|
201
340
|
|
|
202
341
|
if (!hasSmartctl) {
|
|
203
|
-
console.log(chalk.dim(' Install smartmontools for
|
|
342
|
+
console.log(chalk.dim(' Install smartmontools for complete SMART attribute dump.\n'));
|
|
204
343
|
}
|
|
205
344
|
}
|
package/src/state/config.ts
CHANGED
|
@@ -41,6 +41,7 @@ export interface PyreConfig {
|
|
|
41
41
|
mouseEnabled?: boolean;
|
|
42
42
|
cpuAlertPct?: number;
|
|
43
43
|
tempAlertC?: number;
|
|
44
|
+
tempUnit?: 'c' | 'f';
|
|
44
45
|
electricityRateKwH?: number;
|
|
45
46
|
notificationsEnabled?: boolean;
|
|
46
47
|
p2pPort?: number;
|
|
@@ -104,6 +105,14 @@ export interface PyreConfig {
|
|
|
104
105
|
tableDensity?: string;
|
|
105
106
|
defaultFilter?: string;
|
|
106
107
|
defaultSort?: string;
|
|
108
|
+
/** Extra apps (name or bundle id) that `pyre optimize` will never quit */
|
|
109
|
+
optimizeProtectedApps?: string[];
|
|
110
|
+
/** Default idle threshold for `pyre optimize apps` (e.g. "2h") */
|
|
111
|
+
optimizeIdleThreshold?: string;
|
|
112
|
+
/** Number of most-recently-used apps `pyre optimize apps` always keeps open */
|
|
113
|
+
optimizeKeepRecent?: number;
|
|
114
|
+
/** Default launch action when pyre is executed without arguments ('live' | 'menu') */
|
|
115
|
+
defaultAction?: 'live' | 'menu';
|
|
107
116
|
}
|
|
108
117
|
|
|
109
118
|
export const DEFAULT_CONFIG: Required<PyreConfig> = {
|
|
@@ -169,6 +178,10 @@ export const DEFAULT_CONFIG: Required<PyreConfig> = {
|
|
|
169
178
|
tableDensity: 'standard',
|
|
170
179
|
defaultFilter: 'all',
|
|
171
180
|
defaultSort: 'cpu',
|
|
181
|
+
optimizeProtectedApps: [],
|
|
182
|
+
optimizeIdleThreshold: '2h',
|
|
183
|
+
optimizeKeepRecent: 5,
|
|
184
|
+
defaultAction: 'live',
|
|
172
185
|
};
|
|
173
186
|
|
|
174
187
|
export function getConfigDir(): string {
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pyre statusline — Headless Terminal Statusline Emitter.
|
|
3
|
+
*
|
|
4
|
+
* Emits lightweight, high-performance telemetry strings designed for
|
|
5
|
+
* Tmux status-right, Starship prompt, Waybar, Polybar, and Neovim lualine.
|
|
6
|
+
*/
|
|
7
|
+
import os from 'node:os';
|
|
8
|
+
import chalk from 'chalk';
|
|
9
|
+
import { collectAll } from './monitors/collectors.js';
|
|
10
|
+
|
|
11
|
+
export type StatuslineFormat = 'tmux' | 'starship' | 'json' | 'pango' | 'plain';
|
|
12
|
+
|
|
13
|
+
export interface StatuslineMetrics {
|
|
14
|
+
cpuUsage: number;
|
|
15
|
+
memUsage: number;
|
|
16
|
+
tempC?: number;
|
|
17
|
+
batteryPct?: number;
|
|
18
|
+
batteryCharging?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function collectStatuslineMetrics(): Promise<StatuslineMetrics> {
|
|
22
|
+
const data = await collectAll();
|
|
23
|
+
const cpuUsage = data.cpu.usage;
|
|
24
|
+
const memUsage = data.memory.usagePercent;
|
|
25
|
+
const tempC = data.cpu.temperature;
|
|
26
|
+
const batteryPct = data.battery ? data.battery.percent : undefined;
|
|
27
|
+
const batteryCharging = data.battery ? data.battery.isCharging : undefined;
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
cpuUsage,
|
|
31
|
+
memUsage,
|
|
32
|
+
tempC,
|
|
33
|
+
batteryPct,
|
|
34
|
+
batteryCharging,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function formatStatusline(m: StatuslineMetrics, format: StatuslineFormat): string {
|
|
39
|
+
const cpu = Math.round(m.cpuUsage);
|
|
40
|
+
const mem = Math.round(m.memUsage);
|
|
41
|
+
const temp = m.tempC !== undefined ? `${Math.round(m.tempC)}°C` : '';
|
|
42
|
+
const batt = m.batteryPct !== undefined ? `${m.batteryCharging ? '⚡' : ''}${m.batteryPct}%` : '';
|
|
43
|
+
|
|
44
|
+
switch (format) {
|
|
45
|
+
case 'json':
|
|
46
|
+
return JSON.stringify({
|
|
47
|
+
cpu: m.cpuUsage,
|
|
48
|
+
mem: m.memUsage,
|
|
49
|
+
temp: m.tempC,
|
|
50
|
+
battery: m.batteryPct,
|
|
51
|
+
charging: m.batteryCharging,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
case 'tmux': {
|
|
55
|
+
const parts: string[] = [];
|
|
56
|
+
parts.push(`#[fg=cyan]CPU: ${cpu}%`);
|
|
57
|
+
parts.push(`#[fg=magenta]RAM: ${mem}%`);
|
|
58
|
+
if (temp) parts.push(`#[fg=yellow]${temp}`);
|
|
59
|
+
if (batt) parts.push(`#[fg=green]${batt}`);
|
|
60
|
+
return parts.join(' #[fg=dim]| ');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
case 'pango': {
|
|
64
|
+
const parts: string[] = [];
|
|
65
|
+
parts.push(`<span color="#22d3ee">CPU: ${cpu}%</span>`);
|
|
66
|
+
parts.push(`<span color="#a78bfa">RAM: ${mem}%</span>`);
|
|
67
|
+
if (temp) parts.push(`<span color="#fb923c">${temp}</span>`);
|
|
68
|
+
if (batt) parts.push(`<span color="#34d399">${batt}</span>`);
|
|
69
|
+
return parts.join(' | ');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
case 'starship':
|
|
73
|
+
case 'plain':
|
|
74
|
+
default: {
|
|
75
|
+
const parts = [`🔥 ${cpu}%`, `${mem}%`];
|
|
76
|
+
if (temp) parts.push(temp);
|
|
77
|
+
if (batt) parts.push(batt);
|
|
78
|
+
return parts.join(' | ');
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export async function runStatuslineCommand(format: StatuslineFormat = 'plain'): Promise<void> {
|
|
84
|
+
const metrics = await collectStatuslineMetrics();
|
|
85
|
+
const output = formatStatusline(metrics, format);
|
|
86
|
+
process.stdout.write(output + '\n');
|
|
87
|
+
}
|
package/src/syncCmd.ts
CHANGED
|
@@ -41,6 +41,18 @@ interface SyncManifest {
|
|
|
41
41
|
function getDefaultSyncDir(): string {
|
|
42
42
|
if (process.env.PYRE_SYNC_DIR) return process.env.PYRE_SYNC_DIR;
|
|
43
43
|
|
|
44
|
+
// Windows native OneDrive path
|
|
45
|
+
if (process.platform === 'win32') {
|
|
46
|
+
const oneDriveEnv = process.env.OneDrive || process.env.OneDriveConsumer;
|
|
47
|
+
if (oneDriveEnv && fs.existsSync(oneDriveEnv)) {
|
|
48
|
+
return path.join(oneDriveEnv, 'PyreSync');
|
|
49
|
+
}
|
|
50
|
+
const userOneDrive = path.join(os.homedir(), 'OneDrive');
|
|
51
|
+
if (fs.existsSync(userOneDrive)) {
|
|
52
|
+
return path.join(userOneDrive, 'PyreSync');
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
44
56
|
// macOS native iCloud Drive path
|
|
45
57
|
const icloudPath = path.join(os.homedir(), 'Library', 'Mobile Documents', 'com~apple~CloudDocs', 'PyreSync');
|
|
46
58
|
if (fs.existsSync(path.dirname(icloudPath))) {
|