pyre-cli 10.0.0 → 10.2.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 +195 -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 +4243 -735
- 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 +516 -23
- 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
package/src/monitors/types.ts
CHANGED
|
@@ -43,6 +43,14 @@ export interface GpuData {
|
|
|
43
43
|
utilization: number;
|
|
44
44
|
temperature?: number;
|
|
45
45
|
processes: number;
|
|
46
|
+
powerDraw?: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface DisplayInfo {
|
|
50
|
+
name: string;
|
|
51
|
+
resolution: string;
|
|
52
|
+
connectionType?: string;
|
|
53
|
+
isMain?: boolean;
|
|
46
54
|
}
|
|
47
55
|
|
|
48
56
|
export interface PowerData {
|
package/src/serveCmd.ts
CHANGED
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
* pyre serve — Dedicated REST API Mode.
|
|
3
3
|
*
|
|
4
4
|
* Exposes all metrics as structured JSON REST endpoints with optional
|
|
5
|
-
* API key authorization and CORS support.
|
|
5
|
+
* API key authorization and CORS support. Also serves a live Chart.js
|
|
6
|
+
* dashboard at the root path (/).
|
|
6
7
|
*
|
|
7
8
|
* Endpoints:
|
|
9
|
+
* GET / → Live Chart.js web dashboard (auto-refresh every 2s)
|
|
8
10
|
* GET /api/all
|
|
9
11
|
* GET /api/cpu
|
|
10
12
|
* GET /api/memory
|
|
@@ -20,6 +22,7 @@
|
|
|
20
22
|
import http from 'node:http';
|
|
21
23
|
import os from 'node:os';
|
|
22
24
|
import chalk from 'chalk';
|
|
25
|
+
import { getWebDashboardHtml } from './web/dashboard.js';
|
|
23
26
|
import {
|
|
24
27
|
collectAll,
|
|
25
28
|
collectCpu,
|
|
@@ -77,6 +80,15 @@ export async function runServeCommand(opts: ServeOptions = {}): Promise<void> {
|
|
|
77
80
|
let data: any;
|
|
78
81
|
|
|
79
82
|
switch (pathname) {
|
|
83
|
+
case '/':
|
|
84
|
+
case '/dashboard':
|
|
85
|
+
case '/web': {
|
|
86
|
+
const apiBase = `http://${req.headers.host || `localhost:${port}`}/api/all`;
|
|
87
|
+
const html = getWebDashboardHtml(apiBase, apiKey);
|
|
88
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
89
|
+
res.end(html);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
80
92
|
case '/api/all':
|
|
81
93
|
case '/api/stats':
|
|
82
94
|
case '/api':
|
|
@@ -128,6 +140,8 @@ export async function runServeCommand(opts: ServeOptions = {}): Promise<void> {
|
|
|
128
140
|
res.end(JSON.stringify({
|
|
129
141
|
error: 'Not found',
|
|
130
142
|
availableEndpoints: [
|
|
143
|
+
'/',
|
|
144
|
+
'/dashboard',
|
|
131
145
|
'/api/all',
|
|
132
146
|
'/api/cpu',
|
|
133
147
|
'/api/memory',
|
|
@@ -164,8 +178,9 @@ export async function runServeCommand(opts: ServeOptions = {}): Promise<void> {
|
|
|
164
178
|
|
|
165
179
|
await new Promise<void>((resolve) => server.listen(port, '0.0.0.0', resolve));
|
|
166
180
|
|
|
167
|
-
console.log(chalk.bold('\n 🔥 pyre serve — REST API
|
|
181
|
+
console.log(chalk.bold('\n 🔥 pyre serve — REST API + Web Dashboard\n'));
|
|
168
182
|
console.log(` ${chalk.green('✔')} API base URL: ${chalk.cyan(`http://0.0.0.0:${port}/api/all`)}`);
|
|
183
|
+
console.log(` ${chalk.green('✔')} Dashboard: ${chalk.cyan(`http://0.0.0.0:${port}/`)}`);
|
|
169
184
|
console.log(` ${chalk.green('✔')} Health check: ${chalk.cyan(`http://0.0.0.0:${port}/api/health`)}`);
|
|
170
185
|
if (apiKey) {
|
|
171
186
|
console.log(` ${chalk.yellow('🔒')} API Key protection: ${chalk.bold('ENABLED')} (Pass via header x-api-key or ?key=)`);
|
package/src/service.ts
CHANGED
|
@@ -36,6 +36,55 @@ export function uninstallService(): void {
|
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
export interface ServiceStatus {
|
|
40
|
+
installed: boolean;
|
|
41
|
+
platform: string;
|
|
42
|
+
serviceLabel: string;
|
|
43
|
+
path: string;
|
|
44
|
+
logPath: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function isServiceInstalled(): boolean {
|
|
48
|
+
const platform = process.platform;
|
|
49
|
+
if (platform === 'darwin') {
|
|
50
|
+
return fs.existsSync(PLIST_PATH);
|
|
51
|
+
} else if (platform === 'linux') {
|
|
52
|
+
return fs.existsSync(SYSTEMD_UNIT_PATH);
|
|
53
|
+
} else if (platform === 'win32') {
|
|
54
|
+
try {
|
|
55
|
+
execSync(`schtasks /query /tn "${TASK_NAME}"`, { stdio: 'pipe' });
|
|
56
|
+
return true;
|
|
57
|
+
} catch {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function getServiceStatus(): ServiceStatus {
|
|
65
|
+
const platform = process.platform;
|
|
66
|
+
const installed = isServiceInstalled();
|
|
67
|
+
let serviceLabel = SERVICE_LABEL;
|
|
68
|
+
let servicePath = PLIST_PATH;
|
|
69
|
+
const logPath = path.join(os.homedir(), '.config', 'pyre', 'web.log');
|
|
70
|
+
|
|
71
|
+
if (platform === 'linux') {
|
|
72
|
+
serviceLabel = 'pyre-web.service';
|
|
73
|
+
servicePath = SYSTEMD_UNIT_PATH;
|
|
74
|
+
} else if (platform === 'win32') {
|
|
75
|
+
serviceLabel = TASK_NAME;
|
|
76
|
+
servicePath = 'Windows Task Scheduler (onlogon)';
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
installed,
|
|
81
|
+
platform,
|
|
82
|
+
serviceLabel,
|
|
83
|
+
path: servicePath,
|
|
84
|
+
logPath,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
39
88
|
// Backward-compatible exports for existing callers
|
|
40
89
|
export const installLaunchdAgent = installService;
|
|
41
90
|
export const uninstallLaunchdAgent = uninstallService;
|
package/src/state/config.ts
CHANGED
|
@@ -14,6 +14,20 @@ import os from 'node:os';
|
|
|
14
14
|
import path from 'node:path';
|
|
15
15
|
|
|
16
16
|
|
|
17
|
+
export interface PyreBookmark {
|
|
18
|
+
name: string;
|
|
19
|
+
activePanel: string;
|
|
20
|
+
sortMode: string;
|
|
21
|
+
processFilter: string;
|
|
22
|
+
treeView: boolean;
|
|
23
|
+
showGraphs: boolean;
|
|
24
|
+
graphMode: string;
|
|
25
|
+
trackedPid: number | null;
|
|
26
|
+
currentTheme: string;
|
|
27
|
+
panelLayout: string[];
|
|
28
|
+
detailed: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
17
31
|
export interface PyreConfig {
|
|
18
32
|
theme?: string;
|
|
19
33
|
interval?: number;
|
|
@@ -56,6 +70,10 @@ export interface PyreConfig {
|
|
|
56
70
|
/** Optional API key for external cloud providers */
|
|
57
71
|
aiApiKey?: string;
|
|
58
72
|
dockerModeConfirmed?: boolean;
|
|
73
|
+
/** Use Firebase Firestore as the sync backend instead of a local folder */
|
|
74
|
+
firebaseSyncEnabled?: boolean;
|
|
75
|
+
/** Unique host identifier used as the Firestore document ID (defaults to os.hostname()) */
|
|
76
|
+
firebaseHostId?: string;
|
|
59
77
|
visiblePanels?: {
|
|
60
78
|
cpu?: boolean;
|
|
61
79
|
mem?: boolean;
|
|
@@ -73,6 +91,19 @@ export interface PyreConfig {
|
|
|
73
91
|
tree?: boolean;
|
|
74
92
|
};
|
|
75
93
|
panelLayout?: string[];
|
|
94
|
+
/** Saved session bookmarks */
|
|
95
|
+
bookmarks?: PyreBookmark[];
|
|
96
|
+
/** Graph zoom level (1-5, higher = more history points visible) */
|
|
97
|
+
graphZoomLevel?: number;
|
|
98
|
+
/** Web UI preferences */
|
|
99
|
+
compact?: boolean;
|
|
100
|
+
alwaysOnTop?: boolean;
|
|
101
|
+
exposeLan?: boolean;
|
|
102
|
+
port?: number;
|
|
103
|
+
historyDuration?: number;
|
|
104
|
+
tableDensity?: string;
|
|
105
|
+
defaultFilter?: string;
|
|
106
|
+
defaultSort?: string;
|
|
76
107
|
}
|
|
77
108
|
|
|
78
109
|
export const DEFAULT_CONFIG: Required<PyreConfig> = {
|
|
@@ -109,6 +140,8 @@ export const DEFAULT_CONFIG: Required<PyreConfig> = {
|
|
|
109
140
|
aiModel: 'expert-rules-v1',
|
|
110
141
|
aiApiKey: '',
|
|
111
142
|
dockerModeConfirmed: false,
|
|
143
|
+
firebaseSyncEnabled: false,
|
|
144
|
+
firebaseHostId: '',
|
|
112
145
|
visiblePanels: {
|
|
113
146
|
cpu: true,
|
|
114
147
|
mem: true,
|
|
@@ -126,6 +159,16 @@ export const DEFAULT_CONFIG: Required<PyreConfig> = {
|
|
|
126
159
|
tree: false,
|
|
127
160
|
},
|
|
128
161
|
panelLayout: ['mem', 'disk', 'net'],
|
|
162
|
+
bookmarks: [],
|
|
163
|
+
graphZoomLevel: 1,
|
|
164
|
+
compact: false,
|
|
165
|
+
alwaysOnTop: false,
|
|
166
|
+
exposeLan: false,
|
|
167
|
+
port: 3000,
|
|
168
|
+
historyDuration: 60,
|
|
169
|
+
tableDensity: 'standard',
|
|
170
|
+
defaultFilter: 'all',
|
|
171
|
+
defaultSort: 'cpu',
|
|
129
172
|
};
|
|
130
173
|
|
|
131
174
|
export function getConfigDir(): string {
|
|
@@ -220,3 +263,8 @@ export function getConfigPath(): string {
|
|
|
220
263
|
return getConfigFile();
|
|
221
264
|
}
|
|
222
265
|
|
|
266
|
+
export function resetConfig(): Required<PyreConfig> {
|
|
267
|
+
writeConfig(DEFAULT_CONFIG);
|
|
268
|
+
return { ...DEFAULT_CONFIG };
|
|
269
|
+
}
|
|
270
|
+
|
package/src/syncCmd.ts
CHANGED
|
@@ -3,19 +3,27 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Synchronizes configuration profiles (~/.config/pyre/profiles/),
|
|
5
5
|
* alert rules, thresholds, and anomaly history across multiple Macs
|
|
6
|
-
* via iCloud Drive
|
|
6
|
+
* via iCloud Drive / any user-defined sync folder, OR via Firebase Firestore.
|
|
7
7
|
*
|
|
8
8
|
* Commands:
|
|
9
9
|
* pyre sync status
|
|
10
10
|
* pyre sync push
|
|
11
|
-
* pyre sync pull
|
|
11
|
+
* pyre sync pull [hostname]
|
|
12
|
+
* pyre sync firebase <enable|disable|status>
|
|
12
13
|
*/
|
|
13
14
|
import fs from 'node:fs';
|
|
14
15
|
import path from 'node:path';
|
|
15
16
|
import os from 'node:os';
|
|
16
17
|
import crypto from 'node:crypto';
|
|
17
18
|
import chalk from 'chalk';
|
|
18
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
getConfigDir,
|
|
21
|
+
getConfigFile,
|
|
22
|
+
getProfilesDir,
|
|
23
|
+
readConfig,
|
|
24
|
+
writeConfig,
|
|
25
|
+
PyreConfig,
|
|
26
|
+
} from './state/config.js';
|
|
19
27
|
|
|
20
28
|
export interface SyncOptions {
|
|
21
29
|
syncDir?: string;
|
|
@@ -58,7 +66,147 @@ function computeDirChecksum(dirPath: string): string {
|
|
|
58
66
|
return hash.digest('hex').slice(0, 16);
|
|
59
67
|
}
|
|
60
68
|
|
|
69
|
+
// ─── Load profiles from disk ──────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
function loadLocalProfiles(): Record<string, PyreConfig> {
|
|
72
|
+
const profilesDir = getProfilesDir();
|
|
73
|
+
const profiles: Record<string, PyreConfig> = {};
|
|
74
|
+
if (!fs.existsSync(profilesDir)) return profiles;
|
|
75
|
+
for (const f of fs.readdirSync(profilesDir).filter((f) => f.endsWith('.json'))) {
|
|
76
|
+
try {
|
|
77
|
+
const name = f.replace(/\.json$/, '');
|
|
78
|
+
profiles[name] = JSON.parse(fs.readFileSync(path.join(profilesDir, f), 'utf-8'));
|
|
79
|
+
} catch { /* skip corrupted profiles */ }
|
|
80
|
+
}
|
|
81
|
+
return profiles;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function saveLocalProfiles(profiles: Record<string, PyreConfig>): void {
|
|
85
|
+
const profilesDir = getProfilesDir();
|
|
86
|
+
if (!fs.existsSync(profilesDir)) fs.mkdirSync(profilesDir, { recursive: true });
|
|
87
|
+
for (const [name, cfg] of Object.entries(profiles)) {
|
|
88
|
+
fs.writeFileSync(path.join(profilesDir, `${name}.json`), JSON.stringify(cfg, null, 2) + '\n');
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ─── Firebase subcommand ──────────────────────────────────────────────────────
|
|
93
|
+
|
|
94
|
+
async function runFirebaseSubcommand(action: string): Promise<void> {
|
|
95
|
+
const config = readConfig();
|
|
96
|
+
|
|
97
|
+
if (action === 'enable') {
|
|
98
|
+
writeConfig({ firebaseSyncEnabled: true });
|
|
99
|
+
console.log(chalk.green(' ✔ Firebase sync enabled.'));
|
|
100
|
+
console.log(chalk.dim(' Run "pyre sync push" to upload your config and profiles to Firestore.\n'));
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (action === 'disable') {
|
|
105
|
+
writeConfig({ firebaseSyncEnabled: false });
|
|
106
|
+
console.log(chalk.yellow(' ✔ Firebase sync disabled. Falling back to local folder sync.\n'));
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (action === 'status' || !action) {
|
|
111
|
+
console.log(chalk.bold(` Firebase Sync: ${config.firebaseSyncEnabled ? chalk.green('ENABLED') : chalk.dim('DISABLED')}`));
|
|
112
|
+
if (!config.firebaseSyncEnabled) {
|
|
113
|
+
console.log(chalk.dim(' Run "pyre sync firebase enable" to activate.\n'));
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const { firebaseStatus } = await import('./firebaseSync.js');
|
|
117
|
+
const hosts = await firebaseStatus();
|
|
118
|
+
if (hosts.length === 0) {
|
|
119
|
+
console.log(chalk.dim(' No remote hosts found in Firestore. Run "pyre sync push" to upload.\n'));
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
console.log();
|
|
123
|
+
for (const h of hosts) {
|
|
124
|
+
console.log(` ${chalk.bold(h.sourceHost)} (${chalk.cyan(h.hostId)})`);
|
|
125
|
+
console.log(` Last Updated: ${chalk.green(h.lastUpdated)}`);
|
|
126
|
+
console.log(` Profiles: ${chalk.bold(String(h.profilesCount))}`);
|
|
127
|
+
console.log(` Checksum: ${chalk.dim(h.checksum)}\n`);
|
|
128
|
+
}
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
console.log(chalk.yellow(` Unknown firebase action: "${action}". Available: enable, disable, status\n`));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ─── Main export ──────────────────────────────────────────────────────────────
|
|
136
|
+
|
|
61
137
|
export async function runSyncCommand(subcommand: string = 'status', opts: SyncOptions = {}): Promise<void> {
|
|
138
|
+
const config = readConfig();
|
|
139
|
+
|
|
140
|
+
console.log(chalk.bold('\n 🔥 pyre sync — Configuration & Profile Cloud Sync\n'));
|
|
141
|
+
|
|
142
|
+
// ── Firebase subcommand ────────────────────────────────────────────────────
|
|
143
|
+
if (subcommand === 'firebase') {
|
|
144
|
+
const action = process.argv[process.argv.indexOf('firebase') + 1] || 'status';
|
|
145
|
+
await runFirebaseSubcommand(action);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ── Firebase backend ───────────────────────────────────────────────────────
|
|
150
|
+
if (config.firebaseSyncEnabled) {
|
|
151
|
+
console.log(` Backend: ${chalk.cyan('Firebase Firestore')} (${chalk.dim('dearborn-internal-tool')})`);
|
|
152
|
+
const hostId = config.firebaseHostId || undefined;
|
|
153
|
+
|
|
154
|
+
if (subcommand === 'status') {
|
|
155
|
+
const { firebaseStatus } = await import('./firebaseSync.js');
|
|
156
|
+
const hosts = await firebaseStatus();
|
|
157
|
+
if (hosts.length === 0) {
|
|
158
|
+
console.log(chalk.dim(' No remote hosts found. Run "pyre sync push" to upload your config.\n'));
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
console.log();
|
|
162
|
+
for (const h of hosts) {
|
|
163
|
+
console.log(` ${chalk.bold(h.sourceHost)} (${chalk.cyan(h.hostId)})`);
|
|
164
|
+
console.log(` Last Updated: ${chalk.green(h.lastUpdated)}`);
|
|
165
|
+
console.log(` Profiles: ${chalk.bold(String(h.profilesCount))}`);
|
|
166
|
+
console.log(` Checksum: ${chalk.dim(h.checksum)}\n`);
|
|
167
|
+
}
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (subcommand === 'push') {
|
|
172
|
+
const { firebasePush } = await import('./firebaseSync.js');
|
|
173
|
+
const profiles = loadLocalProfiles();
|
|
174
|
+
const result = await firebasePush(config, profiles, hostId);
|
|
175
|
+
const profileCount = Object.keys(profiles).length;
|
|
176
|
+
console.log(chalk.green(` ✔ Successfully pushed config and ${profileCount} profile(s) to Firestore.`));
|
|
177
|
+
console.log(` Host ID: ${chalk.cyan(result.hostId)}`);
|
|
178
|
+
console.log(` Checksum: ${chalk.dim(result.checksum)} | Timestamp: ${chalk.dim(result.lastUpdated)}\n`);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (subcommand === 'pull') {
|
|
183
|
+
// Optional: pyre sync pull <hostname>
|
|
184
|
+
const targetHost = process.argv[process.argv.indexOf('pull') + 1];
|
|
185
|
+
const resolvedTarget = (targetHost && !targetHost.startsWith('-')) ? targetHost : undefined;
|
|
186
|
+
|
|
187
|
+
const { firebasePull } = await import('./firebaseSync.js');
|
|
188
|
+
const result = await firebasePull(resolvedTarget);
|
|
189
|
+
|
|
190
|
+
if (!result) {
|
|
191
|
+
const hint = resolvedTarget ? `host "${resolvedTarget}"` : 'any host';
|
|
192
|
+
console.error(chalk.red(` ✖ No sync data found for ${hint} in Firestore. Run "pyre sync push" from another machine.\n`));
|
|
193
|
+
process.exit(1);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
writeConfig(result.doc.config);
|
|
197
|
+
saveLocalProfiles(result.doc.profiles);
|
|
198
|
+
|
|
199
|
+
const pulledCount = result.doc.profilesCount;
|
|
200
|
+
console.log(chalk.green(` ✔ Successfully pulled config and ${pulledCount} profile(s) from ${chalk.bold(result.doc.sourceHost)}.`));
|
|
201
|
+
console.log(chalk.dim(` Applied to ${getConfigFile()}\n`));
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
console.log(chalk.yellow(` Unknown subcommand: "${subcommand}". Available: status, push, pull, firebase\n`));
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ── Folder-based backend (original behaviour) ──────────────────────────────
|
|
62
210
|
const syncDir = opts.syncDir || getDefaultSyncDir();
|
|
63
211
|
const manifestPath = path.join(syncDir, 'sync-manifest.json');
|
|
64
212
|
const remoteProfilesDir = path.join(syncDir, 'profiles');
|
|
@@ -67,22 +215,22 @@ export async function runSyncCommand(subcommand: string = 'status', opts: SyncOp
|
|
|
67
215
|
const localProfilesDir = getProfilesDir();
|
|
68
216
|
const localConfigFile = getConfigFile();
|
|
69
217
|
|
|
70
|
-
console.log(chalk.
|
|
71
|
-
console.log(` Sync Target
|
|
218
|
+
console.log(` Backend: ${chalk.cyan('Local Folder')}`);
|
|
219
|
+
console.log(` Sync Target: ${chalk.cyan(syncDir)}`);
|
|
72
220
|
|
|
73
221
|
if (subcommand === 'status' || !subcommand) {
|
|
74
222
|
const hasSyncDir = fs.existsSync(syncDir);
|
|
75
223
|
const hasManifest = fs.existsSync(manifestPath);
|
|
76
224
|
|
|
77
|
-
console.log(` Local Profiles:
|
|
78
|
-
console.log(` Sync Target
|
|
225
|
+
console.log(` Local Profiles: ${chalk.bold(String(fs.existsSync(localProfilesDir) ? fs.readdirSync(localProfilesDir).filter(f => f.endsWith('.json')).length : 0))}`);
|
|
226
|
+
console.log(` Sync Target Exists: ${hasSyncDir ? chalk.green('YES') : chalk.yellow('NO (will be created on push)')}`);
|
|
79
227
|
|
|
80
228
|
if (hasManifest) {
|
|
81
229
|
try {
|
|
82
230
|
const manifest: SyncManifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
|
|
83
|
-
console.log(` Remote Last Updated:
|
|
84
|
-
console.log(` Remote Profiles
|
|
85
|
-
console.log(` Remote Checksum:
|
|
231
|
+
console.log(` Remote Last Updated: ${chalk.green(manifest.lastUpdated)} (from ${chalk.bold(manifest.sourceHost)})`);
|
|
232
|
+
console.log(` Remote Profiles: ${chalk.bold(String(manifest.profilesCount))}`);
|
|
233
|
+
console.log(` Remote Checksum: ${chalk.dim(manifest.checksum)}`);
|
|
86
234
|
} catch {
|
|
87
235
|
console.log(chalk.yellow(' Remote manifest corrupted.'));
|
|
88
236
|
}
|
|
@@ -162,5 +310,5 @@ export async function runSyncCommand(subcommand: string = 'status', opts: SyncOp
|
|
|
162
310
|
return;
|
|
163
311
|
}
|
|
164
312
|
|
|
165
|
-
console.log(chalk.yellow(` Unknown subcommand: "${subcommand}". Available: status, push, pull\n`));
|
|
313
|
+
console.log(chalk.yellow(` Unknown subcommand: "${subcommand}". Available: status, push, pull, firebase\n`));
|
|
166
314
|
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pyre web dashboard module.
|
|
3
|
+
*
|
|
4
|
+
* Provides a proper auto-refreshing web dashboard with
|
|
5
|
+
* live Chart.js graphs served over HTTP. Powers `pyre web`.
|
|
6
|
+
*
|
|
7
|
+
* The dashboard polls the pyre REST API every 2 seconds
|
|
8
|
+
* and renders live sparkline-style charts for:
|
|
9
|
+
* - CPU usage %
|
|
10
|
+
* - Memory usage %
|
|
11
|
+
* - Temperature °C
|
|
12
|
+
* - Network RX/TX rate
|
|
13
|
+
* - Power draw (watts)
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export interface WebDashboardOptions {
|
|
17
|
+
port: number;
|
|
18
|
+
apiUrl: string;
|
|
19
|
+
apiKey?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const DASHBOARD_HTML = (apiUrl: string, apiKey?: string): string => {
|
|
23
|
+
const authHeader = apiKey ? `"x-api-key": "${apiKey}",\n ` : '';
|
|
24
|
+
return `<!DOCTYPE html>
|
|
25
|
+
<html lang="en">
|
|
26
|
+
<head>
|
|
27
|
+
<meta charset="UTF-8"/>
|
|
28
|
+
<meta name="viewport" content="width=device-width,initial-scale=1.0"/>
|
|
29
|
+
<title>Pyre — Live Dashboard</title>
|
|
30
|
+
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.4/dist/chart.umd.min.js"></script>
|
|
31
|
+
<style>
|
|
32
|
+
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
|
33
|
+
:root{
|
|
34
|
+
--bg:#0a0a0f;--surface:#141418;--border:rgba(255,255,255,.08);
|
|
35
|
+
--text:#f5f5f7;--muted:#a1a1a6;--accent:#ff5500;
|
|
36
|
+
--cpu:#00d4ff;--mem:#ff9f0a;--gpu:#af52de;--thm:#ff453a;
|
|
37
|
+
--net:#34c759;--pwr:#ffcc02;--bat:#64d2ff;
|
|
38
|
+
--ok:#34c759;--warn:#ff9f0a;--crit:#ff453a;
|
|
39
|
+
}
|
|
40
|
+
html,body{height:100%;background:var(--bg);color:var(--text);
|
|
41
|
+
font-family:-apple-system,BlinkMacSystemFont,"SF Pro Display","SF Pro Text","Helvetica Neue",Helvetica,Arial,sans-serif;
|
|
42
|
+
-webkit-font-smoothing:antialiased;overflow-x:hidden}
|
|
43
|
+
header{position:sticky;top:0;z-index:10;height:48px;background:rgba(10,10,15,.9);
|
|
44
|
+
backdrop-filter:saturate(180%) blur(20px);border-bottom:1px solid var(--border);
|
|
45
|
+
display:flex;align-items:center;padding:0 20px;gap:16px}
|
|
46
|
+
.logo{font-weight:700;font-size:15px;color:var(--accent);letter-spacing:-.01em}
|
|
47
|
+
.hostname{font-size:13px;color:var(--muted)}
|
|
48
|
+
.status{margin-left:auto;display:flex;gap:12px;align-items:center}
|
|
49
|
+
.badge{padding:3px 10px;border-radius:980px;font-size:11px;font-weight:600;font-family:ui-monospace,monospace}
|
|
50
|
+
.badge-live{background:rgba(52,199,89,.15);color:var(--ok);border:1px solid rgba(52,199,89,.3)}
|
|
51
|
+
.badge-err{background:rgba(255,69,58,.15);color:var(--crit);border:1px solid rgba(255,69,58,.3)}
|
|
52
|
+
.refresh{font-size:11px;color:var(--muted);font-family:ui-monospace,monospace}
|
|
53
|
+
main{padding:20px;display:grid;gap:16px;
|
|
54
|
+
grid-template-columns:repeat(auto-fit,minmax(340px,1fr));
|
|
55
|
+
max-width:1600px;margin:0 auto}
|
|
56
|
+
.card{background:var(--surface);border:1px solid var(--border);border-radius:16px;
|
|
57
|
+
padding:16px 18px;display:flex;flex-direction:column;min-height:200px}
|
|
58
|
+
.card-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px}
|
|
59
|
+
.card-title{font-size:13px;font-weight:600;color:var(--muted);letter-spacing:-.01em;text-transform:uppercase}
|
|
60
|
+
.card-value{font-size:24px;font-weight:700;font-family:ui-monospace,monospace;letter-spacing:-.02em}
|
|
61
|
+
.card-sub{font-size:11px;color:var(--muted);margin-top:2px;font-family:ui-monospace,monospace}
|
|
62
|
+
.chart-wrap{flex:1;position:relative;min-height:80px;margin-top:8px}
|
|
63
|
+
canvas{width:100%!important;height:100%!important}
|
|
64
|
+
.summary{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:12px;margin-bottom:16px}
|
|
65
|
+
.stat{background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:12px 14px}
|
|
66
|
+
.stat-label{font-size:11px;color:var(--muted);text-transform:uppercase;font-weight:600;letter-spacing:-.01em}
|
|
67
|
+
.stat-val{font-size:20px;font-weight:700;font-family:ui-monospace,monospace;margin-top:2px}
|
|
68
|
+
.footer{text-align:center;padding:16px;font-size:11px;color:var(--muted)}
|
|
69
|
+
@media(max-width:768px){main{grid-template-columns:1fr}.summary{grid-template-columns:repeat(2,1fr)}}
|
|
70
|
+
</style>
|
|
71
|
+
</head>
|
|
72
|
+
<body>
|
|
73
|
+
<header>
|
|
74
|
+
<div class="logo">🔥 Pyre</div>
|
|
75
|
+
<div class="hostname" id="hostname">loading…</div>
|
|
76
|
+
<div class="status">
|
|
77
|
+
<div class="refresh" id="refresh">refreshing 2s</div>
|
|
78
|
+
<div class="badge badge-live" id="liveBadge">● LIVE</div>
|
|
79
|
+
</div>
|
|
80
|
+
</header>
|
|
81
|
+
<main>
|
|
82
|
+
<div class="summary" id="summary"></div>
|
|
83
|
+
<div class="card">
|
|
84
|
+
<div class="card-header">
|
|
85
|
+
<div class="card-title" style="color:var(--cpu)">CPU Usage %</div>
|
|
86
|
+
<div class="card-value" id="cpuVal" style="color:var(--cpu)">--</div>
|
|
87
|
+
</div>
|
|
88
|
+
<div class="chart-wrap"><canvas id="cpuChart"></canvas></div>
|
|
89
|
+
</div>
|
|
90
|
+
<div class="card">
|
|
91
|
+
<div class="card-header">
|
|
92
|
+
<div class="card-title" style="color:var(--mem)">Memory Usage %</div>
|
|
93
|
+
<div class="card-value" id="memVal" style="color:var(--mem)">--</div>
|
|
94
|
+
</div>
|
|
95
|
+
<div class="chart-wrap"><canvas id="memChart"></canvas></div>
|
|
96
|
+
</div>
|
|
97
|
+
<div class="card">
|
|
98
|
+
<div class="card-header">
|
|
99
|
+
<div class="card-title" style="color:var(--thm)">Temperature °C</div>
|
|
100
|
+
<div class="card-value" id="tempVal" style="color:var(--thm)">--</div>
|
|
101
|
+
</div>
|
|
102
|
+
<div class="chart-wrap"><canvas id="tempChart"></canvas></div>
|
|
103
|
+
</div>
|
|
104
|
+
<div class="card">
|
|
105
|
+
<div class="card-header">
|
|
106
|
+
<div class="card-title" style="color:var(--net)">Network RX/TX MB/s</div>
|
|
107
|
+
<div class="card-value" id="netVal" style="color:var(--net)">--</div>
|
|
108
|
+
</div>
|
|
109
|
+
<div class="chart-wrap"><canvas id="netChart"></canvas></div>
|
|
110
|
+
</div>
|
|
111
|
+
<div class="card">
|
|
112
|
+
<div class="card-header">
|
|
113
|
+
<div class="card-title" style="color:var(--pwr)">Power Draw (W)</div>
|
|
114
|
+
<div class="card-value" id="pwrVal" style="color:var(--pwr)">--</div>
|
|
115
|
+
</div>
|
|
116
|
+
<div class="chart-wrap"><canvas id="pwrChart"></canvas></div>
|
|
117
|
+
</div>
|
|
118
|
+
</main>
|
|
119
|
+
<div class="footer">Pyre live dashboard · auto-refresh every 2s · <span id="lastUpdate">--</span></div>
|
|
120
|
+
<script>
|
|
121
|
+
const MAX_POINTS=60;
|
|
122
|
+
const history={cpu:[],mem:[],temp:[],netRx:[],netTx:[],pwr:[],labels:[]};
|
|
123
|
+
const chartOpts=(color,label,unit='')=>({
|
|
124
|
+
type:'line',data:{labels:history.labels,datasets:[{
|
|
125
|
+
label:label,data:[],borderColor:color,backgroundColor:color+'22',
|
|
126
|
+
borderWidth:2,pointRadius:0,tension:.4,fill:true
|
|
127
|
+
}]},
|
|
128
|
+
options:{responsive:true,maintainAspectRatio:false,animation:false,
|
|
129
|
+
scales:{x:{display:false},y:{beginAtZero:true,grid:{color:'rgba(255,255,255,.05)'},
|
|
130
|
+
ticks:{color:'#a1a1a6',font:{size:10},callback:v=>v+unit}}},
|
|
131
|
+
plugins:{legend:{display:false}},
|
|
132
|
+
interaction:{intersect:false,mode:'index'}}
|
|
133
|
+
});
|
|
134
|
+
const charts={
|
|
135
|
+
cpu:new Chart(document.getElementById('cpuChart'),chartOpts('#00d4ff','CPU %','%')),
|
|
136
|
+
mem:new Chart(document.getElementById('memChart'),chartOpts('#ff9f0a','Memory %','%')),
|
|
137
|
+
temp:new Chart(document.getElementById('tempChart'),chartOpts('#ff453a','Temp','°C')),
|
|
138
|
+
net:new Chart(document.getElementById('netChart'),chartOpts('#34c759','Net','MB/s')),
|
|
139
|
+
pwr:new Chart(document.getElementById('pwrChart'),chartOpts('#ffcc02','Power','W')),
|
|
140
|
+
};
|
|
141
|
+
function fmtTime(d){return d.toLocaleTimeString([],{hour:'2-digit',minute:'2-digit',second:'2-digit'})}
|
|
142
|
+
function fmtBytes(b){if(!b)return'0';if(b<1024)return b+'B';if(b<1048576)return(b/1024).toFixed(1)+'KB';return(b/1048576).toFixed(1)+'MB'}
|
|
143
|
+
const AUTH_HEADERS = { ${authHeader} };
|
|
144
|
+
async function fetchData(){
|
|
145
|
+
try{
|
|
146
|
+
const r=await fetch('${apiUrl}',{headers:AUTH_HEADERS});
|
|
147
|
+
if(!r.ok)throw new Error(r.status);
|
|
148
|
+
const d=await r.json();
|
|
149
|
+
document.getElementById('hostname').textContent=d.header?.hostname||'pyre';
|
|
150
|
+
document.getElementById('lastUpdate').textContent='updated '+fmtTime(new Date());
|
|
151
|
+
const cpu=d.cpu?.usage??0,mem=d.memory?.usagePercent??0;
|
|
152
|
+
const temp=d.cpu?.temperature??d.thermal?.temperatures?.cpu_die??null;
|
|
153
|
+
const rxRate=d.network?.rxRate??(d.network?.rxBytes?0:0);
|
|
154
|
+
const txRate=d.network?.txRate??(d.network?.txBytes?0:0);
|
|
155
|
+
const pwr=d.power?.combinedWatts??d.power?.cpuWatts??0;
|
|
156
|
+
const now=fmtTime(new Date());
|
|
157
|
+
history.labels.push(now);
|
|
158
|
+
history.cpu.push(cpu);history.mem.push(mem);
|
|
159
|
+
history.temp.push(temp);history.netRx.push(rxRate/1048576);history.netTx.push(txRate/1048576);
|
|
160
|
+
history.pwr.push(pwr);
|
|
161
|
+
if(history.labels.length>MAX_POINTS){
|
|
162
|
+
history.labels.shift();history.cpu.shift();history.mem.shift();
|
|
163
|
+
history.temp.shift();history.netRx.shift();history.netTx.shift();history.pwr.shift();
|
|
164
|
+
}
|
|
165
|
+
function updChart(chart,data,color){
|
|
166
|
+
chart.data.labels=history.labels;
|
|
167
|
+
chart.data.datasets[0].data=data;
|
|
168
|
+
chart.update('none');
|
|
169
|
+
}
|
|
170
|
+
updChart(charts.cpu,history.cpu,'#00d4ff');
|
|
171
|
+
updChart(charts.mem,history.mem,'#ff9f0a');
|
|
172
|
+
const tempData=history.temp.map(v=>v===null?null:v);
|
|
173
|
+
updChart(charts.temp,tempData,'#ff453a');
|
|
174
|
+
const netData=history.netRx.map((r,i)=>r+history.netTx[i]);
|
|
175
|
+
updChart(charts.net,netData,'#34c759');
|
|
176
|
+
updChart(charts.pwr,history.pwr,'#ffcc02');
|
|
177
|
+
document.getElementById('cpuVal').textContent=cpu.toFixed(1)+'%';
|
|
178
|
+
document.getElementById('memVal').textContent=mem.toFixed(1)+'%';
|
|
179
|
+
document.getElementById('tempVal').textContent=temp!==null?temp.toFixed(1)+'°C':'--';
|
|
180
|
+
document.getElementById('netVal').textContent=fmtBytes(rxRate+txRate)+'/s';
|
|
181
|
+
document.getElementById('pwrVal').textContent=pwr.toFixed(1)+'W';
|
|
182
|
+
const summary=document.getElementById('summary');
|
|
183
|
+
summary.innerHTML=
|
|
184
|
+
'<div class="stat"><div class="stat-label">CPU</div><div class="stat-val" style="color:var(--cpu)">'+cpu.toFixed(1)+'%</div></div>'+
|
|
185
|
+
'<div class="stat"><div class="stat-label">Memory</div><div class="stat-val" style="color:var(--mem)">'+mem.toFixed(1)+'%</div></div>'+
|
|
186
|
+
'<div class="stat"><div class="stat-label">Temp</div><div class="stat-val" style="color:var(--thm)">'+(temp!==null?temp.toFixed(1)+'°C':'--')+'</div></div>'+
|
|
187
|
+
'<div class="stat"><div class="stat-label">Network</div><div class="stat-val" style="color:var(--net)">'+fmtBytes(d.network?.rxBytes??0)+' ↓</div></div>'+
|
|
188
|
+
'<div class="stat"><div class="stat-label">Power</div><div class="stat-val" style="color:var(--pwr)">'+(pwr>0?pwr.toFixed(1)+'W':'--')+'</div></div>';
|
|
189
|
+
const badge=document.getElementById('liveBadge');
|
|
190
|
+
badge.className='badge badge-live';badge.textContent='● LIVE';
|
|
191
|
+
}catch(e){
|
|
192
|
+
const badge=document.getElementById('liveBadge');
|
|
193
|
+
badge.className='badge badge-err';badge.textContent='⚠ ERR';
|
|
194
|
+
console.error('pyre dashboard fetch error:',e);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
fetchData();setInterval(fetchData,2000);
|
|
198
|
+
</script>
|
|
199
|
+
</body>
|
|
200
|
+
</html>`;
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
export function getWebDashboardHtml(apiUrl = 'http://localhost:8080/api/all', apiKey?: string): string {
|
|
204
|
+
return DASHBOARD_HTML(apiUrl, apiKey);
|
|
205
|
+
}
|