skydive-cli 0.5.0-beta.44 → 0.5.0-beta.46

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/dist/js/bin.mjs CHANGED
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
- import { o as name, s as version, t as createRestClient } from "./rest-DpsJUYTL.mjs";
3
- import { B as resolveSession$1, C as deleteConfig, D as getPreference, E as getLastSeenVersion, G as setLastSeenVersion, H as saveConfig, I as resolveAppUrl, L as resolveChatAuth, M as getStoredApiKeyId, N as getStoredApiKeyWorkspaceName, O as getPromptHistoryPath, P as getUpdateCheckDisabled, R as resolveConfig, S as PREFERENCES, T as getDefaultAgent, U as saveSession, _ as API_KEYS_URL, b as DEFAULT_API_URL, i as resolveAgent$1, j as getShareMachineDefault, v as API_KEY_FAMILY_PREFIX, w as getConfigPath, y as API_KEY_PREFIX, z as resolveManagementAuth } from "./print-Ds16-Q4O.mjs";
4
- import { A as installAgentAlias, B as resolveWorkspaceId, D as machineOsFromPlatform, E as buildImportSeedPrompt, F as detectShell, I as ensureActiveOrganization, L as getActiveWorkspaceId, M as takenAliasNames, N as SUPPORTED_SHELLS, O as aliasActivationHint, P as defaultInstallEnv, R as getSessionIdentity, V as setActiveWorkspace, c as getAutomaticLogsDir, j as slugifyAliasName, k as dedupeAliasName, l as maybeStartProfiling, o as brandHelpArt, t as installCrashHandler, u as profileDir, w as themes, z as listWorkspaces } from "./install-DrtNldUN.mjs";
2
+ import { o as name, s as version, t as createRestClient } from "./rest-BtHVI0y6.mjs";
3
+ import { B as resolveSession$1, C as deleteConfig, D as getPreference, E as getLastSeenVersion, G as setLastSeenVersion, H as saveConfig, I as resolveAppUrl, L as resolveChatAuth, M as getStoredApiKeyId, N as getStoredApiKeyWorkspaceName, O as getPromptHistoryPath, P as getUpdateCheckDisabled, R as resolveConfig, S as PREFERENCES, T as getDefaultAgent, U as saveSession, _ as API_KEYS_URL, b as DEFAULT_API_URL, i as resolveAgent$1, j as getShareMachineDefault, v as API_KEY_FAMILY_PREFIX, w as getConfigPath, y as API_KEY_PREFIX, z as resolveManagementAuth } from "./print-Bg8rzq9t.mjs";
4
+ import { A as installAgentAlias, B as resolveWorkspaceId, D as machineOsFromPlatform, E as buildImportSeedPrompt, F as detectShell, I as ensureActiveOrganization, L as getActiveWorkspaceId, M as takenAliasNames, N as SUPPORTED_SHELLS, O as aliasActivationHint, P as defaultInstallEnv, R as getSessionIdentity, V as setActiveWorkspace, c as getAutomaticLogsDir, j as slugifyAliasName, k as dedupeAliasName, l as maybeStartProfiling, o as brandHelpArt, t as installCrashHandler, u as profileDir, w as themes, z as listWorkspaces } from "./install-DNJ6k578.mjs";
5
5
  import { n as printError, r as printTable, t as output } from "./output-C9mb3sUB.mjs";
6
6
  import { t as HttpError } from "./http-error-BF2NZZE3.mjs";
7
7
  import "./billing-blocked-D3l5kJlX.mjs";
8
- import { r as resolveMachineIdentity } from "./client-BnpgrtCC.mjs";
9
- import { i as queryDaemonStatus, l as PORTAL_DAEMON_FLAG, n as ensureDaemonRunning, s as stopDaemon, u as daemonPaths } from "./daemon-w8vtZ81t.mjs";
8
+ import { r as resolveMachineIdentity } from "./client-C2lPer4b.mjs";
9
+ import { d as daemonPaths, i as queryDaemonStatus, n as ensureDaemonRunning, s as stopDaemon, u as PORTAL_DAEMON_FLAG } from "./daemon-MZN78SYL.mjs";
10
10
  import "./tls-cert-CV-pwxVN.mjs";
