buildcrew 1.9.1 → 1.9.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/bin/watch.js +83 -24
- package/lib/install-hooks.js +18 -2
- package/package.json +1 -1
package/bin/watch.js
CHANGED
|
@@ -32,11 +32,23 @@ const c = NO_COLOR
|
|
|
32
32
|
reset: "\x1b[0m", bold: "\x1b[1m", dim: "\x1b[2m",
|
|
33
33
|
black: "\x1b[30m", red: "\x1b[31m", green: "\x1b[32m",
|
|
34
34
|
gold: "\x1b[33m", blue: "\x1b[34m", mag: "\x1b[35m",
|
|
35
|
-
cyan: "\x1b[36m",
|
|
35
|
+
cyan: "\x1b[36m",
|
|
36
|
+
// Primary secondary text — readable on dark terminals (was \x1b[90m which rendered too dim)
|
|
37
|
+
gray: "\x1b[38;5;250m",
|
|
38
|
+
// Muted — for truly tertiary metadata (timestamps, separators)
|
|
39
|
+
muted: "\x1b[38;5;244m",
|
|
36
40
|
bgWood: "\x1b[48;5;94m",
|
|
37
41
|
};
|
|
38
42
|
|
|
39
|
-
|
|
43
|
+
// Anti-flicker rendering primitives.
|
|
44
|
+
// HOME moves cursor to top-left WITHOUT clearing — we overwrite in place and
|
|
45
|
+
// use CLR_EOL per line + CLR_BELOW at the end to erase leftovers. The old
|
|
46
|
+
// `\x1b[2J\x1b[H` caused a visible flash every frame (blank → redraw).
|
|
47
|
+
const HOME = "\x1b[H";
|
|
48
|
+
const CLR_EOL = "\x1b[K"; // clear from cursor to end of line
|
|
49
|
+
const CLR_BELOW = "\x1b[J"; // clear from cursor to end of screen
|
|
50
|
+
const ALT_SCREEN_ON = "\x1b[?1049h";
|
|
51
|
+
const ALT_SCREEN_OFF = "\x1b[?1049l";
|
|
40
52
|
const HIDE_CURSOR = "\x1b[?25l";
|
|
41
53
|
const SHOW_CURSOR = "\x1b[?25h";
|
|
42
54
|
|
|
@@ -148,12 +160,37 @@ function handleEvent(ev) {
|
|
|
148
160
|
|
|
149
161
|
switch (ev.type) {
|
|
150
162
|
case "session.start":
|
|
163
|
+
// New session → clear per-session state. Watch is a live observer for the
|
|
164
|
+
// current session; persistent project progress belongs in docs/ (PDCA).
|
|
165
|
+
// Keep: coherence (file-derived), session metadata itself.
|
|
166
|
+
state.currentStage = null;
|
|
167
|
+
state.completedStages = new Set();
|
|
168
|
+
state.activeAgents = new Map();
|
|
169
|
+
state.completedAgents = new Map();
|
|
170
|
+
state.events = 0;
|
|
171
|
+
state.files = 0;
|
|
172
|
+
state.issues = { critical: 0, high: 0, med: 0, low: 0 };
|
|
173
|
+
state.recent = [];
|
|
174
|
+
state.recentFiles = [];
|
|
175
|
+
state.recentIssues = [];
|
|
151
176
|
state.sessionStartAt = at;
|
|
152
177
|
state.sessionEndAt = null;
|
|
153
178
|
if (ev.session_id) state.sessionId = ev.session_id;
|
|
154
179
|
break;
|
|
155
180
|
case "session.end":
|
|
156
181
|
state.sessionEndAt = at;
|
|
182
|
+
// Sweep any agents still marked active — completed events can be missed
|
|
183
|
+
// (e.g. @mentions in prompt text that hook logs as dispatched but never
|
|
184
|
+
// actually invoke the Agent tool). Session end implies nothing is running.
|
|
185
|
+
for (const id of [...state.activeAgents.keys()]) {
|
|
186
|
+
const a = state.activeAgents.get(id);
|
|
187
|
+
state.activeAgents.delete(id);
|
|
188
|
+
state.completedAgents.set(id, {
|
|
189
|
+
lastAt: at,
|
|
190
|
+
duration: Math.max(0, at - a.startAt),
|
|
191
|
+
summary: "",
|
|
192
|
+
});
|
|
193
|
+
}
|
|
157
194
|
break;
|
|
158
195
|
case "agent.dispatched": {
|
|
159
196
|
if (!ev.agent) break;
|
|
@@ -284,7 +321,7 @@ function renderNow(width) {
|
|
|
284
321
|
const emoji = (AGENTS.find(a => a.id === id)?.emoji) ?? "●";
|
|
285
322
|
const elapsed = formatDuration(Math.floor((now - info.startAt) / 1000));
|
|
286
323
|
const prompt = truncate(info.prompt, Math.max(20, width - 28));
|
|
287
|
-
console.log(` ${c.gold}●${c.reset} ${emoji} ${c.bold}${id}${c.reset} ${c.
|
|
324
|
+
console.log(` ${c.gold}●${c.reset} ${emoji} ${c.bold}${id}${c.reset} ${c.muted}${elapsed} ·${c.reset} ${prompt}`);
|
|
288
325
|
}
|
|
289
326
|
}
|
|
290
327
|
console.log("");
|
|
@@ -411,7 +448,7 @@ function formatEvent(ev, maxLen) {
|
|
|
411
448
|
let body;
|
|
412
449
|
switch (ev.type) {
|
|
413
450
|
case "agent.dispatched":
|
|
414
|
-
body = `${c.gold}▶${c.reset} ${c.bold}${ev.agent ?? "?"}${c.reset} ${c.
|
|
451
|
+
body = `${c.gold}▶${c.reset} ${c.bold}${ev.agent ?? "?"}${c.reset} ${c.muted}·${c.reset} ${truncate(ev.prompt, 60)}`;
|
|
415
452
|
break;
|
|
416
453
|
case "agent.completed":
|
|
417
454
|
body = `${c.green}✓${c.reset} ${ev.agent ?? "*"} ${c.gray}done${c.reset}`;
|
|
@@ -439,16 +476,36 @@ function formatEvent(ev, maxLen) {
|
|
|
439
476
|
|
|
440
477
|
function render() {
|
|
441
478
|
const width = Math.max(60, process.stdout.columns ?? 80);
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
479
|
+
|
|
480
|
+
// Capture every render*() call's output into an in-memory buffer by
|
|
481
|
+
// monkey-patching console.log for the duration of the render. This lets us
|
|
482
|
+
// emit the whole frame in a single process.stdout.write — eliminating the
|
|
483
|
+
// per-line flicker that came from 30+ separate writes.
|
|
484
|
+
const lines = [];
|
|
485
|
+
const origLog = console.log;
|
|
486
|
+
console.log = (...args) => {
|
|
487
|
+
lines.push(args.length === 0 ? "" : args.map(String).join(" "));
|
|
488
|
+
};
|
|
489
|
+
try {
|
|
490
|
+
renderHeader();
|
|
491
|
+
renderNow(width);
|
|
492
|
+
renderPipeline(width);
|
|
493
|
+
renderAgents(width);
|
|
494
|
+
renderFiles(width);
|
|
495
|
+
renderIssues(width);
|
|
496
|
+
renderCoherence(width);
|
|
497
|
+
renderRecent(width);
|
|
498
|
+
} finally {
|
|
499
|
+
console.log = origLog;
|
|
500
|
+
}
|
|
501
|
+
lines.push("");
|
|
502
|
+
lines.push(`${c.gray}q quit · r show full coherence report${c.reset}`);
|
|
503
|
+
|
|
504
|
+
// Single atomic frame: cursor home → each line + clear-to-EOL (erases any
|
|
505
|
+
// leftover chars from a previous longer line) → clear-below (handles frame
|
|
506
|
+
// shrinkage). No `\x1b[2J` flash.
|
|
507
|
+
const frame = HOME + lines.map(l => l + CLR_EOL).join("\n") + "\n" + CLR_BELOW;
|
|
508
|
+
process.stdout.write(frame);
|
|
452
509
|
}
|
|
453
510
|
|
|
454
511
|
// ------------------------------------------------------------------
|
|
@@ -520,10 +577,13 @@ function subscribeTail() {
|
|
|
520
577
|
// ------------------------------------------------------------------
|
|
521
578
|
// Bootstrap
|
|
522
579
|
// ------------------------------------------------------------------
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
process.
|
|
526
|
-
|
|
580
|
+
// Enter alternate screen so the dashboard doesn't scribble over the user's
|
|
581
|
+
// scrollback. On exit we return the terminal to its pre-watch state.
|
|
582
|
+
process.stdout.write(ALT_SCREEN_ON + HIDE_CURSOR);
|
|
583
|
+
const restoreTerm = () => process.stdout.write(SHOW_CURSOR + ALT_SCREEN_OFF);
|
|
584
|
+
process.on("exit", restoreTerm);
|
|
585
|
+
process.on("SIGINT", () => { restoreTerm(); process.exit(0); });
|
|
586
|
+
process.on("SIGTERM", () => { restoreTerm(); process.exit(0); });
|
|
527
587
|
|
|
528
588
|
// Keypress handlers: q/Ctrl-C quit, r open full coherence report
|
|
529
589
|
if (process.stdin.isTTY) {
|
|
@@ -536,20 +596,19 @@ if (process.stdin.isTTY) {
|
|
|
536
596
|
}
|
|
537
597
|
if (key?.name === "r") {
|
|
538
598
|
// Open the full coherence report. Hand off the terminal to setup.js's
|
|
539
|
-
// report subcommand which uses `less -R` for paging.
|
|
540
|
-
//
|
|
541
|
-
process.stdout.write(SHOW_CURSOR);
|
|
599
|
+
// report subcommand which uses `less -R` for paging. Leave the alt
|
|
600
|
+
// screen so less paints on the main buffer; re-enter on return.
|
|
601
|
+
process.stdout.write(SHOW_CURSOR + ALT_SCREEN_OFF);
|
|
542
602
|
process.stdin.setRawMode(false);
|
|
543
|
-
process.stdout.write(CLEAR);
|
|
544
603
|
const setupEntry = resolve(__dirname, "setup.js");
|
|
545
604
|
spawnSync(process.execPath, [setupEntry, "report"], {
|
|
546
605
|
stdio: "inherit",
|
|
547
606
|
cwd: process.cwd(),
|
|
548
607
|
env: process.env,
|
|
549
608
|
});
|
|
550
|
-
// Restore raw mode + hide cursor + redraw
|
|
609
|
+
// Restore raw mode + alt screen + hide cursor + redraw
|
|
551
610
|
process.stdin.setRawMode(true);
|
|
552
|
-
process.stdout.write(HIDE_CURSOR);
|
|
611
|
+
process.stdout.write(ALT_SCREEN_ON + HIDE_CURSOR);
|
|
553
612
|
scheduleRender();
|
|
554
613
|
}
|
|
555
614
|
});
|
package/lib/install-hooks.js
CHANGED
|
@@ -2,10 +2,18 @@
|
|
|
2
2
|
* buildcrew CC hook installer.
|
|
3
3
|
*
|
|
4
4
|
* Registers hook entries in .claude/settings.json that invoke
|
|
5
|
-
* `
|
|
5
|
+
* `node <abs-path>/lib/hook.js <kind>` on each agent/file event. The hook writes
|
|
6
6
|
* a styled banner to the terminal AND appends to events.jsonl so that
|
|
7
7
|
* `npx buildcrew watch` can show a live view in a separate pane.
|
|
8
8
|
*
|
|
9
|
+
* We resolve an absolute path to the installed buildcrew package's hook.js at
|
|
10
|
+
* install time rather than using `npx buildcrew-hook` because:
|
|
11
|
+
* - Bare `npx buildcrew-hook` looks up a package literally named
|
|
12
|
+
* "buildcrew-hook" → E404 (it's a bin inside the `buildcrew` package).
|
|
13
|
+
* - `npx -p buildcrew buildcrew-hook` works but re-fetches on cache miss and
|
|
14
|
+
* adds 200-500ms latency per CC hook invocation.
|
|
15
|
+
* - Absolute node path is zero-overhead and immune to npx cache eviction.
|
|
16
|
+
*
|
|
9
17
|
* Idempotent — re-install replaces prior buildcrew entries without
|
|
10
18
|
* touching other hooks or permissions in the file.
|
|
11
19
|
*/
|
|
@@ -13,9 +21,15 @@
|
|
|
13
21
|
import { promises as fsp } from "node:fs";
|
|
14
22
|
import path from "node:path";
|
|
15
23
|
import os from "node:os";
|
|
24
|
+
import { fileURLToPath } from "node:url";
|
|
16
25
|
|
|
17
26
|
const BUILDCREW_TAG = "buildcrew-hook";
|
|
18
27
|
|
|
28
|
+
// Absolute path to this package's hook script — resolved at import time so the
|
|
29
|
+
// generated settings.json entries are self-contained (no PATH lookups needed).
|
|
30
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
31
|
+
const HOOK_SCRIPT = path.resolve(__dirname, "hook.js");
|
|
32
|
+
|
|
19
33
|
export function resolveSettingsPath({ scope, cwd }) {
|
|
20
34
|
if (scope === "global") return path.join(os.homedir(), ".claude", "settings.json");
|
|
21
35
|
return path.join(cwd, ".claude", "settings.json");
|
|
@@ -27,7 +41,9 @@ export function resolvePermissionsPath({ scope, cwd }) {
|
|
|
27
41
|
}
|
|
28
42
|
|
|
29
43
|
export function buildcrewHooks() {
|
|
30
|
-
|
|
44
|
+
// Shell-escape the path in case the install location contains spaces or
|
|
45
|
+
// non-ASCII characters (e.g. Korean path segments on macOS).
|
|
46
|
+
const cmd = (kind) => `node "${HOOK_SCRIPT}" ${kind}`;
|
|
31
47
|
const mk = (kind, matcher) => ({
|
|
32
48
|
[BUILDCREW_TAG]: true,
|
|
33
49
|
...(matcher ? { matcher } : {}),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "buildcrew",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.2",
|
|
4
4
|
"description": "15 AI agents for Claude Code — full development lifecycle from product thinking to production monitoring",
|
|
5
5
|
"homepage": "https://buildcrew-landing.vercel.app",
|
|
6
6
|
"author": "z1nun",
|