claude-company 0.1.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.
Files changed (62) hide show
  1. package/.claude/agents/architect.md +69 -0
  2. package/.claude/agents/auditor.md +44 -0
  3. package/.claude/agents/developer.md +86 -0
  4. package/.claude/agents/devops-engineer.md +38 -0
  5. package/.claude/agents/docs-librarian.md +36 -0
  6. package/.claude/agents/ideation-strategist.md +54 -0
  7. package/.claude/agents/product-manager.md +54 -0
  8. package/.claude/agents/qa-engineer.md +58 -0
  9. package/.claude/agents/security-reviewer.md +44 -0
  10. package/.claude/agents/tech-lead.md +80 -0
  11. package/.claude/hooks/_common.py +209 -0
  12. package/.claude/hooks/gate_stamp.py +77 -0
  13. package/.claude/hooks/gates_detect.py +387 -0
  14. package/.claude/hooks/guard_commit.py +123 -0
  15. package/.claude/hooks/guard_frozen.py +135 -0
  16. package/.claude/hooks/guard_spec.py +95 -0
  17. package/.claude/hooks/guard_tests.py +124 -0
  18. package/.claude/hooks/no_slop.py +134 -0
  19. package/.claude/hooks/session_start.py +63 -0
  20. package/.claude/hooks/stop_gate.py +59 -0
  21. package/.claude/settings.json +70 -0
  22. package/.claude/skills/autopilot/SKILL.md +65 -0
  23. package/.claude/skills/brainstorm/SKILL.md +61 -0
  24. package/.claude/skills/company-init/SKILL.md +51 -0
  25. package/.claude/skills/cr/SKILL.md +44 -0
  26. package/.claude/skills/feature/SKILL.md +42 -0
  27. package/.claude/skills/gates/SKILL.md +33 -0
  28. package/.claude/skills/onboard/SKILL.md +52 -0
  29. package/.claude/skills/orchestrator/SKILL.md +84 -0
  30. package/.claude/skills/standup/SKILL.md +38 -0
  31. package/.mcp.json +1 -0
  32. package/LICENSE +21 -0
  33. package/ORCHESTRATOR.md +191 -0
  34. package/README.md +236 -0
  35. package/bin/claude-company.js +112 -0
  36. package/company/EXTENDING.md +58 -0
  37. package/company/GATES.md +59 -0
  38. package/company/GIT.md +119 -0
  39. package/company/IDEATION.md +102 -0
  40. package/company/LOOPS.md +108 -0
  41. package/company/METHOD.md +151 -0
  42. package/company/frozen-surfaces.json +17 -0
  43. package/company/gates.config +5 -0
  44. package/company/run-gates.sh +158 -0
  45. package/company/state/DECISIONS.md +9 -0
  46. package/company/state/RESUME.md +28 -0
  47. package/company/state/STATUS.md +30 -0
  48. package/company/state/WORRIES.md +12 -0
  49. package/company/state/adherence.log +1 -0
  50. package/company/templates/BRIEF-TEMPLATE.md +69 -0
  51. package/company/templates/CR-TEMPLATE.md +28 -0
  52. package/company/templates/MODULE-TEMPLATE.md +23 -0
  53. package/company/templates/OPTIONS-TEMPLATE.md +42 -0
  54. package/company/templates/REPORT-TEMPLATE.md +35 -0
  55. package/company/templates/SPEC-TEMPLATE.md +69 -0
  56. package/docs/customizing.md +88 -0
  57. package/docs/getting-started.md +99 -0
  58. package/docs/how-it-works.md +153 -0
  59. package/install +4 -0
  60. package/install.sh +331 -0
  61. package/lib/install-tui.js +1600 -0
  62. package/package.json +47 -0
