pyre-cli 10.2.0 → 10.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/README.md +75 -18
  2. package/dist/{alertChannels-TPSQIJ7S.js → alertChannels-MNX4XH6J.js} +1 -0
  3. package/dist/{anomaliesCmd-25OFKYPK.js → anomaliesCmd-HHTY5LYL.js} +2 -0
  4. package/dist/benchGitCmd-HIXUC5EK.js +87 -0
  5. package/dist/{benchmarkCmd-A3ETK6VY.js → benchmarkCmd-B7DLP2VC.js} +2 -0
  6. package/dist/{blender-FGH3ASEQ.js → blender-6ZX5SPYC.js} +3 -1
  7. package/dist/{brewHealth-P2GW5Y5C.js → brewHealth-VFW33QB3.js} +1 -0
  8. package/dist/budgetCmd-TGO3RYLN.js +11 -0
  9. package/dist/{buildCmd-CDPZ2ZFA.js → buildCmd-E7KUCFYG.js} +3 -1
  10. package/dist/{checkCmd-WQAHAUWO.js → checkCmd-QLYTP77Y.js} +5 -3
  11. package/dist/chunk-3RG5ZIWI.js +10 -0
  12. package/dist/{chunk-VIACN2RU.js → chunk-FYCBGUTW.js} +41 -19
  13. package/dist/chunk-JH7P6KLN.js +143 -0
  14. package/dist/{chunk-QM5L43EK.js → chunk-LDJB5V4N.js} +5 -1
  15. package/dist/chunk-NMGQVRCA.js +111 -0
  16. package/dist/chunk-OCL7P7N7.js +936 -0
  17. package/dist/{chunk-23YWCI3O.js → chunk-QEH4LR6P.js} +28 -686
  18. package/dist/{chunk-KVUMOA6C.js → chunk-RKPGPVTO.js} +77 -4
  19. package/dist/chunk-UF3XQ3R3.js +3800 -0
  20. package/dist/{collectors-JDN5Y7EV.js → collectors-V7SCIIDH.js} +5 -3
  21. package/dist/{completions-ZVTNE52B.js → completions-FTPXDSR2.js} +44 -3
  22. package/dist/{config-J4YMADEF.js → config-5WW5M63G.js} +2 -1
  23. package/dist/containersCmd-FSMYOBG2.js +84 -0
  24. package/dist/{doctor-LHFW4UJP.js → doctor-O4DIAMHK.js} +3 -2
  25. package/dist/{explainCmd-MF6PIMLW.js → explainCmd-7PBPA7O5.js} +1 -0
  26. package/dist/{extensions-XJGHG7X3.js → extensions-QQEV7U3D.js} +1 -0
  27. package/dist/{firebaseSync-ON3RZ6KX.js → firebaseSync-LRN67UTV.js} +2 -0
  28. package/dist/flightRecorder-GJIAQOXC.js +162 -0
  29. package/dist/index.js +737 -4125
  30. package/dist/inspectCmd-5FXJBJTK.js +137 -0
  31. package/dist/menu-AJFGDLBP.js +1233 -0
  32. package/dist/monitors/platform/windows-ui.ps1 +31 -29
  33. package/dist/{netUsageCmd-67IBZZYN.js → netUsageCmd-JP6YKBPF.js} +7 -5
  34. package/dist/optimizeCmd-WZKLFYZU.js +1110 -0
  35. package/dist/{pipeCmd-2N4JPKYE.js → pipeCmd-CPK6FVKB.js} +5 -3
  36. package/dist/playbookCmd-FRROIEXR.js +145 -0
  37. package/dist/{profileProcCmd-N4H6MCBT.js → profileProcCmd-5Y3733E7.js} +64 -13
  38. package/dist/{prometheus-5BL5PVLC.js → prometheus-CYNUR3QT.js} +1 -0
  39. package/dist/{prometheusCmd-LEUM5WCZ.js → prometheusCmd-G2CSEZRQ.js} +5 -3
  40. package/dist/{replayCmd-Z3PYXY7V.js → replayCmd-ODB5SPOQ.js} +25 -6
  41. package/dist/{run-HOGQDDTA.js → run-UJQYT2TO.js} +1 -0
  42. package/dist/{serveCmd-L7LVJOZN.js → serveCmd-KDPWLVQI.js} +5 -3
  43. package/dist/{service-7GIX3YNC.js → service-LIYYR3UI.js} +1 -0
  44. package/dist/{smartCmd-ODWKSFNG.js → smartCmd-2BJJ37GV.js} +130 -5
  45. package/dist/statuslineCmd-EUKYRR2G.js +75 -0
  46. package/dist/{stressCmd-MFXMFXYX.js → stressCmd-GKHGY26X.js} +2 -0
  47. package/dist/{syncCmd-42IRF633.js → syncCmd-MIBFKX24.js} +16 -5
  48. package/dist/topoCmd-RTXQJ6XA.js +189 -0
  49. package/dist/{updateCheck-52X6LJG5.js → updateCheck-LJHEMG3G.js} +2 -0
  50. package/dist/{watchdogCmd-6GR4UN6B.js → watchdogCmd-XM6OPTKM.js} +41 -6
  51. package/dist/{wizard-JQMQWFAW.js → wizard-PRDOJROQ.js} +2 -1
  52. package/package.json +2 -2
  53. package/src/benchGitCmd.ts +110 -0
  54. package/src/budgetCmd.ts +199 -0
  55. package/src/completions.ts +42 -3
  56. package/src/containersCmd.ts +106 -0
  57. package/src/doctor.ts +86 -6
  58. package/src/easy_features.test.ts +83 -0
  59. package/src/flightRecorder.ts +214 -0
  60. package/src/index.ts +183 -9
  61. package/src/inspectCmd.ts +165 -0
  62. package/src/live/input.ts +70 -3
  63. package/src/live/render.ts +1 -1
  64. package/src/live/state.ts +2 -1
  65. package/src/menu/commands.ts +754 -0
  66. package/src/menu/index.ts +358 -0
  67. package/src/menu/render.ts +272 -0
  68. package/src/menu/types.ts +63 -0
  69. package/src/menu.test.ts +110 -0
  70. package/src/monitors/blender.ts +49 -19
  71. package/src/monitors/buildTracker.ts +72 -54
  72. package/src/monitors/platform/windows-ui.ps1 +31 -29
  73. package/src/monitors/platform/windows.ts +613 -280
  74. package/src/optimizeCmd.ts +1248 -0
  75. package/src/platform.test.ts +135 -0
  76. package/src/playbookCmd.ts +156 -0
  77. package/src/power_features.test.ts +251 -0
  78. package/src/profileProcCmd.ts +68 -21
  79. package/src/replayCmd.ts +15 -6
  80. package/src/smartCmd.ts +149 -10
  81. package/src/state/config.ts +13 -0
  82. package/src/statuslineCmd.ts +87 -0
  83. package/src/syncCmd.ts +12 -0
  84. package/src/topoCmd.ts +83 -50
  85. package/src/watchdogCmd.ts +40 -3
  86. package/src/xbar.ts +195 -17
  87. package/dist/chunk-M73HCQD2.js +0 -95
  88. package/dist/topoCmd-DZ3YS5IL.js +0 -148
