skydive-cli 0.2.0 → 0.3.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,607 @@
1
+ #!/usr/bin/env node
2
+ import { o as isRecord } from "./rest-BlN_uWmL.mjs";
3
+ import { t as PortalClient } from "./client-Dc7GZ3PG.mjs";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import { z } from "zod";
7
+ import { spawn } from "node:child_process";
8
+ import { createHash } from "node:crypto";
9
+ import { connect, createServer } from "node:net";
10
+ import { access, appendFile, mkdir, readFile, unlink, writeFile } from "node:fs/promises";
11
+
12
+ //#region src/chat/portal/local-protocol.ts
13
+ /**
14
+ * Local IPC between the portal DAEMON and the `skydive` CLI processes attached
15
+ * to it, on one host.
16
+ *
17
+ * Why a daemon at all: every `skydive` process used to open its OWN portal
18
+ * WebSocket and register under the identical `<host>-cli` machine name. Portal
19
+ * presence is a single key per (org, machineName), so N processes overwrote it
20
+ * on every heartbeat and the server superseded them in turn — the portal
21
+ * flapped "connected / reconnecting" once per heartbeat while never settling.
22
+ *
23
+ * The daemon fixes this by construction: ONE long-lived process per (user,
24
+ * appUrl) owns the single portal WebSocket and is the only writer of the
25
+ * presence key, so there is nothing to fight over. Every `skydive` process is a
26
+ * thin CLIENT: on startup it spawns the daemon if absent, connects to this Unix
27
+ * socket, registers its conversation→cwd binding, and otherwise ignores the
28
+ * portal entirely.
29
+ *
30
+ * Per-conversation cwd: an agent's `platform portal exec` carries the
31
+ * originating `ANYONE_CONVERSATION_ID`; the server threads it into the `open`
32
+ * directive; the daemon looks the conversation up in its live `conversationId →
33
+ * cwd` map (populated + updated by the attached CLI, including after `!cd`) and
34
+ * runs the child there. So exec for a conversation always lands in that
35
+ * conversation's current shell directory, deterministically, regardless of how
36
+ * many terminals are open.
37
+ *
38
+ * Wire format: newline-delimited JSON, one object per line, both directions —
39
+ * the same shape as the cmux control socket already used elsewhere in the CLI.
40
+ */
41
+ /** Bump when the client/daemon message shapes change incompatibly. */
42
+ const LOCAL_PROTOCOL_VERSION = 1;
43
+ /**
44
+ * argv flag that makes the CLI entry run AS the portal daemon instead of
45
+ * dispatching a normal command. `ensureDaemonRunning` respawns this same
46
+ * executable with `<flag> <appUrl>`; `bin.ts` routes it before yargs, mirroring
47
+ * the update-check worker. Kept here so both the spawner and the entry agree.
48
+ */
49
+ const PORTAL_DAEMON_FLAG = "--skydive-internal-portal-daemon";
50
+ /**
51
+ * Per-user, per-app daemon socket + state paths. Scoped by OS user and a hash
52
+ * of the appUrl so two logins or two deployments (prod vs a PR preview) get
53
+ * distinct daemons and never share a portal identity. Kept under the OS temp
54
+ * dir because Unix socket paths are length-limited (~104 bytes on macOS).
55
+ */
56
+ function daemonPaths(appUrl) {
57
+ const user = safeSegment(os.userInfo().username || "user");
58
+ const app = createHash("sha256").update(appUrl).digest("hex").slice(0, 12);
59
+ const dir = path.join(os.tmpdir(), `skydive-portal-${user}-${app}`);
60
+ return {
61
+ dir,
62
+ socketPath: path.join(dir, "daemon.sock"),
63
+ statePath: path.join(dir, "state.json"),
64
+ logPath: path.join(dir, "daemon.log")
65
+ };
66
+ }
67
+ function safeSegment(value) {
68
+ return value.replace(/[^a-zA-Z0-9_-]/g, "").slice(0, 24) || "user";
69
+ }
70
+ /**
71
+ * `hello` — a CLI attaches. `sessionId` is unique per CLI process; `token` is
72
+ * the CLI's session token, which the daemon needs to mint the portal device
73
+ * token (the daemon is spawned lazily and holds no session of its own). The
74
+ * daemon uses the FIRST hello's token to connect and refreshes from later
75
+ * hellos so a long-lived daemon can outlive the CLI that spawned it.
76
+ */
77
+ const clientHelloSchema = z.object({
78
+ t: z.literal("hello"),
79
+ v: z.number(),
80
+ sessionId: z.string().min(1),
81
+ token: z.string().min(1)
82
+ });
83
+ /**
84
+ * `bind` — associate a conversation with a working directory. Sent on chat
85
+ * start and re-sent whenever the conversation's shell cwd changes (e.g. after
86
+ * `!cd`), so the daemon always runs that conversation's exec in the right
87
+ * place. `conversationId` may be null for a brand-new conversation with no id
88
+ * yet; the CLI re-binds once the server assigns one.
89
+ */
90
+ const clientBindSchema = z.object({
91
+ t: z.literal("bind"),
92
+ sessionId: z.string().min(1),
93
+ conversationId: z.string().min(1),
94
+ cwd: z.string().min(1)
95
+ });
96
+ /** `enable`/`disable` — turn machine sharing on/off (user-initiated). */
97
+ const clientEnableSchema = z.object({ t: z.literal("enable") });
98
+ const clientDisableSchema = z.object({ t: z.literal("disable") });
99
+ /** `grant` — authorize one agent to reach this machine (user-initiated). */
100
+ const clientGrantSchema = z.object({
101
+ t: z.literal("grant"),
102
+ agentId: z.string().min(1)
103
+ });
104
+ /** `bye` — a CLI detaches cleanly (its conversations drop from the map). */
105
+ const clientByeSchema = z.object({
106
+ t: z.literal("bye"),
107
+ sessionId: z.string().min(1)
108
+ });
109
+ /**
110
+ * `status` — an operator control request (`skydive portal daemon status`). The
111
+ * daemon replies with a single `status_result` and the connection closes; it is
112
+ * not a full attach, so it carries no sessionId and never becomes a client.
113
+ */
114
+ const clientStatusSchema = z.object({ t: z.literal("status") });
115
+ /**
116
+ * `shutdown` — an operator hard-takedown (`skydive portal daemon stop`). The
117
+ * daemon closes the portal connection, disconnects every attached client, and
118
+ * exits — cutting live exec/tunnel connections, not just refusing new ones.
119
+ */
120
+ const clientShutdownSchema = z.object({ t: z.literal("shutdown") });
121
+ const clientMessageSchema = z.discriminatedUnion("t", [
122
+ clientHelloSchema,
123
+ clientBindSchema,
124
+ clientEnableSchema,
125
+ clientDisableSchema,
126
+ clientGrantSchema,
127
+ clientByeSchema,
128
+ clientStatusSchema,
129
+ clientShutdownSchema
130
+ ]);
131
+ /**
132
+ * `state` — the shared portal status, pushed to every attached client so each
133
+ * process shows the same connected/error/grant state the daemon holds.
134
+ */
135
+ const daemonStateSchema = z.object({
136
+ t: z.literal("state"),
137
+ status: z.enum([
138
+ "off",
139
+ "connecting",
140
+ "connected",
141
+ "error"
142
+ ]),
143
+ machineName: z.string(),
144
+ friendlyName: z.string(),
145
+ error: z.string().nullable(),
146
+ grantedAgentIds: z.array(z.string())
147
+ });
148
+ /** `hello_ok` — the daemon accepts an attach and reports its protocol version. */
149
+ const daemonHelloOkSchema = z.object({
150
+ t: z.literal("hello_ok"),
151
+ v: z.number()
152
+ });
153
+ /**
154
+ * `status_result` — the daemon's reply to a `status` control request: a snapshot
155
+ * of everything an operator wants from `skydive portal daemon status` — the
156
+ * portal connection state, how many CLIs are attached, and the conversation→cwd
157
+ * routing map. The daemon sends one and the control connection closes.
158
+ */
159
+ const daemonStatusResultSchema = z.object({
160
+ t: z.literal("status_result"),
161
+ v: z.number(),
162
+ pid: z.number(),
163
+ appUrl: z.string(),
164
+ portal: z.object({
165
+ status: z.enum([
166
+ "off",
167
+ "connecting",
168
+ "connected",
169
+ "error"
170
+ ]),
171
+ machineName: z.string(),
172
+ friendlyName: z.string(),
173
+ error: z.string().nullable(),
174
+ grantedAgentIds: z.array(z.string())
175
+ }),
176
+ clientCount: z.number(),
177
+ cwds: z.record(z.string(), z.string())
178
+ });
179
+ const daemonMessageSchema = z.discriminatedUnion("t", [
180
+ daemonStateSchema,
181
+ daemonHelloOkSchema,
182
+ daemonStatusResultSchema
183
+ ]);
184
+ function encodeLine(msg) {
185
+ return `${JSON.stringify(msg)}\n`;
186
+ }
187
+ /**
188
+ * Stateful line splitter: feed raw socket chunks, get back complete JSON lines.
189
+ * A partial trailing line is buffered until its newline arrives.
190
+ */
191
+ function makeLineParser() {
192
+ let buf = "";
193
+ return (chunk) => {
194
+ buf += chunk;
195
+ const lines = [];
196
+ let nl = buf.indexOf("\n");
197
+ while (nl !== -1) {
198
+ lines.push(buf.slice(0, nl));
199
+ buf = buf.slice(nl + 1);
200
+ nl = buf.indexOf("\n");
201
+ }
202
+ return lines;
203
+ };
204
+ }
205
+ /** Parse one line into a client message, or null if malformed. */
206
+ function parseClientMessage(line) {
207
+ try {
208
+ return clientMessageSchema.parse(JSON.parse(line));
209
+ } catch (_error) {
210
+ return null;
211
+ }
212
+ }
213
+ /** Parse one line into a daemon message, or null if malformed. */
214
+ function parseDaemonMessage(line) {
215
+ try {
216
+ return daemonMessageSchema.parse(JSON.parse(line));
217
+ } catch (_error) {
218
+ return null;
219
+ }
220
+ }
221
+
222
+ //#endregion
223
+ //#region src/chat/portal/daemon.ts
224
+ /**
225
+ * The portal DAEMON: one long-lived process per (user, appUrl) that owns the
226
+ * SINGLE portal WebSocket to the server and is the only writer of the machine's
227
+ * presence key. Every `skydive` CLI attaches to it over a Unix socket instead
228
+ * of opening its own portal connection, so N concurrent CLIs can no longer
229
+ * fight over one machine identity (the flap) — there is exactly one identity by
230
+ * construction.
231
+ *
232
+ * State it owns:
233
+ * - the one `PortalClient` (portal WS + device registration + grants);
234
+ * - a live `conversationId -> cwd` map, populated and updated by attached CLIs
235
+ * (including after `!cd`), and PERSISTED so a daemon restart doesn't lose
236
+ * where a conversation's exec should run. An exec directive carries its
237
+ * originating conversation id; the daemon runs the child in that
238
+ * conversation's cwd.
239
+ *
240
+ * Lifecycle: spawned lazily by the first CLI that finds no daemon listening.
241
+ * It exits on its own once the last CLI detaches (after a short linger, so it
242
+ * survives rapid session churn), so there is never an orphan holding the portal
243
+ * open after every chat is closed.
244
+ */
245
+ /** Grace period after the last client detaches before the daemon exits. */
246
+ const IDLE_SHUTDOWN_MS = 3e4;
247
+ var PortalDaemon = class {
248
+ paths;
249
+ server = null;
250
+ client = null;
251
+ lastState = null;
252
+ conns = /* @__PURE__ */ new Set();
253
+ cwds = /* @__PURE__ */ new Map();
254
+ fallbackCwd;
255
+ idleTimer = null;
256
+ sessionToken = null;
257
+ constructor(appUrl) {
258
+ this.appUrl = appUrl;
259
+ this.paths = daemonPaths(appUrl);
260
+ this.fallbackCwd = process.env.HOME ?? process.cwd();
261
+ }
262
+ /** Start listening. Rejects if the socket is already held by another daemon. */
263
+ async listen() {
264
+ await mkdir(this.paths.dir, { recursive: true });
265
+ await this.loadState();
266
+ await this.clearStaleSocket();
267
+ return new Promise((resolve, reject) => {
268
+ const server = createServer((socket) => this.onClientConnect(socket));
269
+ server.on("error", reject);
270
+ server.listen(this.paths.socketPath, () => {
271
+ this.server = server;
272
+ this.armIdleTimer();
273
+ resolve();
274
+ });
275
+ });
276
+ }
277
+ async clearStaleSocket() {
278
+ if (!await pathExists(this.paths.socketPath)) return;
279
+ if (await new Promise((resolve) => {
280
+ const probe = connect(this.paths.socketPath);
281
+ probe.on("connect", () => {
282
+ probe.destroy();
283
+ resolve(true);
284
+ });
285
+ probe.on("error", () => resolve(false));
286
+ })) throw new Error("daemon already running");
287
+ await unlink(this.paths.socketPath).catch((_error) => {});
288
+ }
289
+ onClientConnect(socket) {
290
+ const conn = {
291
+ socket,
292
+ sessionId: null,
293
+ conversations: /* @__PURE__ */ new Set()
294
+ };
295
+ this.conns.add(conn);
296
+ this.clearIdleTimer();
297
+ socket.setEncoding("utf8");
298
+ const parse = makeLineParser();
299
+ socket.on("data", (chunk) => {
300
+ for (const line of parse(chunk)) this.onClientLine(conn, line);
301
+ });
302
+ const drop = () => this.onClientClose(conn);
303
+ socket.on("close", drop);
304
+ socket.on("error", drop);
305
+ }
306
+ onClientLine(conn, line) {
307
+ const msg = parseClientMessage(line);
308
+ if (!msg) return;
309
+ switch (msg.t) {
310
+ case "hello":
311
+ conn.sessionId = msg.sessionId;
312
+ this.sessionToken = msg.token;
313
+ this.ensureClient();
314
+ this.send(conn, {
315
+ t: "hello_ok",
316
+ v: LOCAL_PROTOCOL_VERSION
317
+ });
318
+ if (this.lastState) this.send(conn, stateMessage(this.lastState));
319
+ return;
320
+ case "bind":
321
+ conn.conversations.add(msg.conversationId);
322
+ this.cwds.set(msg.conversationId, msg.cwd);
323
+ this.fallbackCwd = msg.cwd;
324
+ this.persistState();
325
+ return;
326
+ case "enable":
327
+ this.ensureClient();
328
+ this.client?.enable();
329
+ return;
330
+ case "disable":
331
+ this.client?.disable();
332
+ return;
333
+ case "grant":
334
+ this.ensureClient();
335
+ this.client?.grantAgent(msg.agentId).catch((error) => {
336
+ this.logError("grantAgent failed", error);
337
+ });
338
+ return;
339
+ case "bye":
340
+ this.onClientClose(conn);
341
+ return;
342
+ case "status":
343
+ this.send(conn, this.statusResult());
344
+ return;
345
+ case "shutdown":
346
+ this.forceShutdown();
347
+ return;
348
+ default: return msg;
349
+ }
350
+ }
351
+ onClientClose(conn) {
352
+ if (!this.conns.has(conn)) return;
353
+ this.conns.delete(conn);
354
+ try {
355
+ conn.socket.destroy();
356
+ } catch (_error) {}
357
+ if (this.conns.size === 0) this.armIdleTimer();
358
+ }
359
+ /** Create the single PortalClient the first time a client needs the portal. */
360
+ ensureClient() {
361
+ if (this.client || !this.sessionToken) return;
362
+ this.client = new PortalClient({
363
+ appUrl: this.appUrl,
364
+ sessionToken: this.sessionToken,
365
+ resolveCwd: (conversationId) => this.resolveCwd(conversationId),
366
+ onState: (state) => {
367
+ this.lastState = state;
368
+ this.broadcast(stateMessage(state));
369
+ }
370
+ });
371
+ }
372
+ /** The cwd an exec for `conversationId` runs in. */
373
+ resolveCwd(conversationId) {
374
+ if (conversationId) {
375
+ const cwd = this.cwds.get(conversationId);
376
+ if (cwd) return cwd;
377
+ }
378
+ return this.fallbackCwd;
379
+ }
380
+ send(conn, msg) {
381
+ if (conn.socket.writable) conn.socket.write(encodeLine(msg));
382
+ }
383
+ broadcast(msg) {
384
+ for (const conn of this.conns) this.send(conn, msg);
385
+ }
386
+ armIdleTimer() {
387
+ this.clearIdleTimer();
388
+ this.idleTimer = setTimeout(() => this.shutdown(), IDLE_SHUTDOWN_MS);
389
+ this.idleTimer.unref();
390
+ }
391
+ clearIdleTimer() {
392
+ if (this.idleTimer) {
393
+ clearTimeout(this.idleTimer);
394
+ this.idleTimer = null;
395
+ }
396
+ }
397
+ /** Append a line to the daemon log file (best-effort, for post-hoc debugging). */
398
+ logError(context, error) {
399
+ const message = error instanceof Error ? error.message : String(error);
400
+ const line = `${(/* @__PURE__ */ new Date()).toISOString()} ${context}: ${message}\n`;
401
+ appendFile(this.paths.logPath, line).catch((_error) => {});
402
+ }
403
+ shutdown() {
404
+ if (this.conns.size > 0) return;
405
+ this.client?.dispose();
406
+ this.server?.close();
407
+ process.exit(0);
408
+ }
409
+ /** Snapshot for a `status` control request. */
410
+ statusResult() {
411
+ const portal = this.lastState;
412
+ return {
413
+ t: "status_result",
414
+ v: LOCAL_PROTOCOL_VERSION,
415
+ pid: process.pid,
416
+ appUrl: this.appUrl,
417
+ portal: {
418
+ status: portal?.status ?? "off",
419
+ machineName: portal?.machineName ?? "",
420
+ friendlyName: portal?.friendlyName ?? "",
421
+ error: portal?.error ?? null,
422
+ grantedAgentIds: portal?.grantedAgentIds ?? []
423
+ },
424
+ clientCount: [...this.conns].filter((c) => c.sessionId !== null).length,
425
+ cwds: Object.fromEntries(this.cwds)
426
+ };
427
+ }
428
+ /**
429
+ * Operator-initiated hard takedown: unlike the idle `shutdown`, this ignores
430
+ * the client-count guard, actively disconnects every attached CLI (cutting
431
+ * their live exec/tunnel relays), drops the portal connection, and exits.
432
+ */
433
+ forceShutdown() {
434
+ for (const conn of this.conns) try {
435
+ conn.socket.destroy();
436
+ } catch (_error) {}
437
+ this.conns.clear();
438
+ this.client?.dispose();
439
+ this.server?.close();
440
+ process.exit(0);
441
+ }
442
+ async loadState() {
443
+ try {
444
+ const raw = await readFile(this.paths.statePath, "utf8");
445
+ const parsed = JSON.parse(raw);
446
+ if (isRecord(parsed) && parsed.version === LOCAL_PROTOCOL_VERSION && isRecord(parsed.cwds)) {
447
+ for (const [id, cwd] of Object.entries(parsed.cwds)) if (typeof cwd === "string") this.cwds.set(id, cwd);
448
+ }
449
+ } catch (_error) {}
450
+ }
451
+ persistState() {
452
+ const state = {
453
+ version: LOCAL_PROTOCOL_VERSION,
454
+ cwds: Object.fromEntries(this.cwds)
455
+ };
456
+ writeFile(this.paths.statePath, JSON.stringify(state)).catch((error) => {
457
+ this.logError("persistState failed", error);
458
+ });
459
+ }
460
+ };
461
+ /**
462
+ * Spawn a detached daemon process for this appUrl if one isn't already running.
463
+ * The CLI calls this on startup; it returns once the daemon is listening (or
464
+ * immediately if one already is). It respawns THIS executable (the bundled CLI
465
+ * entry) with PORTAL_DAEMON_FLAG, which `bin.ts` routes to `runPortalDaemon`
466
+ * before dispatching a normal command — the same pattern the update-check
467
+ * worker uses, so it survives bundling (import.meta.url points at the bundle,
468
+ * not a standalone daemon module).
469
+ */
470
+ async function ensureDaemonRunning(appUrl) {
471
+ const { socketPath } = daemonPaths(appUrl);
472
+ if (await isDaemonListening(socketPath)) return;
473
+ const entry = process.argv[1];
474
+ const args = entry ? [
475
+ entry,
476
+ PORTAL_DAEMON_FLAG,
477
+ appUrl
478
+ ] : [PORTAL_DAEMON_FLAG, appUrl];
479
+ spawn(process.execPath, args, {
480
+ detached: true,
481
+ stdio: "ignore",
482
+ env: process.env
483
+ }).unref();
484
+ for (let i = 0; i < 50; i += 1) {
485
+ if (await isDaemonListening(socketPath)) return;
486
+ await sleep(100);
487
+ }
488
+ }
489
+ /**
490
+ * Entry the CLI calls when launched with PORTAL_DAEMON_FLAG: become the daemon.
491
+ * `appUrl` is the argv token right after the flag.
492
+ */
493
+ function runPortalDaemon(argv) {
494
+ const appUrl = argv[argv.indexOf(PORTAL_DAEMON_FLAG) + 1];
495
+ if (!appUrl) process.exit(1);
496
+ new PortalDaemon(appUrl).listen().catch(() => process.exit(1));
497
+ }
498
+ async function isDaemonListening(socketPath) {
499
+ if (!await pathExists(socketPath)) return false;
500
+ return new Promise((resolve) => {
501
+ const probe = connect(socketPath);
502
+ probe.on("connect", () => {
503
+ probe.destroy();
504
+ resolve(true);
505
+ });
506
+ probe.on("error", () => resolve(false));
507
+ });
508
+ }
509
+ async function pathExists(p) {
510
+ try {
511
+ await access(p);
512
+ return true;
513
+ } catch (_error) {
514
+ return false;
515
+ }
516
+ }
517
+ /** Open a short-lived control connection to the daemon, or null if none. */
518
+ function connectControl(socketPath) {
519
+ return new Promise((resolve) => {
520
+ const sock = connect(socketPath);
521
+ sock.once("connect", () => resolve(sock));
522
+ sock.once("error", () => resolve(null));
523
+ });
524
+ }
525
+ /**
526
+ * Query a running daemon for its status snapshot. Returns null when no daemon is
527
+ * listening (so callers can print "not running") or if it doesn't reply in time.
528
+ */
529
+ async function queryDaemonStatus(appUrl) {
530
+ const { socketPath } = daemonPaths(appUrl);
531
+ const sock = await connectControl(socketPath);
532
+ if (!sock) return null;
533
+ return new Promise((resolve) => {
534
+ const parse = makeLineParser();
535
+ let settled = false;
536
+ const finish = (result) => {
537
+ if (settled) return;
538
+ settled = true;
539
+ clearTimeout(timer);
540
+ sock.destroy();
541
+ resolve(result);
542
+ };
543
+ const timer = setTimeout(() => finish(null), 3e3);
544
+ sock.setEncoding("utf8");
545
+ sock.on("data", (chunk) => {
546
+ for (const line of parse(chunk)) {
547
+ const msg = parseDaemonMessage(line);
548
+ if (msg?.t === "status_result") finish(msg);
549
+ }
550
+ });
551
+ sock.on("error", () => finish(null));
552
+ sock.on("close", () => finish(null));
553
+ sock.write(encodeLine({ t: "status" }));
554
+ });
555
+ }
556
+ /**
557
+ * Hard-stop a running daemon. Preferred path: send `shutdown` over the control
558
+ * socket so it drains clients and exits cleanly. If the socket is unresponsive
559
+ * (a wedged daemon), fall back to SIGTERM then SIGKILL by the pid the status
560
+ * reports. Returns what actually happened.
561
+ */
562
+ async function stopDaemon(appUrl) {
563
+ const { socketPath } = daemonPaths(appUrl);
564
+ if (!await isDaemonListening(socketPath)) return "not-running";
565
+ const status = await queryDaemonStatus(appUrl);
566
+ const sock = await connectControl(socketPath);
567
+ if (sock) {
568
+ sock.write(encodeLine({ t: "shutdown" }));
569
+ for (let i = 0; i < 30; i += 1) {
570
+ await sleep(100);
571
+ if (!await isDaemonListening(socketPath)) {
572
+ sock.destroy();
573
+ return "stopped";
574
+ }
575
+ }
576
+ sock.destroy();
577
+ }
578
+ const pid = status?.pid;
579
+ if (typeof pid === "number") try {
580
+ process.kill(pid, "SIGTERM");
581
+ for (let i = 0; i < 20; i += 1) {
582
+ await sleep(100);
583
+ if (!await isDaemonListening(socketPath)) return "killed";
584
+ }
585
+ process.kill(pid, "SIGKILL");
586
+ return "killed";
587
+ } catch (_error) {
588
+ return await isDaemonListening(socketPath) ? "failed" : "killed";
589
+ }
590
+ return await isDaemonListening(socketPath) ? "failed" : "stopped";
591
+ }
592
+ function stateMessage(state) {
593
+ return {
594
+ t: "state",
595
+ status: state.status,
596
+ machineName: state.machineName,
597
+ friendlyName: state.friendlyName,
598
+ error: state.error,
599
+ grantedAgentIds: state.grantedAgentIds
600
+ };
601
+ }
602
+ function sleep(ms) {
603
+ return new Promise((resolve) => setTimeout(resolve, ms));
604
+ }
605
+
606
+ //#endregion
607
+ export { runPortalDaemon as a, PORTAL_DAEMON_FLAG as c, makeLineParser as d, parseDaemonMessage as f, queryDaemonStatus as i, daemonPaths as l, ensureDaemonRunning as n, stopDaemon as o, isDaemonListening as r, LOCAL_PROTOCOL_VERSION as s, PortalDaemon as t, encodeLine as u };