pyre-cli 7.1.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.
package/src/sparkline.ts CHANGED
@@ -8,8 +8,15 @@ const BLOCKS = ' ▂▃▄▅▆▇█';
8
8
  * Unicode block characters to represent the range, making
9
9
  * it suitable for inline terminal graphs.
10
10
  */
11
- export function sparkline(values: number[], opts: { min?: number; max?: number } = {}): string {
11
+ export function sparkline(values: number[], opts: { min?: number; max?: number; plainText?: boolean } = {}): string {
12
12
  if (!values.length) return '';
13
+ if (opts.plainText) {
14
+ const first = values[0];
15
+ const last = values[values.length - 1];
16
+ const diff = last - first;
17
+ const sign = diff >= 0 ? '+' : '';
18
+ return `[${first.toFixed(1)} -> ${last.toFixed(1)} (Δ:${sign}${diff.toFixed(1)})]`;
19
+ }
13
20
  let min = opts.min ?? Infinity;
14
21
  let max = opts.max ?? -Infinity;
15
22
  if (opts.min === undefined || opts.max === undefined) {
@@ -29,6 +36,60 @@ export function sparkline(values: number[], opts: { min?: number; max?: number }
29
36
  .join('');
30
37
  }
31
38
 
39
+ /**
40
+ * Render a multi-row unicode sparkline area graph.
41
+ * Returns an array of strings representing each row from top to bottom.
42
+ */
43
+ export function multiRowBrailleGraph(
44
+ values: number[],
45
+ height: number,
46
+ opts: { min?: number; max?: number } = {}
47
+ ): string[] {
48
+ if (!values.length || height <= 0) return Array(height).fill('');
49
+ let min = opts.min ?? Infinity;
50
+ let max = opts.max ?? -Infinity;
51
+ if (opts.min === undefined || opts.max === undefined) {
52
+ for (const v of values) {
53
+ if (v < min) min = v;
54
+ if (v > max) max = v;
55
+ }
56
+ }
57
+ const range = max - min || 1;
58
+ const levels = height * 4;
59
+
60
+ const dotsCol0 = [0x40, 0x04, 0x02, 0x01]; // bottom to top
61
+ const dotsCol1 = [0x80, 0x20, 0x10, 0x08]; // bottom to top
62
+
63
+ const rows: string[][] = Array.from({ length: height }, () => []);
64
+
65
+ for (let i = 0; i < values.length; i += 2) {
66
+ const v0 = values[i];
67
+ const v1 = i + 1 < values.length ? values[i + 1] : v0;
68
+
69
+ const rawSteps0 = Math.round(((v0 - min) / range) * levels);
70
+ const totalSteps0 = Math.min(levels, Math.max(0, rawSteps0));
71
+
72
+ const rawSteps1 = Math.round(((v1 - min) / range) * levels);
73
+ const totalSteps1 = Math.min(levels, Math.max(0, rawSteps1));
74
+
75
+ for (let r = 0; r < height; r++) {
76
+ const rowFromBottom = height - 1 - r;
77
+ const minForThisRow = rowFromBottom * 4;
78
+
79
+ const stepsInThisRow0 = Math.min(4, Math.max(0, totalSteps0 - minForThisRow));
80
+ const stepsInThisRow1 = Math.min(4, Math.max(0, totalSteps1 - minForThisRow));
81
+
82
+ let code = 0x2800;
83
+ for (let d = 0; d < stepsInThisRow0; d++) code |= dotsCol0[d];
84
+ for (let d = 0; d < stepsInThisRow1; d++) code |= dotsCol1[d];
85
+
86
+ rows[r].push(String.fromCharCode(code));
87
+ }
88
+ }
89
+
90
+ return rows.map(r => r.join(''));
91
+ }
92
+
32
93
  /**
33
94
  * Render a multi-row unicode sparkline area graph.
34
95
  * Returns an array of strings representing each row from top to bottom.
@@ -64,7 +125,7 @@ export function multiRowSparkline(
64
125
  if (stepsInThisRow === 0) {
65
126
  rows[r].push(' ');
66
127
  } else {
67
- rows[r].push(BLOCKS[stepsInThisRow]);
128
+ rows[r].push(BLOCKS[Math.min(7, stepsInThisRow)]);
68
129
  }
69
130
  }
70
131
  }
@@ -26,14 +26,16 @@ export interface PyreConfig {
26
26
  graphMode?: string;
27
27
  showGraphs?: boolean;
28
28
  autoLog?: boolean;
29
- mouseEnabled?: boolean;
29
+ mouseEnabled?: boolean;
30
30
  cpuAlertPct?: number;
31
31
  tempAlertC?: number;
32
+ electricityRateKwH?: number;
32
33
  notificationsEnabled?: boolean;
33
34
  p2pPort?: number;
34
35
  p2pPassword?: string;
35
36
  splashEnabled?: boolean;
36
37
  splashColorScheme?: string;
38
+ splashAnimation?: string;
37
39
  watchdogProcess?: string;
38
40
  watchdogCpu?: number;
39
41
  watchdogMem?: number;
@@ -69,6 +71,7 @@ export const DEFAULT_CONFIG: Required<PyreConfig> = {
69
71
  mouseEnabled: true,
70
72
  cpuAlertPct: 90,
71
73
  tempAlertC: 95,
74
+ electricityRateKwH: 0.15,
72
75
  watchdogProcess: '',
73
76
  watchdogCpu: 80,
74
77
  watchdogMem: 80,
@@ -120,6 +123,37 @@ export function writeConfig(config: Partial<PyreConfig>): void {
120
123
  }
121
124
  }
122
125
 
126
+ const PROFILES_DIR = path.join(CONFIG_DIR, 'profiles');
127
+
128
+ export function saveProfile(name: string): void {
129
+ if (!name || name.includes('/') || name.includes('\\')) {
130
+ throw new Error('Invalid profile name.');
131
+ }
132
+ if (!fs.existsSync(PROFILES_DIR)) {
133
+ fs.mkdirSync(PROFILES_DIR, { recursive: true });
134
+ }
135
+ const currentConfig = readConfig();
136
+ const profileFile = path.join(PROFILES_DIR, `${name}.json`);
137
+ fs.writeFileSync(profileFile, JSON.stringify(currentConfig, null, 2) + '\n');
138
+ }
139
+
140
+ export function loadProfile(name: string): void {
141
+ const profileFile = path.join(PROFILES_DIR, `${name}.json`);
142
+ if (!fs.existsSync(profileFile)) {
143
+ throw new Error(`Profile '${name}' does not exist.`);
144
+ }
145
+ const raw = fs.readFileSync(profileFile, 'utf-8');
146
+ const parsed = JSON.parse(raw);
147
+ // Atomic write to main config
148
+ writeConfig(parsed);
149
+ }
150
+
151
+ export function listProfiles(): string[] {
152
+ if (!fs.existsSync(PROFILES_DIR)) return [];
153
+ const files = fs.readdirSync(PROFILES_DIR);
154
+ return files.filter(f => f.endsWith('.json')).map(f => f.replace(/\.json$/, ''));
155
+ }
156
+
123
157
  function deepMerge(target: any, source: any): any {
124
158
  const result = { ...target };
125
159
  for (const key of Object.keys(source)) {
@@ -138,4 +172,4 @@ export function getConfigPath(): string {
138
172
  return CONFIG_FILE;
139
173
  }
140
174
 
141
- export { CONFIG_FILE };
175
+ export { CONFIG_FILE, PROFILES_DIR };
@@ -0,0 +1,94 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import os from 'node:os';
4
+ import https from 'node:https';
5
+ import chalk from 'chalk';
6
+
7
+ const UPDATE_CACHE_FILE = path.join(os.homedir(), '.config', 'pyre', 'update-check.json');
8
+
9
+ export interface UpdateCheckResult {
10
+ currentVersion: string;
11
+ latestVersion: string;
12
+ hasUpdate: boolean;
13
+ }
14
+
15
+ export function checkForUpdates(currentVersion: string): Promise<UpdateCheckResult | null> {
16
+ return new Promise((resolve) => {
17
+ try {
18
+ if (fs.existsSync(UPDATE_CACHE_FILE)) {
19
+ const raw = fs.readFileSync(UPDATE_CACHE_FILE, 'utf-8');
20
+ const cached = JSON.parse(raw);
21
+ // Check once every 24 hours
22
+ if (Date.now() - (cached.lastChecked || 0) < 24 * 60 * 60 * 1000) {
23
+ return resolve({
24
+ currentVersion,
25
+ latestVersion: cached.latestVersion || currentVersion,
26
+ hasUpdate: isNewerVersion(currentVersion, cached.latestVersion),
27
+ });
28
+ }
29
+ }
30
+ } catch {
31
+ // ignore cache errors
32
+ }
33
+
34
+ const req = https.get('https://registry.npmjs.org/pyre-cli/latest', { timeout: 2000 }, (res) => {
35
+ let body = '';
36
+ res.on('data', chunk => body += chunk);
37
+ res.on('end', () => {
38
+ try {
39
+ if (res.statusCode === 200) {
40
+ const data = JSON.parse(body);
41
+ const latestVersion = data.version || currentVersion;
42
+ fs.mkdirSync(path.dirname(UPDATE_CACHE_FILE), { recursive: true });
43
+ fs.writeFileSync(UPDATE_CACHE_FILE, JSON.stringify({ lastChecked: Date.now(), latestVersion }));
44
+ return resolve({
45
+ currentVersion,
46
+ latestVersion,
47
+ hasUpdate: isNewerVersion(currentVersion, latestVersion),
48
+ });
49
+ }
50
+ } catch {
51
+ // ignore parse errors
52
+ }
53
+ resolve(null);
54
+ });
55
+ });
56
+
57
+ req.on('error', () => resolve(null));
58
+ req.on('timeout', () => {
59
+ req.destroy();
60
+ resolve(null);
61
+ });
62
+ });
63
+ }
64
+
65
+ function isNewerVersion(current: string, latest?: string): boolean {
66
+ if (!latest) return false;
67
+ const cParts = current.split('.').map(n => parseInt(n, 10));
68
+ const lParts = latest.split('.').map(n => parseInt(n, 10));
69
+ for (let i = 0; i < Math.max(cParts.length, lParts.length); i++) {
70
+ const c = cParts[i] || 0;
71
+ const l = lParts[i] || 0;
72
+ if (l > c) return true;
73
+ if (l < c) return false;
74
+ }
75
+ return false;
76
+ }
77
+
78
+ export async function printUpdateReport(currentVersion: string): Promise<void> {
79
+ console.log(chalk.bold('\n pyre update — Version Check\n'));
80
+ console.log(` Current Version: ${chalk.bold(currentVersion)}`);
81
+
82
+ const result = await checkForUpdates(currentVersion);
83
+ if (!result) {
84
+ console.log(chalk.yellow(' ⚠ Unable to reach npm registry for update check.\n'));
85
+ return;
86
+ }
87
+
88
+ if (result.hasUpdate) {
89
+ console.log(chalk.green.bold(` 🚀 A new version of pyre is available: v${result.latestVersion}`));
90
+ console.log(chalk.dim(` Run "npm install -g pyre-cli" to update.\n`));
91
+ } else {
92
+ console.log(chalk.green(` ✔ You are running the latest version of pyre (v${currentVersion}).\n`));
93
+ }
94
+ }