clckernel 1.2.6
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/AGENTS.md +124 -0
- package/README.es.md +166 -0
- package/README.md +167 -0
- package/bin/cli.js +12 -0
- package/docs/Journal/001-adr-clckernel-governance.md +18 -0
- package/package.json +44 -0
- package/src/adapters/astro.js +67 -0
- package/src/adapters/base.js +109 -0
- package/src/adapters/django.js +70 -0
- package/src/adapters/fastapi.js +69 -0
- package/src/adapters/go.js +50 -0
- package/src/adapters/laravel.js +52 -0
- package/src/adapters/nextjs.js +72 -0
- package/src/adapters/rails.js +54 -0
- package/src/adapters/rust.js +51 -0
- package/src/catalog.js +217 -0
- package/src/config.js +250 -0
- package/src/detector.js +85 -0
- package/src/doctor.js +125 -0
- package/src/generator.js +473 -0
- package/src/index.js +52 -0
- package/src/technologies/detector.js +243 -0
- package/src/technologies/guards/_shared.js +66 -0
- package/src/ui.js +101 -0
- package/src/yaml.js +173 -0
- package/test/adapters/astro.test.js +102 -0
- package/test/adapters/base.test.js +86 -0
- package/test/adapters/django.test.js +95 -0
- package/test/adapters/fastapi.test.js +88 -0
- package/test/adapters/go.test.js +81 -0
- package/test/adapters/laravel.test.js +106 -0
- package/test/adapters/nextjs.test.js +113 -0
- package/test/adapters/rails.test.js +108 -0
- package/test/adapters/rust.test.js +83 -0
- package/test/catalog.test.js +170 -0
- package/test/config.test.js +512 -0
- package/test/detector.test.js +345 -0
- package/test/doctor.test.js +302 -0
- package/test/generator.test.js +419 -0
- package/test/guards_new.test.js +95 -0
- package/test/helpers.js +36 -0
- package/test/integration/cli-flow.test.js +270 -0
- package/test/technologies/celery_guard.test.js +185 -0
- package/test/technologies/detector.test.js +342 -0
- package/test/technologies/docker_guard.test.js +187 -0
- package/test/technologies/integration.test.js +123 -0
- package/test/technologies/postgres_guard.test.js +87 -0
- package/test/technologies/redis_guard.test.js +128 -0
- package/tools/audit.js +219 -0
- package/tools/audit.py +273 -0
- package/tools/celery_guard.py +209 -0
- package/tools/check_a11y.js +109 -0
- package/tools/check_api_contracts.js +139 -0
- package/tools/check_architecture.js +139 -0
- package/tools/check_architecture.py +264 -0
- package/tools/check_custom.js +163 -0
- package/tools/check_custom.py +395 -0
- package/tools/check_db_efficiency.py +190 -0
- package/tools/check_migrations.py +223 -0
- package/tools/check_performance.js +142 -0
- package/tools/check_responsive.js +131 -0
- package/tools/check_scope.py +180 -0
- package/tools/check_seo.js +139 -0
- package/tools/check_storybook.js +108 -0
- package/tools/check_ui_reuse.js +135 -0
- package/tools/docker_guard.py +210 -0
- package/tools/postgres_guard.py +192 -0
- package/tools/redis_guard.py +197 -0
- package/tools/scan_secrets.js +109 -0
- package/tools/scan_secrets.py +153 -0
- package/tools/verify_tdd.py +212 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLC Forge — PostgreSQL guard integration tests.
|
|
3
|
+
* Tests postgres_guard.py via subprocess against fixture projects.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { describe, it } = require('node:test');
|
|
7
|
+
const assert = require('node:assert');
|
|
8
|
+
const { spawn, spawnSync } = require('child_process');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const { createTmpProject, teardown } = require('../helpers');
|
|
12
|
+
|
|
13
|
+
function runPostgresGuard(targetDir) {
|
|
14
|
+
return new Promise((resolve, reject) => {
|
|
15
|
+
const script = path.join(__dirname, '../../tools/postgres_guard.py');
|
|
16
|
+
const proc = spawn('python3', [script, targetDir], { cwd: targetDir });
|
|
17
|
+
let stdout = '';
|
|
18
|
+
proc.stdout.on('data', (d) => (stdout += d));
|
|
19
|
+
proc.on('close', () => {
|
|
20
|
+
try {
|
|
21
|
+
resolve(JSON.parse(stdout));
|
|
22
|
+
} catch {
|
|
23
|
+
reject(new Error(`Failed to parse: ${stdout}`));
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
proc.on('error', reject);
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function initGitRepo(dir) {
|
|
31
|
+
fs.writeFileSync(path.join(dir, '.gitkeep'), '');
|
|
32
|
+
spawnSync('git', ['init'], { cwd: dir });
|
|
33
|
+
spawnSync('git', ['config', 'user.email', 'test@test.com'], { cwd: dir });
|
|
34
|
+
spawnSync('git', ['config', 'user.name', 'Test'], { cwd: dir });
|
|
35
|
+
spawnSync('git', ['add', '.'], { cwd: dir });
|
|
36
|
+
spawnSync('git', ['commit', '-m', 'initial'], { cwd: dir });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe('postgres_guard', () => {
|
|
40
|
+
it('skips when no git repo', async () => {
|
|
41
|
+
const dir = createTmpProject({});
|
|
42
|
+
try {
|
|
43
|
+
const result = await runPostgresGuard(dir);
|
|
44
|
+
assert.strictEqual(result.skipped, true);
|
|
45
|
+
} finally {
|
|
46
|
+
teardown(dir);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('no violations: no SQL queries in project', async () => {
|
|
51
|
+
const dir = createTmpProject({});
|
|
52
|
+
initGitRepo(dir);
|
|
53
|
+
|
|
54
|
+
fs.writeFileSync(
|
|
55
|
+
path.join(dir, 'main.py'),
|
|
56
|
+
`def hello():\n return "world"\n`
|
|
57
|
+
);
|
|
58
|
+
spawnSync('git', ['add', 'main.py'], { cwd: dir });
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const result = await runPostgresGuard(dir);
|
|
62
|
+
assert.strictEqual(result.exit_code, 0);
|
|
63
|
+
assert.strictEqual(result.data.findings.length, 0, 'should have no findings');
|
|
64
|
+
} finally {
|
|
65
|
+
teardown(dir);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('no violations: no postgres in project', async () => {
|
|
70
|
+
const dir = createTmpProject({});
|
|
71
|
+
initGitRepo(dir);
|
|
72
|
+
|
|
73
|
+
fs.writeFileSync(
|
|
74
|
+
path.join(dir, 'main.py'),
|
|
75
|
+
`def hello():\n return "world"\n`
|
|
76
|
+
);
|
|
77
|
+
spawnSync('git', ['add', 'main.py'], { cwd: dir });
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
const result = await runPostgresGuard(dir);
|
|
81
|
+
assert.strictEqual(result.exit_code, 0);
|
|
82
|
+
assert.strictEqual(result.data.findings.length, 0, 'should have no findings');
|
|
83
|
+
} finally {
|
|
84
|
+
teardown(dir);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLC Forge — Redis guard integration tests.
|
|
3
|
+
* Tests redis_guard.py via subprocess against fixture projects.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { describe, it } = require('node:test');
|
|
7
|
+
const assert = require('node:assert');
|
|
8
|
+
const { spawn, spawnSync } = require('child_process');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const { createTmpProject, teardown } = require('../helpers');
|
|
12
|
+
|
|
13
|
+
function runRedisGuard(targetDir) {
|
|
14
|
+
return new Promise((resolve, reject) => {
|
|
15
|
+
const script = path.join(__dirname, '../../tools/redis_guard.py');
|
|
16
|
+
const proc = spawn('python3', [script, targetDir], { cwd: targetDir });
|
|
17
|
+
let stdout = '';
|
|
18
|
+
proc.stdout.on('data', (d) => (stdout += d));
|
|
19
|
+
proc.on('close', () => {
|
|
20
|
+
try {
|
|
21
|
+
resolve(JSON.parse(stdout));
|
|
22
|
+
} catch {
|
|
23
|
+
reject(new Error(`Failed to parse: ${stdout}`));
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
proc.on('error', reject);
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function initGitRepo(dir) {
|
|
31
|
+
fs.writeFileSync(path.join(dir, '.gitkeep'), '');
|
|
32
|
+
spawnSync('git', ['init'], { cwd: dir });
|
|
33
|
+
spawnSync('git', ['config', 'user.email', 'test@test.com'], { cwd: dir });
|
|
34
|
+
spawnSync('git', ['config', 'user.name', 'Test'], { cwd: dir });
|
|
35
|
+
spawnSync('git', ['add', '.'], { cwd: dir });
|
|
36
|
+
spawnSync('git', ['commit', '-m', 'initial'], { cwd: dir });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe('redis_guard', () => {
|
|
40
|
+
it('skips when no git repo', async () => {
|
|
41
|
+
const dir = createTmpProject({});
|
|
42
|
+
try {
|
|
43
|
+
const result = await runRedisGuard(dir);
|
|
44
|
+
assert.strictEqual(result.skipped, true);
|
|
45
|
+
} finally {
|
|
46
|
+
teardown(dir);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('finds violations: decode_responses=True without password', async () => {
|
|
51
|
+
const dir = createTmpProject({});
|
|
52
|
+
initGitRepo(dir);
|
|
53
|
+
|
|
54
|
+
fs.writeFileSync(
|
|
55
|
+
path.join(dir, 'cache.py'),
|
|
56
|
+
`from redis import Redis\nr = Redis(host="localhost", port=6379, decode_responses=True)\n`
|
|
57
|
+
);
|
|
58
|
+
spawnSync('git', ['add', 'cache.py'], { cwd: dir });
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const result = await runRedisGuard(dir);
|
|
62
|
+
assert.strictEqual(result.exit_code, 1);
|
|
63
|
+
const issues = result.data.findings.map((f) => f.issue);
|
|
64
|
+
assert.ok(issues.includes('decode_responses_without_password'), 'should detect decode_responses without password');
|
|
65
|
+
} finally {
|
|
66
|
+
teardown(dir);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('finds violations: missing socket_connect_timeout', async () => {
|
|
71
|
+
const dir = createTmpProject({});
|
|
72
|
+
initGitRepo(dir);
|
|
73
|
+
|
|
74
|
+
fs.writeFileSync(
|
|
75
|
+
path.join(dir, 'cache.py'),
|
|
76
|
+
`from redis import Redis\nr = Redis(host="localhost", port=6379, decode_responses=False, password="secret")\n`
|
|
77
|
+
);
|
|
78
|
+
spawnSync('git', ['add', 'cache.py'], { cwd: dir });
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
const result = await runRedisGuard(dir);
|
|
82
|
+
assert.strictEqual(result.exit_code, 1);
|
|
83
|
+
const issues = result.data.findings.map((f) => f.issue);
|
|
84
|
+
assert.ok(issues.includes('missing_socket_connect_timeout'), 'should detect missing timeout');
|
|
85
|
+
} finally {
|
|
86
|
+
teardown(dir);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('finds violations: hardcoded Redis host without timeout', async () => {
|
|
91
|
+
const dir = createTmpProject({});
|
|
92
|
+
initGitRepo(dir);
|
|
93
|
+
|
|
94
|
+
fs.writeFileSync(
|
|
95
|
+
path.join(dir, 'cache.py'),
|
|
96
|
+
`from redis import Redis\nr = Redis(host="localhost", port=6379)\n`
|
|
97
|
+
);
|
|
98
|
+
spawnSync('git', ['add', 'cache.py'], { cwd: dir });
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
const result = await runRedisGuard(dir);
|
|
102
|
+
assert.strictEqual(result.exit_code, 1);
|
|
103
|
+
const issues = result.data.findings.map((f) => f.issue);
|
|
104
|
+
assert.ok(issues.includes('missing_socket_connect_timeout'), 'should detect missing timeout');
|
|
105
|
+
} finally {
|
|
106
|
+
teardown(dir);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('no violations: no Redis in project', async () => {
|
|
111
|
+
const dir = createTmpProject({});
|
|
112
|
+
initGitRepo(dir);
|
|
113
|
+
|
|
114
|
+
fs.writeFileSync(
|
|
115
|
+
path.join(dir, 'main.py'),
|
|
116
|
+
`def hello():\n return "world"\n`
|
|
117
|
+
);
|
|
118
|
+
spawnSync('git', ['add', 'main.py'], { cwd: dir });
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
const result = await runRedisGuard(dir);
|
|
122
|
+
assert.strictEqual(result.exit_code, 0);
|
|
123
|
+
assert.strictEqual(result.data.findings.length, 0, 'should have no findings');
|
|
124
|
+
} finally {
|
|
125
|
+
teardown(dir);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
});
|
package/tools/audit.js
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Modular Audit Orchestrator — JS
|
|
4
|
+
* Discovers check_*.js guard scripts at runtime, runs each with graceful
|
|
5
|
+
* degradation, and generates an English audit report (05-audit-report.md).
|
|
6
|
+
* Self-contained: uses only Node.js built-ins.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
|
|
12
|
+
const REPO_ROOT = path.resolve(__dirname, '..');
|
|
13
|
+
|
|
14
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
function getActiveSddDir() {
|
|
17
|
+
const sddsDir = path.join(REPO_ROOT, 'sdds');
|
|
18
|
+
if (!fs.existsSync(sddsDir)) {
|
|
19
|
+
fs.mkdirSync(sddsDir, { recursive: true });
|
|
20
|
+
}
|
|
21
|
+
const dirs = fs.readdirSync(sddsDir).filter(f => {
|
|
22
|
+
try { return fs.statSync(path.join(sddsDir, f)).isDirectory(); }
|
|
23
|
+
catch { return false; }
|
|
24
|
+
});
|
|
25
|
+
if (dirs.length > 0) {
|
|
26
|
+
return path.join(sddsDir, dirs[0]);
|
|
27
|
+
}
|
|
28
|
+
const defaultDir = path.join(sddsDir, 'frontend-audit');
|
|
29
|
+
fs.mkdirSync(defaultDir, { recursive: true });
|
|
30
|
+
return defaultDir;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Discover all check_*.js guard scripts in the tools/ directory.
|
|
35
|
+
* Returns absolute paths sorted alphabetically for deterministic ordering.
|
|
36
|
+
*/
|
|
37
|
+
function discoverGuards() {
|
|
38
|
+
const toolsDir = __dirname;
|
|
39
|
+
return fs.readdirSync(toolsDir)
|
|
40
|
+
.filter(f => (f.startsWith('check_') || f.startsWith('scan_')) && f.endsWith('.js') && f !== path.basename(__filename || 'audit.js'))
|
|
41
|
+
.sort()
|
|
42
|
+
.map(f => path.join(toolsDir, f));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Run a single guard module with try/catch.
|
|
47
|
+
* @param {string} guardPath - Absolute path to the guard script
|
|
48
|
+
* @param {string} targetDir - Directory to scan
|
|
49
|
+
* @returns {object} Result from the guard, or a skip result on failure
|
|
50
|
+
*/
|
|
51
|
+
async function runGuard(guardPath, targetDir) {
|
|
52
|
+
const guardName = path.basename(guardPath, '.js');
|
|
53
|
+
try {
|
|
54
|
+
const mod = require(guardPath);
|
|
55
|
+
const fn = typeof mod === 'function' ? mod : Object.values(mod)[0];
|
|
56
|
+
if (typeof fn !== 'function') {
|
|
57
|
+
return {
|
|
58
|
+
exitCode: 0, skipped: true, name: guardName,
|
|
59
|
+
data: { message: `Guard ${guardName} does not export a function` },
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
const result = await fn(targetDir);
|
|
63
|
+
// Normalize result shape
|
|
64
|
+
return {
|
|
65
|
+
exitCode: result.exitCode || 0,
|
|
66
|
+
skipped: Boolean(result.skipped),
|
|
67
|
+
name: result.name || guardName,
|
|
68
|
+
data: result.data || result,
|
|
69
|
+
};
|
|
70
|
+
} catch (e) {
|
|
71
|
+
return {
|
|
72
|
+
exitCode: 0, skipped: true, name: guardName,
|
|
73
|
+
data: { message: `Guard unavailable: ${e.message}` },
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ── Report Generation ────────────────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Format a single guard result into a markdown section.
|
|
82
|
+
*/
|
|
83
|
+
function formatGuardSection(name, result) {
|
|
84
|
+
const lines = [];
|
|
85
|
+
const icon = result.skipped ? '⏭️' : result.exitCode === 0 ? '✅' : '❌';
|
|
86
|
+
const status = result.skipped ? 'SKIPPED' : result.exitCode === 0 ? 'PASSED' : 'FAILED';
|
|
87
|
+
|
|
88
|
+
lines.push(`### ${icon} ${name.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase())} — ${status}`);
|
|
89
|
+
lines.push('');
|
|
90
|
+
|
|
91
|
+
if (result.skipped) {
|
|
92
|
+
lines.push(`> Skipped: ${result.data?.message || 'Dependencies not available'}`);
|
|
93
|
+
} else if (result.exitCode === 0) {
|
|
94
|
+
lines.push(`> ${result.data?.message || 'All checks passed'}`);
|
|
95
|
+
} else {
|
|
96
|
+
lines.push(`> ${result.data?.message || 'Issues found'}`);
|
|
97
|
+
lines.push('');
|
|
98
|
+
const violations = result.data?.violations || result.data?.findings || {};
|
|
99
|
+
const files = Object.keys(violations);
|
|
100
|
+
if (files.length > 0) {
|
|
101
|
+
for (const file of files) {
|
|
102
|
+
const items = violations[file];
|
|
103
|
+
for (const item of items) {
|
|
104
|
+
const loc = item.lineNo ? ` (line ${item.lineNo})` : '';
|
|
105
|
+
const label = item.label || item.rule || item.rule || item.module || '';
|
|
106
|
+
const msg = item.message || item.reason || '';
|
|
107
|
+
lines.push(`- \`${file}\`${loc}: ${label ? `[${label}] ` : ''}${msg}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return lines.join('\n');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Generate the 05-audit-report.md from aggregated results.
|
|
118
|
+
*/
|
|
119
|
+
function generateReport(results, sddDir) {
|
|
120
|
+
const taskName = path.basename(sddDir);
|
|
121
|
+
const passed = results.filter(r => !r.skipped && r.exitCode === 0).length;
|
|
122
|
+
const failed = results.filter(r => !r.skipped && r.exitCode !== 0).length;
|
|
123
|
+
const skipped = results.filter(r => r.skipped).length;
|
|
124
|
+
const total = results.length;
|
|
125
|
+
|
|
126
|
+
const sections = [];
|
|
127
|
+
|
|
128
|
+
sections.push(`# Code Audit Report: ${taskName}`);
|
|
129
|
+
sections.push('');
|
|
130
|
+
sections.push('> **Note for reviewer:** This report summarizes security, architecture, accessibility,');
|
|
131
|
+
sections.push('> API contracts, performance, UI reuse, and testing coverage to facilitate review.');
|
|
132
|
+
sections.push('');
|
|
133
|
+
sections.push('---');
|
|
134
|
+
sections.push('');
|
|
135
|
+
sections.push('## Summary');
|
|
136
|
+
sections.push('');
|
|
137
|
+
sections.push(`| Metric | Count |`);
|
|
138
|
+
sections.push(`|--------|-------|`);
|
|
139
|
+
sections.push(`| Total guards | ${total} |`);
|
|
140
|
+
sections.push(`| Passed | ${passed} |`);
|
|
141
|
+
sections.push(`| Failed | ${failed} |`);
|
|
142
|
+
sections.push(`| Skipped | ${skipped} |`);
|
|
143
|
+
sections.push('');
|
|
144
|
+
sections.push('---');
|
|
145
|
+
sections.push('');
|
|
146
|
+
|
|
147
|
+
for (const result of results) {
|
|
148
|
+
sections.push(formatGuardSection(result.name, result));
|
|
149
|
+
sections.push('');
|
|
150
|
+
sections.push('---');
|
|
151
|
+
sections.push('');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return sections.join('\n');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ── Console Summary ──────────────────────────────────────────────────────────
|
|
158
|
+
|
|
159
|
+
function printSummary(results, reportPath) {
|
|
160
|
+
console.log('\n=================================================================');
|
|
161
|
+
console.log(' CLC FORGE — EDUCATIONAL AUDIT SUMMARY');
|
|
162
|
+
console.log('=================================================================\n');
|
|
163
|
+
|
|
164
|
+
for (const result of results) {
|
|
165
|
+
const icon = result.skipped ? '⏭️' : result.exitCode === 0 ? '✅' : '❌';
|
|
166
|
+
const label = result.name.replace(/_/g, ' ');
|
|
167
|
+
const msg = result.data?.message || '';
|
|
168
|
+
console.log(` ${icon} ${label}: ${msg}`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
console.log(`\n📄 Full report: ${reportPath}`);
|
|
172
|
+
console.log('=================================================================\n');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ── Main ─────────────────────────────────────────────────────────────────────
|
|
176
|
+
|
|
177
|
+
async function main(targetDir) {
|
|
178
|
+
const dir = targetDir || process.cwd();
|
|
179
|
+
const sddDir = getActiveSddDir();
|
|
180
|
+
|
|
181
|
+
console.log('\nRunning CLC Forge audit...\n');
|
|
182
|
+
|
|
183
|
+
const guardPaths = discoverGuards();
|
|
184
|
+
|
|
185
|
+
if (guardPaths.length === 0) {
|
|
186
|
+
console.log('No guard tools found in tools/. Nothing to audit.');
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
console.log(`Discovered ${guardPaths.length} guard(s): ${guardPaths.map(p => path.basename(p, '.js')).join(', ')}\n`);
|
|
191
|
+
|
|
192
|
+
const results = [];
|
|
193
|
+
for (const guardPath of guardPaths) {
|
|
194
|
+
const result = await runGuard(guardPath, dir);
|
|
195
|
+
results.push(result);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Generate report
|
|
199
|
+
const reportContent = generateReport(results, sddDir);
|
|
200
|
+
const reportPath = path.join(sddDir, '05-audit-report.md');
|
|
201
|
+
fs.writeFileSync(reportPath, reportContent, 'utf-8');
|
|
202
|
+
|
|
203
|
+
// Print summary
|
|
204
|
+
printSummary(results, reportPath);
|
|
205
|
+
|
|
206
|
+
// Exit with failure if any guard failed (not skipped)
|
|
207
|
+
const hasFailure = results.some(r => !r.skipped && r.exitCode !== 0);
|
|
208
|
+
process.exit(hasFailure ? 1 : 0);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Support both require() and direct execution
|
|
212
|
+
if (require.main === module) {
|
|
213
|
+
main(process.argv[2]).catch(err => {
|
|
214
|
+
console.error(`Audit orchestrator error: ${err.message}`);
|
|
215
|
+
process.exit(0); // Graceful degradation
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
module.exports = { main, getActiveSddDir, discoverGuards, runGuard };
|