skydive-cli 0.5.0-beta.31 → 0.5.0-beta.33

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.
Files changed (36) hide show
  1. package/README.md +20 -0
  2. package/dist/js/api-DQCaztBg.mjs +315 -0
  3. package/dist/js/{billing-blocked-Dgu5-oDy.mjs → billing-blocked-SE6tySLd.mjs} +1 -1
  4. package/dist/js/bin.mjs +214 -50
  5. package/dist/js/{boot-iaVSnd7h.mjs → boot-C5HJfwRS.mjs} +17 -14
  6. package/dist/js/chunk-BbwQpWto.mjs +33 -0
  7. package/dist/js/{client-DbqRBquD.mjs → client-d6K9qm6B.mjs} +70 -2
  8. package/dist/js/client-yz3zrpAS.mjs +5 -0
  9. package/dist/js/{daemon-BTyg329O.mjs → daemon-BPGZufS1.mjs} +4 -2
  10. package/dist/js/daemon-WQ8QYrFt.mjs +7 -0
  11. package/dist/js/daemon-client-DTfIza0O.mjs +8 -0
  12. package/dist/js/{daemon-client--A_yMKq6.mjs → daemon-client-MpAGVkqP.mjs} +1 -1
  13. package/dist/js/dist-CRtjM7ba.mjs +1750 -0
  14. package/dist/js/{forward-DL6DYqyc.mjs → forward-zwB55Bls.mjs} +25 -4
  15. package/dist/js/{profiler-BVbYs_Yg.mjs → install-BPUAm8bS.mjs} +274 -210
  16. package/dist/js/localhost-cert-Bn-UBUmj.mjs +67 -0
  17. package/dist/js/{print-DACzRUwQ.mjs → print-B9djx8GU.mjs} +3 -3
  18. package/dist/js/{print-C_TnQSPg.mjs → print-Clgq46GU.mjs} +3 -3
  19. package/dist/js/{print-share-BH9gvCls.mjs → print-share-BBy3OTno.mjs} +6 -2
  20. package/dist/js/{raw-pty-DY4KelZW.mjs → raw-pty-B9Bue8gg.mjs} +1 -1
  21. package/dist/js/raw-pty-g3kATVQe.mjs +5 -0
  22. package/dist/js/{rest-CgfbKXst.mjs → rest-C6E4mS2A.mjs} +2 -2
  23. package/dist/js/rest-CaWUSrnH.mjs +6 -0
  24. package/dist/js/tls-cert-CLgSQALB.mjs +4 -0
  25. package/dist/js/tls-cert-CV-pwxVN.mjs +67 -0
  26. package/package.json +1 -1
  27. package/dist/js/api-DG5W6iwx.mjs +0 -131
  28. package/dist/js/client-hH2PJL8y.mjs +0 -5
  29. package/dist/js/daemon-SPQgtsbW.mjs +0 -6
  30. package/dist/js/daemon-client-C7emKKlj.mjs +0 -7
  31. package/dist/js/raw-pty-DmdUf4_w.mjs +0 -5
  32. package/dist/js/rest-VV5nc-Mn.mjs +0 -6
  33. /package/dist/js/{billing-blocked-2wju4gC_.mjs → billing-blocked-D3l5kJlX.mjs} +0 -0
  34. /package/dist/js/{client-c4c5MmgN.mjs → client-BTQ1fwzM.mjs} +0 -0
  35. /package/dist/js/{http-error-DzyrsLAZ.mjs → http-error-UHH3mVBF.mjs} +0 -0
  36. /package/dist/js/{output-DYzzdXYV.mjs → output-wY0VQDea.mjs} +0 -0
package/README.md CHANGED
@@ -483,6 +483,25 @@ The remaining keys (`apiKey`, `apiUrl`, `sessionToken`, `appUrl`, `themeDark`,
483
483
  `themeLight`, …) are credentials or flow-managed state — leave them to the
484
484
  CLI.
485
485
 
