pyre-cli 10.1.0 → 10.3.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 +211 -494
- package/dist/{checkCmd-RKW6QGDR.js → checkCmd-WQAHAUWO.js} +1 -1
- package/dist/{chunk-GHRDW646.js → chunk-23YWCI3O.js} +375 -135
- package/dist/{service-Q74BBILO.js → chunk-KFPTD266.js} +43 -3
- package/dist/{doctor-GYHFTNZQ.js → chunk-KVUMOA6C.js} +7 -6
- package/dist/{chunk-53O2RGSL.js → chunk-QM5L43EK.js} +19 -2
- package/dist/{chunk-6UHZP4QP.js → chunk-XTOOEKLM.js} +24 -12
- package/dist/{collectors-H43PN4PS.js → collectors-JDN5Y7EV.js} +1 -1
- package/dist/{completions-JKYEJNGR.js → completions-ZVTNE52B.js} +15 -11
- package/dist/{config-GYGJXU2Q.js → config-J4YMADEF.js} +3 -1
- package/dist/doctor-LHFW4UJP.js +10 -0
- package/dist/firebaseSync-ON3RZ6KX.js +91 -0
- package/dist/index.js +4245 -736
- package/dist/monitors/platform/windows-ui.ps1 +42 -0
- package/dist/{netUsageCmd-WKBTW2YG.js → netUsageCmd-67IBZZYN.js} +2 -2
- package/dist/{pipeCmd-7REDBLBU.js → pipeCmd-2N4JPKYE.js} +1 -1
- package/dist/{prometheusCmd-PL64BLN5.js → prometheusCmd-LEUM5WCZ.js} +1 -1
- package/dist/serveCmd-L7LVJOZN.js +353 -0
- package/dist/service-7GIX3YNC.js +16 -0
- package/dist/syncCmd-42IRF633.js +246 -0
- package/dist/{topoCmd-DU2HHZNH.js → topoCmd-DZ3YS5IL.js} +1 -1
- package/dist/{watchdogCmd-UF53JPZB.js → watchdogCmd-6GR4UN6B.js} +7 -7
- package/dist/{wizard-TD7BGX6O.js → wizard-JQMQWFAW.js} +1 -1
- package/package.json +4 -3
- package/parity.md +2 -0
- package/src/completions.ts +15 -11
- package/src/easy_features.test.ts +149 -0
- package/src/firebaseSync.ts +157 -0
- package/src/formatters/html_template.ts +3086 -598
- package/src/formatters/render.ts +25 -12
- package/src/index.ts +518 -24
- package/src/live/input.ts +501 -148
- package/src/live/render.ts +163 -18
- package/src/live/state.ts +52 -2
- package/src/live/types.ts +1 -1
- package/src/monitors/collectors.ts +3 -10
- package/src/monitors/platform/windows-ui.ps1 +42 -0
- package/src/monitors/platform/windows.ts +442 -147
- package/src/monitors/types.ts +8 -0
- package/src/serveCmd.ts +17 -2
- package/src/service.ts +49 -0
- package/src/state/config.ts +48 -0
- package/src/syncCmd.ts +159 -11
- package/src/web/dashboard.ts +205 -0
- package/dist/serveCmd-2H5VLF3P.js +0 -153
- package/dist/syncCmd-A2W3QB3S.js +0 -126
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getConfigFile,
|
|
3
|
+
getProfilesDir,
|
|
4
|
+
readConfig,
|
|
5
|
+
writeConfig
|
|
6
|
+
} from "./chunk-QM5L43EK.js";
|
|
7
|
+
|
|
8
|
+
// src/syncCmd.ts
|
|
9
|
+
import fs from "fs";
|
|
10
|
+
import path from "path";
|
|
11
|
+
import os from "os";
|
|
12
|
+
import crypto from "crypto";
|
|
13
|
+
import chalk from "chalk";
|
|
14
|
+
function getDefaultSyncDir() {
|
|
15
|
+
if (process.env.PYRE_SYNC_DIR) return process.env.PYRE_SYNC_DIR;
|
|
16
|
+
const icloudPath = path.join(os.homedir(), "Library", "Mobile Documents", "com~apple~CloudDocs", "PyreSync");
|
|
17
|
+
if (fs.existsSync(path.dirname(icloudPath))) {
|
|
18
|
+
return icloudPath;
|
|
19
|
+
}
|
|
20
|
+
return path.join(os.homedir(), ".pyre-sync");
|
|
21
|
+
}
|
|
22
|
+
function computeDirChecksum(dirPath) {
|
|
23
|
+
const hash = crypto.createHash("sha256");
|
|
24
|
+
if (!fs.existsSync(dirPath)) return "";
|
|
25
|
+
const files = fs.readdirSync(dirPath).sort();
|
|
26
|
+
for (const file of files) {
|
|
27
|
+
const full = path.join(dirPath, file);
|
|
28
|
+
if (fs.statSync(full).isFile()) {
|
|
29
|
+
hash.update(file);
|
|
30
|
+
hash.update(fs.readFileSync(full));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return hash.digest("hex").slice(0, 16);
|
|
34
|
+
}
|
|
35
|
+
function loadLocalProfiles() {
|
|
36
|
+
const profilesDir = getProfilesDir();
|
|
37
|
+
const profiles = {};
|
|
38
|
+
if (!fs.existsSync(profilesDir)) return profiles;
|
|
39
|
+
for (const f of fs.readdirSync(profilesDir).filter((f2) => f2.endsWith(".json"))) {
|
|
40
|
+
try {
|
|
41
|
+
const name = f.replace(/\.json$/, "");
|
|
42
|
+
profiles[name] = JSON.parse(fs.readFileSync(path.join(profilesDir, f), "utf-8"));
|
|
43
|
+
} catch {
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return profiles;
|
|
47
|
+
}
|
|
48
|
+
function saveLocalProfiles(profiles) {
|
|
49
|
+
const profilesDir = getProfilesDir();
|
|
50
|
+
if (!fs.existsSync(profilesDir)) fs.mkdirSync(profilesDir, { recursive: true });
|
|
51
|
+
for (const [name, cfg] of Object.entries(profiles)) {
|
|
52
|
+
fs.writeFileSync(path.join(profilesDir, `${name}.json`), JSON.stringify(cfg, null, 2) + "\n");
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
async function runFirebaseSubcommand(action) {
|
|
56
|
+
const config = readConfig();
|
|
57
|
+
if (action === "enable") {
|
|
58
|
+
writeConfig({ firebaseSyncEnabled: true });
|
|
59
|
+
console.log(chalk.green(" \u2714 Firebase sync enabled."));
|
|
60
|
+
console.log(chalk.dim(' Run "pyre sync push" to upload your config and profiles to Firestore.\n'));
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (action === "disable") {
|
|
64
|
+
writeConfig({ firebaseSyncEnabled: false });
|
|
65
|
+
console.log(chalk.yellow(" \u2714 Firebase sync disabled. Falling back to local folder sync.\n"));
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (action === "status" || !action) {
|
|
69
|
+
console.log(chalk.bold(` Firebase Sync: ${config.firebaseSyncEnabled ? chalk.green("ENABLED") : chalk.dim("DISABLED")}`));
|
|
70
|
+
if (!config.firebaseSyncEnabled) {
|
|
71
|
+
console.log(chalk.dim(' Run "pyre sync firebase enable" to activate.\n'));
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const { firebaseStatus } = await import("./firebaseSync-ON3RZ6KX.js");
|
|
75
|
+
const hosts = await firebaseStatus();
|
|
76
|
+
if (hosts.length === 0) {
|
|
77
|
+
console.log(chalk.dim(' No remote hosts found in Firestore. Run "pyre sync push" to upload.\n'));
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
console.log();
|
|
81
|
+
for (const h of hosts) {
|
|
82
|
+
console.log(` ${chalk.bold(h.sourceHost)} (${chalk.cyan(h.hostId)})`);
|
|
83
|
+
console.log(` Last Updated: ${chalk.green(h.lastUpdated)}`);
|
|
84
|
+
console.log(` Profiles: ${chalk.bold(String(h.profilesCount))}`);
|
|
85
|
+
console.log(` Checksum: ${chalk.dim(h.checksum)}
|
|
86
|
+
`);
|
|
87
|
+
}
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
console.log(chalk.yellow(` Unknown firebase action: "${action}". Available: enable, disable, status
|
|
91
|
+
`));
|
|
92
|
+
}
|
|
93
|
+
async function runSyncCommand(subcommand = "status", opts = {}) {
|
|
94
|
+
const config = readConfig();
|
|
95
|
+
console.log(chalk.bold("\n \u{1F525} pyre sync \u2014 Configuration & Profile Cloud Sync\n"));
|
|
96
|
+
if (subcommand === "firebase") {
|
|
97
|
+
const action = process.argv[process.argv.indexOf("firebase") + 1] || "status";
|
|
98
|
+
await runFirebaseSubcommand(action);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (config.firebaseSyncEnabled) {
|
|
102
|
+
console.log(` Backend: ${chalk.cyan("Firebase Firestore")} (${chalk.dim("dearborn-internal-tool")})`);
|
|
103
|
+
const hostId = config.firebaseHostId || void 0;
|
|
104
|
+
if (subcommand === "status") {
|
|
105
|
+
const { firebaseStatus } = await import("./firebaseSync-ON3RZ6KX.js");
|
|
106
|
+
const hosts = await firebaseStatus();
|
|
107
|
+
if (hosts.length === 0) {
|
|
108
|
+
console.log(chalk.dim(' No remote hosts found. Run "pyre sync push" to upload your config.\n'));
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
console.log();
|
|
112
|
+
for (const h of hosts) {
|
|
113
|
+
console.log(` ${chalk.bold(h.sourceHost)} (${chalk.cyan(h.hostId)})`);
|
|
114
|
+
console.log(` Last Updated: ${chalk.green(h.lastUpdated)}`);
|
|
115
|
+
console.log(` Profiles: ${chalk.bold(String(h.profilesCount))}`);
|
|
116
|
+
console.log(` Checksum: ${chalk.dim(h.checksum)}
|
|
117
|
+
`);
|
|
118
|
+
}
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (subcommand === "push") {
|
|
122
|
+
const { firebasePush } = await import("./firebaseSync-ON3RZ6KX.js");
|
|
123
|
+
const profiles = loadLocalProfiles();
|
|
124
|
+
const result = await firebasePush(config, profiles, hostId);
|
|
125
|
+
const profileCount = Object.keys(profiles).length;
|
|
126
|
+
console.log(chalk.green(` \u2714 Successfully pushed config and ${profileCount} profile(s) to Firestore.`));
|
|
127
|
+
console.log(` Host ID: ${chalk.cyan(result.hostId)}`);
|
|
128
|
+
console.log(` Checksum: ${chalk.dim(result.checksum)} | Timestamp: ${chalk.dim(result.lastUpdated)}
|
|
129
|
+
`);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (subcommand === "pull") {
|
|
133
|
+
const targetHost = process.argv[process.argv.indexOf("pull") + 1];
|
|
134
|
+
const resolvedTarget = targetHost && !targetHost.startsWith("-") ? targetHost : void 0;
|
|
135
|
+
const { firebasePull } = await import("./firebaseSync-ON3RZ6KX.js");
|
|
136
|
+
const result = await firebasePull(resolvedTarget);
|
|
137
|
+
if (!result) {
|
|
138
|
+
const hint = resolvedTarget ? `host "${resolvedTarget}"` : "any host";
|
|
139
|
+
console.error(chalk.red(` \u2716 No sync data found for ${hint} in Firestore. Run "pyre sync push" from another machine.
|
|
140
|
+
`));
|
|
141
|
+
process.exit(1);
|
|
142
|
+
}
|
|
143
|
+
writeConfig(result.doc.config);
|
|
144
|
+
saveLocalProfiles(result.doc.profiles);
|
|
145
|
+
const pulledCount = result.doc.profilesCount;
|
|
146
|
+
console.log(chalk.green(` \u2714 Successfully pulled config and ${pulledCount} profile(s) from ${chalk.bold(result.doc.sourceHost)}.`));
|
|
147
|
+
console.log(chalk.dim(` Applied to ${getConfigFile()}
|
|
148
|
+
`));
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
console.log(chalk.yellow(` Unknown subcommand: "${subcommand}". Available: status, push, pull, firebase
|
|
152
|
+
`));
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
const syncDir = opts.syncDir || getDefaultSyncDir();
|
|
156
|
+
const manifestPath = path.join(syncDir, "sync-manifest.json");
|
|
157
|
+
const remoteProfilesDir = path.join(syncDir, "profiles");
|
|
158
|
+
const remoteConfigFile = path.join(syncDir, "config.json");
|
|
159
|
+
const localProfilesDir = getProfilesDir();
|
|
160
|
+
const localConfigFile = getConfigFile();
|
|
161
|
+
console.log(` Backend: ${chalk.cyan("Local Folder")}`);
|
|
162
|
+
console.log(` Sync Target: ${chalk.cyan(syncDir)}`);
|
|
163
|
+
if (subcommand === "status" || !subcommand) {
|
|
164
|
+
const hasSyncDir = fs.existsSync(syncDir);
|
|
165
|
+
const hasManifest = fs.existsSync(manifestPath);
|
|
166
|
+
console.log(` Local Profiles: ${chalk.bold(String(fs.existsSync(localProfilesDir) ? fs.readdirSync(localProfilesDir).filter((f) => f.endsWith(".json")).length : 0))}`);
|
|
167
|
+
console.log(` Sync Target Exists: ${hasSyncDir ? chalk.green("YES") : chalk.yellow("NO (will be created on push)")}`);
|
|
168
|
+
if (hasManifest) {
|
|
169
|
+
try {
|
|
170
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
|
|
171
|
+
console.log(` Remote Last Updated: ${chalk.green(manifest.lastUpdated)} (from ${chalk.bold(manifest.sourceHost)})`);
|
|
172
|
+
console.log(` Remote Profiles: ${chalk.bold(String(manifest.profilesCount))}`);
|
|
173
|
+
console.log(` Remote Checksum: ${chalk.dim(manifest.checksum)}`);
|
|
174
|
+
} catch {
|
|
175
|
+
console.log(chalk.yellow(" Remote manifest corrupted."));
|
|
176
|
+
}
|
|
177
|
+
} else {
|
|
178
|
+
console.log(chalk.dim(' No remote sync bundle detected yet. Run "pyre sync push" to initialize.'));
|
|
179
|
+
}
|
|
180
|
+
console.log();
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if (subcommand === "push") {
|
|
184
|
+
if (!fs.existsSync(syncDir)) {
|
|
185
|
+
fs.mkdirSync(syncDir, { recursive: true });
|
|
186
|
+
}
|
|
187
|
+
if (!fs.existsSync(remoteProfilesDir)) {
|
|
188
|
+
fs.mkdirSync(remoteProfilesDir, { recursive: true });
|
|
189
|
+
}
|
|
190
|
+
if (fs.existsSync(localConfigFile)) {
|
|
191
|
+
fs.copyFileSync(localConfigFile, remoteConfigFile);
|
|
192
|
+
}
|
|
193
|
+
let profileCount = 0;
|
|
194
|
+
if (fs.existsSync(localProfilesDir)) {
|
|
195
|
+
const files = fs.readdirSync(localProfilesDir).filter((f) => f.endsWith(".json"));
|
|
196
|
+
profileCount = files.length;
|
|
197
|
+
for (const f of files) {
|
|
198
|
+
fs.copyFileSync(path.join(localProfilesDir, f), path.join(remoteProfilesDir, f));
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
const checksum = computeDirChecksum(remoteProfilesDir);
|
|
202
|
+
const manifest = {
|
|
203
|
+
version: 1,
|
|
204
|
+
lastUpdated: (/* @__PURE__ */ new Date()).toISOString(),
|
|
205
|
+
sourceHost: os.hostname(),
|
|
206
|
+
checksum,
|
|
207
|
+
profilesCount: profileCount
|
|
208
|
+
};
|
|
209
|
+
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
|
|
210
|
+
console.log(chalk.green(` \u2714 Successfully pushed config and ${profileCount} profile(s) to sync target.`));
|
|
211
|
+
console.log(` Checksum: ${chalk.dim(checksum)} | Timestamp: ${chalk.dim(manifest.lastUpdated)}
|
|
212
|
+
`);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
if (subcommand === "pull") {
|
|
216
|
+
if (!fs.existsSync(manifestPath) || !fs.existsSync(remoteConfigFile)) {
|
|
217
|
+
console.error(chalk.red(` \u2716 Error: No sync manifest found at ${syncDir}. Run "pyre sync push" first from another machine.
|
|
218
|
+
`));
|
|
219
|
+
process.exit(1);
|
|
220
|
+
}
|
|
221
|
+
if (fs.existsSync(remoteConfigFile)) {
|
|
222
|
+
const remoteConfig = JSON.parse(fs.readFileSync(remoteConfigFile, "utf-8"));
|
|
223
|
+
writeConfig(remoteConfig);
|
|
224
|
+
}
|
|
225
|
+
let pulledCount = 0;
|
|
226
|
+
if (fs.existsSync(remoteProfilesDir)) {
|
|
227
|
+
if (!fs.existsSync(localProfilesDir)) {
|
|
228
|
+
fs.mkdirSync(localProfilesDir, { recursive: true });
|
|
229
|
+
}
|
|
230
|
+
const files = fs.readdirSync(remoteProfilesDir).filter((f) => f.endsWith(".json"));
|
|
231
|
+
pulledCount = files.length;
|
|
232
|
+
for (const f of files) {
|
|
233
|
+
fs.copyFileSync(path.join(remoteProfilesDir, f), path.join(localProfilesDir, f));
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
console.log(chalk.green(` \u2714 Successfully pulled latest configuration and ${pulledCount} profile(s).`));
|
|
237
|
+
console.log(chalk.dim(` Applied to ${localConfigFile}
|
|
238
|
+
`));
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
console.log(chalk.yellow(` Unknown subcommand: "${subcommand}". Available: status, push, pull, firebase
|
|
242
|
+
`));
|
|
243
|
+
}
|
|
244
|
+
export {
|
|
245
|
+
runSyncCommand
|
|
246
|
+
};
|
|
@@ -1,16 +1,16 @@
|
|
|
1
|
+
import {
|
|
2
|
+
collectAll
|
|
3
|
+
} from "./chunk-23YWCI3O.js";
|
|
4
|
+
import "./chunk-M73HCQD2.js";
|
|
5
|
+
import "./chunk-VIACN2RU.js";
|
|
1
6
|
import {
|
|
2
7
|
sendAlert
|
|
3
8
|
} from "./chunk-Z2YV5HKV.js";
|
|
9
|
+
import "./chunk-MZO5QKUR.js";
|
|
4
10
|
import {
|
|
5
11
|
getConfigDir,
|
|
6
12
|
readConfig
|
|
7
|
-
} from "./chunk-
|
|
8
|
-
import {
|
|
9
|
-
collectAll
|
|
10
|
-
} from "./chunk-GHRDW646.js";
|
|
11
|
-
import "./chunk-M73HCQD2.js";
|
|
12
|
-
import "./chunk-VIACN2RU.js";
|
|
13
|
-
import "./chunk-MZO5QKUR.js";
|
|
13
|
+
} from "./chunk-QM5L43EK.js";
|
|
14
14
|
|
|
15
15
|
// src/watchdogCmd.ts
|
|
16
16
|
import fs from "fs";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pyre-cli",
|
|
3
|
-
"version": "10.
|
|
3
|
+
"version": "10.3.0",
|
|
4
4
|
"description": "Cross-platform system monitoring CLI for macOS, Linux, and Windows: temps, system stats, live dashboard, packet monitor, battery predictor, export",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -40,7 +40,8 @@
|
|
|
40
40
|
"license": "MIT",
|
|
41
41
|
"dependencies": {
|
|
42
42
|
"chalk": "^5.3.0",
|
|
43
|
-
"commander": "^11.1.0"
|
|
43
|
+
"commander": "^11.1.0",
|
|
44
|
+
"firebase": "^12.19.0"
|
|
44
45
|
},
|
|
45
46
|
"devDependencies": {
|
|
46
47
|
"@types/node": "^20.10.0",
|
|
@@ -48,4 +49,4 @@
|
|
|
48
49
|
"tsx": "^4.7.0",
|
|
49
50
|
"typescript": "^5.3.3"
|
|
50
51
|
}
|
|
51
|
-
}
|
|
52
|
+
}
|
package/parity.md
CHANGED
|
@@ -527,6 +527,8 @@ pyre provides Mac-native security, diagnostics, and ecosystem capabilities that
|
|
|
527
527
|
* **Kill Confirmation Guard Rails** — Hard confirmation safety check when attempting to terminate protected system PIDs (`launchd`, pyre PID, parent PID).
|
|
528
528
|
* **Accessible Plain-Text Mode** — `--plain`/`--a11y` mode for screen reader compatibility and clean file logging without ANSI noise.
|
|
529
529
|
* **P2P Live Data Streaming & Fleet** — Multi-Mac live streaming with password/TLS auth, HMAC signing, rate-limiting, and IP filtering.
|
|
530
|
+
* **Power User TUI Tools** — Quick-reference overlay (?), fuzzy-search command palette (:), session bookmarks (Shift+M save / M recall), graph zoom ([/]), panel quick-stack (Tab/Shift+Tab), and a footer badge showing active bookmarks, zoom level, and panel history depth.
|
|
531
|
+
* **Live Web Dashboard** — `pyre serve` now serves a Chart.js-based auto-refreshing dashboard at `/` with live CPU, memory, temperature, network, and power sparkline charts.
|
|
530
532
|
|
|
531
533
|
---
|
|
532
534
|
|
package/src/completions.ts
CHANGED
|
@@ -8,6 +8,8 @@ _pyre() {
|
|
|
8
8
|
'info:Display static system overview'
|
|
9
9
|
'doctor:Run system permissions and diagnostic checks'
|
|
10
10
|
'web:Launch web dashboard server'
|
|
11
|
+
'ui:Launch Activity Monitor UI window'
|
|
12
|
+
'launch:Launch Activity Monitor UI window'
|
|
11
13
|
'config:Show or reset configuration options'
|
|
12
14
|
'p2p:P2P streaming server or client'
|
|
13
15
|
'ssh:Stream metrics over SSH'
|
|
@@ -56,38 +58,38 @@ _pyre_completions() {
|
|
|
56
58
|
COMPREPLY=()
|
|
57
59
|
cur="\${COMP_WORDS[COMP_CWORD]}"
|
|
58
60
|
prev="\${COMP_WORDS[COMP_CWORD-1]}"
|
|
59
|
-
commands="live info doctor web config p2p ssh bench completions"
|
|
61
|
+
commands="live info doctor web ui launch config p2p ssh bench completions"
|
|
60
62
|
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
63
|
|
|
62
|
-
case "
|
|
64
|
+
case "${prev}" in
|
|
63
65
|
--theme)
|
|
64
|
-
COMPREPLY=( $(compgen -W "default dracula cyberpunk monochrome nord gruvbox" --
|
|
66
|
+
COMPREPLY=( $(compgen -W "default dracula cyberpunk monochrome nord gruvbox" -- ${cur}) )
|
|
65
67
|
return 0
|
|
66
68
|
;;
|
|
67
69
|
--sort)
|
|
68
|
-
COMPREPLY=( $(compgen -W "cpu mem pid user command state threads runtime" --
|
|
70
|
+
COMPREPLY=( $(compgen -W "cpu mem pid user command state threads runtime" -- ${cur}) )
|
|
69
71
|
return 0
|
|
70
72
|
;;
|
|
71
73
|
--temp-unit)
|
|
72
|
-
COMPREPLY=( $(compgen -W "c f" --
|
|
74
|
+
COMPREPLY=( $(compgen -W "c f" -- ${cur}) )
|
|
73
75
|
return 0
|
|
74
76
|
;;
|
|
75
77
|
config)
|
|
76
|
-
COMPREPLY=( $(compgen -W "show reset" --
|
|
78
|
+
COMPREPLY=( $(compgen -W "show reset" -- ${cur}) )
|
|
77
79
|
return 0
|
|
78
80
|
;;
|
|
79
81
|
p2p)
|
|
80
|
-
COMPREPLY=( $(compgen -W "server connect" --
|
|
82
|
+
COMPREPLY=( $(compgen -W "server connect" -- ${cur}) )
|
|
81
83
|
return 0
|
|
82
84
|
;;
|
|
83
85
|
esac
|
|
84
86
|
|
|
85
|
-
if [[
|
|
86
|
-
COMPREPLY=( $(compgen -W "
|
|
87
|
+
if [[ ${cur} == -* ]] ; then
|
|
88
|
+
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
|
|
87
89
|
return 0
|
|
88
90
|
fi
|
|
89
91
|
|
|
90
|
-
COMPREPLY=( $(compgen -W "
|
|
92
|
+
COMPREPLY=( $(compgen -W "${commands}" -- ${cur}) )
|
|
91
93
|
}
|
|
92
94
|
|
|
93
95
|
complete -F _pyre_completions pyre
|
|
@@ -101,6 +103,8 @@ complete -c pyre -n "__fish_use_subcommand" -a live -d "Start interactive live m
|
|
|
101
103
|
complete -c pyre -n "__fish_use_subcommand" -a info -d "Display static system overview"
|
|
102
104
|
complete -c pyre -n "__fish_use_subcommand" -a doctor -d "Run system diagnostics"
|
|
103
105
|
complete -c pyre -n "__fish_use_subcommand" -a web -d "Launch web dashboard server"
|
|
106
|
+
complete -c pyre -n "__fish_use_subcommand" -a ui -d "Launch Activity Monitor UI window"
|
|
107
|
+
complete -c pyre -n "__fish_use_subcommand" -a launch -d "Launch Activity Monitor UI window"
|
|
104
108
|
complete -c pyre -n "__fish_use_subcommand" -a config -d "Show or reset configuration options"
|
|
105
109
|
complete -c pyre -n "__fish_use_subcommand" -a p2p -d "P2P streaming mode"
|
|
106
110
|
complete -c pyre -n "__fish_use_subcommand" -a completions -d "Generate shell completions"
|
|
@@ -121,7 +125,7 @@ export function generatePowerShellCompletions(): string {
|
|
|
121
125
|
return `# PowerShell completion for pyre
|
|
122
126
|
Register-ArgumentCompleter -Native -CommandName pyre -ScriptBlock {
|
|
123
127
|
param($wordToComplete, $commandAst, $cursorPosition)
|
|
124
|
-
$commands = @('live', 'check', 'pipe', 'stress', 'ui', 'web', 'ssh', 'fleet', 'bench', 'benchmark', 'anomalies', 'doctor', 'extensions', 'brew', 'update', 'profile', 'config', 'history', 'diff', 'info', 'completions', 'p2p', 'blender', 'prometheus', 'smart', 'replay', 'explain')
|
|
128
|
+
$commands = @('live', 'check', 'pipe', 'stress', 'ui', 'launch', 'web', 'ssh', 'fleet', 'bench', 'benchmark', 'anomalies', 'doctor', 'extensions', 'brew', 'update', 'profile', 'config', 'history', 'diff', 'info', 'completions', 'p2p', 'blender', 'prometheus', 'smart', 'replay', 'explain')
|
|
125
129
|
$options = @('--json', '--html', '--md', '--csv', '--tsv', '--detailed', '--theme', '--interval', '--once', '--out', '--export-dir', '--log', '--tree', '--sort', '--packets', '--limit', '--alert-cpu', '--alert-temp', '--temp-unit', '--plain', '--help', '--version')
|
|
126
130
|
|
|
127
131
|
$elements = $commandAst.CommandElements
|
|
@@ -81,3 +81,152 @@ describe('Cross-Platform Support', () => {
|
|
|
81
81
|
});
|
|
82
82
|
});
|
|
83
83
|
|
|
84
|
+
describe('Web and UI Security (LAN exposure & flags)', () => {
|
|
85
|
+
it('should parse --host, --lan, and --no-lan CLI flags in commander help', async () => {
|
|
86
|
+
const { execSync } = await import('node:child_process');
|
|
87
|
+
const help = execSync('node dist/index.js --help', { encoding: 'utf-8' });
|
|
88
|
+
assert.ok(help.includes('--host <host>'));
|
|
89
|
+
assert.ok(help.includes('--lan'));
|
|
90
|
+
assert.ok(help.includes('--no-lan'));
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('should bind to localhost and suppress Network URL when --no-lan is passed', async () => {
|
|
94
|
+
const { spawn } = await import('node:child_process');
|
|
95
|
+
const child = spawn('node', ['dist/index.js', 'web', '--port', '34881', '--no-lan']);
|
|
96
|
+
let output = '';
|
|
97
|
+
await new Promise<void>((resolve) => {
|
|
98
|
+
child.stdout.on('data', (d) => {
|
|
99
|
+
output += d.toString();
|
|
100
|
+
if (output.includes('SSE Stream:')) {
|
|
101
|
+
resolve();
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
child.kill('SIGINT');
|
|
106
|
+
assert.ok(output.includes('Network: Disabled (localhost only for security)'));
|
|
107
|
+
assert.ok(!output.includes('Accessible on same Wi-Fi / LAN'));
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('should expose Network URL when --lan is passed', async () => {
|
|
111
|
+
const { spawn } = await import('node:child_process');
|
|
112
|
+
const child = spawn('node', ['dist/index.js', 'web', '--port', '34882', '--lan']);
|
|
113
|
+
let output = '';
|
|
114
|
+
await new Promise<void>((resolve) => {
|
|
115
|
+
child.stdout.on('data', (d) => {
|
|
116
|
+
output += d.toString();
|
|
117
|
+
if (output.includes('SSE Stream:')) {
|
|
118
|
+
resolve();
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
child.kill('SIGINT');
|
|
123
|
+
assert.ok(output.includes('Accessible on same Wi-Fi / LAN'));
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe('Pyre-UI Logging and Recording Features', () => {
|
|
128
|
+
it('should render HTML dashboard containing Logs tab, session recorder, and export dropdown', async () => {
|
|
129
|
+
const { getDashboardHtml } = await import('./formatters/index.js');
|
|
130
|
+
const html = getDashboardHtml();
|
|
131
|
+
assert.ok(html.includes('data-tab="logs"'));
|
|
132
|
+
assert.ok(html.includes('id="btn-record"'));
|
|
133
|
+
assert.ok(html.includes('id="export-menu"'));
|
|
134
|
+
assert.ok(html.includes('id="logs-view"'));
|
|
135
|
+
assert.ok(html.includes('id="panel-logs"'));
|
|
136
|
+
assert.ok(html.includes('switchTab(\'logs\')'));
|
|
137
|
+
assert.ok(html.includes('toggleWebRecording'));
|
|
138
|
+
assert.ok(html.includes('downloadSnapshot'));
|
|
139
|
+
assert.ok(html.includes('analyzeAndLog'));
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('should register and export all logging REST endpoints and stop cleanly on SIGINT', async () => {
|
|
143
|
+
const fs = await import('node:fs');
|
|
144
|
+
const indexSrc = fs.readFileSync(path.join(process.cwd(), 'src/index.ts'), 'utf-8');
|
|
145
|
+
assert.ok(indexSrc.includes('/api/logging/status'));
|
|
146
|
+
assert.ok(indexSrc.includes('/api/logging/start'));
|
|
147
|
+
assert.ok(indexSrc.includes('/api/logging/stop'));
|
|
148
|
+
assert.ok(indexSrc.includes('/api/logging/history'));
|
|
149
|
+
assert.ok(indexSrc.includes('/api/logging/download'));
|
|
150
|
+
assert.ok(indexSrc.includes('/api/export'));
|
|
151
|
+
assert.ok(indexSrc.includes('stopWebLogging()'));
|
|
152
|
+
|
|
153
|
+
const { spawn } = await import('node:child_process');
|
|
154
|
+
const testPort = '34883';
|
|
155
|
+
const child = spawn('node', ['dist/index.js', 'web', '--port', testPort, '--no-lan']);
|
|
156
|
+
let output = '';
|
|
157
|
+
await new Promise<void>((resolve) => {
|
|
158
|
+
child.stdout.on('data', (d) => {
|
|
159
|
+
output += d.toString();
|
|
160
|
+
if (output.includes('SSE Stream:')) {
|
|
161
|
+
resolve();
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
assert.ok(output.includes(`running on port ${testPort}`));
|
|
166
|
+
child.kill('SIGINT');
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
describe('Pyre Preferences, Autostart Service, and Settings UI', () => {
|
|
171
|
+
it('should query service status and installation without crashing', async () => {
|
|
172
|
+
const { getServiceStatus, isServiceInstalled } = await import('./service.js');
|
|
173
|
+
const status = getServiceStatus();
|
|
174
|
+
assert.strictEqual(typeof status.installed, 'boolean');
|
|
175
|
+
assert.strictEqual(typeof status.platform, 'string');
|
|
176
|
+
assert.strictEqual(typeof status.serviceLabel, 'string');
|
|
177
|
+
assert.ok(status.serviceLabel.includes('pyre'));
|
|
178
|
+
assert.strictEqual(typeof isServiceInstalled(), 'boolean');
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('should render HTML dashboard containing Settings modal, tabs, and controls', async () => {
|
|
182
|
+
const { getDashboardHtml } = await import('./formatters/index.js');
|
|
183
|
+
const html = getDashboardHtml();
|
|
184
|
+
assert.ok(html.includes('id="modal-settings"'));
|
|
185
|
+
assert.ok(html.includes('id="btn-settings"'));
|
|
186
|
+
assert.ok(html.includes('openSettingsModal'));
|
|
187
|
+
assert.ok(html.includes('closeSettingsModal'));
|
|
188
|
+
assert.ok(html.includes('switchSettingsTab'));
|
|
189
|
+
assert.ok(html.includes('id="spane-general"'));
|
|
190
|
+
assert.ok(html.includes('id="spane-autostart"'));
|
|
191
|
+
assert.ok(html.includes('id="spane-alerts"'));
|
|
192
|
+
assert.ok(html.includes('id="spane-appearance"'));
|
|
193
|
+
assert.ok(html.includes('id="spane-diagnostics"'));
|
|
194
|
+
assert.ok(html.includes('toggleAutostartFromUI'));
|
|
195
|
+
assert.ok(html.includes('sendTestAlertFromUI'));
|
|
196
|
+
assert.ok(html.includes('runDoctorFromUI'));
|
|
197
|
+
assert.ok(html.includes('resetConfigFromUI'));
|
|
198
|
+
assert.ok(html.includes('onDensityChange'));
|
|
199
|
+
assert.ok(html.includes('density-compact'));
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('should read, write, and reset configuration with web preferences', async () => {
|
|
203
|
+
const { readConfig, writeConfig, resetConfig } = await import('./state/config.js');
|
|
204
|
+
const initial = readConfig();
|
|
205
|
+
assert.strictEqual(typeof initial.port, 'number');
|
|
206
|
+
assert.strictEqual(typeof initial.tableDensity, 'string');
|
|
207
|
+
|
|
208
|
+
writeConfig({ tableDensity: 'compact', compact: true, port: 8080 });
|
|
209
|
+
const updated = readConfig();
|
|
210
|
+
assert.strictEqual(updated.tableDensity, 'compact');
|
|
211
|
+
assert.strictEqual(updated.compact, true);
|
|
212
|
+
assert.strictEqual(updated.port, 8080);
|
|
213
|
+
|
|
214
|
+
const reset = resetConfig();
|
|
215
|
+
assert.strictEqual(reset.tableDensity, 'standard');
|
|
216
|
+
assert.strictEqual(reset.compact, false);
|
|
217
|
+
assert.strictEqual(reset.port, 3000);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('should register settings REST endpoints in web server', () => {
|
|
221
|
+
const indexSrc = fs.readFileSync(path.join(process.cwd(), 'src/index.ts'), 'utf-8');
|
|
222
|
+
assert.ok(indexSrc.includes('/api/config'));
|
|
223
|
+
assert.ok(indexSrc.includes('/api/config/reset'));
|
|
224
|
+
assert.ok(indexSrc.includes('/api/service/status'));
|
|
225
|
+
assert.ok(indexSrc.includes('/api/service/toggle'));
|
|
226
|
+
assert.ok(indexSrc.includes('/api/alerts/test'));
|
|
227
|
+
assert.ok(indexSrc.includes('/api/doctor'));
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
|