clckernel 1.2.18 → 2.0.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.
- package/.agents/skills/clckernel_cli/SKILL.md +23 -6
- package/.agents/skills/clckernel_cli/reference/audit.md +13 -3
- package/.agents/skills/clckernel_cli/reference/commit.md +1 -1
- package/.agents/skills/clckernel_cli/reference/doctor.md +5 -3
- package/.agents/skills/clckernel_cli/reference/feature.md +3 -3
- package/.agents/skills/clckernel_cli/reference/guard.md +48 -12
- package/.agents/skills/clckernel_cli/reference/phase.md +6 -3
- package/.agents/skills/clckernel_cli/reference/start.md +14 -4
- package/.agents/skills/clckernel_cli/reference/test.md +1 -1
- package/.agents/skills/clckernel_cli/reference/update.md +19 -19
- package/CHANGELOG.md +244 -0
- package/LICENSE +21 -0
- package/README.es.md +280 -51
- package/README.md +274 -50
- package/bin/cli.js +8 -0
- package/package.json +12 -2
- package/src/adapters/agent-os.js +161 -84
- package/src/adapters/astro.js +8 -6
- package/src/adapters/base.js +27 -46
- package/src/adapters/django.js +14 -13
- package/src/adapters/fastapi.js +16 -15
- package/src/adapters/go.js +11 -9
- package/src/adapters/laravel.js +12 -10
- package/src/adapters/nextjs.js +7 -5
- package/src/adapters/rails.js +13 -11
- package/src/adapters/rust.js +11 -9
- package/src/catalog.js +58 -49
- package/src/config.js +38 -240
- package/src/detector.js +243 -44
- package/src/doctor.js +18 -9
- package/src/generator.js +737 -309
- package/src/gitignore.js +144 -0
- package/src/index.js +257 -18
- package/src/io.js +409 -0
- package/src/known-hashes.json +309 -0
- package/src/onboarding.js +15 -0
- package/src/technologies/detector.js +1 -1
- package/src/technologies/guards/_shared.js +1 -1
- package/src/ui.js +233 -40
- package/src/updater.js +271 -36
- package/tools/_changes.js +279 -0
- package/tools/_changes.py +233 -0
- package/tools/_config.js +294 -0
- package/tools/_config.py +271 -0
- package/tools/_yaml.py +489 -0
- package/tools/audit.js +511 -111
- package/tools/audit.py +506 -128
- package/tools/celery_guard.py +13 -35
- package/tools/check_a11y.js +8 -28
- package/tools/check_api_contracts.js +13 -39
- package/tools/check_architecture.js +35 -44
- package/tools/check_architecture.py +172 -105
- package/tools/check_custom.js +108 -61
- package/tools/check_custom.py +108 -271
- package/tools/check_data_files.js +88 -0
- package/tools/check_db_efficiency.py +80 -65
- package/tools/check_hitl.js +112 -69
- package/tools/check_links.js +138 -0
- package/tools/check_migrations.py +143 -94
- package/tools/check_performance.js +35 -44
- package/tools/check_placeholders.js +104 -0
- package/tools/check_responsive.js +8 -28
- package/tools/check_scope.py +40 -31
- package/tools/check_seo.js +8 -28
- package/tools/check_skills.js +158 -0
- package/tools/check_storybook.js +8 -21
- package/tools/check_symlinks.js +40 -22
- package/tools/check_ui_reuse.js +17 -29
- package/tools/docker_guard.py +19 -41
- package/tools/postgres_guard.py +15 -36
- package/tools/redis_guard.py +13 -35
- package/tools/scan_secrets.js +134 -67
- package/tools/scan_secrets.py +163 -72
- package/tools/verify_tdd.js +867 -0
- package/tools/verify_tdd.py +407 -165
- package/tools/yaml.js +404 -132
- package/tools/check_domain_data.js +0 -100
- package/tools/check_test_calendar_sync.js +0 -111
package/tools/check_custom.js
CHANGED
|
@@ -1,47 +1,71 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
3
|
* Custom Rules Guard
|
|
4
|
-
* Reads .
|
|
4
|
+
* Reads .clckernel.yaml and executes user-defined regex rules against changed files.
|
|
5
5
|
* Self-contained: uses only Node.js built-ins (inline YAML parser, no npm deps).
|
|
6
6
|
*
|
|
7
7
|
* @param {string} targetDir - Directory to scan (defaults to cwd)
|
|
8
8
|
* @returns {{ exitCode: number, skipped: boolean, name: string, data: object }}
|
|
9
9
|
*/
|
|
10
10
|
const { parseYaml, globToRegex } = require('./yaml.js');
|
|
11
|
+
const { changedFiles, readFile } = require('./_changes');
|
|
12
|
+
|
|
13
|
+
// Lookup order: the canonical name first, then older names kept for compatibility.
|
|
14
|
+
const CONFIG_NAMES = ['.clckernel.yaml', '.clckernel.yml', '.clc-forge.yaml', '.clc-forge.yml'];
|
|
15
|
+
const CANONICAL_CONFIG = CONFIG_NAMES[0];
|
|
16
|
+
const DEPRECATED_PREFIX = '.clc-forge.';
|
|
17
|
+
|
|
11
18
|
const check = async function checkCustom(targetDir) {
|
|
12
19
|
const fs = require('fs');
|
|
13
20
|
const path = require('path');
|
|
14
|
-
const { execSync } = require('child_process');
|
|
15
21
|
|
|
16
22
|
const dir = targetDir || process.cwd();
|
|
17
23
|
|
|
18
24
|
// ── 1. Load config ──────────────────────────────────────────────────
|
|
19
|
-
const
|
|
20
|
-
const configPath = candidates.map(c => path.join(dir, c)).find(p => fs.existsSync(p));
|
|
25
|
+
const configPath = CONFIG_NAMES.map(c => path.join(dir, c)).find(p => fs.existsSync(p));
|
|
21
26
|
if (!configPath) {
|
|
22
27
|
return {
|
|
23
28
|
exitCode: 0, skipped: true, name: 'check_custom',
|
|
24
|
-
data: { violations: {}, message:
|
|
29
|
+
data: { violations: {}, message: `No ${CANONICAL_CONFIG} found` },
|
|
25
30
|
};
|
|
26
31
|
}
|
|
27
32
|
|
|
33
|
+
const configName = path.basename(configPath);
|
|
34
|
+
const result = runRules(dir, configPath, configName);
|
|
35
|
+
if (configName.startsWith(DEPRECATED_PREFIX)) {
|
|
36
|
+
result.data.warnings = [`${configName}: deprecated config name, rename to ${CANONICAL_CONFIG}`];
|
|
37
|
+
}
|
|
38
|
+
return result;
|
|
39
|
+
};
|
|
40
|
+
module.exports = check;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Validate the rules in one config file and run them against changed files.
|
|
44
|
+
* @returns {{ exitCode: number, skipped: boolean, name: string, data: object }}
|
|
45
|
+
*/
|
|
46
|
+
function runRules(dir, configPath, configName) {
|
|
47
|
+
const fs = require('fs');
|
|
48
|
+
|
|
49
|
+
const configError = (message) => ({
|
|
50
|
+
exitCode: 1, skipped: false, name: 'check_custom',
|
|
51
|
+
data: { violations: {}, message: `Config error in ${configName}: ${message}` },
|
|
52
|
+
});
|
|
53
|
+
|
|
28
54
|
let config;
|
|
29
55
|
try {
|
|
30
56
|
const raw = fs.readFileSync(configPath, 'utf-8');
|
|
31
57
|
config = parseYaml(raw);
|
|
32
58
|
} catch (e) {
|
|
59
|
+
// Fail closed: rules we cannot read must not turn into a silent PASS.
|
|
33
60
|
return {
|
|
34
|
-
exitCode:
|
|
35
|
-
data: { violations: {}, message: `Config parse error: ${e.message}` },
|
|
61
|
+
exitCode: 1, skipped: false, name: 'check_custom',
|
|
62
|
+
data: { violations: {}, message: `Config parse error in ${configName}: ${e.message}` },
|
|
36
63
|
};
|
|
37
64
|
}
|
|
38
65
|
|
|
39
66
|
const rules = config && config.rules;
|
|
40
|
-
if (rules !== undefined && !Array.isArray(rules)) {
|
|
41
|
-
return
|
|
42
|
-
exitCode: 0, skipped: true, name: 'check_custom',
|
|
43
|
-
data: { violations: {}, message: 'Config error: "rules" must be a list' },
|
|
44
|
-
};
|
|
67
|
+
if (rules !== undefined && rules !== null && !Array.isArray(rules)) {
|
|
68
|
+
return configError('"rules" must be a list');
|
|
45
69
|
}
|
|
46
70
|
if (!Array.isArray(rules) || rules.length === 0) {
|
|
47
71
|
return {
|
|
@@ -50,54 +74,47 @@ const check = async function checkCustom(targetDir) {
|
|
|
50
74
|
};
|
|
51
75
|
}
|
|
52
76
|
|
|
53
|
-
// ── 2. Validate rules:
|
|
77
|
+
// ── 2. Validate rules: shape and regex; any invalid rule fails the guard ──
|
|
54
78
|
const validated = [];
|
|
55
|
-
|
|
56
|
-
|
|
79
|
+
const errors = [];
|
|
80
|
+
rules.forEach((rule, i) => {
|
|
81
|
+
const problems = validateRule(rule);
|
|
82
|
+
const label = rule && typeof rule.name === 'string' ? `"${rule.name}"` : `#${i + 1}`;
|
|
83
|
+
if (problems.length > 0) {
|
|
84
|
+
errors.push(`rule ${label}: ${problems.join(', ')}`);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
let regex;
|
|
57
88
|
try {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
severity: rule.severity || 'error',
|
|
63
|
-
exclude: rule.exclude || [],
|
|
64
|
-
regex: new RegExp(rule.pattern, 'gm'),
|
|
65
|
-
scopeRe: globToRegex(rule.scope || '**/*'),
|
|
66
|
-
excludeRe: (rule.exclude || []).map(globToRegex),
|
|
67
|
-
});
|
|
68
|
-
} catch {
|
|
69
|
-
// Invalid regex — skip this rule silently
|
|
89
|
+
regex = new RegExp(rule.pattern, 'gm');
|
|
90
|
+
} catch (e) {
|
|
91
|
+
errors.push(`rule ${label}: invalid pattern: ${e.message}`);
|
|
92
|
+
return;
|
|
70
93
|
}
|
|
71
|
-
|
|
94
|
+
validated.push({
|
|
95
|
+
name: rule.name,
|
|
96
|
+
pattern: rule.pattern,
|
|
97
|
+
message: rule.message,
|
|
98
|
+
severity: rule.severity || 'error',
|
|
99
|
+
exclude: rule.exclude || [],
|
|
100
|
+
regex,
|
|
101
|
+
scopeRe: globToRegex(rule.scope || '**/*'),
|
|
102
|
+
excludeRe: (rule.exclude || []).map(globToRegex),
|
|
103
|
+
});
|
|
104
|
+
});
|
|
72
105
|
|
|
73
|
-
if (
|
|
106
|
+
if (errors.length > 0) return configError(errors.join('; '));
|
|
107
|
+
|
|
108
|
+
// ── 3. Changed files of the snapshot being verified ──────────────
|
|
109
|
+
// The index, or HEAD when nothing is staged; never the working copy.
|
|
110
|
+
const { mode, files } = changedFiles(dir);
|
|
111
|
+
if (mode === null) {
|
|
74
112
|
return {
|
|
75
|
-
exitCode: 0, skipped:
|
|
76
|
-
data: { violations: {}, message: 'No
|
|
113
|
+
exitCode: 0, skipped: true, name: 'check_custom',
|
|
114
|
+
data: { violations: {}, message: 'No git diff available' },
|
|
77
115
|
};
|
|
78
116
|
}
|
|
79
117
|
|
|
80
|
-
// ── 3. Get changed files via git diff ───────────────────────────────
|
|
81
|
-
let files;
|
|
82
|
-
try {
|
|
83
|
-
const out = execSync('git diff --name-only HEAD~1', {
|
|
84
|
-
cwd: dir, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'],
|
|
85
|
-
});
|
|
86
|
-
files = out.split('\n').filter(f => f.trim());
|
|
87
|
-
} catch {
|
|
88
|
-
try {
|
|
89
|
-
const out = execSync('git diff --name-only --cached', {
|
|
90
|
-
cwd: dir, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'],
|
|
91
|
-
});
|
|
92
|
-
files = out.split('\n').filter(f => f.trim());
|
|
93
|
-
} catch {
|
|
94
|
-
return {
|
|
95
|
-
exitCode: 0, skipped: true, name: 'check_custom',
|
|
96
|
-
data: { violations: {}, message: 'No git diff available' },
|
|
97
|
-
};
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
118
|
if (!files || files.length === 0) {
|
|
102
119
|
return {
|
|
103
120
|
exitCode: 0, skipped: false, name: 'check_custom',
|
|
@@ -110,13 +127,8 @@ const check = async function checkCustom(targetDir) {
|
|
|
110
127
|
let hasError = false;
|
|
111
128
|
|
|
112
129
|
for (const file of files) {
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
try {
|
|
116
|
-
content = fs.readFileSync(filePath, 'utf-8');
|
|
117
|
-
} catch {
|
|
118
|
-
continue; // permission denied or unreadable — skip
|
|
119
|
-
}
|
|
130
|
+
const content = readFile(dir, file, mode);
|
|
131
|
+
if (content === null) continue; // missing or binary in the snapshot — skip
|
|
120
132
|
|
|
121
133
|
for (const rule of validated) {
|
|
122
134
|
if (!rule.scopeRe.test(file)) continue;
|
|
@@ -125,6 +137,9 @@ const check = async function checkCustom(targetDir) {
|
|
|
125
137
|
rule.regex.lastIndex = 0;
|
|
126
138
|
let match;
|
|
127
139
|
while ((match = rule.regex.exec(content)) !== null) {
|
|
140
|
+
// An empty match does not move lastIndex: step over one code point
|
|
141
|
+
// (never between surrogates) so the loop ends, like Python's finditer.
|
|
142
|
+
if (match[0] === '') rule.regex.lastIndex = nextCodePointIndex(content, rule.regex.lastIndex);
|
|
128
143
|
const lineNo = content.slice(0, match.index).split('\n').length;
|
|
129
144
|
if (!violations[file]) violations[file] = [];
|
|
130
145
|
violations[file].push({
|
|
@@ -151,11 +166,43 @@ const check = async function checkCustom(targetDir) {
|
|
|
151
166
|
: 'No custom rule violations',
|
|
152
167
|
},
|
|
153
168
|
};
|
|
154
|
-
}
|
|
155
|
-
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Index just past the code point at `i` (two UTF-16 units for a surrogate pair).
|
|
173
|
+
* @param {string} s
|
|
174
|
+
* @param {number} i
|
|
175
|
+
* @returns {number}
|
|
176
|
+
*/
|
|
177
|
+
function nextCodePointIndex(s, i) {
|
|
178
|
+
const code = s.codePointAt(i);
|
|
179
|
+
return i + (code !== undefined && code > 0xffff ? 2 : 1);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Shape problems of one rule (empty list when valid).
|
|
184
|
+
* @param {*} rule
|
|
185
|
+
* @returns {string[]}
|
|
186
|
+
*/
|
|
187
|
+
function validateRule(rule) {
|
|
188
|
+
if (!rule || typeof rule !== 'object' || Array.isArray(rule)) return ['must be a mapping'];
|
|
189
|
+
const problems = [];
|
|
190
|
+
for (const field of ['name', 'pattern', 'message']) {
|
|
191
|
+
if (typeof rule[field] !== 'string' || rule[field] === '') problems.push(`"${field}" must be a non-empty string`);
|
|
192
|
+
}
|
|
193
|
+
if (rule.severity != null && !['error', 'warning'].includes(rule.severity)) {
|
|
194
|
+
problems.push('"severity" must be "error" or "warning"');
|
|
195
|
+
}
|
|
196
|
+
if (rule.scope != null && typeof rule.scope !== 'string') problems.push('"scope" must be a string');
|
|
197
|
+
if (rule.exclude != null && !(Array.isArray(rule.exclude) && rule.exclude.every(e => typeof e === 'string'))) {
|
|
198
|
+
problems.push('"exclude" must be a list of strings');
|
|
199
|
+
}
|
|
200
|
+
return problems;
|
|
201
|
+
}
|
|
156
202
|
|
|
157
203
|
if (require.main === module) {
|
|
158
204
|
check(process.argv[2]).then(result => {
|
|
205
|
+
for (const warning of result.data.warnings || []) console.error(`[check_custom] warning: ${warning}`);
|
|
159
206
|
console.log(JSON.stringify(result, null, 2));
|
|
160
207
|
process.exit(result.exitCode);
|
|
161
208
|
});
|