@rigour-labs/cli 2.22.0 → 3.0.1

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.
@@ -28,6 +28,38 @@ export async function explainCommand(cwd) {
28
28
  console.log(chalk.bold('Status: ') + (report.status === 'PASS'
29
29
  ? chalk.green.bold('āœ… PASS')
30
30
  : chalk.red.bold('šŸ›‘ FAIL')));
31
+ // Score summary
32
+ if (report.stats) {
33
+ const stats = report.stats;
34
+ const scoreParts = [];
35
+ if (stats.score !== undefined)
36
+ scoreParts.push(`Score: ${stats.score}/100`);
37
+ if (stats.ai_health_score !== undefined)
38
+ scoreParts.push(`AI Health: ${stats.ai_health_score}/100`);
39
+ if (stats.structural_score !== undefined)
40
+ scoreParts.push(`Structural: ${stats.structural_score}/100`);
41
+ if (scoreParts.length > 0) {
42
+ console.log(chalk.bold('\n' + scoreParts.join(' | ')));
43
+ }
44
+ // Severity breakdown
45
+ if (stats.severity_breakdown) {
46
+ const sevParts = Object.entries(stats.severity_breakdown)
47
+ .filter(([, count]) => count > 0)
48
+ .map(([sev, count]) => `${sev}: ${count}`);
49
+ if (sevParts.length > 0) {
50
+ console.log(chalk.dim('Severity: ' + sevParts.join(', ')));
51
+ }
52
+ }
53
+ // Provenance breakdown
54
+ if (stats.provenance_breakdown) {
55
+ const provParts = Object.entries(stats.provenance_breakdown)
56
+ .filter(([, count]) => count > 0)
57
+ .map(([prov, count]) => `${prov}: ${count}`);
58
+ if (provParts.length > 0) {
59
+ console.log(chalk.dim('Categories: ' + provParts.join(', ')));
60
+ }
61
+ }
62
+ }
31
63
  console.log(chalk.bold('\nGate Summary:'));
32
64
  for (const [gate, status] of Object.entries(report.summary || {})) {
33
65
  const icon = status === 'PASS' ? 'āœ…' : status === 'FAIL' ? 'āŒ' : 'ā­ļø';
@@ -35,8 +67,21 @@ export async function explainCommand(cwd) {
35
67
  }
36
68
  if (report.failures && report.failures.length > 0) {
37
69
  console.log(chalk.bold.red(`\nšŸ”§ ${report.failures.length} Violation(s) to Fix:\n`));
70
+ // Severity color helper
71
+ const sevColor = (s) => {
72
+ switch (s) {
73
+ case 'critical': return chalk.red.bold;
74
+ case 'high': return chalk.red;
75
+ case 'medium': return chalk.yellow;
76
+ case 'low': return chalk.dim;
77
+ default: return chalk.dim;
78
+ }
79
+ };
38
80
  report.failures.forEach((failure, index) => {
39
- console.log(chalk.white(`${index + 1}. `) + chalk.bold.yellow(`[${failure.id.toUpperCase()}]`) + chalk.white(` ${failure.title}`));
81
+ const sev = failure.severity || 'medium';
82
+ const sevLabel = sevColor(sev)(`[${sev.toUpperCase()}]`);
83
+ const provLabel = failure.provenance ? chalk.dim(`(${failure.provenance})`) : '';
84
+ console.log(chalk.white(`${index + 1}. `) + sevLabel + ' ' + chalk.bold.yellow(`[${failure.id.toUpperCase()}]`) + ' ' + provLabel + chalk.white(` ${failure.title}`));
40
85
  console.log(chalk.dim(` └─ ${failure.details}`));
41
86
  if (failure.files && failure.files.length > 0) {
42
87
  console.log(chalk.cyan(` šŸ“ Files: ${failure.files.join(', ')}`));
@@ -0,0 +1,16 @@
1
+ /**
2
+ * export-audit command
3
+ *
4
+ * Generates a compliance audit package from the last gate check.
5
+ * The artifact compliance officers hand to auditors.
6
+ *
7
+ * Formats: JSON (structured) or Markdown (human-readable)
8
+ *
9
+ * @since v2.17.0
10
+ */
11
+ export interface ExportAuditOptions {
12
+ format?: 'json' | 'md';
13
+ output?: string;
14
+ run?: boolean;
15
+ }
16
+ export declare function exportAuditCommand(cwd: string, options?: ExportAuditOptions): Promise<void>;
@@ -0,0 +1,245 @@
1
+ /**
2
+ * export-audit command
3
+ *
4
+ * Generates a compliance audit package from the last gate check.
5
+ * The artifact compliance officers hand to auditors.
6
+ *
7
+ * Formats: JSON (structured) or Markdown (human-readable)
8
+ *
9
+ * @since v2.17.0
10
+ */
11
+ import fs from 'fs-extra';
12
+ import path from 'path';
13
+ import chalk from 'chalk';
14
+ import yaml from 'yaml';
15
+ import { getScoreHistory, getScoreTrend } from '@rigour-labs/core';
16
+ const CLI_VERSION = '2.0.0';
17
+ export async function exportAuditCommand(cwd, options = {}) {
18
+ const format = options.format || 'json';
19
+ const configPath = path.join(cwd, 'rigour.yml');
20
+ let reportPath = path.join(cwd, 'rigour-report.json');
21
+ // If --run, execute a fresh check first
22
+ if (options.run) {
23
+ console.log(chalk.blue('Running fresh rigour check...\n'));
24
+ const { checkCommand } = await import('./check.js');
25
+ try {
26
+ await checkCommand(cwd, [], {});
27
+ }
28
+ catch {
29
+ // checkCommand calls process.exit, so we catch here for the --run flow
30
+ }
31
+ }
32
+ // Read config
33
+ let config = {};
34
+ if (await fs.pathExists(configPath)) {
35
+ try {
36
+ const configContent = await fs.readFile(configPath, 'utf-8');
37
+ config = yaml.parse(configContent);
38
+ if (config?.output?.report_path) {
39
+ reportPath = path.join(cwd, config.output.report_path);
40
+ }
41
+ }
42
+ catch { }
43
+ }
44
+ // Read report
45
+ if (!(await fs.pathExists(reportPath))) {
46
+ console.error(chalk.red(`Error: No report found at ${reportPath}`));
47
+ console.error(chalk.dim('Run `rigour check` first, or use `rigour export-audit --run`.'));
48
+ process.exit(2);
49
+ }
50
+ let report;
51
+ try {
52
+ const reportContent = await fs.readFile(reportPath, 'utf-8');
53
+ report = JSON.parse(reportContent);
54
+ }
55
+ catch (error) {
56
+ console.error(chalk.red(`Error reading report: ${error.message}`));
57
+ process.exit(3);
58
+ }
59
+ // Build audit package
60
+ const auditPackage = buildAuditPackage(cwd, report, config);
61
+ // Determine output path
62
+ const outputPath = options.output
63
+ ? path.resolve(cwd, options.output)
64
+ : path.join(cwd, `rigour-audit-report.${format}`);
65
+ // Write output
66
+ if (format === 'md') {
67
+ const markdown = renderMarkdown(auditPackage);
68
+ await fs.writeFile(outputPath, markdown, 'utf-8');
69
+ }
70
+ else {
71
+ await fs.writeJson(outputPath, auditPackage, { spaces: 2 });
72
+ }
73
+ console.log(chalk.green(`\nāœ” Audit report exported: ${path.relative(cwd, outputPath)}`));
74
+ console.log(chalk.dim(` Format: ${format.toUpperCase()} | Status: ${auditPackage.summary.status} | Score: ${auditPackage.summary.score}/100`));
75
+ }
76
+ function buildAuditPackage(cwd, report, config) {
77
+ const stats = report.stats || {};
78
+ const failures = report.failures || [];
79
+ // Score trend
80
+ const trend = getScoreTrend(cwd);
81
+ const history = getScoreHistory(cwd, 5);
82
+ // Severity breakdown
83
+ const severityBreakdown = stats.severity_breakdown || {};
84
+ const provenanceBreakdown = stats.provenance_breakdown || {};
85
+ // Gate results from summary
86
+ const gateResults = Object.entries(report.summary || {}).map(([gate, status]) => ({
87
+ gate,
88
+ status: status,
89
+ }));
90
+ // Top violations
91
+ const violations = failures.map((f) => ({
92
+ id: f.id,
93
+ severity: f.severity || 'medium',
94
+ provenance: f.provenance || 'traditional',
95
+ title: f.title,
96
+ details: f.details,
97
+ files: f.files || [],
98
+ line: f.line,
99
+ hint: f.hint,
100
+ }));
101
+ return {
102
+ schema_version: '1.0.0',
103
+ metadata: {
104
+ project: path.basename(cwd),
105
+ rigour_version: CLI_VERSION,
106
+ timestamp: new Date().toISOString(),
107
+ preset: config.preset || 'custom',
108
+ config_path: 'rigour.yml',
109
+ generated_by: 'rigour export-audit',
110
+ },
111
+ summary: {
112
+ status: report.status,
113
+ score: stats.score ?? 100,
114
+ ai_health_score: stats.ai_health_score,
115
+ structural_score: stats.structural_score,
116
+ duration_ms: stats.duration_ms,
117
+ total_violations: failures.length,
118
+ },
119
+ severity_breakdown: {
120
+ critical: severityBreakdown.critical || 0,
121
+ high: severityBreakdown.high || 0,
122
+ medium: severityBreakdown.medium || 0,
123
+ low: severityBreakdown.low || 0,
124
+ info: severityBreakdown.info || 0,
125
+ },
126
+ provenance_breakdown: {
127
+ 'ai-drift': provenanceBreakdown['ai-drift'] || 0,
128
+ traditional: provenanceBreakdown.traditional || 0,
129
+ security: provenanceBreakdown.security || 0,
130
+ governance: provenanceBreakdown.governance || 0,
131
+ },
132
+ gate_results: gateResults,
133
+ violations,
134
+ score_trend: trend ? {
135
+ direction: trend.direction,
136
+ delta: trend.delta,
137
+ recent_average: trend.recentAvg,
138
+ previous_average: trend.previousAvg,
139
+ last_scores: trend.recentScores,
140
+ } : null,
141
+ recent_history: history.map(h => ({
142
+ timestamp: h.timestamp,
143
+ score: h.score,
144
+ status: h.status,
145
+ })),
146
+ };
147
+ }
148
+ function renderMarkdown(audit) {
149
+ const lines = [];
150
+ lines.push(`# Rigour Audit Report`);
151
+ lines.push('');
152
+ lines.push(`**Project:** ${audit.metadata.project}`);
153
+ lines.push(`**Generated:** ${audit.metadata.timestamp}`);
154
+ lines.push(`**Rigour Version:** ${audit.metadata.rigour_version}`);
155
+ lines.push(`**Preset:** ${audit.metadata.preset}`);
156
+ lines.push('');
157
+ // Summary
158
+ lines.push('## Summary');
159
+ lines.push('');
160
+ const statusEmoji = audit.summary.status === 'PASS' ? 'āœ…' : 'šŸ›‘';
161
+ lines.push(`| Metric | Value |`);
162
+ lines.push(`|:-------|:------|`);
163
+ lines.push(`| **Status** | ${statusEmoji} ${audit.summary.status} |`);
164
+ lines.push(`| **Overall Score** | ${audit.summary.score}/100 |`);
165
+ if (audit.summary.ai_health_score !== undefined) {
166
+ lines.push(`| **AI Health Score** | ${audit.summary.ai_health_score}/100 |`);
167
+ }
168
+ if (audit.summary.structural_score !== undefined) {
169
+ lines.push(`| **Structural Score** | ${audit.summary.structural_score}/100 |`);
170
+ }
171
+ lines.push(`| **Total Violations** | ${audit.summary.total_violations} |`);
172
+ lines.push(`| **Duration** | ${audit.summary.duration_ms}ms |`);
173
+ lines.push('');
174
+ // Severity Breakdown
175
+ lines.push('## Severity Breakdown');
176
+ lines.push('');
177
+ lines.push('| Severity | Count |');
178
+ lines.push('|:---------|:------|');
179
+ for (const [sev, count] of Object.entries(audit.severity_breakdown)) {
180
+ if (count > 0) {
181
+ lines.push(`| ${sev.charAt(0).toUpperCase() + sev.slice(1)} | ${count} |`);
182
+ }
183
+ }
184
+ lines.push('');
185
+ // Provenance Breakdown
186
+ lines.push('## Provenance Breakdown');
187
+ lines.push('');
188
+ lines.push('| Category | Count |');
189
+ lines.push('|:---------|:------|');
190
+ for (const [prov, count] of Object.entries(audit.provenance_breakdown)) {
191
+ if (count > 0) {
192
+ lines.push(`| ${prov} | ${count} |`);
193
+ }
194
+ }
195
+ lines.push('');
196
+ // Gate Results
197
+ lines.push('## Gate Results');
198
+ lines.push('');
199
+ lines.push('| Gate | Status |');
200
+ lines.push('|:-----|:-------|');
201
+ for (const gate of audit.gate_results) {
202
+ const icon = gate.status === 'PASS' ? 'āœ…' : gate.status === 'FAIL' ? 'āŒ' : 'ā­ļø';
203
+ lines.push(`| ${gate.gate} | ${icon} ${gate.status} |`);
204
+ }
205
+ lines.push('');
206
+ // Violations
207
+ if (audit.violations.length > 0) {
208
+ lines.push('## Violations');
209
+ lines.push('');
210
+ for (let i = 0; i < audit.violations.length; i++) {
211
+ const v = audit.violations[i];
212
+ lines.push(`### ${i + 1}. [${v.severity.toUpperCase()}] ${v.title}`);
213
+ lines.push('');
214
+ lines.push(`- **ID:** \`${v.id}\``);
215
+ lines.push(`- **Severity:** ${v.severity}`);
216
+ lines.push(`- **Provenance:** ${v.provenance}`);
217
+ lines.push(`- **Details:** ${v.details}`);
218
+ if (v.files && v.files.length > 0) {
219
+ lines.push(`- **Files:** ${v.files.join(', ')}`);
220
+ }
221
+ if (v.hint) {
222
+ lines.push(`- **Hint:** ${v.hint}`);
223
+ }
224
+ lines.push('');
225
+ }
226
+ }
227
+ // Score Trend
228
+ if (audit.score_trend) {
229
+ lines.push('## Score Trend');
230
+ lines.push('');
231
+ const arrow = audit.score_trend.direction === 'improving' ? '↑' :
232
+ audit.score_trend.direction === 'degrading' ? '↓' : '→';
233
+ lines.push(`**Direction:** ${audit.score_trend.direction} ${arrow}`);
234
+ lines.push(`**Recent Average:** ${audit.score_trend.recent_average}/100`);
235
+ lines.push(`**Previous Average:** ${audit.score_trend.previous_average}/100`);
236
+ lines.push(`**Delta:** ${audit.score_trend.delta > 0 ? '+' : ''}${audit.score_trend.delta}`);
237
+ lines.push(`**Recent Scores:** ${audit.score_trend.last_scores.join(' → ')}`);
238
+ lines.push('');
239
+ }
240
+ // Footer
241
+ lines.push('---');
242
+ lines.push(`*Generated by Rigour v${audit.metadata.rigour_version} — ${audit.metadata.timestamp}*`);
243
+ lines.push('');
244
+ return lines.join('\n');
245
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * `rigour hooks init` — Generate tool-specific hook configurations.
3
+ *
4
+ * Detects which AI coding tools are present (or accepts --tool flag)
5
+ * and generates the appropriate hook files so that Rigour runs
6
+ * quality checks after every file write/edit.
7
+ *
8
+ * Supported tools:
9
+ * - Claude Code (.claude/settings.json PostToolUse)
10
+ * - Cursor (.cursor/hooks.json afterFileEdit)
11
+ * - Cline (.clinerules/hooks/PostToolUse)
12
+ * - Windsurf (.windsurf/hooks.json post_write_code)
13
+ *
14
+ * @since v3.0.0
15
+ */
16
+ export interface HooksOptions {
17
+ tool?: string;
18
+ dryRun?: boolean;
19
+ force?: boolean;
20
+ block?: boolean;
21
+ }
22
+ export declare function hooksInitCommand(cwd: string, options?: HooksOptions): Promise<void>;
@@ -0,0 +1,274 @@
1
+ /**
2
+ * `rigour hooks init` — Generate tool-specific hook configurations.
3
+ *
4
+ * Detects which AI coding tools are present (or accepts --tool flag)
5
+ * and generates the appropriate hook files so that Rigour runs
6
+ * quality checks after every file write/edit.
7
+ *
8
+ * Supported tools:
9
+ * - Claude Code (.claude/settings.json PostToolUse)
10
+ * - Cursor (.cursor/hooks.json afterFileEdit)
11
+ * - Cline (.clinerules/hooks/PostToolUse)
12
+ * - Windsurf (.windsurf/hooks.json post_write_code)
13
+ *
14
+ * @since v3.0.0
15
+ */
16
+ import fs from 'fs-extra';
17
+ import path from 'path';
18
+ import chalk from 'chalk';
19
+ import { randomUUID } from 'crypto';
20
+ // ── Studio event logging ─────────────────────────────────────────────
21
+ async function logStudioEvent(cwd, event) {
22
+ try {
23
+ const rigourDir = path.join(cwd, '.rigour');
24
+ await fs.ensureDir(rigourDir);
25
+ const eventsPath = path.join(rigourDir, 'events.jsonl');
26
+ const logEntry = JSON.stringify({
27
+ id: randomUUID(),
28
+ timestamp: new Date().toISOString(),
29
+ ...event,
30
+ }) + '\n';
31
+ await fs.appendFile(eventsPath, logEntry);
32
+ }
33
+ catch {
34
+ // Silent fail
35
+ }
36
+ }
37
+ // ── Tool detection ───────────────────────────────────────────────────
38
+ const TOOL_MARKERS = {
39
+ claude: ['CLAUDE.md', '.claude'],
40
+ cursor: ['.cursor', '.cursorrules'],
41
+ cline: ['.clinerules'],
42
+ windsurf: ['.windsurfrules', '.windsurf'],
43
+ };
44
+ function detectTools(cwd) {
45
+ const detected = [];
46
+ for (const [tool, markers] of Object.entries(TOOL_MARKERS)) {
47
+ for (const marker of markers) {
48
+ if (fs.existsSync(path.join(cwd, marker))) {
49
+ detected.push(tool);
50
+ break;
51
+ }
52
+ }
53
+ }
54
+ return detected;
55
+ }
56
+ function resolveCheckerPath(cwd) {
57
+ const localPath = path.join(cwd, 'node_modules', '@rigour-labs', 'core', 'dist', 'hooks', 'standalone-checker.js');
58
+ if (fs.existsSync(localPath)) {
59
+ return localPath;
60
+ }
61
+ return 'npx rigour-hook-check';
62
+ }
63
+ // ── Tool resolution (from --tool flag or auto-detect) ────────────────
64
+ const ALL_TOOLS = ['claude', 'cursor', 'cline', 'windsurf'];
65
+ function resolveTools(cwd, toolFlag) {
66
+ if (toolFlag === 'all') {
67
+ return ALL_TOOLS;
68
+ }
69
+ if (toolFlag) {
70
+ const requested = toolFlag.split(',').map(t => t.trim().toLowerCase());
71
+ const valid = requested.filter(t => ALL_TOOLS.includes(t));
72
+ if (valid.length === 0) {
73
+ console.error(chalk.red(`Unknown tool: ${toolFlag}. Valid: claude, cursor, cline, windsurf, all`));
74
+ process.exit(1);
75
+ }
76
+ return valid;
77
+ }
78
+ // Auto-detect
79
+ const detected = detectTools(cwd);
80
+ if (detected.length === 0) {
81
+ console.log(chalk.yellow('No AI coding tools detected. Defaulting to Claude Code.'));
82
+ console.log(chalk.dim(' Use --tool <name> to specify: claude, cursor, cline, windsurf, all\n'));
83
+ return ['claude'];
84
+ }
85
+ console.log(chalk.green(`Detected tools: ${detected.join(', ')}`));
86
+ return detected;
87
+ }
88
+ // ── Per-tool hook generators ─────────────────────────────────────────
89
+ function generateClaudeHooks(checkerPath, block) {
90
+ const blockFlag = block ? ' --block' : '';
91
+ const settings = {
92
+ hooks: {
93
+ PostToolUse: [{
94
+ matcher: "Write|Edit|MultiEdit",
95
+ hooks: [{
96
+ type: "command",
97
+ command: `node ${checkerPath} --files "$TOOL_INPUT_file_path"${blockFlag}`,
98
+ }]
99
+ }]
100
+ }
101
+ };
102
+ return [{
103
+ path: '.claude/settings.json',
104
+ content: JSON.stringify(settings, null, 4),
105
+ description: 'Claude Code PostToolUse hook',
106
+ }];
107
+ }
108
+ function generateCursorHooks(checkerPath, _block) {
109
+ const hooks = {
110
+ version: 1,
111
+ hooks: { afterFileEdit: [{ command: `node ${checkerPath} --stdin` }] }
112
+ };
113
+ return [{
114
+ path: '.cursor/hooks.json',
115
+ content: JSON.stringify(hooks, null, 4),
116
+ description: 'Cursor afterFileEdit hook config',
117
+ }];
118
+ }
119
+ function generateClineHooks(checkerPath, _block) {
120
+ const script = buildClineScript(checkerPath);
121
+ return [{
122
+ path: '.clinerules/hooks/PostToolUse',
123
+ content: script,
124
+ executable: true,
125
+ description: 'Cline PostToolUse executable hook',
126
+ }];
127
+ }
128
+ function buildClineScript(checkerPath) {
129
+ return `#!/usr/bin/env node
130
+ /**
131
+ * Cline PostToolUse hook for Rigour.
132
+ * Receives JSON on stdin with { toolName, toolInput }.
133
+ */
134
+ const WRITE_TOOLS = ['write_to_file', 'replace_in_file'];
135
+
136
+ let data = '';
137
+ process.stdin.on('data', chunk => { data += chunk; });
138
+ process.stdin.on('end', async () => {
139
+ try {
140
+ const payload = JSON.parse(data);
141
+ if (!WRITE_TOOLS.includes(payload.toolName)) {
142
+ process.stdout.write(JSON.stringify({}));
143
+ return;
144
+ }
145
+ const filePath = payload.toolInput?.path || payload.toolInput?.file_path;
146
+ if (!filePath) {
147
+ process.stdout.write(JSON.stringify({}));
148
+ return;
149
+ }
150
+
151
+ const { execSync } = require('child_process');
152
+ const raw = execSync(
153
+ \`node ${checkerPath} --files "\${filePath}"\`,
154
+ { encoding: 'utf-8', timeout: 5000 }
155
+ );
156
+ const result = JSON.parse(raw);
157
+ if (result.status === 'fail') {
158
+ const msgs = result.failures
159
+ .map(f => \`[rigour/\${f.gate}] \${f.file}: \${f.message}\`)
160
+ .join('\\n');
161
+ process.stdout.write(JSON.stringify({
162
+ contextModification: \`\\n[Rigour] \${result.failures.length} issue(s):\\n\${msgs}\\nPlease fix before continuing.\`,
163
+ }));
164
+ } else {
165
+ process.stdout.write(JSON.stringify({}));
166
+ }
167
+ } catch (err) {
168
+ process.stderr.write(\`Rigour hook error: \${err.message}\\n\`);
169
+ process.stdout.write(JSON.stringify({}));
170
+ }
171
+ });
172
+ `;
173
+ }
174
+ function generateWindsurfHooks(checkerPath, _block) {
175
+ const hooks = {
176
+ version: 1,
177
+ hooks: { post_write_code: [{ command: `node ${checkerPath} --stdin` }] }
178
+ };
179
+ return [{
180
+ path: '.windsurf/hooks.json',
181
+ content: JSON.stringify(hooks, null, 4),
182
+ description: 'Windsurf post_write_code hook config',
183
+ }];
184
+ }
185
+ const GENERATORS = {
186
+ claude: generateClaudeHooks,
187
+ cursor: generateCursorHooks,
188
+ cline: generateClineHooks,
189
+ windsurf: generateWindsurfHooks,
190
+ };
191
+ // ── File writing ─────────────────────────────────────────────────────
192
+ function printDryRun(files) {
193
+ console.log(chalk.cyan('\nDry run — files that would be created:\n'));
194
+ for (const file of files) {
195
+ console.log(chalk.bold(` ${file.path}`));
196
+ console.log(chalk.dim(` ${file.description}`));
197
+ if (file.executable) {
198
+ console.log(chalk.dim(' (executable)'));
199
+ }
200
+ }
201
+ console.log('');
202
+ }
203
+ async function writeHookFiles(cwd, files, force) {
204
+ let written = 0;
205
+ let skipped = 0;
206
+ for (const file of files) {
207
+ const fullPath = path.join(cwd, file.path);
208
+ const exists = await fs.pathExists(fullPath);
209
+ if (exists && !force) {
210
+ console.log(chalk.yellow(` SKIP ${file.path} (already exists, use --force to overwrite)`));
211
+ skipped++;
212
+ continue;
213
+ }
214
+ await fs.ensureDir(path.dirname(fullPath));
215
+ await fs.writeFile(fullPath, file.content, 'utf-8');
216
+ if (file.executable) {
217
+ await fs.chmod(fullPath, 0o755);
218
+ }
219
+ console.log(chalk.green(` CREATE ${file.path}`));
220
+ console.log(chalk.dim(` ${file.description}`));
221
+ written++;
222
+ }
223
+ return { written, skipped };
224
+ }
225
+ // ── Next-steps guidance ──────────────────────────────────────────────
226
+ const NEXT_STEPS = {
227
+ claude: 'Claude Code: Hooks are active immediately. Rigour runs after every Write/Edit.',
228
+ cursor: 'Cursor: Reload window (Cmd+Shift+P > Reload). Check Output > Hooks panel for logs.',
229
+ cline: 'Cline: Hook is active. Quality feedback appears in agent context on violations.',
230
+ windsurf: 'Windsurf: Reload editor. Check terminal for Rigour output after Cascade writes.',
231
+ };
232
+ function printNextSteps(tools) {
233
+ console.log(chalk.cyan('\nNext steps:'));
234
+ for (const tool of tools) {
235
+ console.log(chalk.dim(` ${NEXT_STEPS[tool]}`));
236
+ }
237
+ console.log('');
238
+ }
239
+ // ── Main command entry point ─────────────────────────────────────────
240
+ export async function hooksInitCommand(cwd, options = {}) {
241
+ console.log(chalk.blue('\nRigour Hooks Setup\n'));
242
+ await logStudioEvent(cwd, {
243
+ type: 'tool_call',
244
+ tool: 'rigour_hooks_init',
245
+ arguments: { tool: options.tool, dryRun: options.dryRun },
246
+ });
247
+ const tools = resolveTools(cwd, options.tool);
248
+ const checkerPath = resolveCheckerPath(cwd);
249
+ const block = !!options.block;
250
+ // Collect generated files from all tools
251
+ const allFiles = [];
252
+ for (const tool of tools) {
253
+ allFiles.push(...GENERATORS[tool](checkerPath, block));
254
+ }
255
+ if (options.dryRun) {
256
+ printDryRun(allFiles);
257
+ return;
258
+ }
259
+ const { written, skipped } = await writeHookFiles(cwd, allFiles, !!options.force);
260
+ console.log('');
261
+ if (written > 0) {
262
+ console.log(chalk.green.bold(`Created ${written} hook file(s).`));
263
+ }
264
+ if (skipped > 0) {
265
+ console.log(chalk.yellow(`Skipped ${skipped} existing file(s).`));
266
+ }
267
+ printNextSteps(tools);
268
+ await logStudioEvent(cwd, {
269
+ type: 'tool_response',
270
+ tool: 'rigour_hooks_init',
271
+ status: 'success',
272
+ content: [{ type: 'text', text: `Generated hooks for: ${tools.join(', ')}` }],
273
+ });
274
+ }
@@ -0,0 +1 @@
1
+ export {};