tribunal-kit 4.6.1 → 5.8.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 (72) hide show
  1. package/.agent/ARCHITECTURE.md +6 -7
  2. package/.agent/agents/frontend-reviewer.md +13 -0
  3. package/.agent/agents/frontend-specialist.md +14 -0
  4. package/.agent/agents/logic-reviewer.md +11 -0
  5. package/.agent/agents/orchestrator.md +15 -0
  6. package/.agent/agents/security-auditor.md +13 -0
  7. package/.agent/agents/ui-ux-auditor.md +7 -31
  8. package/.agent/history/memory/.memory.idx +766 -0
  9. package/.agent/history/memory/MEMORY.md +62 -0
  10. package/.agent/routing_index.json +694 -714
  11. package/.agent/rules/GEMINI.md +58 -8
  12. package/.agent/scripts/_colors.js +131 -89
  13. package/.agent/scripts/_utils.js +163 -128
  14. package/.agent/scripts/auto_preview.js +207 -197
  15. package/.agent/scripts/bundle_analyzer.js +227 -192
  16. package/.agent/scripts/case_law_manager.js +991 -689
  17. package/.agent/scripts/checklist.js +233 -190
  18. package/.agent/scripts/context_broker.js +930 -605
  19. package/.agent/scripts/dependency_analyzer.js +275 -184
  20. package/.agent/scripts/graph_builder.js +412 -341
  21. package/.agent/scripts/graph_visualizer.js +392 -390
  22. package/.agent/scripts/graph_zoom.js +198 -156
  23. package/.agent/scripts/inner_loop_validator.js +523 -445
  24. package/.agent/scripts/lint_runner.js +199 -157
  25. package/.agent/scripts/marathon_harness.js +819 -661
  26. package/.agent/scripts/minify_context.js +115 -100
  27. package/.agent/scripts/mutation_runner.js +321 -280
  28. package/.agent/scripts/prompt_compiler.js +62 -42
  29. package/.agent/scripts/schema_validator.js +373 -280
  30. package/.agent/scripts/security_scan.js +333 -190
  31. package/.agent/scripts/session_manager.js +306 -270
  32. package/.agent/scripts/skill_evolution.js +810 -637
  33. package/.agent/scripts/skill_integrator.js +327 -307
  34. package/.agent/scripts/strengthen_skills.js +203 -193
  35. package/.agent/scripts/swarm_dispatcher.js +558 -457
  36. package/.agent/scripts/test_runner.js +178 -152
  37. package/.agent/scripts/verify_all.js +200 -168
  38. package/.agent/skills/fabel-protocol/SKILL.md +235 -0
  39. package/.agent/skills/thinking-protocol/SKILL.md +27 -0
  40. package/.agent/workflows/generate.md +1 -1
  41. package/.agent/workflows/tribunal-speed.md +1 -1
  42. package/README.md +61 -47
  43. package/bin/mcp-server.js +476 -121
  44. package/bin/tribunal-kit.js +1245 -987
  45. package/bin/wrapper.js +104 -73
  46. package/dist/cli.js +265 -0
  47. package/dist/commands/case.js +71 -0
  48. package/dist/commands/compile.js +84 -0
  49. package/dist/commands/context.js +66 -0
  50. package/dist/commands/graph.js +38 -0
  51. package/dist/commands/hook.js +28 -0
  52. package/dist/commands/init.js +339 -0
  53. package/dist/commands/learn.js +117 -0
  54. package/dist/commands/marathon.js +45 -0
  55. package/dist/commands/memory.js +456 -0
  56. package/dist/commands/mutate.js +30 -0
  57. package/dist/commands/status.js +35 -0
  58. package/dist/commands/sync.js +25 -0
  59. package/dist/commands/uninstall.js +42 -0
  60. package/dist/commands/update.js +37 -0
  61. package/dist/mcp/server.js +142 -0
  62. package/dist/types.js +8 -0
  63. package/dist/utils/fs.js +96 -0
  64. package/dist/utils/hasher.js +142 -0
  65. package/dist/utils/helpers.js +68 -0
  66. package/dist/utils/logger.js +54 -0
  67. package/dist/utils/version.js +150 -0
  68. package/package.json +3 -2
  69. package/scripts/benchmark.js +197 -0
  70. package/scripts/changelog.js +196 -168
  71. package/scripts/sync-version.js +94 -81
  72. package/scripts/validate-payload.js +85 -78
