docguard-cli 0.11.2 → 0.13.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 +35 -16
- package/cli/commands/fix.mjs +55 -0
- package/cli/commands/guard.mjs +129 -5
- package/cli/commands/init.mjs +52 -1
- package/cli/commands/sync.mjs +50 -0
- package/cli/commands/trace.mjs +105 -0
- package/cli/commands/upgrade.mjs +250 -0
- package/cli/docguard.mjs +39 -3
- package/cli/shared-git.mjs +0 -0
- package/cli/shared-ignore.mjs +50 -0
- package/cli/shared.mjs +62 -0
- package/cli/validators/cross-reference.mjs +289 -0
- package/cli/validators/docs-sync.mjs +15 -0
- package/cli/validators/freshness.mjs +5 -10
- package/cli/validators/generated-staleness.mjs +97 -0
- package/cli/writers/fix-memory.mjs +133 -0
- package/cli/writers/mechanical.mjs +22 -0
- 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
|
@@ -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
|
+
}
|
package/cli/docguard.mjs
CHANGED
|
@@ -40,10 +40,12 @@ import { runPublish } from './commands/publish.mjs';
|
|
|
40
40
|
import { runTrace } from './commands/trace.mjs';
|
|
41
41
|
import { runLlms } from './commands/llms.mjs';
|
|
42
42
|
import { runSetup } from './commands/setup.mjs';
|
|
43
|
+
import { runUpgrade } from './commands/upgrade.mjs';
|
|
43
44
|
import { ensureSkills } from './ensure-skills.mjs';
|
|
44
45
|
|
|
45
46
|
// ── Shared constants (imported to break circular dependencies) ──────────
|
|
46
47
|
import { c, PROFILES } from './shared.mjs';
|
|
48
|
+
import { mergeIgnoreFile } from './shared-ignore.mjs';
|
|
47
49
|
export { c, PROFILES };
|
|
48
50
|
|
|
49
51
|
// ── Config Loading ─────────────────────────────────────────────────────────
|
|
@@ -138,6 +140,9 @@ export function loadConfig(projectDir) {
|
|
|
138
140
|
merged.testPatterns.push(merged.testPattern);
|
|
139
141
|
}
|
|
140
142
|
}
|
|
143
|
+
// Merge .docguardignore patterns into config.ignore so every validator
|
|
144
|
+
// honors them without having to know about the file.
|
|
145
|
+
mergeIgnoreFile(projectDir, merged);
|
|
141
146
|
return merged;
|
|
142
147
|
} catch (e) {
|
|
143
148
|
console.error(`${c.red}Error parsing .docguard.json: ${e.message}${c.reset}`);
|
|
@@ -148,6 +153,9 @@ export function loadConfig(projectDir) {
|
|
|
148
153
|
// No config file — auto-detect everything
|
|
149
154
|
defaults.projectType = autoDetectProjectType(projectDir);
|
|
150
155
|
defaults.projectTypeConfig = getProjectTypeDefaults(defaults.projectType);
|
|
156
|
+
// .docguardignore is read even when no .docguard.json exists — keeps
|
|
157
|
+
// ignore-only projects (no config but want to skip paths) working.
|
|
158
|
+
mergeIgnoreFile(projectDir, defaults);
|
|
151
159
|
return defaults;
|
|
152
160
|
}
|
|
153
161
|
|
|
@@ -367,6 +375,21 @@ async function main() {
|
|
|
367
375
|
i++;
|
|
368
376
|
} else if (args[i] === '--show-failing') {
|
|
369
377
|
flags.showFailing = true;
|
|
378
|
+
} else if (args[i] === '--check-only') {
|
|
379
|
+
flags.checkOnly = true;
|
|
380
|
+
} else if (args[i] === '--apply') {
|
|
381
|
+
flags.apply = true;
|
|
382
|
+
} else if (args[i] === '--changed-only') {
|
|
383
|
+
flags.changedOnly = true;
|
|
384
|
+
} else if (args[i] === '--reverse') {
|
|
385
|
+
flags.reverse = true;
|
|
386
|
+
} else if (args[i] === '--history') {
|
|
387
|
+
flags.history = true;
|
|
388
|
+
} else if (!args[i].startsWith('--') && i > 0) {
|
|
389
|
+
// Positional args go into flags.args for commands that take them (e.g.
|
|
390
|
+
// `docguard trace --reverse <path>`). Skip the command itself (i === 0).
|
|
391
|
+
flags.args = flags.args || [];
|
|
392
|
+
flags.args.push(args[i]);
|
|
370
393
|
} else if (args[i] === '--doc' && args[i + 1]) {
|
|
371
394
|
flags.doc = args[i + 1];
|
|
372
395
|
i++;
|
|
@@ -405,12 +428,21 @@ async function main() {
|
|
|
405
428
|
process.exit(0);
|
|
406
429
|
}
|
|
407
430
|
|
|
408
|
-
|
|
431
|
+
// In JSON mode the entire stdout MUST be parseable JSON. The banner and
|
|
432
|
+
// ensureSkills' install message would corrupt the output for any
|
|
433
|
+
// programmatic consumer (CI, dashboards, the Score-on-PR Action recipe).
|
|
434
|
+
// Headless flags (`--write`, `--check-only`, `--auto`) also suppress chrome.
|
|
435
|
+
const jsonMode = flags.format === 'json';
|
|
436
|
+
const headless = jsonMode || flags.write || flags.checkOnly || flags.changedOnly;
|
|
437
|
+
|
|
438
|
+
if (!jsonMode) printBanner();
|
|
409
439
|
|
|
410
440
|
const config = loadConfig(projectDir);
|
|
411
441
|
|
|
412
|
-
// Silent auto-check: install skills/commands if missing
|
|
413
|
-
|
|
442
|
+
// Silent auto-check: install skills/commands if missing. Skip entirely in
|
|
443
|
+
// headless modes where the user wants deterministic, parseable output and
|
|
444
|
+
// doesn't expect side effects on their AI-agent skill directories.
|
|
445
|
+
if (command !== 'setup' && command !== 'init' && !headless) {
|
|
414
446
|
ensureSkills(projectDir, flags);
|
|
415
447
|
}
|
|
416
448
|
|
|
@@ -478,6 +510,10 @@ async function main() {
|
|
|
478
510
|
case 'llms':
|
|
479
511
|
runLlms(projectDir, config, flags);
|
|
480
512
|
break;
|
|
513
|
+
case 'upgrade':
|
|
514
|
+
case 'update':
|
|
515
|
+
await runUpgrade(projectDir, config, flags);
|
|
516
|
+
break;
|
|
481
517
|
default:
|
|
482
518
|
console.error(`${c.red}Unknown command: ${command}${c.reset}`);
|
|
483
519
|
console.log(`Run ${c.cyan}docguard --help${c.reset} for usage.`);
|
|
Binary file
|
package/cli/shared-ignore.mjs
CHANGED
|
@@ -40,6 +40,56 @@ export const DEFAULT_IGNORE_DIRS = new Set([
|
|
|
40
40
|
const ALWAYS_REJECT_PATH_RE =
|
|
41
41
|
/(?:^|[/\\])(?:node_modules|\.claude[/\\]worktrees|\.git[/\\]worktrees|\.jj)(?:[/\\]|$)/;
|
|
42
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Read `.docguardignore` from a project directory and return its patterns.
|
|
45
|
+
*
|
|
46
|
+
* Format: gitignore-style — one pattern per line, `#` for comments, blank lines
|
|
47
|
+
* ignored. Returned patterns are normalized but not transformed (callers
|
|
48
|
+
* decide whether to expand directory globs).
|
|
49
|
+
*
|
|
50
|
+
* Returns [] if the file is missing or unreadable — never throws.
|
|
51
|
+
*/
|
|
52
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
53
|
+
import { resolve as resolvePath } from 'node:path';
|
|
54
|
+
|
|
55
|
+
export function loadDocguardIgnore(projectDir) {
|
|
56
|
+
const p = resolvePath(projectDir, '.docguardignore');
|
|
57
|
+
if (!existsSync(p)) return [];
|
|
58
|
+
try {
|
|
59
|
+
const raw = readFileSync(p, 'utf-8');
|
|
60
|
+
return raw
|
|
61
|
+
.split(/\r?\n/)
|
|
62
|
+
.map(line => line.trim())
|
|
63
|
+
.filter(line => line && !line.startsWith('#'));
|
|
64
|
+
} catch {
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Merge `.docguardignore` patterns into a config object's `ignore` array.
|
|
71
|
+
*
|
|
72
|
+
* Used at config-load time so every validator sees the combined set without
|
|
73
|
+
* having to know about the file. Mutates and returns the config for ergonomics.
|
|
74
|
+
*
|
|
75
|
+
* Idempotent — calling twice produces the same result. Skips duplicates.
|
|
76
|
+
*/
|
|
77
|
+
export function mergeIgnoreFile(projectDir, config) {
|
|
78
|
+
const filePatterns = loadDocguardIgnore(projectDir);
|
|
79
|
+
if (filePatterns.length === 0) return config;
|
|
80
|
+
const existing = Array.isArray(config.ignore) ? config.ignore : [];
|
|
81
|
+
const seen = new Set(existing);
|
|
82
|
+
const merged = [...existing];
|
|
83
|
+
for (const p of filePatterns) {
|
|
84
|
+
if (!seen.has(p)) {
|
|
85
|
+
merged.push(p);
|
|
86
|
+
seen.add(p);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
config.ignore = merged;
|
|
90
|
+
return config;
|
|
91
|
+
}
|
|
92
|
+
|
|
43
93
|
/**
|
|
44
94
|
* Convert a glob pattern to a RegExp.
|
|
45
95
|
* Supports: * (any chars except /), ** (any path segments), . (literal dot).
|
package/cli/shared.mjs
CHANGED
|
@@ -4,6 +4,68 @@
|
|
|
4
4
|
* All commands import from here instead of docguard.mjs.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Current .docguard.json schema version that this CLI version writes via
|
|
9
|
+
* `docguard init`. Bump this when adding fields that need migration (e.g.
|
|
10
|
+
* v0.12 adds `severity` overrides per validator).
|
|
11
|
+
*
|
|
12
|
+
* The post-guard nudge fires when an existing project's stored
|
|
13
|
+
* `.docguard.json.version` is BEHIND this constant — pointing users at
|
|
14
|
+
* `docguard upgrade` to migrate.
|
|
15
|
+
*/
|
|
16
|
+
export const CURRENT_SCHEMA_VERSION = '0.5';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Allowed severity values for per-validator `severity` overrides in
|
|
20
|
+
* `.docguard.json`. Affects EXIT-CODE behavior of `docguard guard`:
|
|
21
|
+
* - 'high': warnings from this validator fail CI (exit 1)
|
|
22
|
+
* - 'medium': default — warnings exit 2 (informational)
|
|
23
|
+
* - 'low': warnings ignored for exit code (exit 0)
|
|
24
|
+
*
|
|
25
|
+
* Display (the per-validator status lines and the summary) is unchanged
|
|
26
|
+
* regardless of severity — severity is a CI/operational knob, not a UI one.
|
|
27
|
+
*/
|
|
28
|
+
export const SEVERITY_LEVELS = new Set(['high', 'medium', 'low']);
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Resolve a validator's effective severity from config.
|
|
32
|
+
* Returns 'medium' (default) if no override is set or the override is bogus.
|
|
33
|
+
*/
|
|
34
|
+
export function resolveSeverity(config, validatorKey) {
|
|
35
|
+
const s = config && config.severity && config.severity[validatorKey];
|
|
36
|
+
if (typeof s === 'string' && SEVERITY_LEVELS.has(s.toLowerCase())) {
|
|
37
|
+
return s.toLowerCase();
|
|
38
|
+
}
|
|
39
|
+
return 'medium';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Parse a dotted-decimal version string into a tuple of integers for
|
|
44
|
+
* comparison. Tolerates extra suffixes (e.g. `0.4-beta` → [0, 4]).
|
|
45
|
+
* Returns null when the string is unparseable.
|
|
46
|
+
*/
|
|
47
|
+
export function parseVersion(v) {
|
|
48
|
+
if (!v || typeof v !== 'string') return null;
|
|
49
|
+
const m = v.match(/^(\d+)(?:\.(\d+))?(?:\.(\d+))?/);
|
|
50
|
+
if (!m) return null;
|
|
51
|
+
return [Number(m[1] || 0), Number(m[2] || 0), Number(m[3] || 0)];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Compare two version strings. Returns -1 if a<b, 0 if equal, 1 if a>b.
|
|
56
|
+
* Unparseable inputs sort as equal (no nag).
|
|
57
|
+
*/
|
|
58
|
+
export function compareVersions(a, b) {
|
|
59
|
+
const pa = parseVersion(a);
|
|
60
|
+
const pb = parseVersion(b);
|
|
61
|
+
if (!pa || !pb) return 0;
|
|
62
|
+
for (let i = 0; i < 3; i++) {
|
|
63
|
+
if (pa[i] < pb[i]) return -1;
|
|
64
|
+
if (pa[i] > pb[i]) return 1;
|
|
65
|
+
}
|
|
66
|
+
return 0;
|
|
67
|
+
}
|
|
68
|
+
|
|
7
69
|
// ── Colors (ANSI escape codes, zero deps) ──────────────────────────────────
|
|
8
70
|
export const c = {
|
|
9
71
|
reset: '\x1b[0m',
|