skydive-cli 0.1.0-beta.382 → 0.1.0-beta.390

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/README.md CHANGED
@@ -25,7 +25,10 @@ single `config.json`:
25
25
  same machine replaces it rather than accumulating rows, and
26
26
  `skydive auth logout` revokes it, so signing out doesn't leave a live key
27
27
  behind. The workspace is in the name because the key is pinned to it: unlike
28
- the session, a key never follows `workspace switch`.
28
+ the session, a key never follows `workspace switch` — and whenever the key
29
+ (not the session) is the credential actually driving a management command,
30
+ the CLI says so on stderr, naming the pinned workspace, so commands never
31
+ silently act in a workspace you switched away from.
29
32
  - a **user session** for `skydive chat`. Chat is user-level and multi-agent,
30
33
  so it authenticates as you — and unlike the API key (which is pinned to the
31
34
  workspace that minted it), the session follows `skydive workspace switch`.
@@ -318,6 +321,7 @@ Hand-editable keys:
318
321
  | Key | Type | Effect |
319
322
  | --------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
320
323
  | `shareMachineDefault` | boolean | When `true`, `skydive chat` (TUI and `-p`) shares this machine over the portal on launch, as if `--share-machine` were passed. An explicit `--share-machine`/`--no-share-machine` overrides it per invocation. Default `false`. |
324
+ | `updateCheck` | boolean | When `false`, disables the daily background update check and its "Update available" notice. The persistent equivalent of setting `SKYDIVE_NO_UPDATE_CHECK` (or `NO_UPDATE_NOTIFIER`) in the environment. Default `true`. |
321
325
 
322
326
  The remaining keys (`apiKey`, `apiUrl`, `sessionToken`, `appUrl`, `themeDark`,
323
327
  `themeLight`, …) are credentials or flow-managed state — leave them to the