@@ -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
- * or fallback thread analysis, then presents an annotated call-tree and
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
- // Try macOS native `sample` command (available on macOS)
42
- if (process.platform === 'darwin') {
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
- // Fallback: If sample produced no usable output or running on Linux, sample via thread / proc stats
56
- if (!sampleOutput || sampleOutput.length < 100) {
98
+ // 3. POSIX Fallback (Linux / fallback macOS)
99
+ if (!sampleOutput || sampleOutput.length < 50) {
57
100
  usedFallback = true;
58
- try {
59
- const { stdout } = await execFileAsync('ps', ['-M', '-p', String(pid), '-o', 'pid,thread,pcpu,pmem,state,time,command']);
60
- sampleOutput = stdout;
61
- } catch {
62
- sampleOutput = `Process PID ${pid} active. Deep stack sampling requires elevated permissions or user-owned process.`;
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:') || line.includes('Identifier:')) {
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, 4)) {
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 Functions:'));
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: Kernel/SIP restriction prevented deep call-graph unwind.'));
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('█'.repeat(progress)) + chalk.dim('░'.repeat(progWidth - progress));
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} ${chalk.bold(`${idx + 1}/${total}`)} ${chalk.dim(spStr)}`);
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, via brew install smartmontools)
5
- * for detailed SMART data. Falls back to diskutil info when
6
- * smartctl is not installed.
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 fallback
133
+ // 3. Windows native query
63
134
  if (process.platform === 'win32') {
64
- return ['/dev/sda', '/dev/csmi0,0'];
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 basic disk info.'));
160
- console.log(chalk.dim(' For full SMART data: ') + chalk.cyan('brew install smartmontools') + '\n');
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 collectDiskutilFallback(device);
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 full SMART attributes.\n'));
342
+ console.log(chalk.dim(' Install smartmontools for complete SMART attribute dump.\n'));
204
343
  }
205
344
  }
@@ -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))) {