pyre-cli 9.5.2 → 10.1.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 (86) hide show
  1. package/README.md +73 -16
  2. package/dist/alertChannels-TPSQIJ7S.js +8 -0
  3. package/dist/{blender-7UT5J7IP.js → blender-FGH3ASEQ.js} +2 -2
  4. package/dist/brewHealth-P2GW5Y5C.js +176 -0
  5. package/dist/buildCmd-CDPZ2ZFA.js +38 -0
  6. package/dist/{checkCmd-T7O4KXXA.js → checkCmd-RKW6QGDR.js} +9 -11
  7. package/dist/{chunk-CXL4GYGQ.js → chunk-53O2RGSL.js} +44 -17
  8. package/dist/{chunk-3TE7MJLS.js → chunk-6UHZP4QP.js} +36 -10
  9. package/dist/chunk-GHRDW646.js +2506 -0
  10. package/dist/{prometheus-LFYHF6VO.js → chunk-HT4VQ4OH.js} +1 -0
  11. package/dist/chunk-M73HCQD2.js +95 -0
  12. package/dist/chunk-MZO5QKUR.js +32 -0
  13. package/dist/{chunk-25I533OF.js → chunk-VIACN2RU.js} +1 -1
  14. package/dist/chunk-Z2YV5HKV.js +87 -0
  15. package/dist/chunk-ZLVIVFSO.js +110 -0
  16. package/dist/{collectors-JB4OGUEF.js → collectors-H43PN4PS.js} +6 -3
  17. package/dist/{completions-VQMHQCXE.js → completions-JKYEJNGR.js} +34 -1
  18. package/dist/{config-KS5ZZ3VN.js → config-GYGJXU2Q.js} +9 -1
  19. package/dist/doctor-GYHFTNZQ.js +387 -0
  20. package/dist/explainCmd-MF6PIMLW.js +222 -0
  21. package/dist/extensions-XJGHG7X3.js +134 -0
  22. package/dist/index.js +460 -88
  23. package/dist/ioreport.py +72 -0
  24. package/dist/netUsageCmd-WKBTW2YG.js +90 -0
  25. package/dist/{pipeCmd-TYLJHQ5B.js → pipeCmd-7REDBLBU.js} +4 -3
  26. package/dist/profileProcCmd-N4H6MCBT.js +107 -0
  27. package/dist/prometheus-5BL5PVLC.js +6 -0
  28. package/dist/prometheusCmd-PL64BLN5.js +93 -0
  29. package/dist/replayCmd-Z3PYXY7V.js +164 -0
  30. package/dist/run-HOGQDDTA.js +8 -0
  31. package/dist/serveCmd-2H5VLF3P.js +153 -0
  32. package/dist/service-Q74BBILO.js +181 -0
  33. package/dist/smartCmd-ODWKSFNG.js +166 -0
  34. package/dist/syncCmd-A2W3QB3S.js +126 -0
  35. package/dist/topoCmd-DU2HHZNH.js +148 -0
  36. package/dist/watchdogCmd-UF53JPZB.js +161 -0
  37. package/dist/{wizard-Z3RGA3VB.js → wizard-TD7BGX6O.js} +1 -1
  38. package/install.ps1 +29 -0
  39. package/install.sh +10 -9
  40. package/package.json +7 -3
  41. package/src/ai/models.ts +138 -0
  42. package/src/alertChannels.ts +119 -0
  43. package/src/brewHealth.ts +157 -40
  44. package/src/buildCmd.ts +48 -0
  45. package/src/checkCmd.ts +5 -5
  46. package/src/completions.ts +35 -0
  47. package/src/doctor.ts +235 -7
  48. package/src/easy_features.test.ts +40 -0
  49. package/src/explainCmd.ts +261 -0
  50. package/src/extensions.ts +71 -7
  51. package/src/formatters/render.ts +34 -10
  52. package/src/formatters/types.ts +2 -0
  53. package/src/index.ts +251 -36
  54. package/src/live/input.ts +60 -0
  55. package/src/live/render.ts +67 -0
  56. package/src/live/state.ts +5 -0
  57. package/src/live/types.ts +1 -1
  58. package/src/monitors/batteryPredictor.ts +143 -0
  59. package/src/monitors/buildTracker.ts +121 -0
  60. package/src/monitors/collectors.ts +342 -178
  61. package/src/monitors/ioreport.py +72 -0
  62. package/src/monitors/platform/linux.ts +756 -0
  63. package/src/monitors/platform/windows.ts +519 -0
  64. package/src/monitors/run.ts +27 -3
  65. package/src/monitors/smc.ts +21 -1
  66. package/src/monitors/types.ts +15 -0
  67. package/src/netUsageCmd.ts +121 -0
  68. package/src/platform.test.ts +99 -0
  69. package/src/plugins/index.ts +143 -0
  70. package/src/profileProcCmd.ts +133 -0
  71. package/src/prometheusCmd.ts +107 -0
  72. package/src/replayCmd.ts +186 -0
  73. package/src/serveCmd.ts +180 -0
  74. package/src/service.ts +125 -7
  75. package/src/smartCmd.ts +205 -0
  76. package/src/state/config.ts +58 -17
  77. package/src/syncCmd.ts +166 -0
  78. package/src/topoCmd.ts +176 -0
  79. package/src/watchdogCmd.ts +198 -0
  80. package/src/xbar.ts +22 -11
  81. package/dist/brewHealth-F2BXF5M6.js +0 -74
  82. package/dist/chunk-FH5WDJBF.js +0 -15
  83. package/dist/chunk-N4WJVF5M.js +0 -1184
  84. package/dist/doctor-UV7TWIHX.js +0 -184
  85. package/dist/extensions-DVA5YLGH.js +0 -74
  86. package/dist/service-WTS3NBQU.js +0 -73
