@crouter/api 0.3.387 → 0.3.389

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 (131) hide show
  1. package/dist/api/__tests__/integration/client.test.js +97 -0
  2. package/dist/api/client.d.ts +7 -0
  3. package/dist/api/client.js +40 -21
  4. package/dist/api/dto/config.d.ts +11 -1
  5. package/dist/core/asset-root.d.ts +7 -0
  6. package/dist/core/asset-root.js +18 -0
  7. package/dist/core/canvas/boot-id.d.ts +6 -0
  8. package/dist/core/canvas/boot-id.js +26 -0
  9. package/dist/core/canvas/paths.d.ts +72 -0
  10. package/dist/core/canvas/paths.js +163 -0
  11. package/dist/core/canvas/pid.d.ts +391 -0
  12. package/dist/core/canvas/pid.js +948 -0
  13. package/dist/core/command-plugins/bundle.d.ts +149 -0
  14. package/dist/core/command-plugins/bundle.js +588 -0
  15. package/dist/core/command-plugins/endpoint.d.ts +24 -0
  16. package/dist/core/command-plugins/endpoint.js +51 -0
  17. package/dist/core/config.d.ts +233 -0
  18. package/dist/core/config.js +1120 -0
  19. package/dist/core/env-name.d.ts +6 -0
  20. package/dist/core/env-name.js +9 -0
  21. package/dist/core/errors.d.ts +38 -0
  22. package/dist/core/errors.js +90 -0
  23. package/dist/core/events/emit.d.ts +6 -0
  24. package/dist/core/events/emit.js +42 -0
  25. package/dist/core/events/envelope.d.ts +2 -0
  26. package/dist/core/events/envelope.js +84 -0
  27. package/dist/core/events/errors.d.ts +4 -0
  28. package/dist/core/events/errors.js +69 -0
  29. package/dist/core/events/operation-id.d.ts +4 -0
  30. package/dist/core/events/operation-id.js +24 -0
  31. package/dist/core/events/serialize.d.ts +4 -0
  32. package/dist/core/events/serialize.js +199 -0
  33. package/dist/core/events/source.d.ts +16 -0
  34. package/dist/core/events/source.js +31 -0
  35. package/dist/core/events/types.d.ts +68 -0
  36. package/dist/core/events/types.js +11 -0
  37. package/dist/core/exclusive-lock.d.ts +34 -0
  38. package/dist/core/exclusive-lock.js +197 -0
  39. package/dist/core/fs-utils.d.ts +44 -0
  40. package/dist/core/fs-utils.js +208 -0
  41. package/dist/core/help.d.ts +309 -0
  42. package/dist/core/help.js +406 -0
  43. package/dist/core/human/page-catalog.d.ts +57 -0
  44. package/dist/core/human/page-catalog.js +172 -0
  45. package/dist/core/installed-plugins.d.ts +2 -0
  46. package/dist/core/installed-plugins.js +79 -0
  47. package/dist/core/io.d.ts +122 -0
  48. package/dist/core/io.js +373 -0
  49. package/dist/core/keybindings/attach-control.d.ts +49 -0
  50. package/dist/core/keybindings/attach-control.js +42 -0
  51. package/dist/core/keybindings/catalog.d.ts +18 -0
  52. package/dist/core/keybindings/catalog.js +257 -0
  53. package/dist/core/keybindings/types.d.ts +42 -0
  54. package/dist/core/keybindings/types.js +1 -0
  55. package/dist/core/layout.d.ts +26 -0
  56. package/dist/core/layout.js +94 -0
  57. package/dist/core/locked-file.d.ts +27 -0
  58. package/dist/core/locked-file.js +118 -0
  59. package/dist/core/log.d.ts +9 -0
  60. package/dist/core/log.js +89 -0
  61. package/dist/core/manifest.d.ts +5 -0
  62. package/dist/core/manifest.js +15 -0
  63. package/dist/core/plugin-env.d.ts +8 -0
  64. package/dist/core/plugin-env.js +31 -0
  65. package/dist/core/plugin-extensions.d.ts +29 -0
  66. package/dist/core/plugin-extensions.js +191 -0
  67. package/dist/core/plugin-swap-lock.d.ts +9 -0
  68. package/dist/core/plugin-swap-lock.js +31 -0
  69. package/dist/core/preview-result-path.d.ts +4 -0
  70. package/dist/core/preview-result-path.js +26 -0
  71. package/dist/core/profiles/env-store.d.ts +22 -0
  72. package/dist/core/profiles/env-store.js +163 -0
  73. package/dist/core/profiles/fuzzy-match.d.ts +19 -0
  74. package/dist/core/profiles/fuzzy-match.js +92 -0
  75. package/dist/core/profiles/manifest.d.ts +120 -0
  76. package/dist/core/profiles/manifest.js +529 -0
  77. package/dist/core/rate-limit-scope.d.ts +25 -0
  78. package/dist/core/rate-limit-scope.js +64 -0
  79. package/dist/core/render.d.ts +12 -0
  80. package/dist/core/render.js +138 -0
  81. package/dist/core/resolver.d.ts +14 -0
  82. package/dist/core/resolver.js +111 -0
  83. package/dist/core/runtime/branded-host.d.ts +25 -0
  84. package/dist/core/runtime/branded-host.js +264 -0
  85. package/dist/core/runtime/broker/daemon-ops.d.ts +65 -0
  86. package/dist/core/runtime/broker/daemon-ops.js +177 -0
  87. package/dist/core/runtime/broker/signal-stream.d.ts +30 -0
  88. package/dist/core/runtime/broker/signal-stream.js +149 -0
  89. package/dist/core/scope.d.ts +32 -0
  90. package/dist/core/scope.js +184 -0
  91. package/dist/core/scoped-state/db.d.ts +17 -0
  92. package/dist/core/scoped-state/db.js +247 -0
  93. package/dist/core/scoped-state/migrate.d.ts +8 -0
  94. package/dist/core/scoped-state/migrate.js +187 -0
  95. package/dist/core/scoped-state/paths.d.ts +9 -0
  96. package/dist/core/scoped-state/paths.js +27 -0
  97. package/dist/core/scoped-state/profiles.d.ts +27 -0
  98. package/dist/core/scoped-state/profiles.js +93 -0
  99. package/dist/core/scoped-state/providers.d.ts +24 -0
  100. package/dist/core/scoped-state/providers.js +19 -0
  101. package/dist/core/scoped-state/schema.d.ts +6 -0
  102. package/dist/core/scoped-state/schema.js +43 -0
  103. package/dist/core/scoped-state/settings.d.ts +28 -0
  104. package/dist/core/scoped-state/settings.js +83 -0
  105. package/dist/core/spaces/open-beneath.d.ts +71 -0
  106. package/dist/core/spaces/open-beneath.js +581 -0
  107. package/dist/core/sqlite-statements.d.ts +4 -0
  108. package/dist/core/sqlite-statements.js +17 -0
  109. package/dist/core/subscription-state.d.ts +121 -0
  110. package/dist/core/subscription-state.js +287 -0
  111. package/dist/core/user-settings.d.ts +377 -0
  112. package/dist/core/user-settings.js +458 -0
  113. package/dist/daemon/broker-signals/bus.d.ts +30 -0
  114. package/dist/daemon/broker-signals/bus.js +87 -0
  115. package/dist/daemon/manage.d.ts +176 -0
  116. package/dist/daemon/manage.js +664 -0
  117. package/dist/daemon/pidfile.d.ts +8 -0
  118. package/dist/daemon/pidfile.js +37 -0
  119. package/dist/daemon/startup-policy.d.ts +1 -0
  120. package/dist/daemon/startup-policy.js +1 -0
  121. package/dist/native/linux.d.ts +29 -0
  122. package/dist/native/linux.js +20 -0
  123. package/dist/shared/env.d.ts +116 -0
  124. package/dist/shared/env.js +271 -0
  125. package/dist/shared/inbox-entry-body.d.ts +22 -0
  126. package/dist/shared/inbox-entry-body.js +116 -0
  127. package/dist/shared/working-activity.d.ts +9 -0
  128. package/dist/shared/working-activity.js +27 -0
  129. package/dist/types.d.ts +562 -0
  130. package/dist/types.js +186 -0
  131. package/package.json +1 -1