486
+ ## Diagnostic logs
487
+
488
+ The CLI records local diagnostics by default. It keeps the latest 20 sessions
489
+ beside the CLI config. Nothing uploads automatically.
490
+
491
+ ```sh
492
+ skydive logs # list recent sessions
493
+ skydive logs path # print the latest session directory
494
+ skydive logs export # archive the latest session for sharing
495
+ skydive logs export <session> # archive one session from `logs list`
496
+ ```
497
+
498
+ Automatic logs include request timing, TUI state changes, render performance,
499
+ and process metadata. Command values and URL query values are removed.
500
+
501
+ Set `SKYDIVE_PROFILE=<path>` to write a full profile to a chosen directory.
502
+ Full profiles also include CPU data. Set `SKYDIVE_PROFILE=0` to disable all
503
+ session diagnostics.
504
+
486
505
  ## Environment variables
487
506
 
488
507
  | Variable | Effect |
@@ -493,6 +512,7 @@ CLI.
493
512
  | `SKYDIVE_APP_URL` | Chat app/session origin (overrides stored value) |
494
513
  | `SKYDIVE_THEME` | Chat colorscheme id (flag `--theme` overrides) |
495
514
  | `SKYDIVE_CONFIG_NAME` | On-disk config profile name (default `skydive`) |
515
+ | `SKYDIVE_PROFILE` | Full profile path; `0` disables diagnostics |
496
516
 
497
517
  ## Testing the TUI
498
518
 
