pyre-cli 7.0.0 → 8.0.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.
@@ -0,0 +1,121 @@
1
+ // src/state/config.ts
2
+ import fs from "fs";
3
+ import os from "os";
4
+ import path from "path";
5
+ var CONFIG_DIR = path.join(os.homedir(), ".config", "pyre");
6
+ var CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
7
+ var DEFAULT_CONFIG = {
8
+ theme: "default",
9
+ interval: 2,
10
+ exportDir: "./pyre-exports",
11
+ detailed: false,
12
+ sortMode: "cpu",
13
+ treeView: false,
14
+ graphMode: "spark",
15
+ showGraphs: true,
16
+ autoLog: false,
17
+ mouseEnabled: true,
18
+ cpuAlertPct: 90,
19
+ tempAlertC: 95,
20
+ electricityRateKwH: 0.15,
21
+ watchdogProcess: "",
22
+ watchdogCpu: 80,
23
+ watchdogMem: 80,
24
+ notificationsEnabled: true,
25
+ p2pPort: 9876,
26
+ p2pPassword: "pyre",
27
+ splashEnabled: true,
28
+ splashColorScheme: "fire",
29
+ splashAnimation: "classic",
30
+ webhookUrl: "",
31
+ alertCmd: "",
32
+ visiblePanels: {
33
+ cpu: true,
34
+ mem: true,
35
+ gpu: true,
36
+ power: true,
37
+ battery: true,
38
+ thermal: true,
39
+ network: true,
40
+ packets: true,
41
+ tasks: true,
42
+ disk: true,
43
+ process: true,
44
+ tree: false
45
+ },
46
+ panelLayout: ["mem", "disk", "net"]
47
+ };
48
+ function readConfig() {
49
+ try {
50
+ if (!fs.existsSync(CONFIG_FILE)) return { ...DEFAULT_CONFIG };
51
+ const raw = fs.readFileSync(CONFIG_FILE, "utf-8");
52
+ const parsed = JSON.parse(raw);
53
+ return deepMerge(DEFAULT_CONFIG, parsed);
54
+ } catch {
55
+ return { ...DEFAULT_CONFIG };
56
+ }
57
+ }
58
+ function writeConfig(config) {
59
+ try {
60
+ if (!fs.existsSync(CONFIG_DIR)) {
61
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
62
+ }
63
+ const merged = deepMerge(readConfig(), config);
64
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2) + "\n");
65
+ } catch {
66
+ }
67
+ }
68
+ var PROFILES_DIR = path.join(CONFIG_DIR, "profiles");
69
+ function saveProfile(name) {
70
+ if (!name || name.includes("/") || name.includes("\\")) {
71
+ throw new Error("Invalid profile name.");
72
+ }
73
+ if (!fs.existsSync(PROFILES_DIR)) {
74
+ fs.mkdirSync(PROFILES_DIR, { recursive: true });
75
+ }
76
+ const currentConfig = readConfig();
77
+ const profileFile = path.join(PROFILES_DIR, `${name}.json`);
78
+ fs.writeFileSync(profileFile, JSON.stringify(currentConfig, null, 2) + "\n");
79
+ }
80
+ function loadProfile(name) {
81
+ const profileFile = path.join(PROFILES_DIR, `${name}.json`);
82
+ if (!fs.existsSync(profileFile)) {
83
+ throw new Error(`Profile '${name}' does not exist.`);
84
+ }
85
+ const raw = fs.readFileSync(profileFile, "utf-8");
86
+ const parsed = JSON.parse(raw);
87
+ writeConfig(parsed);
88
+ }
89
+ function listProfiles() {
90
+ if (!fs.existsSync(PROFILES_DIR)) return [];
91
+ const files = fs.readdirSync(PROFILES_DIR);
92
+ return files.filter((f) => f.endsWith(".json")).map((f) => f.replace(/\.json$/, ""));
93
+ }
94
+ function deepMerge(target, source) {
95
+ const result = { ...target };
96
+ for (const key of Object.keys(source)) {
97
+ if (source[key] !== void 0 && source[key] !== null) {
98
+ if (typeof source[key] === "object" && !Array.isArray(source[key]) && typeof target[key] === "object" && !Array.isArray(target[key])) {
99
+ result[key] = deepMerge(target[key] || {}, source[key]);
100
+ } else {
101
+ result[key] = source[key];
102
+ }
103
+ }
104
+ }
105
+ return result;
106
+ }
107
+ function getConfigPath() {
108
+ return CONFIG_FILE;
109
+ }
110
+
111
+ export {
112
+ CONFIG_FILE,
113
+ DEFAULT_CONFIG,
114
+ readConfig,
115
+ writeConfig,
116
+ PROFILES_DIR,
117
+ saveProfile,
118
+ loadProfile,
119
+ listProfiles,
120
+ getConfigPath
121
+ };
@@ -0,0 +1,33 @@
1
+ import {
2
+ collectAll,
3
+ collectBattery,
4
+ collectCpu,
5
+ collectDisk,
6
+ collectGpu,
7
+ collectMemory,
8
+ collectNetwork,
9
+ collectPackets,
10
+ collectPower,
11
+ collectProcesses,
12
+ collectTasks,
13
+ collectThermal,
14
+ getDisplayInfo,
15
+ getTimeMachineStatus
16
+ } from "./chunk-KBKXU2ZV.js";
17
+ import "./chunk-GLH7EKAX.js";
18
+ export {
19
+ collectAll,
20
+ collectBattery,
21
+ collectCpu,
22
+ collectDisk,
23
+ collectGpu,
24
+ collectMemory,
25
+ collectNetwork,
26
+ collectPackets,
27
+ collectPower,
28
+ collectProcesses,
29
+ collectTasks,
30
+ collectThermal,
31
+ getDisplayInfo,
32
+ getTimeMachineStatus
33
+ };
@@ -0,0 +1,122 @@
1
+ // src/completions.ts
2
+ function generateZshCompletions() {
3
+ return `#compdef pyre
4
+
5
+ _pyre() {
6
+ local -a commands
7
+ commands=(
8
+ 'live:Start interactive live monitoring dashboard'
9
+ 'info:Display static system overview'
10
+ 'doctor:Run system permissions and diagnostic checks'
11
+ 'web:Launch web dashboard server'
12
+ 'config:Show or reset configuration options'
13
+ 'p2p:P2P streaming server or client'
14
+ 'ssh:Stream metrics over SSH'
15
+ 'bench:Benchmark command while logging system stats'
16
+ 'completions:Generate shell completion scripts'
17
+ )
18
+
19
+ _arguments -s \\
20
+ '(-j --json)'{-j,--json}'[Output snapshot as JSON]' \\
21
+ '--html[Output snapshot as HTML]' \\
22
+ '--md[Output snapshot as Markdown]' \\
23
+ '(-c --csv)'{-c,--csv}'[Output snapshot as CSV]' \\
24
+ '(-t --tsv)'{-t,--tsv}'[Output snapshot as TSV]' \\
25
+ '--detailed[Include detailed system info and sensor readings]' \\
26
+ '--theme[Default theme]:theme:(default dracula cyberpunk monochrome nord gruvbox)' \\
27
+ '--interval[Refresh interval in seconds]:seconds:' \\
28
+ '--once[Show a single static snapshot]' \\
29
+ '--out[Write snapshot output to file]:file:_files' \\
30
+ '--export-dir[Directory for snapshot exports and logs]:directory:_files -/' \\
31
+ '--log[Start continuous CSV logging]' \\
32
+ '--tree[Show process tree view]' \\
33
+ '--sort[Sort processes by]:key:(cpu mem pid user command state threads runtime)' \\
34
+ '--packets[Include packet monitor panel]' \\
35
+ '--limit[Max processes to include]:count:' \\
36
+ '--alert-cpu[CPU alert threshold pct]:pct:' \\
37
+ '--alert-temp[CPU alert threshold temp C]:temp:' \\
38
+ '--temp-unit[Temperature display unit]:unit:(c f)' \\
39
+ '*::command:->command'
40
+
41
+ case $state in
42
+ command)
43
+ _describe -t commands 'pyre command' commands
44
+ ;;
45
+ esac
46
+ }
47
+
48
+ _pyre "$@"
49
+ `;
50
+ }
51
+ function generateBashCompletions() {
52
+ return `# bash completion for pyre
53
+
54
+ _pyre_completions() {
55
+ local cur prev opts commands
56
+ COMPREPLY=()
57
+ cur="\${COMP_WORDS[COMP_CWORD]}"
58
+ prev="\${COMP_WORDS[COMP_CWORD-1]}"
59
+ commands="live info doctor web config p2p ssh bench completions"
60
+ opts="--json --html --md --csv --tsv --detailed --theme --interval --once --out --export-dir --log --tree --sort --packets --limit --alert-cpu --alert-temp --temp-unit"
61
+
62
+ case "\${prev}" in
63
+ --theme)
64
+ COMPREPLY=( $(compgen -W "default dracula cyberpunk monochrome nord gruvbox" -- \${cur}) )
65
+ return 0
66
+ ;;
67
+ --sort)
68
+ COMPREPLY=( $(compgen -W "cpu mem pid user command state threads runtime" -- \${cur}) )
69
+ return 0
70
+ ;;
71
+ --temp-unit)
72
+ COMPREPLY=( $(compgen -W "c f" -- \${cur}) )
73
+ return 0
74
+ ;;
75
+ config)
76
+ COMPREPLY=( $(compgen -W "show reset" -- \${cur}) )
77
+ return 0
78
+ ;;
79
+ p2p)
80
+ COMPREPLY=( $(compgen -W "server connect" -- \${cur}) )
81
+ return 0
82
+ ;;
83
+ esac
84
+
85
+ if [[ \${cur} == -* ]] ; then
86
+ COMPREPLY=( $(compgen -W "\${opts}" -- \${cur}) )
87
+ return 0
88
+ fi
89
+
90
+ COMPREPLY=( $(compgen -W "\${commands}" -- \${cur}) )
91
+ }
92
+
93
+ complete -F _pyre_completions pyre
94
+ `;
95
+ }
96
+ function generateFishCompletions() {
97
+ return `# fish completion for pyre
98
+
99
+ complete -c pyre -n "__fish_use_subcommand" -a live -d "Start interactive live monitoring dashboard"
100
+ complete -c pyre -n "__fish_use_subcommand" -a info -d "Display static system overview"
101
+ complete -c pyre -n "__fish_use_subcommand" -a doctor -d "Run system diagnostics"
102
+ complete -c pyre -n "__fish_use_subcommand" -a web -d "Launch web dashboard server"
103
+ complete -c pyre -n "__fish_use_subcommand" -a config -d "Show or reset configuration options"
104
+ complete -c pyre -n "__fish_use_subcommand" -a p2p -d "P2P streaming mode"
105
+ complete -c pyre -n "__fish_use_subcommand" -a completions -d "Generate shell completions"
106
+
107
+ complete -c pyre -l json -s j -d "Output snapshot as JSON"
108
+ complete -c pyre -l html -d "Output snapshot as HTML"
109
+ complete -c pyre -l md -d "Output snapshot as Markdown"
110
+ complete -c pyre -l csv -s c -d "Output snapshot as CSV"
111
+ complete -c pyre -l tsv -s t -d "Output snapshot as TSV"
112
+ complete -c pyre -l detailed -d "Include detailed system info and sensor readings"
113
+ complete -c pyre -l theme -x -a "default dracula cyberpunk monochrome nord gruvbox" -d "Set color theme"
114
+ complete -c pyre -l sort -x -a "cpu mem pid user command state threads runtime" -d "Sort processes by key"
115
+ complete -c pyre -l temp-unit -x -a "c f" -d "Temperature unit"
116
+ `;
117
+ }
118
+ export {
119
+ generateBashCompletions,
120
+ generateFishCompletions,
121
+ generateZshCompletions
122
+ };
@@ -0,0 +1,22 @@
1
+ import {
2
+ CONFIG_FILE,
3
+ DEFAULT_CONFIG,
4
+ PROFILES_DIR,
5
+ getConfigPath,
6
+ listProfiles,
7
+ loadProfile,
8
+ readConfig,
9
+ saveProfile,
10
+ writeConfig
11
+ } from "./chunk-RVGKEJXT.js";
12
+ export {
13
+ CONFIG_FILE,
14
+ DEFAULT_CONFIG,
15
+ PROFILES_DIR,
16
+ getConfigPath,
17
+ listProfiles,
18
+ loadProfile,
19
+ readConfig,
20
+ saveProfile,
21
+ writeConfig
22
+ };
@@ -0,0 +1,184 @@
1
+ import {
2
+ CONFIG_FILE,
3
+ readConfig
4
+ } from "./chunk-RVGKEJXT.js";
5
+ import {
6
+ run
7
+ } from "./chunk-GLH7EKAX.js";
8
+
9
+ // src/doctor.ts
10
+ import chalk from "chalk";
11
+ async function runDoctor() {
12
+ const checks = [];
13
+ try {
14
+ const pmResult = await run("sudo -n powermetrics --samplers cpu_power -n 1 2>&1", "");
15
+ if (pmResult.includes("Password:") || pmResult.includes("Permission denied") || pmResult.includes("sudo: a password is required")) {
16
+ checks.push({
17
+ name: "Sudo / Powermetrics",
18
+ status: "warn",
19
+ message: "Passwordless sudo for powermetrics is not configured.",
20
+ details: "Powermetrics allows high-precision GPU & CPU power sampling. Run with sudo or configure passwordless sudo in /etc/sudoers."
21
+ });
22
+ } else {
23
+ checks.push({
24
+ name: "Sudo / Powermetrics",
25
+ status: "ok",
26
+ message: "Powermetrics access available."
27
+ });
28
+ }
29
+ } catch {
30
+ checks.push({
31
+ name: "Sudo / Powermetrics",
32
+ status: "warn",
33
+ message: "Powermetrics check failed."
34
+ });
35
+ }
36
+ try {
37
+ const psCheck = await run("ps -ax -o pid,user,command", "");
38
+ if (psCheck.length > 0) {
39
+ checks.push({
40
+ name: "TCC / Process Visibility",
41
+ status: "ok",
42
+ message: "Process listing access available."
43
+ });
44
+ } else {
45
+ checks.push({
46
+ name: "TCC / Process Visibility",
47
+ status: "warn",
48
+ message: "Restricted process visibility.",
49
+ details: "Grant Full Disk Access or Terminal permissions in System Settings -> Privacy & Security."
50
+ });
51
+ }
52
+ } catch {
53
+ checks.push({
54
+ name: "TCC / Process Visibility",
55
+ status: "error",
56
+ message: "Unable to list system processes."
57
+ });
58
+ }
59
+ try {
60
+ const netCheck = await run("netstat -rn", "");
61
+ if (netCheck.includes("default") || netCheck.includes("gateway")) {
62
+ checks.push({
63
+ name: "Network Reachability",
64
+ status: "ok",
65
+ message: "Network interface active with default gateway."
66
+ });
67
+ } else {
68
+ checks.push({
69
+ name: "Network Reachability",
70
+ status: "warn",
71
+ message: "No active network route detected for P2P mode."
72
+ });
73
+ }
74
+ } catch {
75
+ checks.push({
76
+ name: "Network Reachability",
77
+ status: "warn",
78
+ message: "Could not verify network routes."
79
+ });
80
+ }
81
+ try {
82
+ const spctlOut = await run("spctl --status 2>&1", "");
83
+ if (spctlOut.includes("assessments enabled")) {
84
+ checks.push({
85
+ name: "Gatekeeper",
86
+ status: "ok",
87
+ message: "Gatekeeper enforcement is enabled."
88
+ });
89
+ } else {
90
+ checks.push({
91
+ name: "Gatekeeper",
92
+ status: "warn",
93
+ message: "Gatekeeper is disabled or status check unconfirmed.",
94
+ details: 'Re-enable Gatekeeper using "sudo spctl --master-enable" to protect against untrusted software.'
95
+ });
96
+ }
97
+ } catch {
98
+ checks.push({
99
+ name: "Gatekeeper",
100
+ status: "warn",
101
+ message: "Could not query Gatekeeper status."
102
+ });
103
+ }
104
+ try {
105
+ const sipOut = await run("csrutil status 2>&1", "");
106
+ if (sipOut.includes("enabled")) {
107
+ checks.push({
108
+ name: "System Integrity Protection",
109
+ status: "ok",
110
+ message: "SIP is enabled."
111
+ });
112
+ } else {
113
+ checks.push({
114
+ name: "System Integrity Protection",
115
+ status: "warn",
116
+ message: "SIP is disabled or restricted.",
117
+ details: "System Integrity Protection protects core system files. Enable it from macOS Recovery."
118
+ });
119
+ }
120
+ } catch {
121
+ checks.push({
122
+ name: "System Integrity Protection",
123
+ status: "warn",
124
+ message: "Could not query SIP status."
125
+ });
126
+ }
127
+ try {
128
+ const xprotectOut = await run("system_profiler SPInstallHistoryDataType 2>&1", "");
129
+ const xpMatches = Array.from(xprotectOut.matchAll(/XProtect(?:Remediator)?ConfigData.*?\n.*?Install Date:\s*(.+)/gi));
130
+ if (xpMatches.length > 0) {
131
+ const latestDateStr = xpMatches[xpMatches.length - 1][1];
132
+ checks.push({
133
+ name: "XProtect Definitions",
134
+ status: "ok",
135
+ message: `XProtect active (last updated ${latestDateStr.trim()}).`
136
+ });
137
+ } else {
138
+ checks.push({
139
+ name: "XProtect Definitions",
140
+ status: "ok",
141
+ message: "XProtect definitions installed."
142
+ });
143
+ }
144
+ } catch {
145
+ checks.push({
146
+ name: "XProtect Definitions",
147
+ status: "warn",
148
+ message: "Could not determine XProtect update history."
149
+ });
150
+ }
151
+ try {
152
+ const cfg = readConfig();
153
+ checks.push({
154
+ name: "Configuration File",
155
+ status: "ok",
156
+ message: `Config file valid (${CONFIG_FILE}).`
157
+ });
158
+ } catch (err) {
159
+ checks.push({
160
+ name: "Configuration File",
161
+ status: "error",
162
+ message: "Failed to parse config file.",
163
+ details: err.message
164
+ });
165
+ }
166
+ return checks;
167
+ }
168
+ function printDoctorReport(checks) {
169
+ console.log(chalk.bold("\n pyre doctor \u2014 System Diagnostics\n"));
170
+ for (const check of checks) {
171
+ let icon = chalk.green("\u2714");
172
+ if (check.status === "warn") icon = chalk.yellow("\u26A0");
173
+ if (check.status === "error") icon = chalk.red("\u2716");
174
+ console.log(` ${icon} ${chalk.bold(check.name.padEnd(26))} ${check.message}`);
175
+ if (check.details) {
176
+ console.log(chalk.dim(` \u21B3 ${check.details}`));
177
+ }
178
+ }
179
+ console.log();
180
+ }
181
+ export {
182
+ printDoctorReport,
183
+ runDoctor
184
+ };
@@ -0,0 +1,74 @@
1
+ import {
2
+ run
3
+ } from "./chunk-GLH7EKAX.js";
4
+
5
+ // src/extensions.ts
6
+ import chalk from "chalk";
7
+ async function getSystemExtensions() {
8
+ try {
9
+ const raw = await run("systemextensionsctl list 2>&1", "");
10
+ return parseSystemExtensionsOutput(raw);
11
+ } catch (err) {
12
+ return [];
13
+ }
14
+ }
15
+ function parseSystemExtensionsOutput(output) {
16
+ const result = [];
17
+ let currentCategory = "Other Extensions";
18
+ let currentList = [];
19
+ const lines = output.split("\n");
20
+ for (const line of lines) {
21
+ const trimmed = line.trim();
22
+ if (!trimmed) continue;
23
+ if (trimmed.startsWith("--- category") || trimmed.startsWith("--- ")) {
24
+ if (currentList.length > 0 || result.length > 0) {
25
+ result.push({ category: currentCategory, extensions: currentList });
26
+ currentList = [];
27
+ }
28
+ const catMatch = trimmed.match(/category\s+([\w\.-]+)/i);
29
+ currentCategory = catMatch ? catMatch[1] : trimmed.replace(/^-+\s*/, "").replace(/\s*-+$/, "");
30
+ continue;
31
+ }
32
+ const extMatch = trimmed.match(/^(?:\*\s*)*([A-Z0-9]{10}|\?\?\?\?\?\?\?\?\?\?)\s+([\w\.-]+)\s+\(([^)]+)\)\s+\[([^\]]+)\]/i);
33
+ if (extMatch) {
34
+ currentList.push({
35
+ teamId: extMatch[1],
36
+ bundleId: extMatch[2],
37
+ version: extMatch[3],
38
+ state: extMatch[4],
39
+ category: currentCategory
40
+ });
41
+ }
42
+ }
43
+ if (currentList.length > 0 || result.length === 0) {
44
+ result.push({ category: currentCategory, extensions: currentList });
45
+ }
46
+ return result.filter((group) => group.extensions.length > 0 || result.length === 1);
47
+ }
48
+ async function printExtensionsReport() {
49
+ console.log(chalk.bold("\n pyre extensions \u2014 System Extensions Inspector\n"));
50
+ const categories = await getSystemExtensions();
51
+ let totalCount = 0;
52
+ for (const cat of categories) {
53
+ totalCount += cat.extensions.length;
54
+ console.log(chalk.cyan.bold(` \u{1F4C2} ${cat.category}`));
55
+ if (cat.extensions.length === 0) {
56
+ console.log(chalk.dim(" (No extensions registered in this category)"));
57
+ continue;
58
+ }
59
+ for (const ext of cat.extensions) {
60
+ const stateColor = ext.state.includes("activated") || ext.state.includes("enabled") ? chalk.green : chalk.yellow;
61
+ console.log(` \u2022 ${chalk.bold(ext.bundleId)} (${ext.version})`);
62
+ console.log(chalk.dim(` Team: ${ext.teamId} | State: `) + stateColor(ext.state));
63
+ }
64
+ console.log();
65
+ }
66
+ if (totalCount === 0) {
67
+ console.log(chalk.dim(" No active system extensions detected on this system.\n"));
68
+ }
69
+ }
70
+ export {
71
+ getSystemExtensions,
72
+ parseSystemExtensionsOutput,
73
+ printExtensionsReport
74
+ };