@@ -0,0 +1,153 @@
1
+ import {
2
+ collectAll,
3
+ collectBattery,
4
+ collectContainers,
5
+ collectCpu,
6
+ collectDisk,
7
+ collectGpu,
8
+ collectMemory,
9
+ collectNetwork,
10
+ collectProcesses,
11
+ collectSystem,
12
+ collectThermal
13
+ } from "./chunk-GHRDW646.js";
14
+ import "./chunk-M73HCQD2.js";
15
+ import "./chunk-VIACN2RU.js";
16
+ import "./chunk-MZO5QKUR.js";
17
+
18
+ // src/serveCmd.ts
19
+ import http from "http";
20
+ import os from "os";
21
+ import chalk from "chalk";
22
+ async function runServeCommand(opts = {}) {
23
+ const port = opts.port || 8080;
24
+ const apiKey = opts.apiKey || process.env.PYRE_API_KEY || "";
25
+ const server = http.createServer(async (req, res) => {
26
+ res.setHeader("Access-Control-Allow-Origin", "*");
27
+ res.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS");
28
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, x-api-key");
29
+ if (req.method === "OPTIONS") {
30
+ res.writeHead(204);
31
+ res.end();
32
+ return;
33
+ }
34
+ const urlObj = new URL(req.url || "/", `http://localhost:${port}`);
35
+ const pathname = urlObj.pathname;
36
+ if (apiKey) {
37
+ const authHeader = req.headers["authorization"] || "";
38
+ const xApiKey = req.headers["x-api-key"] || "";
39
+ const queryKey = urlObj.searchParams.get("key") || "";
40
+ const token = authHeader.startsWith("Bearer ") ? authHeader.slice(7) : authHeader;
41
+ if (token !== apiKey && xApiKey !== apiKey && queryKey !== apiKey) {
42
+ res.writeHead(401, { "Content-Type": "application/json" });
43
+ res.end(JSON.stringify({ error: "Unauthorized: Invalid or missing API key" }));
44
+ return;
45
+ }
46
+ }
47
+ try {
48
+ let data;
49
+ switch (pathname) {
50
+ case "/api/all":
51
+ case "/api/stats":
52
+ case "/api":
53
+ data = await collectAll({ detailed: opts.detailed });
54
+ break;
55
+ case "/api/cpu":
56
+ data = await collectCpu();
57
+ break;
58
+ case "/api/memory":
59
+ case "/api/mem":
60
+ data = await collectMemory();
61
+ break;
62
+ case "/api/gpu":
63
+ data = await collectGpu(opts.detailed);
64
+ break;
65
+ case "/api/battery":
66
+ case "/api/power":
67
+ data = await collectBattery();
68
+ break;
69
+ case "/api/thermal":
70
+ case "/api/temp":
71
+ data = await collectThermal(opts.detailed);
72
+ break;
73
+ case "/api/network":
74
+ case "/api/net":
75
+ data = await collectNetwork();
76
+ break;
77
+ case "/api/disk":
78
+ data = await collectDisk();
79
+ break;
80
+ case "/api/processes":
81
+ case "/api/proc":
82
+ data = await collectProcesses(50);
83
+ break;
84
+ case "/api/containers":
85
+ data = await collectContainers();
86
+ break;
87
+ case "/api/system":
88
+ case "/api/info":
89
+ data = await collectSystem();
90
+ break;
91
+ case "/health":
92
+ case "/api/health":
93
+ res.writeHead(200, { "Content-Type": "application/json" });
94
+ res.end(JSON.stringify({ status: "ok", uptime: os.uptime(), timestamp: (/* @__PURE__ */ new Date()).toISOString() }));
95
+ return;
96
+ default:
97
+ res.writeHead(404, { "Content-Type": "application/json" });
98
+ res.end(JSON.stringify({
99
+ error: "Not found",
100
+ availableEndpoints: [
101
+ "/api/all",
102
+ "/api/cpu",
103
+ "/api/memory",
104
+ "/api/gpu",
105
+ "/api/battery",
106
+ "/api/thermal",
107
+ "/api/network",
108
+ "/api/disk",
109
+ "/api/processes",
110
+ "/api/containers",
111
+ "/api/system",
112
+ "/api/health"
113
+ ]
114
+ }));
115
+ return;
116
+ }
117
+ res.writeHead(200, { "Content-Type": "application/json" });
118
+ res.end(JSON.stringify(data, null, 2));
119
+ } catch (err) {
120
+ res.writeHead(500, { "Content-Type": "application/json" });
121
+ res.end(JSON.stringify({ error: err.message || "Internal metric collection error" }));
122
+ }
123
+ });
124
+ server.on("error", (err) => {
125
+ if (err.code === "EADDRINUSE") {
126
+ console.error(chalk.red(`
127
+ \u2716 Error: Port ${port} is already in use. Use --port <port> to select an alternate port.
128
+ `));
129
+ } else {
130
+ console.error(chalk.red(`
131
+ \u2716 Server error: ${err.message}
132
+ `));
133
+ }
134
+ process.exit(1);
135
+ });
136
+ await new Promise((resolve) => server.listen(port, "0.0.0.0", resolve));
137
+ console.log(chalk.bold("\n \u{1F525} pyre serve \u2014 REST API Server\n"));
138
+ console.log(` ${chalk.green("\u2714")} API base URL: ${chalk.cyan(`http://0.0.0.0:${port}/api/all`)}`);
139
+ console.log(` ${chalk.green("\u2714")} Health check: ${chalk.cyan(`http://0.0.0.0:${port}/api/health`)}`);
140
+ if (apiKey) {
141
+ console.log(` ${chalk.yellow("\u{1F512}")} API Key protection: ${chalk.bold("ENABLED")} (Pass via header x-api-key or ?key=)`);
142
+ } else {
143
+ console.log(` ${chalk.dim("\u{1F513}")} API Key protection: ${chalk.dim("DISABLED (Public local network access)")}`);
144
+ }
145
+ console.log(`
146
+ ${chalk.dim("Endpoints: /api/cpu, /api/memory, /api/gpu, /api/battery, /api/thermal, /api/network, /api/disk, /api/processes")}`);
147
+ console.log(chalk.dim("\n Press Ctrl+C to stop.\n"));
148
+ await new Promise(() => {
149
+ });
150
+ }
151
+ export {
152
+ runServeCommand
153
+ };
@@ -0,0 +1,181 @@
1
+ // src/service.ts
2
+ import fs from "fs";
3
+ import os from "os";
4
+ import path from "path";
5
+ import { execSync } from "child_process";
6
+ import chalk from "chalk";
7
+ var SERVICE_LABEL = "com.pyre.web";
8
+ var PLIST_PATH = path.join(os.homedir(), "Library", "LaunchAgents", `${SERVICE_LABEL}.plist`);
9
+ var SYSTEMD_USER_DIR = path.join(os.homedir(), ".config", "systemd", "user");
10
+ var SYSTEMD_UNIT_PATH = path.join(SYSTEMD_USER_DIR, "pyre-web.service");
11
+ var TASK_NAME = "PyreWeb";
12
+ function installService(port = 3e3) {
13
+ const platform = process.platform;
14
+ if (platform === "darwin") {
15
+ installLaunchd(port);
16
+ } else if (platform === "linux") {
17
+ installSystemd(port);
18
+ } else if (platform === "win32") {
19
+ installWindowsTask(port);
20
+ } else {
21
+ console.log(chalk.yellow(`\u26A0 Background service installation not supported on ${platform}.`));
22
+ }
23
+ }
24
+ function uninstallService() {
25
+ const platform = process.platform;
26
+ if (platform === "darwin") {
27
+ uninstallLaunchd();
28
+ } else if (platform === "linux") {
29
+ uninstallSystemd();
30
+ } else if (platform === "win32") {
31
+ uninstallWindowsTask();
32
+ } else {
33
+ console.log(chalk.yellow(`\u26A0 Background service uninstallation not supported on ${platform}.`));
34
+ }
35
+ }
36
+ var installLaunchdAgent = installService;
37
+ var uninstallLaunchdAgent = uninstallService;
38
+ function installLaunchd(port) {
39
+ const nodePath = process.execPath;
40
+ const scriptPath = path.resolve(process.argv[1]);
41
+ const plistContent = `<?xml version="1.0" encoding="UTF-8"?>
42
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
43
+ <plist version="1.0">
44
+ <dict>
45
+ <key>Label</key>
46
+ <string>${SERVICE_LABEL}</string>
47
+ <key>ProgramArguments</key>
48
+ <array>
49
+ <string>${nodePath}</string>
50
+ <string>${scriptPath}</string>
51
+ <string>web</string>
52
+ <string>--port</string>
53
+ <string>${port}</string>
54
+ </array>
55
+ <key>RunAtLoad</key>
56
+ <true/>
57
+ <key>KeepAlive</key>
58
+ <true/>
59
+ <key>StandardOutPath</key>
60
+ <string>${path.join(os.homedir(), ".config", "pyre", "web.log")}</string>
61
+ <key>StandardErrorPath</key>
62
+ <string>${path.join(os.homedir(), ".config", "pyre", "web.err")}</string>
63
+ </dict>
64
+ </plist>
65
+ `;
66
+ try {
67
+ const launchAgentsDir = path.dirname(PLIST_PATH);
68
+ if (!fs.existsSync(launchAgentsDir)) {
69
+ fs.mkdirSync(launchAgentsDir, { recursive: true });
70
+ }
71
+ fs.writeFileSync(PLIST_PATH, plistContent);
72
+ try {
73
+ execSync(`launchctl unload ${PLIST_PATH} 2>/dev/null`);
74
+ } catch {
75
+ }
76
+ execSync(`launchctl load ${PLIST_PATH}`);
77
+ console.log(chalk.green(`\u2714 Installed and started launchd agent at ${PLIST_PATH}`));
78
+ console.log(chalk.dim(` Service label: ${SERVICE_LABEL}`));
79
+ } catch (err) {
80
+ console.log(chalk.red(`\u2716 Failed to install launchd agent: ${err.message}`));
81
+ }
82
+ }
83
+ function uninstallLaunchd() {
84
+ try {
85
+ if (fs.existsSync(PLIST_PATH)) {
86
+ try {
87
+ execSync(`launchctl unload ${PLIST_PATH} 2>/dev/null`);
88
+ } catch {
89
+ }
90
+ fs.unlinkSync(PLIST_PATH);
91
+ console.log(chalk.green(`\u2714 Uninstalled launchd agent ${PLIST_PATH}`));
92
+ } else {
93
+ console.log(chalk.yellow(` No launchd agent found at ${PLIST_PATH}`));
94
+ }
95
+ } catch (err) {
96
+ console.log(chalk.red(`\u2716 Failed to uninstall launchd agent: ${err.message}`));
97
+ }
98
+ }
99
+ function installSystemd(port) {
100
+ const nodePath = process.execPath;
101
+ const scriptPath = path.resolve(process.argv[1]);
102
+ const logDir = path.join(os.homedir(), ".config", "pyre");
103
+ if (!fs.existsSync(logDir)) fs.mkdirSync(logDir, { recursive: true });
104
+ if (!fs.existsSync(SYSTEMD_USER_DIR)) fs.mkdirSync(SYSTEMD_USER_DIR, { recursive: true });
105
+ const unitContent = `[Unit]
106
+ Description=Pyre System Monitor Web Service
107
+ After=network.target
108
+
109
+ [Service]
110
+ Type=simple
111
+ ExecStart=${nodePath} "${scriptPath}" web --port ${port}
112
+ Restart=always
113
+ RestartSec=5
114
+ StandardOutput=append:${path.join(logDir, "web.log")}
115
+ StandardError=append:${path.join(logDir, "web.err")}
116
+
117
+ [Install]
118
+ WantedBy=default.target
119
+ `;
120
+ try {
121
+ fs.writeFileSync(SYSTEMD_UNIT_PATH, unitContent);
122
+ try {
123
+ execSync("systemctl --user daemon-reload");
124
+ execSync("systemctl --user enable --now pyre-web.service");
125
+ } catch (cmdErr) {
126
+ console.log(chalk.yellow(` Unit file written to ${SYSTEMD_UNIT_PATH}, but could not auto-enable: ${cmdErr.message}`));
127
+ console.log(chalk.dim(" You can manually enable it using: systemctl --user enable --now pyre-web.service"));
128
+ return;
129
+ }
130
+ console.log(chalk.green(`\u2714 Installed and started systemd user service at ${SYSTEMD_UNIT_PATH}`));
131
+ console.log(chalk.dim(" Service name: pyre-web.service"));
132
+ } catch (err) {
133
+ console.log(chalk.red(`\u2716 Failed to install systemd user service: ${err.message}`));
134
+ }
135
+ }
136
+ function uninstallSystemd() {
137
+ try {
138
+ if (fs.existsSync(SYSTEMD_UNIT_PATH)) {
139
+ try {
140
+ execSync("systemctl --user disable --now pyre-web.service 2>/dev/null");
141
+ } catch {
142
+ }
143
+ fs.unlinkSync(SYSTEMD_UNIT_PATH);
144
+ try {
145
+ execSync("systemctl --user daemon-reload 2>/dev/null");
146
+ } catch {
147
+ }
148
+ console.log(chalk.green(`\u2714 Uninstalled systemd user service ${SYSTEMD_UNIT_PATH}`));
149
+ } else {
150
+ console.log(chalk.yellow(` No systemd service found at ${SYSTEMD_UNIT_PATH}`));
151
+ }
152
+ } catch (err) {
153
+ console.log(chalk.red(`\u2716 Failed to uninstall systemd service: ${err.message}`));
154
+ }
155
+ }
156
+ function installWindowsTask(port) {
157
+ const nodePath = process.execPath;
158
+ const scriptPath = path.resolve(process.argv[1]);
159
+ try {
160
+ const cmd = `schtasks /create /tn "${TASK_NAME}" /tr "\\"${nodePath}\\" \\"${scriptPath}\\" web --port ${port}" /sc onlogon /f`;
161
+ execSync(cmd, { stdio: "pipe" });
162
+ console.log(chalk.green(`\u2714 Installed Windows scheduled task '${TASK_NAME}' (runs on logon)`));
163
+ } catch (err) {
164
+ console.log(chalk.red(`\u2716 Failed to register Windows scheduled task: ${err.message}`));
165
+ }
166
+ }
167
+ function uninstallWindowsTask() {
168
+ try {
169
+ const cmd = `schtasks /delete /tn "${TASK_NAME}" /f`;
170
+ execSync(cmd, { stdio: "pipe" });
171
+ console.log(chalk.green(`\u2714 Uninstalled Windows scheduled task '${TASK_NAME}'`));
172
+ } catch (err) {
173
+ console.log(chalk.yellow(` No scheduled task '${TASK_NAME}' found or deletion failed: ${err.message}`));
174
+ }
175
+ }
176
+ export {
177
+ installLaunchdAgent,
178
+ installService,
179
+ uninstallLaunchdAgent,
180
+ uninstallService
181
+ };
@@ -0,0 +1,166 @@
1
+ // src/smartCmd.ts
2
+ import { execFile } from "child_process";
3
+ import { promisify } from "util";
4
+ import chalk from "chalk";
5
+ var execFileAsync = promisify(execFile);
6
+ async function runCmd(cmd, args, timeoutMs = 5e3) {
7
+ try {
8
+ const { stdout } = await execFileAsync(cmd, args, { timeout: timeoutMs });
9
+ return stdout.trim();
10
+ } catch {
11
+ return "";
12
+ }
13
+ }
14
+ async function getPhysicalDisks() {
15
+ const scanRaw = await runCmd("smartctl", ["--scan", "--json"]);
16
+ if (scanRaw) {
17
+ try {
18
+ const parsed = JSON.parse(scanRaw);
19
+ if (Array.isArray(parsed.devices) && parsed.devices.length > 0) {
20
+ return parsed.devices.map((d) => d.name).filter(Boolean);
21
+ }
22
+ } catch {
23
+ }
24
+ }
25
+ if (process.platform === "linux") {
26
+ const lsblk = await runCmd("lsblk", ["-dpno", "NAME"]);
27
+ if (lsblk) {
28
+ const disks = lsblk.split("\n").map((s) => s.trim()).filter((d) => /^\/dev\/(sd[a-z]|nvme\d+n\d+|vd[a-z])/.test(d));
29
+ if (disks.length > 0) return disks;
30
+ }
31
+ return ["/dev/sda"];
32
+ }
33
+ if (process.platform === "win32") {
34
+ return ["/dev/sda", "/dev/csmi0,0"];
35
+ }
36
+ const raw = await runCmd("diskutil", ["list", "-plist"]);
37
+ if (!raw) return ["/dev/disk0"];
38
+ const matches = raw.match(/\/dev\/disk\d+(?!s)/g) ?? [];
39
+ return [...new Set(matches)].filter((d) => /^\/dev\/disk\d+$/.test(d));
40
+ }
41
+ async function collectSmartctlDisk(device) {
42
+ const raw = await runCmd("smartctl", ["-a", "--json", device], 6e3);
43
+ if (!raw) return null;
44
+ try {
45
+ const j = JSON.parse(raw);
46
+ const model = j.model_name ?? j.device?.name ?? "Unknown";
47
+ const serial = j.serial_number ?? "";
48
+ const protocol = j.device?.protocol ?? "";
49
+ const passed = j.smart_status?.passed;
50
+ const health = passed === true ? "PASSED" : passed === false ? "FAILED" : "Unknown";
51
+ let tempC;
52
+ if (j.temperature?.current !== void 0) tempC = j.temperature.current;
53
+ const poh = j.power_on_time?.hours;
54
+ let reallocated;
55
+ if (Array.isArray(j.ata_smart_attributes?.table)) {
56
+ const attr = j.ata_smart_attributes.table.find((a) => a.id === 5);
57
+ if (attr) reallocated = attr.raw?.value ?? 0;
58
+ }
59
+ let percentUsed;
60
+ if (j.nvme_smart_health_information_log?.percentage_used !== void 0) {
61
+ percentUsed = j.nvme_smart_health_information_log.percentage_used;
62
+ }
63
+ let bytesRead;
64
+ let bytesWritten;
65
+ if (j.nvme_smart_health_information_log) {
66
+ const unitsRead = j.nvme_smart_health_information_log.data_units_read;
67
+ const unitsWritten = j.nvme_smart_health_information_log.data_units_written;
68
+ if (unitsRead !== void 0) bytesRead = unitsRead * 512e3;
69
+ if (unitsWritten !== void 0) bytesWritten = unitsWritten * 512e3;
70
+ }
71
+ const userCap = j.user_capacity?.bytes;
72
+ const capacity = userCap !== void 0 ? userCap >= 1e12 ? `${(userCap / 1e12).toFixed(1)} TB` : userCap >= 1e9 ? `${(userCap / 1e9).toFixed(0)} GB` : `${(userCap / 1e6).toFixed(0)} MB` : void 0;
73
+ return {
74
+ device,
75
+ model,
76
+ serialNumber: serial,
77
+ health,
78
+ tempC,
79
+ powerOnHours: poh,
80
+ reallocatedSectors: reallocated,
81
+ percentUsed,
82
+ bytesRead,
83
+ bytesWritten,
84
+ protocol,
85
+ capacity
86
+ };
87
+ } catch {
88
+ return null;
89
+ }
90
+ }
91
+ async function collectDiskutilFallback(device) {
92
+ const raw = await runCmd("diskutil", ["info", device]);
93
+ if (!raw) return null;
94
+ const nameMatch = raw.match(/(?:Device \/ Media Name|Volume Name|Media Name):\s*(.+)/i);
95
+ const sizeMatch = raw.match(/Disk Size:\s*(.+?\([^)]+\))/i);
96
+ return {
97
+ device,
98
+ model: nameMatch?.[1]?.trim() || "Unknown",
99
+ serialNumber: "",
100
+ health: "Unknown",
101
+ capacity: sizeMatch?.[1]?.trim()
102
+ };
103
+ }
104
+ function formatBytes(bytes) {
105
+ if (bytes >= 1e15) return `${(bytes / 1e15).toFixed(1)} PB`;
106
+ if (bytes >= 1e12) return `${(bytes / 1e12).toFixed(1)} TB`;
107
+ if (bytes >= 1e9) return `${(bytes / 1e9).toFixed(1)} GB`;
108
+ if (bytes >= 1e6) return `${(bytes / 1e6).toFixed(0)} MB`;
109
+ return `${bytes} B`;
110
+ }
111
+ function healthColor(h) {
112
+ if (h === "PASSED") return chalk.green(h);
113
+ if (h === "FAILED") return chalk.red.bold(h);
114
+ return chalk.dim(h);
115
+ }
116
+ async function runSmartCommand() {
117
+ console.log(chalk.bold("\n \u{1F525} pyre \u2014 S.M.A.R.T. Disk Health\n"));
118
+ const smartctlVersion = await runCmd("smartctl", ["--version"], 2e3);
119
+ const hasSmartctl = smartctlVersion.includes("smartmontools");
120
+ if (!hasSmartctl) {
121
+ console.log(chalk.yellow(" \u26A0 smartctl not found. Showing basic disk info."));
122
+ console.log(chalk.dim(" For full SMART data: ") + chalk.cyan("brew install smartmontools") + "\n");
123
+ }
124
+ const devices = await getPhysicalDisks();
125
+ if (devices.length === 0) {
126
+ console.log(chalk.yellow(" No physical disks found.\n"));
127
+ return;
128
+ }
129
+ for (const device of devices) {
130
+ let info = null;
131
+ if (hasSmartctl) info = await collectSmartctlDisk(device);
132
+ if (!info) info = await collectDiskutilFallback(device);
133
+ if (!info) continue;
134
+ console.log(chalk.bold(` ${info.device}`));
135
+ console.log(` Model: ${info.model}`);
136
+ if (info.serialNumber) console.log(` Serial: ${chalk.dim(info.serialNumber)}`);
137
+ if (info.protocol) console.log(` Protocol: ${info.protocol}`);
138
+ if (info.capacity) console.log(` Capacity: ${info.capacity}`);
139
+ console.log(` Health: ${healthColor(info.health)}`);
140
+ if (info.tempC !== void 0) {
141
+ const tc = info.tempC >= 60 ? chalk.red : info.tempC >= 45 ? chalk.yellow : chalk.green;
142
+ console.log(` Temperature: ${tc(`${info.tempC}\xB0C`)}`);
143
+ }
144
+ if (info.powerOnHours !== void 0) {
145
+ const days = Math.floor(info.powerOnHours / 24);
146
+ console.log(` Power-On Hours: ${info.powerOnHours.toLocaleString()} h (${days} days)`);
147
+ }
148
+ if (info.reallocatedSectors !== void 0) {
149
+ const sc = info.reallocatedSectors > 0 ? chalk.yellow : chalk.green;
150
+ console.log(` Reallocated Secs: ${sc(info.reallocatedSectors.toString())}`);
151
+ }
152
+ if (info.percentUsed !== void 0) {
153
+ const wc = info.percentUsed >= 90 ? chalk.red : info.percentUsed >= 50 ? chalk.yellow : chalk.green;
154
+ console.log(` Wear Level: ${wc(`${info.percentUsed}% used`)}`);
155
+ }
156
+ if (info.bytesRead !== void 0) console.log(` Data Read: ${formatBytes(info.bytesRead)}`);
157
+ if (info.bytesWritten !== void 0) console.log(` Data Written: ${formatBytes(info.bytesWritten)}`);
158
+ console.log();
159
+ }
160
+ if (!hasSmartctl) {
161
+ console.log(chalk.dim(" Install smartmontools for full SMART attributes.\n"));
162
+ }
163
+ }
164
+ export {
165
+ runSmartCommand
166
+ };
@@ -0,0 +1,126 @@
1
+ import {
2
+ getConfigFile,
3
+ getProfilesDir,
4
+ writeConfig
5
+ } from "./chunk-53O2RGSL.js";
6
+
7
+ // src/syncCmd.ts
8
+ import fs from "fs";
9
+ import path from "path";
10
+ import os from "os";
11
+ import crypto from "crypto";
12
+ import chalk from "chalk";
13
+ function getDefaultSyncDir() {
14
+ if (process.env.PYRE_SYNC_DIR) return process.env.PYRE_SYNC_DIR;
15
+ const icloudPath = path.join(os.homedir(), "Library", "Mobile Documents", "com~apple~CloudDocs", "PyreSync");
16
+ if (fs.existsSync(path.dirname(icloudPath))) {
17
+ return icloudPath;
18
+ }
19
+ return path.join(os.homedir(), ".pyre-sync");
20
+ }
21
+ function computeDirChecksum(dirPath) {
22
+ const hash = crypto.createHash("sha256");
23
+ if (!fs.existsSync(dirPath)) return "";
24
+ const files = fs.readdirSync(dirPath).sort();
25
+ for (const file of files) {
26
+ const full = path.join(dirPath, file);
27
+ if (fs.statSync(full).isFile()) {
28
+ hash.update(file);
29
+ hash.update(fs.readFileSync(full));
30
+ }
31
+ }
32
+ return hash.digest("hex").slice(0, 16);
33
+ }
34
+ async function runSyncCommand(subcommand = "status", opts = {}) {
35
+ const syncDir = opts.syncDir || getDefaultSyncDir();
36
+ const manifestPath = path.join(syncDir, "sync-manifest.json");
37
+ const remoteProfilesDir = path.join(syncDir, "profiles");
38
+ const remoteConfigFile = path.join(syncDir, "config.json");
39
+ const localProfilesDir = getProfilesDir();
40
+ const localConfigFile = getConfigFile();
41
+ console.log(chalk.bold("\n \u{1F525} pyre sync \u2014 Configuration & Profile Cloud Sync\n"));
42
+ console.log(` Sync Target Directory: ${chalk.cyan(syncDir)}`);
43
+ if (subcommand === "status" || !subcommand) {
44
+ const hasSyncDir = fs.existsSync(syncDir);
45
+ const hasManifest = fs.existsSync(manifestPath);
46
+ console.log(` Local Profiles: ${chalk.bold(String(fs.existsSync(localProfilesDir) ? fs.readdirSync(localProfilesDir).filter((f) => f.endsWith(".json")).length : 0))}`);
47
+ console.log(` Sync Target Available: ${hasSyncDir ? chalk.green("YES") : chalk.yellow("NO (will be created on push)")}`);
48
+ if (hasManifest) {
49
+ try {
50
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
51
+ console.log(` Remote Last Updated: ${chalk.green(manifest.lastUpdated)} (from ${chalk.bold(manifest.sourceHost)})`);
52
+ console.log(` Remote Profiles Count: ${chalk.bold(String(manifest.profilesCount))}`);
53
+ console.log(` Remote Checksum: ${chalk.dim(manifest.checksum)}`);
54
+ } catch {
55
+ console.log(chalk.yellow(" Remote manifest corrupted."));
56
+ }
57
+ } else {
58
+ console.log(chalk.dim(' No remote sync bundle detected yet. Run "pyre sync push" to initialize.'));
59
+ }
60
+ console.log();
61
+ return;
62
+ }
63
+ if (subcommand === "push") {
64
+ if (!fs.existsSync(syncDir)) {
65
+ fs.mkdirSync(syncDir, { recursive: true });
66
+ }
67
+ if (!fs.existsSync(remoteProfilesDir)) {
68
+ fs.mkdirSync(remoteProfilesDir, { recursive: true });
69
+ }
70
+ if (fs.existsSync(localConfigFile)) {
71
+ fs.copyFileSync(localConfigFile, remoteConfigFile);
72
+ }
73
+ let profileCount = 0;
74
+ if (fs.existsSync(localProfilesDir)) {
75
+ const files = fs.readdirSync(localProfilesDir).filter((f) => f.endsWith(".json"));
76
+ profileCount = files.length;
77
+ for (const f of files) {
78
+ fs.copyFileSync(path.join(localProfilesDir, f), path.join(remoteProfilesDir, f));
79
+ }
80
+ }
81
+ const checksum = computeDirChecksum(remoteProfilesDir);
82
+ const manifest = {
83
+ version: 1,
84
+ lastUpdated: (/* @__PURE__ */ new Date()).toISOString(),
85
+ sourceHost: os.hostname(),
86
+ checksum,
87
+ profilesCount: profileCount
88
+ };
89
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
90
+ console.log(chalk.green(` \u2714 Successfully pushed config and ${profileCount} profile(s) to sync target.`));
91
+ console.log(` Checksum: ${chalk.dim(checksum)} | Timestamp: ${chalk.dim(manifest.lastUpdated)}
92
+ `);
93
+ return;
94
+ }
95
+ if (subcommand === "pull") {
96
+ if (!fs.existsSync(manifestPath) || !fs.existsSync(remoteConfigFile)) {
97
+ console.error(chalk.red(` \u2716 Error: No sync manifest found at ${syncDir}. Run "pyre sync push" first from another machine.
98
+ `));
99
+ process.exit(1);
100
+ }
101
+ if (fs.existsSync(remoteConfigFile)) {
102
+ const remoteConfig = JSON.parse(fs.readFileSync(remoteConfigFile, "utf-8"));
103
+ writeConfig(remoteConfig);
104
+ }
105
+ let pulledCount = 0;
106
+ if (fs.existsSync(remoteProfilesDir)) {
107
+ if (!fs.existsSync(localProfilesDir)) {
108
+ fs.mkdirSync(localProfilesDir, { recursive: true });
109
+ }
110
+ const files = fs.readdirSync(remoteProfilesDir).filter((f) => f.endsWith(".json"));
111
+ pulledCount = files.length;
112
+ for (const f of files) {
113
+ fs.copyFileSync(path.join(remoteProfilesDir, f), path.join(localProfilesDir, f));
114
+ }
115
+ }
116
+ console.log(chalk.green(` \u2714 Successfully pulled latest configuration and ${pulledCount} profile(s).`));
117
+ console.log(chalk.dim(` Applied to ${localConfigFile}
118
+ `));
119
+ return;
120
+ }
121
+ console.log(chalk.yellow(` Unknown subcommand: "${subcommand}". Available: status, push, pull
122
+ `));
123
+ }
124
+ export {
125
+ runSyncCommand
126
+ };