@celilo/cli 0.2.1 → 0.3.1

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.
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Wire a module's manifest `subscriptions:` block into the SQLite event
3
+ * bus. Called from `module/import.ts` on install and from
4
+ * `cli/commands/module-remove.ts` on remove.
5
+ *
6
+ * Substitutions performed at subscribe time:
7
+ * - `$self` in `pattern` → the module's id
8
+ * - `${MODULE_PATH}` in `handler` → the module's installed targetPath
9
+ *
10
+ * The bus subscriber's name is namespaced as `<module-id>.<sub-name>`
11
+ * so two modules can declare a subscription named `smoke` without
12
+ * colliding.
13
+ */
14
+
15
+ import { defineEvents, openBus } from '@celilo/event-bus';
16
+ import { getEventBusPath } from '../config/paths';
17
+ import type { ModuleManifest, ModuleSubscription } from '../manifest/schema';
18
+
19
+ /**
20
+ * The bus is opened by the celilo CLI without an event registry — the
21
+ * CLI doesn't know the schemas of every module's events. The bus's
22
+ * empty-registry path skips payload validation, leaving that to the
23
+ * linked handlers (which open the bus *with* their own registry).
24
+ */
25
+ const NO_SCHEMAS = defineEvents({});
26
+
27
+ /**
28
+ * Resolve the per-module substitutions on a single subscription. Pure
29
+ * function: takes a parsed manifest entry, returns the bus-shaped
30
+ * subscribe options.
31
+ */
32
+ export function resolveSubscription(
33
+ sub: ModuleSubscription,
34
+ moduleId: string,
35
+ modulePath: string,
36
+ ): {
37
+ name: string;
38
+ pattern: string;
39
+ handler: string;
40
+ maxAttempts?: number;
41
+ timeoutMs?: number;
42
+ registeredBy: string;
43
+ } {
44
+ return {
45
+ name: scopedName(moduleId, sub.name),
46
+ pattern: substituteSelf(sub.pattern, moduleId),
47
+ handler: substituteModulePath(sub.handler, modulePath),
48
+ maxAttempts: sub.max_attempts,
49
+ timeoutMs: sub.timeout_ms,
50
+ registeredBy: moduleId,
51
+ };
52
+ }
53
+
54
+ /**
55
+ * Register all of a module's subscriptions on the bus. Idempotent —
56
+ * re-running with the same manifest updates existing rows in place.
57
+ *
58
+ * If the module's manifest declares no subscriptions, this is a
59
+ * cheap no-op (the bus DB isn't even touched).
60
+ */
61
+ export function registerModuleSubscriptions(
62
+ manifest: ModuleManifest,
63
+ modulePath: string,
64
+ ): { registered: number } {
65
+ const subs = manifest.subscriptions ?? [];
66
+ if (subs.length === 0) return { registered: 0 };
67
+
68
+ const bus = openBus({ dbPath: getEventBusPath(), events: NO_SCHEMAS });
69
+ try {
70
+ for (const sub of subs) {
71
+ const resolved = resolveSubscription(sub, manifest.id, modulePath);
72
+ bus.subscribe(resolved);
73
+ }
74
+ return { registered: subs.length };
75
+ } finally {
76
+ bus.close();
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Tear down every bus subscription that belongs to this module. Looks
82
+ * up rows by name prefix `<module-id>.` rather than rereading the
83
+ * old manifest, so a stale subscription left behind by a manifest
84
+ * change still gets cleaned up.
85
+ *
86
+ * Best-effort: if the bus DB doesn't exist (never opened), returns 0.
87
+ */
88
+ export function unregisterModuleSubscriptions(moduleId: string): {
89
+ unregistered: number;
90
+ } {
91
+ const bus = openBus({ dbPath: getEventBusPath(), events: NO_SCHEMAS });
92
+ try {
93
+ const likePattern = `${moduleId}.%`;
94
+ const rows = bus.db
95
+ .query<{ name: string }, [string]>('SELECT name FROM subscribers WHERE name LIKE ?')
96
+ .all(likePattern);
97
+ for (const row of rows) {
98
+ bus.unsubscribe(row.name);
99
+ }
100
+ return { unregistered: rows.length };
101
+ } finally {
102
+ bus.close();
103
+ }
104
+ }
105
+
106
+ function scopedName(moduleId: string, subName: string): string {
107
+ return `${moduleId}.${subName}`;
108
+ }
109
+
110
+ function substituteSelf(pattern: string, moduleId: string): string {
111
+ // `$self` matches when followed by a dot or end-of-string, so a
112
+ // pattern like `deploy.$self.foo` substitutes correctly without
113
+ // confusing `$selfish` if anyone wrote that. (No real reason they
114
+ // would, but be precise.)
115
+ return pattern.replace(/\$self(?=\.|$)/g, moduleId);
116
+ }
117
+
118
+ function substituteModulePath(handler: string, modulePath: string): string {
119
+ return handler.replace(/\$\{MODULE_PATH\}/g, modulePath);
120
+ }