docguard-cli 0.7.3 → 0.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -3
- package/cli/commands/agents.mjs +1 -1
- package/cli/commands/badge.mjs +1 -1
- package/cli/commands/ci.mjs +1 -1
- package/cli/commands/diagnose.mjs +16 -9
- package/cli/commands/diff.mjs +14 -3
- package/cli/commands/fix.mjs +4 -3
- package/cli/commands/generate.mjs +60 -2
- package/cli/commands/guard.mjs +30 -1
- package/cli/commands/hooks.mjs +1 -1
- package/cli/commands/init.mjs +107 -69
- package/cli/commands/publish.mjs +1 -1
- package/cli/commands/score.mjs +91 -27
- package/cli/commands/trace.mjs +1 -1
- package/cli/commands/watch.mjs +1 -1
- package/cli/docguard.mjs +36 -85
- package/cli/shared.mjs +106 -0
- package/cli/validators/docs-coverage.mjs +387 -0
- package/cli/validators/docs-diff.mjs +185 -0
- package/cli/validators/metadata-sync.mjs +179 -0
- package/cli/validators/metrics-consistency.mjs +166 -0
- package/cli/validators/test-spec.mjs +33 -0
- package/package.json +1 -1
- package/templates/TEST-SPEC.md.template +13 -0
- package/cli/commands/audit.mjs +0 -92
package/README.md
CHANGED
|
@@ -81,6 +81,27 @@ diagnose → AI reads prompts → AI fixes docs → guard verifies
|
|
|
81
81
|
|
|
82
82
|
---
|
|
83
83
|
|
|
84
|
+
## Usage
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
# Initialize CDD docs for your project
|
|
88
|
+
npx docguard-cli init
|
|
89
|
+
|
|
90
|
+
# Reverse-engineer docs from existing code
|
|
91
|
+
npx docguard-cli generate
|
|
92
|
+
|
|
93
|
+
# Validate project — use as CI gate
|
|
94
|
+
npx docguard-cli guard
|
|
95
|
+
|
|
96
|
+
# Get AI-actionable fix prompts
|
|
97
|
+
npx docguard-cli diagnose
|
|
98
|
+
|
|
99
|
+
# Check CDD maturity score
|
|
100
|
+
npx docguard-cli score
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
84
105
|
## 13 Commands
|
|
85
106
|
|
|
86
107
|
### 🔮 Generate — Reverse-engineer docs from code
|
|
@@ -202,7 +223,7 @@ $ npx docguard-cli guard
|
|
|
202
223
|
|
|
203
224
|
---
|
|
204
225
|
|
|
205
|
-
##
|
|
226
|
+
## 14 Validators
|
|
206
227
|
|
|
207
228
|
| # | Validator | What It Checks | Default |
|
|
208
229
|
|---|-----------|---------------|---------|
|
|
@@ -213,8 +234,15 @@ $ npx docguard-cli guard
|
|
|
213
234
|
| 5 | **Changelog** | CHANGELOG.md has [Unreleased] section | ✅ On |
|
|
214
235
|
| 6 | **Test-Spec** | Tests exist per TEST-SPEC.md rules | ✅ On |
|
|
215
236
|
| 7 | **Environment** | Env vars documented, .env.example exists | ✅ On |
|
|
216
|
-
| 8 | **Security** | No hardcoded secrets in source code |
|
|
217
|
-
| 9 | **Architecture** | Imports follow layer boundaries |
|
|
237
|
+
| 8 | **Security** | No hardcoded secrets in source code | ✅ On |
|
|
238
|
+
| 9 | **Architecture** | Imports follow layer boundaries | ✅ On |
|
|
239
|
+
| 10 | **Freshness** | Docs not stale relative to code changes | ✅ On |
|
|
240
|
+
| 11 | **Traceability** | Canonical docs linked to source code | ✅ On |
|
|
241
|
+
| 12 | **Docs-Diff** | Code artifacts match documented entities | ✅ On |
|
|
242
|
+
| 13 | **Metadata-Sync** | Version refs consistent across docs | ✅ On |
|
|
243
|
+
| 14 | **Docs-Coverage** | Code features referenced in documentation | ✅ On |
|
|
244
|
+
| 15 | **Metrics-Consistency** | Hardcoded numbers match actual counts | ✅ On |
|
|
245
|
+
| 16 | **Docs-Sync** | Source files have matching doc entries | ✅ On |
|
|
218
246
|
|
|
219
247
|
---
|
|
220
248
|
|
package/cli/commands/agents.mjs
CHANGED
package/cli/commands/badge.mjs
CHANGED
package/cli/commands/ci.mjs
CHANGED
|
@@ -13,11 +13,12 @@
|
|
|
13
13
|
* --format prompt Full AI-ready prompt (all issues combined)
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import { c } from '../
|
|
16
|
+
import { c } from '../shared.mjs';
|
|
17
17
|
import { runGuardInternal } from './guard.mjs';
|
|
18
18
|
import { runScoreInternal } from './score.mjs';
|
|
19
19
|
import { existsSync, readFileSync, mkdirSync } from 'node:fs';
|
|
20
|
-
import { resolve } from 'node:path';
|
|
20
|
+
import { resolve, dirname } from 'node:path';
|
|
21
|
+
import { fileURLToPath } from 'node:url';
|
|
21
22
|
import { execSync } from 'node:child_process';
|
|
22
23
|
|
|
23
24
|
// Map validator failures to the right fix --doc target
|
|
@@ -103,16 +104,16 @@ export function runDiagnose(projectDir, config, flags) {
|
|
|
103
104
|
// ── Step 2: Collect issues ──
|
|
104
105
|
let issues = collectIssues(guardData);
|
|
105
106
|
|
|
106
|
-
// ── Step 3: Auto-fix
|
|
107
|
-
const shouldAutoFix =
|
|
108
|
-
if (
|
|
107
|
+
// ── Step 3: Auto-fix (only with --auto flag) or suggest fixes ──
|
|
108
|
+
const shouldAutoFix = flags.auto && flags.format !== 'json';
|
|
109
|
+
if (issues.length > 0) {
|
|
109
110
|
const autoFixable = issues.filter(i => i.autoFixable);
|
|
110
111
|
const hasStructural = issues.some(i => i.validator === 'Structure');
|
|
111
112
|
|
|
112
|
-
if (hasStructural || autoFixable.length > 0) {
|
|
113
|
+
if (shouldAutoFix && (hasStructural || autoFixable.length > 0)) {
|
|
113
114
|
// Run init to create missing files
|
|
114
115
|
try {
|
|
115
|
-
const cliPath = resolve(import.meta.
|
|
116
|
+
const cliPath = resolve(dirname(fileURLToPath(import.meta.url)), '..', 'docguard.mjs');
|
|
116
117
|
execSync(`node "${cliPath}" init --dir "${projectDir}"`, {
|
|
117
118
|
encoding: 'utf-8',
|
|
118
119
|
stdio: 'pipe',
|
|
@@ -121,7 +122,7 @@ export function runDiagnose(projectDir, config, flags) {
|
|
|
121
122
|
|
|
122
123
|
// Run generate to fill in content
|
|
123
124
|
try {
|
|
124
|
-
const cliPath = resolve(import.meta.
|
|
125
|
+
const cliPath = resolve(dirname(fileURLToPath(import.meta.url)), '..', 'docguard.mjs');
|
|
125
126
|
execSync(`node "${cliPath}" generate --dir "${projectDir}" --force`, {
|
|
126
127
|
encoding: 'utf-8',
|
|
127
128
|
stdio: 'pipe',
|
|
@@ -138,6 +139,12 @@ export function runDiagnose(projectDir, config, flags) {
|
|
|
138
139
|
console.log(` ${c.green}⚡ Auto-fixed ${fixedCount} issue(s)${c.reset} (created/regenerated docs)\n`);
|
|
139
140
|
}
|
|
140
141
|
}
|
|
142
|
+
} else if (!shouldAutoFix && (hasStructural || autoFixable.length > 0) && (!flags.format || flags.format === 'text')) {
|
|
143
|
+
// Suggest-only mode: tell user what they can do
|
|
144
|
+
console.log(` ${c.yellow}💡 ${autoFixable.length + (hasStructural ? 1 : 0)} issue(s) can be auto-fixed.${c.reset} Run with ${c.cyan}--auto${c.reset} to create/regenerate docs, or manually:`);
|
|
145
|
+
if (hasStructural) console.log(` ${c.dim}docguard init --dir .${c.reset}`);
|
|
146
|
+
if (autoFixable.length > 0) console.log(` ${c.dim}docguard generate --dir . --force${c.reset}`);
|
|
147
|
+
console.log('');
|
|
141
148
|
}
|
|
142
149
|
}
|
|
143
150
|
|
|
@@ -340,7 +347,7 @@ function outputPrompt(projectDir, guardData, scoreData, issues, flags) {
|
|
|
340
347
|
lines.push('VALIDATION:');
|
|
341
348
|
lines.push('After making all fixes, run: docguard guard');
|
|
342
349
|
lines.push('Expected result: All checks pass (0 errors, 0 warnings)');
|
|
343
|
-
lines.push(`Target score: ≥${scoreData.score + 5}/100`);
|
|
350
|
+
lines.push(`Target score: ≥${Math.min(scoreData.score + 5, 100)}/100`);
|
|
344
351
|
|
|
345
352
|
// Agent-aware: add explicit checklist for basic-tier agents
|
|
346
353
|
if (agentTier === 'basic') {
|
package/cli/commands/diff.mjs
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
7
7
|
import { resolve, join, extname, basename } from 'node:path';
|
|
8
|
-
import { c } from '../
|
|
8
|
+
import { c } from '../shared.mjs';
|
|
9
9
|
|
|
10
10
|
const IGNORE_DIRS = new Set([
|
|
11
11
|
'node_modules', '.git', '.next', 'dist', 'build',
|
|
@@ -99,10 +99,13 @@ function diffRoutes(dir) {
|
|
|
99
99
|
|
|
100
100
|
// Extract route-like patterns from ARCHITECTURE.md
|
|
101
101
|
const docRoutes = new Set();
|
|
102
|
-
const routeRegex = /(?:\/api\/\S+|GET|POST|PUT|DELETE|PATCH)\s+(
|
|
102
|
+
const routeRegex = /(?:\/api\/\S+|(?:GET|POST|PUT|DELETE|PATCH)\s+(\/\S+))/gi;
|
|
103
103
|
let match;
|
|
104
104
|
while ((match = routeRegex.exec(content)) !== null) {
|
|
105
|
-
|
|
105
|
+
const route = match[1] || match[0];
|
|
106
|
+
// Skip markdown table syntax and non-route content
|
|
107
|
+
if (route.startsWith('|') || route.startsWith('(') || route.length < 3) continue;
|
|
108
|
+
docRoutes.add(route);
|
|
106
109
|
}
|
|
107
110
|
|
|
108
111
|
// Also check for paths in tables
|
|
@@ -183,6 +186,14 @@ function diffEntities(dir) {
|
|
|
183
186
|
'weighted', 'method', 'provider', 'token', 'expiry', 'role',
|
|
184
187
|
'permissions', 'secret', 'rotation', 'access', 'variable', 'tool',
|
|
185
188
|
'command', 'run', 'component', 'responsibility', 'location', 'tests',
|
|
189
|
+
// Data types — common in table schemas, not entity names
|
|
190
|
+
'string', 'boolean', 'number', 'integer', 'float', 'double', 'decimal',
|
|
191
|
+
'array', 'object', 'null', 'undefined', 'enum', 'varchar', 'text',
|
|
192
|
+
'timestamp', 'uuid', 'bigint', 'serial', 'json', 'jsonb', 'blob',
|
|
193
|
+
'char', 'date', 'time', 'datetime', 'binary', 'bit', 'money',
|
|
194
|
+
// Common table headers and template words
|
|
195
|
+
'true', 'false', 'header', 'checks', 'project', 'count', 'grade',
|
|
196
|
+
'breakdown', 'issuecount', 'autofixable', 'projectname', 'projecttype',
|
|
186
197
|
]);
|
|
187
198
|
while ((match = tableRegex.exec(content)) !== null) {
|
|
188
199
|
const name = match[1];
|
package/cli/commands/fix.mjs
CHANGED
|
@@ -14,9 +14,10 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import { existsSync, readFileSync, mkdirSync } from 'node:fs';
|
|
17
|
-
import { resolve, basename } from 'node:path';
|
|
17
|
+
import { resolve, basename, dirname } from 'node:path';
|
|
18
18
|
import { execSync } from 'node:child_process';
|
|
19
|
-
import {
|
|
19
|
+
import { fileURLToPath } from 'node:url';
|
|
20
|
+
import { c } from '../shared.mjs';
|
|
20
21
|
|
|
21
22
|
// ── Document Quality Definitions ───────────────────────────────────────────
|
|
22
23
|
// What each doc SHOULD contain, and what to look for in the codebase
|
|
@@ -471,7 +472,7 @@ function autoFixIssues(projectDir, config, issues) {
|
|
|
471
472
|
}
|
|
472
473
|
|
|
473
474
|
try {
|
|
474
|
-
const cliPath = resolve(import.meta.
|
|
475
|
+
const cliPath = resolve(dirname(fileURLToPath(import.meta.url)), '..', 'docguard.mjs');
|
|
475
476
|
execSync(`node ${cliPath} init --dir "${projectDir}"`, {
|
|
476
477
|
encoding: 'utf-8',
|
|
477
478
|
stdio: 'pipe',
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import { existsSync, readFileSync, writeFileSync, readdirSync, statSync, mkdirSync } from 'node:fs';
|
|
9
9
|
import { resolve, join, extname, basename, relative, dirname } from 'node:path';
|
|
10
|
-
import { c } from '../
|
|
10
|
+
import { c } from '../shared.mjs';
|
|
11
11
|
import { detectDocTools } from '../scanners/doc-tools.mjs';
|
|
12
12
|
import { scanRoutesDeep } from '../scanners/routes.mjs';
|
|
13
13
|
import { scanSchemasDeep, generateERDiagram } from '../scanners/schemas.mjs';
|
|
@@ -318,7 +318,7 @@ function scanProject(dir) {
|
|
|
318
318
|
}
|
|
319
319
|
});
|
|
320
320
|
|
|
321
|
-
// Find tests
|
|
321
|
+
// Find tests — top-level test dirs
|
|
322
322
|
['tests', 'test', '__tests__', 'spec', 'e2e'].forEach(testDir => {
|
|
323
323
|
const fullDir = resolve(dir, testDir);
|
|
324
324
|
if (existsSync(fullDir)) {
|
|
@@ -329,6 +329,55 @@ function scanProject(dir) {
|
|
|
329
329
|
}
|
|
330
330
|
});
|
|
331
331
|
|
|
332
|
+
// Find co-located tests: src/**/__tests__/ and src/**/*.test.* / src/**/*.spec.*
|
|
333
|
+
const srcDir = resolve(dir, 'src');
|
|
334
|
+
if (existsSync(srcDir)) {
|
|
335
|
+
walkDir(srcDir, (filePath) => {
|
|
336
|
+
const rel = relative(dir, filePath);
|
|
337
|
+
const isTestDir = rel.includes('__tests__') || rel.includes('__test__');
|
|
338
|
+
const isTestFile = /\.(test|spec)\.[^.]+$/.test(rel);
|
|
339
|
+
if ((isTestDir || isTestFile) && !scan.tests.includes(rel)) {
|
|
340
|
+
scan.tests.push(rel);
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Read vitest/jest config for custom test patterns
|
|
346
|
+
const testConfigs = ['vitest.config.ts', 'vitest.config.js', 'vitest.config.mts', 'jest.config.ts', 'jest.config.js'];
|
|
347
|
+
for (const cfgFile of testConfigs) {
|
|
348
|
+
const cfgPath = resolve(dir, cfgFile);
|
|
349
|
+
if (existsSync(cfgPath)) {
|
|
350
|
+
try {
|
|
351
|
+
const cfgContent = readFileSync(cfgPath, 'utf-8');
|
|
352
|
+
// Extract include patterns like: include: ['src/**/*.test.ts']
|
|
353
|
+
const includeMatch = cfgContent.match(/include\s*:\s*\[([^\]]+)\]/);
|
|
354
|
+
if (includeMatch) {
|
|
355
|
+
// Parse the test root from the pattern (e.g., 'src/**/*.test.ts' → 'src')
|
|
356
|
+
const patterns = includeMatch[1].match(/['"]([^'"]+)['"]/g);
|
|
357
|
+
if (patterns) {
|
|
358
|
+
for (const p of patterns) {
|
|
359
|
+
const pattern = p.replace(/['"]|\s/g, '');
|
|
360
|
+
// Extract root dir from glob (e.g., 'src/**/*.test.ts' → 'src')
|
|
361
|
+
const rootDir = pattern.split('/')[0];
|
|
362
|
+
if (rootDir && rootDir !== '**' && rootDir !== '*') {
|
|
363
|
+
const fullDir = resolve(dir, rootDir);
|
|
364
|
+
if (existsSync(fullDir)) {
|
|
365
|
+
walkDir(fullDir, (filePath) => {
|
|
366
|
+
const rel = relative(dir, filePath);
|
|
367
|
+
if (/\.(test|spec)\.[^.]+$/.test(rel) && !scan.tests.includes(rel)) {
|
|
368
|
+
scan.tests.push(rel);
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
} catch { /* config parse may fail */ }
|
|
377
|
+
break; // Use first found config
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
332
381
|
// Find components
|
|
333
382
|
['src/components', 'components', 'src/ui'].forEach(compDir => {
|
|
334
383
|
const fullDir = resolve(dir, compDir);
|
|
@@ -367,6 +416,15 @@ function scanProject(dir) {
|
|
|
367
416
|
// Count files and lines
|
|
368
417
|
countFilesAndLines(dir, scan);
|
|
369
418
|
|
|
419
|
+
// ── Filter test files out of source lists ──
|
|
420
|
+
// Test files (*.test.*, *.spec.*, __tests__/) should NOT appear as source files
|
|
421
|
+
const isTestFile = (f) => f.includes('__tests__') || f.includes('__test__') || /\.(test|spec)\.[^.]+$/.test(f);
|
|
422
|
+
scan.routes = scan.routes.filter(f => !isTestFile(f));
|
|
423
|
+
scan.models = scan.models.filter(f => !isTestFile(f));
|
|
424
|
+
scan.services = scan.services.filter(f => !isTestFile(f));
|
|
425
|
+
scan.components = scan.components.filter(f => !isTestFile(f));
|
|
426
|
+
scan.middlewares = scan.middlewares.filter(f => !isTestFile(f));
|
|
427
|
+
|
|
370
428
|
return scan;
|
|
371
429
|
}
|
|
372
430
|
|
package/cli/commands/guard.mjs
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* runGuardInternal() → returns data, no side effects (for diagnose, ci)
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { c } from '../
|
|
10
|
+
import { c } from '../shared.mjs';
|
|
11
11
|
import { validateStructure, validateDocSections } from '../validators/structure.mjs';
|
|
12
12
|
import { validateDrift } from '../validators/drift.mjs';
|
|
13
13
|
import { validateChangelog } from '../validators/changelog.mjs';
|
|
@@ -18,6 +18,10 @@ import { validateDocsSync } from '../validators/docs-sync.mjs';
|
|
|
18
18
|
import { validateArchitecture } from '../validators/architecture.mjs';
|
|
19
19
|
import { validateFreshness } from '../validators/freshness.mjs';
|
|
20
20
|
import { validateTraceability } from '../validators/traceability.mjs';
|
|
21
|
+
import { validateDocsDiff } from '../validators/docs-diff.mjs';
|
|
22
|
+
import { validateMetadataSync } from '../validators/metadata-sync.mjs';
|
|
23
|
+
import { validateMetricsConsistency } from '../validators/metrics-consistency.mjs';
|
|
24
|
+
import { validateDocsCoverage } from '../validators/docs-coverage.mjs';
|
|
21
25
|
|
|
22
26
|
/**
|
|
23
27
|
* Internal guard — returns structured data, no console output, no process.exit.
|
|
@@ -50,6 +54,10 @@ export function runGuardInternal(projectDir, config) {
|
|
|
50
54
|
return { errors, warnings, passed, total: passed + warnings.length + errors.length };
|
|
51
55
|
}},
|
|
52
56
|
{ key: 'traceability', name: 'Traceability', fn: () => validateTraceability(projectDir, config) },
|
|
57
|
+
{ key: 'docsDiff', name: 'Docs-Diff', fn: () => validateDocsDiff(projectDir, config) },
|
|
58
|
+
{ key: 'metadataSync', name: 'Metadata-Sync', fn: () => validateMetadataSync(projectDir, config) },
|
|
59
|
+
{ key: 'docsCoverage', name: 'Docs-Coverage', fn: () => validateDocsCoverage(projectDir, config) },
|
|
60
|
+
// Metrics-Consistency runs post-loop (needs guard results)
|
|
53
61
|
];
|
|
54
62
|
|
|
55
63
|
for (const { key, name, fn } of validatorMap) {
|
|
@@ -82,6 +90,21 @@ export function runGuardInternal(projectDir, config) {
|
|
|
82
90
|
}
|
|
83
91
|
}
|
|
84
92
|
|
|
93
|
+
// ── Metrics-Consistency runs AFTER all other validators (needs their results) ──
|
|
94
|
+
if (validators.metricsConsistency !== false) {
|
|
95
|
+
try {
|
|
96
|
+
const result = validateMetricsConsistency(projectDir, config, results);
|
|
97
|
+
const hasErrors = result.errors.length > 0;
|
|
98
|
+
const hasWarnings = result.warnings.length > 0;
|
|
99
|
+
const status = hasErrors ? 'fail' : hasWarnings ? 'warn' : 'pass';
|
|
100
|
+
const ratio = result.total > 0 ? result.passed / result.total : 1;
|
|
101
|
+
const quality = hasErrors ? 'LOW' : hasWarnings ? 'MEDIUM' : ratio >= 0.9 ? 'HIGH' : 'MEDIUM';
|
|
102
|
+
results.push({ ...result, name: 'Metrics-Consistency', key: 'metricsConsistency', status, quality });
|
|
103
|
+
} catch (err) {
|
|
104
|
+
results.push({ name: 'Metrics-Consistency', key: 'metricsConsistency', status: 'fail', quality: 'LOW', errors: [err.message], warnings: [], passed: 0, total: 1 });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
85
108
|
const activeResults = results.filter(r => r.status !== 'skipped');
|
|
86
109
|
const totalErrors = activeResults.reduce((sum, r) => sum + r.errors.length, 0);
|
|
87
110
|
const totalWarnings = activeResults.reduce((sum, r) => sum + r.warnings.length, 0);
|
|
@@ -169,6 +192,12 @@ export function runGuard(projectDir, config, flags) {
|
|
|
169
192
|
console.log(` ${c.dim}Run ${c.cyan}docguard diagnose${c.dim} to get AI fix prompts.${c.reset}`);
|
|
170
193
|
}
|
|
171
194
|
|
|
195
|
+
// Badge snippet
|
|
196
|
+
const pct = data.total > 0 ? Math.round((data.passed / data.total) * 100) : 0;
|
|
197
|
+
const bColor = pct >= 90 ? 'brightgreen' : pct >= 70 ? 'green' : pct >= 50 ? 'yellow' : 'red';
|
|
198
|
+
const badgeUrl = `https://img.shields.io/badge/CDD_Guard-${data.passed}%2F${data.total}_passed-${bColor}`;
|
|
199
|
+
console.log(`\n ${c.dim}📎 Badge: ${c.reset}`);
|
|
200
|
+
|
|
172
201
|
console.log('');
|
|
173
202
|
|
|
174
203
|
if (data.errors > 0) process.exit(1);
|
package/cli/commands/hooks.mjs
CHANGED