@@ -0,0 +1,1600 @@
1
+ "use strict";
2
+
3
+ // install-tui.js - the claude-company installer TUI, ported to zero-dependency
4
+ // Node from the original Python `install`. This is a thin, good-looking front
5
+ // end over install.sh (the engine); it never changes what gets installed, only
6
+ // how the install is presented and configured.
7
+ //
8
+ // Layout: UI primitives (palette, hero font, terminal, key reader, widgets),
9
+ // then the flow (preflight, target, options, confirm, install stream, gates,
10
+ // done), then run().
11
+ //
12
+ // Node >= 16, stdlib only. ASCII punctuation in strings; box-drawing glyphs OK.
13
+
14
+ const fs = require("fs");
15
+ const path = require("path");
16
+ const os = require("os");
17
+ const { spawn, spawnSync } = require("child_process");
18
+
19
+ // --------------------------------------------------------------------------
20
+ // Paths and constants
21
+ // --------------------------------------------------------------------------
22
+
23
+ const ROOT = path.join(__dirname, "..");
24
+ const INSTALL_SH = path.join(ROOT, "install.sh");
25
+
26
+ const EXIT_OK = 0;
27
+ const EXIT_USAGE = 1;
28
+ const EXIT_PREFLIGHT = 2;
29
+ const EXIT_INSTALL = 3;
30
+ const EXIT_CANCEL = 130;
31
+
32
+ const MAX_WIDTH = 100;
33
+ const SPIN_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼",
34
+ "⠴", "⠦", "⠧", "⠇", "⠏"];
35
+ const SPIN_INTERVAL = 0.12; // seconds
36
+
37
+ const ESC = "\x1b";
38
+ const CSI = ESC + "[";
39
+
40
+ function up(n) {
41
+ return n ? CSI + n + "A" : "";
42
+ }
43
+
44
+ const CLEAR_SCREEN = CSI + "2J" + CSI + "H";
45
+ const CLEAR_LINE = CSI + "2K";
46
+ const CLEAR_DOWN = CSI + "0J";
47
+ const HIDE_CURSOR = CSI + "?25l";
48
+ const SHOW_CURSOR = CSI + "?25h";
49
+
50
+ function out(s) {
51
+ process.stdout.write(s);
52
+ }
53
+
54
+ function sleep(ms) {
55
+ return new Promise((resolve) => setTimeout(resolve, ms));
56
+ }
57
+
58
+ // --------------------------------------------------------------------------
59
+ // Palette / gradient
60
+ // --------------------------------------------------------------------------
61
+
62
+ const STOPS = [[0x6d, 0x5b, 0xd0], [0x8b, 0x5c, 0xf6], [0xb4, 0x78, 0xf0]];
63
+ const GREEN = [0x53, 0xc0, 0x6a];
64
+ const YELLOW = [0xe4, 0xb5, 0x5e];
65
+ const RED = [0xe5, 0x6b, 0x6b];
66
+ // DIMGREY reserved for parity with the original palette definition.
67
+
68
+ class Palette {
69
+ constructor(enabled, truecolor) {
70
+ this.enabled = enabled;
71
+ this.truecolor = truecolor;
72
+ }
73
+
74
+ fg(rgb) {
75
+ if (!this.enabled) return "";
76
+ const [r, g, b] = rgb;
77
+ if (this.truecolor) {
78
+ return CSI + "38;2;" + r + ";" + g + ";" + b + "m";
79
+ }
80
+ return CSI + "38;5;" + Palette.to256(rgb) + "m";
81
+ }
82
+
83
+ static to256(rgb) {
84
+ const [r, g, b] = rgb;
85
+ if (Math.abs(r - g) < 12 && Math.abs(g - b) < 12) {
86
+ let grey = Math.round(((r + g + b) / 3 - 8) / 247 * 24);
87
+ grey = Math.max(0, Math.min(23, grey));
88
+ return 232 + grey;
89
+ }
90
+ const q = (v) => Math.round((v / 255) * 5);
91
+ return 16 + 36 * q(r) + 6 * q(g) + q(b);
92
+ }
93
+
94
+ grad(t) {
95
+ t = Math.max(0.0, Math.min(1.0, t));
96
+ const n = STOPS.length - 1;
97
+ const pos = t * n;
98
+ let i = Math.floor(pos);
99
+ if (i >= n) return STOPS[STOPS.length - 1];
100
+ const frac = pos - i;
101
+ const a = STOPS[i];
102
+ const b = STOPS[i + 1];
103
+ return [
104
+ Math.round(a[0] + (b[0] - a[0]) * frac),
105
+ Math.round(a[1] + (b[1] - a[1]) * frac),
106
+ Math.round(a[2] + (b[2] - a[2]) * frac),
107
+ ];
108
+ }
109
+
110
+ gradFg(t) {
111
+ return this.fg(this.grad(t));
112
+ }
113
+
114
+ get reset() {
115
+ return this.enabled ? CSI + "0m" : "";
116
+ }
117
+
118
+ get dim() {
119
+ return this.enabled ? CSI + "2m" : "";
120
+ }
121
+
122
+ get bold() {
123
+ return this.enabled ? CSI + "1m" : "";
124
+ }
125
+
126
+ paint(text, ...codes) {
127
+ if (!this.enabled || codes.length === 0) return text;
128
+ return codes.join("") + text + this.reset;
129
+ }
130
+
131
+ brand(text) {
132
+ return this.paint(text, this.bold, this.gradFg(0.5));
133
+ }
134
+
135
+ dimmed(text) {
136
+ return this.paint(text, this.dim);
137
+ }
138
+ }
139
+
140
+ function decidePalette(noColorFlag) {
141
+ const enabled = !noColorFlag && process.env.NO_COLOR === undefined;
142
+ const colorterm = (process.env.COLORTERM || "").toLowerCase();
143
+ const truecolor = colorterm.indexOf("truecolor") >= 0 ||
144
+ colorterm.indexOf("24bit") >= 0;
145
+ return new Palette(enabled, truecolor);
146
+ }
147
+
148
+ // --------------------------------------------------------------------------
149
+ // Hero art font -- standard figlet "ANSI Shadow" letterforms.
150
+ //
151
+ // Six rows per letter, built verbatim from block/box-drawing glyphs. Two
152
+ // invariants the hero relies on and that are enforced below: every letter has
153
+ // exactly HERO_ROWS rows, and every row of a given letter is the same display
154
+ // width so columns line up regardless of which letters are combined. (These
155
+ // were the two failure modes of the old hand-rolled font: broken letterforms
156
+ // and ragged per-row widths that rendered as noise.)
157
+ // --------------------------------------------------------------------------
158
+
159
+ const HERO_ROWS = 6;
160
+
161
+ const FONT_ANSI_SHADOW = {
162
+ C: [" ██████╗", "██╔════╝", "██║ ", "██║ ", "╚██████╗", " ╚═════╝"],
163
+ L: ["██╗ ", "██║ ", "██║ ", "██║ ", "███████╗", "╚══════╝"],
164
+ A: [" █████╗ ", "██╔══██╗", "███████║", "██╔══██║", "██║ ██║", "╚═╝ ╚═╝"],
165
+ U: ["██╗ ██╗", "██║ ██║", "██║ ██║", "██║ ██║", "╚██████╔╝", " ╚═════╝ "],
166
+ D: ["██████╗ ", "██╔══██╗", "██║ ██║", "██║ ██║", "██████╔╝", "╚═════╝ "],
167
+ E: ["███████╗", "██╔════╝", "█████╗ ", "██╔══╝ ", "███████╗", "╚══════╝"],
168
+ O: [" ██████╗ ", "██╔═══██╗", "██║ ██║", "██║ ██║", "╚██████╔╝", " ╚═════╝ "],
169
+ M: ["███╗ ███╗", "████╗ ████║", "██╔████╔██║", "██║╚██╔╝██║", "██║ ╚═╝ ██║", "╚═╝ ╚═╝"],
170
+ P: ["██████╗ ", "██╔══██╗", "██████╔╝", "██╔═══╝ ", "██║ ", "╚═╝ "],
171
+ N: ["███╗ ██╗", "████╗ ██║", "██╔██╗ ██║", "██║╚██╗██║", "██║ ╚████║", "╚═╝ ╚═══╝"],
172
+ Y: ["██╗ ██╗", "╚██╗ ██╔╝", " ╚████╔╝ ", " ╚██╔╝ ", " ██║ ", " ╚═╝ "],
173
+ };
174
+
175
+ // Enforce the invariants once at module load: exactly HERO_ROWS rows per
176
+ // letter, and pad every row of a letter out to that letter's widest row so the
177
+ // width is uniform by construction. Throws loudly on a malformed letter so a
178
+ // bad edit can never silently render as garbage. Every glyph char above is a
179
+ // single-width BMP code point, so String.length is the display width.
180
+ for (const ch of Object.keys(FONT_ANSI_SHADOW)) {
181
+ const glyph = FONT_ANSI_SHADOW[ch];
182
+ if (glyph.length !== HERO_ROWS) {
183
+ throw new Error(
184
+ "hero font: letter " + ch + " has " + glyph.length +
185
+ " rows, expected " + HERO_ROWS,
186
+ );
187
+ }
188
+ const w = Math.max.apply(null, glyph.map((r) => r.length));
189
+ for (let r = 0; r < HERO_ROWS; r++) {
190
+ if (glyph[r].length < w) glyph[r] += repeat(" ", w - glyph[r].length);
191
+ }
192
+ }
193
+
194
+ // Render text into HERO_ROWS lines of block art with one blank column of
195
+ // letter-spacing between letters. Trailing padding is preserved so every row
196
+ // keeps the same display width (needed for correct centering). Unknown chars
197
+ // are skipped; a space yields a gap.
198
+ function heroRows(text) {
199
+ const rows = [];
200
+ for (let r = 0; r < HERO_ROWS; r++) rows.push("");
201
+ const glyphs = [];
202
+ for (const ch of text) {
203
+ if (ch === " ") {
204
+ glyphs.push(null);
205
+ continue;
206
+ }
207
+ const g = FONT_ANSI_SHADOW[ch.toUpperCase()];
208
+ if (g) glyphs.push(g);
209
+ }
210
+ for (let i = 0; i < glyphs.length; i++) {
211
+ const g = glyphs[i];
212
+ const sep = i === 0 ? "" : " ";
213
+ for (let r = 0; r < HERO_ROWS; r++) {
214
+ rows[r] += sep + (g ? g[r] : " ");
215
+ }
216
+ }
217
+ return rows;
218
+ }
219
+
220
+ // --------------------------------------------------------------------------
221
+ // Terminal / raw-mode management
222
+ // --------------------------------------------------------------------------
223
+
224
+ const KEY_UP = "UP";
225
+ const KEY_DOWN = "DOWN";
226
+ const KEY_LEFT = "LEFT";
227
+ const KEY_RIGHT = "RIGHT";
228
+ const KEY_ENTER = "ENTER";
229
+ const KEY_ESC = "ESC";
230
+ const KEY_TAB = "TAB";
231
+ const KEY_BACKSPACE = "BACKSPACE";
232
+
233
+ const NAMED_KEYS = new Set([
234
+ KEY_UP, KEY_DOWN, KEY_LEFT, KEY_RIGHT,
235
+ KEY_ENTER, KEY_ESC, KEY_TAB, KEY_BACKSPACE,
236
+ ]);
237
+
238
+ // Event-driven key reader. Decodes raw stdin bytes into logical keypresses and
239
+ // hands them to awaiting readKey() calls (or queues them). Ctrl-C triggers the
240
+ // supplied interrupt callback so the terminal is always restored cleanly.
241
+ class KeyReader {
242
+ constructor(onInterrupt) {
243
+ this.onInterrupt = onInterrupt;
244
+ this.queue = [];
245
+ this.waiters = [];
246
+ this.buf = Buffer.alloc(0);
247
+ this.escTimer = null;
248
+ this.active = false;
249
+ this._onData = this._onData.bind(this);
250
+ }
251
+
252
+ start() {
253
+ if (this.active) return;
254
+ this.active = true;
255
+ if (process.stdin.isTTY && process.stdin.setRawMode) {
256
+ process.stdin.setRawMode(true);
257
+ }
258
+ process.stdin.resume();
259
+ process.stdin.on("data", this._onData);
260
+ }
261
+
262
+ stop() {
263
+ if (!this.active) return;
264
+ this.active = false;
265
+ process.stdin.removeListener("data", this._onData);
266
+ if (this.escTimer) {
267
+ clearTimeout(this.escTimer);
268
+ this.escTimer = null;
269
+ }
270
+ for (const w of this.waiters) {
271
+ if (w.timer) clearTimeout(w.timer);
272
+ }
273
+ this.waiters = [];
274
+ if (process.stdin.isTTY && process.stdin.setRawMode) {
275
+ process.stdin.setRawMode(false);
276
+ }
277
+ process.stdin.pause();
278
+ }
279
+
280
+ readKey(timeoutSec) {
281
+ return new Promise((resolve) => {
282
+ if (this.queue.length) {
283
+ resolve(this.queue.shift());
284
+ return;
285
+ }
286
+ const waiter = { resolve: resolve, timer: null };
287
+ if (timeoutSec !== undefined && timeoutSec !== null) {
288
+ waiter.timer = setTimeout(() => {
289
+ const i = this.waiters.indexOf(waiter);
290
+ if (i >= 0) this.waiters.splice(i, 1);
291
+ resolve(null);
292
+ }, timeoutSec * 1000);
293
+ }
294
+ this.waiters.push(waiter);
295
+ });
296
+ }
297
+
298
+ _deliver(key) {
299
+ const waiter = this.waiters.shift();
300
+ if (waiter) {
301
+ if (waiter.timer) clearTimeout(waiter.timer);
302
+ waiter.resolve(key);
303
+ } else {
304
+ this.queue.push(key);
305
+ }
306
+ }
307
+
308
+ _onData(chunk) {
309
+ if (this.escTimer) {
310
+ clearTimeout(this.escTimer);
311
+ this.escTimer = null;
312
+ }
313
+ this.buf = Buffer.concat([this.buf, chunk]);
314
+ this._parse();
315
+ }
316
+
317
+ _parse() {
318
+ const b = this.buf;
319
+ let i = 0;
320
+ const keys = [];
321
+ let pendingEsc = false;
322
+ while (i < b.length) {
323
+ const c = b[i];
324
+ if (c === 0x03) { // Ctrl-C
325
+ this.buf = b.slice(i + 1);
326
+ for (const k of keys) this._deliver(k);
327
+ if (this.onInterrupt) this.onInterrupt();
328
+ return;
329
+ }
330
+ if (c === 0x1b) { // ESC or escape sequence
331
+ if (i + 1 >= b.length) {
332
+ pendingEsc = true;
333
+ break; // wait briefly for a possible sequence tail
334
+ }
335
+ const nxt = b[i + 1];
336
+ if (nxt === 0x5b || nxt === 0x4f) { // '[' or 'O'
337
+ if (i + 2 >= b.length) {
338
+ pendingEsc = true;
339
+ break;
340
+ }
341
+ const third = b[i + 2];
342
+ const arrows = { 0x41: KEY_UP, 0x42: KEY_DOWN, 0x43: KEY_RIGHT, 0x44: KEY_LEFT };
343
+ if (arrows[third]) {
344
+ keys.push(arrows[third]);
345
+ i += 3;
346
+ continue;
347
+ }
348
+ // Consume the rest of the CSI sequence up to its final byte.
349
+ let j = i + 2;
350
+ while (j < b.length && !(b[j] >= 0x40 && b[j] <= 0x7e)) j++;
351
+ if (j < b.length) {
352
+ keys.push(KEY_ESC);
353
+ i = j + 1;
354
+ continue;
355
+ }
356
+ break; // incomplete sequence, wait for more
357
+ }
358
+ keys.push(KEY_ESC);
359
+ i += 1;
360
+ continue;
361
+ }
362
+ if (c === 0x0d || c === 0x0a) { keys.push(KEY_ENTER); i += 1; continue; }
363
+ if (c === 0x09) { keys.push(KEY_TAB); i += 1; continue; }
364
+ if (c === 0x7f || c === 0x08) { keys.push(KEY_BACKSPACE); i += 1; continue; }
365
+ if (c < 0x80) { keys.push(String.fromCharCode(c)); i += 1; continue; }
366
+ // Multi-byte UTF-8 sequence.
367
+ const len = c >= 0xf0 ? 4 : c >= 0xe0 ? 3 : 2;
368
+ if (i + len > b.length) break; // incomplete, wait
369
+ keys.push(b.slice(i, i + len).toString("utf8"));
370
+ i += len;
371
+ }
372
+ this.buf = b.slice(i);
373
+ for (const k of keys) this._deliver(k);
374
+ if (pendingEsc && this.buf.length && this.buf[0] === 0x1b) {
375
+ this._scheduleEscFlush();
376
+ }
377
+ }
378
+
379
+ _scheduleEscFlush() {
380
+ if (this.escTimer) return;
381
+ this.escTimer = setTimeout(() => {
382
+ this.escTimer = null;
383
+ if (this.buf.length && this.buf[0] === 0x1b) {
384
+ this.buf = this.buf.slice(1);
385
+ this._deliver(KEY_ESC);
386
+ this._parse();
387
+ }
388
+ }, 50);
389
+ }
390
+ }
391
+
392
+ class Terminal {
393
+ constructor(pal) {
394
+ this.pal = pal;
395
+ this.reader = new KeyReader(() => this.cancel());
396
+ this._cancelled = false;
397
+ this._restored = false;
398
+ this._onSig = this._onSig.bind(this);
399
+ }
400
+
401
+ enter() {
402
+ out(HIDE_CURSOR);
403
+ this.reader.start();
404
+ process.on("SIGINT", this._onSig);
405
+ process.on("SIGTERM", this._onSig);
406
+ return this;
407
+ }
408
+
409
+ restore() {
410
+ if (this._restored) return;
411
+ this._restored = true;
412
+ this.reader.stop();
413
+ process.removeListener("SIGINT", this._onSig);
414
+ process.removeListener("SIGTERM", this._onSig);
415
+ out(SHOW_CURSOR);
416
+ }
417
+
418
+ _onSig() {
419
+ this.cancel();
420
+ }
421
+
422
+ cancel() {
423
+ if (this._cancelled) return;
424
+ this._cancelled = true;
425
+ this.restore();
426
+ out("\n" + this.pal.paint(" Install cancelled.", this.pal.fg(YELLOW)) + "\n");
427
+ process.exit(EXIT_CANCEL);
428
+ }
429
+
430
+ readKey(timeoutSec) {
431
+ return this.reader.readKey(timeoutSec);
432
+ }
433
+ }
434
+
435
+ function termWidth() {
436
+ let cols = process.stdout.columns || 80;
437
+ if (cols <= 0) cols = 80;
438
+ return Math.max(40, Math.min(cols, MAX_WIDTH));
439
+ }
440
+
441
+ // --------------------------------------------------------------------------
442
+ // UI primitives
443
+ // --------------------------------------------------------------------------
444
+
445
+ const ANSI_RE = /\x1b\[[0-9;?]*[A-Za-z]/g;
446
+
447
+ function visibleLen(s) {
448
+ return s.replace(ANSI_RE, "").length;
449
+ }
450
+
451
+ function repeat(ch, n) {
452
+ return n > 0 ? ch.repeat(n) : "";
453
+ }
454
+
455
+ function center(s, width) {
456
+ const pad = Math.max(0, Math.floor((width - visibleLen(s)) / 2));
457
+ return repeat(" ", pad) + s;
458
+ }
459
+
460
+ function padRight(s, n) {
461
+ const diff = n - s.length;
462
+ return diff > 0 ? s + repeat(" ", diff) : s;
463
+ }
464
+
465
+ function elide(s, n) {
466
+ if (s.length <= n) return s;
467
+ if (n <= 3) return s.slice(0, n);
468
+ return s.slice(0, n - 3) + "...";
469
+ }
470
+
471
+ function rule(pal, width, gradient) {
472
+ const line = repeat("─", width);
473
+ if (gradient && pal.enabled) {
474
+ const parts = [];
475
+ for (let i = 0; i < line.length; i++) {
476
+ parts.push(pal.gradFg(i / Math.max(1, width - 1)) + line[i]);
477
+ }
478
+ return parts.join("") + pal.reset;
479
+ }
480
+ return pal.dimmed(line);
481
+ }
482
+
483
+ function sectionHeader(pal, title) {
484
+ const bar = pal.paint("┃", pal.gradFg(0.35));
485
+ return bar + " " + pal.paint(title, pal.bold);
486
+ }
487
+
488
+ const Frame = {
489
+ TL: "╭", TR: "╮", BL: "╰", BR: "╯",
490
+ H: "─", V: "│",
491
+ render(pal, lines, width, accent, title) {
492
+ if (accent === undefined) accent = 0.5;
493
+ const inner = width - 2;
494
+ const edge = pal.gradFg(accent);
495
+ let top = pal.paint(this.TL + repeat(this.H, inner) + this.TR, edge);
496
+ if (title) {
497
+ const label = " " + title + " ";
498
+ const fill = inner - visibleLen(label);
499
+ top = pal.paint(this.TL + this.H, edge) + pal.paint(label, pal.bold) +
500
+ pal.paint(repeat(this.H, Math.max(0, fill - 1)) + this.TR, edge);
501
+ }
502
+ const bottom = pal.paint(this.BL + repeat(this.H, inner) + this.BR, edge);
503
+ const result = [top];
504
+ for (let ln of lines) {
505
+ if (visibleLen(ln) === ln.length && ln.length > inner - 1) {
506
+ ln = elide(ln, inner - 1);
507
+ }
508
+ const pad = inner - visibleLen(ln) - 1;
509
+ const v = pal.paint(this.V, edge);
510
+ result.push(v + " " + ln + repeat(" ", Math.max(0, pad)) + v);
511
+ }
512
+ result.push(bottom);
513
+ return result;
514
+ },
515
+ };
516
+
517
+ // A re-drawable region: successive render() calls overwrite in place.
518
+ class LiveBlock {
519
+ constructor() {
520
+ this.height = 0;
521
+ }
522
+
523
+ render(lines) {
524
+ const buf = [];
525
+ if (this.height) {
526
+ buf.push(up(this.height));
527
+ buf.push("\r");
528
+ buf.push(CLEAR_DOWN);
529
+ }
530
+ buf.push(lines.join("\n"));
531
+ buf.push("\n");
532
+ out(buf.join(""));
533
+ this.height = lines.length;
534
+ }
535
+ }
536
+
537
+ // A pinned bottom spinner line with rows scrolling above it.
538
+ class Activity {
539
+ constructor(pal) {
540
+ this.pal = pal;
541
+ this.frame = 0;
542
+ this.label = "";
543
+ this._active = false;
544
+ }
545
+
546
+ _spinline() {
547
+ const glyph = this.pal.paint(SPIN_FRAMES[this.frame % SPIN_FRAMES.length],
548
+ this.pal.gradFg(0.5));
549
+ return " " + glyph + " " + this.pal.dimmed(this.label);
550
+ }
551
+
552
+ start(label) {
553
+ this.label = label;
554
+ this._active = true;
555
+ out(this._spinline());
556
+ }
557
+
558
+ setLabel(label) {
559
+ this.label = label;
560
+ }
561
+
562
+ tick() {
563
+ if (!this._active) return;
564
+ this.frame += 1;
565
+ out("\r" + CLEAR_LINE + this._spinline());
566
+ }
567
+
568
+ emit(row) {
569
+ out("\r" + CLEAR_LINE + row + "\n");
570
+ if (this._active) out(this._spinline());
571
+ }
572
+
573
+ stop() {
574
+ if (this._active) out("\r" + CLEAR_LINE);
575
+ this._active = false;
576
+ }
577
+ }
578
+
579
+ // --------------------------------------------------------------------------
580
+ // Preflight probes
581
+ // --------------------------------------------------------------------------
582
+
583
+ const PASS = "PASS";
584
+ const WARN = "WARN";
585
+ const FAIL = "FAIL";
586
+ const GLYPH = { PASS: "✔", WARN: "△", FAIL: "✘" };
587
+
588
+ function toolVersion(binary, args) {
589
+ let res;
590
+ try {
591
+ res = spawnSync(binary, args, { encoding: "utf8", timeout: 5000 });
592
+ } catch (e) {
593
+ return null;
594
+ }
595
+ if (!res || res.error) return null;
596
+ const text = ((res.stdout || "") + (res.stderr || "")).trim();
597
+ const first = text.split("\n")[0] || "";
598
+ const m = first.match(/\d+\.\d+(\.\d+)?/);
599
+ return m ? m[0] : (first || "found");
600
+ }
601
+
602
+ function probePython() {
603
+ let res;
604
+ try {
605
+ res = spawnSync("python3", ["--version"], { encoding: "utf8", timeout: 5000 });
606
+ } catch (e) {
607
+ return [FAIL, "not found"];
608
+ }
609
+ if (!res || res.error) return [FAIL, "not found"];
610
+ const text = ((res.stdout || "") + (res.stderr || "")).trim();
611
+ const m = text.match(/(\d+)\.(\d+)(?:\.(\d+))?/);
612
+ if (!m) return [FAIL, "unknown"];
613
+ const major = parseInt(m[1], 10);
614
+ const minor = parseInt(m[2], 10);
615
+ const ver = m[3] !== undefined ? m[1] + "." + m[2] + "." + m[3] : m[1] + "." + m[2];
616
+ const okVer = major > 3 || (major === 3 && minor >= 8);
617
+ return [okVer ? PASS : FAIL, ver];
618
+ }
619
+
620
+ function probeGit() {
621
+ const v = toolVersion("git", ["--version"]);
622
+ return v === null ? [FAIL, "not found"] : [PASS, v];
623
+ }
624
+
625
+ function probeBash() {
626
+ const v = toolVersion("bash", ["--version"]);
627
+ return v === null ? [FAIL, "not found"] : [PASS, v];
628
+ }
629
+
630
+ function probeClaude() {
631
+ const v = toolVersion("claude", ["--version"]);
632
+ return v === null
633
+ ? [WARN, "not found - install Claude Code to use the company"]
634
+ : [PASS, v];
635
+ }
636
+
637
+ function probeNode() {
638
+ // Self-evidently present: we are running inside Node.
639
+ return [PASS, process.version.replace(/^v/, "")];
640
+ }
641
+
642
+ function probeNpx() {
643
+ const v = toolVersion("npx", ["--version"]);
644
+ return v === null
645
+ ? [WARN, "not found - needed for browser QA screenshots"]
646
+ : [PASS, v];
647
+ }
648
+
649
+ const PROBES = [
650
+ { name: "python3 >= 3.8", hard: true, reason: "required by the enforcement hooks", fn: probePython },
651
+ { name: "git", hard: true, reason: "required by the enforcement hooks", fn: probeGit },
652
+ { name: "bash", hard: true, reason: "required by the enforcement hooks", fn: probeBash },
653
+ { name: "claude CLI", hard: false, fn: probeClaude },
654
+ { name: "node", hard: false, fn: probeNode },
655
+ { name: "npx", hard: false, fn: probeNpx },
656
+ ];
657
+
658
+ // --------------------------------------------------------------------------
659
+ // Source inventory (for the confirm summary)
660
+ // --------------------------------------------------------------------------
661
+
662
+ function sourceCounts() {
663
+ function countFiles(rel, pattern) {
664
+ const d = path.join(ROOT, rel);
665
+ let entries;
666
+ try {
667
+ entries = fs.readdirSync(d);
668
+ } catch (e) {
669
+ return 0;
670
+ }
671
+ return entries.filter((f) => pattern.test(f)).length;
672
+ }
673
+ function countDirs(rel) {
674
+ const d = path.join(ROOT, rel);
675
+ let entries;
676
+ try {
677
+ entries = fs.readdirSync(d);
678
+ } catch (e) {
679
+ return 0;
680
+ }
681
+ return entries.filter((f) => {
682
+ try {
683
+ return fs.statSync(path.join(d, f)).isDirectory();
684
+ } catch (e) {
685
+ return false;
686
+ }
687
+ }).length;
688
+ }
689
+ return {
690
+ agents: countFiles(".claude/agents", /\.md$/),
691
+ skills: countDirs(".claude/skills"),
692
+ hooks: countFiles(".claude/hooks", /\.py$/),
693
+ };
694
+ }
695
+
696
+ // --------------------------------------------------------------------------
697
+ // Target validation
698
+ // --------------------------------------------------------------------------
699
+
700
+ function expandUser(p) {
701
+ if (p === "~") return os.homedir();
702
+ if (p.startsWith("~/")) return path.join(os.homedir(), p.slice(2));
703
+ return p;
704
+ }
705
+
706
+ function realpathOr(p) {
707
+ try {
708
+ return fs.realpathSync(p);
709
+ } catch (e) {
710
+ return p;
711
+ }
712
+ }
713
+
714
+ function validateTarget(raw) {
715
+ if (!raw || !raw.trim()) return [false, "", "enter a directory"];
716
+ const p = path.resolve(expandUser(raw.trim()));
717
+ let st;
718
+ try {
719
+ st = fs.statSync(p);
720
+ } catch (e) {
721
+ return [false, p, "does not exist - enter an existing directory"];
722
+ }
723
+ if (!st.isDirectory()) return [false, p, "not a directory"];
724
+ if (realpathOr(p) === realpathOr(ROOT)) {
725
+ return [false, p, "that is the claude-company repo itself - pick your project"];
726
+ }
727
+ try {
728
+ fs.accessSync(p, fs.constants.W_OK);
729
+ } catch (e) {
730
+ return [false, p, "not writable - check permissions"];
731
+ }
732
+ const isGit = fs.existsSync(path.join(p, ".git"));
733
+ let n = 0;
734
+ try {
735
+ n = fs.readdirSync(p).length;
736
+ } catch (e) {
737
+ n = 0;
738
+ }
739
+ if (isGit) return [true, p, "git repo, " + n + " entries"];
740
+ return [true, p, "directory exists, not a git repo - will still install"];
741
+ }
742
+
743
+ // --------------------------------------------------------------------------
744
+ // TUI screens
745
+ // --------------------------------------------------------------------------
746
+
747
+ const TAGLINE = "An AI software company you drop into your repo.";
748
+
749
+ // Paint a plain wordmark char-by-char along the purple gradient, bold. Used as
750
+ // the narrow-terminal fallback so the hero never renders clipped/wrapped art.
751
+ function heroWordmark(pal, text) {
752
+ if (!pal.enabled) return text;
753
+ const parts = [];
754
+ for (let i = 0; i < text.length; i++) {
755
+ const t = text.length > 1 ? i / (text.length - 1) : 0;
756
+ parts.push(pal.bold + pal.gradFg(t) + text[i]);
757
+ }
758
+ return parts.join("") + pal.reset;
759
+ }
760
+
761
+ function drawHero(pal) {
762
+ const width = termWidth();
763
+ out(CLEAR_SCREEN);
764
+
765
+ const claude = heroRows("CLAUDE");
766
+ const company = heroRows("COMPANY");
767
+ const artWidth = Math.max(
768
+ Math.max.apply(null, claude.map(visibleLen)),
769
+ Math.max.apply(null, company.map(visibleLen)),
770
+ );
771
+
772
+ const lines = [];
773
+ if (width >= artWidth + 4) {
774
+ // Full block-art hero: CLAUDE stacked over COMPANY with one line-gap, each
775
+ // of the 12 art rows painted top-to-bottom along the purple gradient
776
+ // (#6D5BD0 at the top row through #B478F0 at the bottom).
777
+ const total = claude.length + company.length;
778
+ for (let i = 0; i < claude.length; i++) {
779
+ const t = i / (total - 1);
780
+ lines.push(center(pal.paint(claude[i], pal.gradFg(t), pal.bold), width));
781
+ }
782
+ lines.push("");
783
+ for (let i = 0; i < company.length; i++) {
784
+ const t = (claude.length + i) / (total - 1);
785
+ lines.push(center(pal.paint(company[i], pal.gradFg(t), pal.bold), width));
786
+ }
787
+ lines.unshift("", "");
788
+ } else {
789
+ // Too narrow for the art: typographic wordmark instead of clipped letters.
790
+ lines.push("", "");
791
+ lines.push(center(heroWordmark(pal, "claude-company"), width));
792
+ }
793
+ lines.push("");
794
+ lines.push(center(pal.dimmed(TAGLINE), width));
795
+ lines.push("");
796
+ lines.push(rule(pal, width, true));
797
+ lines.push("");
798
+ lines.push(center(pal.dimmed("press any key to begin - q or Esc to quit"), width));
799
+ out(lines.join("\n") + "\n");
800
+ }
801
+
802
+ async function screenHero(term, pal) {
803
+ drawHero(pal);
804
+ for (;;) {
805
+ const key = await term.readKey(0.5);
806
+ if (key === null) {
807
+ drawHero(pal);
808
+ continue;
809
+ }
810
+ if (key === "q" || key === "Q" || key === KEY_ESC) return false;
811
+ return true;
812
+ }
813
+ }
814
+
815
+ function preflightRow(pal, name, status, detail, width, spinFrame) {
816
+ let glyph;
817
+ let right;
818
+ if (status === null) {
819
+ glyph = pal.paint(SPIN_FRAMES[spinFrame % SPIN_FRAMES.length], pal.gradFg(0.5));
820
+ right = pal.dimmed("checking...");
821
+ } else {
822
+ const color = { PASS: GREEN, WARN: YELLOW, FAIL: RED }[status];
823
+ glyph = pal.paint(GLYPH[status], pal.fg(color));
824
+ right = pal.paint(detail, status !== PASS ? pal.fg(color) : pal.dim);
825
+ }
826
+ const left = " " + glyph + " " + name;
827
+ const gap = width - visibleLen(left) - visibleLen(right) - 2;
828
+ return left + repeat(" ", Math.max(1, gap)) + right;
829
+ }
830
+
831
+ async function screenPreflight(term, pal) {
832
+ const width = termWidth();
833
+ out(CLEAR_SCREEN);
834
+ const header = [
835
+ "",
836
+ " " + sectionHeader(pal, "Preflight"),
837
+ " " + pal.dimmed("checking your machine has what the company needs"),
838
+ "",
839
+ ];
840
+ out(header.join("\n") + "\n");
841
+
842
+ const block = new LiveBlock();
843
+ const results = [];
844
+ const statuses = PROBES.map(() => null);
845
+
846
+ function render(spin) {
847
+ const rows = [];
848
+ for (let i = 0; i < PROBES.length; i++) {
849
+ const st = statuses[i];
850
+ if (st === null) {
851
+ rows.push(preflightRow(pal, PROBES[i].name, null, "", width, spin));
852
+ } else {
853
+ rows.push(preflightRow(pal, PROBES[i].name, st[0], st[1], width, spin));
854
+ }
855
+ }
856
+ block.render(rows);
857
+ }
858
+
859
+ render(0);
860
+ for (let i = 0; i < PROBES.length; i++) {
861
+ const start = Date.now();
862
+ const result = PROBES[i].fn();
863
+ let frame = 0;
864
+ while (Date.now() - start < 360) {
865
+ render(frame);
866
+ // eslint-disable-next-line no-await-in-loop
867
+ await sleep(SPIN_INTERVAL * 1000);
868
+ frame += 1;
869
+ }
870
+ statuses[i] = result;
871
+ results.push([PROBES[i], result[0], result[1]]);
872
+ render(frame);
873
+ }
874
+
875
+ const hardFail = results.filter((r) => r[0].hard && r[1] === FAIL);
876
+ return [hardFail.length === 0, results];
877
+ }
878
+
879
+ function screenPreflightFail(pal, results) {
880
+ const width = termWidth();
881
+ const missing = results.filter((r) => r[0].hard && r[1] === FAIL).map((r) => r[0].name);
882
+ const lines = [
883
+ pal.paint("A required tool is missing.", pal.fg(RED), pal.bold),
884
+ "",
885
+ "claude-company needs these to run its enforcement hooks:",
886
+ ];
887
+ for (const name of missing) {
888
+ lines.push(" " + pal.paint("✘ " + name, pal.fg(RED)));
889
+ }
890
+ lines.push("");
891
+ lines.push(pal.dimmed("Install the missing tool(s), then run ./install again."));
892
+ const box = Frame.render(pal, lines, width, 0.0, "Cannot continue");
893
+ out("\n" + box.join("\n") + "\n");
894
+ }
895
+
896
+ function completePath(value) {
897
+ const expanded = expandUser(value);
898
+ const directory = path.dirname(expanded) || ".";
899
+ const partial = path.basename(expanded);
900
+ let entries;
901
+ try {
902
+ entries = fs.readdirSync(directory);
903
+ } catch (e) {
904
+ return value;
905
+ }
906
+ const matches = entries.filter((e) => e.startsWith(partial));
907
+ if (matches.length === 0) return value;
908
+ let completed;
909
+ if (matches.length === 1) {
910
+ completed = matches[0];
911
+ } else {
912
+ completed = commonPrefix(matches);
913
+ if (completed === partial) return value;
914
+ }
915
+ let full = path.join(directory, completed);
916
+ let isDir = false;
917
+ try {
918
+ isDir = fs.statSync(expandUser(full)).isDirectory();
919
+ } catch (e) {
920
+ isDir = false;
921
+ }
922
+ if (isDir && matches.length === 1) full += path.sep;
923
+ if (value.startsWith("~")) {
924
+ const home = os.homedir();
925
+ if (full.startsWith(home)) full = "~" + full.slice(home.length);
926
+ }
927
+ return full;
928
+ }
929
+
930
+ function commonPrefix(strings) {
931
+ if (strings.length === 0) return "";
932
+ let prefix = strings[0];
933
+ for (const s of strings) {
934
+ while (!s.startsWith(prefix)) {
935
+ prefix = prefix.slice(0, -1);
936
+ if (prefix === "") return "";
937
+ }
938
+ }
939
+ return prefix;
940
+ }
941
+
942
+ async function screenTarget(term, pal) {
943
+ out(CLEAR_SCREEN);
944
+ const header = [
945
+ "",
946
+ " " + sectionHeader(pal, "Target project"),
947
+ " " + pal.dimmed("where should the company move in? (Tab completes, Esc quits)"),
948
+ "",
949
+ ];
950
+ out(header.join("\n") + "\n");
951
+
952
+ let value = process.cwd();
953
+ const block = new LiveBlock();
954
+
955
+ function render() {
956
+ const [ok, , msg] = validateTarget(value);
957
+ const caret = pal.paint("█", pal.gradFg(0.5));
958
+ const prompt = " " + pal.paint("❯", pal.gradFg(0.4)) + " " + value + caret;
959
+ let status;
960
+ if (ok) {
961
+ const color = msg.indexOf("git repo") >= 0 ? GREEN : YELLOW;
962
+ status = pal.paint(" " + msg, pal.fg(color));
963
+ } else {
964
+ status = pal.paint(" " + msg, pal.fg(RED));
965
+ }
966
+ block.render([prompt, "", status]);
967
+ }
968
+
969
+ render();
970
+ for (;;) {
971
+ const key = await term.readKey(0.5);
972
+ if (key === null) {
973
+ render();
974
+ continue;
975
+ }
976
+ if (key === KEY_ESC) return null;
977
+ if (key === KEY_ENTER) {
978
+ const [ok, p] = validateTarget(value);
979
+ if (ok) return p;
980
+ render();
981
+ continue;
982
+ }
983
+ if (key === KEY_BACKSPACE) {
984
+ value = value.slice(0, -1);
985
+ render();
986
+ continue;
987
+ }
988
+ if (key === KEY_TAB) {
989
+ value = completePath(value);
990
+ render();
991
+ continue;
992
+ }
993
+ if (!NAMED_KEYS.has(key) && key.length === 1 && isPrintable(key)) {
994
+ value += key;
995
+ render();
996
+ }
997
+ }
998
+ }
999
+
1000
+ function isPrintable(ch) {
1001
+ const code = ch.charCodeAt(0);
1002
+ return code >= 0x20 && code !== 0x7f;
1003
+ }
1004
+
1005
+ function makeOptions(detectGates, orientation) {
1006
+ return {
1007
+ items: [
1008
+ ["Auto-detect this project's gates (test/lint commands) after install", detectGates],
1009
+ ["Show the 60-second orientation at the end", orientation],
1010
+ ],
1011
+ };
1012
+ }
1013
+
1014
+ async function screenOptions(term, pal, opts) {
1015
+ out(CLEAR_SCREEN);
1016
+ const header = [
1017
+ "",
1018
+ " " + sectionHeader(pal, "Options"),
1019
+ " " + pal.dimmed("arrows or j/k move - space toggles - Enter confirms"),
1020
+ "",
1021
+ ];
1022
+ out(header.join("\n") + "\n");
1023
+
1024
+ let cursor = 0;
1025
+ const block = new LiveBlock();
1026
+
1027
+ function render() {
1028
+ const rows = [];
1029
+ for (let i = 0; i < opts.items.length; i++) {
1030
+ const [label, on] = opts.items[i];
1031
+ const mark = on ? pal.paint("◉", pal.gradFg(0.5)) : pal.dimmed("◯");
1032
+ const pointer = i === cursor ? pal.paint("❯", pal.gradFg(0.4)) : " ";
1033
+ const text = i === cursor ? pal.paint(label, pal.bold) : label;
1034
+ rows.push(" " + pointer + " " + mark + " " + text);
1035
+ }
1036
+ rows.push("");
1037
+ rows.push(" " + pal.dimmed("[ Enter to confirm ]"));
1038
+ block.render(rows);
1039
+ }
1040
+
1041
+ render();
1042
+ for (;;) {
1043
+ const key = await term.readKey(0.5);
1044
+ if (key === null) {
1045
+ render();
1046
+ continue;
1047
+ }
1048
+ if (key === KEY_ESC) return null;
1049
+ if (key === KEY_UP || key === "k") {
1050
+ cursor = (cursor - 1 + opts.items.length) % opts.items.length;
1051
+ render();
1052
+ } else if (key === KEY_DOWN || key === "j") {
1053
+ cursor = (cursor + 1) % opts.items.length;
1054
+ render();
1055
+ } else if (key === " ") {
1056
+ opts.items[cursor][1] = !opts.items[cursor][1];
1057
+ render();
1058
+ } else if (key === KEY_ENTER) {
1059
+ return opts;
1060
+ }
1061
+ }
1062
+ }
1063
+
1064
+ async function screenConfirm(term, pal, target, opts) {
1065
+ const width = termWidth();
1066
+ out(CLEAR_SCREEN);
1067
+ const counts = sourceCounts();
1068
+ const header = [
1069
+ "",
1070
+ " " + sectionHeader(pal, "Ready to install"),
1071
+ "",
1072
+ ];
1073
+ out(header.join("\n") + "\n");
1074
+
1075
+ const kv = (k, v) => pal.dimmed(padRight(k, 10)) + " " + v;
1076
+ const detect = opts.items[0][1];
1077
+ const orient = opts.items[1][1];
1078
+ const flag = (on) => (on ? pal.paint("on", pal.fg(GREEN)) : pal.dimmed("off"));
1079
+
1080
+ const lines = [
1081
+ kv("target", pal.paint(elide(target, width - 16), pal.bold)),
1082
+ "",
1083
+ kv("installs", "agents " + counts.agents + ", skills " + counts.skills +
1084
+ ", hooks " + counts.hooks),
1085
+ kv("", "canon docs, templates, and company/state scaffolds"),
1086
+ "",
1087
+ kv("gates", flag(detect) + pal.dimmed(" (auto-detect after install)")),
1088
+ kv("tour", flag(orient) + pal.dimmed(" (60-second orientation)")),
1089
+ "",
1090
+ pal.dimmed("The installer merges with your settings and never overwrites your state."),
1091
+ ];
1092
+ const box = Frame.render(pal, lines, width, 0.5);
1093
+ out(box.join("\n") + "\n\n");
1094
+ const prompt = " " +
1095
+ (pal.paint("Enter", pal.gradFg(0.5), pal.bold) + pal.dimmed(" install")) +
1096
+ " " +
1097
+ (pal.paint("Esc", pal.bold) + pal.dimmed(" cancel"));
1098
+ out(prompt + "\n");
1099
+
1100
+ for (;;) {
1101
+ const key = await term.readKey(0.5);
1102
+ if (key === null) continue;
1103
+ if (key === KEY_ENTER) return true;
1104
+ if (key === KEY_ESC || key === "q" || key === "Q") return false;
1105
+ }
1106
+ }
1107
+
1108
+ // --------------------------------------------------------------------------
1109
+ // Install streaming
1110
+ // --------------------------------------------------------------------------
1111
+
1112
+ const RE_INFO = /^==> (.*)$/;
1113
+ const RE_OK = /^ ok (.*)$/;
1114
+ const RE_KEEP = /^ keep (.*)$/;
1115
+ const RE_WARN = /^warning: (.*)$/;
1116
+ const RE_ERR = /^error: (.*)$/;
1117
+
1118
+ const EPILOGUE_PREFIXES = [
1119
+ "source:", "target:", "1.", "2.", "3.", "4.", "cd ", "claude",
1120
+ "/company-init", "/onboard", "/orchestrator", "Next steps:", "Configure",
1121
+ "In Claude Code", "bash company",
1122
+ ];
1123
+
1124
+ function styleInstallLine(pal, line) {
1125
+ let m = line.match(RE_INFO);
1126
+ if (m) {
1127
+ const title = m[1];
1128
+ if (title === "claude-company installer" || title === "claude-company installed") {
1129
+ return ["meta", null];
1130
+ }
1131
+ return ["section", " " + sectionHeader(pal, title)];
1132
+ }
1133
+ m = line.match(RE_OK);
1134
+ if (m) {
1135
+ const glyph = pal.paint("✔", pal.fg(GREEN));
1136
+ return ["ok", " " + glyph + " " + pal.dimmed(m[1])];
1137
+ }
1138
+ m = line.match(RE_KEEP);
1139
+ if (m) {
1140
+ const glyph = pal.paint("∙", pal.dim);
1141
+ return ["keep", " " + glyph + " " + pal.dimmed(m[1] + " (kept)")];
1142
+ }
1143
+ m = line.match(RE_WARN);
1144
+ if (m) {
1145
+ const glyph = pal.paint("△", pal.fg(YELLOW));
1146
+ return ["warn", " " + glyph + " " + pal.paint(m[1], pal.fg(YELLOW))];
1147
+ }
1148
+ m = line.match(RE_ERR);
1149
+ if (m) {
1150
+ return ["error", " " + pal.paint("✘ " + m[1], pal.fg(RED))];
1151
+ }
1152
+ if (!line.trim()) return null;
1153
+ const stripped = line.trim();
1154
+ for (const pfx of EPILOGUE_PREFIXES) {
1155
+ if (stripped.startsWith(pfx)) return null;
1156
+ }
1157
+ return ["dim", " " + pal.dimmed(line.replace(/\s+$/, ""))];
1158
+ }
1159
+
1160
+ function runInstallStream(pal, target, activity) {
1161
+ return new Promise((resolve) => {
1162
+ const env = Object.assign({}, process.env, { PYTHONDONTWRITEBYTECODE: "1" });
1163
+ const proc = spawn("bash", [INSTALL_SH, target], { env, stdio: ["ignore", "pipe", "pipe"] });
1164
+ const tail = [];
1165
+
1166
+ function handle(raw) {
1167
+ tail.push(raw);
1168
+ if (tail.length > 40) tail.splice(0, tail.length - 40);
1169
+ const styled = styleInstallLine(pal, raw);
1170
+ if (styled === null) return;
1171
+ const kind = styled[0];
1172
+ const text = styled[1];
1173
+ if (kind === "section" && activity) {
1174
+ const m = raw.match(RE_INFO);
1175
+ if (m) activity.setLabel(m[1]);
1176
+ }
1177
+ if (text === null) return;
1178
+ if (activity) activity.emit(text);
1179
+ else out(text + "\n");
1180
+ }
1181
+
1182
+ let bufOut = "";
1183
+ let bufErr = "";
1184
+ proc.stdout.on("data", (d) => {
1185
+ bufOut += d.toString("utf8");
1186
+ let idx;
1187
+ while ((idx = bufOut.indexOf("\n")) >= 0) {
1188
+ handle(bufOut.slice(0, idx));
1189
+ bufOut = bufOut.slice(idx + 1);
1190
+ }
1191
+ });
1192
+ proc.stderr.on("data", (d) => {
1193
+ bufErr += d.toString("utf8");
1194
+ let idx;
1195
+ while ((idx = bufErr.indexOf("\n")) >= 0) {
1196
+ handle(bufErr.slice(0, idx));
1197
+ bufErr = bufErr.slice(idx + 1);
1198
+ }
1199
+ });
1200
+
1201
+ let timer = null;
1202
+ if (activity) timer = setInterval(() => activity.tick(), SPIN_INTERVAL * 1000);
1203
+
1204
+ proc.on("error", () => {
1205
+ if (timer) clearInterval(timer);
1206
+ resolve([1, tail]);
1207
+ });
1208
+ proc.on("close", (code) => {
1209
+ if (timer) clearInterval(timer);
1210
+ if (bufOut.trim()) handle(bufOut);
1211
+ if (bufErr.trim()) handle(bufErr);
1212
+ resolve([code === null ? 1 : code, tail]);
1213
+ });
1214
+ });
1215
+ }
1216
+
1217
+ async function screenInstall(pal, target) {
1218
+ out(CLEAR_SCREEN);
1219
+ const header = [
1220
+ "",
1221
+ " " + sectionHeader(pal, "Installing"),
1222
+ "",
1223
+ ];
1224
+ out(header.join("\n") + "\n");
1225
+ const activity = new Activity(pal);
1226
+ activity.start("starting install");
1227
+ const [rc, tail] = await runInstallStream(pal, target, activity);
1228
+ activity.stop();
1229
+ return [rc, tail];
1230
+ }
1231
+
1232
+ function screenInstallError(pal, tail) {
1233
+ const width = termWidth();
1234
+ const body = [pal.paint("The install did not finish cleanly.", pal.fg(RED), pal.bold), ""];
1235
+ for (const line of tail.slice(-14)) {
1236
+ body.push(pal.dimmed(line.replace(/\s+$/, "").slice(0, width - 4)));
1237
+ }
1238
+ const box = Frame.render(pal, body, width, 0.0, "install.sh failed");
1239
+ out("\n" + box.join("\n") + "\n");
1240
+ }
1241
+
1242
+ // --------------------------------------------------------------------------
1243
+ // Gates detection
1244
+ // --------------------------------------------------------------------------
1245
+
1246
+ function runGatesDetect(target, write) {
1247
+ const script = path.join(target, ".claude", "hooks", "gates_detect.py");
1248
+ if (!fs.existsSync(script)) return null;
1249
+ const env = Object.assign({}, process.env, {
1250
+ CLAUDE_PROJECT_DIR: target,
1251
+ PYTHONDONTWRITEBYTECODE: "1",
1252
+ });
1253
+ const args = [script];
1254
+ if (write) args.push("--write");
1255
+ let res;
1256
+ try {
1257
+ res = spawnSync("python3", args, { cwd: target, env, encoding: "utf8", timeout: 60000 });
1258
+ } catch (e) {
1259
+ return null;
1260
+ }
1261
+ if (!res || res.error) return null;
1262
+ const text = res.stdout || "";
1263
+ for (const line of text.split("\n")) {
1264
+ if (line.startsWith("GATES_JSON: ")) {
1265
+ try {
1266
+ return JSON.parse(line.slice("GATES_JSON: ".length));
1267
+ } catch (e) {
1268
+ return null;
1269
+ }
1270
+ }
1271
+ }
1272
+ return null;
1273
+ }
1274
+
1275
+ function screenGates(pal, data) {
1276
+ const width = termWidth();
1277
+ out("\n " + sectionHeader(pal, "Gate detection") + "\n\n");
1278
+ if (!data || data.status === "no_stack") {
1279
+ out(" " + pal.dimmed(
1280
+ "no stack detected - company/gates.config keeps its placeholders for the agents to fill.") + "\n");
1281
+ return;
1282
+ }
1283
+ const stacks = (data.stacks && data.stacks.join(", ")) || "unknown";
1284
+ const lines = [pal.dimmed("stack: " + stacks), ""];
1285
+ const proposed = data.proposed || [];
1286
+ if (proposed.length) {
1287
+ for (const g of proposed) {
1288
+ const name = pal.paint(padRight(g.name || "", 10), pal.fg(GREEN));
1289
+ lines.push(name + " " + pal.dimmed(g.command || ""));
1290
+ }
1291
+ } else {
1292
+ lines.push(pal.dimmed("no invocable gate commands on this machine."));
1293
+ }
1294
+ const status = data.status;
1295
+ if (status === "wrote") {
1296
+ lines.push("");
1297
+ lines.push(pal.paint("wrote " + proposed.length + " gate(s) to company/gates.config", pal.fg(GREEN)));
1298
+ } else if (status === "preserved_existing") {
1299
+ lines.push("");
1300
+ lines.push(pal.dimmed("existing real gates preserved - not overwritten."));
1301
+ }
1302
+ const box = Frame.render(pal, lines, width, 0.5);
1303
+ out(box.join("\n") + "\n");
1304
+ }
1305
+
1306
+ // --------------------------------------------------------------------------
1307
+ // Done screen
1308
+ // --------------------------------------------------------------------------
1309
+
1310
+ function screenDone(pal, target, orientation) {
1311
+ const width = termWidth();
1312
+ out("\n" + rule(pal, width, true) + "\n\n");
1313
+ const check = pal.paint("✔", pal.fg(GREEN), pal.bold);
1314
+ out(center(check + " " + pal.brand("claude-company is installed"), width) + "\n\n");
1315
+
1316
+ const orch = pal.paint("/orchestrator build me <what you want>", pal.gradFg(0.5), pal.bold);
1317
+ if (orientation) {
1318
+ const targetLine = elide("Launch Claude Code in " + target, width - 12);
1319
+ const steps = [
1320
+ ["Open the company", targetLine],
1321
+ ["Give the order", "Type " + orch],
1322
+ ["Review the evidence", "The company self-onboards, builds, and reports back with proof"],
1323
+ ];
1324
+ const blocks = [];
1325
+ for (let idx = 0; idx < steps.length; idx++) {
1326
+ const i = idx + 1;
1327
+ const [title, bodyText] = steps[idx];
1328
+ const num = pal.paint(" " + i + " ", pal.gradFg(i / 3.0), pal.bold);
1329
+ const inner = [
1330
+ num + " " + pal.paint(title, pal.bold),
1331
+ " " + pal.dimmed(bodyText),
1332
+ ];
1333
+ const box = Frame.render(pal, inner, width - 4, i / 3.0);
1334
+ for (const ln of box) blocks.push(" " + ln);
1335
+ blocks.push("");
1336
+ }
1337
+ out(blocks.join("\n") + "\n");
1338
+ out(" " + pal.dimmed("More: docs/getting-started.md") + "\n\n");
1339
+ } else {
1340
+ out(" Next: open Claude Code in your project and run\n");
1341
+ out(" " + orch + "\n\n");
1342
+ }
1343
+
1344
+ out(" " + pal.dimmed(
1345
+ "Re-run anytime - the installer never overwrites your settings or state.") + "\n");
1346
+ }
1347
+
1348
+ // --------------------------------------------------------------------------
1349
+ // TUI driver
1350
+ // --------------------------------------------------------------------------
1351
+
1352
+ async function runTui(args, pal) {
1353
+ const term = new Terminal(pal).enter();
1354
+ try {
1355
+ if (!(await screenHero(term, pal))) {
1356
+ out("\n" + pal.dimmed(" Nothing installed.") + "\n");
1357
+ return EXIT_OK;
1358
+ }
1359
+ const [ok, results] = await screenPreflight(term, pal);
1360
+ if (!ok) {
1361
+ screenPreflightFail(pal, results);
1362
+ return EXIT_PREFLIGHT;
1363
+ }
1364
+
1365
+ let target = null;
1366
+ if (args.target || args.positional) {
1367
+ const cand = args.target || args.positional;
1368
+ const [good, p] = validateTarget(cand);
1369
+ if (good) target = p;
1370
+ }
1371
+ if (target === null) {
1372
+ target = await screenTarget(term, pal);
1373
+ if (target === null) {
1374
+ out("\n" + pal.dimmed(" Install cancelled.") + "\n");
1375
+ return EXIT_CANCEL;
1376
+ }
1377
+ }
1378
+
1379
+ const opts = makeOptions(args.detectGates, args.orientation);
1380
+ const chosen = await screenOptions(term, pal, opts);
1381
+ if (chosen === null) {
1382
+ out("\n" + pal.dimmed(" Install cancelled.") + "\n");
1383
+ return EXIT_CANCEL;
1384
+ }
1385
+
1386
+ if (!(await screenConfirm(term, pal, target, chosen))) {
1387
+ out("\n" + pal.dimmed(" Install cancelled.") + "\n");
1388
+ return EXIT_CANCEL;
1389
+ }
1390
+
1391
+ const [rc, tail] = await screenInstall(pal, target);
1392
+ if (rc !== 0) {
1393
+ screenInstallError(pal, tail);
1394
+ return EXIT_INSTALL;
1395
+ }
1396
+
1397
+ if (chosen.items[0][1]) {
1398
+ const data = runGatesDetect(target, true);
1399
+ screenGates(pal, data);
1400
+ }
1401
+
1402
+ screenDone(pal, target, chosen.items[1][1]);
1403
+ return EXIT_OK;
1404
+ } finally {
1405
+ term.restore();
1406
+ }
1407
+ }
1408
+
1409
+ // --------------------------------------------------------------------------
1410
+ // Plain (non-interactive) driver
1411
+ // --------------------------------------------------------------------------
1412
+
1413
+ function runPlain(args) {
1414
+ const targetArg = args.target || args.positional;
1415
+ if (!targetArg) {
1416
+ process.stderr.write(
1417
+ "install: a target is required in non-interactive mode.\n" +
1418
+ "Usage: install --target /path/to/your/project [--yes]\n");
1419
+ return EXIT_USAGE;
1420
+ }
1421
+
1422
+ // -- preflight --
1423
+ process.stdout.write("claude-company installer - preflight\n");
1424
+ const results = PROBES.map((p) => {
1425
+ const [status, detail] = p.fn();
1426
+ return [p, status, detail];
1427
+ });
1428
+ for (const [probe, status, detail] of results) {
1429
+ process.stdout.write(" [" + status + "] " + padRight(probe.name, 18) + " " + detail + "\n");
1430
+ }
1431
+ const hardFail = results.filter((r) => r[0].hard && r[1] === FAIL);
1432
+ if (hardFail.length) {
1433
+ const names = hardFail.map((r) => r[0].name).join(", ");
1434
+ process.stderr.write("install: missing required tool(s): " + names + "\n");
1435
+ process.stderr.write("install: these are required by the enforcement hooks.\n");
1436
+ return EXIT_PREFLIGHT;
1437
+ }
1438
+
1439
+ // -- validate target --
1440
+ const [ok, p, msg] = validateTarget(targetArg);
1441
+ if (!ok) {
1442
+ process.stderr.write("install: target " + targetArg + ": " + msg + "\n");
1443
+ return EXIT_PREFLIGHT;
1444
+ }
1445
+ process.stdout.write("\ntarget: " + p + " (" + msg + ")\n");
1446
+
1447
+ // -- run install.sh, streaming raw --
1448
+ process.stdout.write("\ninstalling...\n");
1449
+ const env = Object.assign({}, process.env, { PYTHONDONTWRITEBYTECODE: "1" });
1450
+ const res = spawnSync("bash", [INSTALL_SH, p], { env, stdio: "inherit" });
1451
+ const rc = res.status;
1452
+ if (res.error || rc !== 0) {
1453
+ process.stderr.write("install: install.sh failed (exit " + (rc === null ? 1 : rc) + ").\n");
1454
+ return EXIT_INSTALL;
1455
+ }
1456
+
1457
+ // -- optional gate detection --
1458
+ if (args.detectGates) {
1459
+ process.stdout.write("\ndetecting gates...\n");
1460
+ const data = runGatesDetect(p, true);
1461
+ if (!data || data.status === "no_stack") {
1462
+ process.stdout.write(" no stack detected - gates.config keeps its placeholders.\n");
1463
+ } else {
1464
+ process.stdout.write(" stack: " + ((data.stacks && data.stacks.join(", ")) || "") + "\n");
1465
+ for (const g of data.proposed || []) {
1466
+ process.stdout.write(" " + padRight(g.name || "", 10) + " " + (g.command || "") + "\n");
1467
+ }
1468
+ if (data.status === "wrote") {
1469
+ process.stdout.write(" wrote " + (data.proposed || []).length + " gate(s) to company/gates.config\n");
1470
+ } else if (data.status === "preserved_existing") {
1471
+ process.stdout.write(" existing real gates preserved.\n");
1472
+ }
1473
+ }
1474
+ }
1475
+
1476
+ // -- summary --
1477
+ const counts = sourceCounts();
1478
+ process.stdout.write("\nclaude-company installed into " + p + "\n");
1479
+ process.stdout.write(" agents " + counts.agents + ", skills " + counts.skills +
1480
+ ", hooks " + counts.hooks + ", canon docs, state scaffolds\n");
1481
+ process.stdout.write(" next: open Claude Code and run /orchestrator build me <what you want>\n");
1482
+ return EXIT_OK;
1483
+ }
1484
+
1485
+ // --------------------------------------------------------------------------
1486
+ // Argument parsing and entry
1487
+ // --------------------------------------------------------------------------
1488
+
1489
+ function helpText() {
1490
+ return [
1491
+ "Install claude-company - an AI software company you drop into your repo.",
1492
+ "",
1493
+ "Usage:",
1494
+ " install [TARGET] [options]",
1495
+ "",
1496
+ "Options:",
1497
+ " --target DIR Target project directory (must already exist).",
1498
+ " -y, --yes Non-interactive; accept defaults (implies plain).",
1499
+ " --plain Force plain, non-interactive output.",
1500
+ " --no-color Monochrome output (NO_COLOR is honored too).",
1501
+ " --detect-gates Auto-detect gates after install (default).",
1502
+ " --no-detect-gates Skip gate auto-detection.",
1503
+ " --orientation Show the 60-second orientation (default).",
1504
+ " --no-orientation Skip the orientation on the done screen.",
1505
+ " -h, --help Show this help.",
1506
+ "",
1507
+ "Examples:",
1508
+ " install full TUI",
1509
+ " install --target ~/proj --yes non-interactive install",
1510
+ " install ~/proj --plain plain line-per-step output",
1511
+ "",
1512
+ "Exit codes: 0 ok, 1 usage, 2 preflight/target hard-fail, 3 install failure,",
1513
+ "130 cancelled.",
1514
+ ].join("\n");
1515
+ }
1516
+
1517
+ function parseArgs(argv) {
1518
+ const args = {
1519
+ positional: null,
1520
+ target: null,
1521
+ yes: false,
1522
+ plain: false,
1523
+ noColor: false,
1524
+ detectGates: true,
1525
+ orientation: true,
1526
+ help: false,
1527
+ };
1528
+ for (let i = 0; i < argv.length; i++) {
1529
+ const a = argv[i];
1530
+ if (a === "-h" || a === "--help") {
1531
+ args.help = true;
1532
+ } else if (a === "-y" || a === "--yes") {
1533
+ args.yes = true;
1534
+ } else if (a === "--plain") {
1535
+ args.plain = true;
1536
+ } else if (a === "--no-color") {
1537
+ args.noColor = true;
1538
+ } else if (a === "--detect-gates") {
1539
+ args.detectGates = true;
1540
+ } else if (a === "--no-detect-gates") {
1541
+ args.detectGates = false;
1542
+ } else if (a === "--orientation") {
1543
+ args.orientation = true;
1544
+ } else if (a === "--no-orientation") {
1545
+ args.orientation = false;
1546
+ } else if (a === "--target") {
1547
+ args.target = argv[++i];
1548
+ if (args.target === undefined) {
1549
+ return { error: "install: --target requires a directory argument" };
1550
+ }
1551
+ } else if (a.startsWith("--target=")) {
1552
+ args.target = a.slice("--target=".length);
1553
+ } else if (a === "--") {
1554
+ // remaining are positional
1555
+ if (i + 1 < argv.length && args.positional === null) args.positional = argv[i + 1];
1556
+ break;
1557
+ } else if (a.startsWith("-") && a !== "-") {
1558
+ return { error: "install: unrecognized option '" + a + "'" };
1559
+ } else if (args.positional === null) {
1560
+ args.positional = a;
1561
+ } else {
1562
+ return { error: "install: unexpected extra argument '" + a + "'" };
1563
+ }
1564
+ }
1565
+ return { args };
1566
+ }
1567
+
1568
+ function stdioIsTty() {
1569
+ return Boolean(process.stdin.isTTY) && Boolean(process.stdout.isTTY);
1570
+ }
1571
+
1572
+ // Public entry: runs the installer. `argv` is the args after the `install`
1573
+ // subcommand. Returns a Promise resolving to the process exit code.
1574
+ async function run(argv) {
1575
+ const parsed = parseArgs(argv || []);
1576
+ if (parsed.error) {
1577
+ process.stderr.write(parsed.error + "\n\n");
1578
+ process.stderr.write(helpText() + "\n");
1579
+ return EXIT_PREFLIGHT;
1580
+ }
1581
+ const args = parsed.args;
1582
+ if (args.help) {
1583
+ process.stdout.write(helpText() + "\n");
1584
+ return EXIT_OK;
1585
+ }
1586
+ const pal = decidePalette(args.noColor);
1587
+
1588
+ const interactive = stdioIsTty() && !args.yes && !args.plain &&
1589
+ Boolean(process.stdin.setRawMode);
1590
+
1591
+ if (interactive) {
1592
+ return runTui(args, pal);
1593
+ }
1594
+ return runPlain(args);
1595
+ }
1596
+
1597
+ module.exports = { run, helpText };
1598
+ // Test-only surface: lets tests validate the hero font table and rendering
1599
+ // without driving the interactive TUI. Not part of the public API.
1600
+ module.exports._hero = { FONT_ANSI_SHADOW, HERO_ROWS, heroRows };