skydive-cli 0.4.1-beta.6 → 0.5.0-beta.2

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 { C as ensureActiveOrganization, D as setActiveWorkspace, E as listWorkspaces, O as name, T as getSessionIdentity, a as installCrashHandler, k as version, t as maybeStartProfiling, u as brandHelpArt, w as getActiveWorkspaceId, x as themes } from "./profiler-94qrV53c.mjs";
2
+ import { C as ensureActiveOrganization, D as setActiveWorkspace, E as listWorkspaces, O as name, T as getSessionIdentity, a as installCrashHandler, k as version, t as maybeStartProfiling, u as brandHelpArt, w as getActiveWorkspaceId, x as themes } from "./profiler-y43HVmq7.mjs";
3
3
  import { A as getStoredApiKeyWorkspaceName, C as getLastSeenVersion, F as resolveManagementAuth, I as resolveSession, M as resolveAppUrl, N as resolveChatAuth, O as getShareMachineDefault, P as resolveConfig, R as saveConfig, S as getConfigPath, T as getPromptHistoryPath, V as setLastSeenVersion, _ as API_KEY_PREFIX, b as PREFERENCES, g as API_KEY_FAMILY_PREFIX, h as API_KEYS_URL, i as resolveAgent, j as getUpdateCheckDisabled, k as getStoredApiKeyId, v as DEFAULT_API_URL, w as getPreference, x as deleteConfig, z as saveSession } from "./print-CbayCa87.mjs";
4
4
  import { n as printError, r as printTable, t as output } from "./output-DYzzdXYV.mjs";
5
5
  import { t as HttpError } from "./http-error-DzyrsLAZ.mjs";
6
6
  import { t as createRestClient } from "./rest-BY2nADw5.mjs";
7
7
  import "./billing-blocked-2wju4gC_.mjs";
8
- import { a as registerPortalDevice, c as resolveMachineIdentity, i as grantPortalAccess, n as fetchPortalDevices, o as revokePortalAccess, r as findThisDevice } from "./client-mykp1DVb.mjs";
9
- import { i as queryDaemonStatus, l as PORTAL_DAEMON_FLAG, n as ensureDaemonRunning, s as stopDaemon, u as daemonPaths } from "./daemon-CfbpCjAw.mjs";
8
+ import { a as registerPortalDevice, c as resolveMachineIdentity, i as grantPortalAccess, n as fetchPortalDevices, o as revokePortalAccess, r as findThisDevice } from "./client-Dd5sMXPv.mjs";
9
+ import { i as queryDaemonStatus, l as PORTAL_DAEMON_FLAG, n as ensureDaemonRunning, s as stopDaemon, u as daemonPaths } from "./daemon-Bq93vOIk.mjs";
10
10
  import { t as SandboxStream } from "./client-Cn2af31H.mjs";
11
11
  import { hideBin } from "yargs/helpers";
12
12
  import yargs from "yargs";
@@ -1516,7 +1516,7 @@ const importCommand = {
1516
1516
  process.exit(1);
1517
1517
  }
1518
1518
  }
