@nordbyte/nordrelay-auto-updater 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 +22 -0
- package/README.md +139 -0
- package/index.js +24 -0
- package/nordrelay.plugin.json +293 -0
- package/package.json +42 -0
- package/src/commands.js +20 -0
- package/src/format.js +90 -0
- package/src/npm-packages.js +92 -0
- package/src/os-updates.js +19 -0
- package/src/package-managers.js +175 -0
- package/src/render-panel.js +446 -0
- package/src/runtime.js +319 -0
- package/src/storage.js +337 -0
- package/src/subprocess.js +74 -0
- package/src/update-actions.js +108 -0
package/src/runtime.js
ADDED
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { hostname, platform, release } from "node:os";
|
|
3
|
+
import { mkdir } from "node:fs/promises";
|
|
4
|
+
import { collectNpmPackages } from "./npm-packages.js";
|
|
5
|
+
import { collectOsUpdates } from "./os-updates.js";
|
|
6
|
+
import { normalizeCommand } from "./commands.js";
|
|
7
|
+
import { renderDashboardPanel, dashboardPanelScript, dashboardPanelStyles } from "./render-panel.js";
|
|
8
|
+
import {
|
|
9
|
+
cleanupSnapshots,
|
|
10
|
+
dueNpmAutoRules,
|
|
11
|
+
insertSnapshot,
|
|
12
|
+
openDatabase,
|
|
13
|
+
readHistory,
|
|
14
|
+
readLatestSnapshot,
|
|
15
|
+
readNpmAutoRules,
|
|
16
|
+
recordNpmAutoRuleResult,
|
|
17
|
+
storageHealth,
|
|
18
|
+
upsertNpmAutoRule,
|
|
19
|
+
} from "./storage.js";
|
|
20
|
+
import { runNpmUninstallAction, runNpmUpdateAction, runOsUpdateAction } from "./update-actions.js";
|
|
21
|
+
|
|
22
|
+
export async function runPlugin() {
|
|
23
|
+
const request = await readRequest();
|
|
24
|
+
const settings = normalizeSettings(request.settings);
|
|
25
|
+
const dataDir = request.dataDir || process.cwd();
|
|
26
|
+
await mkdir(dataDir, { recursive: true });
|
|
27
|
+
|
|
28
|
+
if (request.type === "web-panel") {
|
|
29
|
+
writeResult({
|
|
30
|
+
ok: true,
|
|
31
|
+
html: renderDashboardPanel(request.input, request.context, settings),
|
|
32
|
+
panel: { script: dashboardPanelScript(), styles: dashboardPanelStyles() },
|
|
33
|
+
});
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (request.type === "diagnostics") {
|
|
38
|
+
const db = await openDatabase(dataDir);
|
|
39
|
+
try {
|
|
40
|
+
writeResult({ ok: true, diagnostics: await diagnostics(db, dataDir, settings) });
|
|
41
|
+
} finally {
|
|
42
|
+
db.close();
|
|
43
|
+
}
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
requireReadPermissions(request, settings);
|
|
48
|
+
const db = await openDatabase(dataDir);
|
|
49
|
+
try {
|
|
50
|
+
if (request.type === "collector") {
|
|
51
|
+
const snapshot = await collectAndStoreSnapshot(db, settings, request);
|
|
52
|
+
writeResult({ ok: true, output: { snapshot } });
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (request.type === "command") {
|
|
56
|
+
await handleCommand(request, db, dataDir, settings);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
} finally {
|
|
60
|
+
db.close();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
writeResult({ ok: false, stderr: `Unsupported request type: ${request.type}` });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function handleCommand(request, db, dataDir, settings) {
|
|
67
|
+
const command = normalizeCommand(request);
|
|
68
|
+
if (command === "refresh") {
|
|
69
|
+
const snapshot = await collectAndStoreSnapshot(db, settings, request);
|
|
70
|
+
writeResult({ ok: true, output: { snapshot } });
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (command === "latest") {
|
|
74
|
+
const snapshot = readLatestSnapshot(db) || await collectAndStoreSnapshot(db, settings, request);
|
|
75
|
+
writeResult({ ok: true, output: { snapshot } });
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (command === "panel-data") {
|
|
79
|
+
const snapshot = request.input?.force === true
|
|
80
|
+
? await collectAndStoreSnapshot(db, settings, request)
|
|
81
|
+
: readLatestSnapshot(db) || await collectAndStoreSnapshot(db, settings, request);
|
|
82
|
+
writeResult({
|
|
83
|
+
ok: true,
|
|
84
|
+
output: {
|
|
85
|
+
panelData: {
|
|
86
|
+
snapshot,
|
|
87
|
+
history: readHistory(db, { range: "24h", limit: request.input?.limit || 240 }),
|
|
88
|
+
automation: { rules: readNpmAutoRules(db) },
|
|
89
|
+
storage: await storageHealth(db, dataDir),
|
|
90
|
+
settings: publicSettings(settings),
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (command === "os-updates") {
|
|
97
|
+
const snapshot = readLatestSnapshot(db) || await collectAndStoreSnapshot(db, settings, request);
|
|
98
|
+
writeResult({ ok: true, output: { os: snapshot.os } });
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (command === "npm-packages") {
|
|
102
|
+
const snapshot = readLatestSnapshot(db) || await collectAndStoreSnapshot(db, settings, request);
|
|
103
|
+
writeResult({ ok: true, output: { npm: snapshot.npm } });
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (command === "update-os") {
|
|
107
|
+
requirePermission(request, "system.updates.write");
|
|
108
|
+
const action = runOsUpdateAction(request.input || {}, settings);
|
|
109
|
+
const snapshot = await collectAndStoreSnapshot(db, settings, request);
|
|
110
|
+
writeResult({ ok: action.ok, output: { action, snapshot }, stderr: action.error || action.results.find((item) => !item.ok)?.stderr || "" });
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (command === "update-npm") {
|
|
114
|
+
requirePermission(request, "system.packages.write");
|
|
115
|
+
const action = runNpmUpdateAction(request.input || {}, settings);
|
|
116
|
+
const snapshot = await collectAndStoreSnapshot(db, settings, request);
|
|
117
|
+
writeResult({ ok: action.ok, output: { action, snapshot }, stderr: action.error || action.results.find((item) => !item.ok)?.stderr || "" });
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (command === "configure-npm-auto") {
|
|
121
|
+
requirePermission(request, "system.packages.write");
|
|
122
|
+
const rule = upsertNpmAutoRule(db, request.input || {});
|
|
123
|
+
writeResult({ ok: true, output: { rule, automation: { rules: readNpmAutoRules(db) } } });
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (command === "uninstall-npm") {
|
|
127
|
+
requirePermission(request, "system.packages.write");
|
|
128
|
+
const action = runNpmUninstallAction(request.input || {}, settings);
|
|
129
|
+
const snapshot = await collectAndStoreSnapshot(db, settings, request);
|
|
130
|
+
writeResult({ ok: action.ok, output: { action, snapshot }, stderr: action.error || action.results.find((item) => !item.ok)?.stderr || "" });
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (command === "history") {
|
|
134
|
+
writeResult({ ok: true, output: { history: readHistory(db, request.input || {}) } });
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (command === "export") {
|
|
138
|
+
writeResult({ ok: true, output: exportData(db, request.input || {}) });
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (command === "storage-health") {
|
|
142
|
+
writeResult({ ok: true, output: { storage: await storageHealth(db, dataDir) } });
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (command === "cleanup") {
|
|
146
|
+
const deleted = cleanupSnapshots(db, settings.retentionDays);
|
|
147
|
+
writeResult({ ok: true, output: { deleted, storage: await storageHealth(db, dataDir) } });
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
writeResult({ ok: false, stderr: `Unknown auto-updater command: ${command}` });
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export async function collectAndStoreSnapshot(db, settings, request) {
|
|
154
|
+
const startedAt = Date.now();
|
|
155
|
+
const os = collectOsUpdates(settings);
|
|
156
|
+
let npm = collectNpmPackages(settings);
|
|
157
|
+
const node = runtimeNode(request);
|
|
158
|
+
const automation = runDueNpmAutomation(db, npm.packages, settings, request);
|
|
159
|
+
if (automation.actions.length) {
|
|
160
|
+
npm = collectNpmPackages(settings);
|
|
161
|
+
}
|
|
162
|
+
const errors = [
|
|
163
|
+
...(os.errors || []).map((item) => ({ area: "os", ...item })),
|
|
164
|
+
...(npm.errors || []).map((item) => ({ area: "npm", ...item })),
|
|
165
|
+
...automation.errors,
|
|
166
|
+
];
|
|
167
|
+
const snapshot = {
|
|
168
|
+
ts: startedAt,
|
|
169
|
+
checkedAt: new Date(startedAt).toISOString(),
|
|
170
|
+
durationMs: Date.now() - startedAt,
|
|
171
|
+
node,
|
|
172
|
+
os,
|
|
173
|
+
npm,
|
|
174
|
+
errors,
|
|
175
|
+
host: {
|
|
176
|
+
hostname: hostname(),
|
|
177
|
+
release: release(),
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
cleanupSnapshots(db, settings.retentionDays);
|
|
181
|
+
return insertSnapshot(db, snapshot);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function runDueNpmAutomation(db, packages, settings, request) {
|
|
185
|
+
const permissions = new Set(request.permissions || []);
|
|
186
|
+
if (settings.enableNpmPackages === false || !permissions.has("system.packages.write")) {
|
|
187
|
+
return { actions: [], errors: [] };
|
|
188
|
+
}
|
|
189
|
+
const packageMap = new Map((Array.isArray(packages) ? packages : []).map((item) => [String(item.name || ""), item]));
|
|
190
|
+
const actions = [];
|
|
191
|
+
const errors = [];
|
|
192
|
+
for (const rule of dueNpmAutoRules(db, packages)) {
|
|
193
|
+
const pkg = packageMap.get(rule.name);
|
|
194
|
+
if (!pkg) continue;
|
|
195
|
+
if (pkg.status !== "outdated" && !rule.installLatest) {
|
|
196
|
+
recordNpmAutoRuleResult(db, rule.name, { ran: false, status: "skipped", message: "Package is already latest." });
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
const action = runNpmUpdateAction({ packages: [rule.name] }, settings);
|
|
200
|
+
actions.push({ rule, action });
|
|
201
|
+
const result = action.results?.[0];
|
|
202
|
+
const message = action.error || result?.stderr || result?.stdout || (action.ok ? "Installed latest package globally." : "npm install failed.");
|
|
203
|
+
recordNpmAutoRuleResult(db, rule.name, { ran: true, status: action.ok ? "installed" : "failed", message });
|
|
204
|
+
if (!action.ok) {
|
|
205
|
+
errors.push({ area: "npm-auto", manager: "npm", package: rule.name, error: message });
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return { actions, errors };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function runtimeNode(request) {
|
|
212
|
+
const runtime = request.context?.runtime || {};
|
|
213
|
+
return {
|
|
214
|
+
id: String(runtime.nodeId || runtime.nodeName || hostname() || "local"),
|
|
215
|
+
name: String(runtime.nodeName || runtime.nodeId || hostname() || "Local node"),
|
|
216
|
+
platform: String(runtime.platform || platform()),
|
|
217
|
+
workspace: runtime.workspace ? String(runtime.workspace) : "",
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async function diagnostics(db, dataDir, settings) {
|
|
222
|
+
return {
|
|
223
|
+
plugin: "auto-updater",
|
|
224
|
+
dataDir,
|
|
225
|
+
storage: await storageHealth(db, dataDir),
|
|
226
|
+
latest: readLatestSnapshot(db),
|
|
227
|
+
settings: publicSettings(settings),
|
|
228
|
+
supported: {
|
|
229
|
+
osUpdates: settings.enableOsUpdates !== false && ["linux", "darwin"].includes(process.platform),
|
|
230
|
+
npmPackages: settings.enableNpmPackages !== false,
|
|
231
|
+
platform: process.platform,
|
|
232
|
+
},
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function exportData(db, input) {
|
|
237
|
+
const format = String(input.format || "json").toLowerCase();
|
|
238
|
+
const latest = readLatestSnapshot(db);
|
|
239
|
+
const history = readHistory(db, { range: input.range || "30d", limit: input.limit || 2000 });
|
|
240
|
+
if (format === "csv") {
|
|
241
|
+
const rows = ["checkedAt,node,platform,osUpdates,npmPackages,npmOutdated,errors"];
|
|
242
|
+
for (const item of history) {
|
|
243
|
+
rows.push([
|
|
244
|
+
item.checkedAt,
|
|
245
|
+
csv(item.node?.name),
|
|
246
|
+
csv(item.node?.platform),
|
|
247
|
+
item.osUpdateCount,
|
|
248
|
+
item.npmPackageCount,
|
|
249
|
+
item.npmOutdatedCount,
|
|
250
|
+
csv((item.errors || []).map((error) => error.error || error.manager || error.area).join("; ")),
|
|
251
|
+
].join(","));
|
|
252
|
+
}
|
|
253
|
+
return { format: "csv", content: `${rows.join("\n")}\n` };
|
|
254
|
+
}
|
|
255
|
+
return { format: "json", latest, history };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function publicSettings(settings) {
|
|
259
|
+
return {
|
|
260
|
+
retentionDays: settings.retentionDays,
|
|
261
|
+
checkIntervalMs: settings.checkIntervalMs,
|
|
262
|
+
updateTimeoutMs: settings.updateTimeoutMs,
|
|
263
|
+
enableOsUpdates: settings.enableOsUpdates,
|
|
264
|
+
enableNpmPackages: settings.enableNpmPackages,
|
|
265
|
+
npmRegistry: settings.npmRegistry,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export function normalizeSettings(settings = {}) {
|
|
270
|
+
return {
|
|
271
|
+
checkIntervalMs: numberSetting(settings.checkIntervalMs, 1_800_000),
|
|
272
|
+
retentionDays: numberSetting(settings.retentionDays, 30),
|
|
273
|
+
commandTimeoutMs: numberSetting(settings.commandTimeoutMs, 20_000),
|
|
274
|
+
updateTimeoutMs: numberSetting(settings.updateTimeoutMs, 600_000),
|
|
275
|
+
enableOsUpdates: settings.enableOsUpdates !== false,
|
|
276
|
+
enableNpmPackages: settings.enableNpmPackages !== false,
|
|
277
|
+
npmRegistry: String(settings.npmRegistry || "https://registry.npmjs.org"),
|
|
278
|
+
disabledManagers: String(settings.disabledManagers || ""),
|
|
279
|
+
refreshPackageIndexes: settings.refreshPackageIndexes === true,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function numberSetting(value, fallback) {
|
|
284
|
+
const parsed = Number(value);
|
|
285
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
async function readRequest() {
|
|
289
|
+
const chunks = [];
|
|
290
|
+
for await (const chunk of process.stdin) chunks.push(Buffer.from(chunk));
|
|
291
|
+
const raw = Buffer.concat(chunks).toString("utf8").trim();
|
|
292
|
+
return raw ? JSON.parse(raw) : {};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function writeResult(result) {
|
|
296
|
+
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function requireReadPermissions(request, settings) {
|
|
300
|
+
const permissions = new Set(request.permissions || []);
|
|
301
|
+
if (settings.enableOsUpdates !== false && !permissions.has("system.updates.read")) {
|
|
302
|
+
throw new Error("Plugin permission required: system.updates.read");
|
|
303
|
+
}
|
|
304
|
+
if (settings.enableNpmPackages !== false && !permissions.has("system.packages.read")) {
|
|
305
|
+
throw new Error("Plugin permission required: system.packages.read");
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function requirePermission(request, permission) {
|
|
310
|
+
const permissions = new Set(request.permissions || []);
|
|
311
|
+
if (!permissions.has(permission)) {
|
|
312
|
+
throw new Error(`Plugin permission required: ${permission}`);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function csv(value) {
|
|
317
|
+
const text = String(value ?? "");
|
|
318
|
+
return /[",\n]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text;
|
|
319
|
+
}
|
package/src/storage.js
ADDED
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { mkdir, stat } from "node:fs/promises";
|
|
3
|
+
|
|
4
|
+
const DB_FILE = "updates.sqlite";
|
|
5
|
+
let DatabaseSyncClass;
|
|
6
|
+
|
|
7
|
+
export async function openDatabase(dataDir) {
|
|
8
|
+
await mkdir(dataDir, { recursive: true });
|
|
9
|
+
const DatabaseSync = await loadDatabaseSync();
|
|
10
|
+
const db = new DatabaseSync(path.join(dataDir, DB_FILE));
|
|
11
|
+
initializeSchema(db);
|
|
12
|
+
return db;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function initializeSchema(db) {
|
|
16
|
+
db.exec(`
|
|
17
|
+
PRAGMA foreign_keys = ON;
|
|
18
|
+
PRAGMA busy_timeout = 3000;
|
|
19
|
+
PRAGMA journal_mode = WAL;
|
|
20
|
+
CREATE TABLE IF NOT EXISTS schema_meta (
|
|
21
|
+
key TEXT PRIMARY KEY,
|
|
22
|
+
value TEXT NOT NULL
|
|
23
|
+
);
|
|
24
|
+
CREATE TABLE IF NOT EXISTS snapshots (
|
|
25
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
26
|
+
ts INTEGER NOT NULL,
|
|
27
|
+
node_id TEXT NOT NULL,
|
|
28
|
+
node_name TEXT,
|
|
29
|
+
node_platform TEXT,
|
|
30
|
+
duration_ms INTEGER,
|
|
31
|
+
os_supported INTEGER NOT NULL,
|
|
32
|
+
os_update_count INTEGER NOT NULL,
|
|
33
|
+
npm_supported INTEGER NOT NULL,
|
|
34
|
+
npm_package_count INTEGER NOT NULL,
|
|
35
|
+
npm_outdated_count INTEGER NOT NULL,
|
|
36
|
+
errors_json TEXT NOT NULL
|
|
37
|
+
);
|
|
38
|
+
CREATE TABLE IF NOT EXISTS os_updates (
|
|
39
|
+
snapshot_id INTEGER NOT NULL REFERENCES snapshots(id) ON DELETE CASCADE,
|
|
40
|
+
manager TEXT NOT NULL,
|
|
41
|
+
name TEXT NOT NULL,
|
|
42
|
+
current TEXT,
|
|
43
|
+
latest TEXT,
|
|
44
|
+
source TEXT,
|
|
45
|
+
arch TEXT
|
|
46
|
+
);
|
|
47
|
+
CREATE TABLE IF NOT EXISTS npm_packages (
|
|
48
|
+
snapshot_id INTEGER NOT NULL REFERENCES snapshots(id) ON DELETE CASCADE,
|
|
49
|
+
name TEXT NOT NULL,
|
|
50
|
+
current TEXT,
|
|
51
|
+
wanted TEXT,
|
|
52
|
+
latest TEXT,
|
|
53
|
+
status TEXT,
|
|
54
|
+
type TEXT,
|
|
55
|
+
location TEXT,
|
|
56
|
+
homepage TEXT
|
|
57
|
+
);
|
|
58
|
+
CREATE TABLE IF NOT EXISTS npm_auto_rules (
|
|
59
|
+
name TEXT PRIMARY KEY,
|
|
60
|
+
enabled INTEGER NOT NULL DEFAULT 0,
|
|
61
|
+
interval_ms INTEGER NOT NULL DEFAULT 86400000,
|
|
62
|
+
install_latest INTEGER NOT NULL DEFAULT 0,
|
|
63
|
+
last_checked_at INTEGER,
|
|
64
|
+
last_run_at INTEGER,
|
|
65
|
+
last_status TEXT,
|
|
66
|
+
last_message TEXT,
|
|
67
|
+
updated_at INTEGER NOT NULL
|
|
68
|
+
);
|
|
69
|
+
CREATE INDEX IF NOT EXISTS idx_snapshots_ts ON snapshots(ts);
|
|
70
|
+
CREATE INDEX IF NOT EXISTS idx_os_snapshot ON os_updates(snapshot_id);
|
|
71
|
+
CREATE INDEX IF NOT EXISTS idx_npm_snapshot ON npm_packages(snapshot_id);
|
|
72
|
+
`);
|
|
73
|
+
db.prepare("INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('schemaVersion', '2')").run();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function insertSnapshot(db, snapshot) {
|
|
77
|
+
const row = db.prepare(`
|
|
78
|
+
INSERT INTO snapshots (
|
|
79
|
+
ts, node_id, node_name, node_platform, duration_ms, os_supported, os_update_count,
|
|
80
|
+
npm_supported, npm_package_count, npm_outdated_count, errors_json
|
|
81
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
82
|
+
`).run(
|
|
83
|
+
snapshot.ts,
|
|
84
|
+
snapshot.node.id,
|
|
85
|
+
snapshot.node.name,
|
|
86
|
+
snapshot.node.platform,
|
|
87
|
+
snapshot.durationMs,
|
|
88
|
+
snapshot.os.supported ? 1 : 0,
|
|
89
|
+
snapshot.os.updates.length,
|
|
90
|
+
snapshot.npm.supported ? 1 : 0,
|
|
91
|
+
snapshot.npm.packages.length,
|
|
92
|
+
snapshot.npm.packages.filter((item) => item.status === "outdated").length,
|
|
93
|
+
JSON.stringify(snapshot.errors || []),
|
|
94
|
+
);
|
|
95
|
+
const snapshotId = Number(row.lastInsertRowid);
|
|
96
|
+
const osInsert = db.prepare("INSERT INTO os_updates (snapshot_id, manager, name, current, latest, source, arch) VALUES (?, ?, ?, ?, ?, ?, ?)");
|
|
97
|
+
for (const item of snapshot.os.updates) {
|
|
98
|
+
osInsert.run(snapshotId, item.manager || "", item.name || "", item.current || "", item.latest || "", item.source || "", item.arch || "");
|
|
99
|
+
}
|
|
100
|
+
const npmInsert = db.prepare("INSERT INTO npm_packages (snapshot_id, name, current, wanted, latest, status, type, location, homepage) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
|
101
|
+
for (const item of snapshot.npm.packages) {
|
|
102
|
+
npmInsert.run(snapshotId, item.name || "", item.current || "", item.wanted || "", item.latest || "", item.status || "", item.type || "", item.location || "", item.homepage || "");
|
|
103
|
+
}
|
|
104
|
+
return readSnapshot(db, snapshotId);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function readLatestSnapshot(db) {
|
|
108
|
+
const row = db.prepare("SELECT * FROM snapshots ORDER BY ts DESC, id DESC LIMIT 1").get();
|
|
109
|
+
return row ? inflateSnapshot(db, row) : null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function readSnapshot(db, snapshotId) {
|
|
113
|
+
const row = db.prepare("SELECT * FROM snapshots WHERE id = ?").get(snapshotId);
|
|
114
|
+
return row ? inflateSnapshot(db, row) : null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function readHistory(db, options = {}) {
|
|
118
|
+
const since = Date.now() - rangeMs(options.range || "7d");
|
|
119
|
+
const limit = Math.max(1, Math.min(2000, Number(options.limit) || 200));
|
|
120
|
+
return db.prepare(`
|
|
121
|
+
SELECT id, ts, node_id, node_name, node_platform, duration_ms, os_update_count,
|
|
122
|
+
npm_package_count, npm_outdated_count, errors_json
|
|
123
|
+
FROM snapshots
|
|
124
|
+
WHERE ts >= ?
|
|
125
|
+
ORDER BY ts DESC, id DESC
|
|
126
|
+
LIMIT ?
|
|
127
|
+
`).all(since, limit).map((row) => ({
|
|
128
|
+
id: row.id,
|
|
129
|
+
checkedAt: new Date(Number(row.ts)).toISOString(),
|
|
130
|
+
node: { id: row.node_id, name: row.node_name, platform: row.node_platform },
|
|
131
|
+
durationMs: Number(row.duration_ms) || 0,
|
|
132
|
+
osUpdateCount: Number(row.os_update_count) || 0,
|
|
133
|
+
npmPackageCount: Number(row.npm_package_count) || 0,
|
|
134
|
+
npmOutdatedCount: Number(row.npm_outdated_count) || 0,
|
|
135
|
+
errors: parseJson(row.errors_json, []),
|
|
136
|
+
}));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function cleanupSnapshots(db, retentionDays = 30) {
|
|
140
|
+
const cutoff = Date.now() - Math.max(1, Number(retentionDays) || 30) * 24 * 60 * 60 * 1000;
|
|
141
|
+
return Number(db.prepare("DELETE FROM snapshots WHERE ts < ?").run(cutoff).changes) || 0;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export async function storageHealth(db, dataDir) {
|
|
145
|
+
const database = path.join(dataDir, DB_FILE);
|
|
146
|
+
const wal = `${database}-wal`;
|
|
147
|
+
const shm = `${database}-shm`;
|
|
148
|
+
const integrity = safeGet(db, "PRAGMA integrity_check");
|
|
149
|
+
const snapshots = safeGet(db, "SELECT COUNT(*) AS count FROM snapshots");
|
|
150
|
+
return {
|
|
151
|
+
database,
|
|
152
|
+
sizeBytes: (await fileSize(database)) + (await fileSize(wal)) + (await fileSize(shm)),
|
|
153
|
+
integrity: String(integrity?.integrity_check || "unknown"),
|
|
154
|
+
snapshots: Number(snapshots?.count) || 0,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function readNpmAutoRules(db) {
|
|
159
|
+
return db.prepare(`
|
|
160
|
+
SELECT name, enabled, interval_ms, install_latest, last_checked_at, last_run_at,
|
|
161
|
+
last_status, last_message, updated_at
|
|
162
|
+
FROM npm_auto_rules
|
|
163
|
+
ORDER BY name
|
|
164
|
+
`).all().map(inflateNpmAutoRule);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function readNpmAutoRuleMap(db) {
|
|
168
|
+
const map = new Map();
|
|
169
|
+
for (const rule of readNpmAutoRules(db)) {
|
|
170
|
+
map.set(rule.name, rule);
|
|
171
|
+
}
|
|
172
|
+
return map;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function upsertNpmAutoRule(db, input = {}) {
|
|
176
|
+
const rule = normalizeNpmAutoRule(input);
|
|
177
|
+
const previous = db.prepare("SELECT * FROM npm_auto_rules WHERE name = ?").get(rule.name);
|
|
178
|
+
const now = Date.now();
|
|
179
|
+
db.prepare(`
|
|
180
|
+
INSERT INTO npm_auto_rules (
|
|
181
|
+
name, enabled, interval_ms, install_latest, last_checked_at, last_run_at,
|
|
182
|
+
last_status, last_message, updated_at
|
|
183
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
184
|
+
ON CONFLICT(name) DO UPDATE SET
|
|
185
|
+
enabled = excluded.enabled,
|
|
186
|
+
interval_ms = excluded.interval_ms,
|
|
187
|
+
install_latest = excluded.install_latest,
|
|
188
|
+
updated_at = excluded.updated_at
|
|
189
|
+
`).run(
|
|
190
|
+
rule.name,
|
|
191
|
+
rule.enabled ? 1 : 0,
|
|
192
|
+
rule.intervalMs,
|
|
193
|
+
rule.installLatest ? 1 : 0,
|
|
194
|
+
previous?.last_checked_at ?? null,
|
|
195
|
+
previous?.last_run_at ?? null,
|
|
196
|
+
previous?.last_status ?? null,
|
|
197
|
+
previous?.last_message ?? null,
|
|
198
|
+
now,
|
|
199
|
+
);
|
|
200
|
+
return inflateNpmAutoRule(db.prepare("SELECT * FROM npm_auto_rules WHERE name = ?").get(rule.name));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function dueNpmAutoRules(db, packages = [], now = Date.now()) {
|
|
204
|
+
const installed = new Set((Array.isArray(packages) ? packages : []).map((item) => String(item.name || "")).filter(Boolean));
|
|
205
|
+
return readNpmAutoRules(db).filter((rule) => {
|
|
206
|
+
const lastCheckedMs = Date.parse(rule.lastCheckedAt || "");
|
|
207
|
+
return rule.enabled && installed.has(rule.name) && (!Number.isFinite(lastCheckedMs) || now - lastCheckedMs >= rule.intervalMs);
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function recordNpmAutoRuleResult(db, name, result = {}) {
|
|
212
|
+
const now = Date.now();
|
|
213
|
+
const ran = result.ran !== false;
|
|
214
|
+
db.prepare(`
|
|
215
|
+
UPDATE npm_auto_rules
|
|
216
|
+
SET last_checked_at = ?, last_run_at = CASE WHEN ? THEN ? ELSE last_run_at END,
|
|
217
|
+
last_status = ?, last_message = ?
|
|
218
|
+
WHERE name = ?
|
|
219
|
+
`).run(now, ran ? 1 : 0, now, String(result.status || ""), String(result.message || ""), String(name || ""));
|
|
220
|
+
const row = db.prepare("SELECT * FROM npm_auto_rules WHERE name = ?").get(String(name || ""));
|
|
221
|
+
return row ? inflateNpmAutoRule(row) : null;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function inflateSnapshot(db, row) {
|
|
225
|
+
const snapshotId = Number(row.id);
|
|
226
|
+
return {
|
|
227
|
+
id: snapshotId,
|
|
228
|
+
checkedAt: new Date(Number(row.ts)).toISOString(),
|
|
229
|
+
durationMs: Number(row.duration_ms) || 0,
|
|
230
|
+
node: { id: row.node_id, name: row.node_name, platform: row.node_platform },
|
|
231
|
+
os: {
|
|
232
|
+
supported: Boolean(row.os_supported),
|
|
233
|
+
updates: db.prepare("SELECT manager, name, current, latest, source, arch FROM os_updates WHERE snapshot_id = ? ORDER BY manager, name").all(snapshotId),
|
|
234
|
+
updateCount: Number(row.os_update_count) || 0,
|
|
235
|
+
},
|
|
236
|
+
npm: {
|
|
237
|
+
supported: Boolean(row.npm_supported),
|
|
238
|
+
packages: db.prepare("SELECT name, current, wanted, latest, status, type, location, homepage FROM npm_packages WHERE snapshot_id = ? ORDER BY status DESC, name").all(snapshotId),
|
|
239
|
+
packageCount: Number(row.npm_package_count) || 0,
|
|
240
|
+
outdatedCount: Number(row.npm_outdated_count) || 0,
|
|
241
|
+
},
|
|
242
|
+
errors: parseJson(row.errors_json, []),
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function inflateNpmAutoRule(row) {
|
|
247
|
+
return {
|
|
248
|
+
name: String(row.name || ""),
|
|
249
|
+
enabled: Boolean(row.enabled),
|
|
250
|
+
intervalMs: Number(row.interval_ms) || 24 * 60 * 60 * 1000,
|
|
251
|
+
intervalValue: intervalValue(Number(row.interval_ms) || 24 * 60 * 60 * 1000),
|
|
252
|
+
intervalUnit: intervalUnit(Number(row.interval_ms) || 24 * 60 * 60 * 1000),
|
|
253
|
+
installLatest: Boolean(row.install_latest),
|
|
254
|
+
lastCheckedAt: row.last_checked_at ? new Date(Number(row.last_checked_at)).toISOString() : "",
|
|
255
|
+
lastRunAt: row.last_run_at ? new Date(Number(row.last_run_at)).toISOString() : "",
|
|
256
|
+
lastStatus: String(row.last_status || ""),
|
|
257
|
+
lastMessage: String(row.last_message || ""),
|
|
258
|
+
updatedAt: row.updated_at ? new Date(Number(row.updated_at)).toISOString() : "",
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function normalizeNpmAutoRule(input) {
|
|
263
|
+
const name = String(input.name || input.package || "").trim();
|
|
264
|
+
if (!/^(?:@[A-Za-z0-9._-]+\/)?[A-Za-z0-9._-]+$/.test(name)) {
|
|
265
|
+
throw new Error("Invalid npm package name.");
|
|
266
|
+
}
|
|
267
|
+
const unit = String(input.intervalUnit || input.unit || "hours").toLowerCase();
|
|
268
|
+
const value = Math.max(1, Math.min(365, Number(input.intervalValue || input.interval || 24) || 24));
|
|
269
|
+
const multiplier = unit === "days" || unit === "day" || unit === "d" ? 24 * 60 * 60 * 1000 : 60 * 60 * 1000;
|
|
270
|
+
return {
|
|
271
|
+
name,
|
|
272
|
+
enabled: input.enabled === true || input.enabled === "true" || input.enabled === 1,
|
|
273
|
+
intervalMs: Math.round(value * multiplier),
|
|
274
|
+
installLatest: input.installLatest === true || input.installLatest === "true" || input.installLatest === 1,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function intervalUnit(ms) {
|
|
279
|
+
return Number(ms) % (24 * 60 * 60 * 1000) === 0 ? "days" : "hours";
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function intervalValue(ms) {
|
|
283
|
+
const value = Number(ms) || 24 * 60 * 60 * 1000;
|
|
284
|
+
const divisor = intervalUnit(value) === "days" ? 24 * 60 * 60 * 1000 : 60 * 60 * 1000;
|
|
285
|
+
return Math.max(1, Math.round(value / divisor));
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
async function loadDatabaseSync() {
|
|
289
|
+
if (DatabaseSyncClass) return DatabaseSyncClass;
|
|
290
|
+
const originalEmitWarning = process.emitWarning;
|
|
291
|
+
process.emitWarning = function emitWarningWithoutSqliteExperimentalNoise(warning, ...args) {
|
|
292
|
+
const text = typeof warning === "string" ? warning : warning?.message;
|
|
293
|
+
const type = typeof args[0] === "string" ? args[0] : warning?.name;
|
|
294
|
+
if (type === "ExperimentalWarning" && /SQLite/i.test(String(text || ""))) return;
|
|
295
|
+
return originalEmitWarning.call(this, warning, ...args);
|
|
296
|
+
};
|
|
297
|
+
try {
|
|
298
|
+
const sqlite = await import("node:sqlite");
|
|
299
|
+
DatabaseSyncClass = sqlite.DatabaseSync;
|
|
300
|
+
return DatabaseSyncClass;
|
|
301
|
+
} finally {
|
|
302
|
+
process.emitWarning = originalEmitWarning;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function parseJson(value, fallback) {
|
|
307
|
+
try {
|
|
308
|
+
return JSON.parse(String(value || ""));
|
|
309
|
+
} catch {
|
|
310
|
+
return fallback;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function safeGet(db, sql) {
|
|
315
|
+
try {
|
|
316
|
+
return db.prepare(sql).get();
|
|
317
|
+
} catch {
|
|
318
|
+
return {};
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
async function fileSize(file) {
|
|
323
|
+
try {
|
|
324
|
+
return (await stat(file)).size;
|
|
325
|
+
} catch {
|
|
326
|
+
return 0;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function rangeMs(range) {
|
|
331
|
+
const match = /^(\d+)(m|h|d)$/i.exec(String(range || ""));
|
|
332
|
+
if (!match) return 7 * 24 * 60 * 60 * 1000;
|
|
333
|
+
const value = Number(match[1]);
|
|
334
|
+
if (match[2].toLowerCase() === "m") return value * 60 * 1000;
|
|
335
|
+
if (match[2].toLowerCase() === "h") return value * 60 * 60 * 1000;
|
|
336
|
+
return value * 24 * 60 * 60 * 1000;
|
|
337
|
+
}
|