skydive-cli 0.5.0-beta.32 → 0.5.0-beta.36

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,6 +1,108 @@
1
1
  #!/usr/bin/env node
2
2
  import { WebSocket } from "ws";
3
3
 
4
+ //#region ../sandbox-stream-protocol/src/fs.ts
5
+ /** fs frame type bytes. Disjoint from FRAME.* in ./index.ts. */
6
+ const FS_FRAME = {
7
+ REQ: 32,
8
+ RES: 33
9
+ };
10
+ /** Filesystem operations the channel supports. One byte on the wire. */
11
+ const FS_OP = {
12
+ LIST: 1,
13
+ STAT: 2,
14
+ READ: 3,
15
+ WRITE: 4,
16
+ MKDIR: 5,
17
+ RENAME: 6,
18
+ REMOVE: 7,
19
+ EXISTS: 8
20
+ };
21
+ /** Reply status. OK carries a result; ERR carries a message in `json.message`. */
22
+ const FS_STATUS = {
23
+ OK: 0,
24
+ ERR: 1
25
+ };
26
+ const FS_MAX_BLOB_BYTES = 8 * 1024 * 1024;
27
+ const OP_TO_CODE = {
28
+ list: FS_OP.LIST,
29
+ stat: FS_OP.STAT,
30
+ read: FS_OP.READ,
31
+ write: FS_OP.WRITE,
32
+ mkdir: FS_OP.MKDIR,
33
+ rename: FS_OP.RENAME,
34
+ remove: FS_OP.REMOVE,
35
+ exists: FS_OP.EXISTS
36
+ };
37
+ const CODE_TO_OP = new Map(Object.entries(OP_TO_CODE).map(([op, code]) => [code, op]));
38
+ const textEncoder = new TextEncoder();
39
+ const textDecoder = new TextDecoder();
40
+ function frame(type, reqId, byte2, json, blob) {
41
+ const jsonBytes = textEncoder.encode(JSON.stringify(json ?? {}));
42
+ const out = new Uint8Array(10 + jsonBytes.length + blob.length);
43
+ const dv = new DataView(out.buffer);
44
+ out[0] = type;
45
+ dv.setUint32(1, reqId >>> 0);
46
+ out[5] = byte2;
47
+ dv.setUint32(6, jsonBytes.length);
48
+ out.set(jsonBytes, 10);
49
+ out.set(blob, 10 + jsonBytes.length);
50
+ return out;
51
+ }
52
+ const EMPTY = new Uint8Array(0);
53
+ /** client → server: encode an fs request. `blob` is the write payload, or null. */
54
+ function encodeFsRequest(reqId, req, blob) {
55
+ return frame(FS_FRAME.REQ, reqId, OP_TO_CODE[req.op], req, blob ?? EMPTY);
56
+ }
57
+ /**
58
+ * Decode a server fs reply frame. Returns null for a malformed frame so a peer
59
+ * on a newer protocol can't crash the client.
60
+ */
61
+ function decodeFsResponse(frameBytes) {
62
+ const parsed = parseFrame(FS_FRAME.RES, frameBytes);
63
+ if (!parsed) return null;
64
+ if (parsed.byte2 === FS_STATUS.ERR) {
65
+ const message = typeof parsed.json.message === "string" ? parsed.json.message : "fs operation failed";
66
+ return {
67
+ reqId: parsed.reqId,
68
+ status: "error",
69
+ message
70
+ };
71
+ }
72
+ return {
73
+ reqId: parsed.reqId,
74
+ status: "ok",
75
+ result: parsed.json,
76
+ blob: parsed.blob
77
+ };
78
+ }
79
+ function parseFrame(expectedType, frameBytes) {
80
+ if (frameBytes.length < 10) return null;
81
+ if (frameBytes[0] !== expectedType) return null;
82
+ const dv = new DataView(frameBytes.buffer, frameBytes.byteOffset, frameBytes.byteLength);
83
+ const reqId = dv.getUint32(1);
84
+ const byte2 = frameBytes[5] ?? 0;
85
+ const jsonLen = dv.getUint32(6);
86
+ const jsonStart = 10;
87
+ const jsonEnd = jsonStart + jsonLen;
88
+ if (jsonEnd > frameBytes.length) return null;
89
+ let json;
90
+ try {
91
+ const parsed = jsonLen ? JSON.parse(textDecoder.decode(frameBytes.subarray(jsonStart, jsonEnd))) : {};
92
+ if (typeof parsed !== "object" || parsed === null) return null;
93
+ json = parsed;
94
+ } catch {
95
+ return null;
96
+ }
97
+ return {
98
+ reqId,
99
+ byte2,
100
+ json,
101
+ blob: frameBytes.subarray(jsonEnd)
102
+ };
103
+ }
104
+
105
+ //#endregion
4
106
  //#region ../sandbox-stream-protocol/src/index.ts
