@camstack/system 1.2.38 → 1.2.40

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 (60) hide show
  1. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  2. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  3. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  4. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  5. package/dist/builtins/alerts/alerts.addon.js +1 -1
  6. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  7. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
  8. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
  9. package/dist/builtins/console-logging/index.js +1 -1
  10. package/dist/builtins/console-logging/index.mjs +1 -1
  11. package/dist/builtins/core-blocks/block-package.d.ts +59 -0
  12. package/dist/builtins/core-blocks/block-supervisor.d.ts +86 -0
  13. package/dist/builtins/core-blocks/core-blocks.addon.d.ts +18 -0
  14. package/dist/builtins/core-blocks/core-blocks.addon.js +533 -68
  15. package/dist/builtins/core-blocks/core-blocks.addon.mjs +528 -69
  16. package/dist/builtins/core-blocks/index.d.ts +5 -3
  17. package/dist/builtins/core-blocks/index.js +6 -0
  18. package/dist/builtins/core-blocks/index.mjs +2 -2
  19. package/dist/builtins/device-manager/device-manager.addon.js +14 -6
  20. package/dist/builtins/device-manager/device-manager.addon.mjs +14 -6
  21. package/dist/builtins/doorbell/virtual-doorbell.addon.js +1 -1
  22. package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +1 -1
  23. package/dist/builtins/hub-forwarder/index.js +1 -1
  24. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  25. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  26. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  27. package/dist/builtins/local-auth/local-auth.addon.js +1 -1
  28. package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
  29. package/dist/builtins/local-network/local-network.addon.js +1 -1
  30. package/dist/builtins/local-network/local-network.addon.mjs +1 -1
  31. package/dist/builtins/loki-logging/index.js +1 -1
  32. package/dist/builtins/loki-logging/index.mjs +1 -1
  33. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
  34. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  35. package/dist/builtins/platform-probe/index.js +1 -1
  36. package/dist/builtins/platform-probe/index.mjs +1 -1
  37. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  38. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  39. package/dist/builtins/snapshot/index.js +281 -16
  40. package/dist/builtins/snapshot/index.mjs +282 -17
  41. package/dist/builtins/snapshot/snapshot-cache.d.ts +45 -0
  42. package/dist/builtins/snapshot/snapshot-media-handler.d.ts +32 -2
  43. package/dist/builtins/snapshot/snapshot-resize.d.ts +50 -0
  44. package/dist/builtins/snapshot/snapshot.addon.d.ts +13 -0
  45. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  46. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  47. package/dist/builtins/sqlite-storage/integration-registry.d.ts +1 -1
  48. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +1 -1
  49. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +1 -1
  50. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
  51. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
  52. package/dist/builtins/system-config/system-config.addon.js +1 -1
  53. package/dist/builtins/system-config/system-config.addon.mjs +1 -1
  54. package/dist/builtins/winston-logging/index.js +1 -1
  55. package/dist/builtins/winston-logging/index.mjs +1 -1
  56. package/dist/{dist-DO76YVO2.mjs → dist-BXS0DP92.mjs} +24 -2
  57. package/dist/{dist-CFinlH7b.js → dist-Cj0LAuXi.js} +24 -2
  58. package/dist/index.js +2 -2
  59. package/dist/index.mjs +2 -2
  60. package/package.json +1 -1
@@ -1,8 +1,407 @@
1
1
  import { o as __toESM } from "../../chunk-CNf5ZN-e.mjs";
2
- import { S as coreBlocksCapability, ct as BaseAddon, o as CoreBlockSchema } from "../../dist-DO76YVO2.mjs";
2
+ import { S as coreBlocksCapability, ct as BaseAddon, o as CoreBlockSchema } from "../../dist-BXS0DP92.mjs";
3
3
  import * as path$1 from "node:path";
4
4
  import { randomUUID } from "node:crypto";
5
5
  import * as fs from "node:fs/promises";