@@ -0,0 +1,315 @@
1
+ #!/usr/bin/env node
2
+ import { t as HttpError } from "./http-error-UHH3mVBF.mjs";
3
+ import { z } from "zod";
4
+ import http from "node:http";
5
+ import http2 from "node:http2";
6
+
7
+ //#region ../portal-daemon/src/tls-forward.ts
8
+ const TLS_PORT_OFFSET = 1e4;
9
+ const STRIP_HEADERS = new Set([
10
+ "connection",
11
+ "keep-alive",
12
+ "proxy-authenticate",
13
+ "proxy-authorization",
14
+ "proxy-connection",
15
+ "te",
16
+ "trailer",
17
+ "transfer-encoding",
18
+ "upgrade",
19
+ "http2-settings"
20
+ ]);
21
+ /**
22
+ * The opt-in TLS+h2 origin for a forwarded/exposed port (ANY-6872, additive
23
+ * next to the plain listener — never a replacement). Browsers only negotiate
24
+ * h2 over TLS, and h2's stream multiplexing is what lifts the
25
+ * 6-connections-per-origin cap that makes unbundled dev servers crawl through
26
+ * the tunnel; each h2 stream is converted here to an h1 request over a tunnel
27
+ * from `acquireTunnel`. Used by BOTH tunnel surfaces: the CLI's
28
+ * `skydive portal forward` and the desktop daemon's `platform portal expose`.
29
+ *
30
+ * The dev server keeps seeing `Host: localhost:<port>` — the browser's
31
+ * authority is a public name it would reject (Vite 403s unknown hosts), and
32
+ * "looks exactly like localhost" is the product contract. The original
33
+ * authority travels in x-forwarded-host / x-forwarded-proto for apps that
34
+ * want it.
35
+ */
36
+ async function startTlsForward({ cert, localPort, targetPort, acquireTunnel, log }) {
37
+ const connectionShims = {
38
+ setNoDelay: () => {},
39
+ setKeepAlive: () => {},
40
+ setTimeout: () => {},
41
+ ref: () => {},
42
+ unref: () => {}
43
+ };
44
+ /**
45
+ * node's http client drives the tunnel: the agent's keep-alive free list
46
+ * gives request→request socket reuse on top of the pool's pre-paid dials,
47
+ * and a tunnel the daemon/dev-server side reaps while idle is evicted from
48
+ * the free list by its 'close' — the next request just adopts a fresh one.
49
+ * maxSockets mirrors the pool cap: every socket here holds an edge WS open,
50
+ * and ANY-6873 is the honest fix for wanting more concurrency than that.
51
+ *
52
+ * The base signature allows synchronous implementations to return the
53
+ * socket; this one is asynchronous, so it always uses the callback and
54
+ * returns undefined (the documented async contract). The agent core always
55
+ * passes the callback — the error branch below guards the contract anyway
56
+ * rather than assuming it.
57
+ */
58
+ class TunnelAgent extends http.Agent {
59
+ createConnection(_options, callback) {
60
+ if (!callback) throw new Error("TunnelAgent.createConnection requires the async callback");
61
+ (async () => {
62
+ let tunnel;
63
+ try {
64
+ tunnel = await acquireTunnel();
65
+ } catch (error) {
66
+ callback(error instanceof Error ? error : new Error(String(error)));
67
+ return;
68
+ }
69
+ Object.assign(tunnel.stream, connectionShims);
70
+ tunnel.stream.resume();
71
+ callback(null, tunnel.stream);
72
+ })();
73
+ }
74
+ }
75
+ const agent = new TunnelAgent({
76
+ keepAlive: true,
77
+ maxSockets: 16
78
+ });
79
+ const server = http2.createSecureServer({
80
+ key: cert.key,
81
+ cert: cert.cert,
82
+ allowHTTP1: true
83
+ });
84
+ const liveSessions = /* @__PURE__ */ new Set();
85
+ server.on("session", (session) => {
86
+ liveSessions.add(session);
87
+ session.once("close", () => liveSessions.delete(session));
88
+ });
89
+ server.on("secureConnection", (socket) => {
90
+ liveSessions.add(socket);
91
+ socket.once("close", () => liveSessions.delete(socket));
92
+ });
93
+ server.on("request", (req, res) => {
94
+ const headers = {};
95
+ for (const [name, value] of Object.entries(req.headers)) {
96
+ if (name.startsWith(":") || STRIP_HEADERS.has(name)) continue;
97
+ if (value !== void 0) headers[name] = value;
98
+ }
99
+ headers.host = `localhost:${targetPort}`;
100
+ headers["x-forwarded-proto"] = "https";
101
+ headers["x-forwarded-host"] = cert.hostname;
102
+ const upstream = http.request({
103
+ host: "localhost",
104
+ port: targetPort,
105
+ method: req.method,
106
+ path: req.url,
107
+ headers,
108
+ agent
109
+ }, (upstreamRes) => {
110
+ const resHeaders = {};
111
+ for (const [name, value] of Object.entries(upstreamRes.headers)) {
112
+ if (STRIP_HEADERS.has(name)) continue;
113
+ if (value !== void 0) resHeaders[name] = value;
114
+ }
115
+ res.writeHead(upstreamRes.statusCode ?? 502, resHeaders);
116
+ upstreamRes.pipe(res);
117
+ upstreamRes.on("error", () => res.destroy());
118
+ });
119
+ upstream.on("error", (err) => {
120
+ log(`forward(tls): upstream request failed: ${err.message}`);
121
+ if (!res.headersSent) {
122
+ res.writeHead(502, { "content-type": "text/plain" });
123
+ res.end("portal forward: tunnel request failed");
124
+ } else res.destroy();
125
+ });
126
+ req.pipe(upstream);
127
+ req.on("error", () => upstream.destroy());
128
+ });
129
+ server.on("upgrade", (req, socket, head) => {
130
+ socket.pause();
131
+ (async () => {
132
+ let tunnel;
133
+ try {
134
+ tunnel = await acquireTunnel();
135
+ } catch (err) {
136
+ log(`forward(tls): upgrade tunnel failed: ${err instanceof Error ? err.message : String(err)}`);
137
+ socket.destroy();
138
+ return;
139
+ }
140
+ const lines = [`${req.method} ${req.url} HTTP/1.1`];
141
+ for (let i = 0; i < req.rawHeaders.length; i += 2) {
142
+ const name = req.rawHeaders[i];
143
+ const value = name?.toLowerCase() === "host" ? `localhost:${targetPort}` : req.rawHeaders[i + 1];
144
+ lines.push(`${name}: ${value}`);
145
+ }
146
+ tunnel.stream.write(lines.join("\r\n") + "\r\n\r\n");
147
+ if (head.length > 0) tunnel.stream.write(head);
148
+ tunnel.stream.on("error", () => socket.destroy());
149
+ tunnel.stream.on("close", () => socket.destroy());
150
+ socket.on("error", () => tunnel.stream.destroy());
151
+ socket.on("close", () => tunnel.stream.destroy());
152
+ socket.pipe(tunnel.stream).pipe(socket);
153
+ socket.resume();
154
+ })();
155
+ });
156
+ server.on("tlsClientError", (err, tlsSocket) => {
157
+ tlsSocket.destroy();
158
+ log(`forward(tls): handshake failed: ${err.message}`);
159
+ });
160
+ const preferredPort = localPort + TLS_PORT_OFFSET <= 65535 ? localPort + TLS_PORT_OFFSET : 0;
161
+ return {
162
+ port: await new Promise((resolve, reject) => {
163
+ const onError = (err) => {
164
+ if (err.code === "EADDRINUSE" && preferredPort !== 0) {
165
+ server.listen(0, "127.0.0.1");
166
+ return;
167
+ }
168
+ server.removeListener("error", onError);
169
+ reject(err);
170
+ };
171
+ server.on("error", onError);
172
+ server.on("listening", () => {
173
+ server.removeListener("error", onError);
174
+ const addr = server.address();
175
+ resolve(addr && typeof addr === "object" ? addr.port : preferredPort);
176
+ });
177
+ server.listen(preferredPort, "127.0.0.1");
178
+ }),
179
+ hostname: cert.hostname,
180
+ close: () => new Promise((resolve) => {
181
+ agent.destroy();
182
+ server.close(() => resolve());
183
+ for (const live of liveSessions) live.destroy();
184
+ })
185
+ };
186
+ }
187
+
188
+ //#endregion
189
+ //#region ../portal-daemon/src/api.ts
190
+ /**
191
+ * The portal's session-authed REST surface, shared by `PortalClient` (the
192
+ * TUI/`portal open` connection) and the `skydive portal` management
193
+ * commands, so the endpoint contracts and response schemas live in exactly
194
+ * one place.
195
+ */
196
+ const deviceSchema = z.object({
197
+ id: z.string(),
198
+ machineName: z.string(),
199
+ friendlyName: z.string(),
200
+ connected: z.boolean(),
201
+ lastSeen: z.string().nullable(),
202
+ grantedAgentIds: z.array(z.string())
203
+ });
204
+ const devicesResponseSchema = z.object({
205
+ devices: z.array(deviceSchema),
206
+ agents: z.array(z.object({
207
+ id: z.string(),
208
+ name: z.string()
209
+ }))
210
+ });
211
+ const deviceTokenSchema = z.object({ token: z.string().min(1) });
212
+ async function portalFetch(auth, path, init) {
213
+ const res = await fetch(`${auth.appUrl}${path}`, {
214
+ method: init.method,
215
+ headers: {
216
+ authorization: `Bearer ${auth.sessionToken}`,
217
+ accept: "application/json",
218
+ ...init.body ? { "content-type": "application/json" } : {}
219
+ },
220
+ ...init.body ? { body: init.body } : {}
221
+ });
222
+ if (!res.ok) {
223
+ const body = await res.text().catch(() => "");
224
+ throw new HttpError(res.status, body);
225
+ }
226
+ return res.json();
227
+ }
228
+ async function fetchPortalDevices(auth) {
229
+ const json = await portalFetch(auth, "/api/v1/portal/devices", { method: "GET" });
230
+ return devicesResponseSchema.parse(json);
231
+ }
232
+ const registerResponseSchema = z.object({ device: z.object({ id: z.string() }) });
233
+ /**
234
+ * Register this machine's device row without connecting. Connecting registers
235
+ * as a side effect; this covers granting an agent on a machine that has never
236
+ * shared yet (the grant references the device row).
237
+ */
238
+ async function registerPortalDevice(auth, { machineName, friendlyName }) {
239
+ const json = await portalFetch(auth, "/api/v1/portal/devices", {
240
+ method: "POST",
241
+ body: JSON.stringify({
242
+ machineName,
243
+ friendlyName
244
+ })
245
+ });
246
+ return registerResponseSchema.parse(json).device;
247
+ }
248
+ /** Short-lived token the machine presents when dialing the portal WebSocket. */
249
+ async function mintPortalDeviceToken(auth) {
250
+ const json = await portalFetch(auth, "/api/v1/portal/device-token", { method: "POST" });
251
+ return deviceTokenSchema.parse(json).token;
252
+ }
253
+ const forwardTargetSchema = z.object({
254
+ daemonOrigin: z.string().min(1),
255
+ token: z.string().min(1),
256
+ expiresInSeconds: z.number()
257
+ });
258
+ /**
259
+ * Everything `portal forward` needs to dial an agent's sandbox daemon through
260
+ * the agent-webserver edge Worker: the daemon's stable public origin and a
261
+ * daemon auth token (canUse-gated server-side).
262
+ */
263
+ async function fetchForwardTarget(auth, agentId) {
264
+ const json = await portalFetch(auth, "/api/v1/portal/forward-target", {
265
+ method: "POST",
266
+ body: JSON.stringify({ agentId })
267
+ });
268
+ return forwardTargetSchema.parse(json);
269
+ }
270
+ async function grantPortalAccess(auth, { deviceId, agentId, conversationId }) {
271
+ await portalFetch(auth, `/api/v1/portal/devices/${encodeURIComponent(deviceId)}/grants`, {
272
+ method: "POST",
273
+ body: JSON.stringify({
274
+ agentId,
275
+ conversationId
276
+ })
277
+ });
278
+ }
279
+ async function revokePortalAccess(auth, { deviceId, agentId }) {
280
+ await portalFetch(auth, `/api/v1/portal/devices/${encodeURIComponent(deviceId)}/grants/${encodeURIComponent(agentId)}`, { method: "DELETE" });
281
+ }
282
+ /**
283
+ * The device row for a given machine identity. Matching is by `machineName`
284
+ * equality — the stable handle the machine registers under, not the display
285
+ * label.
286
+ */
287
+ function findThisDevice(devices, machineName) {
288
+ return devices.find((device) => device.machineName === machineName) ?? null;
289
+ }
290
+ /**
291
+ * One-time grant migration onto the merged device. Earlier CLI builds
292
+ * registered a separate `<machineName>-cli` device, so a user's existing
293
+ * approvals hang off that row; the merged device would start with zero grants
294
+ * and every already-authorized agent would ask again. Copy any grant the
295
+ * merged device is missing (the grant endpoint upserts, so re-runs are
296
+ * no-ops). The legacy row is left in place — an old CLI build may still
297
+ * connect under it. Returns how many grants were copied.
298
+ */
299
+ async function unifyLegacyCliGrants(auth, machineName) {
300
+ const { devices } = await fetchPortalDevices(auth);
301
+ const merged = findThisDevice(devices, machineName);
302
+ const legacy = findThisDevice(devices, `${machineName}-cli`);
303
+ if (!merged || !legacy) return 0;
304
+ const have = new Set(merged.grantedAgentIds);
305
+ const missing = legacy.grantedAgentIds.filter((id) => !have.has(id));
306
+ for (const agentId of missing) await grantPortalAccess(auth, {
307
+ deviceId: merged.id,
308
+ agentId,
309
+ conversationId: null
310
+ });
311
+ return missing.length;
312
+ }
313
+
314
+ //#endregion
315
+ export { mintPortalDeviceToken as a, unifyLegacyCliGrants as c, grantPortalAccess as i, startTlsForward as l, fetchPortalDevices as n, registerPortalDevice as o, findThisDevice as r, revokePortalAccess as s, fetchForwardTarget as t };
@@ -1,4 +1,4 @@
1
1
  #!/usr/bin/env node
2
- import { a as billingBlockedOutcomeSchema, i as billingBlockedOutcomeFromSendResponse, n as BillingBlockedError, r as billingBlockedJsonResult, t as BILLING_BLOCKED_EXIT_CODE } from "./billing-blocked-2wju4gC_.mjs";
2
+ import { a as billingBlockedOutcomeSchema, i as billingBlockedOutcomeFromSendResponse, n as BillingBlockedError, r as billingBlockedJsonResult, t as BILLING_BLOCKED_EXIT_CODE } from "./billing-blocked-D3l5kJlX.mjs";
3
3
 
4
4
  export { BILLING_BLOCKED_EXIT_CODE, BillingBlockedError, billingBlockedJsonResult };