chainlesschain 0.47.9 → 0.49.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.
Files changed (70) hide show
  1. package/bin/chainlesschain.js +0 -0
  2. package/package.json +1 -1
  3. package/src/assets/web-panel/.build-hash +1 -1
  4. package/src/assets/web-panel/assets/{AppLayout-6SPt_8Y_.js → AppLayout-Rvi759IS.js} +1 -1
  5. package/src/assets/web-panel/assets/Dashboard-BS-tzGNj.css +1 -0
  6. package/src/assets/web-panel/assets/{Dashboard-Br7kCwKJ.js → Dashboard-DBhFxXYQ.js} +2 -2
  7. package/src/assets/web-panel/assets/{index-tN-8TosE.js → index-uL0cZ8N_.js} +2 -2
  8. package/src/assets/web-panel/index.html +2 -2
  9. package/src/commands/codegen.js +303 -0
  10. package/src/commands/collab.js +482 -0
  11. package/src/commands/crosschain.js +382 -0
  12. package/src/commands/dbevo.js +388 -0
  13. package/src/commands/dev.js +411 -0
  14. package/src/commands/federation.js +427 -0
  15. package/src/commands/fusion.js +332 -0
  16. package/src/commands/governance.js +505 -0
  17. package/src/commands/hardening.js +110 -0
  18. package/src/commands/incentive.js +373 -0
  19. package/src/commands/inference.js +304 -0
  20. package/src/commands/infra.js +361 -0
  21. package/src/commands/kg.js +371 -0
  22. package/src/commands/marketplace.js +326 -0
  23. package/src/commands/mcp.js +97 -18
  24. package/src/commands/nlprog.js +329 -0
  25. package/src/commands/ops.js +408 -0
  26. package/src/commands/perception.js +385 -0
  27. package/src/commands/pqc.js +34 -0
  28. package/src/commands/privacy.js +345 -0
  29. package/src/commands/quantization.js +280 -0
  30. package/src/commands/recommend.js +336 -0
  31. package/src/commands/reputation.js +349 -0
  32. package/src/commands/runtime.js +500 -0
  33. package/src/commands/sla.js +352 -0
  34. package/src/commands/stress.js +252 -0
  35. package/src/commands/tech.js +268 -0
  36. package/src/commands/tenant.js +576 -0
  37. package/src/commands/trust.js +366 -0
  38. package/src/harness/mcp-client.js +330 -54
  39. package/src/index.js +112 -0
  40. package/src/lib/aiops.js +523 -0
  41. package/src/lib/autonomous-developer.js +524 -0
  42. package/src/lib/code-agent.js +442 -0
  43. package/src/lib/collaboration-governance.js +556 -0
  44. package/src/lib/community-governance.js +649 -0
  45. package/src/lib/content-recommendation.js +600 -0
  46. package/src/lib/cross-chain.js +669 -0
  47. package/src/lib/dbevo.js +669 -0
  48. package/src/lib/decentral-infra.js +445 -0
  49. package/src/lib/federation-hardening.js +587 -0
  50. package/src/lib/hardening-manager.js +409 -0
  51. package/src/lib/inference-network.js +407 -0
  52. package/src/lib/knowledge-graph.js +530 -0
  53. package/src/lib/mcp-client.js +3 -0
  54. package/src/lib/multimodal.js +698 -0
  55. package/src/lib/nl-programming.js +595 -0
  56. package/src/lib/perception.js +500 -0
  57. package/src/lib/pqc-manager.js +141 -9
  58. package/src/lib/privacy-computing.js +575 -0
  59. package/src/lib/protocol-fusion.js +535 -0
  60. package/src/lib/quantization.js +362 -0
  61. package/src/lib/reputation-optimizer.js +509 -0
  62. package/src/lib/skill-marketplace.js +397 -0
  63. package/src/lib/sla-manager.js +484 -0
  64. package/src/lib/stress-tester.js +383 -0
  65. package/src/lib/tech-learning-engine.js +651 -0
  66. package/src/lib/tenant-saas.js +831 -0
  67. package/src/lib/token-incentive.js +513 -0
  68. package/src/lib/trust-security.js +473 -0
  69. package/src/lib/universal-runtime.js +771 -0
  70. 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
+ }