@@ -0,0 +1,177 @@
1
+ // Broker-to-daemon operation client. A broker is launched by crtrd, so a cold
2
+ // daemon socket must fail loud — it must never use the CLI factory that can
3
+ // autostart another daemon.
4
+ import { localClient } from '../../../api/node-transport.js';
5
+ import { ApiError } from '../../../api/errors.js';
6
+ import { envExecutionId } from '../../../shared/env.js';
7
+ import { brokerSignalClient } from './signal-stream.js';
8
+ import { cachedBrokerNodeFields } from '../../../daemon/broker-signals/bus.js';
9
+ import { reportRefNodeId } from '../../../shared/inbox-entry-body.js';
10
+ function daemonClient() {
11
+ return localClient({ autostart: false });
12
+ }
13
+ export function appendBrokerLog(nodeId, record) {
14
+ return daemonClient().appendNodeLog(nodeId, { record, expected_execution_id: brokerExecutionId() });
15
+ }
16
+ export function putBrokerTelemetry(nodeId, telemetry) {
17
+ return daemonClient().putNodeTelemetry(nodeId, { ...telemetry, expected_execution_id: brokerExecutionId() });
18
+ }
19
+ export function putBrokerRecap(nodeId, briefing) {
20
+ return daemonClient().putNodeRecap(nodeId, { briefing, expected_execution_id: brokerExecutionId() });
21
+ }
22
+ /** The body of a direct message the daemon spilled into this node's records. */
23
+ export function readBrokerPassiveMessage(nodeId, ref) {
24
+ return daemonClient().readNodePassiveMessage(nodeId, ref);
25
+ }
26
+ export function clearBrokerPushedFinal(nodeId) {
27
+ return daemonClient().nodePushedFinal(nodeId, { action: 'clear', expected_execution_id: brokerExecutionId() });
28
+ }
29
+ export function consumeBrokerPushedFinal(nodeId) {
30
+ return daemonClient().nodePushedFinal(nodeId, { action: 'consume', expected_execution_id: brokerExecutionId() });
31
+ }
32
+ export function startBrokerBashJob(nodeId, request) {
33
+ return daemonClient().request('POST', `/v1/nodes/${encodeURIComponent(nodeId)}/jobs/start`, {
34
+ ...request, expected_execution_id: brokerExecutionId(),
35
+ });
36
+ }
37
+ /** Record a command the valve just backgrounded as this node's job object. */
38
+ export function recordBackgroundedJob(nodeId, jobId, request) {
39
+ return daemonClient().recordBackgroundedJob(nodeId, jobId, request);
40
+ }
41
+ export function stopBrokerBashJob(nodeId, jobId) {
42
+ return daemonClient().request('DELETE', `/v1/nodes/${encodeURIComponent(nodeId)}/jobs/${encodeURIComponent(jobId)}?foreground=1`, {
43
+ expected_execution_id: brokerExecutionId(),
44
+ });
45
+ }
46
+ export function brokerExecutionId() {
47
+ const id = envExecutionId();
48
+ if (id === undefined || id === '')
49
+ throw new Error('broker launch is missing CRTR_EXECUTION_ID');
50
+ return id;
51
+ }
52
+ export function bindBrokerSession(nodeId, request) {
53
+ return daemonClient().bindBrokerSession(nodeId, { ...request, executionId: brokerExecutionId() });
54
+ }
55
+ export function settleBroker(nodeId, request) {
56
+ return daemonClient().settleBroker(nodeId, { ...request, expected_execution_id: brokerExecutionId() });
57
+ }
58
+ export function completeBrokerPark(nodeId, request) {
59
+ return daemonClient().completeBrokerPark(nodeId, { ...request, expected_execution_id: brokerExecutionId() });
60
+ }
61
+ export function recordBrokerParkActivity(nodeId) {
62
+ return daemonClient().recordBrokerParkActivity(nodeId, { expected_execution_id: brokerExecutionId() });
63
+ }
64
+ export function recordBrokerTelemetry(nodeId, request) {
65
+ return daemonClient().recordBrokerTelemetry(nodeId, { ...request, expected_execution_id: brokerExecutionId() });
66
+ }
67
+ export async function claimBrokerMail(nodeId, channel, reportBodies) {
68
+ const client = daemonClient();
69
+ const executionId = brokerExecutionId();
70
+ const result = await client.claimNodeMail(nodeId, { execution_id: executionId, channel });
71
+ if (result.action === 'hold')
72
+ return result;
73
+ const deliveries = await Promise.all(result.deliveries.map(async (delivery) => {
74
+ const ref = delivery.entry.ref;
75
+ if (ref === undefined || reportRefNodeId(ref) === undefined)
76
+ return delivery;
77
+ try {
78
+ // A claim includes mail already handed to this execution on every poll.
79
+ // Reports are immutable; share even concurrent lookups for the same ref.
80
+ let pending = reportBodies.get(ref);
81
+ if (pending === undefined) {
82
+ pending = client.readNodeInboxReport(nodeId, ref, executionId).then(({ content }) => content);
83
+ reportBodies.set(ref, pending);
84
+ }
85
+ const content = await pending;
86
+ return { ...delivery, entry: { ...delivery.entry, data: { ...delivery.entry.data, report_body: content } } };
87
+ }
88
+ catch (error) {
89
+ // Failed reads are retryable; a missing report still renders its ref.
90
+ reportBodies.delete(ref);
91
+ if (error instanceof ApiError && error.status === 404)
92
+ return delivery;
93
+ throw error;
94
+ }
95
+ }));
96
+ // A hold, error, or transient empty result cannot invalidate the ref cache.
97
+ // Once a non-held claim returns deliveries, retain only refs still claimed.
98
+ if (deliveries.length > 0) {
99
+ const activeRefs = new Set(deliveries.map(({ entry }) => entry.ref).filter((ref) => ref !== undefined));
100
+ for (const ref of reportBodies.keys())
101
+ if (!activeRefs.has(ref))
102
+ reportBodies.delete(ref);
103
+ }
104
+ return { ...result, deliveries };
105
+ }
106
+ export function acknowledgeBrokerMail(nodeId, deliveries) {
107
+ return daemonClient().acknowledgeNodeMail(nodeId, { execution_id: brokerExecutionId(), deliveries });
108
+ }
109
+ export function recordBrokerTurn(nodeId, request) {
110
+ return daemonClient().recordBrokerTurn(nodeId, { ...request, expected_execution_id: brokerExecutionId() });
111
+ }
112
+ export function mutateBrokerProviderRetry(nodeId, request) {
113
+ return daemonClient().mutateBrokerProviderRetry(nodeId, { ...request, expected_execution_id: brokerExecutionId() });
114
+ }
115
+ export function mutateBrokerFault(nodeId, request) {
116
+ return daemonClient().mutateBrokerFault(nodeId, { ...request, expected_execution_id: brokerExecutionId() });
117
+ }
118
+ export function brokerRecovery(nodeId) {
119
+ return daemonClient().getBrokerRecovery(nodeId, brokerExecutionId());
120
+ }
121
+ /** Node-scoped fault writes are for non-broker clients such as attach transport.
122
+ * Broker-owned recovery mutations use the execution-fenced wrappers above. */
123
+ export function recordNodeFault(nodeId, input) {
124
+ return daemonClient().recordNodeFault(nodeId, input);
125
+ }
126
+ export function clearNodeFault(nodeId, opts = {}) {
127
+ return daemonClient().clearNodeFault(nodeId, {
128
+ ...(opts.link === undefined ? {} : { link: opts.link }),
129
+ ...(opts.preserveEpisode === undefined ? {} : { preserve_episode: opts.preserveEpisode }),
130
+ }).then((result) => result.cleared);
131
+ }
132
+ /** Fixed canvas projection for broker extension rendering. It deliberately
133
+ * exposes no generic node/database facade. */
134
+ export function commitBrokerModel(nodeId, request) {
135
+ return daemonClient().commitBrokerModel(nodeId, { ...request, expected_execution_id: brokerExecutionId() });
136
+ }
137
+ export function brokerExtensionState(nodeId) {
138
+ return daemonClient().brokerExtensionState(nodeId);
139
+ }
140
+ /** Only node-owned fields are cached. A disconnected or stopped stream cannot
141
+ * guarantee freshness, so events at those boundaries read the daemon instead. */
142
+ export async function cachedBrokerNode(nodeId) {
143
+ const client = brokerSignalClient();
144
+ if (client.state === 'connected' && client.latestNode !== undefined) {
145
+ return cachedBrokerNodeFields(client.latestNode);
146
+ }
147
+ return cachedBrokerNodeFields((await brokerExtensionState(nodeId)).node);
148
+ }
149
+ /** The node's one delivery route: every rule-driven document delivery the
150
+ * broker applies, recorded by crtrd in the same request. */
151
+ export function brokerDelivery(nodeId, request) {
152
+ return daemonClient().nodeDelivery(nodeId, { ...request, expected_execution_id: brokerExecutionId() });
153
+ }
154
+ export function commitBrokerGeneratedName(nodeId, request) {
155
+ return daemonClient().commitBrokerGeneratedName(nodeId, { ...request, expected_execution_id: brokerExecutionId() });
156
+ }
157
+ export function commitBrokerPersonaAck(nodeId, request) {
158
+ return daemonClient().commitBrokerPersonaAck(nodeId, { ...request, expected_execution_id: brokerExecutionId() });
159
+ }
160
+ /** Reports still use the canonical daemon report handler; this broker adapter
161
+ * only preserves terminal structured output's idempotent finalization result. */
162
+ export async function publishBrokerReport(nodeId, request) {
163
+ try {
164
+ await daemonClient().pushReport(nodeId, request);
165
+ }
166
+ catch (error) {
167
+ if (request.tier === 'final' && error instanceof ApiError && error.code === 'already_finalized')
168
+ return;
169
+ throw error;
170
+ }
171
+ }
172
+ /** Root `/new` remains on the dedicated relaunch operation because it launches
173
+ * a replacement broker and tears down this broker. Callers must not await it
174
+ * from Pi's session-start dispatch. */
175
+ export function relaunchBrokerRoot(nodeId) {
176
+ return daemonClient().relaunchRoot(nodeId);
177
+ }
@@ -0,0 +1,30 @@
1
+ import type { BrokerExtensionNodeDTO } from '../../../api/dto/broker-ops.js';
2
+ export type BrokerSignalState = 'disconnected' | 'connected' | 'unsupported' | 'stopped';
3
+ export type BrokerSignalEvent = 'mail' | 'resync' | 'node' | 'fault-retry';
4
+ type Listener = () => void;
5
+ export declare class BrokerSignalClient {
6
+ private stateValue;
7
+ private latestNodeValue;
8
+ private readonly listeners;
9
+ private controller;
10
+ private retryTimer;
11
+ private heartbeatTimer;
12
+ private retryMs;
13
+ private running;
14
+ private nodeId;
15
+ private executionId;
16
+ get state(): BrokerSignalState;
17
+ /** The last hello/node projection; not an authoritative delivery or settlement read. */
18
+ get latestNode(): BrokerExtensionNodeDTO | undefined;
19
+ subscribe(event: BrokerSignalEvent | 'state', listener: Listener): () => void;
20
+ start(nodeId: string, executionId: string): void;
21
+ stop(): void;
22
+ private emit;
23
+ private setState;
24
+ private clearTimers;
25
+ private armHeartbeat;
26
+ private connect;
27
+ private handleLine;
28
+ }
29
+ export declare function brokerSignalClient(): BrokerSignalClient;
30
+ export {};
@@ -0,0 +1,149 @@
1
+ // Process-wide broker signal connection. Pi's Jiti loader evaluates extensions in
2
+ // another module graph, so a module-local singleton would open duplicate streams.
3
+ import { localClient } from '../../../api/node-transport.js';
4
+ import { ApiError } from '../../../api/errors.js';
5
+ const KEY = Symbol.for('@crouton-kit/crtr:broker-signal-client');
6
+ export class BrokerSignalClient {
7
+ stateValue = 'disconnected';
8
+ latestNodeValue;
9
+ listeners = new Map();
10
+ controller;
11
+ retryTimer;
12
+ heartbeatTimer;
13
+ retryMs = 250;
14
+ running = false;
15
+ nodeId;
16
+ executionId;
17
+ get state() { return this.stateValue; }
18
+ /** The last hello/node projection; not an authoritative delivery or settlement read. */
19
+ get latestNode() { return this.latestNodeValue; }
20
+ subscribe(event, listener) {
21
+ let set = this.listeners.get(event);
22
+ if (!set) {
23
+ set = new Set();
24
+ this.listeners.set(event, set);
25
+ }
26
+ set.add(listener);
27
+ // A late subscriber cannot miss the hello that established this connection.
28
+ if (event === 'state' || (event === 'resync' && this.stateValue === 'connected') ||
29
+ (event === 'node' && this.latestNodeValue !== undefined))
30
+ listener();
31
+ return () => { set.delete(listener); };
32
+ }
33
+ start(nodeId, executionId) {
34
+ if (this.running) {
35
+ if (this.nodeId === nodeId && this.executionId === executionId)
36
+ return;
37
+ this.stop();
38
+ }
39
+ this.nodeId = nodeId;
40
+ this.executionId = executionId;
41
+ this.running = true;
42
+ this.retryMs = 250;
43
+ this.connect();
44
+ }
45
+ stop() {
46
+ this.running = false;
47
+ this.controller?.abort();
48
+ this.controller = undefined;
49
+ this.clearTimers();
50
+ this.setState('stopped');
51
+ }
52
+ emit(event) {
53
+ for (const listener of this.listeners.get(event) ?? []) {
54
+ try {
55
+ listener();
56
+ }
57
+ catch { /* a subscriber cannot kill the connection */ }
58
+ }
59
+ }
60
+ setState(state) {
61
+ if (this.stateValue === state)
62
+ return;
63
+ this.stateValue = state;
64
+ this.emit('state');
65
+ }
66
+ clearTimers() {
67
+ if (this.retryTimer)
68
+ clearTimeout(this.retryTimer);
69
+ if (this.heartbeatTimer)
70
+ clearTimeout(this.heartbeatTimer);
71
+ this.retryTimer = this.heartbeatTimer = undefined;
72
+ }
73
+ armHeartbeat(controller) {
74
+ if (this.heartbeatTimer)
75
+ clearTimeout(this.heartbeatTimer);
76
+ // The server emits ping every 15 seconds. Two missed beats mean reconnect.
77
+ this.heartbeatTimer = setTimeout(() => controller.abort(), 35_000);
78
+ this.heartbeatTimer.unref?.();
79
+ }
80
+ connect() {
81
+ if (!this.running || !this.nodeId || !this.executionId)
82
+ return;
83
+ const controller = new AbortController();
84
+ this.controller = controller;
85
+ void (async () => {
86
+ try {
87
+ for await (const line of localClient({ autostart: false }).getBrokerSignals(this.nodeId, this.executionId, { signal: controller.signal, maxRetries: 0 })) {
88
+ if (!this.running || this.controller !== controller)
89
+ return;
90
+ this.armHeartbeat(controller);
91
+ this.handleLine(line);
92
+ }
93
+ }
94
+ catch (error) {
95
+ if (!this.running || this.controller !== controller)
96
+ return;
97
+ if (error instanceof ApiError && (error.status === 404 || error.status === 405)) {
98
+ this.clearTimers();
99
+ this.controller = undefined;
100
+ this.setState('unsupported');
101
+ return;
102
+ }
103
+ if (error instanceof ApiError && error.status === 409) {
104
+ this.stop();
105
+ return;
106
+ }
107
+ }
108
+ if (!this.running || this.controller !== controller)
109
+ return;
110
+ this.controller = undefined;
111
+ this.clearTimers();
112
+ this.setState('disconnected');
113
+ const delay = Math.round(this.retryMs * (0.8 + Math.random() * 0.4));
114
+ this.retryMs = Math.min(this.retryMs * 2, 5_000);
115
+ this.retryTimer = setTimeout(() => { this.retryTimer = undefined; this.connect(); }, delay);
116
+ this.retryTimer.unref?.();
117
+ })();
118
+ }
119
+ handleLine(line) {
120
+ switch (line.type) {
121
+ case 'hello':
122
+ if (line.execution_id !== this.executionId) {
123
+ this.stop();
124
+ return;
125
+ }
126
+ this.retryMs = 250;
127
+ this.latestNodeValue = line.node;
128
+ this.setState('connected');
129
+ this.emit('node');
130
+ this.emit('resync');
131
+ break;
132
+ case 'mail':
133
+ this.emit('mail');
134
+ break;
135
+ case 'node':
136
+ this.latestNodeValue = line.node;
137
+ this.emit('node');
138
+ break;
139
+ case 'fault-retry':
140
+ this.emit('fault-retry');
141
+ break;
142
+ case 'ping': break;
143
+ }
144
+ }
145
+ }
146
+ export function brokerSignalClient() {
147
+ const globals = globalThis;
148
+ return (globals[KEY] ??= new BrokerSignalClient());
149
+ }
@@ -0,0 +1,32 @@
1
+ import type { InstalledPlugin, Scope } from '../types.js';
2
+ export declare function builtinMemoryRoot(): string;
3
+ export declare function builtinPiPackagesDir(): string;
4
+ export declare function builtinPiPackageDir(name: string): string;
5
+ export declare function userScopeRoot(): string;
6
+ export declare function findProjectScopeRoot(startDir?: string): string | null;
7
+ /** Every project scope root visible from `startDir`, nearest first — now a
8
+ * PROFILE-AWARE pointer walk, not a single-pointer ancestor walk. Pointers are
9
+ * `[startDir]` plus, when `profileId` resolves to a manifest, each of its
10
+ * `projects` in manifest order; each pointer independently walks upward
11
+ * collecting every ancestor `.crouter/` dir, the pointers' contributions
12
+ * concatenate in pointer order, and the combined list dedupes to first
13
+ * occurrence — so a `.crouter/` shared by two pointers contributes once, at
14
+ * whichever pointer reached it first. `profileId` defaults to the process's
15
+ * selected profile (`CRTR_PROFILE_ID`); a null/unresolvable profile collapses
16
+ * to the single `[startDir]` pointer, reproducing today's behavior exactly.
17
+ * Outside a Linux node sandbox, roots are discovered on each call so profile
18
+ * edits and deleted project directories do not leave a stale pointer. A node
19
+ * sandbox has no profile store to read and uses only its cwd pointer. */
20
+ export declare function findProjectScopeRoots(startDir?: string, profileId?: string | null): string[];
21
+ export declare function projectScopeRoot(startDir?: string): string | null;
22
+ export declare function projectScopeRoots(startDir?: string, profileId?: string | null): string[];
23
+ export declare function scopeRoot(scope: Scope): string | null;
24
+ export declare function requireScopeRoot(scope: Scope): string;
25
+ export declare function ensureProjectScopeRoot(startDir?: string): string;
26
+ export declare function pluginsDir(scope: Scope): string | null;
27
+ export declare function marketplacesDir(scope: Scope): string | null;
28
+ /** Where a plugin's substrate memory documents live: `<plugin.root>/memory`.
29
+ * The resolver mounts these under the virtual `<pluginName>/` namespace. */
30
+ export declare function pluginMemoryDir(plugin: InstalledPlugin): string;
31
+ export declare function resolveScopeArg(scopeArg: string | undefined): Scope | 'all';
32
+ export declare function listScopes(scopeArg: string | undefined): Scope[];
@@ -0,0 +1,184 @@
1
+ import { envProfileId, isSandboxedNode } from '../shared/env.js';
2
+ import { homedir } from 'node:os';
3
+ import { existsSync, statSync } from 'node:fs';
4
+ import { join, resolve, dirname, sep } from 'node:path';
5
+ import { CRTR_DIR_NAME } from '../types.js';
6
+ import { assetRoot } from './asset-root.js';
7
+ import { usage } from './errors.js';
8
+ import { loadProfileManifest } from './profiles/manifest.js';
9
+ import { realpathOrSelf } from './fs-utils.js';
10
+ import { appsRoot, layoutRoot, runtimeRoot } from './layout.js';
11
+ export function builtinMemoryRoot() {
12
+ // Builtin memory is the only content the builtin scope carries — the scope
13
+ // has no scopeRoot (scopeRoot('builtin') is null).
14
+ return join(assetRoot(), 'builtin-memory');
15
+ }
16
+ export function builtinPiPackagesDir() {
17
+ return join(assetRoot(), 'builtin-pi-packages');
18
+ }
19
+ export function builtinPiPackageDir(name) {
20
+ return join(builtinPiPackagesDir(), name);
21
+ }
22
+ export function userScopeRoot() {
23
+ return join(runtimeRoot(), 'scope');
24
+ }
25
+ function isProjectScopeDir(candidate, userRoot) {
26
+ if (!existsSync(candidate))
27
+ return false;
28
+ // Compare realpaths, not raw strings: on macOS `$HOME` is commonly under a
29
+ // symlinked prefix (`/tmp` -> `/private/tmp`) while `process.cwd()` (the
30
+ // walk's start point, resolved by the OS) is already realpath'd. A raw
31
+ // string compare then fails to recognize the user root one level up from a
32
+ // project dir nested under HOME, leaking the user scope in as a bogus
33
+ // project scope root.
34
+ if (realpathOrSelf(candidate) === realpathOrSelf(join(homedir(), CRTR_DIR_NAME)))
35
+ return false;
36
+ // App homes are mounted as HOME inside brokers; an old .crouter there is
37
+ // not a project scope even when the daemon resolves a node's target cwd.
38
+ if (realpathOrSelf(candidate).startsWith(`${realpathOrSelf(appsRoot())}${sep}`) && candidate.endsWith(`${sep}home${sep}${CRTR_DIR_NAME}`))
39
+ return false;
40
+ if (!isSandboxedNode() && (realpathOrSelf(candidate) === realpathOrSelf(userRoot) || realpathOrSelf(candidate) === realpathOrSelf(layoutRoot())))
41
+ return false;
42
+ try {
43
+ return statSync(candidate).isDirectory();
44
+ }
45
+ catch {
46
+ return false;
47
+ }
48
+ }
49
+ export function findProjectScopeRoot(startDir = process.cwd()) {
50
+ return findProjectScopeRoots(startDir)[0] ?? null;
51
+ }
52
+ /** Walk `startDir` upward collecting every ancestor `.crouter/` dir (nearest
53
+ * first). The user-global `~/.crouter/` is excluded because it is the separate
54
+ * user scope, not a project ancestor.
55
+ *
56
+ * The walk is FENCED at `homedir()`: it must never climb at or above HOME.
57
+ * Home's own `.crouter/` is the user scope (already excluded), and nothing at
58
+ * or above HOME is a project ancestor. Without this fence, a relocated HOME
59
+ * nested under a directory tree that itself contains a `.crouter/` (e.g. a
60
+ * mock-VM home under the real host home) would sweep the host's `~/.crouter`
61
+ * (and any other above-HOME `.crouter`) in as a bogus project ancestor,
62
+ * leaking one machine's user-global memory into a nested-HOME node. A read whose start dir is outside HOME (e.g. a production VM
63
+ * with HOME=/root and no ancestor `.crouter`) walks to the filesystem root. */
64
+ function collectAncestorScopeRoots(startDir, userRoot) {
65
+ const roots = [];
66
+ const home = realpathOrSelf(homedir());
67
+ let dir = resolve(startDir);
68
+ while (true) {
69
+ const candidate = join(dir, CRTR_DIR_NAME);
70
+ if (isProjectScopeDir(candidate, userRoot))
71
+ roots.push(candidate);
72
+ if (realpathOrSelf(dir) === home)
73
+ return roots;
74
+ const parent = dirname(dir);
75
+ if (parent === dir)
76
+ return roots;
77
+ dir = parent;
78
+ }
79
+ }
80
+ /** Every project scope root visible from `startDir`, nearest first — now a
81
+ * PROFILE-AWARE pointer walk, not a single-pointer ancestor walk. Pointers are
82
+ * `[startDir]` plus, when `profileId` resolves to a manifest, each of its
83
+ * `projects` in manifest order; each pointer independently walks upward
84
+ * collecting every ancestor `.crouter/` dir, the pointers' contributions
85
+ * concatenate in pointer order, and the combined list dedupes to first
86
+ * occurrence — so a `.crouter/` shared by two pointers contributes once, at
87
+ * whichever pointer reached it first. `profileId` defaults to the process's
88
+ * selected profile (`CRTR_PROFILE_ID`); a null/unresolvable profile collapses
89
+ * to the single `[startDir]` pointer, reproducing today's behavior exactly.
90
+ * Outside a Linux node sandbox, roots are discovered on each call so profile
91
+ * edits and deleted project directories do not leave a stale pointer. A node
92
+ * sandbox has no profile store to read and uses only its cwd pointer. */
93
+ export function findProjectScopeRoots(startDir = process.cwd(), profileId = envProfileId() || null) {
94
+ return resolveProjectScopeRoots(startDir, profileId);
95
+ }
96
+ function resolveProjectScopeRoots(startDir, profileId) {
97
+ const resolvedStart = resolve(startDir);
98
+ const userRoot = userScopeRoot();
99
+ const pointers = [resolvedStart];
100
+ if (!isSandboxedNode() && profileId !== null && profileId !== '') {
101
+ try {
102
+ const { manifest } = loadProfileManifest(profileId);
103
+ for (const project of manifest.projects)
104
+ pointers.push(project.path);
105
+ }
106
+ catch {
107
+ // Missing/deleted/invalid profile: this is a hot path every
108
+ // launch-config resolve runs through, so fall back to the bare
109
+ // `[startDir]` pointer rather than throwing.
110
+ }
111
+ }
112
+ const seen = new Set();
113
+ const roots = [];
114
+ for (const pointer of pointers) {
115
+ for (const root of collectAncestorScopeRoots(pointer, userRoot)) {
116
+ if (!seen.has(root)) {
117
+ seen.add(root);
118
+ roots.push(root);
119
+ }
120
+ }
121
+ }
122
+ return roots;
123
+ }
124
+ export function projectScopeRoot(startDir) {
125
+ return findProjectScopeRoot(startDir);
126
+ }
127
+ export function projectScopeRoots(startDir, profileId) {
128
+ return findProjectScopeRoots(startDir, profileId);
129
+ }
130
+ export function scopeRoot(scope) {
131
+ // The builtin scope has no scope-root dir; its content lives in dedicated
132
+ // package directories such as builtinMemoryRoot().
133
+ if (scope === 'builtin')
134
+ return null;
135
+ return scope === 'user' ? userScopeRoot() : projectScopeRoot();
136
+ }
137
+ export function requireScopeRoot(scope) {
138
+ const root = scopeRoot(scope);
139
+ if (!root) {
140
+ throw usage(`no ${scope} scope available — run \`crtr init\` here or use --scope user`);
141
+ }
142
+ return root;
143
+ }
144
+ export function ensureProjectScopeRoot(startDir = process.cwd()) {
145
+ const found = findProjectScopeRoot(startDir);
146
+ if (found)
147
+ return found;
148
+ // The caller explicitly initializes this project scope; discovery never
149
+ // creates it, and the next read sees it directly from disk.
150
+ return join(resolve(startDir), CRTR_DIR_NAME);
151
+ }
152
+ export function pluginsDir(scope) {
153
+ const root = scope === 'user' ? runtimeRoot() : scopeRoot(scope);
154
+ return root ? join(root, 'plugins') : null;
155
+ }
156
+ export function marketplacesDir(scope) {
157
+ const root = scope === 'user' ? runtimeRoot() : scopeRoot(scope);
158
+ return root ? join(root, 'marketplaces') : null;
159
+ }
160
+ /** Where a plugin's substrate memory documents live: `<plugin.root>/memory`.
161
+ * The resolver mounts these under the virtual `<pluginName>/` namespace. */
162
+ export function pluginMemoryDir(plugin) {
163
+ return join(plugin.root, 'memory');
164
+ }
165
+ export function resolveScopeArg(scopeArg) {
166
+ if (scopeArg === undefined)
167
+ return 'all';
168
+ const value = scopeArg.toLowerCase();
169
+ if (value === 'user' || value === 'project' || value === 'builtin' || value === 'all')
170
+ return value;
171
+ throw usage(`invalid --scope: ${scopeArg} (expected user|project|builtin|all)`);
172
+ }
173
+ export function listScopes(scopeArg) {
174
+ const v = resolveScopeArg(scopeArg);
175
+ if (v === 'all') {
176
+ const out = [];
177
+ if (projectScopeRoot())
178
+ out.push('project');
179
+ out.push('user');
180
+ out.push('builtin');
181
+ return out;
182
+ }
183
+ return [v];
184
+ }
@@ -0,0 +1,17 @@
1
+ import { DatabaseSync } from 'node:sqlite';
2
+ import { type ScopedStore } from './paths.js';
3
+ export declare const PROFILE_ID_SHAPE: RegExp;
4
+ export declare function writeOwnerRoot(db: DatabaseSync, key: string, root: string): void;
5
+ export declare function assertScopedStoreReady(store: ScopedStore): void;
6
+ export declare function discardIncompleteScopedStore(store: ScopedStore): void;
7
+ export declare function withProjectStateLock<T>(root: string, run: () => T): T;
8
+ export declare function scopedStoreExists(store: ScopedStore): boolean;
9
+ export declare function legacyScopedStorePaths(store: ScopedStore): string[];
10
+ export declare function initializeFreshScopedStore(input: {
11
+ kind: 'user' | 'project' | 'provider';
12
+ root: string;
13
+ }): void;
14
+ export declare function openScopedStore(store: ScopedStore): DatabaseSync;
15
+ export declare function createScopedStore(store: ScopedStore, create: (db: DatabaseSync) => void): void;
16
+ export declare function withScopedWrite<T>(store: ScopedStore, run: (db: DatabaseSync) => T): T;
17
+ export declare function closeScopedStores(): void;