chainlesschain 0.47.9 → 0.51.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/bin/chainlesschain.js +0 -0
- package/package.json +1 -1
- package/src/assets/web-panel/.build-hash +1 -1
- package/src/assets/web-panel/assets/{AppLayout-6SPt_8Y_.js → AppLayout-Rvi759IS.js} +1 -1
- package/src/assets/web-panel/assets/Dashboard-BS-tzGNj.css +1 -0
- package/src/assets/web-panel/assets/{Dashboard-Br7kCwKJ.js → Dashboard-DBhFxXYQ.js} +2 -2
- package/src/assets/web-panel/assets/{index-tN-8TosE.js → index-uL0cZ8N_.js} +2 -2
- package/src/assets/web-panel/index.html +2 -2
- package/src/commands/codegen.js +303 -0
- package/src/commands/collab.js +482 -0
- package/src/commands/crosschain.js +382 -0
- package/src/commands/dbevo.js +388 -0
- package/src/commands/dev.js +411 -0
- package/src/commands/federation.js +427 -0
- package/src/commands/fusion.js +332 -0
- package/src/commands/governance.js +505 -0
- package/src/commands/hardening.js +110 -0
- package/src/commands/incentive.js +373 -0
- package/src/commands/inference.js +304 -0
- package/src/commands/infra.js +361 -0
- package/src/commands/ipfs.js +392 -0
- package/src/commands/kg.js +371 -0
- package/src/commands/marketplace.js +326 -0
- package/src/commands/mcp.js +97 -18
- package/src/commands/multimodal.js +404 -0
- package/src/commands/nlprog.js +329 -0
- package/src/commands/ops.js +408 -0
- package/src/commands/perception.js +385 -0
- package/src/commands/pqc.js +34 -0
- package/src/commands/privacy.js +345 -0
- package/src/commands/quantization.js +280 -0
- package/src/commands/recommend.js +336 -0
- package/src/commands/reputation.js +349 -0
- package/src/commands/runtime.js +500 -0
- package/src/commands/sla.js +352 -0
- package/src/commands/stress.js +252 -0
- package/src/commands/tech.js +268 -0
- package/src/commands/tenant.js +576 -0
- package/src/commands/trust.js +366 -0
- package/src/harness/mcp-client.js +330 -54
- package/src/index.js +118 -0
- package/src/lib/aiops.js +523 -0
- package/src/lib/autonomous-developer.js +524 -0
- package/src/lib/code-agent.js +442 -0
- package/src/lib/collaboration-governance.js +556 -0
- package/src/lib/community-governance.js +649 -0
- package/src/lib/content-recommendation.js +600 -0
- package/src/lib/cross-chain.js +669 -0
- package/src/lib/dbevo.js +669 -0
- package/src/lib/decentral-infra.js +445 -0
- package/src/lib/federation-hardening.js +587 -0
- package/src/lib/hardening-manager.js +409 -0
- package/src/lib/inference-network.js +407 -0
- package/src/lib/ipfs-storage.js +575 -0
- package/src/lib/knowledge-graph.js +530 -0
- package/src/lib/mcp-client.js +3 -0
- package/src/lib/multimodal.js +725 -0
- package/src/lib/nl-programming.js +595 -0
- package/src/lib/perception.js +500 -0
- package/src/lib/pqc-manager.js +141 -9
- package/src/lib/privacy-computing.js +575 -0
- package/src/lib/protocol-fusion.js +535 -0
- package/src/lib/quantization.js +362 -0
- package/src/lib/reputation-optimizer.js +509 -0
- package/src/lib/skill-marketplace.js +397 -0
- package/src/lib/sla-manager.js +484 -0
- package/src/lib/stress-tester.js +383 -0
- package/src/lib/tech-learning-engine.js +651 -0
- package/src/lib/tenant-saas.js +831 -0
- package/src/lib/token-incentive.js +513 -0
- package/src/lib/trust-security.js +473 -0
- package/src/lib/universal-runtime.js +771 -0
- package/src/assets/web-panel/assets/Dashboard-CKeMmCoT.css +0 -1
|
@@ -0,0 +1,771 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Universal Runtime — CLI port of Phase 63
|
|
3
|
+
* (docs/design/modules/63_统一应用运行时.md).
|
|
4
|
+
*
|
|
5
|
+
* Desktop has UniversalRuntime with 8 IPC handlers:
|
|
6
|
+
* load-plugin / hot-update / profile / sync-state /
|
|
7
|
+
* get-platform-info / configure / health-check / get-metrics.
|
|
8
|
+
*
|
|
9
|
+
* CLI port ships:
|
|
10
|
+
*
|
|
11
|
+
* - Plugin lifecycle (load/unload/list/show) with status tracking
|
|
12
|
+
* - Hot update lifecycle with rollback + update-type catalog
|
|
13
|
+
* - Simulated profile sampling (cpu/memory/flamegraph) with persistence
|
|
14
|
+
* - CRDT-less state sync (last-write-wins key/value) with timestamps
|
|
15
|
+
* - Platform info + health check + runtime metrics
|
|
16
|
+
* - Configure (key/value) + stats aggregation
|
|
17
|
+
*
|
|
18
|
+
* What does NOT port: real plugin sandbox, actual Yjs CRDT merge,
|
|
19
|
+
* real Flame Graph sampling, diff-based hot-patch payloads,
|
|
20
|
+
* automatic self-heal timers (CLI is one-shot).
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import crypto from "crypto";
|
|
24
|
+
import os from "os";
|
|
25
|
+
|
|
26
|
+
/* ── Constants ──────────────────────────────────────────── */
|
|
27
|
+
|
|
28
|
+
export const PLUGIN_STATUS = Object.freeze({
|
|
29
|
+
LOADING: "loading",
|
|
30
|
+
ACTIVE: "active",
|
|
31
|
+
SUSPENDED: "suspended",
|
|
32
|
+
ERROR: "error",
|
|
33
|
+
UNLOADED: "unloaded",
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
export const UPDATE_TYPE = Object.freeze({
|
|
37
|
+
PATCH: "patch",
|
|
38
|
+
MINOR: "minor",
|
|
39
|
+
MAJOR: "major",
|
|
40
|
+
ROLLBACK: "rollback",
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
export const HEALTH_STATUS = Object.freeze({
|
|
44
|
+
HEALTHY: "healthy",
|
|
45
|
+
DEGRADED: "degraded",
|
|
46
|
+
CRITICAL: "critical",
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
export const PROFILE_TYPES = Object.freeze(["cpu", "memory", "flamegraph"]);
|
|
50
|
+
|
|
51
|
+
const DEFAULT_CONFIG = Object.freeze({
|
|
52
|
+
pluginDir: "plugins/",
|
|
53
|
+
sandboxEnabled: true,
|
|
54
|
+
hotUpdateEnabled: true,
|
|
55
|
+
profileSampleRate: 1000,
|
|
56
|
+
crdtSyncIntervalMs: 5000,
|
|
57
|
+
healthCheckIntervalMs: 30000,
|
|
58
|
+
maxPlugins: 100,
|
|
59
|
+
memoryLimitMb: 512,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
/* ── State ──────────────────────────────────────────────── */
|
|
63
|
+
|
|
64
|
+
let _plugins = new Map();
|
|
65
|
+
let _updates = new Map();
|
|
66
|
+
let _profiles = new Map();
|
|
67
|
+
let _state = new Map();
|
|
68
|
+
let _config = { ...DEFAULT_CONFIG };
|
|
69
|
+
let _metrics = {
|
|
70
|
+
pluginsLoaded: 0,
|
|
71
|
+
pluginsUnloaded: 0,
|
|
72
|
+
hotUpdates: 0,
|
|
73
|
+
rollbacks: 0,
|
|
74
|
+
profilesTaken: 0,
|
|
75
|
+
stateWrites: 0,
|
|
76
|
+
errors: 0,
|
|
77
|
+
startedAt: Date.now(),
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
function _id(prefix) {
|
|
81
|
+
return `${prefix}-${crypto.randomUUID()}`;
|
|
82
|
+
}
|
|
83
|
+
function _now() {
|
|
84
|
+
return Date.now();
|
|
85
|
+
}
|
|
86
|
+
function _strip(row) {
|
|
87
|
+
if (!row) return null;
|
|
88
|
+
const out = {};
|
|
89
|
+
for (const [k, v] of Object.entries(row)) {
|
|
90
|
+
if (k !== "_rowid_" && k !== "rowid") out[k] = v;
|
|
91
|
+
}
|
|
92
|
+
return out;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/* ── Schema ─────────────────────────────────────────────── */
|
|
96
|
+
|
|
97
|
+
export function ensureRuntimeTables(db) {
|
|
98
|
+
db.exec(`CREATE TABLE IF NOT EXISTS runtime_plugins (
|
|
99
|
+
id TEXT PRIMARY KEY,
|
|
100
|
+
name TEXT NOT NULL,
|
|
101
|
+
version TEXT NOT NULL,
|
|
102
|
+
status TEXT NOT NULL,
|
|
103
|
+
config TEXT,
|
|
104
|
+
apis TEXT,
|
|
105
|
+
permissions TEXT,
|
|
106
|
+
loaded_at INTEGER,
|
|
107
|
+
updated_at INTEGER NOT NULL
|
|
108
|
+
)`);
|
|
109
|
+
|
|
110
|
+
db.exec(`CREATE TABLE IF NOT EXISTS runtime_updates (
|
|
111
|
+
id TEXT PRIMARY KEY,
|
|
112
|
+
plugin_id TEXT NOT NULL,
|
|
113
|
+
from_version TEXT,
|
|
114
|
+
to_version TEXT,
|
|
115
|
+
update_type TEXT,
|
|
116
|
+
status TEXT NOT NULL,
|
|
117
|
+
created_at INTEGER NOT NULL
|
|
118
|
+
)`);
|
|
119
|
+
|
|
120
|
+
db.exec(`CREATE TABLE IF NOT EXISTS runtime_profiles (
|
|
121
|
+
id TEXT PRIMARY KEY,
|
|
122
|
+
profile_type TEXT NOT NULL,
|
|
123
|
+
duration_ms INTEGER DEFAULT 0,
|
|
124
|
+
data TEXT,
|
|
125
|
+
created_at INTEGER NOT NULL
|
|
126
|
+
)`);
|
|
127
|
+
|
|
128
|
+
db.exec(`CREATE TABLE IF NOT EXISTS runtime_state (
|
|
129
|
+
state_key TEXT PRIMARY KEY,
|
|
130
|
+
state_value TEXT,
|
|
131
|
+
updated_at INTEGER NOT NULL
|
|
132
|
+
)`);
|
|
133
|
+
|
|
134
|
+
db.exec(`CREATE TABLE IF NOT EXISTS runtime_config (
|
|
135
|
+
config_key TEXT PRIMARY KEY,
|
|
136
|
+
config_value TEXT,
|
|
137
|
+
updated_at INTEGER NOT NULL
|
|
138
|
+
)`);
|
|
139
|
+
|
|
140
|
+
_loadAll(db);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function _loadAll(db) {
|
|
144
|
+
_plugins.clear();
|
|
145
|
+
_updates.clear();
|
|
146
|
+
_profiles.clear();
|
|
147
|
+
_state.clear();
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
for (const row of db.prepare("SELECT * FROM runtime_plugins").all()) {
|
|
151
|
+
const r = _strip(row);
|
|
152
|
+
_plugins.set(r.id, r);
|
|
153
|
+
}
|
|
154
|
+
} catch (_e) {
|
|
155
|
+
/* table may not exist */
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
for (const row of db.prepare("SELECT * FROM runtime_updates").all()) {
|
|
160
|
+
const r = _strip(row);
|
|
161
|
+
_updates.set(r.id, r);
|
|
162
|
+
}
|
|
163
|
+
} catch (_e) {
|
|
164
|
+
/* empty */
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
for (const row of db.prepare("SELECT * FROM runtime_profiles").all()) {
|
|
169
|
+
const r = _strip(row);
|
|
170
|
+
_profiles.set(r.id, r);
|
|
171
|
+
}
|
|
172
|
+
} catch (_e) {
|
|
173
|
+
/* empty */
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
try {
|
|
177
|
+
for (const row of db.prepare("SELECT * FROM runtime_state").all()) {
|
|
178
|
+
const r = _strip(row);
|
|
179
|
+
_state.set(r.state_key, r);
|
|
180
|
+
}
|
|
181
|
+
} catch (_e) {
|
|
182
|
+
/* empty */
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
for (const row of db.prepare("SELECT * FROM runtime_config").all()) {
|
|
187
|
+
const r = _strip(row);
|
|
188
|
+
if (r.config_key && r.config_value != null) {
|
|
189
|
+
try {
|
|
190
|
+
_config[r.config_key] = JSON.parse(r.config_value);
|
|
191
|
+
} catch (_e2) {
|
|
192
|
+
_config[r.config_key] = r.config_value;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
} catch (_e) {
|
|
197
|
+
/* empty */
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/* ── Plugin management ──────────────────────────────────── */
|
|
202
|
+
|
|
203
|
+
const VALID_PLUGIN_STATUS = new Set(Object.values(PLUGIN_STATUS));
|
|
204
|
+
|
|
205
|
+
export function loadPlugin(
|
|
206
|
+
db,
|
|
207
|
+
{ name, version, config, apis, permissions } = {},
|
|
208
|
+
) {
|
|
209
|
+
if (!name) return { loaded: false, reason: "missing_name" };
|
|
210
|
+
|
|
211
|
+
if (_plugins.size >= (_config.maxPlugins || 100)) {
|
|
212
|
+
return { loaded: false, reason: "plugin_limit_reached" };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Reject duplicate name
|
|
216
|
+
for (const p of _plugins.values()) {
|
|
217
|
+
if (p.name === name && p.status !== PLUGIN_STATUS.UNLOADED) {
|
|
218
|
+
return { loaded: false, reason: "already_loaded", pluginId: p.id };
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const id = _id("plugin");
|
|
223
|
+
const now = _now();
|
|
224
|
+
const configJson = config
|
|
225
|
+
? typeof config === "string"
|
|
226
|
+
? config
|
|
227
|
+
: JSON.stringify(config)
|
|
228
|
+
: null;
|
|
229
|
+
const apisJson = apis
|
|
230
|
+
? typeof apis === "string"
|
|
231
|
+
? apis
|
|
232
|
+
: JSON.stringify(apis)
|
|
233
|
+
: null;
|
|
234
|
+
const permsJson = permissions
|
|
235
|
+
? typeof permissions === "string"
|
|
236
|
+
? permissions
|
|
237
|
+
: JSON.stringify(permissions)
|
|
238
|
+
: null;
|
|
239
|
+
|
|
240
|
+
const entry = {
|
|
241
|
+
id,
|
|
242
|
+
name,
|
|
243
|
+
version: version || "1.0.0",
|
|
244
|
+
status: PLUGIN_STATUS.ACTIVE,
|
|
245
|
+
config: configJson,
|
|
246
|
+
apis: apisJson,
|
|
247
|
+
permissions: permsJson,
|
|
248
|
+
loaded_at: now,
|
|
249
|
+
updated_at: now,
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
db.prepare(
|
|
253
|
+
`INSERT INTO runtime_plugins (id, name, version, status, config, apis, permissions, loaded_at, updated_at)
|
|
254
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
255
|
+
).run(
|
|
256
|
+
id,
|
|
257
|
+
name,
|
|
258
|
+
entry.version,
|
|
259
|
+
entry.status,
|
|
260
|
+
configJson,
|
|
261
|
+
apisJson,
|
|
262
|
+
permsJson,
|
|
263
|
+
now,
|
|
264
|
+
now,
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
_plugins.set(id, entry);
|
|
268
|
+
_metrics.pluginsLoaded++;
|
|
269
|
+
return { loaded: true, pluginId: id };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export function unloadPlugin(db, pluginId) {
|
|
273
|
+
const p = _plugins.get(pluginId);
|
|
274
|
+
if (!p) return { unloaded: false, reason: "not_found" };
|
|
275
|
+
if (p.status === PLUGIN_STATUS.UNLOADED)
|
|
276
|
+
return { unloaded: false, reason: "already_unloaded" };
|
|
277
|
+
|
|
278
|
+
const now = _now();
|
|
279
|
+
p.status = PLUGIN_STATUS.UNLOADED;
|
|
280
|
+
p.updated_at = now;
|
|
281
|
+
|
|
282
|
+
db.prepare(
|
|
283
|
+
"UPDATE runtime_plugins SET status = ?, updated_at = ? WHERE id = ?",
|
|
284
|
+
).run(PLUGIN_STATUS.UNLOADED, now, pluginId);
|
|
285
|
+
|
|
286
|
+
_metrics.pluginsUnloaded++;
|
|
287
|
+
return { unloaded: true };
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export function setPluginStatus(db, pluginId, status) {
|
|
291
|
+
if (!VALID_PLUGIN_STATUS.has(status))
|
|
292
|
+
return { updated: false, reason: "invalid_status" };
|
|
293
|
+
const p = _plugins.get(pluginId);
|
|
294
|
+
if (!p) return { updated: false, reason: "not_found" };
|
|
295
|
+
|
|
296
|
+
const now = _now();
|
|
297
|
+
p.status = status;
|
|
298
|
+
p.updated_at = now;
|
|
299
|
+
|
|
300
|
+
db.prepare(
|
|
301
|
+
"UPDATE runtime_plugins SET status = ?, updated_at = ? WHERE id = ?",
|
|
302
|
+
).run(status, now, pluginId);
|
|
303
|
+
|
|
304
|
+
if (status === PLUGIN_STATUS.ERROR) _metrics.errors++;
|
|
305
|
+
return { updated: true };
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export function getPlugin(db, pluginId) {
|
|
309
|
+
const p = _plugins.get(pluginId);
|
|
310
|
+
return p ? { ...p } : null;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export function listPlugins(db, { status, limit = 50 } = {}) {
|
|
314
|
+
let results = [..._plugins.values()];
|
|
315
|
+
if (status) results = results.filter((p) => p.status === status);
|
|
316
|
+
return results
|
|
317
|
+
.sort((a, b) => (b.loaded_at || 0) - (a.loaded_at || 0))
|
|
318
|
+
.slice(0, limit)
|
|
319
|
+
.map((p) => ({ ...p }));
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/* ── Hot update ─────────────────────────────────────────── */
|
|
323
|
+
|
|
324
|
+
const VALID_UPDATE_TYPE = new Set(Object.values(UPDATE_TYPE));
|
|
325
|
+
|
|
326
|
+
function _inferUpdateType(from, to) {
|
|
327
|
+
if (!from || !to) return UPDATE_TYPE.PATCH;
|
|
328
|
+
const fp = String(from)
|
|
329
|
+
.split(".")
|
|
330
|
+
.map((n) => parseInt(n, 10) || 0);
|
|
331
|
+
const tp = String(to)
|
|
332
|
+
.split(".")
|
|
333
|
+
.map((n) => parseInt(n, 10) || 0);
|
|
334
|
+
while (fp.length < 3) fp.push(0);
|
|
335
|
+
while (tp.length < 3) tp.push(0);
|
|
336
|
+
if (tp[0] !== fp[0]) return UPDATE_TYPE.MAJOR;
|
|
337
|
+
if (tp[1] !== fp[1]) return UPDATE_TYPE.MINOR;
|
|
338
|
+
return UPDATE_TYPE.PATCH;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
export function hotUpdate(db, pluginId, newVersion, { updateType } = {}) {
|
|
342
|
+
const p = _plugins.get(pluginId);
|
|
343
|
+
if (!p) return { updated: false, reason: "not_found" };
|
|
344
|
+
if (!newVersion) return { updated: false, reason: "missing_version" };
|
|
345
|
+
if (!_config.hotUpdateEnabled)
|
|
346
|
+
return { updated: false, reason: "hot_update_disabled" };
|
|
347
|
+
|
|
348
|
+
const resolvedType =
|
|
349
|
+
updateType && VALID_UPDATE_TYPE.has(updateType)
|
|
350
|
+
? updateType
|
|
351
|
+
: _inferUpdateType(p.version, newVersion);
|
|
352
|
+
|
|
353
|
+
const updateId = _id("update");
|
|
354
|
+
const now = _now();
|
|
355
|
+
const fromVersion = p.version;
|
|
356
|
+
|
|
357
|
+
p.version = newVersion;
|
|
358
|
+
p.status = PLUGIN_STATUS.ACTIVE;
|
|
359
|
+
p.updated_at = now;
|
|
360
|
+
|
|
361
|
+
const entry = {
|
|
362
|
+
id: updateId,
|
|
363
|
+
plugin_id: pluginId,
|
|
364
|
+
from_version: fromVersion,
|
|
365
|
+
to_version: newVersion,
|
|
366
|
+
update_type: resolvedType,
|
|
367
|
+
status: "completed",
|
|
368
|
+
created_at: now,
|
|
369
|
+
};
|
|
370
|
+
_updates.set(updateId, entry);
|
|
371
|
+
_metrics.hotUpdates++;
|
|
372
|
+
|
|
373
|
+
db.prepare(
|
|
374
|
+
"UPDATE runtime_plugins SET version = ?, status = ?, updated_at = ? WHERE id = ?",
|
|
375
|
+
).run(newVersion, PLUGIN_STATUS.ACTIVE, now, pluginId);
|
|
376
|
+
|
|
377
|
+
db.prepare(
|
|
378
|
+
`INSERT INTO runtime_updates (id, plugin_id, from_version, to_version, update_type, status, created_at)
|
|
379
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
380
|
+
).run(
|
|
381
|
+
updateId,
|
|
382
|
+
pluginId,
|
|
383
|
+
fromVersion,
|
|
384
|
+
newVersion,
|
|
385
|
+
resolvedType,
|
|
386
|
+
"completed",
|
|
387
|
+
now,
|
|
388
|
+
);
|
|
389
|
+
|
|
390
|
+
return {
|
|
391
|
+
updated: true,
|
|
392
|
+
updateId,
|
|
393
|
+
fromVersion,
|
|
394
|
+
toVersion: newVersion,
|
|
395
|
+
updateType: resolvedType,
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
export function rollbackUpdate(db, updateId) {
|
|
400
|
+
const u = _updates.get(updateId);
|
|
401
|
+
if (!u) return { rolledBack: false, reason: "not_found" };
|
|
402
|
+
if (u.update_type === UPDATE_TYPE.ROLLBACK)
|
|
403
|
+
return { rolledBack: false, reason: "already_rollback" };
|
|
404
|
+
|
|
405
|
+
const p = _plugins.get(u.plugin_id);
|
|
406
|
+
if (!p) return { rolledBack: false, reason: "plugin_missing" };
|
|
407
|
+
|
|
408
|
+
const rollbackId = _id("update");
|
|
409
|
+
const now = _now();
|
|
410
|
+
const fromVersion = p.version;
|
|
411
|
+
const toVersion = u.from_version;
|
|
412
|
+
|
|
413
|
+
p.version = toVersion;
|
|
414
|
+
p.updated_at = now;
|
|
415
|
+
|
|
416
|
+
const entry = {
|
|
417
|
+
id: rollbackId,
|
|
418
|
+
plugin_id: u.plugin_id,
|
|
419
|
+
from_version: fromVersion,
|
|
420
|
+
to_version: toVersion,
|
|
421
|
+
update_type: UPDATE_TYPE.ROLLBACK,
|
|
422
|
+
status: "completed",
|
|
423
|
+
created_at: now,
|
|
424
|
+
};
|
|
425
|
+
_updates.set(rollbackId, entry);
|
|
426
|
+
_metrics.rollbacks++;
|
|
427
|
+
|
|
428
|
+
db.prepare(
|
|
429
|
+
"UPDATE runtime_plugins SET version = ?, updated_at = ? WHERE id = ?",
|
|
430
|
+
).run(toVersion, now, u.plugin_id);
|
|
431
|
+
|
|
432
|
+
db.prepare(
|
|
433
|
+
`INSERT INTO runtime_updates (id, plugin_id, from_version, to_version, update_type, status, created_at)
|
|
434
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
435
|
+
).run(
|
|
436
|
+
rollbackId,
|
|
437
|
+
u.plugin_id,
|
|
438
|
+
fromVersion,
|
|
439
|
+
toVersion,
|
|
440
|
+
UPDATE_TYPE.ROLLBACK,
|
|
441
|
+
"completed",
|
|
442
|
+
now,
|
|
443
|
+
);
|
|
444
|
+
|
|
445
|
+
return {
|
|
446
|
+
rolledBack: true,
|
|
447
|
+
rollbackId,
|
|
448
|
+
fromVersion,
|
|
449
|
+
toVersion,
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
export function listUpdates(db, { pluginId, limit = 50 } = {}) {
|
|
454
|
+
let results = [..._updates.values()];
|
|
455
|
+
if (pluginId) results = results.filter((u) => u.plugin_id === pluginId);
|
|
456
|
+
return results
|
|
457
|
+
.sort((a, b) => b.created_at - a.created_at)
|
|
458
|
+
.slice(0, limit)
|
|
459
|
+
.map((u) => ({ ...u }));
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/* ── Profile ────────────────────────────────────────────── */
|
|
463
|
+
|
|
464
|
+
function _sampleProfile(type, duration) {
|
|
465
|
+
const now = _now();
|
|
466
|
+
if (type === "cpu") {
|
|
467
|
+
return {
|
|
468
|
+
cores: os.cpus().length,
|
|
469
|
+
loadAverage: os.loadavg ? os.loadavg() : [0, 0, 0],
|
|
470
|
+
samples: Math.max(1, Math.floor((duration || 0) / 100)),
|
|
471
|
+
sampledAt: now,
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
if (type === "memory") {
|
|
475
|
+
const mem = process.memoryUsage();
|
|
476
|
+
return {
|
|
477
|
+
heapUsed: mem.heapUsed,
|
|
478
|
+
heapTotal: mem.heapTotal,
|
|
479
|
+
rss: mem.rss,
|
|
480
|
+
external: mem.external || 0,
|
|
481
|
+
sampledAt: now,
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
if (type === "flamegraph") {
|
|
485
|
+
return {
|
|
486
|
+
frames: [],
|
|
487
|
+
totalSamples: Math.max(1, Math.floor((duration || 0) / 10)),
|
|
488
|
+
duration,
|
|
489
|
+
sampledAt: now,
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
return { sampledAt: now };
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
export function takeProfile(db, { type, duration } = {}) {
|
|
496
|
+
const resolvedType = type || "cpu";
|
|
497
|
+
if (!PROFILE_TYPES.includes(resolvedType))
|
|
498
|
+
return { taken: false, reason: "invalid_type" };
|
|
499
|
+
|
|
500
|
+
const resolvedDuration = Number(duration) || 1000;
|
|
501
|
+
if (resolvedDuration < 0) return { taken: false, reason: "invalid_duration" };
|
|
502
|
+
|
|
503
|
+
const id = _id("profile");
|
|
504
|
+
const now = _now();
|
|
505
|
+
const data = _sampleProfile(resolvedType, resolvedDuration);
|
|
506
|
+
const dataJson = JSON.stringify(data);
|
|
507
|
+
|
|
508
|
+
const entry = {
|
|
509
|
+
id,
|
|
510
|
+
profile_type: resolvedType,
|
|
511
|
+
duration_ms: resolvedDuration,
|
|
512
|
+
data: dataJson,
|
|
513
|
+
created_at: now,
|
|
514
|
+
};
|
|
515
|
+
_profiles.set(id, entry);
|
|
516
|
+
_metrics.profilesTaken++;
|
|
517
|
+
|
|
518
|
+
db.prepare(
|
|
519
|
+
`INSERT INTO runtime_profiles (id, profile_type, duration_ms, data, created_at)
|
|
520
|
+
VALUES (?, ?, ?, ?, ?)`,
|
|
521
|
+
).run(id, resolvedType, resolvedDuration, dataJson, now);
|
|
522
|
+
|
|
523
|
+
return { taken: true, profileId: id, type: resolvedType, data };
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
export function getProfile(db, id) {
|
|
527
|
+
const p = _profiles.get(id);
|
|
528
|
+
if (!p) return null;
|
|
529
|
+
let data = null;
|
|
530
|
+
try {
|
|
531
|
+
data = p.data ? JSON.parse(p.data) : null;
|
|
532
|
+
} catch (_e) {
|
|
533
|
+
data = p.data;
|
|
534
|
+
}
|
|
535
|
+
return { ...p, data };
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
export function listProfiles(db, { type, limit = 50 } = {}) {
|
|
539
|
+
let results = [..._profiles.values()];
|
|
540
|
+
if (type) results = results.filter((p) => p.profile_type === type);
|
|
541
|
+
return results
|
|
542
|
+
.sort((a, b) => b.created_at - a.created_at)
|
|
543
|
+
.slice(0, limit)
|
|
544
|
+
.map((p) => ({ ...p }));
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/* ── State sync (CRDT-less LWW) ─────────────────────────── */
|
|
548
|
+
|
|
549
|
+
export function setState(db, key, value) {
|
|
550
|
+
if (!key) return { set: false, reason: "missing_key" };
|
|
551
|
+
const now = _now();
|
|
552
|
+
const valueJson =
|
|
553
|
+
value === undefined
|
|
554
|
+
? null
|
|
555
|
+
: typeof value === "string"
|
|
556
|
+
? value
|
|
557
|
+
: JSON.stringify(value);
|
|
558
|
+
|
|
559
|
+
const existing = _state.get(key);
|
|
560
|
+
if (existing) {
|
|
561
|
+
db.prepare(
|
|
562
|
+
"UPDATE runtime_state SET state_value = ?, updated_at = ? WHERE state_key = ?",
|
|
563
|
+
).run(valueJson, now, key);
|
|
564
|
+
} else {
|
|
565
|
+
db.prepare(
|
|
566
|
+
`INSERT INTO runtime_state (state_key, state_value, updated_at) VALUES (?, ?, ?)`,
|
|
567
|
+
).run(key, valueJson, now);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
_state.set(key, {
|
|
571
|
+
state_key: key,
|
|
572
|
+
state_value: valueJson,
|
|
573
|
+
updated_at: now,
|
|
574
|
+
});
|
|
575
|
+
_metrics.stateWrites++;
|
|
576
|
+
return { set: true, key, updatedAt: now };
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
export function getState(db, key) {
|
|
580
|
+
const s = _state.get(key);
|
|
581
|
+
if (!s) return null;
|
|
582
|
+
let value = s.state_value;
|
|
583
|
+
if (value) {
|
|
584
|
+
try {
|
|
585
|
+
value = JSON.parse(value);
|
|
586
|
+
} catch (_e) {
|
|
587
|
+
/* plain string */
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
return { key: s.state_key, value, updatedAt: s.updated_at };
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
export function listState(db, { limit = 100 } = {}) {
|
|
594
|
+
return [..._state.values()]
|
|
595
|
+
.sort((a, b) => b.updated_at - a.updated_at)
|
|
596
|
+
.slice(0, limit)
|
|
597
|
+
.map((s) => {
|
|
598
|
+
let value = s.state_value;
|
|
599
|
+
if (value) {
|
|
600
|
+
try {
|
|
601
|
+
value = JSON.parse(value);
|
|
602
|
+
} catch (_e) {
|
|
603
|
+
/* plain */
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
return { key: s.state_key, value, updatedAt: s.updated_at };
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
export function deleteState(db, key) {
|
|
611
|
+
if (!_state.has(key)) return { deleted: false, reason: "not_found" };
|
|
612
|
+
_state.delete(key);
|
|
613
|
+
db.prepare("DELETE FROM runtime_state WHERE state_key = ?").run(key);
|
|
614
|
+
return { deleted: true };
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/* ── Configure ──────────────────────────────────────────── */
|
|
618
|
+
|
|
619
|
+
export function configure(db, key, value) {
|
|
620
|
+
if (!key) return { configured: false, reason: "missing_key" };
|
|
621
|
+
const now = _now();
|
|
622
|
+
const valueJson = typeof value === "string" ? value : JSON.stringify(value);
|
|
623
|
+
|
|
624
|
+
_config[key] = value;
|
|
625
|
+
|
|
626
|
+
const existing = db
|
|
627
|
+
.prepare("SELECT config_key FROM runtime_config WHERE config_key = ?")
|
|
628
|
+
.get(key);
|
|
629
|
+
if (existing) {
|
|
630
|
+
db.prepare(
|
|
631
|
+
"UPDATE runtime_config SET config_value = ?, updated_at = ? WHERE config_key = ?",
|
|
632
|
+
).run(valueJson, now, key);
|
|
633
|
+
} else {
|
|
634
|
+
db.prepare(
|
|
635
|
+
`INSERT INTO runtime_config (config_key, config_value, updated_at) VALUES (?, ?, ?)`,
|
|
636
|
+
).run(key, valueJson, now);
|
|
637
|
+
}
|
|
638
|
+
return { configured: true, key, value };
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
export function getConfig() {
|
|
642
|
+
return { ..._config };
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/* ── Platform info ──────────────────────────────────────── */
|
|
646
|
+
|
|
647
|
+
export function getPlatformInfo() {
|
|
648
|
+
const mem = process.memoryUsage();
|
|
649
|
+
return {
|
|
650
|
+
platform: process.platform,
|
|
651
|
+
arch: process.arch,
|
|
652
|
+
nodeVersion: process.version,
|
|
653
|
+
electronVersion: process.versions?.electron || null,
|
|
654
|
+
pid: process.pid,
|
|
655
|
+
cpus: os.cpus().length,
|
|
656
|
+
totalMemoryBytes: os.totalmem ? os.totalmem() : 0,
|
|
657
|
+
freeMemoryBytes: os.freemem ? os.freemem() : 0,
|
|
658
|
+
hostname: os.hostname ? os.hostname() : null,
|
|
659
|
+
processUptimeMs: Math.floor(process.uptime() * 1000),
|
|
660
|
+
heapUsed: mem.heapUsed,
|
|
661
|
+
heapTotal: mem.heapTotal,
|
|
662
|
+
rss: mem.rss,
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/* ── Health check ───────────────────────────────────────── */
|
|
667
|
+
|
|
668
|
+
export function healthCheck() {
|
|
669
|
+
const mem = process.memoryUsage();
|
|
670
|
+
const limitBytes = (_config.memoryLimitMb || 512) * 1024 * 1024;
|
|
671
|
+
const heapRatio = limitBytes > 0 ? mem.heapUsed / limitBytes : 0;
|
|
672
|
+
|
|
673
|
+
let status = HEALTH_STATUS.HEALTHY;
|
|
674
|
+
if (heapRatio > 0.95 || _metrics.errors > 50) {
|
|
675
|
+
status = HEALTH_STATUS.CRITICAL;
|
|
676
|
+
} else if (heapRatio > 0.75 || _metrics.errors > 10) {
|
|
677
|
+
status = HEALTH_STATUS.DEGRADED;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
const activePlugins = [..._plugins.values()].filter(
|
|
681
|
+
(p) => p.status === PLUGIN_STATUS.ACTIVE,
|
|
682
|
+
).length;
|
|
683
|
+
const errorPlugins = [..._plugins.values()].filter(
|
|
684
|
+
(p) => p.status === PLUGIN_STATUS.ERROR,
|
|
685
|
+
).length;
|
|
686
|
+
|
|
687
|
+
return {
|
|
688
|
+
status,
|
|
689
|
+
uptimeMs: _now() - _metrics.startedAt,
|
|
690
|
+
memory: {
|
|
691
|
+
heapUsed: mem.heapUsed,
|
|
692
|
+
heapTotal: mem.heapTotal,
|
|
693
|
+
rss: mem.rss,
|
|
694
|
+
limitBytes,
|
|
695
|
+
heapRatio: Number(heapRatio.toFixed(4)),
|
|
696
|
+
},
|
|
697
|
+
plugins: {
|
|
698
|
+
total: _plugins.size,
|
|
699
|
+
active: activePlugins,
|
|
700
|
+
errors: errorPlugins,
|
|
701
|
+
},
|
|
702
|
+
errors: _metrics.errors,
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/* ── Metrics ────────────────────────────────────────────── */
|
|
707
|
+
|
|
708
|
+
export function getMetrics() {
|
|
709
|
+
return {
|
|
710
|
+
..._metrics,
|
|
711
|
+
uptimeMs: _now() - _metrics.startedAt,
|
|
712
|
+
activePlugins: [..._plugins.values()].filter(
|
|
713
|
+
(p) => p.status === PLUGIN_STATUS.ACTIVE,
|
|
714
|
+
).length,
|
|
715
|
+
totalPlugins: _plugins.size,
|
|
716
|
+
totalUpdates: _updates.size,
|
|
717
|
+
totalProfiles: _profiles.size,
|
|
718
|
+
stateKeys: _state.size,
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
/* ── Stats (catalog counts) ─────────────────────────────── */
|
|
723
|
+
|
|
724
|
+
export function getRuntimeStats() {
|
|
725
|
+
const byPluginStatus = {};
|
|
726
|
+
for (const s of Object.values(PLUGIN_STATUS)) byPluginStatus[s] = 0;
|
|
727
|
+
for (const p of _plugins.values())
|
|
728
|
+
byPluginStatus[p.status] = (byPluginStatus[p.status] || 0) + 1;
|
|
729
|
+
|
|
730
|
+
const byUpdateType = {};
|
|
731
|
+
for (const t of Object.values(UPDATE_TYPE)) byUpdateType[t] = 0;
|
|
732
|
+
for (const u of _updates.values())
|
|
733
|
+
byUpdateType[u.update_type] = (byUpdateType[u.update_type] || 0) + 1;
|
|
734
|
+
|
|
735
|
+
const byProfileType = {};
|
|
736
|
+
for (const t of PROFILE_TYPES) byProfileType[t] = 0;
|
|
737
|
+
for (const p of _profiles.values())
|
|
738
|
+
byProfileType[p.profile_type] = (byProfileType[p.profile_type] || 0) + 1;
|
|
739
|
+
|
|
740
|
+
return {
|
|
741
|
+
plugins: _plugins.size,
|
|
742
|
+
updates: _updates.size,
|
|
743
|
+
profiles: _profiles.size,
|
|
744
|
+
state: _state.size,
|
|
745
|
+
byPluginStatus,
|
|
746
|
+
byUpdateType,
|
|
747
|
+
byProfileType,
|
|
748
|
+
metrics: getMetrics(),
|
|
749
|
+
health: healthCheck(),
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
/* ── Reset (tests) ──────────────────────────────────────── */
|
|
754
|
+
|
|
755
|
+
export function _resetState() {
|
|
756
|
+
_plugins.clear();
|
|
757
|
+
_updates.clear();
|
|
758
|
+
_profiles.clear();
|
|
759
|
+
_state.clear();
|
|
760
|
+
_config = { ...DEFAULT_CONFIG };
|
|
761
|
+
_metrics = {
|
|
762
|
+
pluginsLoaded: 0,
|
|
763
|
+
pluginsUnloaded: 0,
|
|
764
|
+
hotUpdates: 0,
|
|
765
|
+
rollbacks: 0,
|
|
766
|
+
profilesTaken: 0,
|
|
767
|
+
stateWrites: 0,
|
|
768
|
+
errors: 0,
|
|
769
|
+
startedAt: Date.now(),
|
|
770
|
+
};
|
|
771
|
+
}
|