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.
- package/README.md +75 -18
- 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 +737 -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 +183 -9
- 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/index.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import fs from 'node:fs';
|
|
10
10
|
import os from 'node:os';
|
|
11
11
|
import path from 'node:path';
|
|
12
|
+
import crypto from 'node:crypto';
|
|
12
13
|
import { fileURLToPath } from 'node:url';
|
|
13
14
|
import readline from 'node:readline';
|
|
14
15
|
import { Command } from 'commander';
|
|
@@ -101,10 +102,34 @@ program
|
|
|
101
102
|
.option('--top <n>', 'Number of items to display in netusage command (default: 15)')
|
|
102
103
|
.option('--raw', 'Print unparsed output for profiler')
|
|
103
104
|
.option('--sync-dir <dir>', 'Custom directory path for cloud profile/config sync')
|
|
104
|
-
.option('--scan', 'Perform active network sweep on LAN neighbors for topology map')
|
|
105
|
+
.option('--scan', 'Perform active network sweep on LAN neighbors for topology map')
|
|
106
|
+
.option('--idle <duration>', 'Idle threshold for optimize apps (e.g. 30m, 2h)')
|
|
107
|
+
.option('--keep <n>', 'Always keep the N most recently used apps open (optimize)')
|
|
108
|
+
.option('-y, --yes', 'Skip confirmation prompts (optimize)')
|
|
109
|
+
.option('--force', 'Terminate apps that do not quit gracefully (optimize apps)')
|
|
110
|
+
.option('--clean', 'Delete measured caches after confirmation (optimize caches)')
|
|
111
|
+
.option('--empty', 'Empty the Trash after confirmation (optimize trash)')
|
|
112
|
+
.option('--auto-quit <duration>', 'Let the optimize tracker automatically quit apps idle this long')
|
|
113
|
+
.option('-m, --menu', 'Launch interactive TUI command center & tools navigator')
|
|
114
|
+
.option('-i, --interactive', 'Alias for --menu')
|
|
115
|
+
.option('--budget-cpu <pct>', 'Maximum allowable peak CPU % for pyre run')
|
|
116
|
+
.option('--budget-mem <bytes>', 'Maximum allowable peak memory for pyre run (e.g. 500MB, 2GB)')
|
|
117
|
+
.option('--budget-time <sec>', 'Maximum allowable duration for pyre run in seconds')
|
|
118
|
+
.option('--nice <val>', 'Nice value for pyre inspect --nice (-20 to 19)')
|
|
119
|
+
.option('--format <format>', 'Output format for statusline (tmux, starship, json, pango, plain)');
|
|
105
120
|
|
|
106
121
|
program.addHelpText('after', `
|
|
122
|
+
💡 Tip: Can't remember all commands? Run 'pyre menu' to browse and run all tools interactively!
|
|
123
|
+
|
|
107
124
|
Commands:
|
|
125
|
+
menu, tui, launcher Interactive TUI command center & tools navigator
|
|
126
|
+
playbook [run|list] Execute sequenced multi-step macro recipes (audit, clean, triage)
|
|
127
|
+
flight [dump|status|record] Forensic flight recorder black box in RAM
|
|
128
|
+
run [options] -- <cmd...> Performance budgeting runner for CI/CD gates
|
|
129
|
+
bench-git <b1> <b2> <cmd> Cross-branch git performance differ
|
|
130
|
+
statusline Headless terminal statusline string (tmux, starship, neovim)
|
|
131
|
+
inspect <pid> Process deep-dive inspector (lsof, sockets, nice)
|
|
132
|
+
containers Container and micro-VM telemetry (Docker, Podman, OrbStack)
|
|
108
133
|
live Interactive live dashboard
|
|
109
134
|
check One-line plain-English health summary
|
|
110
135
|
pipe Continuous newline-delimited JSON stream for scripting
|
|
@@ -142,6 +167,8 @@ program.addHelpText('after', `
|
|
|
142
167
|
build Active compiler and build system tracker
|
|
143
168
|
topo Network topology mapper and pyre peer auto-discovery
|
|
144
169
|
sync <push|pull|status> Synchronize config profiles across Macs via iCloud/Drive
|
|
170
|
+
optimize [apps|track|memory|caches|trash|startup|dns]
|
|
171
|
+
Mac optimizer: quit idle apps, free memory, clean caches
|
|
145
172
|
`);
|
|
146
173
|
|
|
147
174
|
program.parse(process.argv);
|
|
@@ -202,7 +229,7 @@ async function runServerCommand(): Promise<void> {
|
|
|
202
229
|
}
|
|
203
230
|
|
|
204
231
|
const port = 9876;
|
|
205
|
-
const password = '
|
|
232
|
+
const password = crypto.randomBytes(12).toString('hex');
|
|
206
233
|
|
|
207
234
|
console.log(chalk.bold(`\n pyre server`));
|
|
208
235
|
console.log(chalk.dim(` Detected IP: ${ip}`));
|
|
@@ -225,6 +252,106 @@ async function main() {
|
|
|
225
252
|
|
|
226
253
|
const cmd = program.args[0];
|
|
227
254
|
|
|
255
|
+
if (cmd === 'menu' || cmd === 'tui' || cmd === 'launcher' || cmd === 'interactive' || opts.menu || opts.interactive) {
|
|
256
|
+
const { runMenu } = await import('./menu/index.js');
|
|
257
|
+
await runMenu({
|
|
258
|
+
theme: opts.theme || config.theme,
|
|
259
|
+
});
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (!cmd && config.defaultAction === 'menu' && !isExportMode() && !opts.once && process.stdout.isTTY) {
|
|
264
|
+
const { runMenu } = await import('./menu/index.js');
|
|
265
|
+
await runMenu({
|
|
266
|
+
theme: opts.theme || config.theme,
|
|
267
|
+
});
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (cmd === 'playbook') {
|
|
272
|
+
const sub = program.args[1];
|
|
273
|
+
const { runPlaybook, listPlaybooks } = await import('./playbookCmd.js');
|
|
274
|
+
if (sub === 'list' || !sub) {
|
|
275
|
+
listPlaybooks();
|
|
276
|
+
} else if (sub === 'run') {
|
|
277
|
+
const id = program.args[2] || 'audit';
|
|
278
|
+
await runPlaybook(id);
|
|
279
|
+
} else {
|
|
280
|
+
await runPlaybook(sub);
|
|
281
|
+
}
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (cmd === 'flight') {
|
|
286
|
+
const sub = program.args[1] || 'dump';
|
|
287
|
+
const { runFlightCommand } = await import('./flightRecorder.js');
|
|
288
|
+
await runFlightCommand(sub, {
|
|
289
|
+
out: opts.out,
|
|
290
|
+
duration: opts.duration ? parseInt(opts.duration, 10) : undefined,
|
|
291
|
+
});
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (cmd === 'run') {
|
|
296
|
+
const rawArgs = process.argv;
|
|
297
|
+
const dashDashIdx = rawArgs.indexOf('--');
|
|
298
|
+
const targetCommand = dashDashIdx !== -1 ? rawArgs.slice(dashDashIdx + 1) : program.args.slice(1);
|
|
299
|
+
const { runWithBudget, parseBytesString } = await import('./budgetCmd.js');
|
|
300
|
+
const res = await runWithBudget(targetCommand, {
|
|
301
|
+
maxCpuPct: opts.budgetCpu ? parseFloat(opts.budgetCpu) : undefined,
|
|
302
|
+
maxMemoryBytes: opts.budgetMem ? parseBytesString(opts.budgetMem) : undefined,
|
|
303
|
+
maxTimeSec: opts.budgetTime ? parseFloat(opts.budgetTime) : undefined,
|
|
304
|
+
}, {
|
|
305
|
+
markdown: !!opts.markdown || !!opts.md,
|
|
306
|
+
outFile: opts.out,
|
|
307
|
+
});
|
|
308
|
+
if (res.exitCode !== 0) {
|
|
309
|
+
process.exit(res.exitCode);
|
|
310
|
+
}
|
|
311
|
+
if (res.violations.length > 0) {
|
|
312
|
+
process.exit(124);
|
|
313
|
+
}
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (cmd === 'bench-git') {
|
|
318
|
+
const branchA = program.args[1];
|
|
319
|
+
const branchB = program.args[2];
|
|
320
|
+
const commandStr = program.args.slice(3).join(' ');
|
|
321
|
+
if (!branchA || !branchB || !commandStr) {
|
|
322
|
+
console.log(chalk.red(' ✖ Usage: pyre bench-git <branchA> <branchB> "<command>"'));
|
|
323
|
+
process.exit(1);
|
|
324
|
+
}
|
|
325
|
+
const { runBenchGitCommand } = await import('./benchGitCmd.js');
|
|
326
|
+
await runBenchGitCommand(branchA, branchB, commandStr);
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (cmd === 'statusline') {
|
|
331
|
+
const { runStatuslineCommand } = await import('./statuslineCmd.js');
|
|
332
|
+
await runStatuslineCommand(opts.format || 'plain');
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (cmd === 'inspect') {
|
|
337
|
+
const targetPid = parseInt(program.args[1], 10);
|
|
338
|
+
const { runInspectCommand } = await import('./inspectCmd.js');
|
|
339
|
+
const niceVal = opts.nice !== undefined ? parseInt(opts.nice, 10) : undefined;
|
|
340
|
+
await runInspectCommand(targetPid, {
|
|
341
|
+
nice: niceVal,
|
|
342
|
+
json: !!opts.json,
|
|
343
|
+
});
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (cmd === 'containers') {
|
|
348
|
+
const { runContainersCommand } = await import('./containersCmd.js');
|
|
349
|
+
await runContainersCommand({
|
|
350
|
+
json: !!opts.json,
|
|
351
|
+
});
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
|
|
228
355
|
if (cmd === 'check') {
|
|
229
356
|
const { runCheckCommand } = await import('./checkCmd.js');
|
|
230
357
|
await runCheckCommand({
|
|
@@ -308,6 +435,23 @@ async function main() {
|
|
|
308
435
|
return;
|
|
309
436
|
}
|
|
310
437
|
|
|
438
|
+
if (cmd === 'optimize') {
|
|
439
|
+
const { runOptimizeCommand } = await import('./optimizeCmd.js');
|
|
440
|
+
await runOptimizeCommand(program.args[1], program.args.slice(2), {
|
|
441
|
+
idle: opts.idle,
|
|
442
|
+
keep: opts.keep,
|
|
443
|
+
yes: !!opts.yes,
|
|
444
|
+
force: !!opts.force,
|
|
445
|
+
clean: !!opts.clean,
|
|
446
|
+
empty: !!opts.empty,
|
|
447
|
+
dryRun: !!opts.dryRun,
|
|
448
|
+
install: !!opts.install,
|
|
449
|
+
uninstall: !!opts.uninstall,
|
|
450
|
+
autoQuit: opts.autoQuit,
|
|
451
|
+
});
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
|
|
311
455
|
if (cmd === 'replay') {
|
|
312
456
|
const file = program.args[1];
|
|
313
457
|
if (!file) {
|
|
@@ -579,7 +723,12 @@ async function main() {
|
|
|
579
723
|
}
|
|
580
724
|
|
|
581
725
|
if (cmd === 'xbar') {
|
|
582
|
-
|
|
726
|
+
const sub = program.args[1];
|
|
727
|
+
await generateXbarPlugin({
|
|
728
|
+
install: !!opts.install || sub === 'install',
|
|
729
|
+
yes: !!opts.yes,
|
|
730
|
+
dir: opts.dir,
|
|
731
|
+
});
|
|
583
732
|
return;
|
|
584
733
|
}
|
|
585
734
|
|
|
@@ -623,9 +772,13 @@ async function main() {
|
|
|
623
772
|
if (opts.alertPushoverToken) config.pushoverToken = opts.alertPushoverToken;
|
|
624
773
|
if (opts.alertPushoverUser) config.pushoverUser = opts.alertPushoverUser;
|
|
625
774
|
if (opts.alertNtfy) config.ntfyUrl = opts.alertNtfy;
|
|
626
|
-
|
|
775
|
+
if (cmd && cmd !== 'live' && cmd !== 'p2p') {
|
|
776
|
+
console.error(chalk.red(`\n ✖ Unknown command: '${cmd}'`));
|
|
777
|
+
console.log(chalk.yellow(` 💡 Tip: Can't remember a command? Run 'pyre menu' to browse and run all tools interactively!\n`));
|
|
778
|
+
process.exit(1);
|
|
779
|
+
}
|
|
627
780
|
|
|
628
|
-
if (cmd === 'live' || (!isExportMode() && !opts.once
|
|
781
|
+
if (cmd === 'live' || (!cmd && !isExportMode() && !opts.once)) {
|
|
629
782
|
if (!process.stdout.isTTY && cmd !== 'live') {
|
|
630
783
|
await runWebCommand();
|
|
631
784
|
return;
|
|
@@ -639,6 +792,7 @@ async function main() {
|
|
|
639
792
|
animation: config.splashAnimation as any,
|
|
640
793
|
});
|
|
641
794
|
const interval = parseFloat(opts.interval) || 2;
|
|
795
|
+
const tempUnit: 'c' | 'f' = (opts.tempUnit?.toLowerCase() === 'f' || config.tempUnit === 'f') ? 'f' : 'c';
|
|
642
796
|
await startLive({
|
|
643
797
|
interval,
|
|
644
798
|
detailed: opts.detailed ?? config.detailed,
|
|
@@ -673,13 +827,14 @@ async function main() {
|
|
|
673
827
|
|
|
674
828
|
const data = await collectAll({ detailed: opts.detailed, processLimit });
|
|
675
829
|
|
|
830
|
+
const tempUnit: 'c' | 'f' = (opts.tempUnit?.toLowerCase() === 'f' || config.tempUnit === 'f') ? 'f' : 'c';
|
|
676
831
|
let output: string;
|
|
677
832
|
if (opts.json) output = formatJson(data);
|
|
678
833
|
else if (opts.html) output = formatHtml(data);
|
|
679
834
|
else if (opts.md) output = formatMarkdown(data);
|
|
680
835
|
else if (opts.csv) output = formatCsv(data);
|
|
681
836
|
else if (opts.tsv) output = formatTsv(data);
|
|
682
|
-
else output = formatTable(data, { width: process.stdout.columns || 80, sortBy: opts.sort, treeView: opts.tree ?? config.treeView, minimal: !!opts.minimal, visible: { packets: opts.packets ? true : undefined } });
|
|
837
|
+
else output = formatTable(data, { width: process.stdout.columns || 80, sortBy: opts.sort, treeView: opts.tree ?? config.treeView, minimal: !!opts.minimal, visible: { packets: opts.packets ? true : undefined }, tempUnit });
|
|
683
838
|
|
|
684
839
|
console.log(output);
|
|
685
840
|
|
|
@@ -931,6 +1086,25 @@ async function runInfoCommand(): Promise<void> {
|
|
|
931
1086
|
}
|
|
932
1087
|
}
|
|
933
1088
|
|
|
1089
|
+
if (process.platform === 'win32') {
|
|
1090
|
+
lines.push('');
|
|
1091
|
+
lines.push(chalk.cyan.bold(' ⏱ System Restore & Recovery:'));
|
|
1092
|
+
try {
|
|
1093
|
+
const { runPowerShell } = await import('./monitors/run.js');
|
|
1094
|
+
const srRaw = await runPowerShell(
|
|
1095
|
+
`(Get-CimInstance -Namespace root/default -ClassName SystemRestore -ErrorAction SilentlyContinue | Select-Object -Last 1 -ExpandProperty Description) -join ''`,
|
|
1096
|
+
''
|
|
1097
|
+
);
|
|
1098
|
+
if (srRaw.trim()) {
|
|
1099
|
+
lines.push(` • Latest Restore Point: ${chalk.green(srRaw.trim())}`);
|
|
1100
|
+
} else {
|
|
1101
|
+
lines.push(' • System Restore active (no custom checkpoints)');
|
|
1102
|
+
}
|
|
1103
|
+
} catch {
|
|
1104
|
+
lines.push(' • System Restore available');
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
|
|
934
1108
|
console.log(lines.join('\n'));
|
|
935
1109
|
}
|
|
936
1110
|
|
|
@@ -1728,10 +1902,10 @@ async function runBenchCommand(benchCmd: string): Promise<void> {
|
|
|
1728
1902
|
let powerSamplesCount = 0;
|
|
1729
1903
|
|
|
1730
1904
|
const child = process.platform === 'win32'
|
|
1731
|
-
? spawn('cmd.exe', ['/c', benchCmd]
|
|
1905
|
+
? spawn('cmd.exe', ['/c', benchCmd])
|
|
1732
1906
|
: spawn('sh', ['-c', benchCmd]);
|
|
1733
|
-
child.stdout.on('data', d => process.stdout.write(d));
|
|
1734
|
-
child.stderr.on('data', d => process.stdout.write(d));
|
|
1907
|
+
if (child.stdout) child.stdout.on('data', d => process.stdout.write(d));
|
|
1908
|
+
if (child.stderr) child.stderr.on('data', d => process.stdout.write(d));
|
|
1735
1909
|
|
|
1736
1910
|
const interval = parseFloat(opts.interval) || 2;
|
|
1737
1911
|
const handle = setInterval(async () => {
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pyre inspect — Deep-Dive Process Inspector.
|
|
3
|
+
*
|
|
4
|
+
* Inspects low-level attributes for a running PID:
|
|
5
|
+
* open file handles (lsof), active sockets, thread count,
|
|
6
|
+
* nice priority, and memory regions.
|
|
7
|
+
*/
|
|
8
|
+
import os from 'node:os';
|
|
9
|
+
import { execSync } from 'node:child_process';
|
|
10
|
+
import chalk from 'chalk';
|
|
11
|
+
import { formatBytes } from './budgetCmd.js';
|
|
12
|
+
|
|
13
|
+
export interface ProcessDetails {
|
|
14
|
+
pid: number;
|
|
15
|
+
name: string;
|
|
16
|
+
user: string;
|
|
17
|
+
ppid: number;
|
|
18
|
+
threads: number;
|
|
19
|
+
nice: number;
|
|
20
|
+
cpuPct: number;
|
|
21
|
+
memRssBytes: number;
|
|
22
|
+
openFiles: string[];
|
|
23
|
+
sockets: string[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function inspectProcess(pid: number): ProcessDetails {
|
|
27
|
+
// Check if process exists
|
|
28
|
+
try {
|
|
29
|
+
process.kill(pid, 0);
|
|
30
|
+
} catch (err: any) {
|
|
31
|
+
if (err.code === 'ESRCH') {
|
|
32
|
+
throw new Error(`Process with PID ${pid} not found or permission denied.`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Query ps metadata
|
|
37
|
+
let name = 'unknown';
|
|
38
|
+
let user = 'unknown';
|
|
39
|
+
let ppid = 0;
|
|
40
|
+
let threads = 1;
|
|
41
|
+
let nice = 0;
|
|
42
|
+
let cpuPct = 0;
|
|
43
|
+
let memRssBytes = 0;
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
const psOut = execSync(`ps -p ${pid} -o user,ppid,nice,%cpu,rss,comm 2>/dev/null`, { encoding: 'utf-8' });
|
|
47
|
+
const lines = psOut.trim().split('\n');
|
|
48
|
+
if (lines.length > 1) {
|
|
49
|
+
const parts = lines[1].trim().split(/\s+/);
|
|
50
|
+
user = parts[0] || 'unknown';
|
|
51
|
+
ppid = parseInt(parts[1], 10) || 0;
|
|
52
|
+
nice = parseInt(parts[2], 10) || 0;
|
|
53
|
+
cpuPct = parseFloat(parts[3]) || 0;
|
|
54
|
+
memRssBytes = (parseInt(parts[4], 10) || 0) * 1024;
|
|
55
|
+
name = parts.slice(5).join(' ') || 'unknown';
|
|
56
|
+
}
|
|
57
|
+
} catch {
|
|
58
|
+
// Fallback if ps is restricted or unavailable
|
|
59
|
+
if (pid === process.pid) {
|
|
60
|
+
try {
|
|
61
|
+
user = os.userInfo().username || 'current';
|
|
62
|
+
} catch {
|
|
63
|
+
user = process.env.USER || 'current';
|
|
64
|
+
}
|
|
65
|
+
ppid = process.ppid || 0;
|
|
66
|
+
name = process.title || 'node';
|
|
67
|
+
memRssBytes = process.memoryUsage().rss;
|
|
68
|
+
threads = 1;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Query lsof for open files and network sockets
|
|
73
|
+
const openFiles: string[] = [];
|
|
74
|
+
const sockets: string[] = [];
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
const lsofOut = execSync(`lsof -p ${pid} -F tn 2>/dev/null`, { encoding: 'utf-8' });
|
|
78
|
+
const lines = lsofOut.split('\n');
|
|
79
|
+
let curType = '';
|
|
80
|
+
|
|
81
|
+
for (const l of lines) {
|
|
82
|
+
if (l.startsWith('t')) {
|
|
83
|
+
curType = l.slice(1).trim();
|
|
84
|
+
} else if (l.startsWith('n')) {
|
|
85
|
+
const nameVal = l.slice(1).trim();
|
|
86
|
+
if (curType === 'IPv4' || curType === 'IPv6' || nameVal.includes(':') || nameVal.includes('->')) {
|
|
87
|
+
sockets.push(nameVal);
|
|
88
|
+
} else if (nameVal.startsWith('/') && !openFiles.includes(nameVal)) {
|
|
89
|
+
openFiles.push(nameVal);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
} catch {
|
|
94
|
+
// lsof may not have permissions for system PIDs without sudo
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
pid,
|
|
99
|
+
name,
|
|
100
|
+
user,
|
|
101
|
+
ppid,
|
|
102
|
+
threads,
|
|
103
|
+
nice,
|
|
104
|
+
cpuPct,
|
|
105
|
+
memRssBytes,
|
|
106
|
+
openFiles: openFiles.slice(0, 15),
|
|
107
|
+
sockets: sockets.slice(0, 15),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export async function runInspectCommand(pid: number, opts?: { nice?: number; json?: boolean }): Promise<void> {
|
|
112
|
+
if (isNaN(pid) || pid <= 0) {
|
|
113
|
+
console.error(chalk.red(' ✖ Invalid PID. Usage: pyre inspect <pid>'));
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// If renicing requested
|
|
118
|
+
if (opts?.nice !== undefined) {
|
|
119
|
+
try {
|
|
120
|
+
execSync(`renice ${opts.nice} -p ${pid}`, { stdio: 'inherit' });
|
|
121
|
+
console.log(chalk.green(` ✔ Successfully set nice priority for PID ${pid} to ${opts.nice}\n`));
|
|
122
|
+
} catch (err: any) {
|
|
123
|
+
console.error(chalk.red(` ✖ Failed to renice PID ${pid}: ${err.message}`));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
const details = inspectProcess(pid);
|
|
129
|
+
|
|
130
|
+
if (opts?.json) {
|
|
131
|
+
console.log(JSON.stringify(details, null, 2));
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
console.log(chalk.bold(`\n 🔥 pyre inspect — Process Deep Dive (PID ${details.pid})\n`));
|
|
136
|
+
console.log(` Process: ${chalk.bold.hex('#ff6a39')(details.name)}`);
|
|
137
|
+
console.log(` User: ${details.user}`);
|
|
138
|
+
console.log(` PPID: ${details.ppid}`);
|
|
139
|
+
console.log(` CPU Usage: ${details.cpuPct.toFixed(1)}%`);
|
|
140
|
+
console.log(` Memory RSS: ${formatBytes(details.memRssBytes)}`);
|
|
141
|
+
console.log(` Nice Value: ${details.nice} (Priority: ${details.nice < 0 ? 'High' : details.nice > 0 ? 'Low' : 'Normal'})`);
|
|
142
|
+
console.log(chalk.dim(' ─────────────────────────────────────────────────────────────'));
|
|
143
|
+
|
|
144
|
+
// Open sockets
|
|
145
|
+
console.log(chalk.bold.cyan('\n Active Network Sockets:'));
|
|
146
|
+
if (details.sockets.length === 0) {
|
|
147
|
+
console.log(chalk.dim(' No open sockets detected (or permission denied).'));
|
|
148
|
+
} else {
|
|
149
|
+
details.sockets.forEach((s) => console.log(` • ${s}`));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Open file descriptors
|
|
153
|
+
console.log(chalk.bold.yellow('\n Open File Descriptors:'));
|
|
154
|
+
if (details.openFiles.length === 0) {
|
|
155
|
+
console.log(chalk.dim(' No open file handles detected.'));
|
|
156
|
+
} else {
|
|
157
|
+
details.openFiles.forEach((f) => console.log(` • ${chalk.dim(f)}`));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
console.log(chalk.dim('\n ─────────────────────────────────────────────────────────────'));
|
|
161
|
+
console.log(chalk.dim(` Adjust priority: pyre inspect ${pid} --nice <value -20 to 19>\n`));
|
|
162
|
+
} catch (err: any) {
|
|
163
|
+
console.error(chalk.red(`\n ✖ ${err.message}\n`));
|
|
164
|
+
}
|
|
165
|
+
}
|
package/src/live/input.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* Imported by `index.ts` as the public API of the live module.
|
|
8
8
|
*/
|
|
9
9
|
import readline from 'node:readline';
|
|
10
|
+
import { execSync } from 'node:child_process';
|
|
10
11
|
import chalk from 'chalk';
|
|
11
12
|
import { collectAll, StatsData } from '../monitors/index.js';
|
|
12
13
|
import { THEMES, type ThemeName, type VisibleItems, getTabHitboxes, TAB_BAR_ROW } from '../formatters/index.js';
|
|
@@ -218,6 +219,9 @@ function isEscKey(key: readline.Key, str?: string): boolean {
|
|
|
218
219
|
case 'Resume Dashboard':
|
|
219
220
|
state.inputMode = null;
|
|
220
221
|
break;
|
|
222
|
+
case 'Command Center (All Tools)':
|
|
223
|
+
openCommandCenter();
|
|
224
|
+
return;
|
|
221
225
|
case 'Settings (Customizer)':
|
|
222
226
|
state.inputMode = 'customizer';
|
|
223
227
|
state.customizerIndex = 0;
|
|
@@ -437,20 +441,31 @@ function killProcess(pidStr: string, signal: string = 'SIGTERM') {
|
|
|
437
441
|
return;
|
|
438
442
|
}
|
|
439
443
|
|
|
440
|
-
const isProtected = pid === 1 || pid === process.pid || pid === process.ppid;
|
|
444
|
+
const isProtected = pid === 1 || pid === 0 || pid === 4 || pid === process.pid || pid === process.ppid;
|
|
441
445
|
if (isProtected && confirmKillPid !== pid) {
|
|
442
446
|
confirmKillPid = pid;
|
|
443
|
-
const name = pid ===
|
|
447
|
+
const name = (pid === 0 || pid === 4) ? 'Windows System Kernel' : (pid === 1 ? 'init/launchd' : (pid === process.pid ? 'pyre itself' : 'parent process'));
|
|
444
448
|
setStatus(`⚠️ PROTECTED PID ${pid} (${name})! Re-enter PID ${pid} to confirm termination.`, 8000);
|
|
445
449
|
return;
|
|
446
450
|
}
|
|
447
451
|
|
|
448
452
|
confirmKillPid = null;
|
|
453
|
+
|
|
454
|
+
if (process.platform === 'win32') {
|
|
455
|
+
try {
|
|
456
|
+
execSync(`taskkill /F /PID ${pid} /T`, { stdio: 'pipe' });
|
|
457
|
+
setStatus(`Terminated PID ${pid} (taskkill /F /T)`);
|
|
458
|
+
return;
|
|
459
|
+
} catch (err: any) {
|
|
460
|
+
// fallback to process.kill
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
449
464
|
try {
|
|
450
465
|
process.kill(pid, signal as NodeJS.Signals);
|
|
451
466
|
setStatus(`Sent ${signal} to PID ${pid}`);
|
|
452
467
|
} catch (err: any) {
|
|
453
|
-
setStatus(`Failed to
|
|
468
|
+
setStatus(`Failed to terminate PID ${pid}: ${err.message}`);
|
|
454
469
|
}
|
|
455
470
|
}
|
|
456
471
|
|
|
@@ -576,6 +591,51 @@ function killProcess(pidStr: string, signal: string = 'SIGTERM') {
|
|
|
576
591
|
}
|
|
577
592
|
}
|
|
578
593
|
|
|
594
|
+
async function openCommandCenter() {
|
|
595
|
+
state.paused = true;
|
|
596
|
+
if (state.intervalHandle) {
|
|
597
|
+
clearInterval(state.intervalHandle);
|
|
598
|
+
state.intervalHandle = null;
|
|
599
|
+
}
|
|
600
|
+
if (state.uiIntervalHandle) {
|
|
601
|
+
clearInterval(state.uiIntervalHandle);
|
|
602
|
+
state.uiIntervalHandle = null;
|
|
603
|
+
}
|
|
604
|
+
if (state.keypressHandler) {
|
|
605
|
+
process.stdin.removeListener('keypress', state.keypressHandler);
|
|
606
|
+
}
|
|
607
|
+
process.stdout.removeListener('resize', onResize);
|
|
608
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
609
|
+
process.stdin.pause();
|
|
610
|
+
process.stdout.write('\x1b[?1000l\x1b[?25h\x1b[?1049l\x1b[2J\x1b[H');
|
|
611
|
+
|
|
612
|
+
const { runMenu } = await import('../menu/index.js');
|
|
613
|
+
await runMenu({
|
|
614
|
+
theme: state.currentTheme,
|
|
615
|
+
onExit: () => {},
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
process.stdout.write('\x1b[?1049h\x1b[?25l\x1b[2J\x1b[H');
|
|
619
|
+
if (state.mouseEnabled) {
|
|
620
|
+
process.stdout.write('\x1b[?1000h');
|
|
621
|
+
}
|
|
622
|
+
readline.emitKeypressEvents(process.stdin);
|
|
623
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(true);
|
|
624
|
+
process.stdin.resume();
|
|
625
|
+
if (state.keypressHandler) {
|
|
626
|
+
process.stdin.on('keypress', state.keypressHandler);
|
|
627
|
+
}
|
|
628
|
+
process.stdout.on('resize', onResize);
|
|
629
|
+
state.paused = false;
|
|
630
|
+
state.inputMode = null;
|
|
631
|
+
restartTicker();
|
|
632
|
+
if (state.uiIntervalHandle) clearInterval(state.uiIntervalHandle);
|
|
633
|
+
state.uiIntervalHandle = setInterval(() => {
|
|
634
|
+
if (state.running && !state.paused) render();
|
|
635
|
+
}, 250);
|
|
636
|
+
render();
|
|
637
|
+
}
|
|
638
|
+
|
|
579
639
|
/**
|
|
580
640
|
* Start the interactive live dashboard.
|
|
581
641
|
*
|
|
@@ -766,6 +826,10 @@ function killProcess(pidStr: string, signal: string = 'SIGTERM') {
|
|
|
766
826
|
case 'right':
|
|
767
827
|
cycleTab(1);
|
|
768
828
|
break;
|
|
829
|
+
case 'o':
|
|
830
|
+
case 'O':
|
|
831
|
+
openCommandCenter();
|
|
832
|
+
return;
|
|
769
833
|
case 'c':
|
|
770
834
|
state.inputMode = 'customizer';
|
|
771
835
|
state.customizerIndex = 0;
|
|
@@ -1047,6 +1111,9 @@ function killProcess(pidStr: string, signal: string = 'SIGTERM') {
|
|
|
1047
1111
|
|
|
1048
1112
|
// Execute the command
|
|
1049
1113
|
switch (cmd.id) {
|
|
1114
|
+
case 'command-center':
|
|
1115
|
+
openCommandCenter();
|
|
1116
|
+
break;
|
|
1050
1117
|
case 'cpu': case 'mem': case 'gpu': case 'power': case 'battery':
|
|
1051
1118
|
case 'thermal': case 'network': case 'packets': case 'tasks': case 'disk':
|
|
1052
1119
|
case 'process': case 'containers': case 'p2p': case 'anomalies': case 'blender':
|
package/src/live/render.ts
CHANGED
|
@@ -502,6 +502,7 @@ function render() {
|
|
|
502
502
|
function footerLine(): string[] {
|
|
503
503
|
const row1: [string, string][] = [
|
|
504
504
|
['q', 'quit'],
|
|
505
|
+
['o', 'tools menu'],
|
|
505
506
|
['c', 'customize UI'],
|
|
506
507
|
['←/→', 'tab'],
|
|
507
508
|
['1-9', 'panels'],
|
|
@@ -509,7 +510,6 @@ function footerLine(): string[] {
|
|
|
509
510
|
['b', `graph:${state.graphMode}`],
|
|
510
511
|
['t', state.treeView ? 'flat' : 'tree'],
|
|
511
512
|
['d', state.detailed ? 'basic' : 'detailed'],
|
|
512
|
-
['T', `temp:${state.tempUnit}`],
|
|
513
513
|
['p', state.paused ? 'resume' : 'pause'],
|
|
514
514
|
];
|
|
515
515
|
const row2: [string, string][] = [
|
package/src/live/state.ts
CHANGED
|
@@ -19,9 +19,10 @@ import { type SplashColorScheme, type SplashAnimation } from '../splash.js';
|
|
|
19
19
|
|
|
20
20
|
export { type SplashColorScheme, type SplashAnimation };
|
|
21
21
|
|
|
22
|
-
export const MENU_OPTIONS = ['Resume Dashboard', 'Settings (Customizer)', 'Readme', 'Credits', 'Quit pyre'] as const;
|
|
22
|
+
export const MENU_OPTIONS = ['Resume Dashboard', 'Command Center (All Tools)', 'Settings (Customizer)', 'Readme', 'Credits', 'Quit pyre'] as const;
|
|
23
23
|
|
|
24
24
|
const PALETTE_COMMANDS = [
|
|
25
|
+
{ id: 'command-center', label: 'Command Center (Browse all tools)', keys: 'o', category: 'Navigation' },
|
|
25
26
|
{ id: 'cpu', label: 'Switch to CPU panel', keys: '1', category: 'Navigation' },
|
|
26
27
|
{ id: 'mem', label: 'Switch to Memory panel', keys: '2', category: 'Navigation' },
|
|
27
28
|
{ id: 'gpu', label: 'Switch to GPU panel', keys: '3', category: 'Navigation' },
|