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.
- package/README.md +20 -0
- package/dist/js/api-DQCaztBg.mjs +315 -0
- package/dist/js/{billing-blocked-Dgu5-oDy.mjs → billing-blocked-SE6tySLd.mjs} +1 -1
- package/dist/js/bin.mjs +214 -50
- package/dist/js/{boot-iaVSnd7h.mjs → boot-C5HJfwRS.mjs} +17 -14
- package/dist/js/chunk-BbwQpWto.mjs +33 -0
- package/dist/js/{client-DbqRBquD.mjs → client-d6K9qm6B.mjs} +70 -2
- package/dist/js/client-yz3zrpAS.mjs +5 -0
- package/dist/js/{daemon-BTyg329O.mjs → daemon-BPGZufS1.mjs} +4 -2
- package/dist/js/daemon-WQ8QYrFt.mjs +7 -0
- package/dist/js/daemon-client-DTfIza0O.mjs +8 -0
- package/dist/js/{daemon-client--A_yMKq6.mjs → daemon-client-MpAGVkqP.mjs} +1 -1
- package/dist/js/dist-CRtjM7ba.mjs +1750 -0
- package/dist/js/{forward-DL6DYqyc.mjs → forward-zwB55Bls.mjs} +25 -4
- package/dist/js/{profiler-BVbYs_Yg.mjs → install-BPUAm8bS.mjs} +274 -210
- package/dist/js/localhost-cert-Bn-UBUmj.mjs +67 -0
- package/dist/js/{print-DACzRUwQ.mjs → print-B9djx8GU.mjs} +3 -3
- package/dist/js/{print-C_TnQSPg.mjs → print-Clgq46GU.mjs} +3 -3
- package/dist/js/{print-share-BH9gvCls.mjs → print-share-BBy3OTno.mjs} +6 -2
- package/dist/js/{raw-pty-DY4KelZW.mjs → raw-pty-B9Bue8gg.mjs} +1 -1
- package/dist/js/raw-pty-g3kATVQe.mjs +5 -0
- package/dist/js/{rest-CgfbKXst.mjs → rest-C6E4mS2A.mjs} +2 -2
- package/dist/js/rest-CaWUSrnH.mjs +6 -0
- package/dist/js/tls-cert-CLgSQALB.mjs +4 -0
- package/dist/js/tls-cert-CV-pwxVN.mjs +67 -0
- package/package.json +1 -1
- package/dist/js/api-DG5W6iwx.mjs +0 -131
- package/dist/js/client-hH2PJL8y.mjs +0 -5
- package/dist/js/daemon-SPQgtsbW.mjs +0 -6
- package/dist/js/daemon-client-C7emKKlj.mjs +0 -7
- package/dist/js/raw-pty-DmdUf4_w.mjs +0 -5
- package/dist/js/rest-VV5nc-Mn.mjs +0 -6
- /package/dist/js/{billing-blocked-2wju4gC_.mjs → billing-blocked-D3l5kJlX.mjs} +0 -0
- /package/dist/js/{client-c4c5MmgN.mjs → client-BTQ1fwzM.mjs} +0 -0
- /package/dist/js/{http-error-DzyrsLAZ.mjs → http-error-UHH3mVBF.mjs} +0 -0
- /package/dist/js/{output-DYzzdXYV.mjs → output-wY0VQDea.mjs} +0 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { r as warnOnRebindProtection } from "./tls-cert-CV-pwxVN.mjs";
|
|
3
|
+
import { l as startTlsForward, t as fetchForwardTarget } from "./api-DQCaztBg.mjs";
|
|
3
4
|
import net from "node:net";
|
|
4
5
|
import { WebSocket, createWebSocketStream } from "ws";
|
|
5
6
|
|
|
@@ -103,7 +104,7 @@ const MAX_POOL_SIZE = 16;
|
|
|
103
104
|
* sandbox recycle just costs the next connection a cold-start wait rather
|
|
104
105
|
* than invalidating the forward.
|
|
105
106
|
*/
|
|
106
|
-
async function startForward({ auth, agentId, localPort, targetPort, log }) {
|
|
107
|
+
async function startForward({ auth, agentId, localPort, targetPort, tlsCertSource, log }) {
|
|
107
108
|
let target = await fetchForwardTarget(auth, agentId);
|
|
108
109
|
let mintedAt = Date.now();
|
|
109
110
|
async function freshTarget() {
|
|
@@ -174,11 +175,31 @@ async function startForward({ auth, agentId, localPort, targetPort, log }) {
|
|
|
174
175
|
});
|
|
175
176
|
});
|
|
176
177
|
const addr = server.address();
|
|
178
|
+
const boundPort = addr && typeof addr === "object" ? addr.port : localPort;
|
|
179
|
+
let tls = null;
|
|
180
|
+
if (tlsCertSource) try {
|
|
181
|
+
const cert = await tlsCertSource();
|
|
182
|
+
if (cert) {
|
|
183
|
+
if (cert.hostname !== "localhost") await warnOnRebindProtection(cert.hostname, log);
|
|
184
|
+
tls = await startTlsForward({
|
|
185
|
+
cert,
|
|
186
|
+
localPort: boundPort,
|
|
187
|
+
targetPort,
|
|
188
|
+
acquireTunnel: async () => pool.take() ?? dialTunnel(),
|
|
189
|
+
log
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
} catch (err) {
|
|
193
|
+
log(`forward: TLS origin unavailable (${err instanceof Error ? err.message : String(err)}); continuing with http only`);
|
|
194
|
+
}
|
|
177
195
|
return {
|
|
178
|
-
port:
|
|
196
|
+
port: boundPort,
|
|
197
|
+
tls,
|
|
179
198
|
close: () => new Promise((resolve) => {
|
|
180
199
|
pool.close();
|
|
181
|
-
|
|
200
|
+
(tls ? tls.close() : Promise.resolve()).then(() => {
|
|
201
|
+
server.close(() => resolve());
|
|
202
|
+
});
|
|
182
203
|
})
|
|
183
204
|
};
|
|
184
205
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { C as getConfigPath } from "./print-
|
|
2
|
+
import { C as getConfigPath } from "./print-Clgq46GU.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.
|
|
11
|
+
var version$1 = "0.5.0-beta.33";
|
|
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
|
-
|
|
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 };
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { r as __toESM } from "./chunk-BbwQpWto.mjs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { mkdir, readFile, unlink, writeFile } from "node:fs/promises";
|
|
6
|
+
|
|
7
|
+
//#region ../portal-daemon/src/localhost-cert.ts
|
|
8
|
+
/**
|
|
9
|
+
* Durable home for the one piece of state devcert doesn't keep for us: the
|
|
10
|
+
* user's decision to skip automatic cert setup. The daemon's own state dir is
|
|
11
|
+
* tmpdir-based (wiped on reboot), and re-raising a system trust prompt on
|
|
12
|
+
* every `portal forward` after a decline is exactly the nagging that trains
|
|
13
|
+
* people to fear the feature.
|
|
14
|
+
*/
|
|
15
|
+
function declineMarkerPath() {
|
|
16
|
+
return path.join(os.homedir(), ".config", "skydive-portal", "tls-declined");
|
|
17
|
+
}
|
|
18
|
+
async function hasDeclined() {
|
|
19
|
+
try {
|
|
20
|
+
await readFile(declineMarkerPath());
|
|
21
|
+
return true;
|
|
22
|
+
} catch (_error) {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
async function recordDecline(reason) {
|
|
27
|
+
const marker = declineMarkerPath();
|
|
28
|
+
try {
|
|
29
|
+
await mkdir(path.dirname(marker), { recursive: true });
|
|
30
|
+
await writeFile(marker, `Automatic localhost TLS setup failed or was declined:\n${reason}\n\nDelete this file to let it try again.\n`);
|
|
31
|
+
} catch (_error) {}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Fully automatic localhost cert: devcert generates a per-machine local CA on
|
|
35
|
+
* first use, installs it into the system/browser trust stores (one OS-native
|
|
36
|
+
* consent prompt, the mkcert pattern), signs a `localhost` leaf, and reuses
|
|
37
|
+
* both silently on every later run. The private key never leaves the machine
|
|
38
|
+
* and the CA is unique to it, so trusting it vouches only for this user's own
|
|
39
|
+
* loopback.
|
|
40
|
+
*
|
|
41
|
+
* Every failure — the user declining the trust prompt included — degrades to
|
|
42
|
+
* the plain-http origin (the product default) with one log line, and writes a
|
|
43
|
+
* marker so subsequent runs don't nag. `resetLocalhostCertDecline` (or
|
|
44
|
+
* deleting the marker file) re-arms it.
|
|
45
|
+
*/
|
|
46
|
+
function localhostCertSource(log) {
|
|
47
|
+
return async () => {
|
|
48
|
+
if (await hasDeclined()) return null;
|
|
49
|
+
try {
|
|
50
|
+
const { certificateFor } = await import("./dist-CRtjM7ba.mjs").then((m) => /* @__PURE__ */ __toESM(m.default, 1));
|
|
51
|
+
const { key, cert } = await certificateFor("localhost", { skipHostsFile: true });
|
|
52
|
+
return {
|
|
53
|
+
key: key.toString(),
|
|
54
|
+
cert: cert.toString(),
|
|
55
|
+
hostname: "localhost"
|
|
56
|
+
};
|
|
57
|
+
} catch (error) {
|
|
58
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
59
|
+
await recordDecline(reason);
|
|
60
|
+
log(`portal: automatic localhost TLS setup didn't complete (${reason}). Continuing with http only; delete ~/.config/skydive-portal/tls-declined to try again.`);
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
//#endregion
|
|
67
|
+
export { localhostCertSource };
|
|
@@ -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-Clgq46GU.mjs";
|
|
3
|
+
import "./rest-C6E4mS2A.mjs";
|
|
4
|
+
import "./billing-blocked-D3l5kJlX.mjs";
|
|
5
5
|
|
|
6
6
|
export { messageGet, readStdin, resolveAgent, runPrint };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { t as HttpError } from "./http-error-
|
|
3
|
-
import { a as isRecord, i as errorMessage, r as sendErrorMessage, t as createRestClient } from "./rest-
|
|
4
|
-
import { i as billingBlockedOutcomeFromSendResponse, n as BillingBlockedError } from "./billing-blocked-
|
|
2
|
+
import { t as HttpError } from "./http-error-UHH3mVBF.mjs";
|
|
3
|
+
import { a as isRecord, i as errorMessage, r as sendErrorMessage, t as createRestClient } from "./rest-C6E4mS2A.mjs";
|
|
4
|
+
import { i as billingBlockedOutcomeFromSendResponse, n as BillingBlockedError } from "./billing-blocked-D3l5kJlX.mjs";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import Conf from "conf";
|
|
7
7
|
import { err, ok } from "neverthrow";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { n as printError } from "./output-
|
|
2
|
+
import { n as printError } from "./output-wY0VQDea.mjs";
|
|
3
3
|
|
|
4
4
|
//#region src/chat/print-share.ts
|
|
5
5
|
/**
|
|
@@ -14,7 +14,8 @@ 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-yz3zrpAS.mjs");
|
|
18
|
+
const { defaultTlsCertSource } = await import("./tls-cert-CLgSQALB.mjs");
|
|
18
19
|
let signalConnected;
|
|
19
20
|
const connected = new Promise((resolve) => {
|
|
20
21
|
signalConnected = resolve;
|
|
@@ -26,6 +27,9 @@ async function connectMachineShare({ appUrl, sessionToken, timeoutHint }) {
|
|
|
26
27
|
deviceToken: null
|
|
27
28
|
}),
|
|
28
29
|
resolveCwd: () => process.cwd(),
|
|
30
|
+
tlsCertSource: defaultTlsCertSource(process.env, (msg) => {
|
|
31
|
+
console.error(msg);
|
|
32
|
+
}),
|
|
29
33
|
persistedMachineName: null,
|
|
30
34
|
onMachineName: () => {},
|
|
31
35
|
onState: (state) => {
|