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,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLC Forge — TechnologyDetector
|
|
3
|
+
* Detects Phase-1 technologies (Celery, Redis, PostgreSQL, Docker) via file signatures.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {'celery' | 'redis' | 'postgresql' | 'docker'} Technology
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* TechnologyDetector scans a project root for Phase-1 technology signatures.
|
|
15
|
+
*/
|
|
16
|
+
class TechnologyDetector {
|
|
17
|
+
/**
|
|
18
|
+
* Detect which Phase-1 technologies are present in targetDir.
|
|
19
|
+
* @param {string} targetDir - Project root path
|
|
20
|
+
* @returns {Technology[]} Array of detected technology identifiers
|
|
21
|
+
*/
|
|
22
|
+
detect(targetDir) {
|
|
23
|
+
/** @type {Technology[]} */
|
|
24
|
+
const detected = [];
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
if (!fs.existsSync(targetDir)) {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Celery detection
|
|
32
|
+
if (this._detectCelery(targetDir)) {
|
|
33
|
+
detected.push('celery');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Redis detection
|
|
37
|
+
if (this._detectRedis(targetDir)) {
|
|
38
|
+
detected.push('redis');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// PostgreSQL detection
|
|
42
|
+
if (this._detectPostgresql(targetDir)) {
|
|
43
|
+
detected.push('postgresql');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Docker detection
|
|
47
|
+
if (this._detectDocker(targetDir)) {
|
|
48
|
+
detected.push('docker');
|
|
49
|
+
}
|
|
50
|
+
} catch (err) {
|
|
51
|
+
console.warn(`[TechnologyDetector] Error during detection: ${err.message}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return detected;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* @param {string} targetDir
|
|
59
|
+
* @returns {boolean}
|
|
60
|
+
*/
|
|
61
|
+
_detectCelery(targetDir) {
|
|
62
|
+
// Check requirements.txt
|
|
63
|
+
if (this._matchInFile(targetDir, 'requirements.txt', 'celery')) {
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Check pyproject.toml
|
|
68
|
+
if (this._matchInFile(targetDir, 'pyproject.toml', 'celery')) {
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Check celery.py or celeryconfig.py
|
|
73
|
+
const celeryFiles = ['celery.py', 'celeryconfig.py', 'celery_app.py'];
|
|
74
|
+
for (const file of celeryFiles) {
|
|
75
|
+
if (fs.existsSync(path.join(targetDir, file))) {
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* @param {string} targetDir
|
|
85
|
+
* @returns {boolean}
|
|
86
|
+
*/
|
|
87
|
+
_detectRedis(targetDir) {
|
|
88
|
+
// Check requirements.txt
|
|
89
|
+
if (this._matchInFile(targetDir, 'requirements.txt', 'redis')) {
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Check pyproject.toml
|
|
94
|
+
if (this._matchInFile(targetDir, 'pyproject.toml', 'redis')) {
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Check redis.conf
|
|
99
|
+
if (fs.existsSync(path.join(targetDir, 'redis.conf'))) {
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Check for redis:// URL in .env files
|
|
104
|
+
if (this._containsRedisUrl(targetDir)) {
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* @param {string} targetDir
|
|
113
|
+
* @returns {boolean}
|
|
114
|
+
*/
|
|
115
|
+
_detectPostgresql(targetDir) {
|
|
116
|
+
// Check for psycopg2, asyncpg, or psycopg2-binary
|
|
117
|
+
const pgPackages = ['psycopg2', 'asyncpg', 'psycopg2-binary'];
|
|
118
|
+
|
|
119
|
+
for (const pkg of pgPackages) {
|
|
120
|
+
if (this._matchInFile(targetDir, 'requirements.txt', pkg)) {
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
if (this._matchInFile(targetDir, 'pyproject.toml', pkg)) {
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* @param {string} targetDir
|
|
133
|
+
* @returns {boolean}
|
|
134
|
+
*/
|
|
135
|
+
_detectDocker(targetDir) {
|
|
136
|
+
if (fs.existsSync(path.join(targetDir, 'Dockerfile'))) {
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
if (fs.existsSync(path.join(targetDir, 'docker-compose.yml'))) {
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Check if a file exists and contains a case-insensitive substring.
|
|
147
|
+
* @param {string} targetDir
|
|
148
|
+
* @param {string} filename
|
|
149
|
+
* @param {string} pattern
|
|
150
|
+
* @returns {boolean}
|
|
151
|
+
*/
|
|
152
|
+
_matchInFile(targetDir, filename, pattern) {
|
|
153
|
+
const filePath = path.join(targetDir, filename);
|
|
154
|
+
if (!fs.existsSync(filePath)) {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
try {
|
|
158
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
159
|
+
return content.toLowerCase().includes(pattern.toLowerCase());
|
|
160
|
+
} catch (err) {
|
|
161
|
+
console.warn(`[TechnologyDetector] Could not read ${filePath}: ${err.message}`);
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Check for redis:// URL in .env files.
|
|
168
|
+
* @param {string} targetDir
|
|
169
|
+
* @returns {boolean}
|
|
170
|
+
*/
|
|
171
|
+
_containsRedisUrl(targetDir) {
|
|
172
|
+
try {
|
|
173
|
+
const entries = fs.readdirSync(targetDir);
|
|
174
|
+
for (const entry of entries) {
|
|
175
|
+
if (entry === '.env' || entry.startsWith('.env.')) {
|
|
176
|
+
const filePath = path.join(targetDir, entry);
|
|
177
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
178
|
+
if (content.toLowerCase().includes('redis://')) {
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
} catch (err) {
|
|
184
|
+
// Ignore read errors
|
|
185
|
+
}
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Returns tool manifest entries for detected technologies.
|
|
191
|
+
* @param {Technology[]} technologies
|
|
192
|
+
* @returns {Array<{name: string, language: string, path: string, command: string, description: string}>}
|
|
193
|
+
*/
|
|
194
|
+
getToolsForTech(technologies) {
|
|
195
|
+
/** @type {Array<{name: string, language: string, path: string, command: string, description: string}>} */
|
|
196
|
+
const tools = [];
|
|
197
|
+
|
|
198
|
+
for (const tech of technologies) {
|
|
199
|
+
switch (tech) {
|
|
200
|
+
case 'celery':
|
|
201
|
+
tools.push({
|
|
202
|
+
name: 'celery_guard',
|
|
203
|
+
language: 'py',
|
|
204
|
+
path: 'celery_guard.py',
|
|
205
|
+
command: 'python3 tools/celery_guard.py',
|
|
206
|
+
description: 'Celery guard: broker URL exposure, missing ignore_result on @shared_task, hardcoded result backend',
|
|
207
|
+
});
|
|
208
|
+
break;
|
|
209
|
+
case 'redis':
|
|
210
|
+
tools.push({
|
|
211
|
+
name: 'redis_guard',
|
|
212
|
+
language: 'py',
|
|
213
|
+
path: 'redis_guard.py',
|
|
214
|
+
command: 'python3 tools/redis_guard.py',
|
|
215
|
+
description: 'Redis guard: decode_responses without password check, missing socket_connect_timeout, hardcoded Redis URLs',
|
|
216
|
+
});
|
|
217
|
+
break;
|
|
218
|
+
case 'postgresql':
|
|
219
|
+
tools.push({
|
|
220
|
+
name: 'postgres_guard',
|
|
221
|
+
language: 'py',
|
|
222
|
+
path: 'postgres_guard.py',
|
|
223
|
+
command: 'python3 tools/postgres_guard.py',
|
|
224
|
+
description: 'PostgreSQL guard: raw SQL with string formatting, hstore raw usage',
|
|
225
|
+
});
|
|
226
|
+
break;
|
|
227
|
+
case 'docker':
|
|
228
|
+
tools.push({
|
|
229
|
+
name: 'docker_guard',
|
|
230
|
+
language: 'py',
|
|
231
|
+
path: 'docker_guard.py',
|
|
232
|
+
command: 'python3 tools/docker_guard.py',
|
|
233
|
+
description: 'Docker guard: non-root USER, ENV secrets exposure, missing HEALTHCHECK',
|
|
234
|
+
});
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return tools;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
module.exports = { TechnologyDetector };
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLC Forge — Shared Guard Utilities
|
|
3
|
+
* Common file-reading and result-formatting helpers for technology guards.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Read file content if it exists, otherwise return null.
|
|
11
|
+
* @param {string} filePath
|
|
12
|
+
* @returns {string | null}
|
|
13
|
+
*/
|
|
14
|
+
function readFileIfExists(filePath) {
|
|
15
|
+
if (!fs.existsSync(filePath)) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
return fs.readFileSync(filePath, 'utf-8');
|
|
20
|
+
} catch (err) {
|
|
21
|
+
console.warn(`[_shared] Could not read ${filePath}: ${err.message}`);
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Check if a pattern matches in file content (case-insensitive).
|
|
28
|
+
* @param {string} pattern - Substring to search for
|
|
29
|
+
* @param {string | null} content - File content or null
|
|
30
|
+
* @returns {boolean}
|
|
31
|
+
*/
|
|
32
|
+
function matchInFile(pattern, content) {
|
|
33
|
+
if (content === null) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
return content.toLowerCase().includes(pattern.toLowerCase());
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Format a standard guard result.
|
|
41
|
+
* @param {string} name - Guard name
|
|
42
|
+
* @param {Array<object>} violations - List of violation findings
|
|
43
|
+
* @param {boolean} skipped - Whether guard was skipped
|
|
44
|
+
* @param {string} [message] - Optional human-readable summary
|
|
45
|
+
* @returns {object} Standard guard result
|
|
46
|
+
*/
|
|
47
|
+
function formatResult(name, violations, skipped, message) {
|
|
48
|
+
const hasViolations = violations.length > 0;
|
|
49
|
+
const exitCode = hasViolations ? 1 : 0;
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
exit_code: exitCode,
|
|
53
|
+
skipped: skipped,
|
|
54
|
+
name: name,
|
|
55
|
+
data: {
|
|
56
|
+
findings: violations,
|
|
57
|
+
message: message || (skipped
|
|
58
|
+
? 'Guard skipped'
|
|
59
|
+
: hasViolations
|
|
60
|
+
? `${violations.length} violation(s) found`
|
|
61
|
+
: 'No violations found'),
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
module.exports = { readFileIfExists, matchInFile, formatResult };
|
package/src/ui.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLC Kernel — Pro Terminal UI & ANSI Renderer
|
|
3
|
+
* Brand Identity: The AI Agent Governance & Quality Engineering Engine
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const readline = require('readline');
|
|
7
|
+
|
|
8
|
+
// ANSI Color Palette
|
|
9
|
+
const colors = {
|
|
10
|
+
reset: '\x1b[0m',
|
|
11
|
+
bold: '\x1b[1m',
|
|
12
|
+
dim: '\x1b[2m',
|
|
13
|
+
italic: '\x1b[3m',
|
|
14
|
+
cyan: '\x1b[36m',
|
|
15
|
+
emerald: '\x1b[32m',
|
|
16
|
+
violet: '\x1b[35m',
|
|
17
|
+
amber: '\x1b[33m',
|
|
18
|
+
red: '\x1b[31m',
|
|
19
|
+
blue: '\x1b[34m',
|
|
20
|
+
gray: '\x1b[90m',
|
|
21
|
+
bgCyan: '\x1b[46m\x1b[30m',
|
|
22
|
+
bgViolet: '\x1b[45m\x1b[30m',
|
|
23
|
+
bgEmerald: '\x1b[42m\x1b[30m'
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
function printBanner() {
|
|
27
|
+
console.clear();
|
|
28
|
+
console.log(`${colors.cyan}
|
|
29
|
+
██████╗██╗ ██████╗ ██╗ ██╗███████╗██████╗ ███╗ ██╗███████╗██╗
|
|
30
|
+
██╔════╝██║ ██╔════╝ ██║ ██╔╝██╔════╝██╔══██╗████╗ ██║██╔════╝██║
|
|
31
|
+
██║ ██║ ██║ █████╗█████═╝ █████╗ ██████╔╝██╔██╗ ██║█████╗ ██║
|
|
32
|
+
██║ ██║ ██║ ╚════╝██╔═██╗ ██╔══╝ ██╔══██╗██║╚██╗██║██╔══╝ ██║
|
|
33
|
+
╚██████╗███████╗╚██████╗ ██║ ╚██╗███████╗██║ ██║██║ ╚████║███████╗███████╗
|
|
34
|
+
╚═════╝╚══════╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═══╝╚══════╝╚══════╝
|
|
35
|
+
${colors.reset}`);
|
|
36
|
+
console.log(` ${colors.bgCyan} CLC KERNEL ${colors.reset} ${colors.bold}The AI Agent Governance & Quality Engineering Engine${colors.reset} ${colors.gray}v1.2.4${colors.reset}\n`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function printBox(title, items) {
|
|
40
|
+
const width = 68;
|
|
41
|
+
console.log(`${colors.dim}┌${'─'.repeat(width)}┐${colors.reset}`);
|
|
42
|
+
console.log(`${colors.dim}│${colors.reset} ${colors.bold}${colors.cyan}${title.padEnd(width - 4)}${colors.reset} ${colors.dim}│${colors.reset}`);
|
|
43
|
+
console.log(`${colors.dim}├${'─'.repeat(width)}┤${colors.reset}`);
|
|
44
|
+
|
|
45
|
+
items.forEach(({ label, value, badgeColor = colors.emerald }) => {
|
|
46
|
+
const line = ` ${colors.gray}${label.padEnd(22)}:${colors.reset} ${badgeColor}${colors.bold}${value}${colors.reset}`;
|
|
47
|
+
const plainLength = `${label} : ${value}`.length;
|
|
48
|
+
const padding = Math.max(0, width - plainLength - 3);
|
|
49
|
+
console.log(`${colors.dim}│${colors.reset} ${line}${' '.repeat(padding)}${colors.dim}│${colors.reset}`);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
console.log(`${colors.dim}└${'─'.repeat(width)}┘${colors.reset}\n`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function promptOptions(detected) {
|
|
56
|
+
return new Promise((resolve) => {
|
|
57
|
+
const rl = readline.createInterface({
|
|
58
|
+
input: process.stdin,
|
|
59
|
+
output: process.stdout
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
console.log(`${colors.bold} \x1b[32m[1]\x1b[0m Confirmar e Instalar CLC Kernel ${colors.dim}(Recomendado)${colors.reset}`);
|
|
63
|
+
console.log(` \x1b[33m[2]\x1b[0m Forzar modo FRONTEND`);
|
|
64
|
+
console.log(` \x1b[33m[3]\x1b[0m Forzar modo BACKEND`);
|
|
65
|
+
console.log(` \x1b[90m[4] Cancelar${colors.reset}\n`);
|
|
66
|
+
|
|
67
|
+
rl.question(`${colors.cyan}${colors.bold}➔ Seleccioná una opción [1]: ${colors.reset}`, (answer) => {
|
|
68
|
+
const choice = answer.trim() || '1';
|
|
69
|
+
rl.close();
|
|
70
|
+
|
|
71
|
+
if (choice === '1') {
|
|
72
|
+
resolve(detected);
|
|
73
|
+
} else if (choice === '2') {
|
|
74
|
+
detected.projectType = 'frontend';
|
|
75
|
+
resolve(detected);
|
|
76
|
+
} else if (choice === '3') {
|
|
77
|
+
detected.projectType = 'backend';
|
|
78
|
+
resolve(detected);
|
|
79
|
+
} else {
|
|
80
|
+
console.log(`\n${colors.red}❌ Instalación de CLC Kernel cancelada.${colors.reset}`);
|
|
81
|
+
process.exit(0);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function printSuccess(targetDir, isFront, config) {
|
|
88
|
+
console.log(`\n${colors.bgEmerald} SUCCESS ${colors.reset} ${colors.bold}${colors.emerald}¡CLC Kernel instalado exitosamente!${colors.reset}\n`);
|
|
89
|
+
console.log(` 🔨 ${colors.bold}Proyecto Inicializado:${colors.reset} ${colors.cyan}${targetDir}${colors.reset}`);
|
|
90
|
+
console.log(` 🛡️ ${colors.bold}Salvaguardas:${colors.reset} ${isFront ? `${colors.emerald}10 Guardias Frontend (A11y, Zod, Next/Image, UI Reuse, Storybook)` : `${colors.violet}7 Guardias Backend (TDD, Architecture, Scope, Secret Scan)`}${colors.reset}`);
|
|
91
|
+
console.log(` 📝 ${colors.bold}Leyes & SDD:${colors.reset} AGENTS.md, sdds/, docs/Journal/ precargados`);
|
|
92
|
+
console.log(` 🔒 ${colors.bold}Git Hooks:${colors.reset} Configurados en ${colors.amber}${config.gitHooks === 'husky' ? '.husky/pre-commit' : '.githooks/pre-commit'}${colors.reset}\n`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
module.exports = {
|
|
96
|
+
colors,
|
|
97
|
+
printBanner,
|
|
98
|
+
printBox,
|
|
99
|
+
promptOptions,
|
|
100
|
+
printSuccess
|
|
101
|
+
};
|
package/src/yaml.js
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLC Forge — Zero-Dependency YAML Parser
|
|
3
|
+
* Self-contained inline YAML parser extracted from tools/check_custom.js.
|
|
4
|
+
* Uses only Node.js built-ins (no npm deps).
|
|
5
|
+
*
|
|
6
|
+
* Supports: scalars, block sequences, block mappings, inline flow
|
|
7
|
+
* sequences/mappings, comments. No anchors/aliases, no multiple documents.
|
|
8
|
+
*
|
|
9
|
+
* Stack entries: { obj, indent, _parentObj, _parentKey }
|
|
10
|
+
* obj: the object we are building key-value pairs into
|
|
11
|
+
* indent: indent level (children at same indent stay)
|
|
12
|
+
* _parentObj/_parentKey: where to find/create the array for sequences
|
|
13
|
+
*
|
|
14
|
+
* @module yaml
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Detect YAML anchors (`&name`) or aliases (`*name`) on a line.
|
|
19
|
+
* @param {string} line - Trimmed content of a YAML line.
|
|
20
|
+
* @param {number} lineNo - 1-based line number for error reporting.
|
|
21
|
+
* @throws {Error} If anchor or alias syntax is found (unsupported).
|
|
22
|
+
*/
|
|
23
|
+
function assertNoAnchorAlias(line, lineNo) {
|
|
24
|
+
// YAML anchors/aliases are token-boundary markers: `&name` / `*name`
|
|
25
|
+
// preceded by whitespace or start of line. Requiring the boundary avoids
|
|
26
|
+
// false positives on regex/glob content such as `pattern: 'a*b'`.
|
|
27
|
+
if (/(?:^|\s)[&*][A-Za-z0-9_-]/.test(line)) {
|
|
28
|
+
throw new Error(
|
|
29
|
+
`Unsupported YAML syntax at line ${lineNo}: anchors/aliases are not supported`
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Parse a YAML string into a JavaScript object.
|
|
36
|
+
* Throws on anchors/aliases (unsupported).
|
|
37
|
+
* @param {string} text - Raw YAML source.
|
|
38
|
+
* @returns {object} Parsed object tree.
|
|
39
|
+
*/
|
|
40
|
+
function parseYaml(text) {
|
|
41
|
+
const lines = text.split('\n');
|
|
42
|
+
const root = {};
|
|
43
|
+
const stack = [{ obj: root, indent: -1, _parentObj: null, _parentKey: null }];
|
|
44
|
+
|
|
45
|
+
for (let i = 0; i < lines.length; i++) {
|
|
46
|
+
let line = lines[i].replace(/\s*#.*$/, '').trimEnd();
|
|
47
|
+
if (line.trim() === '' || line.trim().startsWith('#')) continue;
|
|
48
|
+
|
|
49
|
+
const indent = line.search(/\S/);
|
|
50
|
+
const trimmed = line.trim();
|
|
51
|
+
|
|
52
|
+
// New behavior: anchors/aliases are unsupported
|
|
53
|
+
assertNoAnchorAlias(trimmed, i + 1);
|
|
54
|
+
|
|
55
|
+
while (stack.length > 1 && stack[stack.length - 1].indent >= indent) {
|
|
56
|
+
stack.pop();
|
|
57
|
+
}
|
|
58
|
+
const entry = stack[stack.length - 1];
|
|
59
|
+
|
|
60
|
+
// ── Sequence item: "- rest" ──
|
|
61
|
+
if (trimmed.startsWith('- ')) {
|
|
62
|
+
const rest = trimmed.slice(2).trim();
|
|
63
|
+
|
|
64
|
+
if (rest.includes(': ')) {
|
|
65
|
+
// "- key: value" — mapping inside a sequence
|
|
66
|
+
let arr = null;
|
|
67
|
+
// Check if parent key already points to an array
|
|
68
|
+
if (entry._parentObj && entry._parentKey) {
|
|
69
|
+
const pk = entry._parentObj[entry._parentKey];
|
|
70
|
+
if (Array.isArray(pk)) {
|
|
71
|
+
arr = pk;
|
|
72
|
+
} else if (pk && typeof pk === 'object' && Object.keys(pk).length === 0) {
|
|
73
|
+
// Convert empty object to array
|
|
74
|
+
entry._parentObj[entry._parentKey] = [];
|
|
75
|
+
arr = entry._parentObj[entry._parentKey];
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (arr) {
|
|
79
|
+
const obj = {};
|
|
80
|
+
const colonIdx = rest.indexOf(': ');
|
|
81
|
+
obj[rest.slice(0, colonIdx).trim()] = parseScalar(rest.slice(colonIdx + 2).trim());
|
|
82
|
+
arr.push(obj);
|
|
83
|
+
stack.push({ obj, indent, _parentObj: arr, _parentKey: null });
|
|
84
|
+
}
|
|
85
|
+
} else if (entry._parentObj && entry._parentKey) {
|
|
86
|
+
// "- scalar" inside a sequence
|
|
87
|
+
const pk = entry._parentObj[entry._parentKey];
|
|
88
|
+
if (Array.isArray(pk)) {
|
|
89
|
+
pk.push(parseScalar(rest));
|
|
90
|
+
} else if (pk && typeof pk === 'object' && Object.keys(pk).length === 0) {
|
|
91
|
+
// Convert empty object to array (same rule as the mapping-in-sequence
|
|
92
|
+
// branch above) so plain block sequences like
|
|
93
|
+
// active_guards:
|
|
94
|
+
// - check_custom
|
|
95
|
+
// parse into arrays, not empty objects. Additive: previously valid
|
|
96
|
+
// YAML parses identically.
|
|
97
|
+
entry._parentObj[entry._parentKey] = [parseScalar(rest)];
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ── key: value pair ──
|
|
104
|
+
const kvMatch = trimmed.match(/^([^:]+):\s*(.*)$/);
|
|
105
|
+
if (kvMatch) {
|
|
106
|
+
const key = kvMatch[1].trim();
|
|
107
|
+
const val = kvMatch[2].trim();
|
|
108
|
+
|
|
109
|
+
if (val === '' || val === '|' || val === '>') {
|
|
110
|
+
// Empty value → children follow
|
|
111
|
+
if (!entry.obj[key]) entry.obj[key] = {};
|
|
112
|
+
stack.push({ obj: entry.obj[key], indent, _parentObj: entry.obj, _parentKey: key });
|
|
113
|
+
} else if (val.startsWith('[') && val.endsWith(']')) {
|
|
114
|
+
entry.obj[key] = val.slice(1, -1).split(',').map(s => parseScalar(s.trim()));
|
|
115
|
+
} else if (val.startsWith('{') && val.endsWith('}')) {
|
|
116
|
+
entry.obj[key] = parseFlowMapping(val);
|
|
117
|
+
} else {
|
|
118
|
+
entry.obj[key] = parseScalar(val);
|
|
119
|
+
}
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return root;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function parseScalar(val) {
|
|
128
|
+
if (val === '' || val === '~' || val === 'null') return null;
|
|
129
|
+
if (val === 'true') return true;
|
|
130
|
+
if (val === 'false') return false;
|
|
131
|
+
if (/^-?\d+$/.test(val)) return parseInt(val, 10);
|
|
132
|
+
if (/^-?\d+\.\d+$/.test(val)) return parseFloat(val);
|
|
133
|
+
// Strip quotes and unescape common escape sequences
|
|
134
|
+
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
|
|
135
|
+
return val.slice(1, -1).replace(/\\(.)/g, '$1');
|
|
136
|
+
}
|
|
137
|
+
return val;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function parseFlowMapping(str) {
|
|
141
|
+
const inner = str.slice(1, -1).trim();
|
|
142
|
+
if (!inner) return {};
|
|
143
|
+
const obj = {};
|
|
144
|
+
const pairs = inner.split(',');
|
|
145
|
+
for (const pair of pairs) {
|
|
146
|
+
const colonIdx = pair.indexOf(':');
|
|
147
|
+
if (colonIdx === -1) continue;
|
|
148
|
+
const k = pair.slice(0, colonIdx).trim();
|
|
149
|
+
const v = pair.slice(colonIdx + 1).trim();
|
|
150
|
+
obj[k] = parseScalar(v);
|
|
151
|
+
}
|
|
152
|
+
return obj;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
156
|
+
// Glob-to-Regex Converter (~15 lines)
|
|
157
|
+
// Handles: **, *, {a,b} alternation
|
|
158
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
159
|
+
|
|
160
|
+
function globToRegex(glob) {
|
|
161
|
+
let re = glob
|
|
162
|
+
.replace(/\./g, '\\.') // escape dots
|
|
163
|
+
.replace(/\*\*\//g, '{{GLOBSTAR_SLASH}}') // protect **/ (zero or more segments + separator)
|
|
164
|
+
.replace(/\*\*/g, '{{GLOBSTAR}}') // protect trailing **
|
|
165
|
+
.replace(/\*/g, '[^/]*') // single * = no slashes
|
|
166
|
+
.replace(/\{\{GLOBSTAR_SLASH\}\}/g, '(?:.+/)?') // **/ = optional path segments
|
|
167
|
+
.replace(/\{\{GLOBSTAR\}\}/g, '.*') // ** = anything
|
|
168
|
+
.replace(/\{([^}]+)\}/g, (_, alt) =>
|
|
169
|
+
`(${alt.split(',').join('|')})`); // {a,b} alternation
|
|
170
|
+
return new RegExp('^' + re + '$');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
module.exports = { parseYaml, globToRegex };
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
const { describe, it } = require('node:test');
|
|
2
|
+
const assert = require('node:assert');
|
|
3
|
+
const { createTmpProject, teardown } = require('../helpers');
|
|
4
|
+
|
|
5
|
+
describe('AstroAdapter', () => {
|
|
6
|
+
const AstroAdapter = require('../../src/adapters/astro');
|
|
7
|
+
|
|
8
|
+
describe('constructor', () => {
|
|
9
|
+
it('creates adapter with correct name and projectType', () => {
|
|
10
|
+
const adapter = new AstroAdapter();
|
|
11
|
+
assert.strictEqual(adapter.name, 'Astro Framework');
|
|
12
|
+
assert.strictEqual(adapter.projectType, 'frontend');
|
|
13
|
+
assert.strictEqual(adapter.testRunner, 'Vitest / Playwright');
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
describe('detect()', () => {
|
|
18
|
+
it('returns true when package.json has astro dependency', () => {
|
|
19
|
+
const dir = createTmpProject({
|
|
20
|
+
'package.json': JSON.stringify({ dependencies: { astro: '4.0.0' } }),
|
|
21
|
+
});
|
|
22
|
+
try {
|
|
23
|
+
const adapter = new AstroAdapter();
|
|
24
|
+
assert.strictEqual(adapter.detect(dir), true);
|
|
25
|
+
} finally {
|
|
26
|
+
teardown(dir);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('returns true when astro.config.mjs exists', () => {
|
|
31
|
+
const dir = createTmpProject({
|
|
32
|
+
'astro.config.mjs': 'export default {}',
|
|
33
|
+
'package.json': JSON.stringify({ dependencies: {} }),
|
|
34
|
+
});
|
|
35
|
+
try {
|
|
36
|
+
const adapter = new AstroAdapter();
|
|
37
|
+
assert.strictEqual(adapter.detect(dir), true);
|
|
38
|
+
} finally {
|
|
39
|
+
teardown(dir);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('returns true when astro.config.js exists', () => {
|
|
44
|
+
const dir = createTmpProject({
|
|
45
|
+
'astro.config.js': 'module.exports = {}',
|
|
46
|
+
'package.json': JSON.stringify({ dependencies: {} }),
|
|
47
|
+
});
|
|
48
|
+
try {
|
|
49
|
+
const adapter = new AstroAdapter();
|
|
50
|
+
assert.strictEqual(adapter.detect(dir), true);
|
|
51
|
+
} finally {
|
|
52
|
+
teardown(dir);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('returns false when no package.json exists', () => {
|
|
57
|
+
const dir = createTmpProject({});
|
|
58
|
+
try {
|
|
59
|
+
const adapter = new AstroAdapter();
|
|
60
|
+
assert.strictEqual(adapter.detect(dir), false);
|
|
61
|
+
} finally {
|
|
62
|
+
teardown(dir);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('returns false when package.json has no astro and no config file', () => {
|
|
67
|
+
const dir = createTmpProject({
|
|
68
|
+
'package.json': JSON.stringify({ dependencies: { react: '18.0.0' } }),
|
|
69
|
+
});
|
|
70
|
+
try {
|
|
71
|
+
const adapter = new AstroAdapter();
|
|
72
|
+
assert.strictEqual(adapter.detect(dir), false);
|
|
73
|
+
} finally {
|
|
74
|
+
teardown(dir);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe('getTools()', () => {
|
|
80
|
+
it('returns array of tool objects with required fields', () => {
|
|
81
|
+
const adapter = new AstroAdapter();
|
|
82
|
+
const tools = adapter.getTools();
|
|
83
|
+
assert.ok(Array.isArray(tools));
|
|
84
|
+
assert.ok(tools.length > 0);
|
|
85
|
+
for (const tool of tools) {
|
|
86
|
+
assert.ok(tool.name);
|
|
87
|
+
assert.ok(tool.language);
|
|
88
|
+
assert.ok(typeof tool.command === 'string');
|
|
89
|
+
assert.ok(tool.description);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe('getSafeguards()', () => {
|
|
95
|
+
it('returns non-empty array of strings', () => {
|
|
96
|
+
const adapter = new AstroAdapter();
|
|
97
|
+
const guards = adapter.getSafeguards();
|
|
98
|
+
assert.ok(Array.isArray(guards));
|
|
99
|
+
assert.ok(guards.length >= 5);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
});
|