@xpufx/paseo-twofado 0.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.
- package/LICENSE +21 -0
- package/README.md +62 -0
- package/client/approvals.tsx +1525 -0
- package/client/error-boundary.tsx +28 -0
- package/client/vendor/paseo-plugin-helper/command-center.ts +43 -0
- package/client/vendor/paseo-plugin-helper/components/AboutSection.tsx +493 -0
- package/client/vendor/paseo-plugin-helper/components/AttentionBeacon.tsx +250 -0
- package/client/vendor/paseo-plugin-helper/components/Badge.tsx +156 -0
- package/client/vendor/paseo-plugin-helper/components/Button.tsx +178 -0
- package/client/vendor/paseo-plugin-helper/components/Card.tsx +225 -0
- package/client/vendor/paseo-plugin-helper/components/CodeBlock.tsx +196 -0
- package/client/vendor/paseo-plugin-helper/components/Collapsible.tsx +277 -0
- package/client/vendor/paseo-plugin-helper/components/CommandBox.tsx +172 -0
- package/client/vendor/paseo-plugin-helper/components/CopyButton.tsx +180 -0
- package/client/vendor/paseo-plugin-helper/components/DataTable.tsx +200 -0
- package/client/vendor/paseo-plugin-helper/components/EmptyState.tsx +97 -0
- package/client/vendor/paseo-plugin-helper/components/HighlightedText.tsx +70 -0
- package/client/vendor/paseo-plugin-helper/components/InlineButton.tsx +73 -0
- package/client/vendor/paseo-plugin-helper/components/KeyValue.tsx +446 -0
- package/client/vendor/paseo-plugin-helper/components/MetricGauge.tsx +247 -0
- package/client/vendor/paseo-plugin-helper/components/ProgressBar.tsx +117 -0
- package/client/vendor/paseo-plugin-helper/components/Responsive.tsx +53 -0
- package/client/vendor/paseo-plugin-helper/components/SearchInput.tsx +118 -0
- package/client/vendor/paseo-plugin-helper/components/SectionHeader.tsx +80 -0
- package/client/vendor/paseo-plugin-helper/components/Select.tsx +215 -0
- package/client/vendor/paseo-plugin-helper/components/StatusDot.tsx +80 -0
- package/client/vendor/paseo-plugin-helper/components/Tabs.tsx +319 -0
- package/client/vendor/paseo-plugin-helper/components/TextInput.tsx +150 -0
- package/client/vendor/paseo-plugin-helper/components/Toggle.tsx +163 -0
- package/client/vendor/paseo-plugin-helper/components/TruncatedText.tsx +157 -0
- package/client/vendor/paseo-plugin-helper/components/index.ts +25 -0
- package/client/vendor/paseo-plugin-helper/custom-pills.tsx +224 -0
- package/client/vendor/paseo-plugin-helper/forge-icon.tsx +79 -0
- package/client/vendor/paseo-plugin-helper/host.ts +277 -0
- package/client/vendor/paseo-plugin-helper/icon.tsx +39 -0
- package/client/vendor/paseo-plugin-helper/index.ts +28 -0
- package/client/vendor/paseo-plugin-helper/layout/ActionBar.tsx +49 -0
- package/client/vendor/paseo-plugin-helper/layout/FormRow.tsx +103 -0
- package/client/vendor/paseo-plugin-helper/layout/Grid.tsx +65 -0
- package/client/vendor/paseo-plugin-helper/layout/ModalBody.tsx +378 -0
- package/client/vendor/paseo-plugin-helper/layout/ModalContent.tsx +49 -0
- package/client/vendor/paseo-plugin-helper/layout/Row.tsx +39 -0
- package/client/vendor/paseo-plugin-helper/layout/Stack.tsx +39 -0
- package/client/vendor/paseo-plugin-helper/layout/index.ts +7 -0
- package/client/vendor/paseo-plugin-helper/panel.tsx +81 -0
- package/client/vendor/paseo-plugin-helper/pill.tsx +884 -0
- package/client/vendor/paseo-plugin-helper/query-refresh.ts +79 -0
- package/client/vendor/paseo-plugin-helper/query.ts +66 -0
- package/client/vendor/paseo-plugin-helper/settings-screen.tsx +372 -0
- package/client/vendor/paseo-plugin-helper/settings.ts +181 -0
- package/client/vendor/paseo-plugin-helper/shared-settings.ts +46 -0
- package/client/vendor/paseo-plugin-helper/snapshot.ts +68 -0
- package/client/vendor/paseo-plugin-helper/surface.tsx +80 -0
- package/client/vendor/paseo-plugin-helper/theme/color-utils.ts +118 -0
- package/client/vendor/paseo-plugin-helper/theme/flair.ts +76 -0
- package/client/vendor/paseo-plugin-helper/theme/host-variables.ts +121 -0
- package/client/vendor/paseo-plugin-helper/theme/index.ts +7 -0
- package/client/vendor/paseo-plugin-helper/theme/provider.tsx +214 -0
- package/client/vendor/paseo-plugin-helper/theme/responsive.ts +213 -0
- package/client/vendor/paseo-plugin-helper/theme/tokens.ts +161 -0
- package/client/vendor/paseo-plugin-helper/theme/useResponsive.ts +57 -0
- package/client/vendor/paseo-plugin-helper/utils/clipboard.ts +149 -0
- package/client/vendor/paseo-plugin-helper/utils/haptics.ts +34 -0
- package/package.json +36 -0
- package/paseo-plugin.json +6 -0
- package/server/twofado.ts +536 -0
- package/server/vendor/paseo-plugin-helper/agent.ts +85 -0
- package/server/vendor/paseo-plugin-helper/custom-pills.ts +344 -0
- package/server/vendor/paseo-plugin-helper/index.ts +18 -0
- package/server/vendor/paseo-plugin-helper/jsonc.ts +78 -0
- package/server/vendor/paseo-plugin-helper/logger.ts +210 -0
- package/server/vendor/paseo-plugin-helper/mcp-config.ts +367 -0
- package/server/vendor/paseo-plugin-helper/mcp-injection.ts +85 -0
- package/server/vendor/paseo-plugin-helper/network.ts +91 -0
- package/server/vendor/paseo-plugin-helper/plugins.ts +160 -0
- package/server/vendor/paseo-plugin-helper/process.ts +186 -0
- package/server/vendor/paseo-plugin-helper/redact.ts +86 -0
- package/server/vendor/paseo-plugin-helper/rpc-guard.ts +77 -0
- package/server/vendor/paseo-plugin-helper/settings.ts +97 -0
- package/server/vendor/paseo-plugin-helper/shared-settings.ts +243 -0
- package/server/vendor/paseo-plugin-helper/storage.ts +244 -0
- package/server/vendor/paseo-plugin-helper/system.ts +128 -0
- package/server/vendor/paseo-plugin-helper/task.ts +116 -0
- package/server/vendor/paseo-plugin-helper/version.ts +153 -0
- package/server/vendor/paseo-plugin-helper/workspace-beacon.ts +418 -0
- package/shared/approval.ts +254 -0
- package/shared/vendor/paseo-plugin-helper/async.ts +35 -0
- package/shared/vendor/paseo-plugin-helper/custom-pills.ts +169 -0
- package/shared/vendor/paseo-plugin-helper/forge.ts +110 -0
- package/shared/vendor/paseo-plugin-helper/formatters.ts +271 -0
- package/shared/vendor/paseo-plugin-helper/highlight.ts +184 -0
- package/shared/vendor/paseo-plugin-helper/index.ts +10 -0
- package/shared/vendor/paseo-plugin-helper/rpc.ts +72 -0
- package/shared/vendor/paseo-plugin-helper/settings.ts +138 -0
- package/shared/vendor/paseo-plugin-helper/suite-settings.ts +17 -0
- package/shared/vendor/paseo-plugin-helper/suppressed.ts +31 -0
- package/shared/vendor/paseo-plugin-helper/types.ts +36 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import type { ZodType } from "zod";
|
|
5
|
+
|
|
6
|
+
export interface PluginStorageOptions<T> {
|
|
7
|
+
defaultData?: T;
|
|
8
|
+
/**
|
|
9
|
+
* Base directory override. When provided, storage is located at path.join(baseDir, pluginId).
|
|
10
|
+
* When omitted, defaults to path.join(os.homedir(), ".paseo", namespace ?? "plugin-data/xpufx", pluginId).
|
|
11
|
+
*/
|
|
12
|
+
baseDir?: string;
|
|
13
|
+
/**
|
|
14
|
+
* Namespace directory under ~/.paseo. Defaults to "plugin-data/xpufx".
|
|
15
|
+
*/
|
|
16
|
+
namespace?: string;
|
|
17
|
+
/**
|
|
18
|
+
* Optional Zod schema to validate and parse data on read/write, automatically applying defaults.
|
|
19
|
+
*/
|
|
20
|
+
schema?: ZodType<T>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface StorageStats {
|
|
24
|
+
path: string;
|
|
25
|
+
fileCount: number;
|
|
26
|
+
totalBytes: number;
|
|
27
|
+
lastModified: Date | null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const DEFAULT_NAMESPACE_README = `# Paseo Plugins Storage (xpufx)
|
|
31
|
+
|
|
32
|
+
This directory is managed by \`paseo-plugin-helper\` to store persistent settings, cached metrics, and state for plugins.
|
|
33
|
+
- Safe to inspect or backup.
|
|
34
|
+
- Avoid editing files manually while the Paseo daemon is active.
|
|
35
|
+
`;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Scoped, atomic filesystem-backed document storage for Paseo daemon plugins.
|
|
39
|
+
* Automatically handles directory creation, atomic temporary file swaps,
|
|
40
|
+
* schema validation, default state fallback, and isolated namespace auditing.
|
|
41
|
+
* State lives under ~/.paseo/plugin-data/xpufx/<pluginId>/ by default.
|
|
42
|
+
*/
|
|
43
|
+
export class PluginStorage<T extends Record<string, any>> {
|
|
44
|
+
readonly pluginId: string;
|
|
45
|
+
readonly filename: string;
|
|
46
|
+
readonly pluginDir: string;
|
|
47
|
+
readonly filePath: string;
|
|
48
|
+
readonly namespaceDir: string | null;
|
|
49
|
+
readonly defaultData?: T;
|
|
50
|
+
readonly schema?: ZodType<T>;
|
|
51
|
+
|
|
52
|
+
constructor(pluginId: string, filename = "state.json", options: PluginStorageOptions<T> = {}) {
|
|
53
|
+
this.pluginId = pluginId;
|
|
54
|
+
this.filename = filename;
|
|
55
|
+
this.defaultData = options.defaultData;
|
|
56
|
+
this.schema = options.schema;
|
|
57
|
+
|
|
58
|
+
const namespace = options.namespace ?? "plugin-data/xpufx";
|
|
59
|
+
if (options.baseDir) {
|
|
60
|
+
this.namespaceDir = options.baseDir;
|
|
61
|
+
this.pluginDir = path.join(options.baseDir, pluginId);
|
|
62
|
+
} else {
|
|
63
|
+
this.namespaceDir = path.join(os.homedir(), ".paseo", namespace);
|
|
64
|
+
this.pluginDir = path.join(this.namespaceDir, pluginId);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
this.filePath = path.join(this.pluginDir, filename);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private ensureDir(): void {
|
|
71
|
+
if (this.namespaceDir && !fs.existsSync(this.namespaceDir)) {
|
|
72
|
+
fs.mkdirSync(this.namespaceDir, { recursive: true });
|
|
73
|
+
}
|
|
74
|
+
if (this.namespaceDir) {
|
|
75
|
+
const readmePath = path.join(this.namespaceDir, "README.md");
|
|
76
|
+
if (!fs.existsSync(readmePath)) {
|
|
77
|
+
try {
|
|
78
|
+
fs.writeFileSync(readmePath, DEFAULT_NAMESPACE_README, "utf8");
|
|
79
|
+
} catch {
|
|
80
|
+
// Non-fatal if filesystem is read-only
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const dir = path.dirname(this.filePath);
|
|
85
|
+
if (!fs.existsSync(dir)) {
|
|
86
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private getDefault(): T {
|
|
91
|
+
if (this.schema) {
|
|
92
|
+
const result = this.schema.safeParse(this.defaultData ?? {});
|
|
93
|
+
if (result.success) {
|
|
94
|
+
return result.data;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return this.defaultData ? (JSON.parse(JSON.stringify(this.defaultData)) as T) : ({} as T);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private parseData(raw: unknown): T {
|
|
101
|
+
if (this.schema) {
|
|
102
|
+
const result = this.schema.safeParse(raw);
|
|
103
|
+
if (result.success) {
|
|
104
|
+
return result.data;
|
|
105
|
+
}
|
|
106
|
+
return this.getDefault();
|
|
107
|
+
}
|
|
108
|
+
return raw as T;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Checks if the backing state file exists.
|
|
113
|
+
*/
|
|
114
|
+
exists(): boolean {
|
|
115
|
+
return fs.existsSync(this.filePath);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Reads data synchronously. If file does not exist, returns defaultData.
|
|
120
|
+
*/
|
|
121
|
+
read(): T {
|
|
122
|
+
try {
|
|
123
|
+
if (!fs.existsSync(this.filePath)) {
|
|
124
|
+
return this.getDefault();
|
|
125
|
+
}
|
|
126
|
+
const raw = fs.readFileSync(this.filePath, "utf8");
|
|
127
|
+
return this.parseData(JSON.parse(raw));
|
|
128
|
+
} catch {
|
|
129
|
+
return this.getDefault();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Reads data asynchronously.
|
|
135
|
+
*/
|
|
136
|
+
async readAsync(): Promise<T> {
|
|
137
|
+
try {
|
|
138
|
+
if (!fs.existsSync(this.filePath)) {
|
|
139
|
+
return this.getDefault();
|
|
140
|
+
}
|
|
141
|
+
const raw = await fs.promises.readFile(this.filePath, "utf8");
|
|
142
|
+
return this.parseData(JSON.parse(raw));
|
|
143
|
+
} catch {
|
|
144
|
+
return this.getDefault();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Writes data atomically using a temporary file and atomic rename.
|
|
150
|
+
*/
|
|
151
|
+
write(data: T): void {
|
|
152
|
+
const validated = this.parseData(data);
|
|
153
|
+
this.ensureDir();
|
|
154
|
+
const tempPath = `${this.filePath}.tmp.${process.pid}.${Date.now()}`;
|
|
155
|
+
const serialized = JSON.stringify(validated, null, 2);
|
|
156
|
+
fs.writeFileSync(tempPath, serialized, "utf8");
|
|
157
|
+
fs.renameSync(tempPath, this.filePath);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Writes data atomically using async filesystem operations.
|
|
162
|
+
*/
|
|
163
|
+
async writeAsync(data: T): Promise<void> {
|
|
164
|
+
const validated = this.parseData(data);
|
|
165
|
+
this.ensureDir();
|
|
166
|
+
const tempPath = `${this.filePath}.tmp.${process.pid}.${Date.now()}`;
|
|
167
|
+
const serialized = JSON.stringify(validated, null, 2);
|
|
168
|
+
await fs.promises.writeFile(tempPath, serialized, "utf8");
|
|
169
|
+
await fs.promises.rename(tempPath, this.filePath);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Updates state synchronously using an updater function.
|
|
174
|
+
*/
|
|
175
|
+
update(updater: (prev: T) => T): T {
|
|
176
|
+
const current = this.read();
|
|
177
|
+
const updated = updater(current);
|
|
178
|
+
const validated = this.parseData(updated);
|
|
179
|
+
this.write(validated);
|
|
180
|
+
return validated;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Updates state asynchronously using an updater function.
|
|
185
|
+
*/
|
|
186
|
+
async updateAsync(updater: (prev: T) => Promise<T> | T): Promise<T> {
|
|
187
|
+
const current = await this.readAsync();
|
|
188
|
+
const updated = await updater(current);
|
|
189
|
+
const validated = this.parseData(updated);
|
|
190
|
+
await this.writeAsync(validated);
|
|
191
|
+
return validated;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Removes the state file if it exists.
|
|
196
|
+
*/
|
|
197
|
+
reset(): void {
|
|
198
|
+
if (fs.existsSync(this.filePath)) {
|
|
199
|
+
try {
|
|
200
|
+
fs.unlinkSync(this.filePath);
|
|
201
|
+
} catch {}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Audits storage consumption, returning path, fileCount, totalBytes, and lastModified.
|
|
207
|
+
*/
|
|
208
|
+
async getStorageStats(): Promise<StorageStats> {
|
|
209
|
+
const stats: StorageStats = {
|
|
210
|
+
path: this.pluginDir,
|
|
211
|
+
fileCount: 0,
|
|
212
|
+
totalBytes: 0,
|
|
213
|
+
lastModified: null,
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
if (!fs.existsSync(this.pluginDir)) {
|
|
217
|
+
return stats;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
try {
|
|
221
|
+
const entries = await fs.promises.readdir(this.pluginDir, { withFileTypes: true });
|
|
222
|
+
for (const entry of entries) {
|
|
223
|
+
if (entry.isFile()) {
|
|
224
|
+
stats.fileCount++;
|
|
225
|
+
const target = path.join(this.pluginDir, entry.name);
|
|
226
|
+
const st = await fs.promises.stat(target);
|
|
227
|
+
stats.totalBytes += st.size;
|
|
228
|
+
if (!stats.lastModified || st.mtime > stats.lastModified) {
|
|
229
|
+
stats.lastModified = st.mtime;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
} catch {}
|
|
234
|
+
|
|
235
|
+
return stats;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Alias for getStorageStats()
|
|
240
|
+
*/
|
|
241
|
+
async getStats(): Promise<StorageStats> {
|
|
242
|
+
return this.getStorageStats();
|
|
243
|
+
}
|
|
244
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
|
|
3
|
+
export interface CpuCoreMetrics {
|
|
4
|
+
model: string;
|
|
5
|
+
speed: number;
|
|
6
|
+
usagePercent: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface SystemMetrics {
|
|
10
|
+
hostname: string;
|
|
11
|
+
platform: NodeJS.Platform;
|
|
12
|
+
arch: string;
|
|
13
|
+
uptimeSeconds: number;
|
|
14
|
+
cpu: {
|
|
15
|
+
model: string;
|
|
16
|
+
cores: number;
|
|
17
|
+
usagePercent: number;
|
|
18
|
+
loadAverage: [number, number, number];
|
|
19
|
+
};
|
|
20
|
+
memory: {
|
|
21
|
+
totalBytes: number;
|
|
22
|
+
freeBytes: number;
|
|
23
|
+
usedBytes: number;
|
|
24
|
+
usedPercent: number;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface CpuTickSample {
|
|
29
|
+
idle: number;
|
|
30
|
+
total: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Samples CPU ticks and calculates delta usage percentage over time.
|
|
35
|
+
*/
|
|
36
|
+
export class CpuSampler {
|
|
37
|
+
private lastSample: CpuTickSample[] | null = null;
|
|
38
|
+
|
|
39
|
+
constructor() {
|
|
40
|
+
this.lastSample = this.getTicks();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
private getTicks(): CpuTickSample[] {
|
|
44
|
+
const cpus = os.cpus();
|
|
45
|
+
return cpus.map((cpu) => {
|
|
46
|
+
const times = cpu.times;
|
|
47
|
+
const total =
|
|
48
|
+
times.user + times.nice + times.sys + times.idle + times.irq;
|
|
49
|
+
return { idle: times.idle, total };
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Computes the CPU usage delta since the previous sample (or between two samples).
|
|
55
|
+
* Returns average usage percentage (0 - 100) and per-core breakdown.
|
|
56
|
+
*/
|
|
57
|
+
sample(): { usagePercent: number; perCore: number[] } {
|
|
58
|
+
const current = this.getTicks();
|
|
59
|
+
if (!this.lastSample || this.lastSample.length !== current.length) {
|
|
60
|
+
this.lastSample = current;
|
|
61
|
+
return { usagePercent: 0, perCore: current.map(() => 0) };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const perCore: number[] = [];
|
|
65
|
+
let totalActive = 0;
|
|
66
|
+
let totalTime = 0;
|
|
67
|
+
|
|
68
|
+
for (let i = 0; i < current.length; i++) {
|
|
69
|
+
const prev = this.lastSample[i];
|
|
70
|
+
const cur = current[i];
|
|
71
|
+
|
|
72
|
+
const deltaTotal = cur.total - prev.total;
|
|
73
|
+
const deltaIdle = cur.idle - prev.idle;
|
|
74
|
+
const deltaActive = Math.max(0, deltaTotal - deltaIdle);
|
|
75
|
+
|
|
76
|
+
const corePercent =
|
|
77
|
+
deltaTotal > 0 ? (deltaActive / deltaTotal) * 100 : 0;
|
|
78
|
+
perCore.push(Math.round(corePercent * 10) / 10);
|
|
79
|
+
|
|
80
|
+
totalActive += deltaActive;
|
|
81
|
+
totalTime += deltaTotal;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
this.lastSample = current;
|
|
85
|
+
|
|
86
|
+
const usagePercent =
|
|
87
|
+
totalTime > 0 ? (totalActive / totalTime) * 100 : 0;
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
usagePercent: Math.round(usagePercent * 10) / 10,
|
|
91
|
+
perCore,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const defaultSampler = new CpuSampler();
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Returns instant system resources and metrics (CPU, RAM, load averages, uptime).
|
|
100
|
+
*/
|
|
101
|
+
export function getSystemMetrics(sampler: CpuSampler = defaultSampler): SystemMetrics {
|
|
102
|
+
const cpus = os.cpus();
|
|
103
|
+
const totalmem = os.totalmem();
|
|
104
|
+
const freemem = os.freemem();
|
|
105
|
+
const usedmem = totalmem - freemem;
|
|
106
|
+
const cpuModel = cpus[0]?.model || "Unknown CPU";
|
|
107
|
+
|
|
108
|
+
const { usagePercent } = sampler.sample();
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
hostname: os.hostname(),
|
|
112
|
+
platform: process.platform,
|
|
113
|
+
arch: process.arch,
|
|
114
|
+
uptimeSeconds: Math.floor(os.uptime()),
|
|
115
|
+
cpu: {
|
|
116
|
+
model: cpuModel,
|
|
117
|
+
cores: cpus.length,
|
|
118
|
+
usagePercent,
|
|
119
|
+
loadAverage: os.loadavg() as [number, number, number],
|
|
120
|
+
},
|
|
121
|
+
memory: {
|
|
122
|
+
totalBytes: totalmem,
|
|
123
|
+
freeBytes: freemem,
|
|
124
|
+
usedBytes: usedmem,
|
|
125
|
+
usedPercent: Math.round((usedmem / totalmem) * 1000) / 10,
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { createPluginLogger, type PluginLogger } from "./logger";
|
|
2
|
+
|
|
3
|
+
export interface PeriodicTaskOptions {
|
|
4
|
+
intervalMs: number;
|
|
5
|
+
task: () => Promise<void> | void;
|
|
6
|
+
onError?: (err: unknown, failureCount: number) => void;
|
|
7
|
+
runImmediately?: boolean;
|
|
8
|
+
maxBackoffMs?: number;
|
|
9
|
+
/**
|
|
10
|
+
* Logger for suppressed failures. Defaults to a level-gated
|
|
11
|
+
* `periodic-task` logger so debug lines stay silent unless debug is enabled.
|
|
12
|
+
*/
|
|
13
|
+
logger?: PluginLogger;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface PeriodicTaskHandle {
|
|
17
|
+
stop: () => void;
|
|
18
|
+
isRunning: () => boolean;
|
|
19
|
+
triggerNow: () => Promise<void>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Creates a resilient periodic background task loop for Paseo plugin daemons.
|
|
24
|
+
* Implements exponential backoff on consecutive failures and safe teardown hooks.
|
|
25
|
+
*/
|
|
26
|
+
export function createPeriodicTask(options: PeriodicTaskOptions): PeriodicTaskHandle {
|
|
27
|
+
const {
|
|
28
|
+
intervalMs,
|
|
29
|
+
task,
|
|
30
|
+
onError,
|
|
31
|
+
runImmediately = false,
|
|
32
|
+
maxBackoffMs = 60000,
|
|
33
|
+
} = options;
|
|
34
|
+
|
|
35
|
+
let timer: any = null;
|
|
36
|
+
let running = true;
|
|
37
|
+
let inFlight = false;
|
|
38
|
+
let failureCount = 0;
|
|
39
|
+
|
|
40
|
+
let taskLogger: PluginLogger | undefined;
|
|
41
|
+
function taskLog(): PluginLogger {
|
|
42
|
+
taskLogger ??= options.logger
|
|
43
|
+
? options.logger.child("periodic-task")
|
|
44
|
+
: createPluginLogger("periodic-task", { banner: false });
|
|
45
|
+
return taskLogger;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function execute() {
|
|
49
|
+
if (!running || inFlight) return;
|
|
50
|
+
inFlight = true;
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
await task();
|
|
54
|
+
failureCount = 0;
|
|
55
|
+
} catch (err) {
|
|
56
|
+
failureCount++;
|
|
57
|
+
if (onError) {
|
|
58
|
+
try {
|
|
59
|
+
onError(err, failureCount);
|
|
60
|
+
} catch (suppressed) {
|
|
61
|
+
// Prevent onError handler from breaking task loop, but stay visible in dev logs.
|
|
62
|
+
taskLog().debug(
|
|
63
|
+
`onError handler failed: ${suppressed instanceof Error ? suppressed.message : String(suppressed)}`,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
} else {
|
|
67
|
+
taskLog().debug(
|
|
68
|
+
`suppressed error (failure ${failureCount}): ${err instanceof Error ? err.message : String(err)}`,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
} finally {
|
|
72
|
+
inFlight = false;
|
|
73
|
+
if (running) {
|
|
74
|
+
scheduleNext();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function scheduleNext() {
|
|
80
|
+
if (!running) return;
|
|
81
|
+
if (timer) clearTimeout(timer);
|
|
82
|
+
|
|
83
|
+
let delay = intervalMs;
|
|
84
|
+
if (failureCount > 0) {
|
|
85
|
+
// Exponential backoff with ceiling
|
|
86
|
+
const backoff = intervalMs * Math.pow(1.5, Math.min(failureCount, 8));
|
|
87
|
+
delay = Math.min(backoff, maxBackoffMs);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
timer = setTimeout(execute, delay);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (runImmediately) {
|
|
94
|
+
execute();
|
|
95
|
+
} else {
|
|
96
|
+
scheduleNext();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
stop: () => {
|
|
101
|
+
running = false;
|
|
102
|
+
if (timer) {
|
|
103
|
+
clearTimeout(timer);
|
|
104
|
+
timer = null;
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
isRunning: () => running,
|
|
108
|
+
triggerNow: async () => {
|
|
109
|
+
if (timer) {
|
|
110
|
+
clearTimeout(timer);
|
|
111
|
+
timer = null;
|
|
112
|
+
}
|
|
113
|
+
await execute();
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { execSync } from "node:child_process";
|
|
4
|
+
|
|
5
|
+
export interface ResolveVersionOptions {
|
|
6
|
+
/**
|
|
7
|
+
* Root directory of the plugin (defaults to `process.cwd()`).
|
|
8
|
+
*/
|
|
9
|
+
cwd?: string;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Whether to augment development versions with git commit short hash
|
|
13
|
+
* (e.g. "0.1.0-dev.bfe901a" if not an exact tag).
|
|
14
|
+
* Defaults to `true`.
|
|
15
|
+
*/
|
|
16
|
+
includeGit?: boolean;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Fallback string if neither package.json nor git metadata can be found.
|
|
20
|
+
* Defaults to "0.0.0".
|
|
21
|
+
*/
|
|
22
|
+
fallback?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Finds the nearest package.json starting from directory and moving up.
|
|
27
|
+
*/
|
|
28
|
+
function findPackageJson(startDir: string): { version?: string; name?: string } | null {
|
|
29
|
+
let current = path.resolve(startDir);
|
|
30
|
+
const root = path.parse(current).root;
|
|
31
|
+
|
|
32
|
+
while (current !== root) {
|
|
33
|
+
const pkgPath = path.join(current, "package.json");
|
|
34
|
+
if (fs.existsSync(pkgPath)) {
|
|
35
|
+
try {
|
|
36
|
+
const raw = fs.readFileSync(pkgPath, "utf8");
|
|
37
|
+
return JSON.parse(raw);
|
|
38
|
+
} catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const parent = path.dirname(current);
|
|
43
|
+
if (parent === current) break;
|
|
44
|
+
current = parent;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Resolves the plugin version from `package.json`, augmented by Git tag / commit hash.
|
|
52
|
+
*
|
|
53
|
+
* Precedence:
|
|
54
|
+
* 1. Exact Git tag on current commit (e.g. "v0.1.2" or "0.1.2")
|
|
55
|
+
* 2. `package.json` version (augmented with "-dev.hash" if in git and not on an exact release tag)
|
|
56
|
+
* 3. Git commit short hash fallback (e.g. "dev-bfe901a")
|
|
57
|
+
* 4. Fallback string (defaults to "0.0.0")
|
|
58
|
+
*/
|
|
59
|
+
export function resolvePluginVersion(options: ResolveVersionOptions = {}): string {
|
|
60
|
+
const {
|
|
61
|
+
cwd = process.cwd(),
|
|
62
|
+
includeGit = true,
|
|
63
|
+
fallback = "0.0.0",
|
|
64
|
+
} = options;
|
|
65
|
+
|
|
66
|
+
let pkgVersion: string | undefined;
|
|
67
|
+
|
|
68
|
+
// 1. Read package.json
|
|
69
|
+
const pkg = findPackageJson(cwd);
|
|
70
|
+
if (pkg?.version) {
|
|
71
|
+
pkgVersion = pkg.version;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (!includeGit) {
|
|
75
|
+
return pkgVersion || fallback;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// 2. Check if git is available and on an exact tag
|
|
79
|
+
try {
|
|
80
|
+
const gitTag = execSync("git describe --tags --exact-match 2>/dev/null", {
|
|
81
|
+
cwd,
|
|
82
|
+
encoding: "utf8",
|
|
83
|
+
timeout: 1000,
|
|
84
|
+
}).trim();
|
|
85
|
+
|
|
86
|
+
if (gitTag) {
|
|
87
|
+
// Strip leading 'v' if present for clean semantic format (e.g. "v0.1.2" -> "0.1.2")
|
|
88
|
+
return gitTag.startsWith("v") ? gitTag.slice(1) : gitTag;
|
|
89
|
+
}
|
|
90
|
+
} catch {
|
|
91
|
+
// Not on an exact tag or git not installed
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 3. If package.json has a version, check if we should add git short commit
|
|
95
|
+
try {
|
|
96
|
+
const gitHash = execSync("git rev-parse --short HEAD 2>/dev/null", {
|
|
97
|
+
cwd,
|
|
98
|
+
encoding: "utf8",
|
|
99
|
+
timeout: 1000,
|
|
100
|
+
}).trim();
|
|
101
|
+
|
|
102
|
+
if (gitHash) {
|
|
103
|
+
if (pkgVersion) {
|
|
104
|
+
return `${pkgVersion}+${gitHash}`;
|
|
105
|
+
}
|
|
106
|
+
return `0.0.0+${gitHash}`;
|
|
107
|
+
}
|
|
108
|
+
} catch {
|
|
109
|
+
// Not a git repository
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return pkgVersion || fallback;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface StampVersionOptions extends ResolveVersionOptions {
|
|
116
|
+
/**
|
|
117
|
+
* Destination TypeScript / JavaScript file path.
|
|
118
|
+
* Defaults to "./version.ts" or "./src/version.ts".
|
|
119
|
+
*/
|
|
120
|
+
targetFile?: string;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Build-time utility to stamp the resolved version into a TypeScript file (e.g. `version.ts`),
|
|
125
|
+
* allowing client code (React Native) to import `PLUGIN_VERSION` directly without
|
|
126
|
+
* needing `node:fs` or `process.cwd()` at runtime.
|
|
127
|
+
*/
|
|
128
|
+
export function stampVersion(options: StampVersionOptions = {}): {
|
|
129
|
+
version: string;
|
|
130
|
+
targetFile: string;
|
|
131
|
+
updated: boolean;
|
|
132
|
+
} {
|
|
133
|
+
const cwd = options.cwd ?? process.cwd();
|
|
134
|
+
const targetFile = options.targetFile
|
|
135
|
+
? path.resolve(cwd, options.targetFile)
|
|
136
|
+
: path.resolve(cwd, "version.ts");
|
|
137
|
+
|
|
138
|
+
const version = resolvePluginVersion(options);
|
|
139
|
+
const content = `// Auto-generated by paseo-plugin-helper. Do not edit.\nexport const PLUGIN_VERSION = "${version}";\n`;
|
|
140
|
+
|
|
141
|
+
let existing = "";
|
|
142
|
+
if (fs.existsSync(targetFile)) {
|
|
143
|
+
existing = fs.readFileSync(targetFile, "utf8");
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (existing !== content) {
|
|
147
|
+
fs.mkdirSync(path.dirname(targetFile), { recursive: true });
|
|
148
|
+
fs.writeFileSync(targetFile, content, "utf8");
|
|
149
|
+
return { version, targetFile, updated: true };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return { version, targetFile, updated: false };
|
|
153
|
+
}
|