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.
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
- import { a as errorMessage, t as HttpError } from "./rest-imDZZGQA.mjs";
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/chat/portal/machine.ts
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/chat/portal/exec.ts
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/chat/portal/api.ts
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/chat/portal/client.ts
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, machineIdentity as s, PortalClient as t };
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 };
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ import { t as PortalClient } from "./client-Cf7cvftg.mjs";
3
+
4
+ export { PortalClient };
@@ -1,6 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { o as isRecord } from "./rest-imDZZGQA.mjs";
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/chat/portal/local-protocol.ts
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/chat/portal/daemon.ts
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,6 @@
1
+ #!/usr/bin/env node
2
+ import "./client-Cf7cvftg.mjs";
3
+ import "./daemon-DaUaCMwC.mjs";
4
+ import { t as PortalDaemonClient } from "./daemon-client-CJxbCLOb.mjs";
5
+
6
+ export { PortalDaemonClient };
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ import "./client-Cf7cvftg.mjs";
3
+ import { a as runPortalDaemon, i as queryDaemonStatus, n as ensureDaemonRunning, o as stopDaemon, r as isDaemonListening, t as PortalDaemon } from "./daemon-DaUaCMwC.mjs";
4
+
5
+ export { runPortalDaemon };
@@ -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 { a as errorMessage, i as sendErrorMessage, n as createRestClient, o as isRecord, t as HttpError } from "./rest-imDZZGQA.mjs";
3
- import { i as billingBlockedOutcomeFromSendResponse, n as BillingBlockedError } from "./billing-blocked-CwfG1BTR.mjs";
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 approve its request, or run \`skydive portal grant --agent ${agent.name}\`.`);
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 pass --agent <id|slug|name>. Candidates:\n${formatCandidates(agents)}`);
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}" pass the id instead. Candidates:\n${formatCandidates(matches)}`);
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-X2N5wJ5l.mjs";
3
- import "./rest-imDZZGQA.mjs";
4
- import "./billing-blocked-CwfG1BTR.mjs";
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-D6apKmV0.mjs");
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"} retrying`);
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 machine sharing is unavailable (network, or the portal kill switch is off). ${timeoutHint ?? "Check `skydive portal status`."}`);
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-X2N5wJ5l.mjs";
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.0";
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
- export { setActiveWorkspace as C, listWorkspaces as S, version$1 as T, themes as _, WORDMARK as a, getActiveWorkspaceId as b, applyTheme as c, noColorRequested as d, theme as f, themeVersion as g, themeModeFromColorFgBg as h, MARK_CELLS as i, findTheme as l, themeMode as m, buildCrashReport as n, brandHelpArt as o, themeForMode as p, writeCrashReport as r, DEFAULT_THEME_ID as s, installCrashHandler as t, monoTheme as u, themesForMode as v, name as w, getSessionIdentity as x, ensureActiveOrganization as y };
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 };
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ import "./client-Cn2af31H.mjs";
3
+ import { t as runRawPtyPassthrough } from "./raw-pty-B6mAroiI.mjs";
4
+
5
+ export { runRawPtyPassthrough };
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { t as SandboxStream } from "./client-D8s9vY4p.mjs";
2
+ import { t as SandboxStream } from "./client-Cn2af31H.mjs";
3
3
 
4
4
  //#region src/chat/sandbox/raw-pty.ts
5
5
  /**