6
+ //#region src/builtins/core-blocks/block-package.ts
7
+ /**
8
+ * Turning a stored block into something the addon runner can fork.
9
+ *
10
+ * The runner already knows how to do everything a block needs — fork a
11
+ * process, build the full UDS `ctx`, redirect host-provided imports, and hand
12
+ * the whole thing to `CrashSupervisor` (D6). What it does NOT know how to do is
13
+ * load code out of a database row: `addon-runner` reads a package.json off
14
+ * disk. So a block is MATERIALISED as a minimal addon package and spawned like
15
+ * any other addon. No second runner, no second supervisor, no `node:vm`.
16
+ *
17
+ * Two files are generated per block:
18
+ *
19
+ * `dist/block.js` — the author's TypeScript, transpiled.
20
+ * `dist/index.js` — a generated wrapper that adapts the author's contract
21
+ * (`export default (ctx) => disposer`) to `ICamstackAddon`.
22
+ *
23
+ * The wrapper exists so the author never writes `super({})`, which is the trap
24
+ * that killed `CoreBlocksAddon` in production while the build stayed green and
25
+ * the topology reported it running.
26
+ */
27
+ /** Prefix for the generated addon id AND the runner id — one addon, one
28
+ * process, so the two are the same string (D2). Grep-able on purpose: an
29
+ * operator reading `$process.list` must be able to tell a block runner from
30
+ * an addon runner at a glance. */
31
+ var BLOCK_ADDON_PREFIX = "core-block-";
32
+ /** The generated addon/runner id for a block. */
33
+ function blockAddonId(blockId) {
34
+ return `${BLOCK_ADDON_PREFIX}${blockId}`;
35
+ }
36
+ /** The block id behind a generated addon/runner id, or null when the id
37
+ * belongs to something else. The inverse of {@link blockAddonId} — used to
38
+ * read the live process list back into block terms. */
39
+ function blockIdFromAddonId(addonId) {
40
+ if (!addonId.startsWith("core-block-")) return null;
41
+ const rest = addonId.slice(11);
42
+ return rest.length > 0 ? rest : null;
43
+ }
44
+ /**
45
+ * The wrapper module, generated verbatim.
46
+ *
47
+ * A string rather than a file copied from `dist` because it must land NEXT to
48
+ * the author's code inside the block package: the runner imports one entry, and
49
+ * relative imports have to resolve inside that directory.
50
+ *
51
+ * Three things it guarantees, all of them failures this repo has already paid
52
+ * for once:
53
+ *
54
+ * - `super({})` is here, so no author can omit it;
55
+ * - a module with no default function FAILS LOUDLY with the block's name in
56
+ * the message, instead of starting a process that does nothing;
57
+ * - the disposer is awaited on shutdown, so a block that owns a timer or a
58
+ * socket releases it on `setEnabled(false)` rather than on the next reboot.
59
+ */
60
+ var WRAPPER_SOURCE = `// GENERATED by @camstack/system core-blocks. Do not edit — rewritten on every
61
+ // deploy of the block it wraps.
62
+ import { BaseAddon } from '@camstack/types'
63
+ // NAMESPACE import, not \`import blockMain from\`. A default import of a module
64
+ // that has no default export is a LINK-time SyntaxError: the module never
65
+ // executes, so the friendly check below would never run and the author would
66
+ // get a bare Node stack trace naming neither their block nor the mistake.
67
+ // Verified, not assumed — a test spawns exactly that module.
68
+ import * as blockModule from './block.js'
69
+
70
+ const BLOCK_ID = __BLOCK_ID__
71
+ const BLOCK_NAME = __BLOCK_NAME__
72
+ const blockMain = blockModule.default
73
+
74
+ export default class CoreBlockAddon extends BaseAddon {
75
+ constructor() {
76
+ // NOT optional: BaseAddon.resolveConfig reads Object.keys(this.defaults),
77
+ // so omitting this kills the process at init while the topology still
78
+ // reports the addon running.
79
+ super({})
80
+ this.disposer = null
81
+ }
82
+
83
+ async onInitialize() {
84
+ if (typeof blockMain !== 'function') {
85
+ throw new Error(
86
+ \`core block "\${BLOCK_NAME}" (\${BLOCK_ID}) has no default export function — \` +
87
+ 'a block must "export default" a function taking ctx',
88
+ )
89
+ }
90
+ const result = await blockMain(this.ctx)
91
+ this.disposer = typeof result === 'function' ? result : null
92
+ // One line, always. A block that started and does nothing visible is
93
+ // indistinguishable from one that never loaded without it.
94
+ this.ctx.logger.info('core block started', {
95
+ meta: { blockId: BLOCK_ID, name: BLOCK_NAME, hasDisposer: this.disposer !== null },
96
+ })
97
+ return []
98
+ }
99
+
100
+ async onShutdown() {
101
+ if (this.disposer === null) return
102
+ const dispose = this.disposer
103
+ this.disposer = null
104
+ await dispose()
105
+ }
106
+ }
107
+ `;
108
+ /** Build the wrapper for one block. Exported for the test that asserts the
109
+ * generated source actually carries the block's identity. */
110
+ function renderBlockWrapper(blockId, blockName) {
111
+ return WRAPPER_SOURCE.replace("__BLOCK_ID__", JSON.stringify(blockId)).replace("__BLOCK_NAME__", JSON.stringify(blockName));
112
+ }
113
+ /** The package manifest the addon runner reads to find the entry point. */
114
+ function renderBlockManifest(blockId, blockName) {
115
+ return `${JSON.stringify({
116
+ name: `@camstack/${blockAddonId(blockId)}`,
117
+ version: "0.0.0",
118
+ private: true,
119
+ type: "module",
120
+ main: "./dist/index.js",
121
+ camstack: { addons: [{
122
+ id: blockAddonId(blockId),
123
+ name: blockName,
124
+ category: "system",
125
+ description: "A user-authored core block.",
126
+ entry: "./dist/index.js",
127
+ capabilities: []
128
+ }] }
129
+ }, null, 2)}\n`;
130
+ }
131
+ /**
132
+ * Write (or rewrite) a block's package. Idempotent, and deliberately a full
133
+ * rewrite: the author's code is the thing that changes, and a partial update
134
+ * that left a stale `block.js` behind would run yesterday's code while the
135
+ * editor showed today's.
136
+ */
137
+ async function materializeBlock(input) {
138
+ const addonDir = path$1.join(input.root, input.blockId);
139
+ const distDir = path$1.join(addonDir, "dist");
140
+ await fs.mkdir(distDir, { recursive: true });
141
+ await Promise.all([
142
+ fs.writeFile(path$1.join(addonDir, "package.json"), renderBlockManifest(input.blockId, input.blockName), "utf-8"),
143
+ fs.writeFile(path$1.join(distDir, "block.js"), input.js, "utf-8"),
144
+ fs.writeFile(path$1.join(distDir, "index.js"), renderBlockWrapper(input.blockId, input.blockName), "utf-8")
145
+ ]);
146
+ return {
147
+ addonDir,
148
+ addonId: blockAddonId(input.blockId)
149
+ };
150
+ }
151
+ /** Remove a block's package. Idempotent — a missing directory is the desired
152
+ * end state, not an error. */
153
+ async function removeBlockPackage(root, blockId) {
154
+ await fs.rm(path$1.join(root, blockId), {
155
+ recursive: true,
156
+ force: true
157
+ });
158
+ }
159
+ //#endregion
160
+ //#region src/builtins/core-blocks/block-supervisor.ts
161
+ /** Runner state → what the author should read. `crashed` is the window between
162
+ * an exit and the scheduled respawn, so it reads as `starting` with the reason
163
+ * attached rather than as a terminal failure it is not. */
164
+ var RUNNER_STATE_TO_STATUS = {
165
+ starting: "starting",
166
+ running: "running",
167
+ crashed: "starting",
168
+ failed: "failed",
169
+ stopping: null,
170
+ stopped: null
171
+ };
172
+ var BlockSupervisor = class {
173
+ deps;
174
+ /**
175
+ * `blockId → updatedAt` of the source this supervisor last materialised.
176
+ * The only way to know a running block is running YESTERDAY'S code: the
177
+ * process is alive either way, and nothing on disk carries the version.
178
+ * In-memory is correct — a hub restart takes every child with it.
179
+ */
180
+ materialized = /* @__PURE__ */ new Map();
181
+ timer = null;
182
+ /** Serialises passes so a timer tick cannot interleave with a write-driven
183
+ * converge and spawn the same runner twice. */
184
+ inFlight = Promise.resolve();
185
+ constructor(deps) {
186
+ this.deps = deps;
187
+ }
188
+ /** Start the reconcile timer. The first pass runs immediately. */
189
+ start(intervalMs) {
190
+ if (this.timer !== null) return;
191
+ this.converge();
192
+ this.timer = setInterval(() => void this.converge(), intervalMs);
193
+ this.timer.unref?.();
194
+ }
195
+ stop() {
196
+ if (this.timer === null) return;
197
+ clearInterval(this.timer);
198
+ this.timer = null;
199
+ }
200
+ /** One reconcile pass. Never throws — a pass that failed must not stop the
201
+ * next one, and the reason belongs in the log, not in a rejected promise
202
+ * nobody awaits. */
203
+ converge() {
204
+ this.inFlight = this.inFlight.then(() => this.runPass(), () => this.runPass());
205
+ return this.inFlight;
206
+ }
207
+ async runPass() {
208
+ try {
209
+ await this.reconcile();
210
+ } catch (err) {
211
+ this.deps.logger.error("core-blocks converge failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
212
+ }
213
+ }
214
+ async reconcile() {
215
+ const blocks = this.deps.store.list();
216
+ const mine = blocks.filter((b) => b.enabled && this.isPlacedHere(b));
217
+ const runners = new Map((await this.deps.host.list()).map((r) => [blockIdFromAddonId(r.name), r]).filter((pair) => pair[0] !== null));
218
+ for (const block of blocks) {
219
+ if (!block.enabled || this.isPlacedHere(block)) continue;
220
+ await this.report(block.id, "failed", `no core-block supervisor runs on "${block.placement}" — this block is not running anywhere. Move it to "${this.deps.nodeId}" to run it.`);
221
+ }
222
+ const wanted = new Set(mine.map((b) => b.id));
223
+ for (const [blockId, runner] of runners) {
224
+ if (wanted.has(blockId)) continue;
225
+ this.deps.logger.info("stopping core block runner", { meta: {
226
+ blockId,
227
+ runnerState: runner.state,
228
+ reason: this.stopReason(blockId)
229
+ } });
230
+ await this.deps.host.stop(blockAddonId(blockId));
231
+ this.materialized.delete(blockId);
232
+ if (this.deps.store.get(blockId) !== null) await this.report(blockId, "stopped");
233
+ else await removeBlockPackage(this.deps.root, blockId);
234
+ }
235
+ for (const block of mine) {
236
+ const runner = runners.get(block.id);
237
+ const stale = runner !== void 0 && this.materialized.get(block.id) !== block.updatedAt;
238
+ if (runner !== void 0 && !stale) {
239
+ await this.reportRunnerState(block, runner);
240
+ continue;
241
+ }
242
+ if (stale) {
243
+ this.deps.logger.info("core block code changed — restarting its runner", { meta: {
244
+ blockId: block.id,
245
+ name: block.name
246
+ } });
247
+ await this.deps.host.stop(blockAddonId(block.id));
248
+ this.materialized.delete(block.id);
249
+ }
250
+ await this.launch(block);
251
+ }
252
+ }
253
+ /** Compile, write the package, spawn. Each failure is reported ON the block
254
+ * — the author asked for this to run, so they are owed the reason it does
255
+ * not. */
256
+ async launch(block) {
257
+ const compiled = await this.deps.compile(block.code);
258
+ if (!compiled.ok || compiled.js === void 0) {
259
+ await this.report(block.id, "failed", `the block does not compile: ${compiled.error ?? "unknown"}`);
260
+ return;
261
+ }
262
+ try {
263
+ const pkg = await materializeBlock({
264
+ root: this.deps.root,
265
+ blockId: block.id,
266
+ blockName: block.name,
267
+ js: compiled.js
268
+ });
269
+ await this.deps.host.spawn({
270
+ runnerId: pkg.addonId,
271
+ addonId: pkg.addonId,
272
+ addonDir: pkg.addonDir
273
+ });
274
+ this.materialized.set(block.id, block.updatedAt);
275
+ this.deps.logger.info("core block runner spawned", { meta: {
276
+ blockId: block.id,
277
+ name: block.name,
278
+ runnerId: pkg.addonId
279
+ } });
280
+ await this.report(block.id, "starting");
281
+ } catch (err) {
282
+ const message = err instanceof Error ? err.message : String(err);
283
+ if (/already running/i.test(message)) {
284
+ this.materialized.set(block.id, block.updatedAt);
285
+ await this.report(block.id, "starting");
286
+ return;
287
+ }
288
+ this.deps.logger.error("core block failed to start", { meta: {
289
+ blockId: block.id,
290
+ name: block.name,
291
+ error: message
292
+ } });
293
+ await this.report(block.id, "failed", `could not start the block: ${message}`);
294
+ }
295
+ }
296
+ async reportRunnerState(block, runner) {
297
+ const status = RUNNER_STATE_TO_STATUS[runner.state];
298
+ if (status === null) return;
299
+ if (runner.state === "failed") {
300
+ await this.report(block.id, "failed", "the crash circuit-breaker tripped — the block crashed repeatedly and will not be respawned. Fix it and save, or disable and re-enable it.");
301
+ return;
302
+ }
303
+ if (runner.state === "crashed") {
304
+ await this.report(block.id, "starting", "the block crashed — a respawn is scheduled");
305
+ return;
306
+ }
307
+ await this.report(block.id, status);
308
+ }
309
+ /**
310
+ * Write a status only when it actually changed.
311
+ *
312
+ * A reconcile pass runs on a timer, and every write rewrites the row AND
313
+ * moves `lastChangedAt` — which is the only thing telling an operator WHEN a
314
+ * block last did something. Rewriting it every tick would make it read as
315
+ * "just now", forever.
316
+ */
317
+ async report(blockId, status, error) {
318
+ const current = this.deps.store.get(blockId);
319
+ if (current !== null && current.status === status && current.lastError === error) return;
320
+ await this.deps.store.setStatus(blockId, status, error);
321
+ }
322
+ isPlacedHere(block) {
323
+ return block.placement === this.deps.nodeId;
324
+ }
325
+ stopReason(blockId) {
326
+ const block = this.deps.store.get(blockId);
327
+ if (block === null) return "deleted";
328
+ if (!block.enabled) return "disabled";
329
+ return `moved to "${block.placement}"`;
330
+ }
331
+ };
332
+ //#endregion
333
+ //#region src/builtins/core-blocks/block-type-defs.ts
334
+ /**
335
+ * The declaration files a block is authored against.
336
+ *
337
+ * Served from the node rather than bundled into the admin UI: the graph is
338
+ * 3.4 MB across 344 files. Served rather than hand-stubbed: a block runs with
339
+ * the same `ctx` an addon gets, so a stub would drift from it and the editor
340
+ * would confidently autocomplete methods that do not exist.
341
+ *
342
+ * The paths matter more than they look. Monaco resolves imports against the
343
+ * extra-lib paths, so a file registered as anything other than
344
+ * `file:///node_modules/@camstack/types/…` loads and then resolves NOTHING —
345
+ * the failure that looks most like success, because the editor keeps working
346
+ * and simply stops knowing anything.
347
+ */
348
+ /** Where Monaco must think the files live for `@camstack/types` to resolve. */
349
+ var VIRTUAL_ROOT = "file:///node_modules/@camstack/types";
350
+ /**
351
+ * Read every `.d.ts` under the installed `@camstack/types` dist.
352
+ *
353
+ * Best-effort by design: a node that cannot find them serves an EMPTY list, and
354
+ * the editor then registers an empty lib set rather than keeping a stale one —
355
+ * so an author sees "no types" instead of validating against a contract that
356
+ * has moved.
357
+ */
358
+ async function loadBlockTypeDefs(logger) {
359
+ const root = await resolveTypesDist();
360
+ if (root === null) {
361
+ logger.warn("core-blocks: @camstack/types declarations not found — the editor gets no types");
362
+ return [];
363
+ }
364
+ const out = [];
365
+ await walk(root, root, out);
366
+ return out;
367
+ }
368
+ async function resolveTypesDist() {
369
+ try {
370
+ const entry = await import.meta.resolve?.("@camstack/types");
371
+ if (typeof entry === "string") {
372
+ const dir = path$1.dirname(entry.replace("file://", ""));
373
+ if (await exists(path$1.join(dir, "index.d.ts"))) return dir;
374
+ }
375
+ } catch {}
376
+ const guess = path$1.resolve(process.cwd(), "node_modules/@camstack/types/dist");
377
+ return await exists(guess) ? guess : null;
378
+ }
379
+ async function exists(p) {
380
+ try {
381
+ await fs.access(p);
382
+ return true;
383
+ } catch {
384
+ return false;
385
+ }
386
+ }
387
+ async function walk(root, dir, out) {
388
+ const entries = await fs.readdir(dir, { withFileTypes: true });
389
+ for (const entry of entries) {
390
+ const full = path$1.join(dir, entry.name);
391
+ if (entry.isDirectory()) {
392
+ await walk(root, full, out);
393
+ continue;
394
+ }
395
+ if (!entry.name.endsWith(".d.ts")) continue;
396
+ const content = await fs.readFile(full, "utf-8");
397
+ const relative = path$1.relative(root, full).split(path$1.sep).join("/");
398
+ out.push({
399
+ filePath: `${VIRTUAL_ROOT}/${relative}`,
400
+ content
401
+ });
402
+ }
403
+ }
404
+ //#endregion
6
405
  //#region src/builtins/core-blocks/compile-block.ts
7
406
  /** An esbuild error carries the position; the shape is narrow enough to read
8
407
  * defensively rather than to type against the package. */
@@ -55,6 +454,19 @@ async function compileBlock(transpiler, code) {
55
454
  }
56
455
  //#endregion
57
456
  //#region src/builtins/core-blocks/core-block-store.ts
457
+ /**
458
+ * Durable storage for core blocks.
459
+ *
460
+ * A kernel table, not an addon's: a block is not owned by any addon — it is
461
+ * user code the kernel runs. The name is bare for the same reason `users` and
462
+ * `system-settings` are (the collection-namespacing guard scans
463
+ * `packages/addon-*` only, and this is deliberately not one).
464
+ *
465
+ * The write path is the interesting part. **Source is never stored without
466
+ * compiling it first.** A block that does not compile is not a broken row, it
467
+ * is a fork failure the operator meets minutes later inside a log — so the
468
+ * compile happens where they are still looking at the editor.
469
+ */
58
470
  var CORE_BLOCKS_COLLECTION = "core_blocks";
59
471
  var CORE_BLOCKS_COLUMNS = [
60
472
  {
@@ -200,7 +612,7 @@ var CoreBlockStore = class {
200
612
  lastChangedAt: this.now(),
201
613
  ...error !== void 0 ? { lastError: error } : {}
202
614
  };
203
- if (error === void 0 && next.lastError !== void 0 && status === "running") delete next.lastError;
615
+ if (error === void 0 && next.lastError !== void 0) delete next.lastError;
204
616
  await this.persist(next);
205
617
  this.byId.set(blockId, next);
206
618
  }
@@ -227,78 +639,34 @@ var CoreBlockStore = class {
227
639
  }
228
640
  };
229
641
  //#endregion
230
- //#region src/builtins/core-blocks/block-type-defs.ts
231
- /** Where Monaco must think the files live for `@camstack/types` to resolve. */
232
- var VIRTUAL_ROOT = "file:///node_modules/@camstack/types";
233
- /**
234
- * Read every `.d.ts` under the installed `@camstack/types` dist.
235
- *
236
- * Best-effort by design: a node that cannot find them serves an EMPTY list, and
237
- * the editor then registers an empty lib set rather than keeping a stale one —
238
- * so an author sees "no types" instead of validating against a contract that
239
- * has moved.
240
- */
241
- async function loadBlockTypeDefs(logger) {
242
- const root = await resolveTypesDist();
243
- if (root === null) {
244
- logger.warn("core-blocks: @camstack/types declarations not found — the editor gets no types");
245
- return [];
246
- }
247
- const out = [];
248
- await walk(root, root, out);
249
- return out;
250
- }
251
- async function resolveTypesDist() {
252
- try {
253
- const entry = await import.meta.resolve?.("@camstack/types");
254
- if (typeof entry === "string") {
255
- const dir = path$1.dirname(entry.replace("file://", ""));
256
- if (await exists(path$1.join(dir, "index.d.ts"))) return dir;
257
- }
258
- } catch {}
259
- const guess = path$1.resolve(process.cwd(), "node_modules/@camstack/types/dist");
260
- return await exists(guess) ? guess : null;
261
- }
262
- async function exists(p) {
263
- try {
264
- await fs.access(p);
265
- return true;
266
- } catch {
267
- return false;
268
- }
269
- }
270
- async function walk(root, dir, out) {
271
- const entries = await fs.readdir(dir, { withFileTypes: true });
272
- for (const entry of entries) {
273
- const full = path$1.join(dir, entry.name);
274
- if (entry.isDirectory()) {
275
- await walk(root, full, out);
276
- continue;
277
- }
278
- if (!entry.name.endsWith(".d.ts")) continue;
279
- const content = await fs.readFile(full, "utf-8");
280
- const relative = path$1.relative(root, full).split(path$1.sep).join("/");
281
- out.push({
282
- filePath: `${VIRTUAL_ROOT}/${relative}`,
283
- content
284
- });
285
- }
286
- }
287
- //#endregion
288
642
  //#region src/builtins/core-blocks/core-blocks.addon.ts
289
643
  /**
290
644
  * The `core-blocks` provider — CRUD over user-authored TypeScript, with the
291
- * compile gate in front of every write.
645
+ * compile gate in front of every write, and the supervisor that turns a stored
646
+ * block into a running process.
292
647
  *
293
648
  * A builtin because a block belongs to the kernel, not to an addon: it is user
294
649
  * code the node runs, and it must exist on every install with no opt-in step.
295
650
  *
296
- * **This slice stores and serves. It does not EXECUTE.** The runner (fork per
297
- * block, `ctx` over UDS, `CrashSupervisor`) is the next piece, and a block
298
- * therefore sits at `status: 'stopped'` no matter what it says. That is stated
299
- * here rather than implied because a system that accepted code and quietly
300
- * never ran it would be the exact failure this repo keeps paying for.
651
+ * **Execution is HUB-ONLY for now**, and that limit is loud rather than
652
+ * implied: a block placed on any other node is reported `failed` with a message
653
+ * naming the node, because an enabled block nothing runs must never look like
654
+ * an idle one. The supervisor is already written per-node (`converge()` only
655
+ * ever considers blocks placed on `nodeId`), so the agent case is an
656
+ * implementation of `BlockProcessHost` plus a way to reach the hub's store —
657
+ * not a redesign.
658
+ */
659
+ /**
660
+ * How often the supervisor re-reads the world.
661
+ *
662
+ * It is not the primary trigger — every write converges immediately — but the
663
+ * one that catches what a write cannot: a runner that crashed, a breaker that
664
+ * tripped, a spawn that never landed. D8: events may be dropped, so the
665
+ * reconcile is what makes the outcome eventually true.
301
666
  */
667
+ var CONVERGE_INTERVAL_MS = 15e3;
668
+ /** Where block packages are materialised, under this node's data root. */
669
+ var BLOCK_PACKAGE_DIRNAME = "core-blocks";
302
670
  var CoreBlocksAddon = class extends BaseAddon {
303
671
  /**
304
672
  * `super({})` is NOT optional, and its absence is invisible until runtime:
@@ -313,6 +681,7 @@ var CoreBlocksAddon = class extends BaseAddon {
313
681
  store = null;
314
682
  transpiler = null;
315
683
  typeDefs = null;
684
+ supervisor = null;
316
685
  async onInitialize() {
317
686
  const settingsStore = this.ctx.api?.settingsStore;
318
687
  if (!settingsStore) throw new Error("core-blocks: settings-store API not available — refusing to register the cap");
@@ -323,9 +692,12 @@ var CoreBlocksAddon = class extends BaseAddon {
323
692
  });
324
693
  await this.store.load();
325
694
  this.transpiler = await loadTranspiler(this.ctx.logger);
695
+ this.supervisor = this.buildSupervisor();
696
+ this.supervisor?.start(CONVERGE_INTERVAL_MS);
326
697
  this.ctx.logger.info("core-blocks ready", { meta: {
327
698
  blocks: this.store.list().length,
328
- compile: this.transpiler !== null
699
+ compile: this.transpiler !== null,
700
+ supervising: this.supervisor !== null
329
701
  } });
330
702
  return [{
331
703
  capability: coreBlocksCapability,
@@ -333,10 +705,65 @@ var CoreBlocksAddon = class extends BaseAddon {
333
705
  }];
334
706
  }
335
707
  async onShutdown() {
708
+ this.supervisor?.stop();
709
+ this.supervisor = null;
336
710
  this.store = null;
337
711
  this.transpiler = null;
338
712
  this.typeDefs = null;
339
713
  }
714
+ /**
715
+ * Build the supervisor, or explain in one line why this node runs no blocks.
716
+ *
717
+ * Two ways to end up with none, and both are logged rather than inferred:
718
+ * this is not the hub (execution is hub-only for now), or the kernel exposed
719
+ * no cluster broker, which is the only door to `$process.*`.
720
+ */
721
+ buildSupervisor() {
722
+ const rawNodeId = this.ctx.kernel.localNodeId ?? "hub";
723
+ const nodeId = rawNodeId.includes("/") ? rawNodeId.split("/")[0] ?? "hub" : rawNodeId;
724
+ if (nodeId !== "hub") {
725
+ this.ctx.logger.info("core-blocks execution is hub-only — no blocks will run on this node", { meta: {
726
+ nodeId,
727
+ rawNodeId
728
+ } });
729
+ return null;
730
+ }
731
+ const broker = this.ctx.kernel.cluster?.broker;
732
+ if (!broker) {
733
+ this.ctx.logger.warn("core-blocks: no cluster broker — blocks can be stored and compiled, but nothing can run them");
734
+ return null;
735
+ }
736
+ const store = this.requireStore();
737
+ return new BlockSupervisor({
738
+ nodeId,
739
+ root: path$1.join(this.ctx.nodeDataDir, BLOCK_PACKAGE_DIRNAME),
740
+ store: {
741
+ list: () => store.list(),
742
+ get: (blockId) => store.get(blockId),
743
+ setStatus: (blockId, status, error) => store.setStatus(blockId, status, error)
744
+ },
745
+ host: createBrokerProcessHost(broker),
746
+ compile: async (code) => {
747
+ if (this.transpiler === null) return {
748
+ ok: false,
749
+ error: "the compiler is unavailable on this node"
750
+ };
751
+ return compileBlock(this.transpiler, code);
752
+ },
753
+ logger: this.ctx.logger.child("supervisor")
754
+ });
755
+ }
756
+ /**
757
+ * Reconcile now, after a write.
758
+ *
759
+ * The timer would get there on its own, but an operator who presses Save and
760
+ * watches a block sit at `stopped` for fifteen seconds has been told nothing
761
+ * happened. Deliberately NOT awaited by the cap method: converging forks a
762
+ * process, and the write is already durable.
763
+ */
764
+ convergeSoon() {
765
+ this.supervisor?.converge();
766
+ }
340
767
  requireStore() {
341
768
  if (this.store === null) throw new Error("core-blocks: not initialized");
342
769
  return this.store;
@@ -368,6 +795,7 @@ var CoreBlocksAddon = class extends BaseAddon {
368
795
  name: created.name,
369
796
  by: caller.userId
370
797
  } });
798
+ this.convergeSoon();
371
799
  return { block: created };
372
800
  },
