@thenewlabs/entangle-serve 2.12.2 → 2.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,30 @@
1
+ import * as net from 'net';
2
+ import type { OutputHandler } from '@thenewlabs/entangle-utils';
3
+ /** Connect to a unix socket, rejecting on connection error. */
4
+ export declare function connectSocket(path: string): Promise<net.Socket>;
5
+ /**
6
+ * Poll a socket path until it accepts a connection, resolving the connected
7
+ * socket. Rejects with a pointer to `logFile` if it never comes up in time. The
8
+ * socket is left flowing-paused (no 'data' listener) so no daemon frames are
9
+ * lost before the RemoteHostSession attaches its reader.
10
+ */
11
+ export declare function pollSocket(path: string, logFile: string): Promise<net.Socket>;
12
+ /**
13
+ * Spawn a detached daemon process whose stdout/stderr append to `logFile` and
14
+ * whose config rides entirely in `env`. Returns immediately (the child is
15
+ * unref'd); pair with {@link pollSocket} to wait for its socket.
16
+ */
17
+ export declare function spawnDetached(opts: {
18
+ entry: string;
19
+ args: string[];
20
+ env: NodeJS.ProcessEnv;
21
+ logFile: string;
22
+ }): void;
23
+ /**
24
+ * Attach this terminal to a daemon over its (already connected) socket: wrap it
25
+ * in a RemoteHostSession sized to the current terminal and hand it to the host
26
+ * UI. host-terminal keeps the process alive and calls process.exit on the
27
+ * session's exit/detach path.
28
+ */
29
+ export declare function attachToSocket(socket: net.Socket, output: OutputHandler): void;
30
+ //# sourceMappingURL=daemon-client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"daemon-client.d.ts","sourceRoot":"","sources":["../src/daemon-client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,GAAG,MAAM,KAAK,CAAC;AAG3B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAoBhE,+DAA+D;AAC/D,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAO/D;AAED;;;;;GAKG;AACH,wBAAsB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAcnF;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;IACvB,OAAO,EAAE,MAAM,CAAC;CACjB,GAAG,IAAI,CASP;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,aAAa,GAAG,IAAI,CAM9E"}
@@ -0,0 +1,76 @@
1
+ import * as net from 'net';
2
+ import * as fs from 'fs';
3
+ import { spawn } from 'child_process';
4
+ import { attachHostTerminal } from './host-terminal.js';
5
+ import { RemoteHostSession } from './remote-host-session.js';
6
+ /**
7
+ * The CLIENT half of the tmux-style detach/reattach split: helpers for spawning
8
+ * a detached session daemon and attaching the current terminal to its unix
9
+ * socket. Extracted from the serve CLI wiring so embedders (Locus's CLI) can
10
+ * compose the same spawn/poll/attach flow around their own daemon entry.
11
+ */
12
+ /** How long to wait for a freshly spawned daemon's socket to become connectable. */
13
+ const SOCKET_POLL_INTERVAL_MS = 100;
14
+ const SOCKET_POLL_TIMEOUT_MS = 8000;
15
+ /** Resolve after `ms`. */
16
+ function delay(ms) {
17
+ return new Promise((resolve) => setTimeout(resolve, ms));
18
+ }
19
+ /** Connect to a unix socket, rejecting on connection error. */
20
+ export function connectSocket(path) {
21
+ return new Promise((resolve, reject) => {
22
+ const socket = net.connect(path);
23
+ const onError = (err) => reject(err);
24
+ socket.once('error', onError);
25
+ socket.once('connect', () => { socket.removeListener('error', onError); resolve(socket); });
26
+ });
27
+ }
28
+ /**
29
+ * Poll a socket path until it accepts a connection, resolving the connected
30
+ * socket. Rejects with a pointer to `logFile` if it never comes up in time. The
31
+ * socket is left flowing-paused (no 'data' listener) so no daemon frames are
32
+ * lost before the RemoteHostSession attaches its reader.
33
+ */
34
+ export async function pollSocket(path, logFile) {
35
+ const deadline = Date.now() + SOCKET_POLL_TIMEOUT_MS;
36
+ for (;;) {
37
+ try {
38
+ return await connectSocket(path);
39
+ }
40
+ catch {
41
+ if (Date.now() >= deadline) {
42
+ throw new Error(`Session daemon did not come up within ${SOCKET_POLL_TIMEOUT_MS / 1000}s (socket ${path}); see log: ${logFile}`);
43
+ }
44
+ await delay(SOCKET_POLL_INTERVAL_MS);
45
+ }
46
+ }
47
+ }
48
+ /**
49
+ * Spawn a detached daemon process whose stdout/stderr append to `logFile` and
50
+ * whose config rides entirely in `env`. Returns immediately (the child is
51
+ * unref'd); pair with {@link pollSocket} to wait for its socket.
52
+ */
53
+ export function spawnDetached(opts) {
54
+ const logFd = fs.openSync(opts.logFile, 'a');
55
+ const child = spawn(process.execPath, [opts.entry, ...opts.args], {
56
+ detached: true,
57
+ stdio: ['ignore', logFd, logFd],
58
+ env: opts.env,
59
+ });
60
+ child.unref();
61
+ fs.closeSync(logFd); // the child holds its own fd
62
+ }
63
+ /**
64
+ * Attach this terminal to a daemon over its (already connected) socket: wrap it
65
+ * in a RemoteHostSession sized to the current terminal and hand it to the host
66
+ * UI. host-terminal keeps the process alive and calls process.exit on the
67
+ * session's exit/detach path.
68
+ */
69
+ export function attachToSocket(socket, output) {
70
+ const session = new RemoteHostSession(socket, {
71
+ cols: process.stdout.columns || 80,
72
+ rows: process.stdout.rows || 24,
73
+ });
74
+ attachHostTerminal(session, output);
75
+ }
76
+ //# sourceMappingURL=daemon-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"daemon-client.js","sourceRoot":"","sources":["../src/daemon-client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,57 @@
1
+ import type { OutputHandler } from '@thenewlabs/entangle-utils';
2
+ import type { SharedWorkspace } from './shared-workspace.js';
3
+ import type { LocalHostSession } from './host-session.js';
4
+ /**
5
+ * The reusable socket-server half of a detachable session daemon: everything a
6
+ * daemon does BETWEEN owning a workspace and connecting it to a relay.
7
+ *
8
+ * Extracted from daemon.ts so embedders (Locus's `locus __daemon`) can compose
9
+ * the same detach/reattach machinery around their own boot sequence: it listens
10
+ * on a unix socket where zero or more terminal CLIENTS (RemoteHostSession)
11
+ * attach, fans the session's events out to every attached client, applies each
12
+ * client's input/resize/window ops to the one shared workspace, and keeps the
13
+ * session registry entry current. Detaching a client leaves the daemon (and
14
+ * the session) running.
15
+ *
16
+ * The caller stays responsible for constructing the SharedWorkspace +
17
+ * LocalHostSession, starting the relay agent, and reporting the session URL
18
+ * via {@link DaemonServer.setUrl} once the relay announces it.
19
+ */
20
+ export interface DaemonServerOptions {
21
+ /** Session name — the registry key and the log/socket basename. */
22
+ name: string;
23
+ /** Absolute path of the unix socket to listen on. */
24
+ socketPath: string;
25
+ workspace: SharedWorkspace;
26
+ session: LocalHostSession;
27
+ output: OutputHandler;
28
+ /** What the registry entry records about this session. */
29
+ registry: {
30
+ capId: string;
31
+ kind: 'entangle' | 'locus';
32
+ workspaceRoot?: string;
33
+ };
34
+ /**
35
+ * Extra async teardown run during shutdown, after the workspace and clients
36
+ * are torn down but before the process exits (Locus: close locusd + remove
37
+ * its pipe sockets). Best-effort: a throw here never blocks the exit.
38
+ */
39
+ beforeExit?: () => Promise<void> | void;
40
+ /** Install SIGTERM/SIGINT → shutdown handlers (default true; off for tests). */
41
+ installSignalHandlers?: boolean;
42
+ /** Process exit hook (default process.exit; replaced by tests). */
43
+ exit?: (code: number) => void;
44
+ }
45
+ export interface DaemonServer {
46
+ /** Record the session URL: pushes it to attached clients and the registry. */
47
+ setUrl(url: string): void;
48
+ /** Tear the daemon down (idempotent): broadcast exit, deregister, exit(0). */
49
+ shutdown(code: number | null): void;
50
+ }
51
+ /**
52
+ * Listen on `socketPath`, register the session, and serve attach/detach
53
+ * clients over the IPC protocol (ipc.ts). Resolves once the socket is
54
+ * listening and the registry entry is written (url filled in via setUrl).
55
+ */
56
+ export declare function createDaemonServer(opts: DaemonServerOptions): Promise<DaemonServer>;
57
+ //# sourceMappingURL=daemon-server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"daemon-server.d.ts","sourceRoot":"","sources":["../src/daemon-server.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAChE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAC7D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAW1D;;;;;;;;;;;;;;;GAeG;AACH,MAAM,WAAW,mBAAmB;IAClC,mEAAmE;IACnE,IAAI,EAAE,MAAM,CAAC;IACb,qDAAqD;IACrD,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,eAAe,CAAC;IAC3B,OAAO,EAAE,gBAAgB,CAAC;IAC1B,MAAM,EAAE,aAAa,CAAC;IACtB,0DAA0D;IAC1D,QAAQ,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC;QAAC,aAAa,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAChF;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IACxC,gFAAgF;IAChF,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC,mEAAmE;IACnE,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CAC/B;AAED,MAAM,WAAW,YAAY;IAC3B,8EAA8E;IAC9E,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,8EAA8E;IAC9E,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAAC;CACrC;AAED;;;;GAIG;AACH,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,mBAAmB,GAAG,OAAO,CAAC,YAAY,CAAC,CAiNzF"}
@@ -0,0 +1,258 @@
1
+ import * as net from 'net';
2
+ import * as fs from 'fs';
3
+ import { addSession, assertSocketPathUsable, logPath, removeSession } from './session-registry.js';
4
+ import { createMessageReader, decodeChunk, encodeChunk, writeMessage, } from './ipc.js';
5
+ /**
6
+ * Listen on `socketPath`, register the session, and serve attach/detach
7
+ * clients over the IPC protocol (ipc.ts). Resolves once the socket is
8
+ * listening and the registry entry is written (url filled in via setUrl).
9
+ */
10
+ export async function createDaemonServer(opts) {
11
+ const { name, socketPath: socketPathValue, workspace, session: localSession, output } = opts;
12
+ // Refuse a path bind() would silently truncate — a truncated socket looks
13
+ // dead to every liveness check that stats the full path.
14
+ assertSocketPathUsable(socketPathValue);
15
+ const exit = opts.exit ?? ((code) => process.exit(code));
16
+ const createdAt = Date.now();
17
+ const register = (url) => {
18
+ const info = {
19
+ name,
20
+ socket: socketPathValue,
21
+ logFile: logPath(name),
22
+ pid: process.pid,
23
+ capId: opts.registry.capId,
24
+ url,
25
+ createdAt,
26
+ kind: opts.registry.kind,
27
+ ...(opts.registry.workspaceRoot ? { workspaceRoot: opts.registry.workspaceRoot } : {}),
28
+ };
29
+ addSession(info);
30
+ };
31
+ // --- client set + fan-out -------------------------------------------------
32
+ const clients = new Set();
33
+ const broadcast = (msg) => {
34
+ for (const socket of clients) {
35
+ try {
36
+ writeMessage(socket, msg);
37
+ }
38
+ catch { /* dropped client; its close handler cleans up */ }
39
+ }
40
+ };
41
+ // --- lifecycle / cleanup (idempotent) ------------------------------------
42
+ let server;
43
+ let shuttingDown = false;
44
+ const shutdown = (code) => {
45
+ if (shuttingDown)
46
+ return;
47
+ shuttingDown = true;
48
+ broadcast({ t: 'exit', code });
49
+ try {
50
+ localSession.dispose();
51
+ }
52
+ catch { /* best-effort */ }
53
+ try {
54
+ workspace.kill();
55
+ }
56
+ catch { /* best-effort */ }
57
+ for (const socket of clients) {
58
+ try {
59
+ socket.end();
60
+ }
61
+ catch { /* already gone */ }
62
+ }
63
+ clients.clear();
64
+ try {
65
+ server?.close();
66
+ }
67
+ catch { /* not listening */ }
68
+ removeSession(name);
69
+ try {
70
+ fs.unlinkSync(socketPathValue);
71
+ }
72
+ catch { /* already gone */ }
73
+ // The embedder's teardown may be async (Locus closes locusd); exit after it
74
+ // either way. With no beforeExit this is an immediate exit, as before.
75
+ void (async () => {
76
+ try {
77
+ await opts.beforeExit?.();
78
+ }
79
+ catch { /* best-effort */ }
80
+ exit(0);
81
+ })();
82
+ };
83
+ if (opts.installSignalHandlers !== false) {
84
+ process.on('SIGTERM', () => shutdown(0));
85
+ process.on('SIGINT', () => shutdown(0));
86
+ }
87
+ // Subscribe ONCE to the SESSION-GLOBAL streams and fan each out to every
88
+ // client. Terminal output and window-state are NOT global anymore: each client
89
+ // gets its own workspace viewport (see onConnection) so it can sit on its own
90
+ // active window, so we deliberately do NOT broadcast onHostData/onWindowState.
91
+ localSession.onViewersChange((n) => broadcast({ t: 'viewers', n }));
92
+ localSession.onLog((line) => broadcast({ t: 'log', line }));
93
+ localSession.onUrl((url) => broadcast({ t: 'url', url }));
94
+ // Workspace ended (last window's shell exited) → tear the daemon down.
95
+ localSession.onExit((code) => shutdown(code));
96
+ // --- inbound client handling ---------------------------------------------
97
+ // A client's messages drive ITS OWN workspace viewport (keyed by `vpId`), so
98
+ // input/window ops only move that client's active window. Sizing stays global
99
+ // (the host/workspace size is authoritative), so hello/resize resize the whole
100
+ // workspace as before.
101
+ const handleClientMessage = (msg, socket, vpId) => {
102
+ switch (msg.t) {
103
+ case 'hello':
104
+ workspace.resize(msg.cols, msg.rows);
105
+ break;
106
+ case 'input':
107
+ workspace.writeFromViewport(vpId, decodeChunk(msg.data));
108
+ break;
109
+ case 'resize':
110
+ workspace.resize(msg.cols, msg.rows);
111
+ break;
112
+ case 'win':
113
+ switch (msg.op) {
114
+ case 'new':
115
+ workspace.newWindowForViewport(vpId);
116
+ break;
117
+ case 'next':
118
+ workspace.nextWindowForViewport(vpId);
119
+ break;
120
+ case 'prev':
121
+ workspace.prevWindowForViewport(vpId);
122
+ break;
123
+ case 'select':
124
+ if (msg.index !== undefined)
125
+ workspace.selectWindowForViewport(vpId, msg.index);
126
+ break;
127
+ case 'close':
128
+ if (msg.index !== undefined)
129
+ workspace.closeWindowFromViewport(vpId, msg.index);
130
+ break;
131
+ }
132
+ break;
133
+ case 'refresh':
134
+ // Serialize the viewport's active window NOW and send it back as a
135
+ // `replay` frame (getReplay()'s source client-side). Because this is a
136
+ // full IPC round-trip after the client's own bytes were fed to the
137
+ // emulator, the serialize reflects the window's live screen — this is
138
+ // what lets a host repaint (e.g. after a full-screen app quits) paint
139
+ // the CURRENT primary instead of the stale attach-time cache.
140
+ try {
141
+ const frame = workspace.snapshotForViewport(vpId, msg.scrollback !== undefined ? { scrollback: msg.scrollback } : undefined);
142
+ writeMessage(socket, { t: 'replay', chunk: encodeChunk(frame) });
143
+ }
144
+ catch { /* dropped client; its close handler cleans up */ }
145
+ break;
146
+ case 'scrollback':
147
+ // Serialize the viewport's active window buffer to plain-text lines NOW
148
+ // and send them back for the client's copy-mode pager.
149
+ try {
150
+ writeMessage(socket, { t: 'scrollback', lines: workspace.scrollbackLinesForViewport(vpId) });
151
+ }
152
+ catch { /* dropped client; its close handler cleans up */ }
153
+ break;
154
+ case 'detach':
155
+ // Drop just this client's viewport; the daemon (and session) keep running.
156
+ clients.delete(socket);
157
+ workspace.detachViewport(vpId);
158
+ try {
159
+ socket.end();
160
+ }
161
+ catch { /* already ending */ }
162
+ break;
163
+ case 'kill':
164
+ // End the whole session on a client's request (host UI Ctrl-B q): the
165
+ // exit broadcast inside shutdown() tells every attached client first.
166
+ shutdown(0);
167
+ break;
168
+ }
169
+ };
170
+ let nextVpId = 0;
171
+ const onConnection = (socket) => {
172
+ // Each connection is its own workspace viewport with an independent active
173
+ // window; reuse the connection counter as the viewport key.
174
+ const vpId = `client-${nextVpId++}`;
175
+ clients.add(socket);
176
+ const remove = () => {
177
+ clients.delete(socket);
178
+ workspace.detachViewport(vpId);
179
+ };
180
+ socket.on('close', remove);
181
+ socket.on('error', remove);
182
+ // Attach this client's viewport: the workspace multiplexes ITS active
183
+ // window's output onto this socket and pushes ITS own window-state.
184
+ const { replay } = workspace.attachViewport({
185
+ sid: vpId,
186
+ onData: (chunk) => { try {
187
+ writeMessage(socket, { t: 'data', chunk: encodeChunk(chunk) });
188
+ }
189
+ catch { /* dropped */ } },
190
+ onWindowState: (state) => { try {
191
+ writeMessage(socket, { t: 'window-state', state });
192
+ }
193
+ catch { /* dropped */ } },
194
+ onExit: (code) => { try {
195
+ writeMessage(socket, { t: 'exit', code });
196
+ }
197
+ catch { /* dropped */ } },
198
+ });
199
+ createMessageReader(socket, (msg) => {
200
+ // Only ClientToDaemon frames are expected inbound; a single bad message
201
+ // must not take down the daemon.
202
+ try {
203
+ handleClientMessage(msg, socket, vpId);
204
+ }
205
+ catch (err) {
206
+ output.warn('Client message failed', err instanceof Error ? err.message : String(err));
207
+ }
208
+ }, () => { remove(); try {
209
+ socket.destroy();
210
+ }
211
+ catch { /* already gone */ } });
212
+ // Push this viewport's current state immediately so the client's UI
213
+ // populates fast. Sent synchronously (no await) right after attach, so live
214
+ // onData frames can't slip in before the replay.
215
+ try {
216
+ const url = localSession.getUrl();
217
+ if (url)
218
+ writeMessage(socket, { t: 'url', url });
219
+ writeMessage(socket, { t: 'window-state', state: workspace.windowStateForViewport(vpId) });
220
+ writeMessage(socket, { t: 'viewers', n: workspace.viewerCount() });
221
+ for (const line of localSession.getLogBuffer())
222
+ writeMessage(socket, { t: 'log', line });
223
+ writeMessage(socket, { t: 'replay', chunk: encodeChunk(replay) });
224
+ }
225
+ catch {
226
+ remove();
227
+ }
228
+ };
229
+ // --- listen ---------------------------------------------------------------
230
+ try {
231
+ fs.unlinkSync(socketPathValue);
232
+ }
233
+ catch { /* no stale socket to remove */ }
234
+ server = net.createServer(onConnection);
235
+ server.on('error', (err) => output.error('Daemon socket server error', err.message));
236
+ await new Promise((resolve, reject) => {
237
+ const onError = (err) => reject(err);
238
+ server.once('error', onError);
239
+ server.listen(socketPathValue, () => {
240
+ server.off('error', onError);
241
+ resolve();
242
+ });
243
+ });
244
+ try {
245
+ fs.chmodSync(socketPathValue, 0o600);
246
+ }
247
+ catch { /* best-effort tightening */ }
248
+ // Register now (url filled in via setUrl once the relay assigns it).
249
+ register('');
250
+ return {
251
+ setUrl(url) {
252
+ localSession.setUrl(url);
253
+ register(url);
254
+ },
255
+ shutdown,
256
+ };
257
+ }
258
+ //# sourceMappingURL=daemon-server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"daemon-server.js","sourceRoot":"","sources":["../src/daemon-server.ts"],"names":[],"mappings":"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"}
package/dist/daemon.d.ts CHANGED
@@ -3,12 +3,12 @@
3
3
  *
