skydive-cli 0.4.0 → 0.4.1-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/CHANGELOG.md +4 -0
- package/README.md +51 -0
- package/dist/js/{billing-blocked-euid6MN9.mjs → billing-blocked-Dgu5-oDy.mjs} +1 -1
- package/dist/js/bin.mjs +333 -51
- package/dist/js/{boot-on5ucI_Z.mjs → boot-DZYbTxev.mjs} +988 -259
- package/dist/js/{client-CKzK-12y.mjs → client-Cf7cvftg.mjs} +19 -6
- package/dist/js/client-CneLVPei.mjs +4 -0
- package/dist/js/{daemon-CaFz7Wtw.mjs → daemon-DaUaCMwC.mjs} +22 -5
- package/dist/js/daemon-client-CJxbCLOb.mjs +168 -0
- package/dist/js/daemon-client-DlblQIR9.mjs +6 -0
- package/dist/js/daemon-imLBqTGg.mjs +5 -0
- package/dist/js/http-error-DzyrsLAZ.mjs +20 -0
- package/dist/js/{print-X2N5wJ5l.mjs → print-CbayCa87.mjs} +6 -5
- package/dist/js/{print-BXNq9UxE.mjs → print-ba_0hiV9.mjs} +3 -3
- package/dist/js/{print-share-D72CfWJk.mjs → print-share-CT4ltX7u.mjs} +3 -3
- package/dist/js/{install-Ba-UTi-l.mjs → profiler-BA-c3-gP.mjs} +210 -3
- package/dist/js/raw-pty-3EkG-jjH.mjs +5 -0
- package/dist/js/{raw-pty-DAmb8uqt.mjs → raw-pty-B6mAroiI.mjs} +1 -1
- package/dist/js/{rest-imDZZGQA.mjs → rest-BY2nADw5.mjs} +24 -10
- package/dist/js/rest-DADJh0bi.mjs +6 -0
- package/package.json +3 -2
- package/dist/js/client-D6apKmV0.mjs +0 -6
- package/dist/js/daemon-BADmpJAs.mjs +0 -7
- package/dist/js/raw-pty-t5DC-e1T.mjs +0 -5
- package/dist/js/rest-Br2eFTlj.mjs +0 -5
- /package/dist/js/{billing-blocked-CwfG1BTR.mjs → billing-blocked-2wju4gC_.mjs} +0 -0
- /package/dist/js/{client-D8s9vY4p.mjs → client-Cn2af31H.mjs} +0 -0
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { t as HttpError } from "./http-error-DzyrsLAZ.mjs";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { spawn } from "node:child_process";
|
|
6
6
|
import { WebSocket } from "ws";
|
|
7
7
|
|
|
8
|
-
//#region src/
|
|
8
|
+
//#region ../portal-daemon/src/machine.ts
|
|
9
9
|
/**
|
|
10
10
|
* Identity this machine registers under when the CLI shares it via the portal.
|
|
11
11
|
*
|
|
@@ -60,6 +60,19 @@ function portalWsUrl(appUrl, machine, label) {
|
|
|
60
60
|
return url.toString();
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
//#endregion
|
|
64
|
+
//#region ../portal-daemon/src/util.ts
|
|
65
|
+
/** Narrowing helper for the unknown JSON payloads the daemon reads (persisted
|
|
66
|
+
* state, wire frames). A type predicate (not an `as` cast), so call sites can
|
|
67
|
+
* read properties without asserting. */
|
|
68
|
+
function isRecord(value) {
|
|
69
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
70
|
+
}
|
|
71
|
+
/** Best-effort message from an unknown thrown value. */
|
|
72
|
+
function errorMessage(err) {
|
|
73
|
+
return err instanceof Error ? err.message : String(err);
|
|
74
|
+
}
|
|
75
|
+
|
|
63
76
|
//#endregion
|
|
64
77
|
//#region ../portal-protocol/src/index.ts
|
|
65
78
|
const MAX_WS_FRAME_BYTES = 16 * 1024 * 1024;
|
|
@@ -126,7 +139,7 @@ function decodeFrame(frame) {
|
|
|
126
139
|
}
|
|
127
140
|
|
|
128
141
|
//#endregion
|
|
129
|
-
//#region src/
|
|
142
|
+
//#region ../portal-daemon/src/exec.ts
|
|
130
143
|
/**
|
|
131
144
|
* Runs portal `exec` directives locally. Each `open` spawns a child process
|
|
132
145
|
* whose stdout/stderr stream back as data frames and whose stdin is fed by
|
|
@@ -252,7 +265,7 @@ var JobManager = class {
|
|
|
252
265
|
};
|
|
253
266
|
|
|
254
267
|
//#endregion
|
|
255
|
-
//#region src/
|
|
268
|
+
//#region ../portal-daemon/src/api.ts
|
|
256
269
|
/**
|
|
257
270
|
* The portal's session-authed REST surface, shared by `PortalClient` (the
|
|
258
271
|
* TUI/`portal open` connection) and the `skydive portal` management
|
|
@@ -335,7 +348,7 @@ function findThisDevice(devices, machineName) {
|
|
|
335
348
|
}
|
|
336
349
|
|
|
337
350
|
//#endregion
|
|
338
|
-
//#region src/
|
|
351
|
+
//#region ../portal-daemon/src/client.ts
|
|
339
352
|
const INITIAL_BACKOFF_MS = 500;
|
|
340
353
|
const MAX_BACKOFF_MS = 1e4;
|
|
341
354
|
/**
|
|
@@ -503,4 +516,4 @@ function sleep(ms) {
|
|
|
503
516
|
}
|
|
504
517
|
|
|
505
518
|
//#endregion
|
|
506
|
-
export { registerPortalDevice as a, grantPortalAccess as i, fetchPortalDevices as n, revokePortalAccess as o, findThisDevice as r,
|
|
519
|
+
export { registerPortalDevice as a, machineIdentity as c, grantPortalAccess as i, fetchPortalDevices as n, revokePortalAccess as o, findThisDevice as r, isRecord as s, PortalClient as t };
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import { t as PortalClient } from "./client-CKzK-12y.mjs";
|
|
2
|
+
import { s as isRecord, t as PortalClient } from "./client-Cf7cvftg.mjs";
|
|
4
3
|
import os from "node:os";
|
|
5
4
|
import path from "node:path";
|
|
6
5
|
import { z } from "zod";
|
|
@@ -9,7 +8,7 @@ import { createHash } from "node:crypto";
|
|
|
9
8
|
import { connect, createServer } from "node:net";
|
|
10
9
|
import { access, appendFile, mkdir, readFile, unlink, writeFile } from "node:fs/promises";
|
|
11
10
|
|
|
12
|
-
//#region src/
|
|
11
|
+
//#region ../portal-daemon/src/local-protocol.ts
|
|
13
12
|
/**
|
|
14
13
|
* Local IPC between the portal DAEMON and the `skydive` CLI processes attached
|
|
15
14
|
* to it, on one host.
|
|
@@ -96,6 +95,15 @@ const clientBindSchema = z.object({
|
|
|
96
95
|
/** `enable`/`disable` — turn machine sharing on/off (user-initiated). */
|
|
97
96
|
const clientEnableSchema = z.object({ t: z.literal("enable") });
|
|
98
97
|
const clientDisableSchema = z.object({ t: z.literal("disable") });
|
|
98
|
+
/**
|
|
99
|
+
* `cwd` — set the directory an exec runs in when its conversation is unknown.
|
|
100
|
+
* `skydive portal open` has no conversation of its own: it shares the machine
|
|
101
|
+
* for one directory, so it sets the fallback directly instead of binding.
|
|
102
|
+
*/
|
|
103
|
+
const clientCwdSchema = z.object({
|
|
104
|
+
t: z.literal("cwd"),
|
|
105
|
+
cwd: z.string().min(1)
|
|
106
|
+
});
|
|
99
107
|
/** `grant` — authorize one agent to reach this machine (user-initiated). */
|
|
100
108
|
const clientGrantSchema = z.object({
|
|
101
109
|
t: z.literal("grant"),
|
|
@@ -123,6 +131,7 @@ const clientMessageSchema = z.discriminatedUnion("t", [
|
|
|
123
131
|
clientBindSchema,
|
|
124
132
|
clientEnableSchema,
|
|
125
133
|
clientDisableSchema,
|
|
134
|
+
clientCwdSchema,
|
|
126
135
|
clientGrantSchema,
|
|
127
136
|
clientByeSchema,
|
|
128
137
|
clientStatusSchema,
|
|
@@ -220,7 +229,7 @@ function parseDaemonMessage(line) {
|
|
|
220
229
|
}
|
|
221
230
|
|
|
222
231
|
//#endregion
|
|
223
|
-
//#region src/
|
|
232
|
+
//#region ../portal-daemon/src/daemon.ts
|
|
224
233
|
/**
|
|
225
234
|
* The portal DAEMON: one long-lived process per (user, appUrl) that owns the
|
|
226
235
|
* SINGLE portal WebSocket to the server and is the only writer of the machine's
|
|
@@ -290,7 +299,8 @@ var PortalDaemon = class {
|
|
|
290
299
|
const conn = {
|
|
291
300
|
socket,
|
|
292
301
|
sessionId: null,
|
|
293
|
-
conversations: /* @__PURE__ */ new Set()
|
|
302
|
+
conversations: /* @__PURE__ */ new Set(),
|
|
303
|
+
wantsShare: false
|
|
294
304
|
};
|
|
295
305
|
this.conns.add(conn);
|
|
296
306
|
this.clearIdleTimer();
|
|
@@ -324,12 +334,17 @@ var PortalDaemon = class {
|
|
|
324
334
|
this.persistState();
|
|
325
335
|
return;
|
|
326
336
|
case "enable":
|
|
337
|
+
conn.wantsShare = true;
|
|
327
338
|
this.ensureClient();
|
|
328
339
|
this.client?.enable();
|
|
329
340
|
return;
|
|
330
341
|
case "disable":
|
|
342
|
+
for (const other of this.conns) other.wantsShare = false;
|
|
331
343
|
this.client?.disable();
|
|
332
344
|
return;
|
|
345
|
+
case "cwd":
|
|
346
|
+
this.fallbackCwd = msg.cwd;
|
|
347
|
+
return;
|
|
333
348
|
case "grant":
|
|
334
349
|
this.ensureClient();
|
|
335
350
|
this.client?.grantAgent(msg.agentId).catch((error) => {
|
|
@@ -354,6 +369,8 @@ var PortalDaemon = class {
|
|
|
354
369
|
try {
|
|
355
370
|
conn.socket.destroy();
|
|
356
371
|
} catch (_error) {}
|
|
372
|
+
conn.wantsShare = false;
|
|
373
|
+
if (![...this.conns].some((c) => c.wantsShare)) this.client?.disable();
|
|
357
374
|
if (this.conns.size === 0) this.armIdleTimer();
|
|
358
375
|
}
|
|
359
376
|
/** Create the single PortalClient the first time a client needs the portal. */
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { d as makeLineParser, f as parseDaemonMessage, l as daemonPaths, n as ensureDaemonRunning, s as LOCAL_PROTOCOL_VERSION, u as encodeLine } from "./daemon-DaUaCMwC.mjs";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { connect } from "node:net";
|
|
5
|
+
|
|
6
|
+
//#region ../portal-daemon/src/daemon-client.ts
|
|
7
|
+
var PortalDaemonClient = class {
|
|
8
|
+
sessionId = randomUUID();
|
|
9
|
+
socket = null;
|
|
10
|
+
disposed = false;
|
|
11
|
+
reconnectTimer = null;
|
|
12
|
+
lastBind = null;
|
|
13
|
+
fallbackCwd = null;
|
|
14
|
+
wantEnabled = false;
|
|
15
|
+
pendingGrants = /* @__PURE__ */ new Set();
|
|
16
|
+
grantedAgentIds = /* @__PURE__ */ new Set();
|
|
17
|
+
constructor(opts) {
|
|
18
|
+
this.opts = opts;
|
|
19
|
+
}
|
|
20
|
+
/** Spawn the daemon if needed and attach. Idempotent. */
|
|
21
|
+
async start() {
|
|
22
|
+
if (this.disposed) return;
|
|
23
|
+
await ensureDaemonRunning(this.opts.appUrl);
|
|
24
|
+
this.connect();
|
|
25
|
+
}
|
|
26
|
+
connect() {
|
|
27
|
+
if (this.disposed) return;
|
|
28
|
+
const { socketPath } = daemonPaths(this.opts.appUrl);
|
|
29
|
+
const socket = connect(socketPath);
|
|
30
|
+
socket.setEncoding("utf8");
|
|
31
|
+
this.socket = socket;
|
|
32
|
+
const parse = makeLineParser();
|
|
33
|
+
socket.on("connect", () => {
|
|
34
|
+
this.send({
|
|
35
|
+
t: "hello",
|
|
36
|
+
v: LOCAL_PROTOCOL_VERSION,
|
|
37
|
+
sessionId: this.sessionId,
|
|
38
|
+
token: this.opts.sessionToken
|
|
39
|
+
});
|
|
40
|
+
if (this.wantEnabled) this.send({ t: "enable" });
|
|
41
|
+
if (this.fallbackCwd) this.send({
|
|
42
|
+
t: "cwd",
|
|
43
|
+
cwd: this.fallbackCwd
|
|
44
|
+
});
|
|
45
|
+
if (this.lastBind) this.send({
|
|
46
|
+
t: "bind",
|
|
47
|
+
sessionId: this.sessionId,
|
|
48
|
+
conversationId: this.lastBind.conversationId,
|
|
49
|
+
cwd: this.lastBind.cwd
|
|
50
|
+
});
|
|
51
|
+
for (const agentId of this.pendingGrants) this.send({
|
|
52
|
+
t: "grant",
|
|
53
|
+
agentId
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
socket.on("data", (chunk) => {
|
|
57
|
+
for (const line of parse(chunk)) {
|
|
58
|
+
const msg = parseDaemonMessage(line);
|
|
59
|
+
if (msg) this.onMessage(msg);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
const onClose = () => {
|
|
63
|
+
if (this.socket === socket) this.socket = null;
|
|
64
|
+
this.scheduleReconnect();
|
|
65
|
+
};
|
|
66
|
+
socket.on("close", onClose);
|
|
67
|
+
socket.on("error", onClose);
|
|
68
|
+
}
|
|
69
|
+
scheduleReconnect() {
|
|
70
|
+
if (this.disposed || this.reconnectTimer) return;
|
|
71
|
+
this.reconnectTimer = setTimeout(() => {
|
|
72
|
+
this.reconnectTimer = null;
|
|
73
|
+
this.start();
|
|
74
|
+
}, 500);
|
|
75
|
+
this.reconnectTimer.unref();
|
|
76
|
+
}
|
|
77
|
+
onMessage(msg) {
|
|
78
|
+
if (msg.t === "state") {
|
|
79
|
+
this.grantedAgentIds = new Set(msg.grantedAgentIds);
|
|
80
|
+
this.opts.onState({
|
|
81
|
+
status: msg.status,
|
|
82
|
+
machineName: msg.machineName,
|
|
83
|
+
friendlyName: msg.friendlyName,
|
|
84
|
+
error: msg.error,
|
|
85
|
+
grantedAgentIds: msg.grantedAgentIds
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/** Whether this agent is currently authorized on the machine (last daemon state). */
|
|
90
|
+
isGranted(agentId) {
|
|
91
|
+
return this.grantedAgentIds.has(agentId);
|
|
92
|
+
}
|
|
93
|
+
/** Whether the user has turned machine sharing on from this CLI. */
|
|
94
|
+
isEnabled() {
|
|
95
|
+
return this.wantEnabled;
|
|
96
|
+
}
|
|
97
|
+
send(msg) {
|
|
98
|
+
if (this.socket?.writable) this.socket.write(encodeLine(msg));
|
|
99
|
+
}
|
|
100
|
+
/** Turn machine sharing on (the daemon connects the portal). */
|
|
101
|
+
enable() {
|
|
102
|
+
this.wantEnabled = true;
|
|
103
|
+
this.send({ t: "enable" });
|
|
104
|
+
}
|
|
105
|
+
/** Turn machine sharing off. */
|
|
106
|
+
disable() {
|
|
107
|
+
this.wantEnabled = false;
|
|
108
|
+
this.send({ t: "disable" });
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Set the directory an exec runs in when the daemon can't place its
|
|
112
|
+
* conversation. `skydive portal open` shares one directory and has no
|
|
113
|
+
* conversation of its own, so this is how it routes.
|
|
114
|
+
*/
|
|
115
|
+
setFallbackCwd(cwd) {
|
|
116
|
+
this.fallbackCwd = cwd;
|
|
117
|
+
this.send({
|
|
118
|
+
t: "cwd",
|
|
119
|
+
cwd
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
/** Register / update this conversation's working directory for exec routing. */
|
|
123
|
+
bind(conversationId, cwd) {
|
|
124
|
+
this.lastBind = {
|
|
125
|
+
conversationId,
|
|
126
|
+
cwd
|
|
127
|
+
};
|
|
128
|
+
this.send({
|
|
129
|
+
t: "bind",
|
|
130
|
+
sessionId: this.sessionId,
|
|
131
|
+
conversationId,
|
|
132
|
+
cwd
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Authorize one agent to run commands on this machine. Resolves once the
|
|
137
|
+
* request is sent to the daemon (the daemon performs the grant and pushes the
|
|
138
|
+
* updated state); kept async so it's a drop-in for the old in-process client's
|
|
139
|
+
* awaited `grantAgent`.
|
|
140
|
+
*/
|
|
141
|
+
grantAgent(agentId) {
|
|
142
|
+
this.pendingGrants.add(agentId);
|
|
143
|
+
this.send({
|
|
144
|
+
t: "grant",
|
|
145
|
+
agentId
|
|
146
|
+
});
|
|
147
|
+
return Promise.resolve();
|
|
148
|
+
}
|
|
149
|
+
/** Detach this CLI. The daemon keeps running for other clients. */
|
|
150
|
+
dispose() {
|
|
151
|
+
this.disposed = true;
|
|
152
|
+
if (this.reconnectTimer) {
|
|
153
|
+
clearTimeout(this.reconnectTimer);
|
|
154
|
+
this.reconnectTimer = null;
|
|
155
|
+
}
|
|
156
|
+
this.send({
|
|
157
|
+
t: "bye",
|
|
158
|
+
sessionId: this.sessionId
|
|
159
|
+
});
|
|
160
|
+
try {
|
|
161
|
+
this.socket?.destroy();
|
|
162
|
+
} catch (_error) {}
|
|
163
|
+
this.socket = null;
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
//#endregion
|
|
168
|
+
export { PortalDaemonClient as t };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
//#region ../portal-daemon/src/http-error.ts
|
|
3
|
+
/**
|
|
4
|
+
* HTTP failure that keeps the FULL response body. The message self-clips to
|
|
5
|
+
* 200 chars because it flows into logs and one-line UIs; consumers that can
|
|
6
|
+
* afford the whole body (an expandable transcript block) read `body` directly.
|
|
7
|
+
* The CLI re-exports this class from its REST layer so `instanceof` checks
|
|
8
|
+
* hold across both surfaces.
|
|
9
|
+
*/
|
|
10
|
+
var HttpError = class extends Error {
|
|
11
|
+
constructor(status, body) {
|
|
12
|
+
super(`HTTP ${status}: ${body.slice(0, 200)}`);
|
|
13
|
+
this.status = status;
|
|
14
|
+
this.body = body;
|
|
15
|
+
this.name = "HttpError";
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
//#endregion
|
|
20
|
+
export { HttpError as t };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import { i as
|
|
2
|
+
import { t as HttpError } from "./http-error-DzyrsLAZ.mjs";
|
|
3
|
+
import { a as isRecord, i as errorMessage, r as sendErrorMessage, t as createRestClient } from "./rest-BY2nADw5.mjs";
|
|
4
|
+
import { i as billingBlockedOutcomeFromSendResponse, n as BillingBlockedError } from "./billing-blocked-2wju4gC_.mjs";
|
|
4
5
|
import path from "node:path";
|
|
5
6
|
import Conf from "conf";
|
|
6
7
|
import { err, ok } from "neverthrow";
|
|
@@ -573,7 +574,7 @@ async function runPrint({ appUrl, sessionToken, prompt, agentSelector, conversat
|
|
|
573
574
|
if (machineShare) {
|
|
574
575
|
if (!machineShare.isGranted(agent.id) && grantTargetAgent) await machineShare.grantAgent(agent.id);
|
|
575
576
|
if (machineShare.isGranted(agent.id)) console.error(`portal: shared this machine with ${agent.name} for this run (grant persists until revoked)`);
|
|
576
|
-
else console.error(`portal: this machine is shared (shareMachineDefault), but ${agent.name} has no grant
|
|
577
|
+
else console.error(`portal: this machine is shared (shareMachineDefault), but ${agent.name} has no grant. Approve its request, or run \`skydive portal grant --agent ${agent.name}\`.`);
|
|
577
578
|
}
|
|
578
579
|
let send;
|
|
579
580
|
try {
|
|
@@ -737,7 +738,7 @@ function resolveAgent(agents, selector) {
|
|
|
737
738
|
const [only, ...rest] = agents;
|
|
738
739
|
if (!only) throw new Error("No agents on this account.");
|
|
739
740
|
if (rest.length === 0) return only;
|
|
740
|
-
throw new Error(`Multiple agents on this account
|
|
741
|
+
throw new Error(`Multiple agents on this account. Pass --agent <id|slug|name>. Candidates:\n${formatCandidates(agents)}`);
|
|
741
742
|
}
|
|
742
743
|
const byId = agents.find((a) => a.id === selector);
|
|
743
744
|
if (byId) return byId;
|
|
@@ -745,7 +746,7 @@ function resolveAgent(agents, selector) {
|
|
|
745
746
|
const matches = agents.filter((a) => a.slug && a.slug.toLowerCase() === needle || a.name.toLowerCase() === needle);
|
|
746
747
|
const [firstMatch, ...restMatches] = matches;
|
|
747
748
|
if (firstMatch && restMatches.length === 0) return firstMatch;
|
|
748
|
-
if (restMatches.length > 0) throw new Error(`Multiple agents match "${selector}"
|
|
749
|
+
if (restMatches.length > 0) throw new Error(`Multiple agents match "${selector}". Pass the id instead. Candidates:\n${formatCandidates(matches)}`);
|
|
749
750
|
throw new Error(`No agent matches "${selector}". Candidates:\n${formatCandidates(agents)}`);
|
|
750
751
|
}
|
|
751
752
|
function formatCandidates(agents) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { a as runPrint, i as resolveAgent, n as messageGet, o as toPrintError, r as readStdin, t as collectRunText } from "./print-
|
|
3
|
-
import "./rest-
|
|
4
|
-
import "./billing-blocked-
|
|
2
|
+
import { a as runPrint, i as resolveAgent, n as messageGet, o as toPrintError, r as readStdin, t as collectRunText } from "./print-CbayCa87.mjs";
|
|
3
|
+
import "./rest-BY2nADw5.mjs";
|
|
4
|
+
import "./billing-blocked-2wju4gC_.mjs";
|
|
5
5
|
|
|
6
6
|
export { messageGet, readStdin, resolveAgent, runPrint };
|
|
@@ -14,7 +14,7 @@ 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-
|
|
17
|
+
const { PortalClient } = await import("./client-CneLVPei.mjs");
|
|
18
18
|
let signalConnected;
|
|
19
19
|
const connected = new Promise((resolve) => {
|
|
20
20
|
signalConnected = resolve;
|
|
@@ -25,13 +25,13 @@ async function connectMachineShare({ appUrl, sessionToken, timeoutHint }) {
|
|
|
25
25
|
resolveCwd: () => process.cwd(),
|
|
26
26
|
onState: (state) => {
|
|
27
27
|
if (state.status === "connected") signalConnected();
|
|
28
|
-
if (state.status === "error") console.error(`portal: connection error: ${state.error ?? "unknown"}
|
|
28
|
+
if (state.status === "error") console.error(`portal: connection error: ${state.error ?? "unknown"}; retrying`);
|
|
29
29
|
}
|
|
30
30
|
});
|
|
31
31
|
machineShare.enable();
|
|
32
32
|
if (await Promise.race([connected.then(() => false), new Promise((resolve) => setTimeout(() => resolve(true), 3e4).unref())])) {
|
|
33
33
|
machineShare.dispose();
|
|
34
|
-
printError(`Could not connect the portal within 30s
|
|
34
|
+
printError(`Could not connect the portal within 30s. Machine sharing is unavailable (network, or the portal kill switch is off). ${timeoutHint ?? "Check `skydive portal status`."}`);
|
|
35
35
|
process.exit(1);
|
|
36
36
|
}
|
|
37
37
|
return machineShare;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { S as getConfigPath } from "./print-
|
|
2
|
+
import { S as getConfigPath } from "./print-CbayCa87.mjs";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { err, ok } from "neverthrow";
|
|
@@ -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.
|
|
11
|
+
var version$1 = "0.4.1-beta.2";
|
|
12
12
|
|
|
13
13
|
//#endregion
|
|
14
14
|
//#region src/auth/organization.ts
|
|
@@ -1057,4 +1057,211 @@ function printFatalNotice(file, memory) {
|
|
|
1057
1057
|
}
|
|
1058
1058
|
|
|
1059
1059
|
//#endregion
|
|
1060
|
-
|
|
1060
|
+
//#region src/profiling/profiler.ts
|
|
1061
|
+
/**
|
|
1062
|
+
* Opt-in session profiling. When SKYDIVE_PROFILE is set, an invocation
|
|
1063
|
+
* records everything needed to reconstruct what happened performance-wise —
|
|
1064
|
+
* designed so the whole directory can be handed to an agent (or a human)
|
|
1065
|
+
* and read without special tooling:
|
|
1066
|
+
*
|
|
1067
|
+
* network.ndjson every fetch: method, url, status, timing, content type
|
|
1068
|
+
* state.ndjson TUI state transitions (screen changes, store updates)
|
|
1069
|
+
* commits.ndjson React commits: which subtree rendered, when, how long
|
|
1070
|
+
* render.ndjson OpenTUI renderer samples: fps, frame times, cells drawn
|
|
1071
|
+
* render.json final renderer stats dump (full frame-time series)
|
|
1072
|
+
* cpu-<pid>.cpuprofile V8 CPU profile (Node-run commands only)
|
|
1073
|
+
* meta-<pid>.json argv, versions, runtime, exit code, wall time
|
|
1074
|
+
*
|
|
1075
|
+
* Every ndjson event carries a wall-clock `t` (epoch ms) and `pid`, so
|
|
1076
|
+
* records from the Node parent and the Bun-re-exec'd chat TUI land in the
|
|
1077
|
+
* same files and stay correlatable on one clock. The profile directory is
|
|
1078
|
+
* created by the first process and shared with children through
|
|
1079
|
+
* SKYDIVE_PROFILE_DIR (the chat re-exec inherits the environment).
|
|
1080
|
+
*
|
|
1081
|
+
* Inert unless SKYDIVE_PROFILE is set: no patched fetch, no subscriptions,
|
|
1082
|
+
* no inspector session.
|
|
1083
|
+
*/
|
|
1084
|
+
const ENV_FLAG = "SKYDIVE_PROFILE";
|
|
1085
|
+
const ENV_DIR = "SKYDIVE_PROFILE_DIR";
|
|
1086
|
+
let activeDir = null;
|
|
1087
|
+
let startedAtMs = 0;
|
|
1088
|
+
/**
|
|
1089
|
+
* Buffered event lines per stream, flushed asynchronously. Events are
|
|
1090
|
+
* appended to an in-memory buffer and written with fs.promises off the
|
|
1091
|
+
* hot path, so recording never blocks the TUI's event loop — high-rate
|
|
1092
|
+
* streams (React commits, renderer samples) stay cheap. Whatever is
|
|
1093
|
+
* still buffered when the process exits is drained synchronously in the
|
|
1094
|
+
* exit handler, where async I/O would never flush.
|
|
1095
|
+
*/
|
|
1096
|
+
const pendingLines = /* @__PURE__ */ new Map();
|
|
1097
|
+
let flushScheduled = false;
|
|
1098
|
+
let flushing = Promise.resolve();
|
|
1099
|
+
function drainBuffersSync() {
|
|
1100
|
+
if (activeDir === null) return;
|
|
1101
|
+
for (const [stream, lines] of pendingLines) {
|
|
1102
|
+
if (lines.length === 0) continue;
|
|
1103
|
+
pendingLines.set(stream, []);
|
|
1104
|
+
try {
|
|
1105
|
+
fs.appendFileSync(path.join(activeDir, `${stream}.ndjson`), lines.join("\n") + "\n");
|
|
1106
|
+
} catch (_error) {}
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
function scheduleFlush() {
|
|
1110
|
+
if (flushScheduled || activeDir === null) return;
|
|
1111
|
+
flushScheduled = true;
|
|
1112
|
+
setTimeout(() => {
|
|
1113
|
+
flushScheduled = false;
|
|
1114
|
+
flushing = flushing.then(async () => {
|
|
1115
|
+
if (activeDir === null) return;
|
|
1116
|
+
for (const [stream, lines] of pendingLines) {
|
|
1117
|
+
if (lines.length === 0) continue;
|
|
1118
|
+
pendingLines.set(stream, []);
|
|
1119
|
+
try {
|
|
1120
|
+
await fs.promises.appendFile(path.join(activeDir, `${stream}.ndjson`), lines.join("\n") + "\n");
|
|
1121
|
+
} catch (_error) {}
|
|
1122
|
+
}
|
|
1123
|
+
});
|
|
1124
|
+
}, 100).unref?.();
|
|
1125
|
+
}
|
|
1126
|
+
function profilingEnabled() {
|
|
1127
|
+
const v = process.env[ENV_FLAG];
|
|
1128
|
+
return v !== void 0 && v !== "" && v !== "0";
|
|
1129
|
+
}
|
|
1130
|
+
function sanitizeToken(token) {
|
|
1131
|
+
return token.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 40);
|
|
1132
|
+
}
|
|
1133
|
+
/**
|
|
1134
|
+
* Appends one event to a stream file. Buffered and written asynchronously
|
|
1135
|
+
* so recording never blocks the event loop; the exit handler drains any
|
|
1136
|
+
* remainder synchronously so abrupt exits still keep their events.
|
|
1137
|
+
*/
|
|
1138
|
+
function record(stream, event) {
|
|
1139
|
+
if (activeDir === null) return;
|
|
1140
|
+
const line = JSON.stringify({
|
|
1141
|
+
t: Date.now(),
|
|
1142
|
+
pid: process.pid,
|
|
1143
|
+
...event
|
|
1144
|
+
});
|
|
1145
|
+
const lines = pendingLines.get(stream);
|
|
1146
|
+
if (lines === void 0) pendingLines.set(stream, [line]);
|
|
1147
|
+
else lines.push(line);
|
|
1148
|
+
scheduleFlush();
|
|
1149
|
+
}
|
|
1150
|
+
/** Writes a JSON artifact (non-append) into the profile directory. */
|
|
1151
|
+
function writeArtifact(name, data) {
|
|
1152
|
+
if (activeDir === null) return;
|
|
1153
|
+
try {
|
|
1154
|
+
fs.writeFileSync(path.join(activeDir, name), JSON.stringify(data, null, 2));
|
|
1155
|
+
} catch (_error) {}
|
|
1156
|
+
}
|
|
1157
|
+
/**
|
|
1158
|
+
* Patches globalThis.fetch to record request timing. A wrapper (not
|
|
1159
|
+
* undici's diagnostics_channel) because chat re-execs under Bun, where
|
|
1160
|
+
* fetch is Bun-native and undici events never fire; the wrapper behaves
|
|
1161
|
+
* identically in both runtimes.
|
|
1162
|
+
*/
|
|
1163
|
+
function installFetchRecorder() {
|
|
1164
|
+
const original = globalThis.fetch;
|
|
1165
|
+
const wrapped = async (...args) => {
|
|
1166
|
+
const [input, init] = args;
|
|
1167
|
+
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
1168
|
+
const method = init?.method ?? (input instanceof Request ? input.method : "GET");
|
|
1169
|
+
const started = Date.now();
|
|
1170
|
+
try {
|
|
1171
|
+
const res = await original(...args);
|
|
1172
|
+
const contentType = res.headers.get("content-type") ?? "";
|
|
1173
|
+
record("network", {
|
|
1174
|
+
method,
|
|
1175
|
+
url,
|
|
1176
|
+
status: res.status,
|
|
1177
|
+
durationMs: Date.now() - started,
|
|
1178
|
+
contentType,
|
|
1179
|
+
streamed: contentType.includes("text/event-stream")
|
|
1180
|
+
});
|
|
1181
|
+
return res;
|
|
1182
|
+
} catch (err) {
|
|
1183
|
+
record("network", {
|
|
1184
|
+
method,
|
|
1185
|
+
url,
|
|
1186
|
+
status: 0,
|
|
1187
|
+
durationMs: Date.now() - started,
|
|
1188
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1189
|
+
});
|
|
1190
|
+
throw err;
|
|
1191
|
+
}
|
|
1192
|
+
};
|
|
1193
|
+
globalThis.fetch = Object.assign(wrapped, original);
|
|
1194
|
+
}
|
|
1195
|
+
function isBunRuntime() {
|
|
1196
|
+
return typeof process !== "undefined" && "bun" in process.versions;
|
|
1197
|
+
}
|
|
1198
|
+
/**
|
|
1199
|
+
* Starts the V8 CPU profiler via node:inspector. Node-run commands only —
|
|
1200
|
+
* Bun does not implement the inspector Profiler domain, so the chat TUI
|
|
1201
|
+
* skips this artifact.
|
|
1202
|
+
*/
|
|
1203
|
+
function startCpuProfile() {
|
|
1204
|
+
if (isBunRuntime()) return;
|
|
1205
|
+
(async () => {
|
|
1206
|
+
try {
|
|
1207
|
+
const { Session } = await import("node:inspector/promises");
|
|
1208
|
+
const session = new Session();
|
|
1209
|
+
session.connect();
|
|
1210
|
+
await session.post("Profiler.enable");
|
|
1211
|
+
await session.post("Profiler.start");
|
|
1212
|
+
process.once("beforeExit", () => {
|
|
1213
|
+
(async () => {
|
|
1214
|
+
try {
|
|
1215
|
+
const { profile } = await session.post("Profiler.stop");
|
|
1216
|
+
writeArtifact(`cpu-${process.pid}.cpuprofile`, profile);
|
|
1217
|
+
session.disconnect();
|
|
1218
|
+
} catch (_error) {}
|
|
1219
|
+
})();
|
|
1220
|
+
});
|
|
1221
|
+
} catch (_error) {}
|
|
1222
|
+
})();
|
|
1223
|
+
}
|
|
1224
|
+
/**
|
|
1225
|
+
* Activates profiling for this process if SKYDIVE_PROFILE is set. Creates
|
|
1226
|
+
* the profile directory (or joins the one a parent process created),
|
|
1227
|
+
* patches fetch, starts the CPU profiler, and registers the exit-time
|
|
1228
|
+
* meta dump. Call once, as early as possible.
|
|
1229
|
+
*/
|
|
1230
|
+
function maybeStartProfiling(argv, cliVersion) {
|
|
1231
|
+
if (!profilingEnabled() || activeDir !== null) return;
|
|
1232
|
+
const inherited = process.env[ENV_DIR];
|
|
1233
|
+
if (inherited !== void 0 && inherited !== "") activeDir = inherited;
|
|
1234
|
+
else {
|
|
1235
|
+
const flagValue = process.env[ENV_FLAG] ?? "1";
|
|
1236
|
+
const command = argv.find((a) => !a.startsWith("-")) ?? "chat";
|
|
1237
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").replace("T", "_").slice(0, 19);
|
|
1238
|
+
activeDir = flagValue === "1" || flagValue.toLowerCase() === "true" ? path.resolve(process.cwd(), `skydive-profile-${sanitizeToken(command)}-${stamp}`) : path.resolve(process.cwd(), flagValue);
|
|
1239
|
+
process.env[ENV_DIR] = activeDir;
|
|
1240
|
+
}
|
|
1241
|
+
try {
|
|
1242
|
+
fs.mkdirSync(activeDir, { recursive: true });
|
|
1243
|
+
} catch (_error) {
|
|
1244
|
+
activeDir = null;
|
|
1245
|
+
return;
|
|
1246
|
+
}
|
|
1247
|
+
startedAtMs = Date.now();
|
|
1248
|
+
installFetchRecorder();
|
|
1249
|
+
startCpuProfile();
|
|
1250
|
+
process.on("exit", (code) => {
|
|
1251
|
+
drainBuffersSync();
|
|
1252
|
+
writeArtifact(`meta-${process.pid}.json`, {
|
|
1253
|
+
argv,
|
|
1254
|
+
cliVersion,
|
|
1255
|
+
runtime: isBunRuntime() ? `bun ${process.versions["bun"]}` : `node ${process.version}`,
|
|
1256
|
+
platform: process.platform,
|
|
1257
|
+
pid: process.pid,
|
|
1258
|
+
exitCode: code,
|
|
1259
|
+
startedAt: new Date(startedAtMs).toISOString(),
|
|
1260
|
+
wallTimeMs: Date.now() - startedAtMs
|
|
1261
|
+
});
|
|
1262
|
+
if (process.env[ENV_DIR] === activeDir && inherited === void 0) process.stderr.write(`\nprofile written to ${activeDir}\n`);
|
|
1263
|
+
});
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
//#endregion
|
|
1267
|
+
export { ensureActiveOrganization as C, setActiveWorkspace as D, listWorkspaces as E, name as O, themesForMode as S, getSessionIdentity as T, themeForMode as _, installCrashHandler as a, themeVersion as b, MARK_CELLS as c, DEFAULT_THEME_ID as d, applyTheme as f, theme as g, noColorRequested as h, writeArtifact as i, version$1 as k, WORDMARK as l, monoTheme as m, profilingEnabled as n, buildCrashReport as o, findTheme as p, record as r, writeCrashReport as s, maybeStartProfiling as t, brandHelpArt as u, themeMode as v, getActiveWorkspaceId as w, themes as x, themeModeFromColorFgBg as y };
|