@@ -0,0 +1,197 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Tribunal-Kit Performance Benchmark
4
+ *
5
+ * Measures and reports performance metrics for key operations.
6
+ * Run: node scripts/benchmark.js
7
+ */
8
+
9
+ const { spawnSync } = require("child_process");
10
+ const path = require("path");
11
+ const fs = require("fs");
12
+ const os = require("os");
13
+
14
+ // ANSI colors
15
+ const C = {
16
+ reset: "\x1b[0m",
17
+ bold: "\x1b[1m",
18
+ dim: "\x1b[2m",
19
+ red: "\x1b[91m",
20
+ green: "\x1b[92m",
21
+ yellow: "\x1b[93m",
22
+ cyan: "\x1b[96m",
23
+ white: "\x1b[97m",
24
+ gray: "\x1b[90m",
25
+ };
26
+
27
+ function c(color, text) {
28
+ return `${C[color]}${text}${C.reset}`;
29
+ }
30
+ function bold(text) {
31
+ return `${C.bold}${text}${C.reset}`;
32
+ }
33
+
34
+ /**
35
+ * Time a command execution in milliseconds.
36
+ * @param {string} label - Description of the benchmark
37
+ * @param {function} fn - Function to benchmark
38
+ * @param {number} [runs=3] - Number of runs for averaging
39
+ * @returns {{ label: string, avg: number, min: number, max: number, runs: number }}
40
+ */
41
+ async function benchmark(label, fn, runs = 3) {
42
+ const times = [];
43
+ for (let i = 0; i < runs; i++) {
44
+ const start = performance.now();
45
+ await fn();
46
+ const end = performance.now();
47
+ times.push(end - start);
48
+ }
49
+ const avg = times.reduce((a, b) => a + b, 0) / times.length;
50
+ const min = Math.min(...times);
51
+ const max = Math.max(...times);
52
+ return { label, avg, min, max, runs };
53
+ }
54
+
55
+ /**
56
+ * Time a shell command.
57
+ */
58
+ function benchmarkCommand(label, command, runs = 3) {
59
+ return benchmark(
60
+ label,
61
+ () => {
62
+ spawnSync("node", command.split(" "), {
63
+ stdio: "pipe",
64
+ encoding: "utf8",
65
+ env: { ...process.env, TK_SKIP_UPDATE_CHECK: "1" },
66
+ });
67
+ },
68
+ runs,
69
+ );
70
+ }
71
+
72
+ async function main() {
73
+ console.log();
74
+ console.log(bold(` ⚡ Tribunal-Kit Performance Benchmark`));
75
+ console.log(c("gray", ` ─────────────────────────────────────────`));
76
+ console.log(c("gray", ` Platform: ${os.platform()} ${os.arch()}`));
77
+ console.log(c("gray", ` Node: ${process.version}`));
78
+ console.log(
79
+ c(
80
+ "gray",
81
+ ` CPUs: ${os.cpus().length}x ${os.cpus()[0]?.model || "unknown"}`,
82
+ ),
83
+ );
84
+ console.log(c("gray", ` ─────────────────────────────────────────`));
85
+ console.log();
86
+
87
+ const cliPath = path.resolve(__dirname, "../bin/wrapper.js");
88
+ const tempDir = path.join(os.tmpdir(), `tribunal-bench-${Date.now()}`);
89
+ fs.mkdirSync(tempDir, { recursive: true });
90
+
91
+ const results = [];
92
+
93
+ // 1. Cold start (help)
94
+ console.log(c("cyan", " ▸ Benchmarking: CLI cold-start (--help)"));
95
+ const helpResult = await benchmarkCommand(
96
+ "CLI cold-start (--help)",
97
+ `${cliPath} --help`,
98
+ 5,
99
+ );
100
+ results.push(helpResult);
101
+
102
+ // 2. Status check
103
+ console.log(c("cyan", " ▸ Benchmarking: tk status"));
104
+ const statusResult = await benchmarkCommand(
105
+ "Status check",
106
+ `${cliPath} status --quiet`,
107
+ 5,
108
+ );
109
+ results.push(statusResult);
110
+
111
+ // 3. Init (dry-run)
112
+ console.log(c("cyan", " ▸ Benchmarking: tk init --dry-run"));
113
+ const initResult = await benchmarkCommand(
114
+ "Init (dry-run)",
115
+ `${cliPath} init --dry-run --quiet --skip-update-check --path=${tempDir}`,
116
+ 3,
117
+ );
118
+ results.push(initResult);
119
+
120
+ // 4. Init (real, to temp dir)
121
+ console.log(c("cyan", " ▸ Benchmarking: tk init (real copy)"));
122
+ const initRealResult = await benchmark(
123
+ "Init (full copy)",
124
+ () => {
125
+ const runDir = path.join(tempDir, `run-${Date.now()}`);
126
+ fs.mkdirSync(runDir, { recursive: true });
127
+ spawnSync(
128
+ "node",
129
+ [cliPath, "init", "--quiet", "--skip-update-check", `--path=${runDir}`],
130
+ {
131
+ stdio: "pipe",
132
+ encoding: "utf8",
133
+ env: { ...process.env, TK_SKIP_UPDATE_CHECK: "1" },
134
+ },
135
+ );
136
+ // Cleanup
137
+ try {
138
+ fs.rmSync(runDir, { recursive: true, force: true });
139
+ } catch {}
140
+ },
141
+ 3,
142
+ );
143
+ results.push(initRealResult);
144
+
145
+ // Print results table
146
+ console.log();
147
+ console.log(bold(` Results`));
148
+ console.log(
149
+ c("gray", ` ─────────────────────────────────────────────────────────`),
150
+ );
151
+ console.log(
152
+ ` ${c("white", "Operation".padEnd(30))} ${c("white", "Avg (ms)".padStart(10))} ${c("white", "Min".padStart(8))} ${c("white", "Max".padStart(8))}`,
153
+ );
154
+ console.log(
155
+ c("gray", ` ─────────────────────────────────────────────────────────`),
156
+ );
157
+
158
+ for (const r of results) {
159
+ const avgColor = r.avg < 100 ? "green" : r.avg < 500 ? "yellow" : "red";
160
+ console.log(
161
+ ` ${c("white", r.label.padEnd(30))} ${c(avgColor, String(Math.round(r.avg)).padStart(10))} ${c("gray", String(Math.round(r.min)).padStart(8))} ${c("gray", String(Math.round(r.max)).padStart(8))}`,
162
+ );
163
+ }
164
+
165
+ console.log(
166
+ c("gray", ` ─────────────────────────────────────────────────────────`),
167
+ );
168
+ console.log();
169
+
170
+ // Write results to JSON for CI/comparison
171
+ const outputPath = path.resolve(__dirname, "../benchmark-results.json");
172
+ const outputData = {
173
+ timestamp: new Date().toISOString(),
174
+ platform: `${os.platform()}-${os.arch()}`,
175
+ node: process.version,
176
+ results: results.map((r) => ({
177
+ label: r.label,
178
+ avg_ms: Math.round(r.avg),
179
+ min_ms: Math.round(r.min),
180
+ max_ms: Math.round(r.max),
181
+ runs: r.runs,
182
+ })),
183
+ };
184
+ fs.writeFileSync(outputPath, JSON.stringify(outputData, null, 2));
185
+ console.log(c("green", ` ✔ Results saved to benchmark-results.json`));
186
+
187
+ // Cleanup temp
188
+ try {
189
+ fs.rmSync(tempDir, { recursive: true, force: true });
190
+ } catch {}
191
+ console.log();
192
+ }
193
+
194
+ main().catch((err) => {
195
+ console.error(`Benchmark failed: ${err.message}`);
196
+ process.exit(1);
197
+ });
@@ -1,168 +1,196 @@
1
- #!/usr/bin/env node
2
- /**
3
- * changelog.js — Auto-generate CHANGELOG from git history
4
- *
5
- * Categorizes commits by conventional commit type:
6
- * feat: → ✨ Features
7
- * fix: → 🐛 Bug Fixes
8
- * perf: → ⚡ Performance
9
- * docs: → 📝 Documentation
10
- * test: → ✅ Tests
11
- * refactor: → ♻️ Refactors
12
- * chore: → 🔧 Chores
13
- * BREAKING: → 💥 Breaking Changes
14
- *
15
- * Usage:
16
- * node scripts/changelog.js → Generate full changelog
17
- * node scripts/changelog.js --preview → Preview unreleased changes
18
- * node scripts/changelog.js --since v4.1.0 → Changes since a specific tag
19
- */
20
-
21
- 'use strict';
22
-
23
- const { execSync } = require('child_process');
24
- const fs = require('fs');
25
- const path = require('path');
26
-
27
- const PKG = require(path.resolve(__dirname, '..', 'package.json'));
28
- const CHANGELOG_PATH = path.resolve(__dirname, '..', 'CHANGELOG.md');
29
-
30
- // ── Commit Categories ────────────────────────────────────
31
- const CATEGORIES = {
32
- feat: { emoji: '', title: 'Features' },
33
- fix: { emoji: '🐛', title: 'Bug Fixes' },
34
- perf: { emoji: '', title: 'Performance' },
35
- docs: { emoji: '📝', title: 'Documentation' },
36
- test: { emoji: '', title: 'Tests' },
37
- refactor: { emoji: '♻️', title: 'Refactors' },
38
- chore: { emoji: '🔧', title: 'Chores' },
39
- ci: { emoji: '🏗️', title: 'CI/CD' },
40
- style: { emoji: '🎨', title: 'Style' },
41
- breaking: { emoji: '💥', title: 'Breaking Changes' },
42
- };
43
-
44
- // ── Git Helpers ──────────────────────────────────────────
45
- function git(cmd) {
46
- try {
47
- return execSync(`git ${cmd}`, { encoding: 'utf8', timeout: 10000 }).trim();
48
- } catch {
49
- return '';
50
- }
51
- }
52
-
53
- function getLatestTag() {
54
- return git('describe --tags --abbrev=0 2>nul') || git('describe --tags --abbrev=0 2>/dev/null') || '';
55
- }
56
-
57
- function getCommits(since) {
58
- const range = since ? `${since}..HEAD` : 'HEAD';
59
- const MAX_COMMITS = 500;
60
- const format = '--format=%H||%s||%an||%ai';
61
- const raw = git(`log ${range} ${format} --no-merges -n ${MAX_COMMITS}`);
62
- if (!raw) return [];
63
-
64
- return raw.split('\n').filter(Boolean).map(line => {
65
- const [hash, subject, author, date] = line.split('||');
66
- return { hash: hash?.replace(/^"/, '').slice(0, 7), subject, author, date: date?.replace(/"$/, '').slice(0, 10) };
67
- });
68
- }
69
-
70
- function categorize(subject) {
71
- const lower = subject.toLowerCase();
72
-
73
- // Check for BREAKING CHANGE
74
- if (lower.includes('breaking') || lower.includes('!:')) {
75
- return 'breaking';
76
- }
77
-
78
- // Match conventional commit prefix
79
- const match = subject.match(/^(\w+)(?:\(.+?\))?:\s*/);
80
- if (match) {
81
- const type = match[1].toLowerCase();
82
- if (CATEGORIES[type]) return type;
83
- }
84
-
85
- // Heuristic fallback
86
- if (lower.includes('fix') || lower.includes('bug')) return 'fix';
87
- if (lower.includes('add') || lower.includes('new') || lower.includes('feat')) return 'feat';
88
- if (lower.includes('doc') || lower.includes('readme')) return 'docs';
89
- if (lower.includes('test')) return 'test';
90
- if (lower.includes('refactor') || lower.includes('clean')) return 'refactor';
91
- if (lower.includes('perf') || lower.includes('optim')) return 'perf';
92
- if (lower.includes('ci') || lower.includes('workflow')) return 'ci';
93
-
94
- return 'chore';
95
- }
96
-
97
- // ── Changelog Generation ─────────────────────────────────
98
- function generateChangelog(commits, version, date) {
99
- const grouped = {};
100
- for (const commit of commits) {
101
- const cat = categorize(commit.subject);
102
- if (!grouped[cat]) grouped[cat] = [];
103
- // Strip conventional prefix for cleaner display
104
- const clean = commit.subject.replace(/^\w+(\(.+?\))?:\s*/, '');
105
- grouped[cat].push({ ...commit, clean });
106
- }
107
-
108
- let md = `## [${version}] — ${date}\n\n`;
109
-
110
- // Breaking changes first
111
- const order = ['breaking', 'feat', 'fix', 'perf', 'refactor', 'docs', 'test', 'ci', 'style', 'chore'];
112
- for (const cat of order) {
113
- if (!grouped[cat] || grouped[cat].length === 0) continue;
114
- const { emoji, title } = CATEGORIES[cat];
115
- md += `### ${emoji} ${title}\n\n`;
116
- for (const c of grouped[cat]) {
117
- md += `- ${c.clean} (\`${c.hash}\`)\n`;
118
- }
119
- md += '\n';
120
- }
121
-
122
- return md;
123
- }
124
-
125
- // ── Main ─────────────────────────────────────────────────
126
- function main() {
127
- const args = process.argv.slice(2);
128
- const isPreview = args.includes('--preview');
129
- const sinceIdx = args.indexOf('--since');
130
- const sinceTag = sinceIdx !== -1 ? args[sinceIdx + 1] : null;
131
-
132
- const since = sinceTag || getLatestTag();
133
- const commits = getCommits(since);
134
-
135
- if (commits.length === 0) {
136
- console.log(' ℹ️ No new commits found since', since || 'beginning');
137
- process.exit(0);
138
- }
139
-
140
- const today = new Date().toISOString().slice(0, 10);
141
- const version = isPreview ? 'Unreleased' : PKG.version;
142
-
143
- const changelog = generateChangelog(commits, version, today);
144
-
145
- if (isPreview) {
146
- console.log('\n 📋 Changelog Preview\n ' + '─'.repeat(40) + '\n');
147
- console.log(changelog);
148
- console.log(` 📊 ${commits.length} commits since ${since || 'initial commit'}`);
149
- return;
150
- }
151
-
152
- // Write or prepend to CHANGELOG.md
153
- const header = `# Changelog\n\nAll notable changes to Tribunal Kit are documented here.\nFormat follows [Keep a Changelog](https://keepachangelog.com/).\n\n`;
154
-
155
- let existing = '';
156
- if (fs.existsSync(CHANGELOG_PATH)) {
157
- existing = fs.readFileSync(CHANGELOG_PATH, 'utf8');
158
- // Remove existing header
159
- existing = existing.replace(/^# Changelog[\s\S]*?(?=## )/, '');
160
- }
161
-
162
- const full = header + changelog + existing;
163
- fs.writeFileSync(CHANGELOG_PATH, full, 'utf8');
164
-
165
- console.log(` CHANGELOG.md updated v${version} (${commits.length} commits)`);
166
- }
167
-
168
- main();
1
+ #!/usr/bin/env node
2
+ /**
3
+ * changelog.js — Auto-generate CHANGELOG from git history
4
+ *
5
+ * Categorizes commits by conventional commit type:
6
+ * feat: → ✨ Features
7
+ * fix: → 🐛 Bug Fixes
8
+ * perf: → ⚡ Performance
9
+ * docs: → 📝 Documentation
10
+ * test: → ✅ Tests
11
+ * refactor: → ♻️ Refactors
12
+ * chore: → 🔧 Chores
13
+ * BREAKING: → 💥 Breaking Changes
14
+ *
15
+ * Usage:
16
+ * node scripts/changelog.js → Generate full changelog
17
+ * node scripts/changelog.js --preview → Preview unreleased changes
18
+ * node scripts/changelog.js --since v4.1.0 → Changes since a specific tag
19
+ */
20
+
21
+ "use strict";
22
+
23
+ const { execSync } = require("child_process");
24
+ const fs = require("fs");
25
+ const path = require("path");
26
+
27
+ const PKG = require(path.resolve(__dirname, "..", "package.json"));
28
+ const CHANGELOG_PATH = path.resolve(__dirname, "..", "CHANGELOG.md");
29
+
30
+ // ── Commit Categories ────────────────────────────────────
31
+ const CATEGORIES = {
32
+ feat: { emoji: "", title: "Features" },
33
+ fix: { emoji: "🐛", title: "Bug Fixes" },
34
+ perf: { emoji: "", title: "Performance" },
35
+ docs: { emoji: "📝", title: "Documentation" },
36
+ test: { emoji: "", title: "Tests" },
37
+ refactor: { emoji: "♻️", title: "Refactors" },
38
+ chore: { emoji: "🔧", title: "Chores" },
39
+ ci: { emoji: "🏗️", title: "CI/CD" },
40
+ style: { emoji: "🎨", title: "Style" },
41
+ breaking: { emoji: "💥", title: "Breaking Changes" },
42
+ };
43
+
44
+ // ── Git Helpers ──────────────────────────────────────────
45
+ function git(cmd) {
46
+ try {
47
+ return execSync(`git ${cmd}`, { encoding: "utf8", timeout: 10000 }).trim();
48
+ } catch {
49
+ return "";
50
+ }
51
+ }
52
+
53
+ function getLatestTag() {
54
+ return (
55
+ git("describe --tags --abbrev=0 2>nul") ||
56
+ git("describe --tags --abbrev=0 2>/dev/null") ||
57
+ ""
58
+ );
59
+ }
60
+
61
+ function getCommits(since) {
62
+ const range = since ? `${since}..HEAD` : "HEAD";
63
+ const MAX_COMMITS = 500;
64
+ const format = "--format=%H||%s||%an||%ai";
65
+ const raw = git(`log ${range} ${format} --no-merges -n ${MAX_COMMITS}`);
66
+ if (!raw) return [];
67
+
68
+ return raw
69
+ .split("\n")
70
+ .filter(Boolean)
71
+ .map((line) => {
72
+ const [hash, subject, author, date] = line.split("||");
73
+ return {
74
+ hash: hash?.replace(/^"/, "").slice(0, 7),
75
+ subject,
76
+ author,
77
+ date: date?.replace(/"$/, "").slice(0, 10),
78
+ };
79
+ });
80
+ }
81
+
82
+ function categorize(subject) {
83
+ const lower = subject.toLowerCase();
84
+
85
+ // Check for BREAKING CHANGE
86
+ if (lower.includes("breaking") || lower.includes("!:")) {
87
+ return "breaking";
88
+ }
89
+
90
+ // Match conventional commit prefix
91
+ const match = subject.match(/^(\w+)(?:\(.+?\))?:\s*/);
92
+ if (match) {
93
+ const type = match[1].toLowerCase();
94
+ if (CATEGORIES[type]) return type;
95
+ }
96
+
97
+ // Heuristic fallback
98
+ if (lower.includes("fix") || lower.includes("bug")) return "fix";
99
+ if (lower.includes("add") || lower.includes("new") || lower.includes("feat"))
100
+ return "feat";
101
+ if (lower.includes("doc") || lower.includes("readme")) return "docs";
102
+ if (lower.includes("test")) return "test";
103
+ if (lower.includes("refactor") || lower.includes("clean")) return "refactor";
104
+ if (lower.includes("perf") || lower.includes("optim")) return "perf";
105
+ if (lower.includes("ci") || lower.includes("workflow")) return "ci";
106
+
107
+ return "chore";
108
+ }
109
+
110
+ // ── Changelog Generation ─────────────────────────────────
111
+ function generateChangelog(commits, version, date) {
112
+ const grouped = {};
113
+ for (const commit of commits) {
114
+ const cat = categorize(commit.subject);
115
+ if (!grouped[cat]) grouped[cat] = [];
116
+ // Strip conventional prefix for cleaner display
117
+ const clean = commit.subject.replace(/^\w+(\(.+?\))?:\s*/, "");
118
+ grouped[cat].push({ ...commit, clean });
119
+ }
120
+
121
+ let md = `## [${version}] — ${date}\n\n`;
122
+
123
+ // Breaking changes first
124
+ const order = [
125
+ "breaking",
126
+ "feat",
127
+ "fix",
128
+ "perf",
129
+ "refactor",
130
+ "docs",
131
+ "test",
132
+ "ci",
133
+ "style",
134
+ "chore",
135
+ ];
136
+ for (const cat of order) {
137
+ if (!grouped[cat] || grouped[cat].length === 0) continue;
138
+ const { emoji, title } = CATEGORIES[cat];
139
+ md += `### ${emoji} ${title}\n\n`;
140
+ for (const c of grouped[cat]) {
141
+ md += `- ${c.clean} (\`${c.hash}\`)\n`;
142
+ }
143
+ md += "\n";
144
+ }
145
+
146
+ return md;
147
+ }
148
+
149
+ // ── Main ─────────────────────────────────────────────────
150
+ function main() {
151
+ const args = process.argv.slice(2);
152
+ const isPreview = args.includes("--preview");
153
+ const sinceIdx = args.indexOf("--since");
154
+ const sinceTag = sinceIdx !== -1 ? args[sinceIdx + 1] : null;
155
+
156
+ const since = sinceTag || getLatestTag();
157
+ const commits = getCommits(since);
158
+
159
+ if (commits.length === 0) {
160
+ console.log(" ℹ️ No new commits found since", since || "beginning");
161
+ process.exit(0);
162
+ }
163
+
164
+ const today = new Date().toISOString().slice(0, 10);
165
+ const version = isPreview ? "Unreleased" : PKG.version;
166
+
167
+ const changelog = generateChangelog(commits, version, today);
168
+
169
+ if (isPreview) {
170
+ console.log("\n 📋 Changelog Preview\n " + "─".repeat(40) + "\n");
171
+ console.log(changelog);
172
+ console.log(
173
+ ` 📊 ${commits.length} commits since ${since || "initial commit"}`,
174
+ );
175
+ return;
176
+ }
177
+
178
+ // Write or prepend to CHANGELOG.md
179
+ const header = `# Changelog\n\nAll notable changes to Tribunal Kit are documented here.\nFormat follows [Keep a Changelog](https://keepachangelog.com/).\n\n`;
180
+
181
+ let existing = "";
182
+ if (fs.existsSync(CHANGELOG_PATH)) {
183
+ existing = fs.readFileSync(CHANGELOG_PATH, "utf8");
184
+ // Remove existing header
185
+ existing = existing.replace(/^# Changelog[\s\S]*?(?=## )/, "");
186
+ }
187
+
188
+ const full = header + changelog + existing;
189
+ fs.writeFileSync(CHANGELOG_PATH, full, "utf8");
190
+
191
+ console.log(
192
+ ` ✔ CHANGELOG.md updated — v${version} (${commits.length} commits)`,
193
+ );
194
+ }
195
+
196
+ main();