skydive-cli 0.5.0-beta.8 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/README.md +61 -13
- package/dist/js/api-BFQ4PQDA.mjs +315 -0
- package/dist/js/{billing-blocked-Dgu5-oDy.mjs → billing-blocked-SE6tySLd.mjs} +1 -1
- package/dist/js/bin.mjs +674 -307
- package/dist/js/{boot-BCClEpCM.mjs → boot-DD4T-61U.mjs} +4558 -930
- package/dist/js/chunk-BbwQpWto.mjs +33 -0
- package/dist/js/{client-DabRpc_T.mjs → client--k9cjfkX.mjs} +437 -39
- package/dist/js/{client-c4c5MmgN.mjs → client-Btq6bMzX.mjs} +108 -2
- package/dist/js/client-Ct0-JZSS.mjs +5 -0
- package/dist/js/daemon-CCgNLD0H.mjs +7 -0
- package/dist/js/{daemon-k2kVkJ8D.mjs → daemon-Do1jU2UF.mjs} +123 -43
- package/dist/js/daemon-client-C7nE-lLK.mjs +8 -0
- package/dist/js/{daemon-client-fxf1A25Z.mjs → daemon-client-Dvad009G.mjs} +1 -1
- package/dist/js/dist-CRtjM7ba.mjs +1750 -0
- package/dist/js/forward-C-f04uyE.mjs +208 -0
- package/dist/js/{profiler-DawY0V0Z.mjs → install-CtAVvERm.mjs} +545 -215
- package/dist/js/launcher.mjs +49 -0
- package/dist/js/localhost-cert-Bn-UBUmj.mjs +67 -0
- package/dist/js/{print-Wakr3GJd.mjs → print-Bx8qUC9U.mjs} +3 -3
- package/dist/js/{print-BpuyEfWX.mjs → print-D_UEjdSw.mjs} +257 -35
- package/dist/js/{print-share-CKLPmsg0.mjs → print-share-Cz0EO2RK.mjs} +9 -3
- package/dist/js/raw-pty-Ci2qFR9F.mjs +5 -0
- package/dist/js/{raw-pty-DY4KelZW.mjs → raw-pty-D5PhKZSl.mjs} +1 -1
- package/dist/js/{rest-I3imNduB.mjs → rest-B9__Zsuk.mjs} +144 -19
- package/dist/js/rest-Dc0EEok3.mjs +6 -0
- package/dist/js/tls-cert-BpCaD5AT.mjs +4 -0
- package/dist/js/tls-cert-Rua2oV7n.mjs +67 -0
- package/package.json +12 -4
- package/dist/js/api-DG5W6iwx.mjs +0 -131
- package/dist/js/client-BuU34IVE.mjs +0 -5
- package/dist/js/daemon-BxU59xie.mjs +0 -6
- package/dist/js/daemon-client-C2BvZgKO.mjs +0 -7
- package/dist/js/forward-18QoL5dO.mjs +0 -68
- package/dist/js/raw-pty-DmdUf4_w.mjs +0 -5
- package/dist/js/rest-D29qNkto.mjs +0 -6
- /package/dist/js/{billing-blocked-2wju4gC_.mjs → billing-blocked-D3l5kJlX.mjs} +0 -0
- /package/dist/js/{http-error-DzyrsLAZ.mjs → http-error-BF2NZZE3.mjs} +0 -0
- /package/dist/js/{output-DYzzdXYV.mjs → output-C9mb3sUB.mjs} +0 -0
|
@@ -1,16 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { c as version$1 } from "./rest-B9__Zsuk.mjs";
|
|
3
|
+
import { w as getConfigPath } from "./print-D_UEjdSw.mjs";
|
|
3
4
|
import os from "node:os";
|
|
4
5
|
import path from "node:path";
|
|
5
6
|
import { err, ok } from "neverthrow";
|
|
6
7
|
import { z } from "zod";
|
|
7
8
|
import fs from "node:fs";
|
|
8
9
|
|
|
9
|
-
//#region package.json
|
|
10
|
-
var name = "skydive-cli";
|
|
11
|
-
var version$1 = "0.5.0-beta.8";
|
|
12
|
-
|
|
13
|
-
//#endregion
|
|
14
10
|
//#region src/auth/organization.ts
|
|
15
11
|
const workspaceSchema = z.object({
|
|
16
12
|
id: z.string(),
|
|
@@ -32,6 +28,32 @@ async function listWorkspaces({ appUrl, sessionToken }) {
|
|
|
32
28
|
}
|
|
33
29
|
}
|
|
34
30
|
/**
|
|
31
|
+
* Match a workspace selector against a roster the way `workspace switch` does:
|
|
32
|
+
* exact id first, then case-insensitive slug, then case-insensitive name.
|
|
33
|
+
* Returns null when nothing matches. Pure — the caller owns fetching and error
|
|
34
|
+
* messaging.
|
|
35
|
+
*/
|
|
36
|
+
function matchWorkspace(workspaces, selector) {
|
|
37
|
+
const needle = selector.trim().toLowerCase();
|
|
38
|
+
return workspaces.find((w) => w.id === selector) ?? workspaces.find((w) => w.slug.toLowerCase() === needle) ?? workspaces.find((w) => w.name.toLowerCase() === needle) ?? null;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Resolve a workspace selector (slug, name, or id) to its id by listing the
|
|
42
|
+
* account's workspaces and matching. Used by any command that takes a
|
|
43
|
+
* `--workspace` flag to scope requests without persisting an active-workspace
|
|
44
|
+
* switch.
|
|
45
|
+
*/
|
|
46
|
+
async function resolveWorkspaceId({ appUrl, sessionToken, selector }) {
|
|
47
|
+
const workspaces = await listWorkspaces({
|
|
48
|
+
appUrl,
|
|
49
|
+
sessionToken
|
|
50
|
+
});
|
|
51
|
+
if (workspaces.isErr()) return err(workspaces.error);
|
|
52
|
+
const match = matchWorkspace(workspaces.value, selector);
|
|
53
|
+
if (!match) return err({ message: `No workspace matches "${selector}". Run \`skydive workspace list\` to see available workspaces.` });
|
|
54
|
+
return ok(match);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
35
57
|
* Resolve who the current chat session belongs to: the signed-in user's
|
|
36
58
|
* email/name and the active workspace. Used by `auth status` so a user running
|
|
37
59
|
* multiple accounts can answer "am I logged into the right one?" without
|
|
@@ -125,6 +147,218 @@ async function ensureActiveOrganization({ appUrl, sessionToken }) {
|
|
|
125
147
|
});
|
|
126
148
|
}
|
|
127
149
|
|
|
150
|
+
//#endregion
|
|
151
|
+
//#region src/shell/shell-env.ts
|
|
152
|
+
/**
|
|
153
|
+
* Shared, dependency-light shell primitives used by anything that edits the
|
|
154
|
+
* user's shell startup files — `completion install` and `agents alias`. Kept
|
|
155
|
+
* free of yargs and the CLI's output layer so the TUI can import it without
|
|
156
|
+
* dragging command machinery into its bundle or creating a TUI -> commands
|
|
157
|
+
* import cycle.
|
|
158
|
+
*/
|
|
159
|
+
const SUPPORTED_SHELLS = [
|
|
160
|
+
"bash",
|
|
161
|
+
"zsh",
|
|
162
|
+
"fish"
|
|
163
|
+
];
|
|
164
|
+
const defaultInstallEnv = () => ({
|
|
165
|
+
home: os.homedir(),
|
|
166
|
+
platform: process.platform,
|
|
167
|
+
env: process.env,
|
|
168
|
+
exists: fs.existsSync
|
|
169
|
+
});
|
|
170
|
+
/**
|
|
171
|
+
* The shell to act on when the user didn't name one. `$SHELL` is the only
|
|
172
|
+
* signal available: an install runs as a child process, so the invoking
|
|
173
|
+
* shell's own variables (`$ZSH_VERSION`, `$FISH_VERSION`) aren't visible here.
|
|
174
|
+
*/
|
|
175
|
+
function detectShell(env = process.env) {
|
|
176
|
+
const shell = env["SHELL"];
|
|
177
|
+
if (!shell) return null;
|
|
178
|
+
const name = path.basename(shell);
|
|
179
|
+
return SUPPORTED_SHELLS.find((candidate) => name === candidate) ?? null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
//#endregion
|
|
183
|
+
//#region src/commands/agent-alias.ts
|
|
184
|
+
/**
|
|
185
|
+
* Shell aliases that point a friendly command name at a specific agent's chat.
|
|
186
|
+
*
|
|
187
|
+
* When someone creates an agent named "Ripple", they almost always want to
|
|
188
|
+
* reach it again by typing `ripple`, not `skydive chat --agent <uuid>`. This
|
|
189
|
+
* module writes a managed alias block into the user's shell startup file so
|
|
190
|
+
* `ripple` opens that agent. It reuses the same shell detection and startup-file
|
|
191
|
+
* machinery as `completion install` (imported from ./completion), so the two
|
|
192
|
+
* features agree on which file to touch and how to detect the shell.
|
|
193
|
+
*
|
|
194
|
+
* The alias always targets the agent **id**, never its name: names are not
|
|
195
|
+
* unique and can be renamed, so an id keeps `ripple` pointed at the same agent
|
|
196
|
+
* even if a second "Ripple" is created later or this one is renamed.
|
|
197
|
+
*/
|
|
198
|
+
/**
|
|
199
|
+
* Turn a display name into a shell-safe alias token: lowercase, non-alnum runs
|
|
200
|
+
* collapsed to a single hyphen, leading/trailing hyphens trimmed. "Ripple" ->
|
|
201
|
+
* "ripple", "My Agent" -> "my-agent", "Ripple 2.0" -> "ripple-2-0".
|
|
202
|
+
*
|
|
203
|
+
* A leading digit is prefixed with `a-` because bash rejects an alias name that
|
|
204
|
+
* starts with a digit (`alias 2fast=...` is a syntax error). An empty result
|
|
205
|
+
* (a name that was all punctuation) falls back to "agent" so callers always get
|
|
206
|
+
* a usable token to offer or dedupe from.
|
|
207
|
+
*/
|
|
208
|
+
function slugifyAliasName(name) {
|
|
209
|
+
const slug = name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
210
|
+
if (!slug) return "agent";
|
|
211
|
+
return /^[0-9]/.test(slug) ? `a-${slug}` : slug;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* The alias token to actually install, given a preferred token and the set of
|
|
215
|
+
* names already taken (existing aliases, and — the caller's choice — commands
|
|
216
|
+
* on PATH). Prefers the clean token; on a collision appends the lowest integer
|
|
217
|
+
* that frees it: `ripple` -> `ripple2` -> `ripple3`. Matching is
|
|
218
|
+
* case-insensitive because shells treat `Ripple` and `ripple` as distinct
|
|
219
|
+
* aliases but a user typing one means the other.
|
|
220
|
+
*/
|
|
221
|
+
function dedupeAliasName(preferred, taken) {
|
|
222
|
+
const takenLower = new Set(Array.from(taken, (t) => t.trim().toLowerCase()).filter(Boolean));
|
|
223
|
+
if (!takenLower.has(preferred.toLowerCase())) return preferred;
|
|
224
|
+
for (let n = 2;; n += 1) {
|
|
225
|
+
const candidate = `${preferred}${n}`;
|
|
226
|
+
if (!takenLower.has(candidate.toLowerCase())) return candidate;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
/** The startup file an alias block belongs in, per shell. */
|
|
230
|
+
function aliasTarget(shell, deps = defaultInstallEnv()) {
|
|
231
|
+
const { home, platform, env, exists } = deps;
|
|
232
|
+
const join = path.posix.join;
|
|
233
|
+
if (shell === "fish") return join(env["XDG_CONFIG_HOME"] || join(home, ".config"), "fish", "config.fish");
|
|
234
|
+
if (shell === "zsh") return join(env["ZDOTDIR"] || home, ".zshrc");
|
|
235
|
+
const rc = join(home, ".bashrc");
|
|
236
|
+
const profile = join(home, ".bash_profile");
|
|
237
|
+
const preferred = platform === "darwin" ? profile : rc;
|
|
238
|
+
const alternate = platform === "darwin" ? rc : profile;
|
|
239
|
+
if (!exists(preferred) && exists(alternate)) return alternate;
|
|
240
|
+
return preferred;
|
|
241
|
+
}
|
|
242
|
+
const BLOCK_START = "###-begin-skydive-agent-aliases-###";
|
|
243
|
+
const BLOCK_END = "###-end-skydive-agent-aliases-###";
|
|
244
|
+
const aliasLine = (entry) => {
|
|
245
|
+
const ws = entry.workspaceId ? ` --workspace ${entry.workspaceId}` : "";
|
|
246
|
+
return `alias ${entry.alias}='skydive chat --agent ${entry.agentId}${ws} --new' # skydive-agent:${entry.agentId}`;
|
|
247
|
+
};
|
|
248
|
+
/**
|
|
249
|
+
* Parse the alias entries already inside the managed block. Used to merge a new
|
|
250
|
+
* alias with existing ones and to answer "is this token taken?". Tolerant of
|
|
251
|
+
* hand-edits: only well-formed lines carrying our trailing marker are read
|
|
252
|
+
* back; anything else in the block is preserved verbatim on rewrite.
|
|
253
|
+
*/
|
|
254
|
+
function parseAliasBlock(existing) {
|
|
255
|
+
const start = existing.indexOf(BLOCK_START);
|
|
256
|
+
const end = existing.indexOf(BLOCK_END);
|
|
257
|
+
if (start === -1 || end <= start) return [];
|
|
258
|
+
const body = existing.slice(start + 35, end);
|
|
259
|
+
const entries = [];
|
|
260
|
+
for (const line of body.split("\n")) {
|
|
261
|
+
const match = line.match(/^alias\s+([^=]+)=.*# skydive-agent:([0-9a-f-]+)\s*$/i);
|
|
262
|
+
if (match?.[1] && match[2]) {
|
|
263
|
+
const wsMatch = line.match(/--workspace\s+(\S+)/);
|
|
264
|
+
entries.push({
|
|
265
|
+
alias: match[1].trim(),
|
|
266
|
+
agentId: match[2],
|
|
267
|
+
workspaceId: wsMatch?.[1]
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return entries;
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Merge `entry` into the managed alias block of `existing`, returning the new
|
|
275
|
+
* file contents. Idempotent: re-installing the same alias for the same agent is
|
|
276
|
+
* a no-op edit; a new agent id adds a line; an existing agent id has its alias
|
|
277
|
+
* token refreshed. The block is created if absent.
|
|
278
|
+
*/
|
|
279
|
+
function spliceAliasBlock(existing, entry) {
|
|
280
|
+
const current = parseAliasBlock(existing).filter((e) => e.agentId !== entry.agentId);
|
|
281
|
+
current.push(entry);
|
|
282
|
+
current.sort((a, b) => a.alias.localeCompare(b.alias));
|
|
283
|
+
const block = [
|
|
284
|
+
BLOCK_START,
|
|
285
|
+
"# skydive agent aliases, managed by `skydive agents alias`. Edits between",
|
|
286
|
+
"# these markers are overwritten when an alias is added or refreshed.",
|
|
287
|
+
...current.map(aliasLine),
|
|
288
|
+
BLOCK_END
|
|
289
|
+
].join("\n");
|
|
290
|
+
const start = existing.indexOf(BLOCK_START);
|
|
291
|
+
const endMarker = BLOCK_END;
|
|
292
|
+
const end = existing.indexOf(endMarker);
|
|
293
|
+
if (start !== -1 && end > start) return `${existing.slice(0, start)}${block}\n${existing.slice(end + 33).replace(/^\n/, "")}`;
|
|
294
|
+
return `${existing}${existing === "" || existing.endsWith("\n\n") ? "" : "\n"}\n${block}\n`;
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Write (or refresh) an agent alias in the user's shell startup file. Returns
|
|
298
|
+
* the resolved shell, file, and the alias token actually used. Does not dedupe
|
|
299
|
+
* on its own — pass a token already reconciled with `takenAliasNames` so the
|
|
300
|
+
* caller controls the suggestion UX.
|
|
301
|
+
*/
|
|
302
|
+
function installAgentAlias(input, deps = defaultInstallEnv()) {
|
|
303
|
+
const target = aliasTarget(input.shell, deps);
|
|
304
|
+
const existing = deps.exists(target) ? fs.readFileSync(target, "utf8") : "";
|
|
305
|
+
const contents = spliceAliasBlock(existing, {
|
|
306
|
+
alias: input.alias,
|
|
307
|
+
agentId: input.agentId,
|
|
308
|
+
workspaceId: input.workspaceId
|
|
309
|
+
});
|
|
310
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
311
|
+
fs.writeFileSync(target, contents, "utf8");
|
|
312
|
+
return {
|
|
313
|
+
shell: input.shell,
|
|
314
|
+
path: target,
|
|
315
|
+
alias: input.alias,
|
|
316
|
+
agentId: input.agentId,
|
|
317
|
+
updated: existing.includes(BLOCK_START)
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Alias tokens already defined in our managed block of the given shell's
|
|
322
|
+
* startup file. The dedupe source: a fresh install shouldn't collide with an
|
|
323
|
+
* alias we wrote for another agent. (We don't try to read the user's own
|
|
324
|
+
* hand-written aliases or PATH — a shell child process can't see the parent's
|
|
325
|
+
* live alias table, and probing PATH is noisy; the dedupe covers our own
|
|
326
|
+
* blocks, which is where real collisions come from in practice.)
|
|
327
|
+
*/
|
|
328
|
+
function takenAliasNames(shell, deps = defaultInstallEnv()) {
|
|
329
|
+
const target = aliasTarget(shell, deps);
|
|
330
|
+
if (!deps.exists(target)) return [];
|
|
331
|
+
return parseAliasBlock(fs.readFileSync(target, "utf8")).map((e) => e.alias);
|
|
332
|
+
}
|
|
333
|
+
/** What the user must do before the alias works in the shell they're in. */
|
|
334
|
+
function aliasActivationHint(result) {
|
|
335
|
+
return result.shell === "fish" ? "Open a new fish shell to use it." : `Open a new shell, or run: source ${result.path}`;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
//#endregion
|
|
339
|
+
//#region src/chat/import-seed.ts
|
|
340
|
+
/** Map `process.platform` to the OS family used by the seed prompts. Every
|
|
341
|
+
* non-`win32` platform Node runs on (darwin, linux, the BSDs) is POSIX. */
|
|
342
|
+
function machineOsFromPlatform(platform) {
|
|
343
|
+
return platform === "win32" ? "windows" : "posix";
|
|
344
|
+
}
|
|
345
|
+
/** One clause describing the machine's shell/paths so the agent's discovery
|
|
346
|
+
* sweep uses the right conventions instead of guessing. */
|
|
347
|
+
function osHint(os) {
|
|
348
|
+
return os === "windows" ? "This machine is Windows, so use PowerShell and Windows paths (%USERPROFILE%, backslashes)." : "This machine is POSIX (macOS/Linux), so use a POSIX shell and paths (~, forward slashes).";
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* The first message of an explicit `skydive import` conversation, sent as the
|
|
352
|
+
* user. Frames the migration and points the agent at its import-config skill.
|
|
353
|
+
*/
|
|
354
|
+
function buildImportSeedPrompt(projectDir, os) {
|
|
355
|
+
return [
|
|
356
|
+
"I'm migrating from another coding agent. Import my setup from this machine.",
|
|
357
|
+
"",
|
|
358
|
+
`Use your import-config skill. I ran this from \`${projectDir}\`, so start there and in my home directory. ${osHint(os)} Don't assume one tool — do the discovery sweep so you catch whatever I actually use (Claude Code, Cursor, Codex, Gemini CLI, Copilot, Windsurf, Cline, OpenCode, Aider, and any nested AGENTS.md). Show me the plan first: everything you found, what you'll bring over, where it lands in you, and anything you're leaving out (credentials especially). Then wait for my OK before committing anything.`
|
|
359
|
+
].join("\n");
|
|
360
|
+
}
|
|
361
|
+
|
|
128
362
|
//#endregion
|
|
129
363
|
//#region src/chat/tui/theme.ts
|
|
130
364
|
const tokyonight = {
|
|
@@ -132,6 +366,7 @@ const tokyonight = {
|
|
|
132
366
|
label: "Tokyo Night",
|
|
133
367
|
mode: "dark",
|
|
134
368
|
palette: {
|
|
369
|
+
cursor: "#ffffff",
|
|
135
370
|
fg: "#c0caf5",
|
|
136
371
|
muted: "#7a7a7a",
|
|
137
372
|
dim: "#565f89",
|
|
@@ -160,6 +395,7 @@ const tokyonightDay = {
|
|
|
160
395
|
label: "Tokyo Night Day",
|
|
161
396
|
mode: "light",
|
|
162
397
|
palette: {
|
|
398
|
+
cursor: "#737373",
|
|
163
399
|
fg: "#3760bf",
|
|
164
400
|
muted: "#848cb5",
|
|
165
401
|
dim: "#9da3c2",
|
|
@@ -188,6 +424,7 @@ const catppuccinMocha = {
|
|
|
188
424
|
label: "Catppuccin Mocha",
|
|
189
425
|
mode: "dark",
|
|
190
426
|
palette: {
|
|
427
|
+
cursor: "#ffffff",
|
|
191
428
|
fg: "#cdd6f4",
|
|
192
429
|
muted: "#7f849c",
|
|
193
430
|
dim: "#6c7086",
|
|
@@ -216,6 +453,7 @@ const catppuccinLatte = {
|
|
|
216
453
|
label: "Catppuccin Latte",
|
|
217
454
|
mode: "light",
|
|
218
455
|
palette: {
|
|
456
|
+
cursor: "#737373",
|
|
219
457
|
fg: "#4c4f69",
|
|
220
458
|
muted: "#8c8fa1",
|
|
221
459
|
dim: "#9ca0b0",
|
|
@@ -244,6 +482,7 @@ const gruvboxDark = {
|
|
|
244
482
|
label: "Gruvbox Dark",
|
|
245
483
|
mode: "dark",
|
|
246
484
|
palette: {
|
|
485
|
+
cursor: "#ffffff",
|
|
247
486
|
fg: "#ebdbb2",
|
|
248
487
|
muted: "#928374",
|
|
249
488
|
dim: "#7c6f64",
|
|
@@ -272,6 +511,7 @@ const gruvboxLight = {
|
|
|
272
511
|
label: "Gruvbox Light",
|
|
273
512
|
mode: "light",
|
|
274
513
|
palette: {
|
|
514
|
+
cursor: "#737373",
|
|
275
515
|
fg: "#3c3836",
|
|
276
516
|
muted: "#928374",
|
|
277
517
|
dim: "#a89984",
|
|
@@ -300,6 +540,7 @@ const solarizedDark = {
|
|
|
300
540
|
label: "Solarized Dark",
|
|
301
541
|
mode: "dark",
|
|
302
542
|
palette: {
|
|
543
|
+
cursor: "#ffffff",
|
|
303
544
|
fg: "#93a1a1",
|
|
304
545
|
muted: "#586e75",
|
|
305
546
|
dim: "#586e75",
|
|
@@ -328,6 +569,7 @@ const solarizedLight = {
|
|
|
328
569
|
label: "Solarized Light",
|
|
329
570
|
mode: "light",
|
|
330
571
|
palette: {
|
|
572
|
+
cursor: "#737373",
|
|
331
573
|
fg: "#657b83",
|
|
332
574
|
muted: "#839496",
|
|
333
575
|
dim: "#93a1a1",
|
|
@@ -356,6 +598,7 @@ const nord = {
|
|
|
356
598
|
label: "Nord",
|
|
357
599
|
mode: "dark",
|
|
358
600
|
palette: {
|
|
601
|
+
cursor: "#ffffff",
|
|
359
602
|
fg: "#d8dee9",
|
|
360
603
|
muted: "#616e88",
|
|
361
604
|
dim: "#4c566a",
|
|
@@ -384,6 +627,7 @@ const dracula = {
|
|
|
384
627
|
label: "Dracula",
|
|
385
628
|
mode: "dark",
|
|
386
629
|
palette: {
|
|
630
|
+
cursor: "#ffffff",
|
|
387
631
|
fg: "#f8f8f2",
|
|
388
632
|
muted: "#6272a4",
|
|
389
633
|
dim: "#6272a4",
|
|
@@ -412,6 +656,7 @@ const oneDark = {
|
|
|
412
656
|
label: "One Dark",
|
|
413
657
|
mode: "dark",
|
|
414
658
|
palette: {
|
|
659
|
+
cursor: "#ffffff",
|
|
415
660
|
fg: "#abb2bf",
|
|
416
661
|
muted: "#5c6370",
|
|
417
662
|
dim: "#5c6370",
|
|
@@ -440,6 +685,7 @@ const oneLight = {
|
|
|
440
685
|
label: "One Light",
|
|
441
686
|
mode: "light",
|
|
442
687
|
palette: {
|
|
688
|
+
cursor: "#737373",
|
|
443
689
|
fg: "#383a42",
|
|
444
690
|
muted: "#a0a1a7",
|
|
445
691
|
dim: "#a0a1a7",
|
|
@@ -468,6 +714,7 @@ const rosePine = {
|
|
|
468
714
|
label: "Rosé Pine",
|
|
469
715
|
mode: "dark",
|
|
470
716
|
palette: {
|
|
717
|
+
cursor: "#ffffff",
|
|
471
718
|
fg: "#e0def4",
|
|
472
719
|
muted: "#908caa",
|
|
473
720
|
dim: "#6e6a86",
|
|
@@ -496,6 +743,7 @@ const rosePineDawn = {
|
|
|
496
743
|
label: "Rosé Pine Dawn",
|
|
497
744
|
mode: "light",
|
|
498
745
|
palette: {
|
|
746
|
+
cursor: "#737373",
|
|
499
747
|
fg: "#575279",
|
|
500
748
|
muted: "#797593",
|
|
501
749
|
dim: "#9893a5",
|
|
@@ -524,6 +772,7 @@ const everforestDark = {
|
|
|
524
772
|
label: "Everforest Dark",
|
|
525
773
|
mode: "dark",
|
|
526
774
|
palette: {
|
|
775
|
+
cursor: "#ffffff",
|
|
527
776
|
fg: "#d3c6aa",
|
|
528
777
|
muted: "#859289",
|
|
529
778
|
dim: "#7a8478",
|
|
@@ -552,6 +801,7 @@ const everforestLight = {
|
|
|
552
801
|
label: "Everforest Light",
|
|
553
802
|
mode: "light",
|
|
554
803
|
palette: {
|
|
804
|
+
cursor: "#737373",
|
|
555
805
|
fg: "#5c6a72",
|
|
556
806
|
muted: "#939f91",
|
|
557
807
|
dim: "#a6b0a0",
|
|
@@ -580,6 +830,7 @@ const githubDark = {
|
|
|
580
830
|
label: "GitHub Dark",
|
|
581
831
|
mode: "dark",
|
|
582
832
|
palette: {
|
|
833
|
+
cursor: "#ffffff",
|
|
583
834
|
fg: "#c9d1d9",
|
|
584
835
|
muted: "#8b949e",
|
|
585
836
|
dim: "#6e7681",
|
|
@@ -608,6 +859,7 @@ const githubLight = {
|
|
|
608
859
|
label: "GitHub Light",
|
|
609
860
|
mode: "light",
|
|
610
861
|
palette: {
|
|
862
|
+
cursor: "#737373",
|
|
611
863
|
fg: "#24292f",
|
|
612
864
|
muted: "#57606a",
|
|
613
865
|
dim: "#8c959f",
|
|
@@ -636,6 +888,7 @@ const kanagawa = {
|
|
|
636
888
|
label: "Kanagawa",
|
|
637
889
|
mode: "dark",
|
|
638
890
|
palette: {
|
|
891
|
+
cursor: "#ffffff",
|
|
639
892
|
fg: "#dcd7ba",
|
|
640
893
|
muted: "#727169",
|
|
641
894
|
dim: "#54546d",
|
|
@@ -664,6 +917,7 @@ const cursorDark = {
|
|
|
664
917
|
label: "Cursor Dark",
|
|
665
918
|
mode: "dark",
|
|
666
919
|
palette: {
|
|
920
|
+
cursor: "#ffffff",
|
|
667
921
|
background: "#181818",
|
|
668
922
|
fg: "#d4d4d4",
|
|
669
923
|
muted: "#898989",
|
|
@@ -725,6 +979,7 @@ const monoTheme = {
|
|
|
725
979
|
label: "No color",
|
|
726
980
|
mode: "dark",
|
|
727
981
|
palette: {
|
|
982
|
+
cursor: void 0,
|
|
728
983
|
fg: void 0,
|
|
729
984
|
muted: void 0,
|
|
730
985
|
dim: void 0,
|
|
@@ -807,6 +1062,277 @@ function applyTheme(def) {
|
|
|
807
1062
|
version++;
|
|
808
1063
|
}
|
|
809
1064
|
|
|
1065
|
+
//#endregion
|
|
1066
|
+
//#region src/profiling/profiler.ts
|
|
1067
|
+
/**
|
|
1068
|
+
* Session diagnostics. Every invocation records enough context to reconstruct
|
|
1069
|
+
* what happened after the fact. SKYDIVE_PROFILE can select a custom directory
|
|
1070
|
+
* or disable recording with `0`.
|
|
1071
|
+
*
|
|
1072
|
+
* Default sessions live under the CLI's system state directory. Explicit
|
|
1073
|
+
* profiles keep the original cwd/custom-path behavior and add a CPU profile.
|
|
1074
|
+
* Each directory is designed to be handed to an agent (or a human) and read
|
|
1075
|
+
* without special tooling:
|
|
1076
|
+
*
|
|
1077
|
+
* network.ndjson every fetch: method, url, status, timing, content type
|
|
1078
|
+
* state.ndjson TUI state transitions (screen changes, store updates)
|
|
1079
|
+
* commits.ndjson React commits: which subtree rendered, when, how long
|
|
1080
|
+
* render.ndjson OpenTUI renderer samples: fps, frame times, cells drawn
|
|
1081
|
+
* render.json final renderer stats dump (full frame-time series)
|
|
1082
|
+
* cpu-<pid>.cpuprofile V8 CPU profile (Node-run commands only)
|
|
1083
|
+
* meta-<pid>.json argv, versions, runtime, exit code, wall time
|
|
1084
|
+
*
|
|
1085
|
+
* Every ndjson event carries a wall-clock `t` (epoch ms) and `pid`, so
|
|
1086
|
+
* records from the Node parent and the Bun-re-exec'd chat TUI land in the
|
|
1087
|
+
* same files and stay correlatable on one clock. The profile directory is
|
|
1088
|
+
* created by the first process and shared with children through
|
|
1089
|
+
* SKYDIVE_PROFILE_DIR (the chat re-exec inherits the environment).
|
|
1090
|
+
*
|
|
1091
|
+
*/
|
|
1092
|
+
const ENV_FLAG = "SKYDIVE_PROFILE";
|
|
1093
|
+
const ENV_DIR = "SKYDIVE_PROFILE_DIR";
|
|
1094
|
+
const DEFAULT_SESSION_LIMIT = 20;
|
|
1095
|
+
let activeDir = null;
|
|
1096
|
+
let startedAtMs = 0;
|
|
1097
|
+
/**
|
|
1098
|
+
* Buffered event lines per stream, flushed asynchronously. Events are
|
|
1099
|
+
* appended to an in-memory buffer and written with fs.promises off the
|
|
1100
|
+
* hot path, so recording never blocks the TUI's event loop — high-rate
|
|
1101
|
+
* streams (React commits, renderer samples) stay cheap. Whatever is
|
|
1102
|
+
* still buffered when the process exits is drained synchronously in the
|
|
1103
|
+
* exit handler, where async I/O would never flush.
|
|
1104
|
+
*/
|
|
1105
|
+
const pendingLines = /* @__PURE__ */ new Map();
|
|
1106
|
+
let flushScheduled = false;
|
|
1107
|
+
let flushing = Promise.resolve();
|
|
1108
|
+
function drainBuffersSync() {
|
|
1109
|
+
if (activeDir === null) return;
|
|
1110
|
+
for (const [stream, lines] of pendingLines) {
|
|
1111
|
+
if (lines.length === 0) continue;
|
|
1112
|
+
pendingLines.set(stream, []);
|
|
1113
|
+
try {
|
|
1114
|
+
fs.appendFileSync(path.join(activeDir, `${stream}.ndjson`), lines.join("\n") + "\n");
|
|
1115
|
+
} catch (_error) {}
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
function scheduleFlush() {
|
|
1119
|
+
if (flushScheduled || activeDir === null) return;
|
|
1120
|
+
flushScheduled = true;
|
|
1121
|
+
setTimeout(() => {
|
|
1122
|
+
flushScheduled = false;
|
|
1123
|
+
flushing = flushing.then(async () => {
|
|
1124
|
+
if (activeDir === null) return;
|
|
1125
|
+
for (const [stream, lines] of pendingLines) {
|
|
1126
|
+
if (lines.length === 0) continue;
|
|
1127
|
+
pendingLines.set(stream, []);
|
|
1128
|
+
try {
|
|
1129
|
+
await fs.promises.appendFile(path.join(activeDir, `${stream}.ndjson`), lines.join("\n") + "\n");
|
|
1130
|
+
} catch (_error) {}
|
|
1131
|
+
}
|
|
1132
|
+
});
|
|
1133
|
+
}, 100).unref?.();
|
|
1134
|
+
}
|
|
1135
|
+
function profilingEnabled() {
|
|
1136
|
+
return process.env[ENV_FLAG] !== "0";
|
|
1137
|
+
}
|
|
1138
|
+
function explicitProfilingEnabled() {
|
|
1139
|
+
const value = process.env[ENV_FLAG];
|
|
1140
|
+
return value !== void 0 && value !== "" && value !== "0";
|
|
1141
|
+
}
|
|
1142
|
+
function profileDir() {
|
|
1143
|
+
return activeDir;
|
|
1144
|
+
}
|
|
1145
|
+
function automaticLogsDir(platform, env, home) {
|
|
1146
|
+
const appName = env["SKYDIVE_CONFIG_NAME"] ?? "skydive";
|
|
1147
|
+
if (platform === "darwin") return path.join(home, "Library", "Logs", appName);
|
|
1148
|
+
if (platform === "linux") {
|
|
1149
|
+
const stateHome = env["XDG_STATE_HOME"] || path.join(home, ".local", "state");
|
|
1150
|
+
return path.join(stateHome, appName);
|
|
1151
|
+
}
|
|
1152
|
+
return path.join(path.dirname(getConfigPath()), "logs");
|
|
1153
|
+
}
|
|
1154
|
+
function getAutomaticLogsDir() {
|
|
1155
|
+
return automaticLogsDir(process.platform, process.env, os.homedir());
|
|
1156
|
+
}
|
|
1157
|
+
function sanitizeToken(token) {
|
|
1158
|
+
return token.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 40);
|
|
1159
|
+
}
|
|
1160
|
+
function commandName(argv) {
|
|
1161
|
+
return argv.find((arg) => !arg.startsWith("-")) ?? "chat";
|
|
1162
|
+
}
|
|
1163
|
+
function sessionName(argv) {
|
|
1164
|
+
const command = commandName(argv);
|
|
1165
|
+
return `${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").replace("T", "_").slice(0, 19)}-${sanitizeToken(command)}-${process.pid}`;
|
|
1166
|
+
}
|
|
1167
|
+
/**
|
|
1168
|
+
* Automatic logs retain command shape, not values. Positional arguments and
|
|
1169
|
+
* flag values can contain prompts, secrets, paths, and other private data.
|
|
1170
|
+
*/
|
|
1171
|
+
function safeAutomaticArgv(argv) {
|
|
1172
|
+
return [commandName(argv), ...argv.filter((arg) => arg.startsWith("-")).map((arg) => arg.split("=", 1)[0] ?? arg)];
|
|
1173
|
+
}
|
|
1174
|
+
/** Keeps automatic diagnostics bounded without touching explicit profiles. */
|
|
1175
|
+
function pruneAutomaticSessions(root, activeSession) {
|
|
1176
|
+
try {
|
|
1177
|
+
const sessions = fs.readdirSync(root, { withFileTypes: true }).filter((entry) => entry.isDirectory() && entry.name !== activeSession).map((entry) => entry.name).sort().reverse();
|
|
1178
|
+
for (const stale of sessions.slice(DEFAULT_SESSION_LIMIT - 1)) fs.rmSync(path.join(root, stale), {
|
|
1179
|
+
recursive: true,
|
|
1180
|
+
force: true
|
|
1181
|
+
});
|
|
1182
|
+
} catch (_error) {}
|
|
1183
|
+
}
|
|
1184
|
+
/**
|
|
1185
|
+
* Appends one event to a stream file. Buffered and written asynchronously
|
|
1186
|
+
* so recording never blocks the event loop; the exit handler drains any
|
|
1187
|
+
* remainder synchronously so abrupt exits still keep their events.
|
|
1188
|
+
*/
|
|
1189
|
+
function record(stream, event) {
|
|
1190
|
+
if (activeDir === null) return;
|
|
1191
|
+
const line = JSON.stringify({
|
|
1192
|
+
t: Date.now(),
|
|
1193
|
+
pid: process.pid,
|
|
1194
|
+
...event
|
|
1195
|
+
});
|
|
1196
|
+
const lines = pendingLines.get(stream);
|
|
1197
|
+
if (lines === void 0) pendingLines.set(stream, [line]);
|
|
1198
|
+
else lines.push(line);
|
|
1199
|
+
scheduleFlush();
|
|
1200
|
+
}
|
|
1201
|
+
/** Writes a JSON artifact (non-append) into the profile directory. */
|
|
1202
|
+
function writeArtifact(name, data) {
|
|
1203
|
+
if (activeDir === null) return;
|
|
1204
|
+
try {
|
|
1205
|
+
fs.writeFileSync(path.join(activeDir, name), JSON.stringify(data, null, 2));
|
|
1206
|
+
} catch (_error) {}
|
|
1207
|
+
}
|
|
1208
|
+
function safeAutomaticUrl(value) {
|
|
1209
|
+
try {
|
|
1210
|
+
const url = new URL(value);
|
|
1211
|
+
for (const key of url.searchParams.keys()) url.searchParams.set(key, "<redacted>");
|
|
1212
|
+
url.hash = "";
|
|
1213
|
+
return url.href;
|
|
1214
|
+
} catch (_error) {
|
|
1215
|
+
return "<invalid-url>";
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
/**
|
|
1219
|
+
* Patches globalThis.fetch to record request timing. A wrapper (not
|
|
1220
|
+
* undici's diagnostics_channel) because chat re-execs under Bun, where
|
|
1221
|
+
* fetch is Bun-native and undici events never fire; the wrapper behaves
|
|
1222
|
+
* identically in both runtimes.
|
|
1223
|
+
*/
|
|
1224
|
+
function installFetchRecorder(explicit) {
|
|
1225
|
+
const original = globalThis.fetch;
|
|
1226
|
+
const wrapped = async (...args) => {
|
|
1227
|
+
const [input, init] = args;
|
|
1228
|
+
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
1229
|
+
const method = init?.method ?? (input instanceof Request ? input.method : "GET");
|
|
1230
|
+
const started = Date.now();
|
|
1231
|
+
try {
|
|
1232
|
+
const res = await original(...args);
|
|
1233
|
+
const contentType = res.headers.get("content-type") ?? "";
|
|
1234
|
+
record("network", {
|
|
1235
|
+
method,
|
|
1236
|
+
url: explicit ? url : safeAutomaticUrl(url),
|
|
1237
|
+
status: res.status,
|
|
1238
|
+
durationMs: Date.now() - started,
|
|
1239
|
+
contentType,
|
|
1240
|
+
streamed: contentType.includes("text/event-stream")
|
|
1241
|
+
});
|
|
1242
|
+
return res;
|
|
1243
|
+
} catch (err) {
|
|
1244
|
+
record("network", {
|
|
1245
|
+
method,
|
|
1246
|
+
url: explicit ? url : safeAutomaticUrl(url),
|
|
1247
|
+
status: 0,
|
|
1248
|
+
durationMs: Date.now() - started,
|
|
1249
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1250
|
+
});
|
|
1251
|
+
throw err;
|
|
1252
|
+
}
|
|
1253
|
+
};
|
|
1254
|
+
globalThis.fetch = Object.assign(wrapped, original);
|
|
1255
|
+
}
|
|
1256
|
+
function isBunRuntime() {
|
|
1257
|
+
return typeof process !== "undefined" && "bun" in process.versions;
|
|
1258
|
+
}
|
|
1259
|
+
/**
|
|
1260
|
+
* Starts the V8 CPU profiler via node:inspector. Node-run commands only —
|
|
1261
|
+
* Bun does not implement the inspector Profiler domain, so the chat TUI
|
|
1262
|
+
* skips this artifact.
|
|
1263
|
+
*/
|
|
1264
|
+
function startCpuProfile() {
|
|
1265
|
+
if (isBunRuntime()) return;
|
|
1266
|
+
(async () => {
|
|
1267
|
+
try {
|
|
1268
|
+
const { Session } = await import("node:inspector/promises");
|
|
1269
|
+
const session = new Session();
|
|
1270
|
+
session.connect();
|
|
1271
|
+
await session.post("Profiler.enable");
|
|
1272
|
+
await session.post("Profiler.start");
|
|
1273
|
+
process.once("beforeExit", () => {
|
|
1274
|
+
(async () => {
|
|
1275
|
+
try {
|
|
1276
|
+
const { profile } = await session.post("Profiler.stop");
|
|
1277
|
+
writeArtifact(`cpu-${process.pid}.cpuprofile`, profile);
|
|
1278
|
+
session.disconnect();
|
|
1279
|
+
} catch (_error) {}
|
|
1280
|
+
})();
|
|
1281
|
+
});
|
|
1282
|
+
} catch (_error) {}
|
|
1283
|
+
})();
|
|
1284
|
+
}
|
|
1285
|
+
/**
|
|
1286
|
+
* Starts diagnostics unless SKYDIVE_PROFILE=0. Creates a system-state session
|
|
1287
|
+
* directory (or joins the one a parent process created), patches fetch, and
|
|
1288
|
+
* registers the exit-time metadata dump. Explicit profiles also capture CPU.
|
|
1289
|
+
*/
|
|
1290
|
+
function maybeStartProfiling(argv, cliVersion) {
|
|
1291
|
+
if (!profilingEnabled() || activeDir !== null) return;
|
|
1292
|
+
const inherited = process.env[ENV_DIR];
|
|
1293
|
+
const explicit = explicitProfilingEnabled();
|
|
1294
|
+
let automaticRoot = null;
|
|
1295
|
+
let automaticSession = null;
|
|
1296
|
+
if (inherited !== void 0 && inherited !== "") activeDir = inherited;
|
|
1297
|
+
else if (explicit) {
|
|
1298
|
+
const flagValue = process.env[ENV_FLAG] ?? "1";
|
|
1299
|
+
activeDir = flagValue === "1" || flagValue.toLowerCase() === "true" ? path.resolve(process.cwd(), `skydive-profile-${sessionName(argv)}`) : path.resolve(process.cwd(), flagValue);
|
|
1300
|
+
process.env[ENV_DIR] = activeDir;
|
|
1301
|
+
} else {
|
|
1302
|
+
automaticRoot = getAutomaticLogsDir();
|
|
1303
|
+
automaticSession = sessionName(argv);
|
|
1304
|
+
activeDir = path.join(automaticRoot, automaticSession);
|
|
1305
|
+
process.env[ENV_DIR] = activeDir;
|
|
1306
|
+
}
|
|
1307
|
+
try {
|
|
1308
|
+
fs.mkdirSync(activeDir, {
|
|
1309
|
+
recursive: true,
|
|
1310
|
+
mode: 448
|
|
1311
|
+
});
|
|
1312
|
+
if (automaticRoot !== null && automaticSession !== null) pruneAutomaticSessions(automaticRoot, automaticSession);
|
|
1313
|
+
} catch (_error) {
|
|
1314
|
+
activeDir = null;
|
|
1315
|
+
return;
|
|
1316
|
+
}
|
|
1317
|
+
startedAtMs = Date.now();
|
|
1318
|
+
installFetchRecorder(explicit);
|
|
1319
|
+
if (explicit) startCpuProfile();
|
|
1320
|
+
process.on("exit", (code) => {
|
|
1321
|
+
drainBuffersSync();
|
|
1322
|
+
writeArtifact(`meta-${process.pid}.json`, {
|
|
1323
|
+
argv: explicit ? argv : safeAutomaticArgv(argv),
|
|
1324
|
+
cliVersion,
|
|
1325
|
+
runtime: isBunRuntime() ? `bun ${process.versions["bun"]}` : `node ${process.version}`,
|
|
1326
|
+
platform: process.platform,
|
|
1327
|
+
pid: process.pid,
|
|
1328
|
+
exitCode: code,
|
|
1329
|
+
startedAt: new Date(startedAtMs).toISOString(),
|
|
1330
|
+
wallTimeMs: Date.now() - startedAtMs
|
|
1331
|
+
});
|
|
1332
|
+
if (explicit && process.env[ENV_DIR] === activeDir && inherited === void 0) process.stderr.write(`\nprofile written to ${activeDir}\n`);
|
|
1333
|
+
});
|
|
1334
|
+
}
|
|
1335
|
+
|
|
810
1336
|
//#endregion
|
|
811
1337
|
//#region src/brand.ts
|
|
812
1338
|
const MARK_CELLS = [
|
|
@@ -907,6 +1433,17 @@ const WORDMARK = [
|
|
|
907
1433
|
"███████║██║ ██╗ ██║ ██████╔╝██║ ╚████╔╝ ███████╗",
|
|
908
1434
|
"╚══════╝╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝ ╚══════╝"
|
|
909
1435
|
];
|
|
1436
|
+
/** Columns between the pinwheel and the wordmark. */
|
|
1437
|
+
const SPLASH_GAP = 3;
|
|
1438
|
+
/** Columns the full mark + wordmark occupies (no chrome). */
|
|
1439
|
+
const SPLASH_WIDTH = MARK_WIDTH + SPLASH_GAP + WORDMARK.reduce((max, line) => Math.max(max, line.length), 0);
|
|
1440
|
+
/** Extra columns around the splash: `skydive --help` indents by 2, the TUI
|
|
1441
|
+
* app pads 1 on each side. Same number, so one threshold covers both. */
|
|
1442
|
+
const SPLASH_CHROME = 2;
|
|
1443
|
+
/** Whether the full splash fits on one line of `columns` without wrapping. */
|
|
1444
|
+
function splashFitsWidth(columns) {
|
|
1445
|
+
return columns >= SPLASH_WIDTH + SPLASH_CHROME;
|
|
1446
|
+
}
|
|
910
1447
|
const RESET = "\x1B[0m";
|
|
911
1448
|
function hexToRgb(hex) {
|
|
912
1449
|
const n = Number.parseInt(hex.slice(1), 16);
|
|
@@ -933,7 +1470,7 @@ function markLinesAnsi() {
|
|
|
933
1470
|
function brandHelpArt(stream = process.stdout) {
|
|
934
1471
|
if (!stream.isTTY) return "";
|
|
935
1472
|
const truecolor = (typeof stream.getColorDepth === "function" ? stream.getColorDepth() : 1) >= 24;
|
|
936
|
-
if ((stream.columns ?? 80)
|
|
1473
|
+
if (!splashFitsWidth(stream.columns ?? 80)) return truecolor ? `\n ${sgr("✦", BRAND_ACCENT)} Skydive\n` : "\n ✦ Skydive\n";
|
|
937
1474
|
if (!truecolor) return `\n${WORDMARK.map((l) => ` ${l}`).join("\n")}\n`;
|
|
938
1475
|
const mark = markLinesAnsi();
|
|
939
1476
|
const word = [...WORDMARK];
|
|
@@ -1057,211 +1594,4 @@ function printFatalNotice(file, memory) {
|
|
|
1057
1594
|
}
|
|
1058
1595
|
|
|
1059
1596
|
//#endregion
|
|
1060
|
-
|
|
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 };
|
|
1597
|
+
export { installAgentAlias as A, resolveWorkspaceId as B, themeVersion as C, machineOsFromPlatform as D, buildImportSeedPrompt as E, detectShell as F, 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, 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 };
|