373
801
  update: async ({ blockId, block, caller }) => {
@@ -377,14 +805,18 @@ var CoreBlocksAddon = class extends BaseAddon {
377
805
  blockId,
378
806
  by: caller.userId
379
807
  } });
808
+ this.convergeSoon();
380
809
  return { block: updated };
381
810
  },
382
811
  delete: async ({ blockId }) => {
383
812
  await this.requireStore().delete(blockId);
813
+ this.convergeSoon();
384
814
  return { success: true };
385
815
  },
386
816
  setEnabled: async ({ blockId, enabled }) => {
387
- return { block: await this.requireStore().update(blockId, { enabled }) };
817
+ const updated = await this.requireStore().update(blockId, { enabled });
818
+ this.convergeSoon();
819
+ return { block: updated };
388
820
  },
389
821
  compile: async ({ code }) => {
390
822
  if (this.transpiler === null) return {
@@ -404,6 +836,33 @@ var CoreBlocksAddon = class extends BaseAddon {
404
836
  };
405
837
  }
406
838
  };
839
+ /**
840
+ * The process plane, over the kernel's cluster broker.
841
+ *
842
+ * `$process.*` is the SAME infrastructure every addon runner uses — no new
843
+ * Moleculer action, no second spawner, no parallel supervisor. That is the
844
+ * whole point: a block gets fork, the UDS `ctx`, `CrashSupervisor` (D6) and
845
+ * `execution.placement` because it is spawned exactly like an addon.
846
+ */
847
+ function createBrokerProcessHost(broker) {
848
+ return {
849
+ list: async () => {
850
+ return await broker.call("$process.list");
851
+ },
852
+ spawn: async ({ runnerId, addonId, addonDir }) => {
853
+ await broker.call("$process.spawnRunner", {
854
+ runnerId,
855
+ addons: [{
856
+ addonId,
857
+ addonDir
858
+ }]
859
+ });
860
+ },
861
+ stop: async (runnerId) => {
862
+ await broker.call("$process.stop", { name: runnerId });
863
+ }
864
+ };
865
+ }
407
866
  /** Load esbuild lazily. Returns null — never throws — so a node without it
408
867
  * still boots, with the compile gate refusing writes rather than the node
409
868
  * refusing to start. */