11
11
  import { i as grantPortalAccess, n as fetchPortalDevices, o as registerPortalDevice, r as findThisDevice, s as revokePortalAccess } from "./api-TwLD7ibI.mjs";
12
12
  import { a as encodeFsRequest, i as decodeFsResponse, n as SANDBOX_STREAM_PATH, r as streamSpecToQuery, t as SandboxStream } from "./client-Btq6bMzX.mjs";
@@ -1588,7 +1588,7 @@ const importCommand = {
1588
1588
  process.exit(1);
1589
1589
  }
1590
1590
  }
1591
- const { runChat } = await import("./boot-ClKjP9Mn.mjs");
1591
+ const { runChat } = await import("./boot-BD8HZcOo.mjs");
1592
1592
  await runChat({
1593
1593
  appUrl,
1594
1594
  sessionToken: session.value.sessionToken,
@@ -1618,7 +1618,7 @@ async function runImportPrintMode({ argv, appUrl }) {
1618
1618
  printError(`${session.error.message} For non-interactive use, run \`skydive auth login --web\` first, or set SKYDIVE_SESSION_TOKEN.`);
1619
1619
  process.exit(1);
1620
1620
  }
1621
- const { connectMachineShare } = await import("./print-share-Df6eAG12.mjs");
1621
+ const { connectMachineShare } = await import("./print-share-D20YvVbb.mjs");
1622
1622
  const machineShare = await connectMachineShare({
1623
1623
  appUrl,
1624
1624
  sessionToken: session.value.sessionToken,
@@ -1626,7 +1626,7 @@ async function runImportPrintMode({ argv, appUrl }) {
1626
1626
  });
1627
1627
  const extra = (argv.print ?? "").trim();
1628
1628
  const prompt = buildImportSeedPrompt(process.cwd(), machineOsFromPlatform(process.platform)) + (extra ? `\n\nAdditional instructions: ${extra}` : "");
1629
- const { runPrint } = await import("./print-Dc-xRapT.mjs");
1629
+ const { runPrint } = await import("./print-BAdeImNJ.mjs");
1630
1630
  try {
1631
1631
  const result = await runPrint({
1632
1632
  appUrl,
@@ -2033,7 +2033,7 @@ const chatCommand = {
2033
2033
  sessionToken: auth.value.token,
2034
2034
  agentSelector: argv.agent ?? null
2035
2035
  });
2036
- const { runChat } = await import("./boot-ClKjP9Mn.mjs");
2036
+ const { runChat } = await import("./boot-BD8HZcOo.mjs");
2037
2037
  await runChat({
2038
2038
  appUrl: auth.value.appUrl,
2039
2039
  sessionToken: auth.value.token,
@@ -2117,7 +2117,7 @@ async function runPrintMode({ argv, appUrl }) {
2117
2117
  printError(`${auth.error.message} For non-interactive use, run \`skydive auth login\` first, or set SKYDIVE_API_KEY / SKYDIVE_SESSION_TOKEN.`);
2118
2118
  process.exit(1);
2119
2119
  }
2120
- const { runPrint, readStdin } = await import("./print-Dc-xRapT.mjs");
2120
+ const { runPrint, readStdin } = await import("./print-BAdeImNJ.mjs");
2121
2121
  const workspaceId = await resolveWorkspaceFlag({
2122
2122
  appUrl,
2123
2123
  sessionToken: auth.value.token,
@@ -2147,7 +2147,7 @@ async function runPrintMode({ argv, appUrl }) {
2147
2147
  process.exit(1);
2148
2148
  }
2149
2149
  } else {
2150
- const { connectMachineShare } = await import("./print-share-Df6eAG12.mjs");
2150
+ const { connectMachineShare } = await import("./print-share-D20YvVbb.mjs");
2151
2151
  machineShare = await connectMachineShare({
2152
2152
  appUrl: auth.value.appUrl,
2153
2153
  sessionToken: auth.value.token,
@@ -2212,7 +2212,7 @@ const getCommand$1 = {
2212
2212
  printError(`${auth.error.message} Run \`skydive auth login\` first, or set SKYDIVE_API_KEY / SKYDIVE_SESSION_TOKEN.`);
2213
2213
  process.exit(1);
2214
2214
  }
2215
- const { messageGet } = await import("./print-Dc-xRapT.mjs");
2215
+ const { messageGet } = await import("./print-BAdeImNJ.mjs");
2216
2216
  try {
2217
2217
  const result = await messageGet({
2218
2218
  appUrl: auth.value.appUrl,
@@ -2415,7 +2415,7 @@ const switchCommand = {
2415
2415
  printError("The workspace picker needs the Bun runtime and it could not be set up automatically. Pass a workspace slug instead, or install Bun and retry.");
2416
2416
  process.exit(1);
2417
2417
  }
2418
- const { runWorkspacePicker } = await import("./boot-ClKjP9Mn.mjs");
2418
+ const { runWorkspacePicker } = await import("./boot-BD8HZcOo.mjs");
2419
2419
  await runWorkspacePicker(session);
2420
2420
  return;
2421
2421
  }
@@ -2494,7 +2494,7 @@ const openCommand = {
2494
2494
  const agent = argv.agent ? resolveAgent$1((await fetchPortalDevices(session)).agents, argv.agent) : null;
2495
2495
  const cwd = argv.cwd ? path.resolve(argv.cwd) : process.cwd();
2496
2496
  const { machineName } = await resolveMachineIdentity(null);
2497
- const { PortalDaemonClient } = await import("./daemon-client-D5YCk5Hc.mjs");
2497
+ const { PortalDaemonClient } = await import("./daemon-client-DMx26-Rz.mjs");
2498
2498
  let lastLine = "";
2499
2499
  let signalConnected;
2500
2500
  const connected = new Promise((resolve) => {
@@ -2808,8 +2808,8 @@ const sandboxCommand = {
2808
2808
  }).example("skydive sandbox --agent grace", "Live terminal (Ctrl-] detaches)").example("skydive sandbox --agent grace -- tail -n 50 /tmp/harness.log", "One-shot command (use `--` so its flags reach the sandbox)").example("skydive sandbox --agent grace -- sh -c 'ls /tmp | wc -l'", "Shell features go through an explicit `sh -c`"),
2809
2809
  handler: async (argv) => {
2810
2810
  const session = requireSession(argv);
2811
- const { createRestClient } = await import("./rest-Do3mPVpC.mjs");
2812
- const { resolveAgent } = await import("./print-Dc-xRapT.mjs");
2811
+ const { createRestClient } = await import("./rest-8QGtGZIO.mjs");
2812
+ const { resolveAgent } = await import("./print-BAdeImNJ.mjs");
2813
2813
  const client = createRestClient({
2814
2814
  appUrl: session.appUrl,
2815
2815
  sessionToken: session.sessionToken
@@ -3077,8 +3077,8 @@ const fsEditCommand = {
3077
3077
  handler: async (argv) => {
3078
3078
  const session = requireSession(argv);
3079
3079
  const remotePath = argv.path;
3080
- const { createRestClient } = await import("./rest-Do3mPVpC.mjs");
3081
- const { resolveAgent } = await import("./print-Dc-xRapT.mjs");
3080
+ const { createRestClient } = await import("./rest-8QGtGZIO.mjs");
3081
+ const { resolveAgent } = await import("./print-BAdeImNJ.mjs");
3082
3082
  const client = createRestClient({
3083
3083
  appUrl: session.appUrl,
3084
3084
  sessionToken: session.sessionToken
@@ -4548,7 +4548,7 @@ if (process.argv.includes(UPDATE_WORKER_FLAG)) {
4548
4548
  process.exit(0);
4549
4549
  }
4550
4550
  if (process.argv.includes(PORTAL_DAEMON_FLAG)) {
4551
- const { runPortalDaemon } = await import("./daemon-mLypjXdu.mjs");
4551
+ const { runPortalDaemon } = await import("./daemon-S0p-4FGa.mjs");
4552
4552
  runPortalDaemon(process.argv);
4553
4553
  } else runCli();
4554
4554
  function runCli() {
@@ -1,15 +1,15 @@
1
1
  #!/usr/bin/env node
2
- import { a as isRecord, i as errorMessage, n as errorDetail, r as sendErrorMessage, t as createRestClient } from "./rest-DpsJUYTL.mjs";
3
- import { A as getSavedTheme, F as recordDefaultAgent, V as resolveWebUrl, W as saveTheme, b as DEFAULT_API_URL, c as cardActionErrorMessage, d as reconcileMaskedInput, f as resolveConnectUrl, g as specKeyFor, h as parseConnectCard, i as resolveAgent, k as getReviewStateDir, l as parseExternalOauthConnectParams, m as computeSettledLabel, p as composeQuestionAnswer, s as MASK_CHAR, u as parseOauthConnectParams, w as getConfigPath, x as DEFAULT_APP_URL } from "./print-Ds16-Q4O.mjs";
4
- import { A as installAgentAlias, C as themeVersion, D as machineOsFromPlatform, F as detectShell, L as getActiveWorkspaceId, M as takenAliasNames, O as aliasActivationHint, S as themeModeFromColorFgBg, T as themesForMode, V as setActiveWorkspace, _ as monoTheme, a as WORDMARK, b as themeForMode, d as profilingEnabled, f as record, g as findTheme, h as applyTheme, i as MARK_CELLS, j as slugifyAliasName, k as dedupeAliasName, m as DEFAULT_THEME_ID, n as buildCrashReport, p as writeArtifact, r as writeCrashReport, s as splashFitsWidth, t as installCrashHandler, v as noColorRequested, x as themeMode, y as theme, z as listWorkspaces } from "./install-DrtNldUN.mjs";
2
+ import { a as isRecord, i as errorMessage, n as errorDetail, r as sendErrorMessage, t as createRestClient } from "./rest-BtHVI0y6.mjs";
3
+ import { A as getSavedTheme, F as recordDefaultAgent, V as resolveWebUrl, W as saveTheme, b as DEFAULT_API_URL, c as cardActionErrorMessage, d as reconcileMaskedInput, f as resolveConnectUrl, g as specKeyFor, h as parseConnectCard, i as resolveAgent, k as getReviewStateDir, l as parseExternalOauthConnectParams, m as computeSettledLabel, p as composeQuestionAnswer, s as MASK_CHAR, u as parseOauthConnectParams, w as getConfigPath, x as DEFAULT_APP_URL } from "./print-Bg8rzq9t.mjs";
4
+ import { A as installAgentAlias, C as themeVersion, D as machineOsFromPlatform, F as detectShell, L as getActiveWorkspaceId, M as takenAliasNames, O as aliasActivationHint, S as themeModeFromColorFgBg, T as themesForMode, V as setActiveWorkspace, _ as monoTheme, a as WORDMARK, b as themeForMode, d as profilingEnabled, f as record, g as findTheme, h as applyTheme, i as MARK_CELLS, j as slugifyAliasName, k as dedupeAliasName, m as DEFAULT_THEME_ID, n as buildCrashReport, p as writeArtifact, r as writeCrashReport, s as splashFitsWidth, t as installCrashHandler, v as noColorRequested, x as themeMode, y as theme, z as listWorkspaces } from "./install-DNJ6k578.mjs";
5
5
  import { t as HttpError } from "./http-error-BF2NZZE3.mjs";
6
6
  import { i as billingBlockedOutcomeFromSendResponse } from "./billing-blocked-D3l5kJlX.mjs";
7
- import { t as PortalClient } from "./client-BnpgrtCC.mjs";
8
- import "./daemon-w8vtZ81t.mjs";
7
+ import { t as PortalClient } from "./client-C2lPer4b.mjs";
8
+ import "./daemon-MZN78SYL.mjs";
9
9
  import { t as defaultTlsCertSource } from "./tls-cert-CV-pwxVN.mjs";
10
10
  import "./api-TwLD7ibI.mjs";
11
11
  import { t as SandboxStream } from "./client-Btq6bMzX.mjs";
12
- import { t as PortalDaemonClient } from "./daemon-client-KKNss0NK.mjs";
12
+ import { t as PortalDaemonClient } from "./daemon-client-qNlw9inL.mjs";
13
13
  import { t as runRawPtyPassthrough } from "./raw-pty-D5PhKZSl.mjs";
14
14
  import * as os$1 from "node:os";
15
15
  import { homedir, platform, release, tmpdir } from "node:os";
@@ -4702,7 +4702,11 @@ const maxLines = 6;
4702
4702
  function ToolRead({ item }) {
4703
4703
  const glyph = toolGlyph(item.state);
4704
4704
  const input = isRecord(item.input) ? item.input : {};
4705
- const filePath = typeof input.filePath === "string" ? input.filePath : null;
4705
+ const filePath = pickString(input, [
4706
+ "path",
4707
+ "filePath",
4708
+ "file"
4709
+ ]);
4706
4710
  const offset = typeof input.offset === "number" ? input.offset : null;
4707
4711
  const limit = typeof input.limit === "number" ? input.limit : null;
4708
4712
  const header = filePath ? offset !== null ? `${filePath}:L${offset}${limit ? `-L${offset + limit - 1}` : ""}` : filePath : "(unknown path)";
@@ -568,6 +568,8 @@ var PortalClient = class {
568
568
  legacyGrantsChecked = false;
569
569
  wakeWatch = null;
570
570
  lastTick = Date.now();
571
+ handingOff = false;
572
+ handoffResolve = null;
571
573
  heartbeatConfig;
572
574
  constructor(opts) {
573
575
  this.opts = opts;
@@ -641,6 +643,48 @@ var PortalClient = class {
641
643
  this.jobs = null;
642
644
  this.ws?.close();
643
645
  this.ws = null;
646
+ this.finishHandoff();
647
+ }
648
+ /**
649
+ * Begin a make-before-break handoff to a successor daemon. Keep the current
650
+ * portal socket (and thus this machine's presence claim) alive so there is no
651
+ * `registered but not connected` gap, but stop redialing once it closes — the
652
+ * successor's `claim()` supersedes us and the server closes our socket, which
653
+ * is our cue to step down. Resolves when the socket has closed (clean
654
+ * handoff) OR after `timeoutMs` (the successor never came up, so we release
655
+ * the slot rather than hold it forever). Idempotent.
656
+ */
657
+ beginHandoff(timeoutMs) {
658
+ if (this.disposed) return Promise.resolve();
659
+ if (this.handingOff) return new Promise((resolve) => {
660
+ const prev = this.handoffResolve;
661
+ this.handoffResolve = () => {
662
+ prev?.();
663
+ resolve();
664
+ };
665
+ });
666
+ this.handingOff = true;
667
+ if (!this.ws) return Promise.resolve();
668
+ return new Promise((resolve) => {
669
+ let done = false;
670
+ const finish = () => {
671
+ if (done) return;
672
+ done = true;
673
+ clearTimeout(timer);
674
+ resolve();
675
+ };
676
+ this.handoffResolve = finish;
677
+ const timer = setTimeout(() => {
678
+ this.disable();
679
+ this.finishHandoff();
680
+ }, timeoutMs);
681
+ if (typeof timer.unref === "function") timer.unref();
682
+ });
683
+ }
684
+ finishHandoff() {
685
+ const resolve = this.handoffResolve;
686
+ this.handoffResolve = null;
687
+ resolve?.();
644
688
  }
645
689
  /**
646
690
  * Grant one agent access to this machine (default-deny; user-initiated).
@@ -702,18 +746,18 @@ var PortalClient = class {
702
746
  this.opts.onMachineName(identity.machineName, identity.machineNameSource);
703
747
  }
704
748
  let backoff = this.heartbeatConfig.initialBackoffMs;
705
- while (this.enabled && !this.disposed) {
749
+ while (this.enabled && !this.disposed && !this.handingOff) {
706
750
  this.setStatus("connecting");
707
751
  try {
708
752
  const token = await this.wsToken();
709
753
  if (await this.runConnection(token)) backoff = this.heartbeatConfig.initialBackoffMs;
710
754
  else backoff = Math.min(backoff * 2, this.heartbeatConfig.maxBackoffMs);
711
755
  } catch (err) {
712
- if (!this.enabled || this.disposed) break;
756
+ if (!this.enabled || this.disposed || this.handingOff) break;
713
757
  this.setStatus("error", errorMessage(err));
714
758
  backoff = Math.min(backoff * 2, this.heartbeatConfig.maxBackoffMs);
715
759
  }
716
- if (!this.enabled || this.disposed) break;
760
+ if (!this.enabled || this.disposed || this.handingOff) break;
717
761
  await sleep(Math.random() * backoff);
718
762
  }
719
763
  }
@@ -772,6 +816,7 @@ var PortalClient = class {
772
816
  jobs.killAll();
773
817
  if (this.jobs === jobs) this.jobs = null;
774
818
  if (this.ws === ws) this.ws = null;
819
+ if (this.handingOff) this.finishHandoff();
775
820
  settle();
776
821
  });
777
822
  });
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ import { t as PortalClient } from "./client-C2lPer4b.mjs";
3
+ import "./api-TwLD7ibI.mjs";
4
+
5
+ export { PortalClient };
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { n as isRecord, t as PortalClient } from "./client-BnpgrtCC.mjs";
2
+ import { n as isRecord, t as PortalClient } from "./client-C2lPer4b.mjs";
3
3
  import { t as defaultTlsCertSource } from "./tls-cert-CV-pwxVN.mjs";
4
4
  import os from "node:os";
5
5
  import path from "node:path";
@@ -21,7 +21,7 @@ import { access, appendFile, mkdir, readFile, unlink, writeFile } from "node:fs/
21
21
  * the commit time of the built tree is one monotonic clock they share.
22
22
  */
23
23
  function portalDaemonBuild() {
24
- return "1786941153";
24
+ return "1787048488";
25
25
  }
26
26
  /**
27
27
  * Whether a client carrying `mine` should replace a running daemon carrying
@@ -220,6 +220,20 @@ const clientStatusSchema = z.object({ t: z.literal("status") });
220
220
  * exits — cutting live exec/tunnel connections, not just refusing new ones.
221
221
  */
222
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") });
223
237
  const clientMessageSchema = z.discriminatedUnion("t", [
224
238
  clientHelloSchema,
225
239
  clientBindSchema,
@@ -230,7 +244,8 @@ const clientMessageSchema = z.discriminatedUnion("t", [
230
244
  clientDeclineSchema,
231
245
  clientByeSchema,
232
246
  clientStatusSchema,
233
- clientShutdownSchema
247
+ clientShutdownSchema,
248
+ clientShutdownHandoffSchema
234
249
  ]);
235
250
  /**
236
251
  * `state` — the shared portal status, pushed to every attached client so each
@@ -357,6 +372,14 @@ function parseDaemonMessage(line) {
357
372
  */
358
373
  /** Grace period after the last client detaches before the daemon exits. */
359
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;
360
383
  var PortalDaemon = class {
361
384
  appUrl;
362
385
  paths;
@@ -369,6 +392,7 @@ var PortalDaemon = class {
369
392
  persistedMachineName = null;
370
393
  fallbackCwd;
371
394
  idleTimer = null;
395
+ handingOff = false;
372
396
  sessionToken = null;
373
397
  deviceToken = null;
374
398
  constructor(appUrl) {
@@ -478,6 +502,9 @@ var PortalDaemon = class {
478
502
  case "shutdown":
479
503
  this.forceShutdown();
480
504
  return;
505
+ case "shutdown_handoff":
506
+ this.gracefulHandoffShutdown();
507
+ return;
481
508
  default: return msg;
482
509
  }
483
510
  }
@@ -615,6 +642,37 @@ var PortalDaemon = class {
615
642
  this.server?.close();
616
643
  process.exit(0);
617
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
+ }
618
676
  async loadState() {
619
677
  try {
620
678
  const raw = await readFile(this.paths.statePath, "utf8");
@@ -663,7 +721,7 @@ async function ensureDaemonRunning(appUrl) {
663
721
  if (await isDaemonListening(socketPath)) {
664
722
  const status = await queryDaemonStatus(appUrl);
665
723
  if (!status || !isNewerBuild(portalDaemonBuild(), status.build)) return;
666
- if (await stopDaemon(appUrl) === "failed") return;
724
+ if (await stopDaemonForHandoff(appUrl) === "failed") return;
667
725
  }
668
726
  const entry = process.argv[1];
669
727
  const args = entry ? [
@@ -758,6 +816,37 @@ async function queryDaemonStatus(appUrl) {
758
816
  });
759
817
  }
760
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
+ /**
761
850
  * Hard-stop a running daemon. Preferred path: send `shutdown` over the control
762
851
  * socket so it drains clients and exits cleanly. If the socket is unresponsive
763
852
  * (a wedged daemon), fall back to SIGTERM then SIGKILL by the pid the status
@@ -809,4 +898,4 @@ function sleep(ms) {
809
898
  }
810
899
 
811
900
  //#endregion
812
- export { runPortalDaemon as a, LOCAL_PROTOCOL_VERSION as c, encodeLine as d, makeLineParser as f, queryDaemonStatus as i, PORTAL_DAEMON_FLAG as l, ensureDaemonRunning as n, startPortalDaemon as o, parseDaemonMessage as p, isDaemonListening as r, stopDaemon as s, PortalDaemon as t, daemonPaths as u };
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 };
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ import "./client-C2lPer4b.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-MZN78SYL.mjs";
4
+ import "./tls-cert-CV-pwxVN.mjs";
5
+ import "./api-TwLD7ibI.mjs";
6
+
7
+ export { runPortalDaemon };
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ import "./client-C2lPer4b.mjs";
3
+ import "./daemon-MZN78SYL.mjs";
4
+ import "./tls-cert-CV-pwxVN.mjs";
5
+ import "./api-TwLD7ibI.mjs";
6
+ import { t as PortalDaemonClient } from "./daemon-client-qNlw9inL.mjs";
7
+
8
+ export { PortalDaemonClient };
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { c as LOCAL_PROTOCOL_VERSION, d as encodeLine, f as makeLineParser, n as ensureDaemonRunning, p as parseDaemonMessage, u as daemonPaths } from "./daemon-w8vtZ81t.mjs";
2
+ import { d as daemonPaths, f as encodeLine, l as LOCAL_PROTOCOL_VERSION, m as parseDaemonMessage, n as ensureDaemonRunning, p as makeLineParser } from "./daemon-MZN78SYL.mjs";
3
3
  import { randomUUID } from "node:crypto";
4
4
  import { connect } from "node:net";
5
5
 
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { s as version$1 } from "./rest-DpsJUYTL.mjs";
3
- import { w as getConfigPath } from "./print-Ds16-Q4O.mjs";
2
+ import { s as version$1 } from "./rest-BtHVI0y6.mjs";
3
+ import { w as getConfigPath } from "./print-Bg8rzq9t.mjs";
4
4
  import os from "node:os";
5
5
  import path from "node:path";
6
6
  import { err, ok } from "neverthrow";
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import "./rest-DpsJUYTL.mjs";
3
- import { a as runPrint, i as resolveAgent, n as messageGet, o as toPrintError, r as readStdin, t as collectRunText } from "./print-Ds16-Q4O.mjs";
2
+ import "./rest-BtHVI0y6.mjs";
3
+ import { a as runPrint, i as resolveAgent, n as messageGet, o as toPrintError, r as readStdin, t as collectRunText } from "./print-Bg8rzq9t.mjs";
4
4
  import "./billing-blocked-D3l5kJlX.mjs";
5
5
 
6
6
  export { messageGet, readStdin, resolveAgent, runPrint };
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { a as isRecord, i as errorMessage, r as sendErrorMessage, t as createRestClient } from "./rest-DpsJUYTL.mjs";
2
+ import { a as isRecord, i as errorMessage, r as sendErrorMessage, t as createRestClient } from "./rest-BtHVI0y6.mjs";
3
3
  import { t as HttpError } from "./http-error-BF2NZZE3.mjs";
4
4
  import { i as billingBlockedOutcomeFromSendResponse, n as BillingBlockedError } from "./billing-blocked-D3l5kJlX.mjs";
5
5
  import path from "node:path";
@@ -14,7 +14,7 @@ import { n as printError } from "./output-C9mb3sUB.mjs";
14
14
  * the reply (and to --json).
15
15
  */
16
16
  async function connectMachineShare({ appUrl, sessionToken, timeoutHint }) {
17
- const { PortalClient } = await import("./client-Cga4PJUc.mjs");
17
+ const { PortalClient } = await import("./client-CSKr4Swa.mjs");
18
18
  const { defaultTlsCertSource } = await import("./tls-cert-CLgSQALB.mjs");
19
19
  let signalConnected;
20
20
  const connected = new Promise((resolve) => {
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { n as errorDetail, r as sendErrorMessage, t as createRestClient } from "./rest-DpsJUYTL.mjs";
2
+ import { n as errorDetail, r as sendErrorMessage, t as createRestClient } from "./rest-BtHVI0y6.mjs";
3
3
  import { t as HttpError } from "./http-error-BF2NZZE3.mjs";
4
4
  import "./billing-blocked-D3l5kJlX.mjs";
5
5
 
@@ -6,7 +6,7 @@ import { createParser } from "eventsource-parser";
6
6
 
7
7
  //#region package.json
8
8
  var name = "skydive-cli";
9
- var version = "0.5.0-beta.44";
9
+ var version = "0.5.0-beta.46";
10
10
 
11
11
  //#endregion
12
12
  //#region src/chat/util.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skydive-cli",
3
- "version": "0.5.0-beta.44",
3
+ "version": "0.5.0-beta.46",
4
4
  "description": "Skydive CLI — cloud agents from the command line",
5
5
  "homepage": "https://skydive.com",
6
6
  "license": "MIT",
@@ -69,9 +69,9 @@
69
69
  },
70
70
  "//optionalDependencies": "The per-platform binary packages (skydive-cli-<os>-<arch>) are INTENTIONALLY not committed here. They are published by release-skydive-cli-binaries.yml and injected — pinned to the exact version being published — into the published launcher's optionalDependencies at release time (scripts/prepare-platform-packages.mjs --pin-version, run in release-skydive-cli.yml). Committing them would make `yarn install` try to resolve versions that only exist post-publish, breaking local dev and CI. npm installs of a published skydive-cli still get them (os/cpu-gated); the launcher (src/launcher.ts) resolves and execs the matching one. Local dev never needs them: it runs the JS bundle or `build:binary` directly.",
71
71
  "optionalDependencies": {
72
- "skydive-cli-darwin-arm64": "0.5.0-beta.44",
73
- "skydive-cli-darwin-x64": "0.5.0-beta.44",
74
- "skydive-cli-linux-x64": "0.5.0-beta.44",
75
- "skydive-cli-linux-arm64": "0.5.0-beta.44"
72
+ "skydive-cli-darwin-arm64": "0.5.0-beta.46",
73
+ "skydive-cli-darwin-x64": "0.5.0-beta.46",
74
+ "skydive-cli-linux-x64": "0.5.0-beta.46",
75
+ "skydive-cli-linux-arm64": "0.5.0-beta.46"
76
76
  }
77
77
  }
@@ -1,5 +0,0 @@
1
- #!/usr/bin/env node
2
- import { t as PortalClient } from "./client-BnpgrtCC.mjs";
3
- import "./api-TwLD7ibI.mjs";
4
-
5
- export { PortalClient };
@@ -1,8 +0,0 @@
1
- #!/usr/bin/env node
2
- import "./client-BnpgrtCC.mjs";
3
- import "./daemon-w8vtZ81t.mjs";
4
- import "./tls-cert-CV-pwxVN.mjs";
5
- import "./api-TwLD7ibI.mjs";
6
- import { t as PortalDaemonClient } from "./daemon-client-KKNss0NK.mjs";
7
-
8
- export { PortalDaemonClient };
@@ -1,7 +0,0 @@
1
- #!/usr/bin/env node
2
- import "./client-BnpgrtCC.mjs";
3
- import { a as runPortalDaemon, i as queryDaemonStatus, n as ensureDaemonRunning, o as startPortalDaemon, r as isDaemonListening, s as stopDaemon, t as PortalDaemon } from "./daemon-w8vtZ81t.mjs";
4
- import "./tls-cert-CV-pwxVN.mjs";
5
- import "./api-TwLD7ibI.mjs";
6
-
7
- export { runPortalDaemon };