skydive-cli 0.5.0-beta.5 → 0.5.0-beta.52
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.
- package/CHANGELOG.md +12 -0
- package/README.md +60 -12
- package/dist/js/api-TwLD7ibI.mjs +315 -0
- package/dist/js/{billing-blocked-Dgu5-oDy.mjs → billing-blocked-SE6tySLd.mjs} +1 -1
- package/dist/js/bin.mjs +658 -279
- package/dist/js/{boot-Brwh0it_.mjs → boot-D_VFIRJj.mjs} +3556 -995
- package/dist/js/chunk-BbwQpWto.mjs +33 -0
- package/dist/js/{client-Cn2af31H.mjs → client-Btq6bMzX.mjs} +108 -2
- package/dist/js/client-CkPQG8M1.mjs +5 -0
- package/dist/js/client-aFwHzCPG.mjs +963 -0
- package/dist/js/daemon-CORAITjj.mjs +7 -0
- package/dist/js/{daemon-client-DcuD4v12.mjs → daemon-client-Cfi66Xy9.mjs} +12 -8
- package/dist/js/daemon-client-DJPW9cRp.mjs +8 -0
- package/dist/js/{daemon-BhArnzeW.mjs → daemon-uzpOdRPL.mjs} +135 -10
- package/dist/js/dist-CRtjM7ba.mjs +1750 -0
- package/dist/js/forward-Ct8Vd2WW.mjs +208 -0
- package/dist/js/{profiler-LLaIFZgn.mjs → install-BBplm-Zp.mjs} +529 -215
- package/dist/js/launcher.mjs +49 -0
- package/dist/js/localhost-cert-Bn-UBUmj.mjs +67 -0
- package/dist/js/{print-ba_0hiV9.mjs → print-Cuao6LN6.mjs} +3 -3
- package/dist/js/{print-CbayCa87.mjs → print-DR6Gas-M.mjs} +291 -39
- package/dist/js/{print-share-uwUG16Ov.mjs → print-share-BypBUrNh.mjs} +9 -3
- package/dist/js/raw-pty-Ci2qFR9F.mjs +5 -0
- package/dist/js/{raw-pty-B6mAroiI.mjs → raw-pty-D5PhKZSl.mjs} +1 -1
- package/dist/js/rest-CJkBP2Jz.mjs +6 -0
- package/dist/js/{rest-BY2nADw5.mjs → rest-DkuT5_oX.mjs} +117 -28
- package/dist/js/tls-cert-CLgSQALB.mjs +4 -0
- package/dist/js/tls-cert-CV-pwxVN.mjs +67 -0
- package/package.json +11 -4
- package/dist/js/client-DZstLQ_1.mjs +0 -4
- package/dist/js/client-Dd5sMXPv.mjs +0 -620
- package/dist/js/daemon-CjzrUvXx.mjs +0 -5
- package/dist/js/daemon-client-D8bNXTxu.mjs +0 -6
- package/dist/js/raw-pty-3EkG-jjH.mjs +0 -5
- package/dist/js/rest-DADJh0bi.mjs +0 -6
- /package/dist/js/{billing-blocked-2wju4gC_.mjs → billing-blocked-D3l5kJlX.mjs} +0 -0
- /package/dist/js/{http-error-DzyrsLAZ.mjs → http-error-BF2NZZE3.mjs} +0 -0
- /package/dist/js/{output-DYzzdXYV.mjs → output-C9mb3sUB.mjs} +0 -0
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import "./client-aFwHzCPG.mjs";
|
|
3
|
+
import { a as runPortalDaemon, c as stopDaemonForHandoff, i as queryDaemonStatus, n as ensureDaemonRunning, o as startPortalDaemon, r as isDaemonListening, s as stopDaemon, t as PortalDaemon } from "./daemon-uzpOdRPL.mjs";
|
|
4
|
+
import "./tls-cert-CV-pwxVN.mjs";
|
|
5
|
+
import "./api-TwLD7ibI.mjs";
|
|
6
|
+
|
|
7
|
+
export { runPortalDaemon };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { d as daemonPaths, f as encodeLine, l as LOCAL_PROTOCOL_VERSION, m as parseDaemonMessage, n as ensureDaemonRunning, p as makeLineParser } from "./daemon-uzpOdRPL.mjs";
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
4
|
import { connect } from "node:net";
|
|
5
5
|
|
|
@@ -12,7 +12,7 @@ var PortalDaemonClient = class {
|
|
|
12
12
|
lastBind = null;
|
|
13
13
|
fallbackCwd = null;
|
|
14
14
|
wantEnabled = false;
|
|
15
|
-
pendingGrants = /* @__PURE__ */ new
|
|
15
|
+
pendingGrants = /* @__PURE__ */ new Map();
|
|
16
16
|
pendingDeclines = /* @__PURE__ */ new Map();
|
|
17
17
|
grantedAgentIds = /* @__PURE__ */ new Set();
|
|
18
18
|
constructor(opts) {
|
|
@@ -50,9 +50,10 @@ var PortalDaemonClient = class {
|
|
|
50
50
|
conversationId: this.lastBind.conversationId,
|
|
51
51
|
cwd: this.lastBind.cwd
|
|
52
52
|
});
|
|
53
|
-
for (const agentId of this.pendingGrants) this.send({
|
|
53
|
+
for (const [agentId, conversationId] of this.pendingGrants) this.send({
|
|
54
54
|
t: "grant",
|
|
55
|
-
agentId
|
|
55
|
+
agentId,
|
|
56
|
+
conversationId
|
|
56
57
|
});
|
|
57
58
|
for (const [agentId, declined] of this.pendingDeclines) this.send({
|
|
58
59
|
t: "decline",
|
|
@@ -157,13 +158,16 @@ var PortalDaemonClient = class {
|
|
|
157
158
|
* Authorize one agent to run commands on this machine. Resolves once the
|
|
158
159
|
* request is sent to the daemon (the daemon performs the grant and pushes the
|
|
159
160
|
* updated state); kept async so it's a drop-in for the old in-process client's
|
|
160
|
-
* awaited `grantAgent`.
|
|
161
|
+
* awaited `grantAgent`. `conversationId` names the conversation whose run
|
|
162
|
+
* asked, so the grant can wake the waiting agent; null when the grant is not
|
|
163
|
+
* answering an in-chat request.
|
|
161
164
|
*/
|
|
162
|
-
grantAgent(agentId) {
|
|
163
|
-
this.pendingGrants.
|
|
165
|
+
grantAgent(agentId, conversationId) {
|
|
166
|
+
this.pendingGrants.set(agentId, conversationId);
|
|
164
167
|
this.send({
|
|
165
168
|
t: "grant",
|
|
166
|
-
agentId
|
|
169
|
+
agentId,
|
|
170
|
+
conversationId
|
|
167
171
|
});
|
|
168
172
|
return Promise.resolve();
|
|
169
173
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { n as isRecord, t as PortalClient } from "./client-aFwHzCPG.mjs";
|
|
3
|
+
import { t as defaultTlsCertSource } from "./tls-cert-CV-pwxVN.mjs";
|
|
3
4
|
import os from "node:os";
|
|
4
5
|
import path from "node:path";
|
|
5
6
|
import { z } from "zod";
|
|
@@ -20,7 +21,7 @@ import { access, appendFile, mkdir, readFile, unlink, writeFile } from "node:fs/
|
|
|
20
21
|
* the commit time of the built tree is one monotonic clock they share.
|
|
21
22
|
*/
|
|
22
23
|
function portalDaemonBuild() {
|
|
23
|
-
return "
|
|
24
|
+
return "1787166259";
|
|
24
25
|
}
|
|
25
26
|
/**
|
|
26
27
|
* Whether a client carrying `mine` should replace a running daemon carrying
|
|
@@ -175,10 +176,18 @@ const clientCwdSchema = z.object({
|
|
|
175
176
|
t: z.literal("cwd"),
|
|
176
177
|
cwd: z.string().min(1)
|
|
177
178
|
});
|
|
178
|
-
/**
|
|
179
|
+
/**
|
|
180
|
+
* `grant` — authorize one agent to reach this machine (user-initiated).
|
|
181
|
+
* `conversationId` names the conversation whose run asked for access (the
|
|
182
|
+
* in-chat approval card); the grant carries it upstream so the server wakes
|
|
183
|
+
* the waiting agent. Defaults to null on parse so hellos from CLI builds that
|
|
184
|
+
* predate the field keep granting against a newer daemon — they just skip the
|
|
185
|
+
* wake, same as before the field existed.
|
|
186
|
+
*/
|
|
179
187
|
const clientGrantSchema = z.object({
|
|
180
188
|
t: z.literal("grant"),
|
|
181
|
-
agentId: z.string().min(1)
|
|
189
|
+
agentId: z.string().min(1),
|
|
190
|
+
conversationId: z.string().nullish().default(null)
|
|
182
191
|
});
|
|
183
192
|
/**
|
|
184
193
|
* `decline` — the user answered no to an agent's request for this machine
|
|
@@ -211,6 +220,20 @@ const clientStatusSchema = z.object({ t: z.literal("status") });
|
|
|
211
220
|
* exits — cutting live exec/tunnel connections, not just refusing new ones.
|
|
212
221
|
*/
|
|
213
222
|
const clientShutdownSchema = z.object({ t: z.literal("shutdown") });
|
|
223
|
+
/**
|
|
224
|
+
* `shutdown_handoff` — a newer daemon taking over (self-update / newest-build-
|
|
225
|
+
* wins) asks the incumbent to step down WITHOUT dropping the portal connection
|
|
226
|
+
* first. The incumbent frees the control socket and disconnects attached CLIs
|
|
227
|
+
* (they reconnect to the new daemon in ~500ms, unchanged), but KEEPS its
|
|
228
|
+
* outbound portal WebSocket — and thus its presence claim — alive until the
|
|
229
|
+
* new daemon has dialled out and claimed the machine's presence slot, at which
|
|
230
|
+
* point the server supersedes the incumbent's socket and it exits. This is a
|
|
231
|
+
* make-before-break handoff: the presence key is owned by one daemon or the
|
|
232
|
+
* other for the entire takeover, so `exec`/`file write` never sees a
|
|
233
|
+
* `registered but not connected` gap mid-upgrade. A bounded fallback timeout
|
|
234
|
+
* ensures the incumbent still exits if the successor never comes up.
|
|
235
|
+
*/
|
|
236
|
+
const clientShutdownHandoffSchema = z.object({ t: z.literal("shutdown_handoff") });
|
|
214
237
|
const clientMessageSchema = z.discriminatedUnion("t", [
|
|
215
238
|
clientHelloSchema,
|
|
216
239
|
clientBindSchema,
|
|
@@ -221,7 +244,8 @@ const clientMessageSchema = z.discriminatedUnion("t", [
|
|
|
221
244
|
clientDeclineSchema,
|
|
222
245
|
clientByeSchema,
|
|
223
246
|
clientStatusSchema,
|
|
224
|
-
clientShutdownSchema
|
|
247
|
+
clientShutdownSchema,
|
|
248
|
+
clientShutdownHandoffSchema
|
|
225
249
|
]);
|
|
226
250
|
/**
|
|
227
251
|
* `state` — the shared portal status, pushed to every attached client so each
|
|
@@ -348,6 +372,14 @@ function parseDaemonMessage(line) {
|
|
|
348
372
|
*/
|
|
349
373
|
/** Grace period after the last client detaches before the daemon exits. */
|
|
350
374
|
const IDLE_SHUTDOWN_MS = 3e4;
|
|
375
|
+
/**
|
|
376
|
+
* Upper bound an incumbent daemon waits, during a graceful handoff, for the
|
|
377
|
+
* successor to dial out and claim presence before giving up and releasing the
|
|
378
|
+
* slot itself. Comfortably above a normal successor boot + WS dial (a few
|
|
379
|
+
* hundred ms to a couple of seconds), and short enough that a failed successor
|
|
380
|
+
* doesn't strand the machine as reachable-but-dead for long.
|
|
381
|
+
*/
|
|
382
|
+
const HANDOFF_MAX_WAIT_MS = 1e4;
|
|
351
383
|
var PortalDaemon = class {
|
|
352
384
|
appUrl;
|
|
353
385
|
paths;
|
|
@@ -357,8 +389,10 @@ var PortalDaemon = class {
|
|
|
357
389
|
conns = /* @__PURE__ */ new Set();
|
|
358
390
|
cwds = /* @__PURE__ */ new Map();
|
|
359
391
|
declined = /* @__PURE__ */ new Set();
|
|
392
|
+
persistedMachineName = null;
|
|
360
393
|
fallbackCwd;
|
|
361
394
|
idleTimer = null;
|
|
395
|
+
handingOff = false;
|
|
362
396
|
sessionToken = null;
|
|
363
397
|
deviceToken = null;
|
|
364
398
|
constructor(appUrl) {
|
|
@@ -455,7 +489,7 @@ var PortalDaemon = class {
|
|
|
455
489
|
case "grant":
|
|
456
490
|
this.declined.delete(msg.agentId);
|
|
457
491
|
this.ensureClient();
|
|
458
|
-
this.client?.grantAgent(msg.agentId).catch((error) => {
|
|
492
|
+
this.client?.grantAgent(msg.agentId, msg.conversationId).catch((error) => {
|
|
459
493
|
this.logError("grantAgent failed", error);
|
|
460
494
|
});
|
|
461
495
|
return;
|
|
@@ -468,6 +502,9 @@ var PortalDaemon = class {
|
|
|
468
502
|
case "shutdown":
|
|
469
503
|
this.forceShutdown();
|
|
470
504
|
return;
|
|
505
|
+
case "shutdown_handoff":
|
|
506
|
+
this.gracefulHandoffShutdown();
|
|
507
|
+
return;
|
|
471
508
|
default: return msg;
|
|
472
509
|
}
|
|
473
510
|
}
|
|
@@ -491,12 +528,29 @@ var PortalDaemon = class {
|
|
|
491
528
|
deviceToken: this.deviceToken
|
|
492
529
|
}),
|
|
493
530
|
resolveCwd: (conversationId) => this.resolveCwd(conversationId),
|
|
531
|
+
tlsCertSource: defaultTlsCertSource(process.env, (msg) => this.logInfo(msg)),
|
|
532
|
+
persistedMachineName: this.persistedMachineName,
|
|
533
|
+
onMachineName: (name, source) => this.onMachineName(name, source),
|
|
494
534
|
onState: (state) => {
|
|
495
535
|
this.lastState = state;
|
|
496
536
|
this.broadcastState();
|
|
497
537
|
}
|
|
498
538
|
});
|
|
499
539
|
}
|
|
540
|
+
/**
|
|
541
|
+
* The identity resolved on connect. Persist the name so a later scutil
|
|
542
|
+
* failure reuses it instead of adopting the volatile hostname, and log the
|
|
543
|
+
* source — a `hostname-fallback` after we'd previously registered under
|
|
544
|
+
* scutil is the tell that a flap just orphaned this machine's grants.
|
|
545
|
+
*/
|
|
546
|
+
onMachineName(name, source) {
|
|
547
|
+
if (source === "hostname-fallback" && this.persistedMachineName) this.logInfo(`portal identity WARNING: fell back to hostname "${name}" but had previously registered as "${this.persistedMachineName}" — grants may be orphaned`);
|
|
548
|
+
else this.logInfo(`portal identity: "${name}" (source=${source})`);
|
|
549
|
+
if (name && name !== this.persistedMachineName) {
|
|
550
|
+
this.persistedMachineName = name;
|
|
551
|
+
this.persistState();
|
|
552
|
+
}
|
|
553
|
+
}
|
|
500
554
|
/** The cwd an exec for `conversationId` runs in. */
|
|
501
555
|
resolveCwd(conversationId) {
|
|
502
556
|
if (conversationId) {
|
|
@@ -530,7 +584,14 @@ var PortalDaemon = class {
|
|
|
530
584
|
/** Append a line to the daemon log file (best-effort, for post-hoc debugging). */
|
|
531
585
|
logError(context, error) {
|
|
532
586
|
const message = error instanceof Error ? error.message : String(error);
|
|
533
|
-
|
|
587
|
+
this.logLine(`${context}: ${message}`);
|
|
588
|
+
}
|
|
589
|
+
/** Append an informational line to the daemon log file (best-effort). */
|
|
590
|
+
logInfo(message) {
|
|
591
|
+
this.logLine(message);
|
|
592
|
+
}
|
|
593
|
+
logLine(message) {
|
|
594
|
+
const line = `${(/* @__PURE__ */ new Date()).toISOString()} ${message}\n`;
|
|
534
595
|
appendFile(this.paths.logPath, line).catch((_error) => {});
|
|
535
596
|
}
|
|
536
597
|
/** Close the listener without exiting the process (tests own the process). */
|
|
@@ -581,6 +642,37 @@ var PortalDaemon = class {
|
|
|
581
642
|
this.server?.close();
|
|
582
643
|
process.exit(0);
|
|
583
644
|
}
|
|
645
|
+
/**
|
|
646
|
+
* Graceful takeover step-down (a newer daemon is replacing us). Unlike
|
|
647
|
+
* `forceShutdown`, this does NOT drop the portal connection up front: doing so
|
|
648
|
+
* would let this machine's presence key expire (10s TTL) before the successor
|
|
649
|
+
* dials out and re-claims it, which is the `registered but not connected` gap
|
|
650
|
+
* that made `exec`/`file write` fail mid-self-update.
|
|
651
|
+
*
|
|
652
|
+
* Order matters. We free the control socket and disconnect attached CLIs
|
|
653
|
+
* first, because the successor cannot bind the singleton socket (and thus
|
|
654
|
+
* cannot start its own portal client) until we release it. But we KEEP our
|
|
655
|
+
* outbound portal WebSocket — and its presence claim — alive across that
|
|
656
|
+
* window. When the successor connects and `claim()`s the slot, the server
|
|
657
|
+
* supersedes us and closes our socket; `beginHandoff` resolves on that close
|
|
658
|
+
* and we exit. A bounded fallback inside `beginHandoff` still exits us if the
|
|
659
|
+
* successor never comes up, so we never hold a dead slot forever.
|
|
660
|
+
*/
|
|
661
|
+
async gracefulHandoffShutdown() {
|
|
662
|
+
if (this.handingOff) return;
|
|
663
|
+
this.handingOff = true;
|
|
664
|
+
for (const conn of this.conns) try {
|
|
665
|
+
conn.socket.destroy();
|
|
666
|
+
} catch (_error) {}
|
|
667
|
+
this.conns.clear();
|
|
668
|
+
this.server?.close();
|
|
669
|
+
this.server = null;
|
|
670
|
+
if (this.client) {
|
|
671
|
+
await this.client.beginHandoff(HANDOFF_MAX_WAIT_MS);
|
|
672
|
+
this.client.dispose();
|
|
673
|
+
}
|
|
674
|
+
process.exit(0);
|
|
675
|
+
}
|
|
584
676
|
async loadState() {
|
|
585
677
|
try {
|
|
586
678
|
const raw = await readFile(this.paths.statePath, "utf8");
|
|
@@ -590,6 +682,7 @@ var PortalDaemon = class {
|
|
|
590
682
|
if (Array.isArray(parsed.declined)) {
|
|
591
683
|
for (const id of parsed.declined) if (typeof id === "string") this.declined.add(id);
|
|
592
684
|
}
|
|
685
|
+
if (typeof parsed.machineName === "string" && parsed.machineName) this.persistedMachineName = parsed.machineName;
|
|
593
686
|
}
|
|
594
687
|
} catch (_error) {}
|
|
595
688
|
}
|
|
@@ -597,7 +690,8 @@ var PortalDaemon = class {
|
|
|
597
690
|
const state = {
|
|
598
691
|
version: LOCAL_PROTOCOL_VERSION,
|
|
599
692
|
cwds: Object.fromEntries(this.cwds),
|
|
600
|
-
declined: [...this.declined]
|
|
693
|
+
declined: [...this.declined],
|
|
694
|
+
machineName: this.persistedMachineName ?? void 0
|
|
601
695
|
};
|
|
602
696
|
writeFile(this.paths.statePath, JSON.stringify(state)).catch((error) => {
|
|
603
697
|
this.logError("persistState failed", error);
|
|
@@ -627,7 +721,7 @@ async function ensureDaemonRunning(appUrl) {
|
|
|
627
721
|
if (await isDaemonListening(socketPath)) {
|
|
628
722
|
const status = await queryDaemonStatus(appUrl);
|
|
629
723
|
if (!status || !isNewerBuild(portalDaemonBuild(), status.build)) return;
|
|
630
|
-
if (await
|
|
724
|
+
if (await stopDaemonForHandoff(appUrl) === "failed") return;
|
|
631
725
|
}
|
|
632
726
|
const entry = process.argv[1];
|
|
633
727
|
const args = entry ? [
|
|
@@ -722,6 +816,37 @@ async function queryDaemonStatus(appUrl) {
|
|
|
722
816
|
});
|
|
723
817
|
}
|
|
724
818
|
/**
|
|
819
|
+
* Ask a running incumbent daemon to step down for a make-before-break handoff
|
|
820
|
+
* (see the daemon's `gracefulHandoffShutdown`). Sends `shutdown_handoff` and
|
|
821
|
+
* waits for the incumbent to release the CONTROL SOCKET — at which point the
|
|
822
|
+
* successor can bind it and start its own portal client. Crucially, the
|
|
823
|
+
* incumbent keeps its portal connection (and presence) alive past this point,
|
|
824
|
+
* so the successor's subsequent `claim()` supersedes it with no presence gap.
|
|
825
|
+
*
|
|
826
|
+
* Returns `stopped` once the socket is free, `failed` if it never freed (the
|
|
827
|
+
* caller should then leave the incumbent alone rather than fight it — same
|
|
828
|
+
* fallback semantics as `stopDaemon`), or `not-running` if nothing was there.
|
|
829
|
+
* Unlike `stopDaemon` this never SIGKILLs: the incumbent is intentionally still
|
|
830
|
+
* alive (holding presence) after it frees the socket, so killing it by pid is
|
|
831
|
+
* exactly the gap this path exists to avoid.
|
|
832
|
+
*/
|
|
833
|
+
async function stopDaemonForHandoff(appUrl) {
|
|
834
|
+
const { socketPath } = daemonPaths(appUrl);
|
|
835
|
+
if (!await isDaemonListening(socketPath)) return "not-running";
|
|
836
|
+
const sock = await connectControl(socketPath);
|
|
837
|
+
if (!sock) return "failed";
|
|
838
|
+
sock.write(encodeLine({ t: "shutdown_handoff" }));
|
|
839
|
+
for (let i = 0; i < 50; i += 1) {
|
|
840
|
+
await sleep(100);
|
|
841
|
+
if (!await isDaemonListening(socketPath)) {
|
|
842
|
+
sock.destroy();
|
|
843
|
+
return "stopped";
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
sock.destroy();
|
|
847
|
+
return "failed";
|
|
848
|
+
}
|
|
849
|
+
/**
|
|
725
850
|
* Hard-stop a running daemon. Preferred path: send `shutdown` over the control
|
|
726
851
|
* socket so it drains clients and exits cleanly. If the socket is unresponsive
|
|
727
852
|
* (a wedged daemon), fall back to SIGTERM then SIGKILL by the pid the status
|
|
@@ -773,4 +898,4 @@ function sleep(ms) {
|
|
|
773
898
|
}
|
|
774
899
|
|
|
775
900
|
//#endregion
|
|
776
|
-
export { runPortalDaemon as a,
|
|
901
|
+
export { runPortalDaemon as a, stopDaemonForHandoff as c, daemonPaths as d, encodeLine as f, queryDaemonStatus as i, LOCAL_PROTOCOL_VERSION as l, parseDaemonMessage as m, ensureDaemonRunning as n, startPortalDaemon as o, makeLineParser as p, isDaemonListening as r, stopDaemon as s, PortalDaemon as t, PORTAL_DAEMON_FLAG as u };
|