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