4
4
  * Runs headless (no TTY): it owns a {@link SharedWorkspace} + {@link LocalHostSession}
5
5
  * and connects to the relay via {@link startAgent}, serving the pinned capability
6
- * handed to it. It listens on a local unix socket where zero or more terminal
7
- * CLIENTS ({@link RemoteHostSession}) attach; it fans the session's events out to
8
- * every attached client and applies each client's input/resize/window ops to the
9
- * one shared session. Detaching a client leaves the daemon (and the session)
10
- * running; the daemon exits only when the workspace's last shell exits or it is
11
- * signalled.
6
+ * handed to it. The socket server that terminal CLIENTS ({@link RemoteHostSession})
7
+ * attach to fan-out, per-client viewports, registry bookkeeping, shutdown lives
8
+ * in daemon-server.ts (shared with embedders like Locus); this file is just the
9
+ * entangle-specific composition around it. Detaching a client leaves the daemon
10
+ * (and the session) running; the daemon exits only when the workspace's last
11
+ * shell exits or it is signalled.
12
12
  *
13
13
  * Config comes entirely from the environment (set by the index wiring that spawns
14
14
  * the daemon):
@@ -1 +1 @@
1
- {"version":3,"file":"daemon.d.ts","sourceRoot":"","sources":["../src/daemon.ts"],"names":[],"mappings":"AAiBA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAsB,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC,CAiN/C"}
1
+ {"version":3,"file":"daemon.d.ts","sourceRoot":"","sources":["../src/daemon.ts"],"names":[],"mappings":"AAOA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAsB,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC,CAkC/C"}