@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.
- package/README.md +14 -1
- package/dist/cli.js +68 -5
- package/dist/commands/check.js +68 -11
- package/dist/commands/demo.d.ts +23 -0
- package/dist/commands/demo.js +618 -0
- package/dist/commands/demo.test.d.ts +1 -0
- package/dist/commands/demo.test.js +59 -0
- package/dist/commands/explain.js +46 -1
- package/dist/commands/export-audit.d.ts +16 -0
- package/dist/commands/export-audit.js +245 -0
- package/dist/commands/hooks.d.ts +22 -0
- package/dist/commands/hooks.js +274 -0
- package/dist/commands/hooks.test.d.ts +1 -0
- package/dist/commands/hooks.test.js +77 -0
- package/dist/commands/init.js +25 -1
- package/dist/commands/init.test.d.ts +1 -0
- package/dist/commands/init.test.js +97 -0
- package/dist/init-rules.test.js +2 -1
- package/dist/smoke.test.js +2 -1
- package/package.json +18 -2
- package/studio-dist/assets/index-C0TtM2OR.js +291 -0
- package/studio-dist/index.html +1 -1
- package/studio-dist/assets/index-Ch-q_mnO.js +0 -291
package/dist/commands/explain.js
CHANGED
|
@@ -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
|
-
|
|
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 {};
|