tribunal-kit 2.4.2 → 2.4.3

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.
@@ -1,543 +1,548 @@
1
1
  #!/usr/bin/env node
2
- /**
3
- * tribunal-kit CLI
4
- *
5
- * Commands:
6
- * init — Install .agent/ into target project
7
- * update — Re-install to get latest changes
8
- * status — Check if .agent/ is installed
9
- *
10
- * Usage:
11
- * npx tribunal-kit init
12
- * npx tribunal-kit init --force
13
- * npx tribunal-kit init --path ./myapp
14
- * npx tribunal-kit init --quiet
15
- * npx tribunal-kit init --dry-run
16
- * tribunal-kit update
17
- * tribunal-kit status
18
- */
19
-
20
- const fs = require('fs');
21
- const path = require('path');
22
- const https = require('https');
23
- const { execSync } = require('child_process');
24
-
25
- const PKG = require(path.resolve(__dirname, '..', 'package.json'));
26
- const CURRENT_VERSION = PKG.version;
27
-
28
- // ── Colors ───────────────────────────────────────────────
29
- const C = {
30
- reset: '\x1b[0m',
31
- bold: '\x1b[1m',
32
- dim: '\x1b[2m',
33
- red: '\x1b[91m',
34
- green: '\x1b[92m',
35
- yellow: '\x1b[93m',
36
- blue: '\x1b[94m',
37
- magenta: '\x1b[95m',
38
- cyan: '\x1b[96m',
39
- white: '\x1b[97m',
40
- gray: '\x1b[90m',
41
- bgCyan: '\x1b[46m',
42
- };
43
-
44
- function colorize(color, text) {
45
- return `${C[color]}${text}${C.reset}`;
46
- }
47
-
48
- function c(color, text) { return `${C[color]}${text}${C.reset}`; }
49
- function bold(text) { return `${C.bold}${text}${C.reset}`; }
50
-
51
- // ── Logging ──────────────────────────────────────────────
52
- let quiet = false;
53
-
54
- function log(msg) { if (!quiet) console.log(msg); }
55
- function ok(msg) { if (!quiet) console.log(` ${c('green', '✔')} ${msg}`); }
56
- function warn(msg) { if (!quiet) console.log(` ${c('yellow', '⚠')} ${msg}`); }
57
- function err(msg) { console.error(` ${c('red', '✖')} ${msg}`); }
58
- function dim(msg) { if (!quiet) console.log(` ${c('gray', msg)}`); }
59
-
60
- // ── Arg Parser ───────────────────────────────────────────
61
- function parseArgs(argv) {
62
- const args = { command: null, flags: {} };
63
- const raw = argv.slice(2);
64
-
65
- // First non-flag arg is the command
66
- for (const arg of raw) {
67
- if (!arg.startsWith('--') && !args.command) {
68
- args.command = arg;
69
- continue;
70
- }
71
- if (arg === '--force') { args.flags.force = true; continue; }
72
- if (arg === '--quiet') { args.flags.quiet = true; continue; }
73
- if (arg === '--dry-run') { args.flags.dryRun = true; continue; }
74
- if (arg === '--skip-update-check') { args.flags.skipUpdateCheck = true; continue; }
75
- if (arg.startsWith('--path=')) {
76
- args.flags.path = arg.split('=').slice(1).join('=');
77
- }
78
- if (arg === '--path') {
79
- const idx = raw.indexOf('--path');
80
- args.flags.path = raw[idx + 1] || null;
81
- }
82
- if (arg.startsWith('--branch=')) {
83
- args.flags.branch = arg.split('=').slice(1).join('=');
84
- }
85
- }
86
-
87
- return args;
88
- }
89
-
90
- // ── File Utilities ────────────────────────────────────────
91
- function copyDir(src, dest, dryRun = false) {
92
- if (!dryRun) {
93
- fs.mkdirSync(dest, { recursive: true });
94
- }
95
-
96
- const entries = fs.readdirSync(src, { withFileTypes: true });
97
- let count = 0;
98
-
99
- for (const entry of entries) {
100
- const srcPath = path.join(src, entry.name);
101
- const destPath = path.join(dest, entry.name);
102
-
103
- if (entry.isDirectory()) {
104
- count += copyDir(srcPath, destPath, dryRun);
105
- } else {
106
- if (!dryRun) {
107
- fs.cpSync(srcPath, destPath, { force: true });
108
- }
109
- count++;
110
- }
111
- }
112
-
113
- return count;
114
- }
115
-
116
- function countDir(dir) {
117
- let count = 0;
118
- const entries = fs.readdirSync(dir, { withFileTypes: true });
119
- for (const e of entries) {
120
- if (e.isDirectory()) count += countDir(path.join(dir, e.name));
121
- else count++;
122
- }
123
- return count;
124
- }
125
-
126
- // ── Version Check & Auto-Update ──────────────────────────
127
-
128
- /**
129
- * Compare two semver strings. Returns:
130
- * 1 if a > b, -1 if a < b, 0 if equal.
131
- */
132
- function compareSemver(a, b) {
133
- const pa = a.replace(/^v/, '').split('.').map(Number);
134
- const pb = b.replace(/^v/, '').split('.').map(Number);
135
- for (let i = 0; i < 3; i++) {
136
- const na = pa[i] || 0;
137
- const nb = pb[i] || 0;
138
- if (na > nb) return 1;
139
- if (na < nb) return -1;
140
- }
141
- return 0;
142
- }
143
-
144
- /**
145
- * Fetch the latest version from GitHub Releases.
146
- * Returns the version string (e.g. '2.4.0') or null on failure.
147
- */
148
- function fetchLatestVersion() {
149
- return new Promise((resolve) => {
150
- const req = https.get(
151
- 'https://api.github.com/repos/Harmitx7/tribunal-kit/releases/latest',
152
- {
153
- headers: {
154
- 'Accept': 'application/vnd.github.v3+json',
155
- 'User-Agent': `tribunal-kit/${CURRENT_VERSION}`
156
- },
157
- timeout: 5000
158
- },
159
- (res) => {
160
- let data = '';
161
- res.on('data', (chunk) => { data += chunk; });
162
- res.on('end', () => {
163
- try {
164
- const json = JSON.parse(data);
165
- // GitHub tags usually have a 'v' prefix (e.g., 'v2.4.0')
166
- const version = json.tag_name ? json.tag_name.replace(/^v/, '') : null;
167
- resolve(version);
168
- } catch {
169
- resolve(null);
170
- }
171
- });
172
- }
173
- );
174
- req.on('error', () => resolve(null));
175
- req.on('timeout', () => { req.destroy(); resolve(null); });
176
- });
177
- }
178
-
179
- /**
180
- * Check for a newer version and re-invoke with @latest if found.
181
- * Uses TK_SKIP_UPDATE_CHECK env var as recursion guard.
182
- * Returns true if a re-invoke happened (caller should exit), false otherwise.
183
- */
184
- async function autoUpdateCheck(originalArgs) {
185
- // Recursion guard: if we're already a re-invoked process, skip
186
- if (process.env.TK_SKIP_UPDATE_CHECK === '1') {
187
- return false;
188
- }
189
-
190
- const latestVersion = await fetchLatestVersion();
191
-
192
- if (!latestVersion) {
193
- // Network fail — proceed silently with current version
194
- return false;
195
- }
196
-
197
- if (compareSemver(latestVersion, CURRENT_VERSION) <= 0) {
198
- // Already up to date
199
- dim(`Version ${CURRENT_VERSION} is up to date.`);
200
- return false;
201
- }
202
-
203
- // Newer version available — re-invoke
204
- log('');
205
- log(colorize('cyan', ` ⬆ New version available: ${colorize('bold', CURRENT_VERSION)} → ${colorize('bold', latestVersion)}`));
206
- log(colorize('gray', ' Re-invoking with latest version...'));
207
- log('');
208
-
209
- try {
210
- // Build the command pulling directly from GitHub
211
- const args = originalArgs.join(' ');
212
- const cmd = `npx -y github:Harmitx7/tribunal-kit#v${latestVersion} ${args}`;
213
-
214
- execSync(cmd, {
215
- stdio: 'inherit',
216
- env: { ...process.env, TK_SKIP_UPDATE_CHECK: '1' },
217
- });
218
- return true; // Re-invoke succeeded, caller should exit
219
- } catch (e) {
220
- warn(`Auto-update failed: ${e.message}`);
221
- warn('Continuing with current version...');
222
- return false; // Fall through to current version
223
- }
224
- }
225
-
226
- // ── Kit Source Location ───────────────────────────────────
227
- function getKitAgent() {
228
- // When installed via npm, the .agent/ folder is next to this script's package
229
- const kitRoot = path.resolve(__dirname, '..');
230
- const agentDir = path.join(kitRoot, '.agent');
231
-
232
- if (!fs.existsSync(agentDir)) {
233
- err(`Kit .agent/ folder not found at: ${agentDir}`);
234
- err('The package may be corrupted. Try: npm install -g tribunal-kit');
235
- process.exit(1);
236
- }
237
-
238
- return agentDir;
239
- }
240
-
241
- // ── Self-Install Guard ────────────────────────────────────
242
- /**
243
- * Returns true if the target directory IS the tribunal-kit package itself.
244
- * This prevents `init --force` / `update` from deleting the package's own files
245
- * when run from inside the project directory.
246
- */
247
- function isSelfInstall(targetDir) {
248
- const kitRoot = path.resolve(__dirname, '..');
249
- const resolvedTarget = path.resolve(targetDir);
250
-
251
- // Direct path match
252
- if (resolvedTarget === kitRoot) return true;
253
-
254
- // Check if the target's package.json is this package
255
- const targetPkg = path.join(resolvedTarget, 'package.json');
256
- if (fs.existsSync(targetPkg)) {
257
- try {
258
- const targetName = JSON.parse(fs.readFileSync(targetPkg, 'utf8')).name;
259
- if (targetName === PKG.name) return true;
260
- } catch {
261
- // Unreadable package.json — not a match
262
- }
263
- }
264
-
265
- return false;
266
- }
267
-
268
- // ── Banner ────────────────────────────────────────────────
269
- function banner() {
270
- if (quiet) return;
271
- // Big ASCII art (TRIBUNAL-KIT)
272
- const art = String.raw`
273
- ___________ ._____. .__ ____ __.__ __
274
- \__ ___/______|__\_ |__ __ __ ____ _____ | | | |/ _|__|/ |_
275
- | | \_ __ \ || __ \| | \/ \\__ \ | | ______ | < | \ __\
276
- | | | | \/ || \_\ \ | / | \/ __ \| |__ /_____/ | | \| || |
277
- |____| |__| |__||___ /____/|___| (____ /____/ |____|__ \__||__|
278
- \/ \/ \/ \/ `.split('\n').filter(Boolean);
279
- console.log();
280
- for (const line of art) log(` ${c('cyan', bold(line))}`);
281
- console.log();
282
- // Subtitle strip
283
- const W = 80;
284
- const sub = 'Anti-Hallucination Agent System';
285
- const sp = Math.max(0, W - sub.length);
286
- const centred = ' '.repeat(Math.floor(sp / 2)) + sub + ' '.repeat(Math.ceil(sp / 2));
287
- console.log(` ${c('cyan', '╔' + '═'.repeat(W) + '╗')}`);
288
- console.log(` ${c('cyan', '║')}${c('gray', centred)}${c('cyan', '║')}`);
289
- console.log(` ${c('cyan', '╚' + '═'.repeat(W) + '╝')}`);
290
- console.log();
291
- }
292
-
293
- // ── Commands ──────────────────────────────────────────────
294
- function cmdInit(flags) {
295
- const agentSrc = getKitAgent();
296
- const targetDir = flags.path ? path.resolve(flags.path) : process.cwd();
297
- const agentDest = path.join(targetDir, '.agent');
298
- const dryRun = flags.dryRun || false;
299
-
300
- // ── Self-install guard ──────────────────────────────────
301
- if (isSelfInstall(targetDir)) {
302
- err('Cannot run init/update inside the tribunal-kit package itself.');
303
- err(`Target: ${targetDir}`);
304
- err(`Package: ${path.resolve(__dirname, '..')}`);
305
- console.log();
306
- dim('This command is designed to install .agent/ into OTHER projects.');
307
- dim('Run it from the root of the project you want to set up:');
308
- dim(' cd /path/to/your-project');
309
- dim(' npx tribunal-kit init');
310
- console.log();
311
- process.exit(1);
312
- }
313
- // ────────────────────────────────────────────────────────
314
-
315
- banner();
316
-
317
- if (dryRun) {
318
- log(colorize('yellow', ' DRY RUN — no files will be written'));
319
- console.log();
320
- }
321
-
322
- // Check target exists
323
- if (!fs.existsSync(targetDir)) {
324
- err(`Target directory not found: ${targetDir}`);
325
- process.exit(1);
326
- }
327
-
328
- // Check if .agent already exists
329
- if (fs.existsSync(agentDest) && !flags.force) {
330
- warn('.agent/ already exists in this project.');
331
- log(` ${c('gray', '▸')} To refresh or update it, run: ${colorize('white', 'tribunal-kit init --force')}`);
332
- log(` ${c('gray', '▸')} Or check status with: ${colorize('cyan', 'tribunal-kit status')}`);
333
- console.log();
334
- process.exit(0);
335
- }
336
-
337
- if (!dryRun && fs.existsSync(agentDest) && flags.force) {
338
- const subdirs = ['agents', 'workflows', 'skills', 'scripts', '.shared'];
339
- for (const sub of subdirs) {
340
- const subPath = path.join(agentDest, sub);
341
- if (fs.existsSync(subPath)) {
342
- fs.rmSync(subPath, { recursive: true, force: true });
343
- }
344
- }
345
- }
346
-
347
- // Count what we're installing
348
- const totalFiles = countDir(agentSrc);
349
- log(` ${c('gray','▸')} Scanning ${c('white', String(totalFiles))} files ${c('gray','→')} ${c('gray', agentDest)}`);
350
-
351
- try {
352
- const copied = copyDir(agentSrc, agentDest, dryRun);
353
-
354
- console.log();
355
- if (dryRun) {
356
- ok(`${bold('DRY RUN')} complete — would install ${c('cyan', String(copied))} files`);
357
- dim(`Target: ${agentDest}`);
358
- } else {
359
- // ── Success card — W=62, rows padded by plain-text length ──
360
- const W = 62;
361
- const agentsCount = fs.readdirSync(path.join(agentDest, 'agents')).length;
362
- const workflowsCount = fs.readdirSync(path.join(agentDest, 'workflows')).length;
363
- const skillsCount = fs.readdirSync(path.join(agentDest, 'skills')).length;
364
- const scriptsCount = fs.readdirSync(path.join(agentDest, 'scripts')).length;
365
-
366
- // Stat rows: compute trailing spaces from plain text so right ║ aligns
367
- const statRow = (icon, label, val, col) => {
368
- // emoji JS .length===2 == terminal display width 2 ✓
369
- const plain = ` ${icon} ${label.padEnd(10)}${String(val).padStart(3)} installed`;
370
- const trail = ' '.repeat(Math.max(0, W - plain.length));
371
- return ` ${c('cyan','║')} ${icon} ${c('white',label.padEnd(10))}${c(col,String(val).padStart(3))} ${c('gray','installed')}${trail}${c('cyan','║')}`;
372
- };
373
- // Plain-text rows (header / blank)
374
- const plainRow = (text, wrapFn) => {
375
- const trail = ' '.repeat(Math.max(0, W - text.length));
376
- return ` ${c('cyan','║')}${wrapFn(text)}${trail}${c('cyan','║')}`;
377
- };
378
- // Next-step rows: fixed cmd column + description
379
- const stepRow = (cmd, desc) => {
380
- const plain = ` ${cmd.padEnd(16)}${desc}`;
381
- const trail = ' '.repeat(Math.max(0, W - plain.length));
382
- return ` ${c('cyan','║')} ${c('white',cmd.padEnd(16))}${c('gray',desc)}${trail}${c('cyan','║')}`;
383
- };
384
-
385
- console.log(` ${c('green','✔')} ${bold(c('green','Installation complete'))} ${c('gray','—')} ${c('white',String(copied))} files`);
386
- console.log(` ${c('gray',' ╰─')} ${c('gray', agentDest)}`);
387
- console.log();
388
- console.log(` ${c('cyan', '╔' + '═'.repeat(W) + '╗')}`);
389
- console.log(plainRow(` What's inside:`, s => c('bold', c('white', s))));
390
- console.log(` ${c('cyan', '╠' + '═'.repeat(W) + '╣')}`);
391
- console.log(statRow('🤖', 'Agents', agentsCount, 'magenta'));
392
- console.log(statRow('⚡', 'Workflows', workflowsCount, 'yellow'));
393
- console.log(statRow('🧠', 'Skills', skillsCount, 'blue'));
394
- console.log(statRow('🔧', 'Scripts', scriptsCount, 'green'));
395
- console.log(` ${c('cyan', '╠' + '═'.repeat(W) + '╣')}`);
396
- console.log(plainRow('', () => ''));
397
- console.log(plainRow(` Next steps:`, s => c('gray', s)));
398
- console.log(stepRow('/generate', 'Generate code with anti-hallucination'));
399
- console.log(stepRow('/review', 'Audit existing code for issues'));
400
- console.log(stepRow('/tribunal-full', 'Run all 11 reviewers in parallel'));
401
- console.log(plainRow('', () => ''));
402
- console.log(` ${c('cyan', '╚' + '═'.repeat(W) + '╝')}`);
403
- console.log();
404
- log(` ${c('gray', '✦ Your AI IDE will pick up changes automatically.')}`);
405
- }
406
-
407
- console.log();
408
- } catch (e) {
409
- err(`Failed to install: ${e.message}`);
410
- process.exit(1);
411
- }
412
- }
413
-
414
- function cmdUpdate(flags) {
415
- // ── Self-install guard (early, before banner) ───────────
416
- const targetDir = flags.path ? path.resolve(flags.path) : process.cwd();
417
- if (isSelfInstall(targetDir)) {
418
- err('Cannot run update inside the tribunal-kit package itself.');
419
- err(`Target: ${targetDir}`);
420
- console.log();
421
- dim('This command is designed to update .agent/ in OTHER projects.');
422
- dim('Run it from the root of the project you want to update:');
423
- dim(' cd /path/to/your-project');
424
- dim(' npx tribunal-kit update');
425
- console.log();
426
- process.exit(1);
427
- }
428
- // ────────────────────────────────────────────────────────
429
-
430
- // Update = init with --force
431
- flags.force = true;
432
- if (!quiet) {
433
- log(` ${c('cyan','↻')} ${bold('Updating')} ${c('white','.agent/')} to latest version...`);
434
- console.log();
435
- }
436
- cmdInit(flags);
437
- }
438
-
439
- // ── Async Main Wrapper ───────────────────────────────────
440
- async function runWithUpdateCheck(command, flags) {
441
- const shouldSkip = flags.skipUpdateCheck || process.env.TK_SKIP_UPDATE_CHECK === '1';
442
-
443
- if (!shouldSkip && (command === 'init' || command === 'update')) {
444
- // Pass through the original args (minus the node/script path)
445
- const originalArgs = process.argv.slice(2);
446
- const didReInvoke = await autoUpdateCheck(originalArgs);
447
- if (didReInvoke) {
448
- process.exit(0); // Latest version handled it
449
- }
450
- }
451
-
452
- // Proceed with current version
453
- switch (command) {
454
- case 'init':
455
- cmdInit(flags);
456
- break;
457
- case 'update':
458
- cmdUpdate(flags);
459
- break;
460
- case 'status':
461
- cmdStatus(flags);
462
- break;
463
- case 'help':
464
- case '--help':
465
- case '-h':
466
- case null:
467
- cmdHelp();
468
- break;
469
- default:
470
- err(`Unknown command: "${command}"`);
471
- console.log();
472
- dim('Run tribunal-kit --help for usage');
473
- process.exit(1);
474
- }
475
- }
476
-
477
- function cmdStatus(flags) {
478
- const targetDir = flags.path ? path.resolve(flags.path) : process.cwd();
479
- const agentDest = path.join(targetDir, '.agent');
480
-
481
- banner();
482
-
483
- if (!fs.existsSync(agentDest)) {
484
- log(` ${c('red','✖')} ${bold('Not installed')} in this project`);
485
- console.log();
486
- log(` ${c('gray','Run:')} ${c('cyan','npx tribunal-kit init')}`);
487
- console.log();
488
- return;
489
- }
490
-
491
- log(` ${c('green','✔')} ${bold(c('green','Installed'))} ${c('gray','→')} ${c('gray', agentDest)}`);
492
- console.log();
493
-
494
- const icons = { agents: '🤖', workflows: '⚡', skills: '🧠', scripts: '🔧' };
495
- const colors = { agents: 'magenta', workflows: 'yellow', skills: 'blue', scripts: 'green' };
496
- const subdirs = ['agents', 'workflows', 'skills', 'scripts'];
497
- for (const sub of subdirs) {
498
- const subPath = path.join(agentDest, sub);
499
- if (fs.existsSync(subPath)) {
500
- const count = fs.readdirSync(subPath).filter(f => !fs.statSync(path.join(subPath, f)).isDirectory()).length;
501
- log(` ${icons[sub]} ${c(colors[sub], sub.padEnd(12))}${c('white', String(count).padStart(3))} files`);
502
- }
503
- }
504
- console.log();
505
- }
506
-
507
- function cmdHelp() {
508
- banner();
509
- const cmd = (name, desc) => ` ${c('cyan', name.padEnd(10))} ${c('gray', desc)}`;
510
- const opt = (flag, desc) => ` ${c('yellow', flag.padEnd(22))} ${c('gray', desc)}`;
511
- const ex = (s) => ` ${c('gray', '▸')} ${c('white', s)}`;
512
-
513
- log(bold(' Commands'));
514
- log(` ${c('gray','─'.repeat(40))}`);
515
- log(cmd('init', 'Install .agent/ into current project'));
516
- log(cmd('update', 'Re-install to get latest version'));
517
- log(cmd('status', 'Check if .agent/ is installed'));
518
- console.log();
519
- log(bold(' Options'));
520
- log(` ${c('gray','─'.repeat(40))}`);
521
- log(opt('--force', 'Overwrite existing .agent/ folder'));
522
- log(opt('--path <dir>', 'Install in specific directory'));
523
- log(opt('--quiet', 'Suppress all output'));
524
- log(opt('--dry-run', 'Preview actions without executing'));
525
- log(opt('--skip-update-check', 'Skip auto-update version check'));
526
- console.log();
527
- log(bold(' Examples'));
528
- log(` ${c('gray','─'.repeat(40))}`);
529
- log(ex('npx tribunal-kit init'));
530
- log(ex('npx tribunal-kit init --force'));
531
- log(ex('npx tribunal-kit init --path ./my-app'));
532
- log(ex('npx tribunal-kit init --dry-run'));
533
- log(ex('npx tribunal-kit update'));
534
- log(ex('npx tribunal-kit status'));
535
- console.log();
536
- }
537
-
538
- // ── Main ──────────────────────────────────────────────────
539
- const { command, flags } = parseArgs(process.argv);
540
-
541
- if (flags.quiet) quiet = true;
542
-
543
- runWithUpdateCheck(command, flags);
2
+ /**
3
+ * tribunal-kit CLI
4
+ *
5
+ * Commands:
6
+ * init — Install .agent/ into target project
7
+ * update — Re-install to get latest changes
8
+ * status — Check if .agent/ is installed
9
+ *
10
+ * Usage:
11
+ * npx tribunal-kit init
12
+ * npx tribunal-kit init --force
13
+ * npx tribunal-kit init --path ./myapp
14
+ * npx tribunal-kit init --quiet
15
+ * npx tribunal-kit init --dry-run
16
+ * tribunal-kit update
17
+ * tribunal-kit status
18
+ */
19
+
20
+ const fs = require('fs');
21
+ const path = require('path');
22
+ const https = require('https');
23
+ const { execSync } = require('child_process');
24
+
25
+ const PKG = require(path.resolve(__dirname, '..', 'package.json'));
26
+ const CURRENT_VERSION = PKG.version;
27
+
28
+ // ── Colors ───────────────────────────────────────────────
29
+ const C = {
30
+ reset: '\x1b[0m',
31
+ bold: '\x1b[1m',
32
+ dim: '\x1b[2m',
33
+ red: '\x1b[91m',
34
+ green: '\x1b[92m',
35
+ yellow: '\x1b[93m',
36
+ blue: '\x1b[94m',
37
+ magenta: '\x1b[95m',
38
+ cyan: '\x1b[96m',
39
+ white: '\x1b[97m',
40
+ gray: '\x1b[90m',
41
+ bgCyan: '\x1b[46m',
42
+ };
43
+
44
+ function colorize(color, text) {
45
+ return `${C[color]}${text}${C.reset}`;
46
+ }
47
+
48
+ function c(color, text) { return `${C[color]}${text}${C.reset}`; }
49
+ function bold(text) { return `${C.bold}${text}${C.reset}`; }
50
+
51
+ // ── Logging ──────────────────────────────────────────────
52
+ let quiet = false;
53
+
54
+ function log(msg) { if (!quiet) console.log(msg); }
55
+ function ok(msg) { if (!quiet) console.log(` ${c('green', '✔')} ${msg}`); }
56
+ function warn(msg) { if (!quiet) console.log(` ${c('yellow', '⚠')} ${msg}`); }
57
+ function err(msg) { console.error(` ${c('red', '✖')} ${msg}`); }
58
+ function dim(msg) { if (!quiet) console.log(` ${c('gray', msg)}`); }
59
+
60
+ // ── Arg Parser ───────────────────────────────────────────
61
+ function parseArgs(argv) {
62
+ const args = { command: null, flags: {} };
63
+ const raw = argv.slice(2);
64
+
65
+ // First non-flag arg is the command
66
+ for (const arg of raw) {
67
+ if (!arg.startsWith('--') && !args.command) {
68
+ args.command = arg;
69
+ continue;
70
+ }
71
+ if (arg === '--force') { args.flags.force = true; continue; }
72
+ if (arg === '--quiet') { args.flags.quiet = true; continue; }
73
+ if (arg === '--dry-run') { args.flags.dryRun = true; continue; }
74
+ if (arg === '--skip-update-check') { args.flags.skipUpdateCheck = true; continue; }
75
+ if (arg.startsWith('--path=')) {
76
+ args.flags.path = arg.split('=').slice(1).join('=');
77
+ }
78
+ if (arg === '--path') {
79
+ const idx = raw.indexOf('--path');
80
+ args.flags.path = raw[idx + 1] || null;
81
+ }
82
+ if (arg.startsWith('--branch=')) {
83
+ args.flags.branch = arg.split('=').slice(1).join('=');
84
+ }
85
+ }
86
+
87
+ return args;
88
+ }
89
+
90
+ // ── File Utilities ────────────────────────────────────────
91
+ function copyDir(src, dest, dryRun = false) {
92
+ if (!dryRun) {
93
+ fs.mkdirSync(dest, { recursive: true });
94
+ }
95
+
96
+ const entries = fs.readdirSync(src, { withFileTypes: true });
97
+ let count = 0;
98
+
99
+ for (const entry of entries) {
100
+ const srcPath = path.join(src, entry.name);
101
+ const destPath = path.join(dest, entry.name);
102
+
103
+ if (entry.isDirectory()) {
104
+ count += copyDir(srcPath, destPath, dryRun);
105
+ } else {
106
+ if (!dryRun) {
107
+ fs.cpSync(srcPath, destPath, { force: true });
108
+ }
109
+ count++;
110
+ }
111
+ }
112
+
113
+ return count;
114
+ }
115
+
116
+ function countDir(dir) {
117
+ let count = 0;
118
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
119
+ for (const e of entries) {
120
+ if (e.isDirectory()) count += countDir(path.join(dir, e.name));
121
+ else count++;
122
+ }
123
+ return count;
124
+ }
125
+
126
+ // ── Version Check & Auto-Update ──────────────────────────
127
+
128
+ /**
129
+ * Compare two semver strings. Returns:
130
+ * 1 if a > b, -1 if a < b, 0 if equal.
131
+ */
132
+ function compareSemver(a, b) {
133
+ const pa = a.replace(/^v/, '').split('.').map(Number);
134
+ const pb = b.replace(/^v/, '').split('.').map(Number);
135
+ for (let i = 0; i < 3; i++) {
136
+ const na = pa[i] || 0;
137
+ const nb = pb[i] || 0;
138
+ if (na > nb) return 1;
139
+ if (na < nb) return -1;
140
+ }
141
+ return 0;
142
+ }
143
+
144
+ /**
145
+ * Fetch the latest version from GitHub Releases.
146
+ * Returns the version string (e.g. '2.4.0') or null on failure.
147
+ */
148
+ function fetchLatestVersion() {
149
+ return new Promise((resolve) => {
150
+ const req = https.get(
151
+ 'https://api.github.com/repos/Harmitx7/tribunal-kit/releases/latest',
152
+ {
153
+ headers: {
154
+ 'Accept': 'application/vnd.github.v3+json',
155
+ 'User-Agent': `tribunal-kit/${CURRENT_VERSION}`
156
+ },
157
+ timeout: 5000
158
+ },
159
+ (res) => {
160
+ let data = '';
161
+ res.on('data', (chunk) => { data += chunk; });
162
+ res.on('end', () => {
163
+ try {
164
+ const json = JSON.parse(data);
165
+ // GitHub tags usually have a 'v' prefix (e.g., 'v2.4.0')
166
+ const version = json.tag_name ? json.tag_name.replace(/^v/, '') : null;
167
+ resolve(version);
168
+ } catch {
169
+ resolve(null);
170
+ }
171
+ });
172
+ }
173
+ );
174
+ req.on('error', () => resolve(null));
175
+ req.on('timeout', () => { req.destroy(); resolve(null); });
176
+ });
177
+ }
178
+
179
+ /**
180
+ * Check for a newer version and re-invoke with @latest if found.
181
+ * Uses TK_SKIP_UPDATE_CHECK env var as recursion guard.
182
+ * Returns true if a re-invoke happened (caller should exit), false otherwise.
183
+ */
184
+ async function autoUpdateCheck(originalArgs) {
185
+ // Recursion guard: if we're already a re-invoked process, skip
186
+ if (process.env.TK_SKIP_UPDATE_CHECK === '1') {
187
+ return false;
188
+ }
189
+
190
+ const latestVersion = await fetchLatestVersion();
191
+
192
+ if (!latestVersion) {
193
+ // Network fail — proceed silently with current version
194
+ return false;
195
+ }
196
+
197
+ if (compareSemver(latestVersion, CURRENT_VERSION) <= 0) {
198
+ // Already up to date
199
+ dim(`Version ${CURRENT_VERSION} is up to date.`);
200
+ return false;
201
+ }
202
+
203
+ // Newer version available — re-invoke
204
+ log('');
205
+ log(colorize('cyan', ` ⬆ New version available: ${colorize('bold', CURRENT_VERSION)} → ${colorize('bold', latestVersion)}`));
206
+ log(colorize('gray', ' Re-invoking with latest version...'));
207
+ log('');
208
+
209
+ try {
210
+ // Build the command pulling directly from GitHub
211
+ const args = originalArgs.join(' ');
212
+ const cmd = `npx -y github:Harmitx7/tribunal-kit#v${latestVersion} ${args}`;
213
+
214
+ execSync(cmd, {
215
+ stdio: 'inherit',
216
+ env: { ...process.env, TK_SKIP_UPDATE_CHECK: '1' },
217
+ });
218
+ return true; // Re-invoke succeeded, caller should exit
219
+ } catch (e) {
220
+ warn(`Auto-update failed: ${e.message}`);
221
+ warn('Continuing with current version...');
222
+ return false; // Fall through to current version
223
+ }
224
+ }
225
+
226
+ // ── Kit Source Location ───────────────────────────────────
227
+ function getKitAgent() {
228
+ // When installed via npm, the .agent/ folder is next to this script's package
229
+ const kitRoot = path.resolve(__dirname, '..');
230
+ const agentDir = path.join(kitRoot, '.agent');
231
+
232
+ if (!fs.existsSync(agentDir)) {
233
+ err(`Kit .agent/ folder not found at: ${agentDir}`);
234
+ err('The package may be corrupted. Try: npm install -g tribunal-kit');
235
+ process.exit(1);
236
+ }
237
+
238
+ return agentDir;
239
+ }
240
+
241
+ // ── Self-Install Guard ────────────────────────────────────
242
+ /**
243
+ * Returns true if the target directory IS the tribunal-kit package itself.
244
+ * This prevents `init --force` / `update` from deleting the package's own files
245
+ * when run from inside the project directory.
246
+ */
247
+ function isSelfInstall(targetDir) {
248
+ const kitRoot = path.resolve(__dirname, '..');
249
+ const resolvedTarget = path.resolve(targetDir);
250
+
251
+ // Direct path match
252
+ if (resolvedTarget === kitRoot) return true;
253
+
254
+ // Check if the target's package.json is this package
255
+ const targetPkg = path.join(resolvedTarget, 'package.json');
256
+ if (fs.existsSync(targetPkg)) {
257
+ try {
258
+ const targetName = JSON.parse(fs.readFileSync(targetPkg, 'utf8')).name;
259
+ if (targetName === PKG.name) return true;
260
+ } catch {
261
+ // Unreadable package.json — not a match
262
+ }
263
+ }
264
+
265
+ return false;
266
+ }
267
+
268
+ // ── Banner ────────────────────────────────────────────────
269
+ function banner() {
270
+ if (quiet) return;
271
+ // Big ASCII art (TRIBUNAL-KIT)
272
+ const art = String.raw`
273
+ ___________ ._____. .__ ____ __.__ __
274
+ \__ ___/______|__\_ |__ __ __ ____ _____ | | | |/ _|__|/ |_
275
+ | | \_ __ \ || __ \| | \/ \\__ \ | | ______ | < | \ __\
276
+ | | | | \/ || \_\ \ | / | \/ __ \| |__ /_____/ | | \| || |
277
+ |____| |__| |__||___ /____/|___| (____ /____/ |____|__ \__||__|
278
+ \/ \/ \/ \/ `.split('\n').filter(Boolean);
279
+ console.log();
280
+ for (const line of art) log(` ${c('cyan', bold(line))}`);
281
+ console.log();
282
+ // Subtitle strip
283
+ const W = 80;
284
+ const sub = 'Anti-Hallucination Agent System';
285
+ const sp = Math.max(0, W - sub.length);
286
+ const centred = ' '.repeat(Math.floor(sp / 2)) + sub + ' '.repeat(Math.ceil(sp / 2));
287
+ console.log(` ${c('cyan', '╔' + '═'.repeat(W) + '╗')}`);
288
+ console.log(` ${c('cyan', '║')}${c('gray', centred)}${c('cyan', '║')}`);
289
+ console.log(` ${c('cyan', '╚' + '═'.repeat(W) + '╝')}`);
290
+ console.log();
291
+ }
292
+
293
+ // ── Commands ──────────────────────────────────────────────
294
+ function cmdInit(flags) {
295
+ const agentSrc = getKitAgent();
296
+ const targetDir = flags.path ? path.resolve(flags.path) : process.cwd();
297
+ const agentDest = path.join(targetDir, '.agent');
298
+ const dryRun = flags.dryRun || false;
299
+
300
+ // ── Self-install guard ──────────────────────────────────
301
+ if (isSelfInstall(targetDir)) {
302
+ err('Cannot run init/update inside the tribunal-kit package itself.');
303
+ err(`Target: ${targetDir}`);
304
+ err(`Package: ${path.resolve(__dirname, '..')}`);
305
+ console.log();
306
+ dim('This command is designed to install .agent/ into OTHER projects.');
307
+ dim('Run it from the root of the project you want to set up:');
308
+ dim(' cd /path/to/your-project');
309
+ dim(' npx tribunal-kit init');
310
+ console.log();
311
+ process.exit(1);
312
+ }
313
+ // ────────────────────────────────────────────────────────
314
+
315
+ banner();
316
+
317
+ if (dryRun) {
318
+ log(colorize('yellow', ' DRY RUN — no files will be written'));
319
+ console.log();
320
+ }
321
+
322
+ // Check target exists
323
+ if (!fs.existsSync(targetDir)) {
324
+ err(`Target directory not found: ${targetDir}`);
325
+ process.exit(1);
326
+ }
327
+
328
+ // Check if .agent already exists
329
+ if (fs.existsSync(agentDest) && !flags.force) {
330
+ warn('.agent/ already exists in this project.');
331
+ log(` ${c('gray', '▸')} To refresh or update it, run: ${colorize('white', 'tribunal-kit init --force')}`);
332
+ log(` ${c('gray', '▸')} Or check status with: ${colorize('cyan', 'tribunal-kit status')}`);
333
+ console.log();
334
+ process.exit(0);
335
+ }
336
+
337
+ if (!dryRun && fs.existsSync(agentDest) && flags.force) {
338
+ const subdirs = ['agents', 'workflows', 'skills', 'scripts', '.shared'];
339
+ for (const sub of subdirs) {
340
+ const subPath = path.join(agentDest, sub);
341
+ if (fs.existsSync(subPath)) {
342
+ fs.rmSync(subPath, { recursive: true, force: true });
343
+ }
344
+ }
345
+ }
346
+
347
+ // Count what we're installing
348
+ const totalFiles = countDir(agentSrc);
349
+ log(` ${c('gray','▸')} Scanning ${c('white', String(totalFiles))} files ${c('gray','→')} ${c('gray', agentDest)}`);
350
+
351
+ try {
352
+ const copied = copyDir(agentSrc, agentDest, dryRun);
353
+
354
+ console.log();
355
+ if (dryRun) {
356
+ ok(`${bold('DRY RUN')} complete — would install ${c('cyan', String(copied))} files`);
357
+ dim(`Target: ${agentDest}`);
358
+ } else {
359
+ // ── Success card — W=62, rows padded by plain-text length ──
360
+ const W = 62;
361
+ const agentsCount = fs.readdirSync(path.join(agentDest, 'agents')).length;
362
+ const workflowsCount = fs.readdirSync(path.join(agentDest, 'workflows')).length;
363
+ const skillsCount = fs.readdirSync(path.join(agentDest, 'skills')).length;
364
+ const scriptsCount = fs.readdirSync(path.join(agentDest, 'scripts')).length;
365
+
366
+ // Stat rows: compute trailing spaces from plain text so right ║ aligns
367
+ const statRow = (icon, label, val, col) => {
368
+ // emoji JS .length===2 == terminal display width 2 ✓
369
+ const plain = ` ${icon} ${label.padEnd(10)}${String(val).padStart(3)} installed`;
370
+ const trail = ' '.repeat(Math.max(0, W - plain.length));
371
+ return ` ${c('cyan','║')} ${icon} ${c('white',label.padEnd(10))}${c(col,String(val).padStart(3))} ${c('gray','installed')}${trail}${c('cyan','║')}`;
372
+ };
373
+ // Plain-text rows (header / blank)
374
+ const plainRow = (text, wrapFn) => {
375
+ const trail = ' '.repeat(Math.max(0, W - text.length));
376
+ return ` ${c('cyan','║')}${wrapFn(text)}${trail}${c('cyan','║')}`;
377
+ };
378
+ // Next-step rows: fixed cmd column + description
379
+ const stepRow = (cmd, desc) => {
380
+ const plain = ` ${cmd.padEnd(16)}${desc}`;
381
+ const trail = ' '.repeat(Math.max(0, W - plain.length));
382
+ return ` ${c('cyan','║')} ${c('white',cmd.padEnd(16))}${c('gray',desc)}${trail}${c('cyan','║')}`;
383
+ };
384
+
385
+ console.log(` ${c('green','✔')} ${bold(c('green','Installation complete'))} ${c('gray','—')} ${c('white',String(copied))} files`);
386
+ console.log(` ${c('gray',' ╰─')} ${c('gray', agentDest)}`);
387
+ console.log();
388
+ console.log(` ${c('cyan', '╔' + '═'.repeat(W) + '╗')}`);
389
+ console.log(plainRow(` What's inside:`, s => c('bold', c('white', s))));
390
+ console.log(` ${c('cyan', '╠' + '═'.repeat(W) + '╣')}`);
391
+ console.log(statRow('🤖', 'Agents', agentsCount, 'magenta'));
392
+ console.log(statRow('⚡', 'Workflows', workflowsCount, 'yellow'));
393
+ console.log(statRow('🧠', 'Skills', skillsCount, 'blue'));
394
+ console.log(statRow('🔧', 'Scripts', scriptsCount, 'green'));
395
+ console.log(` ${c('cyan', '╠' + '═'.repeat(W) + '╣')}`);
396
+ console.log(plainRow('', () => ''));
397
+ console.log(plainRow(` Next steps:`, s => c('gray', s)));
398
+ console.log(stepRow('/generate', 'Generate code with anti-hallucination'));
399
+ console.log(stepRow('/review', 'Audit existing code for issues'));
400
+ console.log(stepRow('/tribunal-full', 'Run all 11 reviewers in parallel'));
401
+ console.log(plainRow('', () => ''));
402
+ console.log(` ${c('cyan', '╚' + '═'.repeat(W) + '╝')}`);
403
+ console.log();
404
+ log(` ${c('gray', '✦ Your AI IDE will pick up changes automatically.')}`);
405
+ }
406
+
407
+ console.log();
408
+ } catch (e) {
409
+ err(`Failed to install: ${e.message}`);
410
+ process.exit(1);
411
+ }
412
+ }
413
+
414
+ function cmdUpdate(flags) {
415
+ // ── Self-install guard (early, before banner) ───────────
416
+ const targetDir = flags.path ? path.resolve(flags.path) : process.cwd();
417
+ if (isSelfInstall(targetDir)) {
418
+ err('Cannot run update inside the tribunal-kit package itself.');
419
+ err(`Target: ${targetDir}`);
420
+ console.log();
421
+ dim('This command is designed to update .agent/ in OTHER projects.');
422
+ dim('Run it from the root of the project you want to update:');
423
+ dim(' cd /path/to/your-project');
424
+ dim(' npx tribunal-kit update');
425
+ console.log();
426
+ process.exit(1);
427
+ }
428
+ // ────────────────────────────────────────────────────────
429
+
430
+ // Update = init with --force
431
+ flags.force = true;
432
+ if (!quiet) {
433
+ log(` ${c('cyan','↻')} ${bold('Updating')} ${c('white','.agent/')} to latest version...`);
434
+ console.log();
435
+ }
436
+ cmdInit(flags);
437
+ }
438
+
439
+ // ── Async Main Wrapper ───────────────────────────────────
440
+ async function runWithUpdateCheck(command, flags) {
441
+ const shouldSkip = flags.skipUpdateCheck || process.env.TK_SKIP_UPDATE_CHECK === '1';
442
+
443
+ if (!shouldSkip && (command === 'init' || command === 'update')) {
444
+ // Pass through the original args (minus the node/script path)
445
+ const originalArgs = process.argv.slice(2);
446
+ const didReInvoke = await autoUpdateCheck(originalArgs);
447
+ if (didReInvoke) {
448
+ process.exit(0); // Latest version handled it
449
+ }
450
+ }
451
+
452
+ // Proceed with current version
453
+ switch (command) {
454
+ case 'init':
455
+ cmdInit(flags);
456
+ break;
457
+ case 'update':
458
+ cmdUpdate(flags);
459
+ break;
460
+ case 'status':
461
+ cmdStatus(flags);
462
+ break;
463
+ case 'help':
464
+ case '--help':
465
+ case '-h':
466
+ case null:
467
+ cmdHelp();
468
+ break;
469
+ default:
470
+ err(`Unknown command: "${command}"`);
471
+ console.log();
472
+ dim('Run tribunal-kit --help for usage');
473
+ process.exit(1);
474
+ }
475
+ }
476
+
477
+ function cmdStatus(flags) {
478
+ const targetDir = flags.path ? path.resolve(flags.path) : process.cwd();
479
+ const agentDest = path.join(targetDir, '.agent');
480
+
481
+ banner();
482
+
483
+ if (!fs.existsSync(agentDest)) {
484
+ log(` ${c('red','✖')} ${bold('Not installed')} in this project`);
485
+ console.log();
486
+ log(` ${c('gray','Run:')} ${c('cyan','npx tribunal-kit init')}`);
487
+ console.log();
488
+ return;
489
+ }
490
+
491
+ log(` ${c('green','✔')} ${bold(c('green','Installed'))} ${c('gray','→')} ${c('gray', agentDest)}`);
492
+ console.log();
493
+
494
+ const icons = { agents: '🤖', workflows: '⚡', skills: '🧠', scripts: '🔧' };
495
+ const colors = { agents: 'magenta', workflows: 'yellow', skills: 'blue', scripts: 'green' };
496
+ const subdirs = ['agents', 'workflows', 'skills', 'scripts'];
497
+ for (const sub of subdirs) {
498
+ const subPath = path.join(agentDest, sub);
499
+ if (fs.existsSync(subPath)) {
500
+ const count = fs.readdirSync(subPath).filter(f => !fs.statSync(path.join(subPath, f)).isDirectory()).length;
501
+ log(` ${icons[sub]} ${c(colors[sub], sub.padEnd(12))}${c('white', String(count).padStart(3))} files`);
502
+ }
503
+ }
504
+ console.log();
505
+ }
506
+
507
+ function cmdHelp() {
508
+ banner();
509
+ const cmd = (name, desc) => ` ${c('cyan', name.padEnd(10))} ${c('gray', desc)}`;
510
+ const opt = (flag, desc) => ` ${c('yellow', flag.padEnd(22))} ${c('gray', desc)}`;
511
+ const ex = (s) => ` ${c('gray', '▸')} ${c('white', s)}`;
512
+
513
+ log(bold(' Commands'));
514
+ log(` ${c('gray','─'.repeat(40))}`);
515
+ log(cmd('init', 'Install .agent/ into current project'));
516
+ log(cmd('update', 'Re-install to get latest version'));
517
+ log(cmd('status', 'Check if .agent/ is installed'));
518
+ console.log();
519
+ log(bold(' Options'));
520
+ log(` ${c('gray','─'.repeat(40))}`);
521
+ log(opt('--force', 'Overwrite existing .agent/ folder'));
522
+ log(opt('--path <dir>', 'Install in specific directory'));
523
+ log(opt('--quiet', 'Suppress all output'));
524
+ log(opt('--dry-run', 'Preview actions without executing'));
525
+ log(opt('--skip-update-check', 'Skip auto-update version check'));
526
+ console.log();
527
+ log(bold(' Examples'));
528
+ log(` ${c('gray','─'.repeat(40))}`);
529
+ log(ex('npx tribunal-kit init'));
530
+ log(ex('npx tribunal-kit init --force'));
531
+ log(ex('npx tribunal-kit init --path ./my-app'));
532
+ log(ex('npx tribunal-kit init --dry-run'));
533
+ log(ex('npx tribunal-kit update'));
534
+ log(ex('npx tribunal-kit status'));
535
+ console.log();
536
+ }
537
+
538
+ // ── Main ──────────────────────────────────────────────────
539
+ const { command, flags } = parseArgs(process.argv);
540
+
541
+ if (flags.quiet) quiet = true;
542
+
543
+ runWithUpdateCheck(command, flags);
544
+
545
+ // -- Exports (for testing) -- do not remove
546
+ if (require.main !== module) {
547
+ module.exports = { parseArgs, compareSemver, copyDir, countDir, isSelfInstall };
548
+ }