5
107
  const SANDBOX_STREAM_PATH = "/api/v1/sandbox/stream";
6
108
  const FRAME = {
@@ -19,11 +121,15 @@ function streamSpecToQuery(spec) {
19
121
  cols: String(spec.cols),
20
122
  rows: String(spec.rows)
21
123
  };
22
- return {
124
+ if (spec.mode === "exec") return {
23
125
  agentId: spec.agentId,
24
126
  mode: "exec",
25
127
  command: spec.command
26
128
  };
129
+ return {
130
+ agentId: spec.agentId,
131
+ mode: "fs"
132
+ };
27
133
  }
28
134
  function withType(type, payload) {
29
135
  const frame = new Uint8Array(1 + payload.length);
@@ -166,4 +272,4 @@ function toBuffer(data) {
166
272
  }
167
273
 
168
274
  //#endregion
169
- export { SandboxStream as t };
275
+ export { encodeFsRequest as a, decodeFsResponse as i, SANDBOX_STREAM_PATH as n, streamSpecToQuery as r, SandboxStream as t };
@@ -21,7 +21,7 @@ import { access, appendFile, mkdir, readFile, unlink, writeFile } from "node:fs/
21
21
  * the commit time of the built tree is one monotonic clock they share.
22
22
  */
23
23
  function portalDaemonBuild() {
24
- return "1786765910";
24
+ return "1786860400";
25
25
  }
26
26
  /**
27
27
  * Whether a client carrying `mine` should replace a running daemon carrying
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import "./client-d6K9qm6B.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-CxFxH36O.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-CH95gDij.mjs";
4
4
  import "./tls-cert-CV-pwxVN.mjs";
5
5
  import "./api-DQCaztBg.mjs";
6
6
 
@@ -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-CxFxH36O.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-CH95gDij.mjs";
3
3
  import { randomUUID } from "node:crypto";
4
4
  import { connect } from "node:net";
5
5
 
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ import "./client-d6K9qm6B.mjs";
3
+ import "./daemon-CH95gDij.mjs";
4
+ import "./tls-cert-CV-pwxVN.mjs";
5
+ import "./api-DQCaztBg.mjs";
6
+ import { t as PortalDaemonClient } from "./daemon-client-BNDnEGw6.mjs";
7
+
8
+ export { PortalDaemonClient };
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { C as getConfigPath } from "./print-Clgq46GU.mjs";
2
+ import { C as getConfigPath } from "./print-h22UbIfZ.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.5.0-beta.32";
11
+ var version$1 = "0.5.0-beta.36";
12
12
 
13
13
  //#endregion
14
14
  //#region src/auth/organization.ts
@@ -1050,6 +1050,277 @@ function applyTheme(def) {
1050
1050
  version++;
1051
1051
  }
1052
1052
 
1053
+ //#endregion
1054
+ //#region src/profiling/profiler.ts
1055
+ /**
1056
+ * Session diagnostics. Every invocation records enough context to reconstruct
1057
+ * what happened after the fact. SKYDIVE_PROFILE can select a custom directory
1058
+ * or disable recording with `0`.
1059
+ *
1060
+ * Default sessions live under the CLI's system state directory. Explicit
1061
+ * profiles keep the original cwd/custom-path behavior and add a CPU profile.
1062
+ * Each directory is designed to be handed to an agent (or a human) and read
1063
+ * without special tooling:
1064
+ *
1065
+ * network.ndjson every fetch: method, url, status, timing, content type
1066
+ * state.ndjson TUI state transitions (screen changes, store updates)
1067
+ * commits.ndjson React commits: which subtree rendered, when, how long
1068
+ * render.ndjson OpenTUI renderer samples: fps, frame times, cells drawn
1069
+ * render.json final renderer stats dump (full frame-time series)
1070
+ * cpu-<pid>.cpuprofile V8 CPU profile (Node-run commands only)
1071
+ * meta-<pid>.json argv, versions, runtime, exit code, wall time
1072
+ *
1073
+ * Every ndjson event carries a wall-clock `t` (epoch ms) and `pid`, so
1074
+ * records from the Node parent and the Bun-re-exec'd chat TUI land in the
1075
+ * same files and stay correlatable on one clock. The profile directory is
1076
+ * created by the first process and shared with children through
1077
+ * SKYDIVE_PROFILE_DIR (the chat re-exec inherits the environment).
1078
+ *
1079
+ */
1080
+ const ENV_FLAG = "SKYDIVE_PROFILE";
1081
+ const ENV_DIR = "SKYDIVE_PROFILE_DIR";
1082
+ const DEFAULT_SESSION_LIMIT = 20;
1083
+ let activeDir = null;
1084
+ let startedAtMs = 0;
1085
+ /**
1086
+ * Buffered event lines per stream, flushed asynchronously. Events are
1087
+ * appended to an in-memory buffer and written with fs.promises off the
1088
+ * hot path, so recording never blocks the TUI's event loop — high-rate
1089
+ * streams (React commits, renderer samples) stay cheap. Whatever is
1090
+ * still buffered when the process exits is drained synchronously in the
1091
+ * exit handler, where async I/O would never flush.
1092
+ */
1093
+ const pendingLines = /* @__PURE__ */ new Map();
1094
+ let flushScheduled = false;
1095
+ let flushing = Promise.resolve();
1096
+ function drainBuffersSync() {
1097
+ if (activeDir === null) return;
1098
+ for (const [stream, lines] of pendingLines) {
1099
+ if (lines.length === 0) continue;
1100
+ pendingLines.set(stream, []);
1101
+ try {
1102
+ fs.appendFileSync(path.join(activeDir, `${stream}.ndjson`), lines.join("\n") + "\n");
1103
+ } catch (_error) {}
1104
+ }
1105
+ }
1106
+ function scheduleFlush() {
1107
+ if (flushScheduled || activeDir === null) return;
1108
+ flushScheduled = true;
1109
+ setTimeout(() => {
1110
+ flushScheduled = false;
1111
+ flushing = flushing.then(async () => {
1112
+ if (activeDir === null) return;
1113
+ for (const [stream, lines] of pendingLines) {
1114
+ if (lines.length === 0) continue;
1115
+ pendingLines.set(stream, []);
1116
+ try {
1117
+ await fs.promises.appendFile(path.join(activeDir, `${stream}.ndjson`), lines.join("\n") + "\n");
1118
+ } catch (_error) {}
1119
+ }
1120
+ });
1121
+ }, 100).unref?.();
1122
+ }
1123
+ function profilingEnabled() {
1124
+ return process.env[ENV_FLAG] !== "0";
1125
+ }
1126
+ function explicitProfilingEnabled() {
1127
+ const value = process.env[ENV_FLAG];
1128
+ return value !== void 0 && value !== "" && value !== "0";
1129
+ }
1130
+ function profileDir() {
1131
+ return activeDir;
1132
+ }
1133
+ function automaticLogsDir(platform, env, home) {
1134
+ const appName = env["SKYDIVE_CONFIG_NAME"] ?? "skydive";
1135
+ if (platform === "darwin") return path.join(home, "Library", "Logs", appName);
1136
+ if (platform === "linux") {
1137
+ const stateHome = env["XDG_STATE_HOME"] || path.join(home, ".local", "state");
1138
+ return path.join(stateHome, appName);
1139
+ }
1140
+ return path.join(path.dirname(getConfigPath()), "logs");
1141
+ }
1142
+ function getAutomaticLogsDir() {
1143
+ return automaticLogsDir(process.platform, process.env, os.homedir());
1144
+ }
1145
+ function sanitizeToken(token) {
1146
+ return token.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 40);
1147
+ }
1148
+ function commandName(argv) {
1149
+ return argv.find((arg) => !arg.startsWith("-")) ?? "chat";
1150
+ }
1151
+ function sessionName(argv) {
1152
+ const command = commandName(argv);
1153
+ return `${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").replace("T", "_").slice(0, 19)}-${sanitizeToken(command)}-${process.pid}`;
1154
+ }
1155
+ /**
1156
+ * Automatic logs retain command shape, not values. Positional arguments and
1157
+ * flag values can contain prompts, secrets, paths, and other private data.
1158
+ */
1159
+ function safeAutomaticArgv(argv) {
1160
+ return [commandName(argv), ...argv.filter((arg) => arg.startsWith("-")).map((arg) => arg.split("=", 1)[0] ?? arg)];
1161
+ }
1162
+ /** Keeps automatic diagnostics bounded without touching explicit profiles. */
1163
+ function pruneAutomaticSessions(root, activeSession) {
1164
+ try {
1165
+ const sessions = fs.readdirSync(root, { withFileTypes: true }).filter((entry) => entry.isDirectory() && entry.name !== activeSession).map((entry) => entry.name).sort().reverse();
1166
+ for (const stale of sessions.slice(DEFAULT_SESSION_LIMIT - 1)) fs.rmSync(path.join(root, stale), {
1167
+ recursive: true,
1168
+ force: true
1169
+ });
1170
+ } catch (_error) {}
1171
+ }
1172
+ /**
1173
+ * Appends one event to a stream file. Buffered and written asynchronously
1174
+ * so recording never blocks the event loop; the exit handler drains any
1175
+ * remainder synchronously so abrupt exits still keep their events.
1176
+ */
1177
+ function record(stream, event) {
1178
+ if (activeDir === null) return;
1179
+ const line = JSON.stringify({
1180
+ t: Date.now(),
1181
+ pid: process.pid,
1182
+ ...event
1183
+ });
1184
+ const lines = pendingLines.get(stream);
1185
+ if (lines === void 0) pendingLines.set(stream, [line]);
1186
+ else lines.push(line);
1187
+ scheduleFlush();
1188
+ }
1189
+ /** Writes a JSON artifact (non-append) into the profile directory. */
1190
+ function writeArtifact(name, data) {
1191
+ if (activeDir === null) return;
1192
+ try {
1193
+ fs.writeFileSync(path.join(activeDir, name), JSON.stringify(data, null, 2));
1194
+ } catch (_error) {}
1195
+ }
1196
+ function safeAutomaticUrl(value) {
1197
+ try {
1198
+ const url = new URL(value);
1199
+ for (const key of url.searchParams.keys()) url.searchParams.set(key, "<redacted>");
1200
+ url.hash = "";
1201
+ return url.href;
1202
+ } catch (_error) {
1203
+ return "<invalid-url>";
1204
+ }
1205
+ }
1206
+ /**
1207
+ * Patches globalThis.fetch to record request timing. A wrapper (not
1208
+ * undici's diagnostics_channel) because chat re-execs under Bun, where
1209
+ * fetch is Bun-native and undici events never fire; the wrapper behaves
1210
+ * identically in both runtimes.
1211
+ */
1212
+ function installFetchRecorder(explicit) {
1213
+ const original = globalThis.fetch;
1214
+ const wrapped = async (...args) => {
1215
+ const [input, init] = args;
1216
+ const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
1217
+ const method = init?.method ?? (input instanceof Request ? input.method : "GET");
1218
+ const started = Date.now();
1219
+ try {
1220
+ const res = await original(...args);
1221
+ const contentType = res.headers.get("content-type") ?? "";
1222
+ record("network", {
1223
+ method,
1224
+ url: explicit ? url : safeAutomaticUrl(url),
1225
+ status: res.status,
1226
+ durationMs: Date.now() - started,
1227
+ contentType,
1228
+ streamed: contentType.includes("text/event-stream")
1229
+ });
1230
+ return res;
1231
+ } catch (err) {
1232
+ record("network", {
1233
+ method,
1234
+ url: explicit ? url : safeAutomaticUrl(url),
1235
+ status: 0,
1236
+ durationMs: Date.now() - started,
1237
+ error: err instanceof Error ? err.message : String(err)
1238
+ });
1239
+ throw err;
1240
+ }
1241
+ };
1242
+ globalThis.fetch = Object.assign(wrapped, original);
1243
+ }
1244
+ function isBunRuntime() {
1245
+ return typeof process !== "undefined" && "bun" in process.versions;
1246
+ }
1247
+ /**
1248
+ * Starts the V8 CPU profiler via node:inspector. Node-run commands only —
1249
+ * Bun does not implement the inspector Profiler domain, so the chat TUI
1250
+ * skips this artifact.
1251
+ */
1252
+ function startCpuProfile() {
1253
+ if (isBunRuntime()) return;
1254
+ (async () => {
1255
+ try {
1256
+ const { Session } = await import("node:inspector/promises");
1257
+ const session = new Session();
1258
+ session.connect();
1259
+ await session.post("Profiler.enable");
1260
+ await session.post("Profiler.start");
1261
+ process.once("beforeExit", () => {
1262
+ (async () => {
1263
+ try {
1264
+ const { profile } = await session.post("Profiler.stop");
1265
+ writeArtifact(`cpu-${process.pid}.cpuprofile`, profile);
1266
+ session.disconnect();
1267
+ } catch (_error) {}
1268
+ })();
1269
+ });
1270
+ } catch (_error) {}
1271
+ })();
1272
+ }
1273
+ /**
1274
+ * Starts diagnostics unless SKYDIVE_PROFILE=0. Creates a system-state session
1275
+ * directory (or joins the one a parent process created), patches fetch, and
1276
+ * registers the exit-time metadata dump. Explicit profiles also capture CPU.
1277
+ */
1278
+ function maybeStartProfiling(argv, cliVersion) {
1279
+ if (!profilingEnabled() || activeDir !== null) return;
1280
+ const inherited = process.env[ENV_DIR];
1281
+ const explicit = explicitProfilingEnabled();
1282
+ let automaticRoot = null;
1283
+ let automaticSession = null;
1284
+ if (inherited !== void 0 && inherited !== "") activeDir = inherited;
1285
+ else if (explicit) {
1286
+ const flagValue = process.env[ENV_FLAG] ?? "1";
1287
+ activeDir = flagValue === "1" || flagValue.toLowerCase() === "true" ? path.resolve(process.cwd(), `skydive-profile-${sessionName(argv)}`) : path.resolve(process.cwd(), flagValue);
1288
+ process.env[ENV_DIR] = activeDir;
1289
+ } else {
1290
+ automaticRoot = getAutomaticLogsDir();
1291
+ automaticSession = sessionName(argv);
1292
+ activeDir = path.join(automaticRoot, automaticSession);
1293
+ process.env[ENV_DIR] = activeDir;
1294
+ }
1295
+ try {
1296
+ fs.mkdirSync(activeDir, {
1297
+ recursive: true,
1298
+ mode: 448
1299
+ });
1300
+ if (automaticRoot !== null && automaticSession !== null) pruneAutomaticSessions(automaticRoot, automaticSession);
1301
+ } catch (_error) {
1302
+ activeDir = null;
1303
+ return;
1304
+ }
1305
+ startedAtMs = Date.now();
1306
+ installFetchRecorder(explicit);
1307
+ if (explicit) startCpuProfile();
1308
+ process.on("exit", (code) => {
1309
+ drainBuffersSync();
1310
+ writeArtifact(`meta-${process.pid}.json`, {
1311
+ argv: explicit ? argv : safeAutomaticArgv(argv),
1312
+ cliVersion,
1313
+ runtime: isBunRuntime() ? `bun ${process.versions["bun"]}` : `node ${process.version}`,
1314
+ platform: process.platform,
1315
+ pid: process.pid,
1316
+ exitCode: code,
1317
+ startedAt: new Date(startedAtMs).toISOString(),
1318
+ wallTimeMs: Date.now() - startedAtMs
1319
+ });
1320
+ if (explicit && process.env[ENV_DIR] === activeDir && inherited === void 0) process.stderr.write(`\nprofile written to ${activeDir}\n`);
1321
+ });
1322
+ }
1323
+
1053
1324
  //#endregion
1054
1325
  //#region src/brand.ts
1055
1326
  const MARK_CELLS = [
@@ -1311,211 +1582,4 @@ function printFatalNotice(file, memory) {
1311
1582
  }
1312
1583
 
1313
1584
  //#endregion
1314
- //#region src/profiling/profiler.ts
1315
- /**
1316
- * Opt-in session profiling. When SKYDIVE_PROFILE is set, an invocation
1317
- * records everything needed to reconstruct what happened performance-wise —
1318
- * designed so the whole directory can be handed to an agent (or a human)
1319
- * and read without special tooling:
1320
- *
1321
- * network.ndjson every fetch: method, url, status, timing, content type
1322
- * state.ndjson TUI state transitions (screen changes, store updates)
1323
- * commits.ndjson React commits: which subtree rendered, when, how long
1324
- * render.ndjson OpenTUI renderer samples: fps, frame times, cells drawn
1325
- * render.json final renderer stats dump (full frame-time series)
1326
- * cpu-<pid>.cpuprofile V8 CPU profile (Node-run commands only)
1327
- * meta-<pid>.json argv, versions, runtime, exit code, wall time
1328
- *
1329
- * Every ndjson event carries a wall-clock `t` (epoch ms) and `pid`, so
1330
- * records from the Node parent and the Bun-re-exec'd chat TUI land in the
1331
- * same files and stay correlatable on one clock. The profile directory is
1332
- * created by the first process and shared with children through
1333
- * SKYDIVE_PROFILE_DIR (the chat re-exec inherits the environment).
1334
- *
1335
- * Inert unless SKYDIVE_PROFILE is set: no patched fetch, no subscriptions,
1336
- * no inspector session.
1337
- */
1338
- const ENV_FLAG = "SKYDIVE_PROFILE";
1339
- const ENV_DIR = "SKYDIVE_PROFILE_DIR";
1340
- let activeDir = null;
1341
- let startedAtMs = 0;
1342
- /**
1343
- * Buffered event lines per stream, flushed asynchronously. Events are
1344
- * appended to an in-memory buffer and written with fs.promises off the
1345
- * hot path, so recording never blocks the TUI's event loop — high-rate
1346
- * streams (React commits, renderer samples) stay cheap. Whatever is
1347
- * still buffered when the process exits is drained synchronously in the
1348
- * exit handler, where async I/O would never flush.
1349
- */
1350
- const pendingLines = /* @__PURE__ */ new Map();
1351
- let flushScheduled = false;
1352
- let flushing = Promise.resolve();
1353
- function drainBuffersSync() {
1354
- if (activeDir === null) return;
1355
- for (const [stream, lines] of pendingLines) {
1356
- if (lines.length === 0) continue;
1357
- pendingLines.set(stream, []);
1358
- try {
1359
- fs.appendFileSync(path.join(activeDir, `${stream}.ndjson`), lines.join("\n") + "\n");
1360
- } catch (_error) {}
1361
- }
1362
- }
1363
- function scheduleFlush() {
1364
- if (flushScheduled || activeDir === null) return;
1365
- flushScheduled = true;
1366
- setTimeout(() => {
1367
- flushScheduled = false;
1368
- flushing = flushing.then(async () => {
1369
- if (activeDir === null) return;
1370
- for (const [stream, lines] of pendingLines) {
1371
- if (lines.length === 0) continue;
1372
- pendingLines.set(stream, []);
1373
- try {
1374
- await fs.promises.appendFile(path.join(activeDir, `${stream}.ndjson`), lines.join("\n") + "\n");
1375
- } catch (_error) {}
1376
- }
1377
- });
1378
- }, 100).unref?.();
1379
- }
1380
- function profilingEnabled() {
1381
- const v = process.env[ENV_FLAG];
1382
- return v !== void 0 && v !== "" && v !== "0";
1383
- }
1384
- function sanitizeToken(token) {
1385
- return token.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 40);
1386
- }
1387
- /**
1388
- * Appends one event to a stream file. Buffered and written asynchronously
1389
- * so recording never blocks the event loop; the exit handler drains any
1390
- * remainder synchronously so abrupt exits still keep their events.
1391
- */
1392
- function record(stream, event) {
1393
- if (activeDir === null) return;
1394
- const line = JSON.stringify({
1395
- t: Date.now(),
1396
- pid: process.pid,
1397
- ...event
1398
- });
1399
- const lines = pendingLines.get(stream);
1400
- if (lines === void 0) pendingLines.set(stream, [line]);
1401
- else lines.push(line);
1402
- scheduleFlush();
1403
- }
1404
- /** Writes a JSON artifact (non-append) into the profile directory. */
1405
- function writeArtifact(name, data) {
1406
- if (activeDir === null) return;
1407
- try {
1408
- fs.writeFileSync(path.join(activeDir, name), JSON.stringify(data, null, 2));
1409
- } catch (_error) {}
1410
- }
1411
- /**
1412
- * Patches globalThis.fetch to record request timing. A wrapper (not
1413
- * undici's diagnostics_channel) because chat re-execs under Bun, where
1414
- * fetch is Bun-native and undici events never fire; the wrapper behaves
1415
- * identically in both runtimes.
1416
- */
1417
- function installFetchRecorder() {
1418
- const original = globalThis.fetch;
1419
- const wrapped = async (...args) => {
1420
- const [input, init] = args;
1421
- const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
1422
- const method = init?.method ?? (input instanceof Request ? input.method : "GET");
1423
- const started = Date.now();
1424
- try {
1425
- const res = await original(...args);
1426
- const contentType = res.headers.get("content-type") ?? "";
1427
- record("network", {
1428
- method,
1429
- url,
1430
- status: res.status,
1431
- durationMs: Date.now() - started,
1432
- contentType,
1433
- streamed: contentType.includes("text/event-stream")
1434
- });
1435
- return res;
1436
- } catch (err) {
1437
- record("network", {
1438
- method,
1439
- url,
1440
- status: 0,
1441
- durationMs: Date.now() - started,
1442
- error: err instanceof Error ? err.message : String(err)
1443
- });
1444
- throw err;
1445
- }
1446
- };
1447
- globalThis.fetch = Object.assign(wrapped, original);
1448
- }
1449
- function isBunRuntime() {
1450
- return typeof process !== "undefined" && "bun" in process.versions;
1451
- }
1452
- /**
1453
- * Starts the V8 CPU profiler via node:inspector. Node-run commands only —
1454
- * Bun does not implement the inspector Profiler domain, so the chat TUI
1455
- * skips this artifact.
1456
- */
1457
- function startCpuProfile() {
1458
- if (isBunRuntime()) return;
1459
- (async () => {
1460
- try {
1461
- const { Session } = await import("node:inspector/promises");
1462
- const session = new Session();
1463
- session.connect();
1464
- await session.post("Profiler.enable");
1465
- await session.post("Profiler.start");
1466
- process.once("beforeExit", () => {
1467
- (async () => {
1468
- try {
1469
- const { profile } = await session.post("Profiler.stop");
1470
- writeArtifact(`cpu-${process.pid}.cpuprofile`, profile);
1471
- session.disconnect();
1472
- } catch (_error) {}
1473
- })();
1474
- });
1475
- } catch (_error) {}
1476
- })();
1477
- }
1478
- /**
1479
- * Activates profiling for this process if SKYDIVE_PROFILE is set. Creates
1480
- * the profile directory (or joins the one a parent process created),
1481
- * patches fetch, starts the CPU profiler, and registers the exit-time
1482
- * meta dump. Call once, as early as possible.
1483
- */
1484
- function maybeStartProfiling(argv, cliVersion) {
1485
- if (!profilingEnabled() || activeDir !== null) return;
1486
- const inherited = process.env[ENV_DIR];
1487
- if (inherited !== void 0 && inherited !== "") activeDir = inherited;
1488
- else {
1489
- const flagValue = process.env[ENV_FLAG] ?? "1";
1490
- const command = argv.find((a) => !a.startsWith("-")) ?? "chat";
1491
- const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").replace("T", "_").slice(0, 19);
1492
- activeDir = flagValue === "1" || flagValue.toLowerCase() === "true" ? path.resolve(process.cwd(), `skydive-profile-${sanitizeToken(command)}-${stamp}`) : path.resolve(process.cwd(), flagValue);
1493
- process.env[ENV_DIR] = activeDir;
1494
- }
1495
- try {
1496
- fs.mkdirSync(activeDir, { recursive: true });
1497
- } catch (_error) {
1498
- activeDir = null;
1499
- return;
1500
- }
1501
- startedAtMs = Date.now();
1502
- installFetchRecorder();
1503
- startCpuProfile();
1504
- process.on("exit", (code) => {
1505
- drainBuffersSync();
1506
- writeArtifact(`meta-${process.pid}.json`, {
1507
- argv,
1508
- cliVersion,
1509
- runtime: isBunRuntime() ? `bun ${process.versions["bun"]}` : `node ${process.version}`,
1510
- platform: process.platform,
1511
- pid: process.pid,
1512
- exitCode: code,
1513
- startedAt: new Date(startedAtMs).toISOString(),
1514
- wallTimeMs: Date.now() - startedAtMs
1515
- });
1516
- if (process.env[ENV_DIR] === activeDir && inherited === void 0) process.stderr.write(`\nprofile written to ${activeDir}\n`);
1517
- });
1518
- }
1519
-
1520
- //#endregion
1521
- export { takenAliasNames as A, name as B, themesForMode as C, dedupeAliasName as D, aliasActivationHint as E, getActiveWorkspaceId as F, getSessionIdentity as I, listWorkspaces as L, defaultInstallEnv as M, detectShell as N, installAgentAlias as O, ensureActiveOrganization as P, resolveWorkspaceId as R, themes as S, machineOsFromPlatform as T, version$1 as V, theme as _, installCrashHandler as a, themeModeFromColorFgBg as b, MARK_CELLS as c, splashFitsWidth as d, DEFAULT_THEME_ID as f, noColorRequested as g, monoTheme as h, writeArtifact as i, SUPPORTED_SHELLS as j, slugifyAliasName as k, WORDMARK as l, findTheme as m, profilingEnabled as n, buildCrashReport as o, applyTheme as p, record as r, writeCrashReport as s, maybeStartProfiling as t, brandHelpArt as u, themeForMode as v, buildImportSeedPrompt as w, themeVersion as x, themeMode as y, setActiveWorkspace as z };
1585
+ export { installAgentAlias as A, resolveWorkspaceId as B, themeVersion as C, machineOsFromPlatform as D, buildImportSeedPrompt as E, detectShell as F, name as H, ensureActiveOrganization as I, getActiveWorkspaceId as L, takenAliasNames as M, SUPPORTED_SHELLS as N, aliasActivationHint as O, defaultInstallEnv as P, getSessionIdentity as R, themeModeFromColorFgBg as S, themesForMode as T, version$1 as U, setActiveWorkspace as V, monoTheme as _, WORDMARK as a, themeForMode as b, getAutomaticLogsDir as c, profilingEnabled as d, record as f, findTheme as g, applyTheme as h, MARK_CELLS as i, slugifyAliasName as j, dedupeAliasName as k, maybeStartProfiling as l, DEFAULT_THEME_ID as m, buildCrashReport as n, brandHelpArt as o, writeArtifact as p, writeCrashReport as r, splashFitsWidth as s, installCrashHandler as t, profileDir as u, noColorRequested as v, themes as w, themeMode as x, theme as y, listWorkspaces as z };