1519
- const { runChat } = await import("./boot-B48inW5D.mjs");
1519
+ const { runChat } = await import("./boot-DFYFjlht.mjs");
1520
1520
  await runChat({
1521
1521
  appUrl,
1522
1522
  sessionToken: session.value.sessionToken,
@@ -1543,7 +1543,7 @@ async function runImportPrintMode({ argv, appUrl }) {
1543
1543
  printError(`${session.error.message} For non-interactive use, run \`skydive auth login --web\` first, or set SKYDIVE_SESSION_TOKEN.`);
1544
1544
  process.exit(1);
1545
1545
  }
1546
- const { connectMachineShare } = await import("./print-share-ZV88PqDu.mjs");
1546
+ const { connectMachineShare } = await import("./print-share-C1LPsgTP.mjs");
1547
1547
  const machineShare = await connectMachineShare({
1548
1548
  appUrl,
1549
1549
  sessionToken: session.value.sessionToken,
@@ -1928,7 +1928,7 @@ const chatCommand = {
1928
1928
  sessionToken: auth.value.token,
1929
1929
  agentSelector: argv.agent ?? null
1930
1930
  });
1931
- const { runChat } = await import("./boot-B48inW5D.mjs");
1931
+ const { runChat } = await import("./boot-DFYFjlht.mjs");
1932
1932
  await runChat({
1933
1933
  appUrl: auth.value.appUrl,
1934
1934
  sessionToken: auth.value.token,
@@ -2020,7 +2020,7 @@ async function runPrintMode({ argv, appUrl }) {
2020
2020
  process.exit(1);
2021
2021
  }
2022
2022
  } else {
2023
- const { connectMachineShare } = await import("./print-share-ZV88PqDu.mjs");
2023
+ const { connectMachineShare } = await import("./print-share-C1LPsgTP.mjs");
2024
2024
  machineShare = await connectMachineShare({
2025
2025
  appUrl: auth.value.appUrl,
2026
2026
  sessionToken: auth.value.token,
@@ -2287,7 +2287,7 @@ const switchCommand = {
2287
2287
  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.");
2288
2288
  process.exit(1);
2289
2289
  }
2290
- const { runWorkspacePicker } = await import("./boot-B48inW5D.mjs");
2290
+ const { runWorkspacePicker } = await import("./boot-DFYFjlht.mjs");
2291
2291
  await runWorkspacePicker(session);
2292
2292
  return;
2293
2293
  }
@@ -2366,7 +2366,7 @@ const openCommand = {
2366
2366
  const agent = argv.agent ? resolveAgent((await fetchPortalDevices(session)).agents, argv.agent) : null;
2367
2367
  const cwd = argv.cwd ? path.resolve(argv.cwd) : process.cwd();
2368
2368
  const { machineName } = await resolveMachineIdentity();
2369
- const { PortalDaemonClient } = await import("./daemon-client-BHehCLrO.mjs");
2369
+ const { PortalDaemonClient } = await import("./daemon-client-srAit8Fz.mjs");
2370
2370
  let lastLine = "";
2371
2371
  let signalConnected;
2372
2372
  const connected = new Promise((resolve) => {
@@ -4169,7 +4169,7 @@ if (process.argv.includes(UPDATE_WORKER_FLAG)) {
4169
4169
  process.exit(0);
4170
4170
  }
4171
4171
  if (process.argv.includes(PORTAL_DAEMON_FLAG)) {
4172
- const { runPortalDaemon } = await import("./daemon-D4ZX3wNG.mjs");
4172
+ const { runPortalDaemon } = await import("./daemon-DDBIumf1.mjs");
4173
4173
  runPortalDaemon(process.argv);
4174
4174
  } else runCli();
4175
4175
  function runCli() {
@@ -1,13 +1,13 @@
1
1
  #!/usr/bin/env node
2
- import { D as setActiveWorkspace, E as listWorkspaces, S as themesForMode, _ as themeForMode, a as installCrashHandler, b as themeVersion, c as MARK_CELLS, d as DEFAULT_THEME_ID, f as applyTheme, g as theme, h as noColorRequested, i as writeArtifact, l as WORDMARK, m as monoTheme, n as profilingEnabled, o as buildCrashReport, p as findTheme, r as record, s as writeCrashReport, v as themeMode, w as getActiveWorkspaceId, y as themeModeFromColorFgBg } from "./profiler-94qrV53c.mjs";
2
+ import { D as setActiveWorkspace, E as listWorkspaces, S as themesForMode, _ as themeForMode, a as installCrashHandler, b as themeVersion, c as MARK_CELLS, d as DEFAULT_THEME_ID, f as applyTheme, g as theme, h as noColorRequested, i as writeArtifact, l as WORDMARK, m as monoTheme, n as profilingEnabled, o as buildCrashReport, p as findTheme, r as record, s as writeCrashReport, v as themeMode, w as getActiveWorkspaceId, y as themeModeFromColorFgBg } from "./profiler-y43HVmq7.mjs";
3
3
  import { B as saveTheme, D as getSavedTheme, E as getReviewStateDir, L as resolveWebUrl, S as getConfigPath, c as cardActionErrorMessage, d as reconcileMaskedInput, f as resolveConnectUrl, i as resolveAgent, l as parseExternalOauthConnectParams, m as specKeyFor, p as parseConnectCard, s as MASK_CHAR, u as parseOauthConnectParams, v as DEFAULT_API_URL, y as DEFAULT_APP_URL } from "./print-CbayCa87.mjs";
4
4
  import { t as HttpError } from "./http-error-DzyrsLAZ.mjs";
5
5
  import { a as isRecord, i as errorMessage, n as errorDetail, r as sendErrorMessage, t as createRestClient } from "./rest-BY2nADw5.mjs";
6
6
  import { i as billingBlockedOutcomeFromSendResponse } from "./billing-blocked-2wju4gC_.mjs";
7
- import { t as PortalClient } from "./client-mykp1DVb.mjs";
8
- import "./daemon-CfbpCjAw.mjs";
7
+ import { t as PortalClient } from "./client-Dd5sMXPv.mjs";
8
+ import "./daemon-Bq93vOIk.mjs";
9
9
  import { t as SandboxStream } from "./client-Cn2af31H.mjs";
10
- import { t as PortalDaemonClient } from "./daemon-client-6xtta_n2.mjs";
10
+ import { t as PortalDaemonClient } from "./daemon-client-Bewt98dE.mjs";
11
11
  import { t as runRawPtyPassthrough } from "./raw-pty-B6mAroiI.mjs";
12
12
  import * as os$1 from "node:os";
13
13
  import { homedir, platform, release, tmpdir } from "node:os";
@@ -109,6 +109,7 @@ const initialPortalUi = {
109
109
  machineName: null,
110
110
  error: null,
111
111
  grantedAgentIds: [],
112
+ declinedAgentIds: [],
112
113
  prompt: null
113
114
  };
114
115
  function initialChatTitle(screen) {
@@ -181,12 +182,13 @@ const useStore = create((set, get) => ({
181
182
  chatTitle: null
182
183
  }),
183
184
  setPortalClient: (portalClient) => set({ portalClient }),
184
- syncPortal: ({ status, machineName, error, grantedAgentIds }) => set((state) => ({ portal: {
185
+ syncPortal: ({ status, machineName, error, grantedAgentIds, declinedAgentIds }) => set((state) => ({ portal: {
185
186
  ...state.portal,
186
187
  status,
187
188
  machineName,
188
189
  error,
189
- grantedAgentIds
190
+ grantedAgentIds,
191
+ declinedAgentIds
190
192
  } })),
191
193
  setPortalPrompt: (prompt) => set((state) => ({ portal: {
192
194
  ...state.portal,
@@ -317,14 +319,18 @@ function createPortalClient(opts) {
317
319
  if (portalDaemonDisabled()) {
318
320
  const client = new PortalClient({
319
321
  appUrl: opts.appUrl,
320
- sessionToken: opts.sessionToken,
322
+ credentials: () => ({
323
+ sessionToken: opts.sessionToken,
324
+ deviceToken: null
325
+ }),
321
326
  resolveCwd: () => process.cwd(),
322
327
  onState: (state) => opts.onState({
323
328
  status: state.status,
324
329
  machineName: state.machineName,
325
330
  friendlyName: state.friendlyName,
326
331
  error: state.error,
327
- grantedAgentIds: state.grantedAgentIds
332
+ grantedAgentIds: state.grantedAgentIds,
333
+ declinedAgentIds: []
328
334
  })
329
335
  });
330
336
  return {
@@ -332,6 +338,9 @@ function createPortalClient(opts) {
332
338
  enable: () => client.enable(),
333
339
  disable: () => client.disable(),
334
340
  grantAgent: (agentId) => client.grantAgent(agentId),
341
+ decline: (_agentId, declined) => {
342
+ if (declined && client.grantedAgentIds().length === 0) client.disable();
343
+ },
335
344
  isGranted: (agentId) => client.isGranted(agentId),
336
345
  bind: () => {},
337
346
  dispose: () => client.dispose()
@@ -370,7 +379,8 @@ function usePortal({ appUrl, sessionToken, shareMachine, enabled = true }) {
370
379
  status: state.status,
371
380
  machineName: state.machineName,
372
381
  error: state.error,
373
- grantedAgentIds: state.grantedAgentIds
382
+ grantedAgentIds: state.grantedAgentIds,
383
+ declinedAgentIds: state.declinedAgentIds
374
384
  })
375
385
  });
376
386
  setPortalClient(client);
@@ -11255,6 +11265,7 @@ function ChatScreen({ agent, conversation }) {
11255
11265
  }
11256
11266
  if (portal.status === "off") {
11257
11267
  declinedRef.current.delete(agent.id);
11268
+ portalClient.decline(agent.id, false);
11258
11269
  portalClient.enable();
11259
11270
  if (conversationId) portalClient.bind(conversationId, shellSessionRef.current?.cwd ?? process.cwd());
11260
11271
  } else {
@@ -11288,9 +11299,14 @@ function ChatScreen({ agent, conversation }) {
11288
11299
  ]);
11289
11300
  const declineGrant = useCallback(() => {
11290
11301
  declinedRef.current.add(agent.id);
11302
+ portalClient?.decline(agent.id, true);
11291
11303
  setPortalPrompt(null);
11292
11304
  useStore.getState().showToast({ message: "kept this machine private" });
11293
- }, [agent.id, setPortalPrompt]);
11305
+ }, [
11306
+ portalClient,
11307
+ agent.id,
11308
+ setPortalPrompt
11309
+ ]);
11294
11310
  const openPortalAccess = useCallback(() => {
11295
11311
  if (!portalClient) {
11296
11312
  useStore.getState().showToast({ message: useStore.getState().authKind === "api-key" ? "local access needs a browser session; run `skydive auth login --web`" : "local access is unavailable in this session" });
@@ -11299,6 +11315,7 @@ function ChatScreen({ agent, conversation }) {
11299
11315
  switch (portal.status) {
11300
11316
  case "off":
11301
11317
  declinedRef.current.delete(agent.id);
11318
+ portalClient.decline(agent.id, false);
11302
11319
  portalClient.enable();
11303
11320
  useStore.getState().showToast({ message: "enabling local access on this machine…" });
11304
11321
  break;
@@ -11311,6 +11328,7 @@ function ChatScreen({ agent, conversation }) {
11311
11328
  break;
11312
11329
  }
11313
11330
  declinedRef.current.delete(agent.id);
11331
+ portalClient.decline(agent.id, false);
11314
11332
  setPortalPrompt({
11315
11333
  agentId: agent.id,
11316
11334
  agentName: agent.name
@@ -11515,6 +11533,7 @@ function ChatScreen({ agent, conversation }) {
11515
11533
  useEffect(() => {
11516
11534
  if (!portalClient || portal.status !== "connected") return;
11517
11535
  if (portal.grantedAgentIds.includes(agent.id)) return;
11536
+ if (portal.declinedAgentIds.includes(agent.id)) return;
11518
11537
  if (declinedRef.current.has(agent.id)) return;
11519
11538
  if (portal.prompt?.agentId === agent.id) return;
11520
11539
  setPortalPrompt({
@@ -11532,6 +11551,7 @@ function ChatScreen({ agent, conversation }) {
11532
11551
  portalClient,
11533
11552
  portal.status,
11534
11553
  portal.grantedAgentIds,
11554
+ portal.declinedAgentIds,
11535
11555
  portal.prompt,
11536
11556
  agent.id,
11537
11557
  agent.name,
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ import { t as PortalClient } from "./client-Dd5sMXPv.mjs";
3
+
4
+ export { PortalClient };
@@ -58,12 +58,18 @@ const INHERITED_ENV = [
58
58
  "TERM",
59
59
  "PATH"
60
60
  ];
61
+ const DARWIN_PATH_PREPEND = ["/opt/homebrew/bin", "/usr/local/bin"];
61
62
  function buildEnv(extra) {
62
63
  const env = {};
63
64
  for (const key of INHERITED_ENV) {
64
65
  const value = process.env[key];
65
66
  if (value !== void 0) env[key] = value;
66
67
  }
68
+ if (process.platform === "darwin") {
69
+ const current = env.PATH ? env.PATH.split(":") : [];
70
+ const missing = DARWIN_PATH_PREPEND.filter((dir) => !current.includes(dir));
71
+ if (missing.length > 0) env.PATH = [...missing, ...current].join(":");
72
+ }
67
73
  if (extra) for (const [key, value] of Object.entries(extra)) env[key] = value;
68
74
  return env;
69
75
  }
@@ -430,6 +436,10 @@ var PortalClient = class {
430
436
  isGranted(agentId) {
431
437
  return this.granted.has(agentId);
432
438
  }
439
+ /** Agents currently authorized on this machine (last known server state). */
440
+ grantedAgentIds() {
441
+ return [...this.granted];
442
+ }
433
443
  enable() {
434
444
  if (this.enabled || this.disposed) return;
435
445
  this.enabled = true;
@@ -461,14 +471,36 @@ var PortalClient = class {
461
471
  }
462
472
  /** Grant one agent access to this machine (default-deny; user-initiated). */
463
473
  async grantAgent(agentId) {
464
- const deviceId = await this.ensureDeviceId();
465
- await grantPortalAccess(this.opts, {
466
- deviceId,
474
+ const auth = this.sessionAuth();
475
+ if (!auth) throw new Error("granting needs a signed-in CLI session on this machine (the desktop connection alone cannot manage grants)");
476
+ await grantPortalAccess(auth, {
477
+ deviceId: await this.ensureDeviceId(),
467
478
  agentId
468
479
  });
469
480
  this.granted.add(agentId);
470
481
  this.emit();
471
482
  }
483
+ /** Session-authed REST auth, or null when only a device token is held. */
484
+ sessionAuth() {
485
+ const { sessionToken } = this.opts.credentials();
486
+ return sessionToken ? {
487
+ appUrl: this.opts.appUrl,
488
+ sessionToken
489
+ } : null;
490
+ }
491
+ /**
492
+ * The bearer token for the portal WebSocket dial: minted from the session
493
+ * when one is held, otherwise the pre-minted device token as-is.
494
+ */
495
+ async wsToken() {
496
+ const creds = this.opts.credentials();
497
+ if (creds.sessionToken) return mintPortalDeviceToken({
498
+ appUrl: this.opts.appUrl,
499
+ sessionToken: creds.sessionToken
500
+ });
501
+ if (creds.deviceToken) return creds.deviceToken;
502
+ throw new Error("no portal credentials (not signed in)");
503
+ }
472
504
  setStatus(status, error = null) {
473
505
  this.status = status;
474
506
  this.error = error;
@@ -493,7 +525,7 @@ var PortalClient = class {
493
525
  while (this.enabled && !this.disposed) {
494
526
  this.setStatus("connecting");
495
527
  try {
496
- const token = await mintPortalDeviceToken(this.opts);
528
+ const token = await this.wsToken();
497
529
  await this.runConnection(token);
498
530
  backoff = INITIAL_BACKOFF_MS;
499
531
  } catch (err) {
@@ -547,8 +579,10 @@ var PortalClient = class {
547
579
  throw new Error("this machine is not connected yet");
548
580
  }
549
581
  async refreshDevice() {
582
+ const auth = this.sessionAuth();
583
+ if (!auth) return;
550
584
  try {
551
- const { devices } = await fetchPortalDevices(this.opts);
585
+ const { devices } = await fetchPortalDevices(auth);
552
586
  const mine = findThisDevice(devices, this.machineName);
553
587
  if (!mine) return;
554
588
  this.deviceId = mine.id;
@@ -565,9 +599,11 @@ var PortalClient = class {
565
599
  async syncDeviceState() {
566
600
  await this.refreshDevice();
567
601
  if (this.legacyGrantsChecked) return;
602
+ const auth = this.sessionAuth();
603
+ if (!auth) return;
568
604
  this.legacyGrantsChecked = true;
569
605
  try {
570
- if (await unifyLegacyCliGrants(this.opts, this.machineName) > 0) await this.refreshDevice();
606
+ if (await unifyLegacyCliGrants(auth, this.machineName) > 0) await this.refreshDevice();
571
607
  } catch (_error) {}
572
608
  }
573
609
  };
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { s as isRecord, t as PortalClient } from "./client-mykp1DVb.mjs";
2
+ import { s as isRecord, t as PortalClient } from "./client-Dd5sMXPv.mjs";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
5
  import { z } from "zod";
@@ -47,14 +47,35 @@ const LOCAL_PROTOCOL_VERSION = 1;
47
47
  */
48
48
  const PORTAL_DAEMON_FLAG = "--skydive-internal-portal-daemon";
49
49
  /**
50
+ * One daemon per machine requires one socket key per backend — but the prod
51
+ * backend is reachable under three hosts: the CLI's default appUrl is
52
+ * `https://api.skydive.com` while the desktop app carries
53
+ * `https://skydive.com` (and the web is on www). Hashing those raw strings
54
+ * would give each surface its own daemon, and with the merged machine
55
+ * identity both daemons would register the SAME device — the presence
56
+ * contention the daemon exists to prevent. Collapse the known prod aliases to
57
+ * the CLI's canonical form (proven for both the REST surface and the WS
58
+ * upgrade; the apex 301-redirects, which a WS upgrade cannot follow).
59
+ * Non-prod URLs (previews, localhost) pass through untouched apart from
60
+ * trailing-slash trimming.
61
+ */
62
+ function canonicalPortalAppUrl(appUrl) {
63
+ const trimmed = appUrl.replace(/\/+$/, "");
64
+ const host = trimmed.replace(/^https?:\/\//, "").toLowerCase();
65
+ if (host === "skydive.com" || host === "www.skydive.com" || host === "api.skydive.com") return "https://api.skydive.com";
66
+ return trimmed;
67
+ }
68
+ /**
50
69
  * Per-user, per-app daemon socket + state paths. Scoped by OS user and a hash
51
- * of the appUrl so two logins or two deployments (prod vs a PR preview) get
52
- * distinct daemons and never share a portal identity. Kept under the OS temp
53
- * dir because Unix socket paths are length-limited (~104 bytes on macOS).
70
+ * of the CANONICAL appUrl so two logins or two deployments (prod vs a PR
71
+ * preview) get distinct daemons and never share a portal identity while
72
+ * every alias of one deployment (see {@link canonicalPortalAppUrl}) shares
73
+ * one. Kept under the OS temp dir because Unix socket paths are
74
+ * length-limited (~104 bytes on macOS).
54
75
  */
55
76
  function daemonPaths(appUrl) {
56
77
  const user = safeSegment(os.userInfo().username || "user");
57
- const app = createHash("sha256").update(appUrl).digest("hex").slice(0, 12);
78
+ const app = createHash("sha256").update(canonicalPortalAppUrl(appUrl)).digest("hex").slice(0, 12);
58
79
  const dir = path.join(os.tmpdir(), `skydive-portal-${user}-${app}`);
59
80
  return {
60
81
  dir,
@@ -67,17 +88,30 @@ function safeSegment(value) {
67
88
  return value.replace(/[^a-zA-Z0-9_-]/g, "").slice(0, 24) || "user";
68
89
  }
69
90
  /**
70
- * `hello` — a CLI attaches. `sessionId` is unique per CLI process; `token` is
71
- * the CLI's session token, which the daemon needs to mint the portal device
72
- * token (the daemon is spawned lazily and holds no session of its own). The
73
- * daemon uses the FIRST hello's token to connect and refreshes from later
74
- * hellos so a long-lived daemon can outlive the CLI that spawned it.
91
+ * `hello` — a client attaches. `sessionId` is unique per client process.
92
+ * `token` authenticates the daemon's portal connection; `tokenKind` says what
93
+ * it is:
94
+ *
95
+ * - `session` a CLI session token. The daemon mints short-lived portal
96
+ * device tokens from it, and can call the session-authed portal REST
97
+ * surface (device refresh, grants).
98
+ * - `device` — a pre-minted portal device token (the desktop app is handed
99
+ * one by the web bridge and holds no session of its own). Dials the portal
100
+ * WebSocket directly; the session-only REST surface is unavailable until
101
+ * some client supplies a session token.
102
+ *
103
+ * The daemon uses the FIRST hello's token to connect and refreshes from later
104
+ * hellos so a long-lived daemon can outlive the client that spawned it.
105
+ * `tokenKind` defaults to `session` on parse: hellos from CLI builds that
106
+ * predate the field are session-token clients by definition, and the default
107
+ * keeps them attaching to a newer daemon.
75
108
  */
76
109
  const clientHelloSchema = z.object({
77
110
  t: z.literal("hello"),
78
111
  v: z.number(),
79
112
  sessionId: z.string().min(1),
80
- token: z.string().min(1)
113
+ token: z.string().min(1),
114
+ tokenKind: z.enum(["session", "device"]).default("session")
81
115
  });
82
116
  /**
83
117
  * `bind` — associate a conversation with a working directory. Sent on chat
@@ -109,6 +143,20 @@ const clientGrantSchema = z.object({
109
143
  t: z.literal("grant"),
110
144
  agentId: z.string().min(1)
111
145
  });
146
+ /**
147
+ * `decline` — the user answered no to an agent's request for this machine
148
+ * (`declined: true`), or re-opened the door for it (`declined: false`).
149
+ *
150
+ * The daemon holds this because a decline is about the MACHINE, not one
151
+ * terminal: every attached CLI raises its own grant prompt off the daemon's
152
+ * shared state, so a per-process no would be re-asked by every other open
153
+ * conversation and by the next `skydive` launch.
154
+ */
155
+ const clientDeclineSchema = z.object({
156
+ t: z.literal("decline"),
157
+ agentId: z.string().min(1),
158
+ declined: z.boolean()
159
+ });
112
160
  /** `bye` — a CLI detaches cleanly (its conversations drop from the map). */
113
161
  const clientByeSchema = z.object({
114
162
  t: z.literal("bye"),
@@ -133,6 +181,7 @@ const clientMessageSchema = z.discriminatedUnion("t", [
133
181
  clientDisableSchema,
134
182
  clientCwdSchema,
135
183
  clientGrantSchema,
184
+ clientDeclineSchema,
136
185
  clientByeSchema,
137
186
  clientStatusSchema,
138
187
  clientShutdownSchema
@@ -152,7 +201,8 @@ const daemonStateSchema = z.object({
152
201
  machineName: z.string(),
153
202
  friendlyName: z.string(),
154
203
  error: z.string().nullable(),
155
- grantedAgentIds: z.array(z.string())
204
+ grantedAgentIds: z.array(z.string()),
205
+ declinedAgentIds: z.array(z.string()).default([])
156
206
  });
157
207
  /** `hello_ok` — the daemon accepts an attach and reports its protocol version. */
158
208
  const daemonHelloOkSchema = z.object({
@@ -180,7 +230,8 @@ const daemonStatusResultSchema = z.object({
180
230
  machineName: z.string(),
181
231
  friendlyName: z.string(),
182
232
  error: z.string().nullable(),
183
- grantedAgentIds: z.array(z.string())
233
+ grantedAgentIds: z.array(z.string()),
234
+ declinedAgentIds: z.array(z.string()).default([])
184
235
  }),
185
236
  clientCount: z.number(),
186
237
  cwds: z.record(z.string(), z.string())
@@ -190,6 +241,12 @@ const daemonMessageSchema = z.discriminatedUnion("t", [
190
241
  daemonHelloOkSchema,
191
242
  daemonStatusResultSchema
192
243
  ]);
244
+ /**
245
+ * Encoding is the pre-parse direction, so it takes the schemas' INPUT types:
246
+ * fields with a `.default()` (e.g. `hello.tokenKind`) are required after a
247
+ * parse but optional on the wire, and a sender omitting one — an older CLI
248
+ * build that predates the field — must stay expressible.
249
+ */
193
250
  function encodeLine(msg) {
194
251
  return `${JSON.stringify(msg)}\n`;
195
252
  }
@@ -254,18 +311,21 @@ function parseDaemonMessage(line) {
254
311
  /** Grace period after the last client detaches before the daemon exits. */
255
312
  const IDLE_SHUTDOWN_MS = 3e4;
256
313
  var PortalDaemon = class {
314
+ appUrl;
257
315
  paths;
258
316
  server = null;
259
317
  client = null;
260
318
  lastState = null;
261
319
  conns = /* @__PURE__ */ new Set();
262
320
  cwds = /* @__PURE__ */ new Map();
321
+ declined = /* @__PURE__ */ new Set();
263
322
  fallbackCwd;
264
323
  idleTimer = null;
265
324
  sessionToken = null;
325
+ deviceToken = null;
266
326
  constructor(appUrl) {
267
- this.appUrl = appUrl;
268
- this.paths = daemonPaths(appUrl);
327
+ this.appUrl = canonicalPortalAppUrl(appUrl);
328
+ this.paths = daemonPaths(this.appUrl);
269
329
  this.fallbackCwd = process.env.HOME ?? process.cwd();
270
330
  }
271
331
  /** Start listening. Rejects if the socket is already held by another daemon. */
@@ -319,13 +379,14 @@ var PortalDaemon = class {
319
379
  switch (msg.t) {
320
380
  case "hello":
321
381
  conn.sessionId = msg.sessionId;
322
- this.sessionToken = msg.token;
382
+ if (msg.tokenKind === "device") this.deviceToken = msg.token;
383
+ else this.sessionToken = msg.token;
323
384
  this.ensureClient();
324
385
  this.send(conn, {
325
386
  t: "hello_ok",
326
387
  v: LOCAL_PROTOCOL_VERSION
327
388
  });
328
- if (this.lastState) this.send(conn, stateMessage(this.lastState));
389
+ if (this.lastState) this.send(conn, stateMessage(this.lastState, this.declined));
329
390
  return;
330
391
  case "bind":
331
392
  conn.conversations.add(msg.conversationId);
@@ -342,10 +403,19 @@ var PortalDaemon = class {
342
403
  for (const other of this.conns) other.wantsShare = false;
343
404
  this.client?.disable();
344
405
  return;
406
+ case "decline":
407
+ if (msg.declined) {
408
+ this.declined.add(msg.agentId);
409
+ if ((this.lastState?.grantedAgentIds.length ?? 0) === 0) this.client?.disable();
410
+ } else this.declined.delete(msg.agentId);
411
+ this.persistState();
412
+ this.broadcastState();
413
+ return;
345
414
  case "cwd":
346
415
  this.fallbackCwd = msg.cwd;
347
416
  return;
348
417
  case "grant":
418
+ this.declined.delete(msg.agentId);
349
419
  this.ensureClient();
350
420
  this.client?.grantAgent(msg.agentId).catch((error) => {
351
421
  this.logError("grantAgent failed", error);
@@ -375,14 +445,17 @@ var PortalDaemon = class {
375
445
  }
376
446
  /** Create the single PortalClient the first time a client needs the portal. */
377
447
  ensureClient() {
378
- if (this.client || !this.sessionToken) return;
448
+ if (this.client || !this.sessionToken && !this.deviceToken) return;
379
449
  this.client = new PortalClient({
380
450
  appUrl: this.appUrl,
381
- sessionToken: this.sessionToken,
451
+ credentials: () => ({
452
+ sessionToken: this.sessionToken,
453
+ deviceToken: this.deviceToken
454
+ }),
382
455
  resolveCwd: (conversationId) => this.resolveCwd(conversationId),
383
456
  onState: (state) => {
384
457
  this.lastState = state;
385
- this.broadcast(stateMessage(state));
458
+ this.broadcastState();
386
459
  }
387
460
  });
388
461
  }
@@ -400,6 +473,11 @@ var PortalDaemon = class {
400
473
  broadcast(msg) {
401
474
  for (const conn of this.conns) this.send(conn, msg);
402
475
  }
476
+ /** Push the current portal state (plus the machine's declines) to every CLI. */
477
+ broadcastState() {
478
+ if (!this.lastState) return;
479
+ this.broadcast(stateMessage(this.lastState, this.declined));
480
+ }
403
481
  armIdleTimer() {
404
482
  this.clearIdleTimer();
405
483
  this.idleTimer = setTimeout(() => this.shutdown(), IDLE_SHUTDOWN_MS);
@@ -417,6 +495,13 @@ var PortalDaemon = class {
417
495
  const line = `${(/* @__PURE__ */ new Date()).toISOString()} ${context}: ${message}\n`;
418
496
  appendFile(this.paths.logPath, line).catch((_error) => {});
419
497
  }
498
+ /** Close the listener without exiting the process (tests own the process). */
499
+ stopForTest() {
500
+ this.clearIdleTimer();
501
+ this.client?.dispose();
502
+ this.server?.close();
503
+ this.server = null;
504
+ }
420
505
  shutdown() {
421
506
  if (this.conns.size > 0) return;
422
507
  this.client?.dispose();
@@ -436,7 +521,8 @@ var PortalDaemon = class {
436
521
  machineName: portal?.machineName ?? "",
437
522
  friendlyName: portal?.friendlyName ?? "",
438
523
  error: portal?.error ?? null,
439
- grantedAgentIds: portal?.grantedAgentIds ?? []
524
+ grantedAgentIds: portal?.grantedAgentIds ?? [],
525
+ declinedAgentIds: [...this.declined]
440
526
  },
441
527
  clientCount: [...this.conns].filter((c) => c.sessionId !== null).length,
442
528
  cwds: Object.fromEntries(this.cwds)
@@ -462,13 +548,17 @@ var PortalDaemon = class {
462
548
  const parsed = JSON.parse(raw);
463
549
  if (isRecord(parsed) && parsed.version === LOCAL_PROTOCOL_VERSION && isRecord(parsed.cwds)) {
464
550
  for (const [id, cwd] of Object.entries(parsed.cwds)) if (typeof cwd === "string") this.cwds.set(id, cwd);
551
+ if (Array.isArray(parsed.declined)) {
552
+ for (const id of parsed.declined) if (typeof id === "string") this.declined.add(id);
553
+ }
465
554
  }
466
555
  } catch (_error) {}
467
556
  }
468
557
  persistState() {
469
558
  const state = {
470
559
  version: LOCAL_PROTOCOL_VERSION,
471
- cwds: Object.fromEntries(this.cwds)
560
+ cwds: Object.fromEntries(this.cwds),
561
+ declined: [...this.declined]
472
562
  };
473
563
  writeFile(this.paths.statePath, JSON.stringify(state)).catch((error) => {
474
564
  this.logError("persistState failed", error);
@@ -615,14 +705,15 @@ async function stopDaemon(appUrl) {
615
705
  }
616
706
  return await isDaemonListening(socketPath) ? "failed" : "stopped";
617
707
  }
618
- function stateMessage(state) {
708
+ function stateMessage(state, declined) {
619
709
  return {
620
710
  t: "state",
621
711
  status: state.status,
622
712
  machineName: state.machineName,
623
713
  friendlyName: state.friendlyName,
624
714
  error: state.error,
625
- grantedAgentIds: state.grantedAgentIds
715
+ grantedAgentIds: state.grantedAgentIds,
716
+ declinedAgentIds: [...declined]
626
717
  };
627
718
  }
628
719
  function sleep(ms) {
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import "./client-mykp1DVb.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-CfbpCjAw.mjs";
2
+ import "./client-Dd5sMXPv.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-Bq93vOIk.mjs";
4
4
 
5
5
  export { runPortalDaemon };
@@ -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-CfbpCjAw.mjs";
2
+ import { c as LOCAL_PROTOCOL_VERSION, d as encodeLine, f as makeLineParser, n as ensureDaemonRunning, p as parseDaemonMessage, u as daemonPaths } from "./daemon-Bq93vOIk.mjs";
3
3
  import { randomUUID } from "node:crypto";
4
4
  import { connect } from "node:net";
5
5
 
@@ -13,6 +13,7 @@ var PortalDaemonClient = class {
13
13
  fallbackCwd = null;
14
14
  wantEnabled = false;
15
15
  pendingGrants = /* @__PURE__ */ new Set();
16
+ pendingDeclines = /* @__PURE__ */ new Map();
16
17
  grantedAgentIds = /* @__PURE__ */ new Set();
17
18
  constructor(opts) {
18
19
  this.opts = opts;
@@ -35,7 +36,8 @@ var PortalDaemonClient = class {
35
36
  t: "hello",
36
37
  v: LOCAL_PROTOCOL_VERSION,
37
38
  sessionId: this.sessionId,
38
- token: this.opts.sessionToken
39
+ token: this.opts.sessionToken,
40
+ tokenKind: "session"
39
41
  });
40
42
  if (this.wantEnabled) this.send({ t: "enable" });
41
43
  if (this.fallbackCwd) this.send({
@@ -52,6 +54,11 @@ var PortalDaemonClient = class {
52
54
  t: "grant",
53
55
  agentId
54
56
  });
57
+ for (const [agentId, declined] of this.pendingDeclines) this.send({
58
+ t: "decline",
59
+ agentId,
60
+ declined
61
+ });
55
62
  });
56
63
  socket.on("data", (chunk) => {
57
64
  for (const line of parse(chunk)) {
@@ -82,7 +89,8 @@ var PortalDaemonClient = class {
82
89
  machineName: msg.machineName,
83
90
  friendlyName: msg.friendlyName,
84
91
  error: msg.error,
85
- grantedAgentIds: msg.grantedAgentIds
92
+ grantedAgentIds: msg.grantedAgentIds,
93
+ declinedAgentIds: msg.declinedAgentIds
86
94
  });
87
95
  }
88
96
  }
@@ -108,6 +116,19 @@ var PortalDaemonClient = class {
108
116
  this.send({ t: "disable" });
109
117
  }
110
118
  /**
119
+ * Record that the user said no (or yes again) to one agent on this machine.
120
+ * The daemon owns the answer, so every attached CLI stops asking — and it
121
+ * closes the share when nothing else is granted.
122
+ */
123
+ decline(agentId, declined) {
124
+ this.pendingDeclines.set(agentId, declined);
125
+ this.send({
126
+ t: "decline",
127
+ agentId,
128
+ declined
129
+ });
130
+ }
131
+ /**
111
132
  * Set the directory an exec runs in when the daemon can't place its
112
133
  * conversation. `skydive portal open` shares one directory and has no
113
134
  * conversation of its own, so this is how it routes.
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+ import "./client-Dd5sMXPv.mjs";
3
+ import "./daemon-Bq93vOIk.mjs";
4
+ import { t as PortalDaemonClient } from "./daemon-client-Bewt98dE.mjs";
5
+
6
+ export { PortalDaemonClient };
@@ -14,14 +14,17 @@ import { n as printError } from "./output-DYzzdXYV.mjs";
14
14
  * the reply (and to --json).
15
15
  */
16
16
  async function connectMachineShare({ appUrl, sessionToken, timeoutHint }) {
17
- const { PortalClient } = await import("./client-NBj0KZSP.mjs");
17
+ const { PortalClient } = await import("./client-CKBQ8pft.mjs");
18
18
  let signalConnected;
19
19
  const connected = new Promise((resolve) => {
20
20
  signalConnected = resolve;
21
21
  });
22
22
  const machineShare = new PortalClient({
23
23
  appUrl,
24
- sessionToken,
24
+ credentials: () => ({
25
+ sessionToken,
26
+ deviceToken: null
27
+ }),
25
28
  resolveCwd: () => process.cwd(),
26
29
  onState: (state) => {
27
30
  if (state.status === "connected") signalConnected();
@@ -8,7 +8,7 @@ import fs from "node:fs";
8
8
 
9
9
  //#region package.json
10
10
  var name = "skydive-cli";
11
- var version$1 = "0.4.1-beta.6";
11
+ var version$1 = "0.5.0-beta.2";
12
12
 
13
13
  //#endregion
14
14
  //#region src/auth/organization.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skydive-cli",
3
- "version": "0.4.1-beta.6",
3
+ "version": "0.5.0-beta.2",
4
4
  "description": "Skydive CLI — cloud agents from the command line",
5
5
  "homepage": "https://skydive.com",
6
6
  "license": "MIT",
@@ -23,7 +23,7 @@
23
23
  "build:binary": "node ./scripts/embed-changelog.mjs && bun scripts/build-binary.ts",
24
24
  "test:unit": "vitest run --passWithNoTests && yarn test:tui",
25
25
  "test:ci": "vitest run --coverage --coverage.reporter=lcovonly --reporter=default --reporter=github-actions --minWorkers=1 --maxWorkers=2 --passWithNoTests && yarn test:tui",
26
- "test:tui": "bun test .tui.test",
26
+ "test:tui": "node scripts/run-tui-tests.mjs",
27
27
  "render:frames": "bun scripts/render-frames.tsx",
28
28
  "render:send-errors": "bun scripts/render-send-errors.tsx",
29
29
  "typecheck": "tsgo --noEmit"
@@ -1,4 +0,0 @@
1
- #!/usr/bin/env node
2
- import { t as PortalClient } from "./client-mykp1DVb.mjs";
3
-
4
- export { PortalClient };
@@ -1,6 +0,0 @@
1
- #!/usr/bin/env node
2
- import "./client-mykp1DVb.mjs";
3
- import "./daemon-CfbpCjAw.mjs";
4
- import { t as PortalDaemonClient } from "./daemon-client-6xtta_n2.mjs";
5
-
6
- export { PortalDaemonClient };