@kabran-tecnologia/kabran-config 1.10.0 → 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/package.json +7 -1
- package/src/cli/commands/build.mjs +54 -0
- package/src/cli/commands/check.mjs +107 -0
- package/src/cli/commands/ci.mjs +109 -0
- package/src/cli/commands/test.mjs +133 -0
- package/src/cli/kabran.mjs +144 -0
- package/src/core/config-loader.mjs +305 -0
- package/src/schemas/ci-result.v2.schema.json +1 -1
- package/src/scripts/ci/ci-core.sh +1 -1
- package/src/scripts/ci-result-history.mjs +2 -2
- package/src/scripts/ci-result-utils.mjs +1 -1
- package/src/scripts/deploy/deploy-core.sh +1 -1
- package/src/scripts/env-validator.mjs +12 -11
- package/src/scripts/generate-ci-result.mjs +3 -3
- package/src/scripts/quality-standard-validator.mjs +24 -13
- package/src/scripts/readme-validator.mjs +35 -19
- package/src/scripts/setup.mjs +1 -0
- package/src/scripts/traceability/coverage-report.sh +1 -1
- package/src/scripts/traceability/traceability-core.sh +1 -1
- package/src/scripts/traceability/validate-traceability.sh +1 -1
- package/src/telemetry/README.md +11 -11
- package/src/telemetry/config/defaults.mjs +5 -7
- package/src/telemetry/shared/types.d.ts +1 -1
- package/templates/config/.prettierignore +35 -0
- package/templates/config/kabran.config.mjs +53 -0
- package/CI-CD-MIGRATION.md +0 -388
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kabran Config Loader
|
|
3
|
+
*
|
|
4
|
+
* Loads project configuration from kabran.config.mjs/js/json with fallback to defaults.
|
|
5
|
+
* Enables projects to customize validator behavior without modifying kabran-config.
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Smart detection of tools (ESLint, TypeScript, Prettier, Vitest, Playwright)
|
|
9
|
+
* - Automatic Doppler integration for secrets injection in tests
|
|
10
|
+
* - Convention over configuration - works out of the box
|
|
11
|
+
*
|
|
12
|
+
* @module config-loader
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import {existsSync, readFileSync} from 'node:fs';
|
|
16
|
+
import {readFile} from 'node:fs/promises';
|
|
17
|
+
import {join} from 'node:path';
|
|
18
|
+
import {pathToFileURL} from 'node:url';
|
|
19
|
+
import {execSync} from 'node:child_process';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Default configuration values.
|
|
23
|
+
* These match the current hardcoded values in validators to ensure backwards compatibility.
|
|
24
|
+
*/
|
|
25
|
+
export const DEFAULTS = {
|
|
26
|
+
readme: {
|
|
27
|
+
required: ['Installation', 'Usage', 'License'],
|
|
28
|
+
recommended: ['Development', 'Testing', 'Contributing'],
|
|
29
|
+
},
|
|
30
|
+
env: {
|
|
31
|
+
requireExample: true,
|
|
32
|
+
detectPatterns: ['process.env', 'import.meta.env', 'Deno.env', 'os.getenv', '$_ENV'],
|
|
33
|
+
},
|
|
34
|
+
quality: {
|
|
35
|
+
standardPath: 'docs/quality/001-quality-standard.md',
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Config file patterns for each tool.
|
|
41
|
+
*/
|
|
42
|
+
const TOOL_CONFIGS = {
|
|
43
|
+
eslint: ['eslint.config.mjs', 'eslint.config.js', 'eslint.config.cjs', '.eslintrc.js', '.eslintrc.json', '.eslintrc.cjs'],
|
|
44
|
+
typescript: ['tsconfig.json'],
|
|
45
|
+
prettier: ['prettier.config.mjs', 'prettier.config.js', '.prettierrc', '.prettierrc.json', '.prettierrc.js'],
|
|
46
|
+
vitest: ['vitest.config.ts', 'vitest.config.mts', 'vitest.config.js', 'vitest.config.mjs'],
|
|
47
|
+
playwright: ['playwright.config.ts', 'playwright.config.js'],
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Check if any of the config files exist for a tool.
|
|
52
|
+
* @param {string} cwd - Working directory
|
|
53
|
+
* @param {string[]} configFiles - List of possible config file names
|
|
54
|
+
* @returns {boolean} True if any config file exists
|
|
55
|
+
*/
|
|
56
|
+
function hasToolConfig(cwd, configFiles) {
|
|
57
|
+
return configFiles.some(file => existsSync(join(cwd, file)));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Check if Doppler is configured for the current directory.
|
|
62
|
+
* @param {string} cwd - Working directory
|
|
63
|
+
* @returns {boolean} True if Doppler token is configured
|
|
64
|
+
*/
|
|
65
|
+
function hasDopplerConfigured(cwd) {
|
|
66
|
+
try {
|
|
67
|
+
const result = execSync('doppler configure get token', {
|
|
68
|
+
cwd,
|
|
69
|
+
stdio: 'pipe',
|
|
70
|
+
encoding: 'utf-8',
|
|
71
|
+
timeout: 5000,
|
|
72
|
+
});
|
|
73
|
+
return result.trim().length > 0;
|
|
74
|
+
} catch {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Wrap command with Doppler if configured.
|
|
81
|
+
* @param {string} cmd - Command to wrap
|
|
82
|
+
* @param {boolean} useDoppler - Whether to use Doppler
|
|
83
|
+
* @returns {string} Wrapped command
|
|
84
|
+
*/
|
|
85
|
+
export function wrapWithDoppler(cmd, useDoppler) {
|
|
86
|
+
return useDoppler ? `doppler run -- ${cmd}` : cmd;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Detect available tools and generate smart defaults.
|
|
91
|
+
* Only configures commands for tools that are actually set up in the project.
|
|
92
|
+
* @param {string} cwd - Working directory
|
|
93
|
+
* @returns {object} Detected defaults for CLI commands
|
|
94
|
+
*/
|
|
95
|
+
export function detectToolDefaults(cwd) {
|
|
96
|
+
const defaults = {
|
|
97
|
+
check: {},
|
|
98
|
+
test: {},
|
|
99
|
+
build: {},
|
|
100
|
+
ci: { steps: [] },
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const hasDoppler = hasDopplerConfigured(cwd);
|
|
104
|
+
|
|
105
|
+
// L1: Static Analysis
|
|
106
|
+
if (hasToolConfig(cwd, TOOL_CONFIGS.eslint)) {
|
|
107
|
+
defaults.check.lint = 'npx eslint .';
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (hasToolConfig(cwd, TOOL_CONFIGS.typescript)) {
|
|
111
|
+
defaults.check.types = 'npx tsc --noEmit';
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (hasToolConfig(cwd, TOOL_CONFIGS.prettier)) {
|
|
115
|
+
defaults.check.format = 'npx prettier --check .';
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// L2: Unit Tests (with Doppler support)
|
|
119
|
+
if (hasToolConfig(cwd, TOOL_CONFIGS.vitest)) {
|
|
120
|
+
const cmd = 'npx vitest run';
|
|
121
|
+
defaults.test.unit = {
|
|
122
|
+
command: wrapWithDoppler(cmd, hasDoppler),
|
|
123
|
+
doppler: hasDoppler,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// L3: Integration Tests (with Doppler support)
|
|
128
|
+
const integrationConfig = join(cwd, 'vitest.integration.config.ts');
|
|
129
|
+
if (existsSync(integrationConfig)) {
|
|
130
|
+
const cmd = 'npx vitest run --config vitest.integration.config.ts';
|
|
131
|
+
defaults.test.integration = {
|
|
132
|
+
command: wrapWithDoppler(cmd, hasDoppler),
|
|
133
|
+
doppler: hasDoppler,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// L4: E2E Tests (with Doppler support)
|
|
138
|
+
if (hasToolConfig(cwd, TOOL_CONFIGS.playwright)) {
|
|
139
|
+
const cmd = 'npx playwright test';
|
|
140
|
+
defaults.test.e2e = {
|
|
141
|
+
command: wrapWithDoppler(cmd, hasDoppler),
|
|
142
|
+
doppler: hasDoppler,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Build - check if npm run build exists in package.json
|
|
147
|
+
const packageJsonPath = join(cwd, 'package.json');
|
|
148
|
+
if (existsSync(packageJsonPath)) {
|
|
149
|
+
try {
|
|
150
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
|
|
151
|
+
if (packageJson.scripts?.build) {
|
|
152
|
+
defaults.build.command = 'npm run build';
|
|
153
|
+
}
|
|
154
|
+
} catch {
|
|
155
|
+
// Ignore parse errors
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// CI steps - add detected tools
|
|
160
|
+
if (Object.keys(defaults.check).length > 0) {
|
|
161
|
+
defaults.ci.steps.push('check');
|
|
162
|
+
}
|
|
163
|
+
if (defaults.test.unit) {
|
|
164
|
+
defaults.ci.steps.push('test:unit');
|
|
165
|
+
}
|
|
166
|
+
if (defaults.build.command) {
|
|
167
|
+
defaults.ci.steps.push('build');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return defaults;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Configuration file names to search for, in priority order.
|
|
175
|
+
*/
|
|
176
|
+
const CONFIG_FILES = ['kabran.config.mjs', 'kabran.config.js', 'kabran.config.json'];
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Deep merge two objects. Arrays are replaced, not merged.
|
|
180
|
+
* @param {object} target - Base object
|
|
181
|
+
* @param {object} source - Object to merge in
|
|
182
|
+
* @returns {object} Merged object
|
|
183
|
+
*/
|
|
184
|
+
function deepMerge(target, source) {
|
|
185
|
+
const result = {...target};
|
|
186
|
+
|
|
187
|
+
for (const key of Object.keys(source)) {
|
|
188
|
+
const sourceValue = source[key];
|
|
189
|
+
const targetValue = target[key];
|
|
190
|
+
|
|
191
|
+
if (sourceValue === null || sourceValue === undefined) {
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (
|
|
196
|
+
typeof sourceValue === 'object' &&
|
|
197
|
+
!Array.isArray(sourceValue) &&
|
|
198
|
+
typeof targetValue === 'object' &&
|
|
199
|
+
!Array.isArray(targetValue)
|
|
200
|
+
) {
|
|
201
|
+
result[key] = deepMerge(targetValue, sourceValue);
|
|
202
|
+
} else {
|
|
203
|
+
result[key] = sourceValue;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return result;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Load JSON configuration file.
|
|
212
|
+
* @param {string} configPath - Path to JSON config file
|
|
213
|
+
* @returns {Promise<object>} Parsed configuration
|
|
214
|
+
*/
|
|
215
|
+
async function loadJsonConfig(configPath) {
|
|
216
|
+
const content = await readFile(configPath, 'utf-8');
|
|
217
|
+
return JSON.parse(content);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Load JavaScript/ESM configuration file.
|
|
222
|
+
* @param {string} configPath - Path to JS/MJS config file
|
|
223
|
+
* @returns {Promise<object>} Parsed configuration
|
|
224
|
+
*/
|
|
225
|
+
async function loadJsConfig(configPath) {
|
|
226
|
+
const configUrl = pathToFileURL(configPath).href;
|
|
227
|
+
const module = await import(configUrl);
|
|
228
|
+
return module.default || module;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Load project configuration with fallback to defaults.
|
|
233
|
+
*
|
|
234
|
+
* Configuration priority (highest to lowest):
|
|
235
|
+
* 1. Project config (kabran.config.mjs/js/json)
|
|
236
|
+
* 2. Detected tool defaults (based on existing config files)
|
|
237
|
+
* 3. Static defaults (validators)
|
|
238
|
+
*
|
|
239
|
+
* Features:
|
|
240
|
+
* - Smart detection of ESLint, TypeScript, Prettier, Vitest, Playwright
|
|
241
|
+
* - Automatic Doppler integration for secrets in tests
|
|
242
|
+
* - Project-specific overrides take precedence
|
|
243
|
+
*
|
|
244
|
+
* @param {string} [cwd=process.cwd()] - Directory to search for config
|
|
245
|
+
* @returns {Promise<object>} Merged configuration
|
|
246
|
+
*
|
|
247
|
+
* @example
|
|
248
|
+
* // Zero-config: auto-detects tools in project
|
|
249
|
+
* const config = await loadConfig();
|
|
250
|
+
* // If project has vitest.config.ts, config.test.unit.command is set
|
|
251
|
+
*
|
|
252
|
+
* @example
|
|
253
|
+
* // With custom config in kabran.config.mjs (overrides detected defaults):
|
|
254
|
+
* // export default { test: { unit: { command: 'npm test' } } }
|
|
255
|
+
* const config = await loadConfig('/path/to/project');
|
|
256
|
+
*/
|
|
257
|
+
export async function loadConfig(cwd = process.cwd()) {
|
|
258
|
+
// 1. Start with static defaults (validators)
|
|
259
|
+
let config = {...DEFAULTS};
|
|
260
|
+
|
|
261
|
+
// 2. Merge detected tool defaults (CLI commands)
|
|
262
|
+
const toolDefaults = detectToolDefaults(cwd);
|
|
263
|
+
config = deepMerge(config, toolDefaults);
|
|
264
|
+
|
|
265
|
+
// 3. Merge project-specific configuration (highest priority)
|
|
266
|
+
for (const fileName of CONFIG_FILES) {
|
|
267
|
+
const configPath = join(cwd, fileName);
|
|
268
|
+
|
|
269
|
+
if (existsSync(configPath)) {
|
|
270
|
+
try {
|
|
271
|
+
let projectConfig;
|
|
272
|
+
|
|
273
|
+
if (fileName.endsWith('.json')) {
|
|
274
|
+
projectConfig = await loadJsonConfig(configPath);
|
|
275
|
+
} else {
|
|
276
|
+
projectConfig = await loadJsConfig(configPath);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return deepMerge(config, projectConfig);
|
|
280
|
+
} catch (error) {
|
|
281
|
+
// Log warning but continue with detected defaults
|
|
282
|
+
console.warn(`Warning: Failed to load ${fileName}: ${error.message}`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return config;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Check if a config file exists in the given directory.
|
|
292
|
+
* @param {string} [cwd=process.cwd()] - Directory to check
|
|
293
|
+
* @returns {{exists: boolean, path?: string, name?: string}} Config file info
|
|
294
|
+
*/
|
|
295
|
+
export function findConfigFile(cwd = process.cwd()) {
|
|
296
|
+
for (const fileName of CONFIG_FILES) {
|
|
297
|
+
const configPath = join(cwd, fileName);
|
|
298
|
+
|
|
299
|
+
if (existsSync(configPath)) {
|
|
300
|
+
return {exists: true, path: configPath, name: fileName};
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return {exists: false};
|
|
305
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
-
"$id": "
|
|
3
|
+
"$id": "ci-result.v2.json",
|
|
4
4
|
"title": "CI Result Schema v2",
|
|
5
5
|
"description": "Unified schema for CI/CD results with quality metrics, timing, and observability data",
|
|
6
6
|
"type": "object",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# ==============================================================================
|
|
3
3
|
# Kabran CI Core - Shared Functions
|
|
4
|
-
# Part of @kabran-
|
|
4
|
+
# Part of @kabran-tecnologia/kabran-config
|
|
5
5
|
# ==============================================================================
|
|
6
6
|
|
|
7
7
|
# Version
|
|
@@ -51,7 +51,7 @@ export function extractHistoryEntry(result) {
|
|
|
51
51
|
export function loadHistory(filePath) {
|
|
52
52
|
if (!existsSync(filePath)) {
|
|
53
53
|
return {
|
|
54
|
-
$schema: '
|
|
54
|
+
$schema: 'ci-result-history.json',
|
|
55
55
|
version: '1.0.0',
|
|
56
56
|
meta: {
|
|
57
57
|
created_at: new Date().toISOString(),
|
|
@@ -75,7 +75,7 @@ export function loadHistory(filePath) {
|
|
|
75
75
|
} catch (error) {
|
|
76
76
|
// If file is corrupted, start fresh
|
|
77
77
|
return {
|
|
78
|
-
$schema: '
|
|
78
|
+
$schema: 'ci-result-history.json',
|
|
79
79
|
version: '1.0.0',
|
|
80
80
|
meta: {
|
|
81
81
|
created_at: new Date().toISOString(),
|
|
@@ -436,7 +436,7 @@ export function createMinimalResult({ projectName, passed = true }) {
|
|
|
436
436
|
const now = new Date().toISOString()
|
|
437
437
|
|
|
438
438
|
return {
|
|
439
|
-
$schema: '
|
|
439
|
+
$schema: 'ci-result.v2.json',
|
|
440
440
|
version: '1.0.0',
|
|
441
441
|
meta: {
|
|
442
442
|
generated_at: now,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# ==============================================================================
|
|
3
3
|
# Kabran Deploy Core - Shared Functions
|
|
4
|
-
# Part of @kabran-
|
|
4
|
+
# Part of @kabran-tecnologia/kabran-config
|
|
5
5
|
# ==============================================================================
|
|
6
6
|
|
|
7
7
|
# ==============================================================================
|
|
@@ -18,9 +18,13 @@ import {exec} from 'node:child_process';
|
|
|
18
18
|
import {promisify} from 'node:util';
|
|
19
19
|
import fs from 'node:fs';
|
|
20
20
|
import path from 'node:path';
|
|
21
|
+
import {loadConfig, DEFAULTS} from '../core/config-loader.mjs';
|
|
21
22
|
|
|
22
23
|
const execAsync = promisify(exec);
|
|
23
24
|
|
|
25
|
+
// Default patterns for backwards compatibility
|
|
26
|
+
export const DEFAULT_DETECT_PATTERNS = DEFAULTS.env.detectPatterns;
|
|
27
|
+
|
|
24
28
|
/**
|
|
25
29
|
* Check if .env file is tracked in git (CRITICAL SECURITY ISSUE)
|
|
26
30
|
* @param {string} [cwd] - Directory to check (defaults to process.cwd())
|
|
@@ -40,17 +44,10 @@ export async function checkEnvInGit(cwd = process.cwd()) {
|
|
|
40
44
|
/**
|
|
41
45
|
* Detect if project uses environment variables
|
|
42
46
|
* @param {string} [cwd] - Directory to check (defaults to process.cwd())
|
|
47
|
+
* @param {string[]} [patterns] - Patterns to search for (defaults to config patterns)
|
|
43
48
|
* @returns {Promise<{usesEnv: boolean, files: string[]}>}
|
|
44
49
|
*/
|
|
45
|
-
export async function detectEnvUsage(cwd = process.cwd()) {
|
|
46
|
-
const patterns = [
|
|
47
|
-
'process.env', // Node.js
|
|
48
|
-
'os.getenv', // Python
|
|
49
|
-
'import.meta.env', // Vite/ESM
|
|
50
|
-
'Deno.env', // Deno
|
|
51
|
-
'$_ENV', // PHP
|
|
52
|
-
];
|
|
53
|
-
|
|
50
|
+
export async function detectEnvUsage(cwd = process.cwd(), patterns = DEFAULT_DETECT_PATTERNS) {
|
|
54
51
|
const extensions = ['js', 'ts', 'jsx', 'tsx', 'mjs', 'cjs', 'py', 'php'];
|
|
55
52
|
const extensionPattern = extensions.join(',');
|
|
56
53
|
|
|
@@ -157,6 +154,9 @@ export async function validateEnv(cwd = process.cwd(), silent = false) {
|
|
|
157
154
|
const errors = [];
|
|
158
155
|
const warnings = [];
|
|
159
156
|
|
|
157
|
+
// Load project config
|
|
158
|
+
const config = await loadConfig(cwd);
|
|
159
|
+
|
|
160
160
|
// CRITICAL: Check if .env is committed to git
|
|
161
161
|
log('Checking for .env in git...');
|
|
162
162
|
const envInGit = await checkEnvInGit(cwd);
|
|
@@ -171,7 +171,7 @@ export async function validateEnv(cwd = process.cwd(), silent = false) {
|
|
|
171
171
|
|
|
172
172
|
// Detect if project uses environment variables
|
|
173
173
|
log('Detecting environment variable usage...');
|
|
174
|
-
const {usesEnv, files} = await detectEnvUsage(cwd);
|
|
174
|
+
const {usesEnv, files} = await detectEnvUsage(cwd, config.env.detectPatterns);
|
|
175
175
|
|
|
176
176
|
if (!usesEnv) {
|
|
177
177
|
log(' No environment variable usage detected');
|
|
@@ -236,9 +236,10 @@ export async function validateEnv(cwd = process.cwd(), silent = false) {
|
|
|
236
236
|
* @returns {Promise<Object>} Check result for ci-result.json
|
|
237
237
|
*/
|
|
238
238
|
export async function getEnvCheckResult(cwd = process.cwd()) {
|
|
239
|
+
const config = await loadConfig(cwd);
|
|
239
240
|
const envInGit = await checkEnvInGit(cwd);
|
|
240
241
|
const envExample = checkEnvExampleExists(cwd);
|
|
241
|
-
const {usesEnv} = await detectEnvUsage(cwd);
|
|
242
|
+
const {usesEnv} = await detectEnvUsage(cwd, config.env.detectPatterns);
|
|
242
243
|
|
|
243
244
|
let undocumented = [];
|
|
244
245
|
if (envExample.exists) {
|
|
@@ -103,7 +103,7 @@ async function runValidators(projectRoot, options = {}) {
|
|
|
103
103
|
|
|
104
104
|
if (!options.skipReadme) {
|
|
105
105
|
try {
|
|
106
|
-
results.readme = getReadmeCheckResult(projectRoot)
|
|
106
|
+
results.readme = await getReadmeCheckResult(projectRoot)
|
|
107
107
|
} catch (err) {
|
|
108
108
|
results.readme = { status: 'fail', error: err.message }
|
|
109
109
|
}
|
|
@@ -119,7 +119,7 @@ async function runValidators(projectRoot, options = {}) {
|
|
|
119
119
|
|
|
120
120
|
if (!options.skipQualityStandard) {
|
|
121
121
|
try {
|
|
122
|
-
results.quality_standard = getQualityStandardCheckResult(projectRoot)
|
|
122
|
+
results.quality_standard = await getQualityStandardCheckResult(projectRoot)
|
|
123
123
|
} catch (err) {
|
|
124
124
|
results.quality_standard = { status: 'fail', error: err.message }
|
|
125
125
|
}
|
|
@@ -216,7 +216,7 @@ export function generateCiResult(input) {
|
|
|
216
216
|
|
|
217
217
|
// Build result object
|
|
218
218
|
const result = {
|
|
219
|
-
$schema: '
|
|
219
|
+
$schema: 'ci-result.v2.json',
|
|
220
220
|
version: '1.0.0',
|
|
221
221
|
|
|
222
222
|
meta,
|
|
@@ -16,6 +16,10 @@
|
|
|
16
16
|
|
|
17
17
|
import {existsSync, readFileSync} from 'node:fs';
|
|
18
18
|
import {join} from 'node:path';
|
|
19
|
+
import {loadConfig, DEFAULTS} from '../core/config-loader.mjs';
|
|
20
|
+
|
|
21
|
+
// Default path for backwards compatibility
|
|
22
|
+
export const DEFAULT_STANDARD_PATH = DEFAULTS.quality.standardPath;
|
|
19
23
|
|
|
20
24
|
/**
|
|
21
25
|
* Required sections that must be present in quality-standard.md
|
|
@@ -33,10 +37,11 @@ export const REQUIRED_FRONTMATTER = ['title', 'type', 'status'];
|
|
|
33
37
|
/**
|
|
34
38
|
* Find quality-standard.md file in the project
|
|
35
39
|
* @param {string} cwd - Current working directory
|
|
40
|
+
* @param {string} [standardPath] - Path to quality standard file (relative to cwd)
|
|
36
41
|
* @returns {{exists: boolean, path?: string} | null} File info or null
|
|
37
42
|
*/
|
|
38
|
-
export function findQualityStandard(cwd = process.cwd()) {
|
|
39
|
-
const expectedPath = join(cwd,
|
|
43
|
+
export function findQualityStandard(cwd = process.cwd(), standardPath = DEFAULT_STANDARD_PATH) {
|
|
44
|
+
const expectedPath = join(cwd, standardPath);
|
|
40
45
|
|
|
41
46
|
if (existsSync(expectedPath)) {
|
|
42
47
|
return {
|
|
@@ -183,26 +188,30 @@ export function compareOverrides(documented, code) {
|
|
|
183
188
|
* Validate quality-standard.md
|
|
184
189
|
* @param {string} cwd - Current working directory
|
|
185
190
|
* @param {boolean} silent - Suppress console output (for testing)
|
|
186
|
-
* @returns {{valid: boolean, errors: string[], warnings: string[]}} Validation result
|
|
191
|
+
* @returns {Promise<{valid: boolean, errors: string[], warnings: string[]}>} Validation result
|
|
187
192
|
*/
|
|
188
|
-
export function validate(cwd = process.cwd(), silent = false) {
|
|
193
|
+
export async function validate(cwd = process.cwd(), silent = false) {
|
|
189
194
|
const log = silent ? () => {} : console.log.bind(console);
|
|
190
195
|
const error = silent ? () => {} : console.error.bind(console);
|
|
191
196
|
|
|
192
197
|
const errors = [];
|
|
193
198
|
const warnings = [];
|
|
194
199
|
|
|
200
|
+
// Load project config
|
|
201
|
+
const config = await loadConfig(cwd);
|
|
202
|
+
const standardPath = config.quality.standardPath;
|
|
203
|
+
|
|
195
204
|
log('');
|
|
196
205
|
log('Validating Quality Standard...');
|
|
197
206
|
log('='.repeat(50));
|
|
198
207
|
|
|
199
208
|
// 1. Check file exists
|
|
200
|
-
const fileInfo = findQualityStandard(cwd);
|
|
209
|
+
const fileInfo = findQualityStandard(cwd, standardPath);
|
|
201
210
|
|
|
202
211
|
if (!fileInfo) {
|
|
203
|
-
errors.push(
|
|
212
|
+
errors.push(`Missing required file: ${standardPath}`);
|
|
204
213
|
error('');
|
|
205
|
-
error(
|
|
214
|
+
error(`ERROR: Missing required file: ${standardPath}`);
|
|
206
215
|
error('');
|
|
207
216
|
error('Run "npx kabran-setup" to create it, or create manually.');
|
|
208
217
|
log('='.repeat(50));
|
|
@@ -210,7 +219,7 @@ export function validate(cwd = process.cwd(), silent = false) {
|
|
|
210
219
|
}
|
|
211
220
|
|
|
212
221
|
log('');
|
|
213
|
-
log(
|
|
222
|
+
log(`File: ${standardPath}`);
|
|
214
223
|
log(' Status: Found');
|
|
215
224
|
|
|
216
225
|
// 2. Read and parse content
|
|
@@ -317,10 +326,12 @@ export function validate(cwd = process.cwd(), silent = false) {
|
|
|
317
326
|
/**
|
|
318
327
|
* Get quality standard validation result in ci-result.json format
|
|
319
328
|
* @param {string} [cwd] - Directory to validate (defaults to process.cwd())
|
|
320
|
-
* @returns {Object} Check result for ci-result.json
|
|
329
|
+
* @returns {Promise<Object>} Check result for ci-result.json
|
|
321
330
|
*/
|
|
322
|
-
export function getQualityStandardCheckResult(cwd = process.cwd()) {
|
|
323
|
-
const
|
|
331
|
+
export async function getQualityStandardCheckResult(cwd = process.cwd()) {
|
|
332
|
+
const config = await loadConfig(cwd);
|
|
333
|
+
const standardPath = config.quality.standardPath;
|
|
334
|
+
const fileInfo = findQualityStandard(cwd, standardPath);
|
|
324
335
|
|
|
325
336
|
if (!fileInfo) {
|
|
326
337
|
return {
|
|
@@ -372,11 +383,11 @@ if (isMainModule) {
|
|
|
372
383
|
const cwd = args.find(a => !a.startsWith('--')) || process.cwd();
|
|
373
384
|
|
|
374
385
|
if (jsonOutput) {
|
|
375
|
-
const result = getQualityStandardCheckResult(cwd);
|
|
386
|
+
const result = await getQualityStandardCheckResult(cwd);
|
|
376
387
|
console.log(JSON.stringify(result, null, 2));
|
|
377
388
|
process.exit(result.status === 'fail' ? 1 : 0);
|
|
378
389
|
} else {
|
|
379
|
-
const result = validate(cwd);
|
|
390
|
+
const result = await validate(cwd);
|
|
380
391
|
process.exit(result.valid ? 0 : 1);
|
|
381
392
|
}
|
|
382
393
|
} catch (err) {
|
|
@@ -16,21 +16,27 @@
|
|
|
16
16
|
|
|
17
17
|
import fs from 'node:fs';
|
|
18
18
|
import path from 'node:path';
|
|
19
|
+
import {loadConfig, DEFAULTS} from '../core/config-loader.mjs';
|
|
19
20
|
|
|
20
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Build section patterns from config array.
|
|
23
|
+
* @param {string[]} sections - Section names
|
|
24
|
+
* @returns {Array<{pattern: RegExp, name: string}>}
|
|
25
|
+
*/
|
|
26
|
+
function buildSectionPatterns(sections) {
|
|
27
|
+
return sections.map(name => ({
|
|
28
|
+
pattern: new RegExp(`^##\\s+${name}`, 'mi'),
|
|
29
|
+
name,
|
|
30
|
+
}));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Default sections for backwards compatibility (used when importing directly)
|
|
21
34
|
export const REQUIRED_SECTIONS = [
|
|
22
35
|
{pattern: /^#\s+.+/m, name: 'Project Title (# Heading)'},
|
|
23
|
-
|
|
24
|
-
{pattern: /^##\s+Usage/mi, name: 'Usage'},
|
|
25
|
-
{pattern: /^##\s+License/mi, name: 'License'},
|
|
36
|
+
...buildSectionPatterns(DEFAULTS.readme.required),
|
|
26
37
|
];
|
|
27
38
|
|
|
28
|
-
|
|
29
|
-
export const RECOMMENDED_SECTIONS = [
|
|
30
|
-
{pattern: /^##\s+Development/mi, name: 'Development'},
|
|
31
|
-
{pattern: /^##\s+Contributing/mi, name: 'Contributing'},
|
|
32
|
-
{pattern: /^##\s+Testing/mi, name: 'Testing'},
|
|
33
|
-
];
|
|
39
|
+
export const RECOMMENDED_SECTIONS = buildSectionPatterns(DEFAULTS.readme.recommended);
|
|
34
40
|
|
|
35
41
|
/**
|
|
36
42
|
* Find README.md in directory
|
|
@@ -64,9 +70,9 @@ export function checkSection(content, section) {
|
|
|
64
70
|
* Main validation function
|
|
65
71
|
* @param {string} [cwd] - Directory to validate (defaults to process.cwd())
|
|
66
72
|
* @param {boolean} [silent] - Suppress console output
|
|
67
|
-
* @returns {{valid: boolean, errors: string[], warnings: string[]}}
|
|
73
|
+
* @returns {Promise<{valid: boolean, errors: string[], warnings: string[]}>}
|
|
68
74
|
*/
|
|
69
|
-
export function validateReadme(cwd = process.cwd(), silent = false) {
|
|
75
|
+
export async function validateReadme(cwd = process.cwd(), silent = false) {
|
|
70
76
|
const log = silent ? () => {} : console.log.bind(console);
|
|
71
77
|
const error = silent ? () => {} : console.error.bind(console);
|
|
72
78
|
|
|
@@ -75,6 +81,16 @@ export function validateReadme(cwd = process.cwd(), silent = false) {
|
|
|
75
81
|
const errors = [];
|
|
76
82
|
const warnings = [];
|
|
77
83
|
|
|
84
|
+
// Load project config
|
|
85
|
+
const config = await loadConfig(cwd);
|
|
86
|
+
|
|
87
|
+
// Build section patterns from config
|
|
88
|
+
const requiredSections = [
|
|
89
|
+
{pattern: /^#\s+.+/m, name: 'Project Title (# Heading)'},
|
|
90
|
+
...buildSectionPatterns(config.readme.required),
|
|
91
|
+
];
|
|
92
|
+
const recommendedSections = buildSectionPatterns(config.readme.recommended);
|
|
93
|
+
|
|
78
94
|
// Check if README exists
|
|
79
95
|
const readme = findReadme(cwd);
|
|
80
96
|
if (!readme) {
|
|
@@ -90,7 +106,7 @@ export function validateReadme(cwd = process.cwd(), silent = false) {
|
|
|
90
106
|
|
|
91
107
|
// Check required sections
|
|
92
108
|
log('Checking required sections:');
|
|
93
|
-
for (const section of
|
|
109
|
+
for (const section of requiredSections) {
|
|
94
110
|
const exists = checkSection(content, section);
|
|
95
111
|
if (exists) {
|
|
96
112
|
log(` OK ${section.name}`);
|
|
@@ -102,7 +118,7 @@ export function validateReadme(cwd = process.cwd(), silent = false) {
|
|
|
102
118
|
|
|
103
119
|
// Check recommended sections
|
|
104
120
|
log('\nChecking recommended sections:');
|
|
105
|
-
for (const section of
|
|
121
|
+
for (const section of recommendedSections) {
|
|
106
122
|
const exists = checkSection(content, section);
|
|
107
123
|
if (exists) {
|
|
108
124
|
log(` OK ${section.name}`);
|
|
@@ -134,10 +150,10 @@ export function validateReadme(cwd = process.cwd(), silent = false) {
|
|
|
134
150
|
/**
|
|
135
151
|
* Get README validation result in ci-result.json format
|
|
136
152
|
* @param {string} [cwd] - Directory to validate (defaults to process.cwd())
|
|
137
|
-
* @returns {Object} Check result for ci-result.json
|
|
153
|
+
* @returns {Promise<Object>} Check result for ci-result.json
|
|
138
154
|
*/
|
|
139
|
-
export function getReadmeCheckResult(cwd = process.cwd()) {
|
|
140
|
-
const result = validateReadme(cwd, true);
|
|
155
|
+
export async function getReadmeCheckResult(cwd = process.cwd()) {
|
|
156
|
+
const result = await validateReadme(cwd, true);
|
|
141
157
|
|
|
142
158
|
// Determine status
|
|
143
159
|
let status = 'pass';
|
|
@@ -169,11 +185,11 @@ if (isMainModule) {
|
|
|
169
185
|
const cwd = args.find(a => !a.startsWith('--')) || process.cwd();
|
|
170
186
|
|
|
171
187
|
if (jsonOutput) {
|
|
172
|
-
const result = getReadmeCheckResult(cwd);
|
|
188
|
+
const result = await getReadmeCheckResult(cwd);
|
|
173
189
|
console.log(JSON.stringify(result, null, 2));
|
|
174
190
|
process.exit(result.status === 'fail' ? 1 : 0);
|
|
175
191
|
} else {
|
|
176
|
-
const result = validateReadme(cwd);
|
|
192
|
+
const result = await validateReadme(cwd);
|
|
177
193
|
process.exit(result.valid ? 0 : 1);
|
|
178
194
|
}
|
|
179
195
|
} catch (err) {
|
package/src/scripts/setup.mjs
CHANGED
|
@@ -427,6 +427,7 @@ export function setupConfigs(projectDir, templatesDir, options) {
|
|
|
427
427
|
// Other configs (same for all types)
|
|
428
428
|
const otherConfigs = [
|
|
429
429
|
{name: 'prettier.config.mjs', src: 'prettier.config.mjs'},
|
|
430
|
+
{name: '.prettierignore', src: '.prettierignore'},
|
|
430
431
|
{name: 'commitlint.config.mjs', src: 'commitlint.config.mjs'},
|
|
431
432
|
{name: 'lint-staged.config.mjs', src: 'lint-staged.config.mjs'},
|
|
432
433
|
];
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# ==============================================================================
|
|
3
3
|
# Kabran Traceability Coverage Report
|
|
4
|
-
# Part of @kabran-
|
|
4
|
+
# Part of @kabran-tecnologia/kabran-config
|
|
5
5
|
# Implements PROP-006: JSDoc Traceability Tags
|
|
6
6
|
#
|
|
7
7
|
# Generates a report of spec coverage in the codebase.
|