@@ -418,4 +877,4 @@ async function loadTranspiler(logger) {
418
877
  }
419
878
  }
420
879
  //#endregion
421
- export { CoreBlocksAddon, compileBlock as i, CORE_BLOCKS_COLLECTION as n, CoreBlockStore as r, loadBlockTypeDefs as t };
880
+ export { CoreBlocksAddon, BlockSupervisor as a, blockIdFromAddonId as c, loadBlockTypeDefs as i, materializeBlock as l, CoreBlockStore as n, BLOCK_ADDON_PREFIX as o, compileBlock as r, blockAddonId as s, CORE_BLOCKS_COLLECTION as t, removeBlockPackage as u };
@@ -1,4 +1,6 @@
1
+ export { BLOCK_ADDON_PREFIX, blockAddonId, blockIdFromAddonId, materializeBlock, removeBlockPackage, } from './block-package.js';
2
+ export { type BlockProcessHost, type BlockRunnerSnapshot, type BlockRunnerState, BlockSupervisor, } from './block-supervisor.js';
3
+ export { type BlockTypeLib, loadBlockTypeDefs } from './block-type-defs.js';
4
+ export { type BlockTranspiler, compileBlock } from './compile-block.js';
5
+ export { CORE_BLOCKS_COLLECTION, CoreBlockStore } from './core-block-store.js';
1
6
  export { CoreBlocksAddon as default, CoreBlocksAddon } from './core-blocks.addon.js';
2
- export { CoreBlockStore, CORE_BLOCKS_COLLECTION } from './core-block-store.js';
3
- export { compileBlock, type BlockTranspiler } from './compile-block.js';
4
- export { loadBlockTypeDefs, type BlockTypeLib } from './block-type-defs.js';