@cam5/baby-bird 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +56 -4
- package/dist/{chunk-RWSCST2I.js → chunk-YI2UL4IC.js} +706 -52
- package/dist/chunk-YI2UL4IC.js.map +1 -0
- package/dist/cli.js +380 -23
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +317 -6
- package/dist/index.js +21 -1
- package/package.json +2 -1
- package/dist/chunk-RWSCST2I.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -4,30 +4,37 @@ import {
|
|
|
4
4
|
BbError,
|
|
5
5
|
CliRenderer,
|
|
6
6
|
CommandProvider,
|
|
7
|
+
ConfigError,
|
|
7
8
|
NotARepoError,
|
|
8
9
|
TourCache,
|
|
9
10
|
UsageError,
|
|
10
11
|
allPresets,
|
|
12
|
+
buildChatContext,
|
|
13
|
+
chatSlots,
|
|
11
14
|
describeSource2 as describeSource,
|
|
12
15
|
generateTour,
|
|
16
|
+
git,
|
|
13
17
|
gitRoot,
|
|
18
|
+
launchChat,
|
|
14
19
|
loadConfig,
|
|
15
20
|
prepareTour,
|
|
16
21
|
projectConfigPath,
|
|
17
22
|
relativeTime,
|
|
23
|
+
resolveChat,
|
|
18
24
|
resolveLlm,
|
|
25
|
+
wrap,
|
|
19
26
|
writeMaybePaged,
|
|
20
27
|
writeStdout
|
|
21
|
-
} from "./chunk-
|
|
28
|
+
} from "./chunk-YI2UL4IC.js";
|
|
22
29
|
|
|
23
30
|
// src/cli/main.ts
|
|
24
31
|
import { Command, InvalidArgumentError } from "commander";
|
|
25
|
-
import
|
|
32
|
+
import pc6 from "picocolors";
|
|
26
33
|
|
|
27
34
|
// package.json
|
|
28
35
|
var package_default = {
|
|
29
36
|
name: "@cam5/baby-bird",
|
|
30
|
-
version: "0.1.
|
|
37
|
+
version: "0.1.2",
|
|
31
38
|
description: "Guided code tours for git changes, on the command line.",
|
|
32
39
|
license: "MIT",
|
|
33
40
|
type: "module",
|
|
@@ -73,6 +80,7 @@ var package_default = {
|
|
|
73
80
|
},
|
|
74
81
|
dependencies: {
|
|
75
82
|
commander: "^15.0.0",
|
|
83
|
+
lowlight: "^3.3.0",
|
|
76
84
|
picocolors: "^1.1.1",
|
|
77
85
|
zod: "^4.6.2"
|
|
78
86
|
},
|
|
@@ -101,6 +109,7 @@ function flagsToOverrides(flags) {
|
|
|
101
109
|
else if (flags.color === false) o.render = { color: "never" };
|
|
102
110
|
if (flags.cache === false) o.cache = { enabled: false };
|
|
103
111
|
if (flags.pager === false) o.render = { ...o.render ?? {}, pager: "never" };
|
|
112
|
+
if (flags.highlight === false) o.render = { ...o.render ?? {}, highlight: "never" };
|
|
104
113
|
return o;
|
|
105
114
|
}
|
|
106
115
|
async function openSession(flags, opts) {
|
|
@@ -126,6 +135,10 @@ function resolveColor(mode) {
|
|
|
126
135
|
if (mode === "never") return false;
|
|
127
136
|
return Boolean(process.stdout.isTTY) && process.env.TERM !== "dumb";
|
|
128
137
|
}
|
|
138
|
+
function resolveHighlight(mode, colorEnabled) {
|
|
139
|
+
if (mode === "never") return false;
|
|
140
|
+
return colorEnabled;
|
|
141
|
+
}
|
|
129
142
|
function terminalWidth(configured, stream = process.stdout) {
|
|
130
143
|
return configured ?? (stream.columns || 80);
|
|
131
144
|
}
|
|
@@ -191,6 +204,13 @@ async function configCommand(flags) {
|
|
|
191
204
|
} catch (err) {
|
|
192
205
|
out.push(` ${c.red(err.message)}`);
|
|
193
206
|
}
|
|
207
|
+
try {
|
|
208
|
+
const chat = resolveChat(config);
|
|
209
|
+
const provider = new CommandProvider({ command: chat.command, promptVia: "arg", timeoutMs: 1 });
|
|
210
|
+
out.push(` chat: ${provider.describe()}${c.dim(" (bb ask)")}`);
|
|
211
|
+
} catch (err) {
|
|
212
|
+
out.push(` chat: ${c.dim(`none: ${err.message}`)}`);
|
|
213
|
+
}
|
|
194
214
|
out.push("");
|
|
195
215
|
out.push(c.bold("Presets"));
|
|
196
216
|
const presets = allPresets(config);
|
|
@@ -205,6 +225,311 @@ async function configCommand(flags) {
|
|
|
205
225
|
process.stdout.write(out.join("\n") + "\n");
|
|
206
226
|
}
|
|
207
227
|
|
|
228
|
+
// src/cli/ask-cmd.ts
|
|
229
|
+
import pc5 from "picocolors";
|
|
230
|
+
|
|
231
|
+
// src/cli/progress.ts
|
|
232
|
+
import pc4 from "picocolors";
|
|
233
|
+
var SPINNER = ["\u2722", "\u2733", "\u2736", "\u273B", "\u273D", "\u273B", "\u2736", "\u2733"];
|
|
234
|
+
var VERBS = ["Hatching", "Pecking", "Nesting", "Chirping", "Preening", "Fledging", "Brooding", "Warbling", "Peeping", "Squawking", "Swooping", "Tweeting", "Xeeting", "Cawing", "Twiddling About", "Hopping", "\u{1F425}-ing", "\u{1F423}-ing", "Masticating"];
|
|
235
|
+
var THINKING_TAIL_CHARS = 2e3;
|
|
236
|
+
var VERB_INTERVAL_MS = 7500;
|
|
237
|
+
var LiveProgress = class {
|
|
238
|
+
constructor(stream, opts) {
|
|
239
|
+
this.stream = stream;
|
|
240
|
+
this.opts = opts;
|
|
241
|
+
this.now = opts.now ?? Date.now;
|
|
242
|
+
this.startedAt = this.now();
|
|
243
|
+
this.verbs = opts.verb ? [opts.verb] : opts.verbs && opts.verbs.length ? opts.verbs : shuffle(VERBS);
|
|
244
|
+
this.windowLines = opts.windowLines ?? 5;
|
|
245
|
+
}
|
|
246
|
+
stream;
|
|
247
|
+
opts;
|
|
248
|
+
phaseLabel = "";
|
|
249
|
+
stage = "prep";
|
|
250
|
+
thinking = "";
|
|
251
|
+
pulses = 0;
|
|
252
|
+
answer = "";
|
|
253
|
+
titles = [];
|
|
254
|
+
notices = [];
|
|
255
|
+
outputBytes = 0;
|
|
256
|
+
usage = {};
|
|
257
|
+
startedAt;
|
|
258
|
+
frameIndex = 0;
|
|
259
|
+
lastLines = 0;
|
|
260
|
+
timer = null;
|
|
261
|
+
stopped = false;
|
|
262
|
+
verbs;
|
|
263
|
+
now;
|
|
264
|
+
windowLines;
|
|
265
|
+
onSigint = () => {
|
|
266
|
+
this.stop();
|
|
267
|
+
process.exit(130);
|
|
268
|
+
};
|
|
269
|
+
start() {
|
|
270
|
+
this.stream.write("\x1B[?25l");
|
|
271
|
+
process.once("SIGINT", this.onSigint);
|
|
272
|
+
this.timer = setInterval(() => {
|
|
273
|
+
this.frameIndex++;
|
|
274
|
+
this.render();
|
|
275
|
+
}, this.opts.frameMs ?? 80);
|
|
276
|
+
this.timer.unref?.();
|
|
277
|
+
this.render();
|
|
278
|
+
}
|
|
279
|
+
phase(label) {
|
|
280
|
+
this.phaseLabel = label;
|
|
281
|
+
if (/^asking/i.test(label)) {
|
|
282
|
+
this.stage = "thinking";
|
|
283
|
+
this.thinking = "";
|
|
284
|
+
this.pulses = 0;
|
|
285
|
+
this.answer = "";
|
|
286
|
+
this.titles = [];
|
|
287
|
+
this.notices = [];
|
|
288
|
+
this.outputBytes = 0;
|
|
289
|
+
} else if (this.stage !== "prep") {
|
|
290
|
+
this.stage = "prep";
|
|
291
|
+
}
|
|
292
|
+
this.render();
|
|
293
|
+
}
|
|
294
|
+
llm(event) {
|
|
295
|
+
switch (event.type) {
|
|
296
|
+
case "thinking":
|
|
297
|
+
this.thinking += event.text;
|
|
298
|
+
this.stage = "thinking";
|
|
299
|
+
break;
|
|
300
|
+
case "thinking-pulse":
|
|
301
|
+
this.pulses++;
|
|
302
|
+
this.stage = "thinking";
|
|
303
|
+
break;
|
|
304
|
+
case "answer-start":
|
|
305
|
+
this.answer = "";
|
|
306
|
+
this.titles = [];
|
|
307
|
+
break;
|
|
308
|
+
case "notice":
|
|
309
|
+
this.notices.push(event.text);
|
|
310
|
+
break;
|
|
311
|
+
case "text":
|
|
312
|
+
this.answer += event.text;
|
|
313
|
+
this.titles = extractTitles(this.answer);
|
|
314
|
+
this.stage = "writing";
|
|
315
|
+
break;
|
|
316
|
+
case "output":
|
|
317
|
+
this.outputBytes = event.bytes;
|
|
318
|
+
this.stage = "output";
|
|
319
|
+
break;
|
|
320
|
+
case "usage":
|
|
321
|
+
if (event.outputTokens !== void 0) this.usage.outputTokens = event.outputTokens;
|
|
322
|
+
if (event.thinkingTokens !== void 0) this.usage.thinkingTokens = event.thinkingTokens;
|
|
323
|
+
if (event.model !== void 0) this.usage.model = event.model;
|
|
324
|
+
break;
|
|
325
|
+
default:
|
|
326
|
+
break;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
/** The lines that make up the current status block. */
|
|
330
|
+
frame() {
|
|
331
|
+
const c = pc4.createColors(this.opts.color);
|
|
332
|
+
const width = Math.max(30, this.opts.width);
|
|
333
|
+
const spin = c.magenta(SPINNER[this.frameIndex % SPINNER.length]);
|
|
334
|
+
const elapsed = this.elapsed();
|
|
335
|
+
const fit = (s) => s.length > width - 1 ? s.slice(0, width - 2) + "\u2026" : s;
|
|
336
|
+
const lines = [];
|
|
337
|
+
switch (this.stage) {
|
|
338
|
+
case "prep":
|
|
339
|
+
lines.push(`${spin} ${fit(this.phaseLabel || "Starting")}\u2026 ${c.dim(elapsed)}`);
|
|
340
|
+
break;
|
|
341
|
+
case "thinking": {
|
|
342
|
+
const meta = [elapsed, this.phaseLabel.replace(/^asking\s+/i, "")].filter(Boolean).join(" \xB7 ");
|
|
343
|
+
lines.push(`${spin} ${this.verb()}\u2026 ${c.dim(meta)}`);
|
|
344
|
+
for (const n of this.notices.slice(-1)) lines.push(` ${c.yellow("\u21BB")} ${c.yellow(fit(n).slice(0, width - 4))}`);
|
|
345
|
+
if (this.thinking) {
|
|
346
|
+
const tail = this.thinking.slice(-THINKING_TAIL_CHARS).replace(/\s+/g, " ").trim();
|
|
347
|
+
const wrapped = wrap(tail, width - 4).slice(-this.windowLines);
|
|
348
|
+
for (const l of wrapped) lines.push(` ${c.dim("\u2502")} ${c.dim(l)}`);
|
|
349
|
+
} else if (this.pulses > 0) {
|
|
350
|
+
const room = Math.max(8, width - 16);
|
|
351
|
+
const n = this.pulses % (room + 1);
|
|
352
|
+
lines.push(` ${c.dim("\u2502")} ${c.dim("reasoning")} ${c.magenta("\xB7".repeat(n))}`);
|
|
353
|
+
}
|
|
354
|
+
break;
|
|
355
|
+
}
|
|
356
|
+
case "writing": {
|
|
357
|
+
const n = Math.max(0, this.titles.length - 1);
|
|
358
|
+
lines.push(`${spin} Writing the tour\u2026 ${c.dim(`${elapsed} \xB7 ${n} section${n === 1 ? "" : "s"} so far`)}`);
|
|
359
|
+
for (const note of this.notices.slice(-1)) lines.push(` ${c.yellow("\u21BB")} ${c.yellow(fit(note).slice(0, width - 4))}`);
|
|
360
|
+
const [tourTitle, ...sections] = this.titles;
|
|
361
|
+
const items = [];
|
|
362
|
+
if (tourTitle) items.push(` \u{1F423} ${c.bold(fit(tourTitle).slice(0, width - 6))}`);
|
|
363
|
+
sections.forEach((t) => items.push(` ${c.green("\u2713")} ${fit(t).slice(0, width - 5)}`));
|
|
364
|
+
for (const l of items.slice(-this.windowLines)) lines.push(l);
|
|
365
|
+
break;
|
|
366
|
+
}
|
|
367
|
+
case "output":
|
|
368
|
+
lines.push(`${spin} ${this.verb()}\u2026 ${c.dim(`${elapsed} \xB7 ${formatBytes(this.outputBytes)} received`)}`);
|
|
369
|
+
break;
|
|
370
|
+
}
|
|
371
|
+
return lines;
|
|
372
|
+
}
|
|
373
|
+
render() {
|
|
374
|
+
if (this.stopped) return;
|
|
375
|
+
const lines = this.frame();
|
|
376
|
+
this.stream.write(this.erase() + lines.join("\n") + "\n");
|
|
377
|
+
this.lastLines = lines.length;
|
|
378
|
+
}
|
|
379
|
+
/** Clear the block, restore the cursor, and optionally leave a final line behind. */
|
|
380
|
+
stop(finalLine) {
|
|
381
|
+
if (this.stopped) return;
|
|
382
|
+
this.stopped = true;
|
|
383
|
+
if (this.timer) clearInterval(this.timer);
|
|
384
|
+
process.removeListener("SIGINT", this.onSigint);
|
|
385
|
+
this.stream.write(this.erase() + (finalLine ? finalLine + "\n" : "") + "\x1B[?25h");
|
|
386
|
+
this.lastLines = 0;
|
|
387
|
+
}
|
|
388
|
+
/** A one-line summary of the model call, for the final line. */
|
|
389
|
+
summary() {
|
|
390
|
+
const c = pc4.createColors(this.opts.color);
|
|
391
|
+
const parts = [`Hatched in ${this.elapsed()}`];
|
|
392
|
+
if (this.usage.outputTokens) {
|
|
393
|
+
parts.push(`${formatCount(this.usage.outputTokens)} tokens${this.usage.thinkingTokens ? ` (${formatCount(this.usage.thinkingTokens)} thinking)` : ""}`);
|
|
394
|
+
}
|
|
395
|
+
if (this.usage.model) parts.push(this.usage.model);
|
|
396
|
+
return c.dim(`\u273B ${parts.join(" \xB7 ")}`);
|
|
397
|
+
}
|
|
398
|
+
/** The current verb; rotates every verbIntervalMs so long waits do not stare at one word. */
|
|
399
|
+
verb() {
|
|
400
|
+
const step = Math.floor((this.now() - this.startedAt) / (this.opts.verbIntervalMs ?? VERB_INTERVAL_MS));
|
|
401
|
+
return this.verbs[step % this.verbs.length];
|
|
402
|
+
}
|
|
403
|
+
elapsed() {
|
|
404
|
+
const s = Math.max(0, Math.round((this.now() - this.startedAt) / 1e3));
|
|
405
|
+
return s >= 60 ? `${Math.floor(s / 60)}m ${s % 60}s` : `${s}s`;
|
|
406
|
+
}
|
|
407
|
+
erase() {
|
|
408
|
+
return this.lastLines ? `\x1B[${this.lastLines}A\x1B[0J` : "";
|
|
409
|
+
}
|
|
410
|
+
};
|
|
411
|
+
function extractTitles(text) {
|
|
412
|
+
const out = [];
|
|
413
|
+
for (const m of text.matchAll(/"title"\s*:\s*"((?:[^"\\]|\\.)*)"/g)) {
|
|
414
|
+
out.push(m[1].replace(/\\(["\\/bfnrt]|u[0-9a-fA-F]{4})/g, (_, esc) => {
|
|
415
|
+
switch (esc[0]) {
|
|
416
|
+
case '"':
|
|
417
|
+
return '"';
|
|
418
|
+
case "\\":
|
|
419
|
+
return "\\";
|
|
420
|
+
case "/":
|
|
421
|
+
return "/";
|
|
422
|
+
case "n":
|
|
423
|
+
return "\n";
|
|
424
|
+
case "t":
|
|
425
|
+
return " ";
|
|
426
|
+
case "u":
|
|
427
|
+
return String.fromCharCode(parseInt(esc.slice(1), 16));
|
|
428
|
+
default:
|
|
429
|
+
return "";
|
|
430
|
+
}
|
|
431
|
+
}));
|
|
432
|
+
}
|
|
433
|
+
return out;
|
|
434
|
+
}
|
|
435
|
+
function loggingProgress(log) {
|
|
436
|
+
return {
|
|
437
|
+
phase: (label) => log(label),
|
|
438
|
+
llm: () => {
|
|
439
|
+
}
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
function shuffle(items) {
|
|
443
|
+
const out = [...items];
|
|
444
|
+
for (let i = out.length - 1; i > 0; i--) {
|
|
445
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
446
|
+
[out[i], out[j]] = [out[j], out[i]];
|
|
447
|
+
}
|
|
448
|
+
return out;
|
|
449
|
+
}
|
|
450
|
+
function formatBytes(n) {
|
|
451
|
+
return n < 1024 ? `${n} B` : `${(n / 1024).toFixed(1)} KB`;
|
|
452
|
+
}
|
|
453
|
+
function formatCount(n) {
|
|
454
|
+
return n >= 1e3 ? `${(n / 1e3).toFixed(1)}k` : String(n);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// src/cli/ask-cmd.ts
|
|
458
|
+
async function splitAskArgs(words, opts) {
|
|
459
|
+
const [first, ...rest] = words;
|
|
460
|
+
if (first !== void 0 && opts.rangeAllowed && await isRange(first, opts.cwd)) {
|
|
461
|
+
return { range: first, message: joinMessage(rest) };
|
|
462
|
+
}
|
|
463
|
+
return { message: joinMessage(words) };
|
|
464
|
+
}
|
|
465
|
+
async function isRange(word, cwd) {
|
|
466
|
+
if (!word || /\s/.test(word)) return false;
|
|
467
|
+
const res = await git(["rev-parse", "--quiet", "--end-of-options", word], { cwd, allowFailure: true });
|
|
468
|
+
return res.code === 0;
|
|
469
|
+
}
|
|
470
|
+
function joinMessage(words) {
|
|
471
|
+
const text = words.join(" ").trim();
|
|
472
|
+
return text ? text : void 0;
|
|
473
|
+
}
|
|
474
|
+
async function askCommand(words, flags) {
|
|
475
|
+
const session = await openSession(flags, { requireRepo: true });
|
|
476
|
+
if (!session.root) throw new NotARepoError(session.cwd);
|
|
477
|
+
const { config } = session.loaded;
|
|
478
|
+
const c = pc5.createColors(session.colorEnabled);
|
|
479
|
+
const chat = resolveChat(config);
|
|
480
|
+
const slots = chatSlots(chat.command);
|
|
481
|
+
if (!slots.context) {
|
|
482
|
+
throw new ConfigError("The chat command has no {context-file} or {context} token, so the tour cannot be handed to it.", "Add one of them to the chatCommand; see the README.");
|
|
483
|
+
}
|
|
484
|
+
const llm = resolveLlm(config);
|
|
485
|
+
const { range, message } = await splitAskArgs(words, { cwd: session.root, rangeAllowed: !flags.working && !flags.staged });
|
|
486
|
+
session.debug(range ? `range: ${range}` : "no range argument");
|
|
487
|
+
session.debug(message ? `opening message: ${JSON.stringify(message)}` : "no opening message");
|
|
488
|
+
const live = flags.progress !== false && !flags.debug && Boolean(process.stderr.isTTY) && !flags.dumpContext;
|
|
489
|
+
const progress = live ? new LiveProgress(process.stderr, { color: session.colorEnabled, width: terminalWidth(null, process.stderr) }) : loggingProgress(session.debug);
|
|
490
|
+
const options = {
|
|
491
|
+
cwd: session.root,
|
|
492
|
+
config,
|
|
493
|
+
cacheDir: session.loaded.cacheDir,
|
|
494
|
+
range: { arg: range, working: flags.working, staged: flags.staged },
|
|
495
|
+
refresh: flags.refresh,
|
|
496
|
+
noCache: flags.cache === false,
|
|
497
|
+
debug: session.debug,
|
|
498
|
+
warn: session.warn,
|
|
499
|
+
progress
|
|
500
|
+
};
|
|
501
|
+
if (progress instanceof LiveProgress) progress.start();
|
|
502
|
+
let prepared;
|
|
503
|
+
let result;
|
|
504
|
+
try {
|
|
505
|
+
prepared = await prepareTour(options);
|
|
506
|
+
result = await generateTour(options, prepared);
|
|
507
|
+
} catch (err) {
|
|
508
|
+
if (progress instanceof LiveProgress) progress.stop();
|
|
509
|
+
throw err;
|
|
510
|
+
}
|
|
511
|
+
if (progress instanceof LiveProgress) progress.stop(result.fromCache ? void 0 : progress.summary());
|
|
512
|
+
const context = buildChatContext({ tour: result.tour, change: prepared.context, maxBytes: llm.maxPromptBytes, tourPath: result.cachePath });
|
|
513
|
+
if (context.truncation.truncated.length || context.truncation.omitted.length) {
|
|
514
|
+
session.warn(`Chat context exceeded ${llm.maxPromptBytes} bytes; truncated ${context.truncation.truncated.length} and omitted ${context.truncation.omitted.length} file diff(s).`);
|
|
515
|
+
}
|
|
516
|
+
if (flags.dumpContext) {
|
|
517
|
+
await writeStdout(context.text.endsWith("\n") ? context.text : context.text + "\n");
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
const tour = result.tour;
|
|
521
|
+
const when = result.fromCache ? `cached ${relativeTime(tour.generatedAt)}` : "generated just now";
|
|
522
|
+
process.stderr.write(`${c.bold(`\u{1F423} ${tour.title}`)}${c.dim(` \xB7 ${describeSource(tour.source)} \xB7 ${when}`)}
|
|
523
|
+
`);
|
|
524
|
+
if (message && !slots.message) {
|
|
525
|
+
session.warn("This chat command has no {message} slot; type your question into the chat once it opens.");
|
|
526
|
+
}
|
|
527
|
+
process.stderr.write(c.dim(`Opening ${chat.preset ?? chat.command[0]} with the tour as context\u2026
|
|
528
|
+
`));
|
|
529
|
+
const outcome = await launchChat({ command: chat.command, context: context.text, message, cwd: session.root, env: chat.env, debug: session.debug });
|
|
530
|
+
process.exitCode = outcome.exitCode;
|
|
531
|
+
}
|
|
532
|
+
|
|
208
533
|
// src/cli/init-cmd.ts
|
|
209
534
|
import { mkdir, writeFile } from "fs/promises";
|
|
210
535
|
import { dirname } from "path";
|
|
@@ -232,6 +557,8 @@ async function tourCommand(rangeArg, flags) {
|
|
|
232
557
|
const session = await openSession(flags, { requireRepo: true });
|
|
233
558
|
if (!session.root) throw new NotARepoError(session.cwd);
|
|
234
559
|
const { config } = session.loaded;
|
|
560
|
+
const live = flags.progress !== false && !flags.debug && Boolean(process.stderr.isTTY) && !flags.dumpPrompt;
|
|
561
|
+
const progress = live ? new LiveProgress(process.stderr, { color: session.colorEnabled, width: terminalWidth(null, process.stderr) }) : loggingProgress(session.debug);
|
|
235
562
|
const options = {
|
|
236
563
|
cwd: session.root,
|
|
237
564
|
config,
|
|
@@ -240,14 +567,23 @@ async function tourCommand(rangeArg, flags) {
|
|
|
240
567
|
refresh: flags.refresh,
|
|
241
568
|
noCache: flags.cache === false,
|
|
242
569
|
debug: session.debug,
|
|
243
|
-
warn: session.warn
|
|
570
|
+
warn: session.warn,
|
|
571
|
+
progress
|
|
244
572
|
};
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
573
|
+
if (progress instanceof LiveProgress) progress.start();
|
|
574
|
+
let result;
|
|
575
|
+
try {
|
|
576
|
+
const prepared = await prepareTour(options);
|
|
577
|
+
if (flags.dumpPrompt) {
|
|
578
|
+
await writeStdout(prepared.built.prompt.endsWith("\n") ? prepared.built.prompt : prepared.built.prompt + "\n");
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
result = await generateTour(options, prepared);
|
|
582
|
+
} catch (err) {
|
|
583
|
+
if (progress instanceof LiveProgress) progress.stop();
|
|
584
|
+
throw err;
|
|
249
585
|
}
|
|
250
|
-
|
|
586
|
+
if (progress instanceof LiveProgress) progress.stop(result.fromCache ? void 0 : progress.summary());
|
|
251
587
|
if (flags.json) {
|
|
252
588
|
await writeStdout(JSON.stringify(result.tour, null, 2) + "\n");
|
|
253
589
|
return;
|
|
@@ -257,7 +593,8 @@ async function tourCommand(rangeArg, flags) {
|
|
|
257
593
|
color: session.colorEnabled,
|
|
258
594
|
width: terminalWidth(config.render.width),
|
|
259
595
|
section: flags.section,
|
|
260
|
-
fromCache: result.fromCache
|
|
596
|
+
fromCache: result.fromCache,
|
|
597
|
+
highlight: resolveHighlight(config.render.highlight, session.colorEnabled)
|
|
261
598
|
});
|
|
262
599
|
await writeMaybePaged(output, {
|
|
263
600
|
mode: config.render.pager,
|
|
@@ -272,6 +609,9 @@ function positiveInt(value) {
|
|
|
272
609
|
if (!Number.isInteger(n) || n < 1) throw new InvalidArgumentError("must be a positive integer");
|
|
273
610
|
return n;
|
|
274
611
|
}
|
|
612
|
+
function optsWithGlobals(cmd) {
|
|
613
|
+
return cmd.optsWithGlobals();
|
|
614
|
+
}
|
|
275
615
|
function addGlobalFlags(cmd) {
|
|
276
616
|
return cmd.option("-C, --cwd <dir>", "run as if started in <dir>").option("--preset <name>", "LLM preset to use (see `bb config`)").option("--color", "force colored output").option("--no-color", "disable colored output").option("--debug", "print diagnostics (resolved range, prompt size, raw model output) to stderr");
|
|
277
617
|
}
|
|
@@ -280,8 +620,8 @@ process.stdout.on("error", (err) => {
|
|
|
280
620
|
throw err;
|
|
281
621
|
});
|
|
282
622
|
var program = new Command();
|
|
283
|
-
program.name("bb").description("Guided code tours for git changes, on the command line.").version(package_default.version, "-V, --version").configureOutput({ outputError: (str, write) => write(
|
|
284
|
-
addGlobalFlags(program).argument("[range]", "what to tour: A..B, A...B, or a single ref (compared at its merge-base with HEAD)").option("-w, --working", "tour uncommitted changes in the working tree (tracked and untracked)").option("-s, --staged", "tour staged changes only").option("--section <n>", "render only section <n>", positiveInt).option("--json", "print the tour as JSON instead of rendering it").option("--dump-prompt", "print the prompt that would be sent to the model and exit").option("--refresh", "ignore a cached tour and regenerate it").option("--no-cache", "neither read nor write the tour cache").option("--no-pager", "never pipe output through a pager").addHelpText(
|
|
623
|
+
program.name("bb").description("Guided code tours for git changes, on the command line.").version(package_default.version, "-V, --version").enablePositionalOptions().configureOutput({ outputError: (str, write) => write(pc6.red(str)) }).showHelpAfterError("(run with --help for usage)");
|
|
624
|
+
addGlobalFlags(program).argument("[range]", "what to tour: A..B, A...B, or a single ref (compared at its merge-base with HEAD)").option("-w, --working", "tour uncommitted changes in the working tree (tracked and untracked)").option("-s, --staged", "tour staged changes only").option("--section <n>", "render only section <n>", positiveInt).option("--json", "print the tour as JSON instead of rendering it").option("--dump-prompt", "print the prompt that would be sent to the model and exit").option("--refresh", "ignore a cached tour and regenerate it").option("--no-cache", "neither read nor write the tour cache").option("--no-pager", "never pipe output through a pager").option("--no-highlight", "plain excerpts with green/red +/- signs instead of syntax colors").option("--no-progress", "do not show the live status block while generating").addHelpText(
|
|
285
625
|
"after",
|
|
286
626
|
`
|
|
287
627
|
Examples:
|
|
@@ -292,34 +632,51 @@ Examples:
|
|
|
292
632
|
bb --section 2 show only the second section
|
|
293
633
|
bb --json | jq machine-readable output
|
|
294
634
|
bb --preset claude-sonnet --refresh
|
|
635
|
+
bb ask HEAD~1 "what changed in progress.ts?" chat about a tour (see bb ask --help)
|
|
295
636
|
`
|
|
296
637
|
).action(async (range, flags) => {
|
|
297
638
|
await tourCommand(range, flags);
|
|
298
639
|
});
|
|
640
|
+
addGlobalFlags(program.command("ask").description("chat with the LLM about a tour (the tour is generated or taken from the cache first)")).argument("[range]", "what to tour, as for `bb`; omit it to tour the same thing bare `bb` would").argument("[message...]", "opening message for the chat").option("-w, --working", "tour uncommitted changes in the working tree (tracked and untracked)").option("-s, --staged", "tour staged changes only").option("--refresh", "ignore a cached tour and regenerate it").option("--no-cache", "neither read nor write the tour cache").option("--no-progress", "do not show the live status block while generating").option("--dump-context", "print the context that would be handed to the chat and exit").addHelpText(
|
|
641
|
+
"after",
|
|
642
|
+
`
|
|
643
|
+
The first word is taken as a range only when git can resolve it (HEAD~3, main..feature);
|
|
644
|
+
everything else is the opening message. The chat command comes from the preset (see \`bb config\`).
|
|
645
|
+
|
|
646
|
+
Examples:
|
|
647
|
+
bb ask chat about the current branch's tour
|
|
648
|
+
bb ask "why does the cache key include the command?"
|
|
649
|
+
bb ask HEAD~1 "what is going on in progress.ts?"
|
|
650
|
+
bb ask main..feature --preset claude-opus
|
|
651
|
+
bb ask --staged "is this safe to commit?"
|
|
652
|
+
`
|
|
653
|
+
).action(async (range, message, _flags, cmd) => {
|
|
654
|
+
await askCommand([range, ...message].filter((w) => w !== void 0), optsWithGlobals(cmd));
|
|
655
|
+
});
|
|
299
656
|
var cache = program.command("cache").description("manage cached tours");
|
|
300
|
-
addGlobalFlags(cache.command("ls").description("list cached tours")).action(cacheLsCommand);
|
|
301
|
-
addGlobalFlags(cache.command("clear").description("delete all cached tours")).action(cacheClearCommand);
|
|
302
|
-
addGlobalFlags(cache.command("path").description("print the cache directory")).action(cachePathCommand);
|
|
303
|
-
addGlobalFlags(program.command("config").description("show the effective configuration, its sources, and available presets")).option("--json", "print only the effective config as JSON").action(configCommand);
|
|
304
|
-
addGlobalFlags(program.command("init").description("write a starter .baby-bird/config.json in this repository")).option("--force", "overwrite an existing project config").action(initCommand);
|
|
657
|
+
addGlobalFlags(cache.command("ls").description("list cached tours")).action((_flags, cmd) => cacheLsCommand(optsWithGlobals(cmd)));
|
|
658
|
+
addGlobalFlags(cache.command("clear").description("delete all cached tours")).action((_flags, cmd) => cacheClearCommand(optsWithGlobals(cmd)));
|
|
659
|
+
addGlobalFlags(cache.command("path").description("print the cache directory")).action((_flags, cmd) => cachePathCommand(optsWithGlobals(cmd)));
|
|
660
|
+
addGlobalFlags(program.command("config").description("show the effective configuration, its sources, and available presets")).option("--json", "print only the effective config as JSON").action((_flags, cmd) => configCommand(optsWithGlobals(cmd)));
|
|
661
|
+
addGlobalFlags(program.command("init").description("write a starter .baby-bird/config.json in this repository")).option("--force", "overwrite an existing project config").action((_flags, cmd) => initCommand(optsWithGlobals(cmd)));
|
|
305
662
|
program.parseAsync(process.argv).catch((err) => {
|
|
306
663
|
const debug = process.argv.includes("--debug");
|
|
307
664
|
if (err instanceof BbError) {
|
|
308
|
-
process.stderr.write(
|
|
665
|
+
process.stderr.write(pc6.red(`error: ${err.message}
|
|
309
666
|
`));
|
|
310
|
-
if (err.hint) process.stderr.write(
|
|
667
|
+
if (err.hint) process.stderr.write(pc6.dim(`${err.hint}
|
|
311
668
|
`));
|
|
312
|
-
if (debug && err instanceof BadLlmOutputError) process.stderr.write(
|
|
669
|
+
if (debug && err instanceof BadLlmOutputError) process.stderr.write(pc6.dim(`--- raw model output ---
|
|
313
670
|
${err.raw}
|
|
314
671
|
`));
|
|
315
|
-
if (debug && err.cause) process.stderr.write(
|
|
672
|
+
if (debug && err.cause) process.stderr.write(pc6.dim(`${String(err.cause.stack ?? err.cause)}
|
|
316
673
|
`));
|
|
317
674
|
process.exitCode = err.exitCode;
|
|
318
675
|
return;
|
|
319
676
|
}
|
|
320
|
-
process.stderr.write(
|
|
677
|
+
process.stderr.write(pc6.red(`error: ${err.message ?? String(err)}
|
|
321
678
|
`));
|
|
322
|
-
if (debug && err.stack) process.stderr.write(
|
|
679
|
+
if (debug && err.stack) process.stderr.write(pc6.dim(`${err.stack}
|
|
323
680
|
`));
|
|
324
681
|
process.exitCode = 1;
|
|
325
682
|
});
|