docguard-cli 0.11.1 → 0.12.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/README.md +34 -15
- package/cli/commands/diff.mjs +15 -17
- package/cli/commands/guard.mjs +124 -7
- package/cli/commands/init.mjs +52 -1
- package/cli/commands/upgrade.mjs +250 -0
- package/cli/docguard.mjs +32 -3
- package/cli/ensure-skills.mjs +8 -1
- package/cli/shared-ignore.mjs +50 -0
- package/cli/shared-source.mjs +17 -6
- package/cli/shared.mjs +62 -0
- package/cli/validators/cross-reference.mjs +281 -0
- package/cli/validators/docs-coverage.mjs +4 -1
- package/cli/validators/environment.mjs +9 -3
- package/commands/docguard.guard.md +2 -2
- package/docs/quickstart.md +1 -1
- package/extensions/spec-kit-docguard/README.md +1 -1
- package/extensions/spec-kit-docguard/commands/guard.md +1 -1
- package/extensions/spec-kit-docguard/extension.yml +11 -1
- package/extensions/spec-kit-docguard/skills/docguard-fix/SKILL.md +2 -2
- package/extensions/spec-kit-docguard/skills/docguard-guard/SKILL.md +3 -3
- package/extensions/spec-kit-docguard/skills/docguard-review/SKILL.md +2 -2
- package/extensions/spec-kit-docguard/skills/docguard-score/SKILL.md +2 -2
- package/extensions/spec-kit-docguard/skills/docguard-sync/SKILL.md +1 -1
- package/extensions/spec-kit-docguard/templates/github-workflows/docguard-autofix.yml +51 -0
- package/extensions/spec-kit-docguard/templates/github-workflows/docguard-guard.yml +48 -0
- package/package.json +1 -1
- package/templates/commands/docguard.guard.md +2 -2
package/README.md
CHANGED
|
@@ -343,7 +343,7 @@ DocGuard provides AI agent slash commands for integrated workflows. Installed au
|
|
|
343
343
|
|
|
344
344
|
| Command | What It Does |
|
|
345
345
|
|:--------|:-------------|
|
|
346
|
-
| `/docguard.guard` | Run quality validation — check all
|
|
346
|
+
| `/docguard.guard` | Run quality validation — check all 21 validators |
|
|
347
347
|
| `/docguard.review` | Analyze doc quality and suggest improvements |
|
|
348
348
|
| `/docguard.fix` | Generate targeted fix prompts for specific issues |
|
|
349
349
|
| `/docguard.score` | Show CDD maturity score with category breakdown |
|
|
@@ -433,20 +433,42 @@ DocGuard runs its own `guard`, `score`, `diff`, `diagnose`, and `badge` commands
|
|
|
433
433
|
|
|
434
434
|
## ⚙️ CI/CD Integration
|
|
435
435
|
|
|
436
|
-
|
|
436
|
+
> **Full recipes:** see [`docs-canonical/CI-RECIPES.md`](./docs-canonical/CI-RECIPES.md) for guard, auto-fix (commits mechanical fixes back to PRs), nightly sync, score-on-PR, and pre-commit configs.
|
|
437
|
+
|
|
438
|
+
### GitHub Actions — Guard (most common)
|
|
437
439
|
|
|
438
440
|
```yaml
|
|
439
|
-
name: DocGuard
|
|
440
|
-
on: [pull_request]
|
|
441
|
+
name: DocGuard Guard
|
|
442
|
+
on: [pull_request, push]
|
|
441
443
|
jobs:
|
|
442
444
|
docguard:
|
|
443
445
|
runs-on: ubuntu-latest
|
|
444
446
|
steps:
|
|
445
447
|
- uses: actions/checkout@v4
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
448
|
+
with: { fetch-depth: 0 }
|
|
449
|
+
- uses: raccioly/docguard@v0.12.0
|
|
450
|
+
with:
|
|
451
|
+
command: guard
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
### GitHub Actions — Auto-Fix (commits mechanical fixes back)
|
|
455
|
+
|
|
456
|
+
```yaml
|
|
457
|
+
name: DocGuard Auto-Fix
|
|
458
|
+
on: { pull_request: { types: [opened, synchronize, reopened] } }
|
|
459
|
+
permissions: { contents: write, pull-requests: write }
|
|
460
|
+
jobs:
|
|
461
|
+
autofix:
|
|
462
|
+
runs-on: ubuntu-latest
|
|
463
|
+
if: github.event.pull_request.head.repo.full_name == github.repository
|
|
464
|
+
steps:
|
|
465
|
+
- uses: actions/checkout@v4
|
|
466
|
+
with:
|
|
467
|
+
ref: ${{ github.event.pull_request.head.ref }}
|
|
468
|
+
token: ${{ secrets.GITHUB_TOKEN }}
|
|
469
|
+
fetch-depth: 0
|
|
470
|
+
- uses: raccioly/docguard@v0.12.0
|
|
471
|
+
with: { command: fix, auto-commit: 'true', comment-on-pr: 'true' }
|
|
450
472
|
```
|
|
451
473
|
|
|
452
474
|
### Pre-commit Hook
|
|
@@ -455,14 +477,11 @@ jobs:
|
|
|
455
477
|
npx docguard-cli hooks --type pre-commit
|
|
456
478
|
```
|
|
457
479
|
|
|
458
|
-
###
|
|
480
|
+
### Workflow starters (copy directly)
|
|
459
481
|
|
|
460
|
-
|
|
461
|
-
-
|
|
462
|
-
|
|
463
|
-
command: guard
|
|
464
|
-
fail-on-warning: true
|
|
465
|
-
```
|
|
482
|
+
Two ready-to-use templates ship with the Spec Kit extension and as standalone files:
|
|
483
|
+
- `extensions/spec-kit-docguard/templates/github-workflows/docguard-guard.yml` — mandatory CI gate
|
|
484
|
+
- `extensions/spec-kit-docguard/templates/github-workflows/docguard-autofix.yml` — PR auto-fix
|
|
466
485
|
|
|
467
486
|
---
|
|
468
487
|
|
package/cli/commands/diff.mjs
CHANGED
|
@@ -10,6 +10,8 @@ import { collectPackageJsons, detectDocker, grepEnvUsage, resolveSourceRoots } f
|
|
|
10
10
|
import { parseApiReferenceDoc, compareEndpoints } from '../scanners/api-doc.mjs';
|
|
11
11
|
import { resolveApiSurface } from '../validators/api-surface.mjs';
|
|
12
12
|
import { collectCodeTests } from '../validators/docs-diff.mjs';
|
|
13
|
+
import { scanSchemasDeep } from '../scanners/schemas.mjs';
|
|
14
|
+
import { detectDocTools } from '../scanners/doc-tools.mjs';
|
|
13
15
|
|
|
14
16
|
const IGNORE_DIRS = new Set([
|
|
15
17
|
'node_modules', '.git', '.next', 'dist', 'build',
|
|
@@ -163,22 +165,17 @@ function diffEntities(dir, config = {}) {
|
|
|
163
165
|
docEntities.add(name.toLowerCase());
|
|
164
166
|
}
|
|
165
167
|
|
|
166
|
-
//
|
|
168
|
+
// Use the REAL exported entity names from scanSchemasDeep, not file basenames
|
|
169
|
+
// (a file `dynamoModels.ts` exports `User`/`Order`/etc. — its basename is not
|
|
170
|
+
// an entity). scanSchemasDeep covers JS ORMs, SQLAlchemy/Pydantic, Diesel,
|
|
171
|
+
// Go structs, JPA, Rails, and OpenAPI schemas.
|
|
172
|
+
const docTools = detectDocTools(dir);
|
|
173
|
+
const schemas = scanSchemasDeep(dir, {}, docTools);
|
|
167
174
|
const codeEntities = new Set();
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
const modelDir = join(root, sub);
|
|
173
|
-
if (!existsSync(modelDir)) continue;
|
|
174
|
-
const files = getFilesRecursive(modelDir);
|
|
175
|
-
for (const f of files) {
|
|
176
|
-
const name = basename(f, extname(f)).toLowerCase();
|
|
177
|
-
// Skip non-entity infrastructure/aggregation filenames.
|
|
178
|
-
if (CODE_ENTITY_NOISE.has(name)) continue;
|
|
179
|
-
codeEntities.add(name);
|
|
180
|
-
}
|
|
181
|
-
}
|
|
175
|
+
for (const e of (schemas.entities || [])) {
|
|
176
|
+
const n = String(e.name || '').toLowerCase();
|
|
177
|
+
if (!n || CODE_ENTITY_NOISE.has(n)) continue;
|
|
178
|
+
codeEntities.add(n);
|
|
182
179
|
}
|
|
183
180
|
|
|
184
181
|
// No code-side entity source (e.g. DynamoDB single-table design with no model
|
|
@@ -207,7 +204,8 @@ function diffEnvVars(dir, config = {}) {
|
|
|
207
204
|
|
|
208
205
|
// Extract env var names from ENVIRONMENT.md
|
|
209
206
|
const docVars = new Set();
|
|
210
|
-
|
|
207
|
+
// Reject names ending in `_` (e.g. the literal prefix `VITE_` in prose).
|
|
208
|
+
const varRegex = /`([A-Z][A-Z0-9_]*[A-Z0-9])`/g;
|
|
211
209
|
let match;
|
|
212
210
|
while ((match = varRegex.exec(content)) !== null) {
|
|
213
211
|
docVars.add(match[1]);
|
|
@@ -220,7 +218,7 @@ function diffEnvVars(dir, config = {}) {
|
|
|
220
218
|
const envExamplePath = resolve(dir, envFile);
|
|
221
219
|
if (existsSync(envExamplePath)) {
|
|
222
220
|
const envContent = readFileSync(envExamplePath, 'utf-8');
|
|
223
|
-
const envRegex = /^([A-Z][A-Z0-9_]
|
|
221
|
+
const envRegex = /^([A-Z][A-Z0-9_]*[A-Z0-9])\s*=/gm;
|
|
224
222
|
while ((match = envRegex.exec(envContent)) !== null) {
|
|
225
223
|
codeVars.add(match[1]);
|
|
226
224
|
}
|
package/cli/commands/guard.mjs
CHANGED
|
@@ -7,8 +7,9 @@
|
|
|
7
7
|
* runGuardInternal() → returns data, no side effects (for diagnose, ci)
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { c } from '../shared.mjs';
|
|
10
|
+
import { c, resolveSeverity } from '../shared.mjs';
|
|
11
11
|
import { detectAgentMode, isSpecKitInitialized } from '../ensure-skills.mjs';
|
|
12
|
+
import { checkUpgradeStatus } from './upgrade.mjs';
|
|
12
13
|
import { validateStructure, validateDocSections } from '../validators/structure.mjs';
|
|
13
14
|
import { validateDrift } from '../validators/drift.mjs';
|
|
14
15
|
import { validateChangelog } from '../validators/changelog.mjs';
|
|
@@ -25,6 +26,7 @@ import { validateMetadataSync } from '../validators/metadata-sync.mjs';
|
|
|
25
26
|
import { validateMetricsConsistency } from '../validators/metrics-consistency.mjs';
|
|
26
27
|
import { validateDocsCoverage } from '../validators/docs-coverage.mjs';
|
|
27
28
|
import { validateDocQuality } from '../validators/doc-quality.mjs';
|
|
29
|
+
import { validateCrossReferences } from '../validators/cross-reference.mjs';
|
|
28
30
|
import { validateTodoTracking } from '../validators/todo-tracking.mjs';
|
|
29
31
|
import { validateSchemaSync } from '../validators/schema-sync.mjs';
|
|
30
32
|
import { validateSpecKitIntegration } from '../scanners/speckit.mjs';
|
|
@@ -100,6 +102,7 @@ export function runGuardInternal(projectDir, config) {
|
|
|
100
102
|
{ key: 'todoTracking', name: 'TODO-Tracking', fn: () => validateTodoTracking(projectDir, config) },
|
|
101
103
|
{ key: 'schemaSync', name: 'Schema-Sync', fn: () => validateSchemaSync(projectDir, config) },
|
|
102
104
|
{ key: 'specKit', name: 'Spec-Kit', fn: () => validateSpecKitIntegration(projectDir, config) },
|
|
105
|
+
{ key: 'crossReference', name: 'Cross-Reference', fn: () => validateCrossReferences(projectDir, config) },
|
|
103
106
|
// Metrics-Consistency runs post-loop (needs guard results)
|
|
104
107
|
];
|
|
105
108
|
|
|
@@ -133,6 +136,25 @@ export function runGuardInternal(projectDir, config) {
|
|
|
133
136
|
const totalPassed = activeResults.reduce((sum, r) => sum + r.passed, 0);
|
|
134
137
|
const totalChecks = activeResults.reduce((sum, r) => sum + r.total, 0);
|
|
135
138
|
|
|
139
|
+
// Per-validator severity overrides (v0.5 schema). Affects EXIT-CODE only,
|
|
140
|
+
// not display. Annotate each validator with its resolved severity and roll
|
|
141
|
+
// up effective error/warning counts:
|
|
142
|
+
// - high → validator's warnings get promoted to "effective errors"
|
|
143
|
+
// - low → validator's warnings are demoted (ignored for exit code)
|
|
144
|
+
// - medium (default) → warnings stay as-is
|
|
145
|
+
for (const v of activeResults) {
|
|
146
|
+
v.severity = resolveSeverity(config, v.key);
|
|
147
|
+
}
|
|
148
|
+
let effectiveErrors = totalErrors;
|
|
149
|
+
let effectiveWarnings = 0;
|
|
150
|
+
for (const v of activeResults) {
|
|
151
|
+
const wCount = v.warnings.length;
|
|
152
|
+
if (wCount === 0) continue;
|
|
153
|
+
if (v.severity === 'high') effectiveErrors += wCount;
|
|
154
|
+
else if (v.severity === 'low') { /* ignored for exit */ }
|
|
155
|
+
else effectiveWarnings += wCount;
|
|
156
|
+
}
|
|
157
|
+
|
|
136
158
|
const overallStatus = totalErrors > 0 ? 'FAIL' : totalWarnings > 0 ? 'WARN' : 'PASS';
|
|
137
159
|
|
|
138
160
|
return {
|
|
@@ -143,22 +165,66 @@ export function runGuardInternal(projectDir, config) {
|
|
|
143
165
|
total: totalChecks,
|
|
144
166
|
errors: totalErrors,
|
|
145
167
|
warnings: totalWarnings,
|
|
168
|
+
// v0.5: severity-aware counts for exit-code logic. The display still uses
|
|
169
|
+
// the raw counts above so users see every warning, but CI only fails on
|
|
170
|
+
// things they've marked as high-severity.
|
|
171
|
+
effectiveErrors,
|
|
172
|
+
effectiveWarnings,
|
|
146
173
|
validators: results,
|
|
147
174
|
timestamp: new Date().toISOString(),
|
|
148
175
|
};
|
|
149
176
|
}
|
|
150
177
|
|
|
178
|
+
/**
|
|
179
|
+
* The "pre-commit lite" validator set — fast checks suitable for running
|
|
180
|
+
* on every commit/save. Tuned for <2s wall-clock on average repos.
|
|
181
|
+
*
|
|
182
|
+
* The list is intentionally short: validators that catch >80% of the
|
|
183
|
+
* common doc drift that developers introduce mid-feature (route added but
|
|
184
|
+
* not documented, env var renamed but not updated in ENVIRONMENT.md,
|
|
185
|
+
* endpoint deleted but still in API-REFERENCE.md). Heavy validators —
|
|
186
|
+
* Freshness (git log), Traceability (REQ scan), Doc-Quality (prose lint) —
|
|
187
|
+
* stay off for speed.
|
|
188
|
+
*/
|
|
189
|
+
export const CHANGED_ONLY_VALIDATORS = ['docsSync', 'environment', 'apiSurface'];
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Build a validators map that enables only the pre-commit-lite set.
|
|
193
|
+
* Used by `docguard guard --changed-only`.
|
|
194
|
+
*/
|
|
195
|
+
function liteValidatorsConfig() {
|
|
196
|
+
const all = [
|
|
197
|
+
'structure', 'docsSync', 'drift', 'changelog', 'testSpec', 'environment',
|
|
198
|
+
'security', 'architecture', 'freshness', 'traceability', 'docsDiff',
|
|
199
|
+
'apiSurface', 'metadataSync', 'docsCoverage', 'docQuality', 'todoTracking',
|
|
200
|
+
'schemaSync', 'specKit', 'crossReference', 'metricsConsistency',
|
|
201
|
+
];
|
|
202
|
+
const out = {};
|
|
203
|
+
for (const k of all) out[k] = CHANGED_ONLY_VALIDATORS.includes(k);
|
|
204
|
+
return out;
|
|
205
|
+
}
|
|
206
|
+
|
|
151
207
|
/**
|
|
152
208
|
* Public guard — prints results and exits.
|
|
153
209
|
*/
|
|
154
210
|
export function runGuard(projectDir, config, flags) {
|
|
211
|
+
// --changed-only: pre-commit lite mode. Overrides the validator set to a
|
|
212
|
+
// fast subset (Docs-Sync, Environment, API-Surface). Designed for husky/
|
|
213
|
+
// lefthook hooks; expects to finish in under 2 seconds.
|
|
214
|
+
if (flags.changedOnly) {
|
|
215
|
+
config = { ...config, validators: liteValidatorsConfig() };
|
|
216
|
+
console.log(`${c.cyan}⚡ docguard guard --changed-only${c.reset} ${c.dim}(running ${CHANGED_ONLY_VALIDATORS.length} fast validators only — pre-commit lite mode)${c.reset}\n`);
|
|
217
|
+
}
|
|
218
|
+
|
|
155
219
|
const data = runGuardInternal(projectDir, config);
|
|
156
220
|
|
|
157
221
|
// ── JSON output ──
|
|
158
222
|
if (flags.format === 'json') {
|
|
159
223
|
console.log(JSON.stringify(data, null, 2));
|
|
160
|
-
|
|
161
|
-
|
|
224
|
+
// Use severity-aware effective counts for exit code; raw counts stay in the JSON
|
|
225
|
+
// for display tools that want to show the full picture.
|
|
226
|
+
if (data.effectiveErrors > 0) process.exit(1);
|
|
227
|
+
if (data.effectiveWarnings > 0) process.exit(2);
|
|
162
228
|
process.exit(0);
|
|
163
229
|
}
|
|
164
230
|
|
|
@@ -194,16 +260,26 @@ export function runGuard(projectDir, config, flags) {
|
|
|
194
260
|
console.log(` ${c.yellow}⚠️ ${v.name}${c.reset} ${qBadge}${c.dim} ${v.passed}/${v.total} checks passed${c.reset}`);
|
|
195
261
|
}
|
|
196
262
|
|
|
197
|
-
|
|
263
|
+
// --show-failing forces enumeration of every error/warning regardless of
|
|
264
|
+
// overall validator status — useful when a validator passes overall
|
|
265
|
+
// (passed < total) without surfacing the specific failing checks.
|
|
266
|
+
const show = flags.verbose || flags.showFailing;
|
|
267
|
+
if (show || v.status === 'fail') {
|
|
198
268
|
for (const err of v.errors) {
|
|
199
269
|
console.log(` ${c.red}✗ ${err}${c.reset}`);
|
|
200
270
|
}
|
|
201
271
|
}
|
|
202
|
-
if (
|
|
272
|
+
if (show || v.status === 'warn') {
|
|
203
273
|
for (const warn of v.warnings) {
|
|
204
274
|
console.log(` ${c.yellow}⚠ ${warn}${c.reset}`);
|
|
205
275
|
}
|
|
206
276
|
}
|
|
277
|
+
// If a validator reports passed < total but has no errors/warnings, surface
|
|
278
|
+
// the gap honestly so users aren't left wondering where the deficit went.
|
|
279
|
+
if (v.status === 'pass' && v.total > v.passed && v.errors.length === 0 && v.warnings.length === 0) {
|
|
280
|
+
const gap = v.total - v.passed;
|
|
281
|
+
console.log(` ${c.yellow}⚠ ${gap} check(s) did not pass but emitted no message — likely a validator bug. Please file an issue.${c.reset}`);
|
|
282
|
+
}
|
|
207
283
|
}
|
|
208
284
|
|
|
209
285
|
// Summary
|
|
@@ -233,14 +309,55 @@ export function runGuard(projectDir, config, flags) {
|
|
|
233
309
|
const badgeUrl = `https://img.shields.io/badge/CDD_Guard-${data.passed}%2F${data.total}_passed-${bColor}`;
|
|
234
310
|
console.log(`\n ${c.dim}📎 Badge: ${c.reset}`);
|
|
235
311
|
|
|
312
|
+
// Schema upgrade nudge — fires when the project's .docguard.json schema is
|
|
313
|
+
// behind the CLI's CURRENT_SCHEMA_VERSION. Cheap, file-local check; no
|
|
314
|
+
// network access. Suppressed in JSON output to keep machine consumers clean.
|
|
315
|
+
if (!flags || flags.format !== 'json') {
|
|
316
|
+
const upgradeHint = checkUpgradeStatus(projectDir);
|
|
317
|
+
if (upgradeHint) {
|
|
318
|
+
console.log(`\n ${c.yellow}↑ ${upgradeHint}${c.reset}`);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// K-6 / S-2: sweep-needed nudge. Aggregates freshness warnings — if 2+
|
|
322
|
+
// canonical docs are stale (matching the "X code commits since last doc
|
|
323
|
+
// update" pattern), suggest a single `docguard sync --write` pass that
|
|
324
|
+
// refreshes every code-truth section in one shot. Individual freshness
|
|
325
|
+
// warnings already named the docs; this nudge just turns "5 warnings"
|
|
326
|
+
// into one actionable recommendation.
|
|
327
|
+
const freshness = data.validators.find(v => v.key === 'freshness');
|
|
328
|
+
if (freshness && freshness.warnings) {
|
|
329
|
+
const staleDocs = freshness.warnings.filter(w => /\d+ code commits since/.test(w));
|
|
330
|
+
if (staleDocs.length >= 2) {
|
|
331
|
+
console.log(`\n ${c.yellow}↻ ${staleDocs.length} docs are stale (10+ commits since last update). Run ${c.cyan}docguard sync --write${c.yellow} to refresh code-truth sections in one pass.${c.reset}`);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
236
336
|
// Spec-kit reminder — persistent nudge if not initialized
|
|
237
337
|
if (!isSpecKitInitialized(projectDir)) {
|
|
238
338
|
console.log(`\n ${c.yellow}💡${c.reset} ${c.dim}Enhance DocGuard with Spec Kit: ${c.cyan}uv tool install specify-cli --from git+https://github.com/github/spec-kit.git${c.reset}`);
|
|
239
339
|
}
|
|
240
340
|
|
|
341
|
+
// When severity overrides demoted warnings to "low" (or promoted them to
|
|
342
|
+
// "high"), show a one-line note so the user knows the exit code may not
|
|
343
|
+
// match what they expected from reading the warning count.
|
|
344
|
+
const severityShifted =
|
|
345
|
+
data.effectiveErrors !== data.errors || data.effectiveWarnings !== data.warnings;
|
|
346
|
+
if (severityShifted) {
|
|
347
|
+
const upgraded = data.effectiveErrors - data.errors;
|
|
348
|
+
const ignored = data.warnings - data.effectiveWarnings - upgraded;
|
|
349
|
+
const parts = [];
|
|
350
|
+
if (upgraded > 0) parts.push(`${upgraded} warning(s) escalated to fail (severity=high)`);
|
|
351
|
+
if (ignored > 0) parts.push(`${ignored} warning(s) ignored for exit code (severity=low)`);
|
|
352
|
+
if (parts.length > 0) {
|
|
353
|
+
console.log(`\n ${c.dim}Severity override: ${parts.join('; ')}.${c.reset}`);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
241
357
|
console.log('');
|
|
242
358
|
|
|
243
|
-
|
|
244
|
-
if (data.
|
|
359
|
+
// v0.5: severity-aware exit codes (see runGuardInternal for the rollup).
|
|
360
|
+
if (data.effectiveErrors > 0) process.exit(1);
|
|
361
|
+
if (data.effectiveWarnings > 0) process.exit(2);
|
|
245
362
|
process.exit(0);
|
|
246
363
|
}
|
package/cli/commands/init.mjs
CHANGED
|
@@ -168,7 +168,7 @@ export async function runInit(projectDir, config, flags) {
|
|
|
168
168
|
|
|
169
169
|
const defaultConfig = {
|
|
170
170
|
projectName: config.projectName,
|
|
171
|
-
version: '0.
|
|
171
|
+
version: '0.5',
|
|
172
172
|
profile: profileName,
|
|
173
173
|
projectType: detectedType,
|
|
174
174
|
projectTypeConfig: ptc,
|
|
@@ -186,6 +186,13 @@ export async function runInit(projectDir, config, flags) {
|
|
|
186
186
|
environment: true,
|
|
187
187
|
freshness: true,
|
|
188
188
|
},
|
|
189
|
+
// Per-validator severity overrides (v0.5+).
|
|
190
|
+
// 'high': warnings from this validator fail CI (exit 1)
|
|
191
|
+
// 'medium': default — warnings exit 2 (informational)
|
|
192
|
+
// 'low': warnings ignored for exit code (exit 0)
|
|
193
|
+
// Empty by default — every validator uses 'medium'. Add entries to dial
|
|
194
|
+
// strictness up (CI-critical checks) or down (experimental validators).
|
|
195
|
+
severity: {},
|
|
189
196
|
};
|
|
190
197
|
|
|
191
198
|
writeFileSync(configPath, JSON.stringify(defaultConfig, null, 2) + '\n', 'utf-8');
|
|
@@ -196,6 +203,50 @@ export async function runInit(projectDir, config, flags) {
|
|
|
196
203
|
console.log(` ${c.yellow}⏭️${c.reset} .docguard.json ${c.dim}(already exists)${c.reset}`);
|
|
197
204
|
}
|
|
198
205
|
|
|
206
|
+
// ── Create .docguardignore ────────────────────────────────────────────
|
|
207
|
+
// Starter ignore file with gitignore-style syntax. Patterns here are merged
|
|
208
|
+
// into config.ignore at load time so every validator honors them.
|
|
209
|
+
const ignorePath = resolve(projectDir, '.docguardignore');
|
|
210
|
+
if (!existsSync(ignorePath)) {
|
|
211
|
+
const ignoreContent = `# .docguardignore — paths to exclude from DocGuard validation.
|
|
212
|
+
# Gitignore-style syntax: one pattern per line, # for comments.
|
|
213
|
+
# Merged into config.ignore (in .docguard.json) at runtime.
|
|
214
|
+
#
|
|
215
|
+
# Common examples:
|
|
216
|
+
# build/ # exclude a directory
|
|
217
|
+
# **/__generated__/** # exclude anything in any __generated__ dir
|
|
218
|
+
# vendor/legacy.ts # exclude a single file
|
|
219
|
+
# **/*.snap # exclude all files matching a glob
|
|
220
|
+
#
|
|
221
|
+
# Build outputs, vendored libs, generated code are good candidates here.
|
|
222
|
+
|
|
223
|
+
# Vendored / generated code that's not yours to document
|
|
224
|
+
**/__generated__/**
|
|
225
|
+
**/generated/**
|
|
226
|
+
**/*.generated.*
|
|
227
|
+
|
|
228
|
+
# Migrations and lock files
|
|
229
|
+
**/migrations/**
|
|
230
|
+
**/*.lock
|
|
231
|
+
package-lock.json
|
|
232
|
+
yarn.lock
|
|
233
|
+
pnpm-lock.yaml
|
|
234
|
+
Cargo.lock
|
|
235
|
+
poetry.lock
|
|
236
|
+
|
|
237
|
+
# Common build artifacts (defaults also cover these, but listing here is clearer)
|
|
238
|
+
# dist/
|
|
239
|
+
# build/
|
|
240
|
+
# coverage/
|
|
241
|
+
`;
|
|
242
|
+
writeFileSync(ignorePath, ignoreContent, 'utf-8');
|
|
243
|
+
created.push('.docguardignore');
|
|
244
|
+
console.log(` ${c.green}✅${c.reset} Created: ${c.cyan}.docguardignore${c.reset} ${c.dim}(gitignore-style exclusions)${c.reset}`);
|
|
245
|
+
} else {
|
|
246
|
+
skipped.push('.docguardignore');
|
|
247
|
+
console.log(` ${c.yellow}⏭️${c.reset} .docguardignore ${c.dim}(already exists)${c.reset}`);
|
|
248
|
+
}
|
|
249
|
+
|
|
199
250
|
// ── Spec-Kit Integration (Extension-First) ────────────────────────────
|
|
200
251
|
// Delegate LLM/IDE detection and spec-kit skill install to `specify init`
|
|
201
252
|
const specKitAvailable = isSpecKitAvailable();
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `docguard upgrade` — check whether the installed CLI and the project's
|
|
3
|
+
* .docguard.json schema are current, and (with --apply) migrate them.
|
|
4
|
+
*
|
|
5
|
+
* Why this exists:
|
|
6
|
+
* Users were running stale CLI versions against new project setups, getting
|
|
7
|
+
* confusing "validator missing" or "field unknown" warnings. A one-shot
|
|
8
|
+
* `docguard upgrade` lets them see the gap and fix it in seconds.
|
|
9
|
+
*
|
|
10
|
+
* Three modes:
|
|
11
|
+
* docguard upgrade — report current vs latest, exit 0
|
|
12
|
+
* docguard upgrade --check-only — same report, exit 1 if behind (for CI)
|
|
13
|
+
* docguard upgrade --apply — actually run `npm i -g docguard-cli@latest`
|
|
14
|
+
* and migrate .docguard.json if needed
|
|
15
|
+
*
|
|
16
|
+
* Network access (npm registry fetch) is OPTIONAL — if offline, we fall back
|
|
17
|
+
* to "could not check remote version" without erroring.
|
|
18
|
+
*/
|
|
19
|
+
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
20
|
+
import { resolve, dirname } from 'node:path';
|
|
21
|
+
import { fileURLToPath } from 'node:url';
|
|
22
|
+
import { spawnSync } from 'node:child_process';
|
|
23
|
+
|
|
24
|
+
import { c, CURRENT_SCHEMA_VERSION, compareVersions } from '../shared.mjs';
|
|
25
|
+
|
|
26
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
27
|
+
const PKG = JSON.parse(readFileSync(resolve(__dirname, '..', '..', 'package.json'), 'utf-8'));
|
|
28
|
+
const INSTALLED_VERSION = PKG.version;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Fetch the latest published version from the npm registry. Uses node's
|
|
32
|
+
* built-in fetch (Node 18+). Returns null on timeout/error/offline.
|
|
33
|
+
*
|
|
34
|
+
* 3-second timeout — we never want this command to feel slow.
|
|
35
|
+
*/
|
|
36
|
+
async function fetchLatestNpmVersion() {
|
|
37
|
+
const controller = new AbortController();
|
|
38
|
+
const t = setTimeout(() => controller.abort(), 3000);
|
|
39
|
+
try {
|
|
40
|
+
const r = await fetch('https://registry.npmjs.org/docguard-cli/latest', {
|
|
41
|
+
signal: controller.signal,
|
|
42
|
+
headers: { Accept: 'application/json' },
|
|
43
|
+
});
|
|
44
|
+
if (!r.ok) return null;
|
|
45
|
+
const data = await r.json();
|
|
46
|
+
return data && data.version ? data.version : null;
|
|
47
|
+
} catch {
|
|
48
|
+
return null;
|
|
49
|
+
} finally {
|
|
50
|
+
clearTimeout(t);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Read the project's stored schema version from .docguard.json. Returns:
|
|
56
|
+
* - null : file missing OR unparseable (caller shows "init recommended")
|
|
57
|
+
* - '0.0' : file exists but no `version` field (pre-0.4 schemas — these
|
|
58
|
+
* predate the version field and need migration)
|
|
59
|
+
* - 'x.y' : the stored version string
|
|
60
|
+
*/
|
|
61
|
+
function readProjectSchemaVersion(projectDir) {
|
|
62
|
+
const p = resolve(projectDir, '.docguard.json');
|
|
63
|
+
if (!existsSync(p)) return null;
|
|
64
|
+
try {
|
|
65
|
+
const cfg = JSON.parse(readFileSync(p, 'utf-8'));
|
|
66
|
+
// Pre-0.4 schemas (e.g. wu-whatsappinbox's original config from 2024)
|
|
67
|
+
// have no `version` field. Treat as 0.0 so the migration runs end-to-end.
|
|
68
|
+
return cfg.version || '0.0';
|
|
69
|
+
} catch {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Idempotent migration: walk the project config and add any fields introduced
|
|
76
|
+
* since the stored schema version. Returns { changed, newConfig }.
|
|
77
|
+
*
|
|
78
|
+
* Each migration is keyed by the version it migrates TO. Adding a new schema
|
|
79
|
+
* version means adding one entry here.
|
|
80
|
+
*/
|
|
81
|
+
function migrateSchema(cfg, fromVersion) {
|
|
82
|
+
const migrations = {
|
|
83
|
+
// v0.4 — pre-0.4 schemas (no `version` field, often `project` instead
|
|
84
|
+
// of `projectName`) normalize here. Rename `project` → `projectName`
|
|
85
|
+
// when only the old field is present, stamp the version, no other change.
|
|
86
|
+
'0.4': (c) => {
|
|
87
|
+
const out = { ...c, version: '0.4' };
|
|
88
|
+
if (!out.projectName && out.project) {
|
|
89
|
+
out.projectName = out.project;
|
|
90
|
+
delete out.project;
|
|
91
|
+
}
|
|
92
|
+
return out;
|
|
93
|
+
},
|
|
94
|
+
// v0.5 — K-4 per-validator severity overrides. Migration is purely
|
|
95
|
+
// additive: existing projects get an empty severity map and default
|
|
96
|
+
// (medium) behavior. No behavioral change unless they explicitly opt in.
|
|
97
|
+
'0.5': (c) => ({ ...c, severity: c.severity || {}, version: '0.5' }),
|
|
98
|
+
};
|
|
99
|
+
let current = { ...cfg };
|
|
100
|
+
let changed = false;
|
|
101
|
+
const target = CURRENT_SCHEMA_VERSION;
|
|
102
|
+
// No migrations yet — current schema matches the constant.
|
|
103
|
+
if (compareVersions(fromVersion, target) >= 0) return { changed: false, newConfig: current };
|
|
104
|
+
for (const [ver, fn] of Object.entries(migrations)) {
|
|
105
|
+
if (compareVersions(fromVersion, ver) < 0 && compareVersions(ver, target) <= 0) {
|
|
106
|
+
current = fn(current);
|
|
107
|
+
changed = true;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (changed) current.version = target;
|
|
111
|
+
return { changed, newConfig: current };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Apply a CLI upgrade by running `npm install -g docguard-cli@latest`. We
|
|
116
|
+
* shell out instead of importing npm — npm is not a runtime dependency and
|
|
117
|
+
* we want zero-deps. Returns the spawn result.
|
|
118
|
+
*/
|
|
119
|
+
function applyCliUpgrade() {
|
|
120
|
+
const r = spawnSync('npm', ['install', '-g', 'docguard-cli@latest'], {
|
|
121
|
+
stdio: 'inherit',
|
|
122
|
+
shell: process.platform === 'win32',
|
|
123
|
+
});
|
|
124
|
+
return r;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export async function runUpgrade(projectDir, _config, flags) {
|
|
128
|
+
const checkOnly = flags.checkOnly || flags['check-only'];
|
|
129
|
+
const apply = flags.apply;
|
|
130
|
+
|
|
131
|
+
console.log(`${c.bold}🔧 DocGuard Upgrade${c.reset}`);
|
|
132
|
+
console.log(`${c.dim} Checking CLI and schema versions...${c.reset}\n`);
|
|
133
|
+
|
|
134
|
+
// CLI version check
|
|
135
|
+
const latest = await fetchLatestNpmVersion();
|
|
136
|
+
const cliCmp = latest ? compareVersions(INSTALLED_VERSION, latest) : 0;
|
|
137
|
+
const cliBehind = cliCmp < 0;
|
|
138
|
+
|
|
139
|
+
// Schema version check
|
|
140
|
+
const projectSchema = readProjectSchemaVersion(projectDir);
|
|
141
|
+
const schemaCmp = projectSchema ? compareVersions(projectSchema, CURRENT_SCHEMA_VERSION) : 0;
|
|
142
|
+
const schemaBehind = projectSchema !== null && schemaCmp < 0;
|
|
143
|
+
|
|
144
|
+
// ── Report ──────────────────────────────────────────────────────────────
|
|
145
|
+
console.log(` ${c.cyan}CLI${c.reset} installed: ${c.bold}v${INSTALLED_VERSION}${c.reset}`);
|
|
146
|
+
if (latest) {
|
|
147
|
+
if (cliBehind) {
|
|
148
|
+
console.log(` latest: ${c.yellow}v${latest}${c.reset} ${c.yellow}(behind)${c.reset}`);
|
|
149
|
+
} else if (cliCmp > 0) {
|
|
150
|
+
console.log(` latest: v${latest} ${c.dim}(you're ahead — dev build)${c.reset}`);
|
|
151
|
+
} else {
|
|
152
|
+
console.log(` latest: ${c.green}v${latest}${c.reset} ${c.green}(current)${c.reset}`);
|
|
153
|
+
}
|
|
154
|
+
} else {
|
|
155
|
+
console.log(` latest: ${c.dim}could not check (offline?)${c.reset}`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
console.log();
|
|
159
|
+
// Two distinct null cases vs. '0.0' (pre-0.4):
|
|
160
|
+
// null → no .docguard.json at all → run init
|
|
161
|
+
// '0.0' → file exists but missing the `version` field → migration eligible
|
|
162
|
+
// other → real version string
|
|
163
|
+
const labelForProject = projectSchema === null
|
|
164
|
+
? `${c.dim}(no .docguard.json found)${c.reset}`
|
|
165
|
+
: projectSchema === '0.0'
|
|
166
|
+
? `${c.yellow}pre-0.4 (no version field)${c.reset}`
|
|
167
|
+
: `${c.bold}v${projectSchema}${c.reset}`;
|
|
168
|
+
console.log(` ${c.cyan}Schema${c.reset} project: ${labelForProject}`);
|
|
169
|
+
if (projectSchema) {
|
|
170
|
+
if (schemaBehind) {
|
|
171
|
+
console.log(` current: ${c.yellow}v${CURRENT_SCHEMA_VERSION}${c.reset} ${c.yellow}(behind)${c.reset}`);
|
|
172
|
+
} else if (schemaCmp > 0) {
|
|
173
|
+
console.log(` current: v${CURRENT_SCHEMA_VERSION} ${c.dim}(project is ahead — newer CLI needed)${c.reset}`);
|
|
174
|
+
} else {
|
|
175
|
+
console.log(` current: ${c.green}v${CURRENT_SCHEMA_VERSION}${c.reset} ${c.green}(current)${c.reset}`);
|
|
176
|
+
}
|
|
177
|
+
} else {
|
|
178
|
+
console.log(` current: v${CURRENT_SCHEMA_VERSION} ${c.dim}— run ${c.cyan}docguard init${c.dim} to create one${c.reset}`);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
console.log();
|
|
182
|
+
|
|
183
|
+
// ── Decide what to do ───────────────────────────────────────────────────
|
|
184
|
+
const anythingBehind = cliBehind || schemaBehind;
|
|
185
|
+
if (!anythingBehind) {
|
|
186
|
+
console.log(` ${c.green}✅ Everything is up to date.${c.reset}`);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// What needs doing
|
|
191
|
+
console.log(`${c.bold} Recommended actions:${c.reset}`);
|
|
192
|
+
if (cliBehind) {
|
|
193
|
+
console.log(` ${c.yellow}•${c.reset} Upgrade CLI: ${c.cyan}npm install -g docguard-cli@latest${c.reset}`);
|
|
194
|
+
}
|
|
195
|
+
if (schemaBehind) {
|
|
196
|
+
console.log(` ${c.yellow}•${c.reset} Migrate schema: ${c.cyan}docguard upgrade --apply${c.reset} ${c.dim}(or hand-edit .docguard.json)${c.reset}`);
|
|
197
|
+
}
|
|
198
|
+
console.log();
|
|
199
|
+
|
|
200
|
+
// ── --check-only: exit 1 to fail CI ─────────────────────────────────────
|
|
201
|
+
if (checkOnly) {
|
|
202
|
+
console.log(`${c.red}Exit 1 — versions behind (--check-only mode).${c.reset}`);
|
|
203
|
+
process.exit(1);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ── --apply: actually run the migration ─────────────────────────────────
|
|
207
|
+
if (apply) {
|
|
208
|
+
console.log(`${c.bold} Applying upgrades...${c.reset}\n`);
|
|
209
|
+
|
|
210
|
+
if (cliBehind) {
|
|
211
|
+
console.log(` ${c.dim}Running:${c.reset} npm install -g docguard-cli@latest`);
|
|
212
|
+
const r = applyCliUpgrade();
|
|
213
|
+
if (r.status !== 0) {
|
|
214
|
+
console.error(` ${c.red}✗ CLI upgrade failed.${c.reset} Try with sudo, or check npm permissions.`);
|
|
215
|
+
process.exit(1);
|
|
216
|
+
}
|
|
217
|
+
console.log(` ${c.green}✓ CLI upgraded.${c.reset}`);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (schemaBehind && projectSchema) {
|
|
221
|
+
const cfgPath = resolve(projectDir, '.docguard.json');
|
|
222
|
+
const cfg = JSON.parse(readFileSync(cfgPath, 'utf-8'));
|
|
223
|
+
const { changed, newConfig } = migrateSchema(cfg, projectSchema);
|
|
224
|
+
if (changed) {
|
|
225
|
+
writeFileSync(cfgPath, JSON.stringify(newConfig, null, 2) + '\n', 'utf-8');
|
|
226
|
+
console.log(` ${c.green}✓ Schema migrated ${projectSchema} → ${newConfig.version}.${c.reset}`);
|
|
227
|
+
} else {
|
|
228
|
+
console.log(` ${c.dim}Schema migration was a no-op (no recipe registered yet for ${projectSchema} → ${CURRENT_SCHEMA_VERSION}).${c.reset}`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
console.log(`\n ${c.green}✅ Upgrade complete.${c.reset} Run ${c.cyan}docguard guard${c.reset} to verify.`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Lightweight check for the post-guard nudge — returns a string when the
|
|
238
|
+
* project is behind, null when it's current. Cheap to call; never throws.
|
|
239
|
+
*/
|
|
240
|
+
export function checkUpgradeStatus(projectDir) {
|
|
241
|
+
const schema = readProjectSchemaVersion(projectDir);
|
|
242
|
+
if (!schema) return null;
|
|
243
|
+
if (compareVersions(schema, CURRENT_SCHEMA_VERSION) < 0) {
|
|
244
|
+
// '0.0' is the internal sentinel for pre-0.4 schemas (no `version` field).
|
|
245
|
+
// Surface that as a friendlier label so users don't see "Schema 0.0".
|
|
246
|
+
const label = schema === '0.0' ? 'pre-0.4 (no version field)' : `v${schema}`;
|
|
247
|
+
return `Schema ${label} is behind current v${CURRENT_SCHEMA_VERSION}. Run \`docguard upgrade --apply\` to migrate.`;
|
|
248
|
+
}
|
|
249
|
+
return null;
|
|
250
|
+
}
|