@@ -0,0 +1,145 @@
1
+ #!/usr/bin/env node
2
+ import { t as HttpError } from "./rest-DTlkPko_.mjs";
3
+ import os from "node:os";
4
+ import { z } from "zod";
5
+
6
+ //#region src/chat/portal/machine.ts
7
+ /**
8
+ * Identity this machine registers under when the CLI shares it via the portal.
9
+ *
10
+ * The `-cli` suffix / `(CLI)` label keep a CLI-shared machine a DISTINCT portal
11
+ * device from the same host's Skydive Desktop app. `portal_device` is unique on
12
+ * (org, user, machineName), and directives route to whichever socket holds the
13
+ * device — if the CLI and desktop registered the same name they'd share a
14
+ * device row and both execute every directive. Distinct names also make the
15
+ * grant UI unambiguous about which surface is being authorized.
16
+ */
17
+ function machineIdentity() {
18
+ const host = (os.hostname() || "machine").trim().replace(/\.local$/i, "") || "machine";
19
+ return {
20
+ machineName: `${host}-cli`,
21
+ friendlyName: `${host} (CLI)`
22
+ };
23
+ }
24
+ const INHERITED_ENV = [
25
+ "HOME",
26
+ "USER",
27
+ "LOGNAME",
28
+ "SHELL",
29
+ "LANG",
30
+ "LC_ALL",
31
+ "TMPDIR",
32
+ "TERM",
33
+ "PATH"
34
+ ];
35
+ function buildEnv(extra) {
36
+ const env = {};
37
+ for (const key of INHERITED_ENV) {
38
+ const value = process.env[key];
39
+ if (value !== void 0) env[key] = value;
40
+ }
41
+ if (extra) for (const [key, value] of Object.entries(extra)) env[key] = value;
42
+ return env;
43
+ }
44
+ /**
45
+ * Build the desktop-portal WebSocket URL from the chat origin. Mirrors the Rust
46
+ * desktop client: http→ws, https→wss, scheme-less defaults to wss, and the
47
+ * machine/label ride as query pairs (percent-encoded by URL).
48
+ */
49
+ function portalWsUrl(appUrl, machine, label) {
50
+ const base = appUrl.replace(/\/+$/, "");
51
+ let wsBase;
52
+ if (base.startsWith("https://")) wsBase = `wss://${base.slice(8)}`;
53
+ else if (base.startsWith("http://")) wsBase = `ws://${base.slice(7)}`;
54
+ else wsBase = `wss://${base}`;
55
+ const url = new URL(`${wsBase}/api/v1/portal/desktop`);
56
+ url.searchParams.set("machine", machine);
57
+ url.searchParams.set("label", label);
58
+ return url.toString();
59
+ }
60
+
61
+ //#endregion
62
+ //#region src/chat/portal/api.ts
63
+ /**
64
+ * The portal's session-authed REST surface, shared by `PortalClient` (the
65
+ * TUI/`portal open` connection) and the `skydive portal` management
66
+ * commands, so the endpoint contracts and response schemas live in exactly
67
+ * one place.
68
+ */
69
+ const deviceSchema = z.object({
70
+ id: z.string(),
71
+ machineName: z.string(),
72
+ friendlyName: z.string(),
73
+ connected: z.boolean(),
74
+ lastSeen: z.string().nullable(),
75
+ grantedAgentIds: z.array(z.string())
76
+ });
77
+ const devicesResponseSchema = z.object({
78
+ devices: z.array(deviceSchema),
79
+ agents: z.array(z.object({
80
+ id: z.string(),
81
+ name: z.string()
82
+ }))
83
+ });
84
+ const deviceTokenSchema = z.object({ token: z.string().min(1) });
85
+ async function portalFetch(auth, path, init) {
86
+ const res = await fetch(`${auth.appUrl}${path}`, {
87
+ method: init.method,
88
+ headers: {
89
+ authorization: `Bearer ${auth.sessionToken}`,
90
+ accept: "application/json",
91
+ ...init.body ? { "content-type": "application/json" } : {}
92
+ },
93
+ ...init.body ? { body: init.body } : {}
94
+ });
95
+ if (!res.ok) {
96
+ const body = await res.text().catch(() => "");
97
+ throw new HttpError(res.status, body);
98
+ }
99
+ return res.json();
100
+ }
101
+ async function fetchPortalDevices(auth) {
102
+ const json = await portalFetch(auth, "/api/v1/portal/devices", { method: "GET" });
103
+ return devicesResponseSchema.parse(json);
104
+ }
105
+ const registerResponseSchema = z.object({ device: z.object({ id: z.string() }) });
106
+ /**
107
+ * Register this machine's device row without connecting. Connecting registers
108
+ * as a side effect; this covers granting an agent on a machine that has never
109
+ * shared yet (the grant references the device row).
110
+ */
111
+ async function registerPortalDevice(auth, { machineName, friendlyName }) {
112
+ const json = await portalFetch(auth, "/api/v1/portal/devices", {
113
+ method: "POST",
114
+ body: JSON.stringify({
115
+ machineName,
116
+ friendlyName
117
+ })
118
+ });
119
+ return registerResponseSchema.parse(json).device;
120
+ }
121
+ /** Short-lived token the machine presents when dialing the portal WebSocket. */
122
+ async function mintPortalDeviceToken(auth) {
123
+ const json = await portalFetch(auth, "/api/v1/portal/device-token", { method: "POST" });
124
+ return deviceTokenSchema.parse(json).token;
125
+ }
126
+ async function grantPortalAccess(auth, { deviceId, agentId }) {
127
+ await portalFetch(auth, `/api/v1/portal/devices/${encodeURIComponent(deviceId)}/grants`, {
128
+ method: "POST",
129
+ body: JSON.stringify({ agentId })
130
+ });
131
+ }
132
+ async function revokePortalAccess(auth, { deviceId, agentId }) {
133
+ await portalFetch(auth, `/api/v1/portal/devices/${encodeURIComponent(deviceId)}/grants/${encodeURIComponent(agentId)}`, { method: "DELETE" });
134
+ }
135
+ /**
136
+ * The device row for a given machine identity. Matching is by `machineName`
137
+ * equality — the stable per-surface handle (`<host>-cli` vs the desktop's
138
+ * `<host>`), not the display label.
139
+ */
140
+ function findThisDevice(devices, machineName) {
141
+ return devices.find((device) => device.machineName === machineName) ?? null;
142
+ }
143
+
144
+ //#endregion
145
+ export { registerPortalDevice as a, machineIdentity as c, mintPortalDeviceToken as i, portalWsUrl as l, findThisDevice as n, revokePortalAccess as o, grantPortalAccess as r, buildEnv as s, fetchPortalDevices as t };