docguard-cli 0.9.8 → 0.9.10
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/cli/commands/diagnose.mjs +64 -24
- package/cli/commands/fix.mjs +1 -1
- package/cli/commands/guard.mjs +12 -1
- package/cli/commands/hooks.mjs +2 -2
- package/cli/commands/init.mjs +94 -73
- package/cli/commands/score.mjs +58 -16
- package/cli/commands/setup.mjs +60 -30
- package/cli/docguard.mjs +14 -5
- package/cli/ensure-skills.mjs +231 -13
- package/cli/scanners/speckit.mjs +1 -1
- package/cli/shared-ignore.mjs +76 -0
- package/cli/validators/architecture.mjs +21 -6
- package/cli/validators/doc-quality.mjs +1 -1
- package/cli/validators/docs-diff.mjs +79 -12
- package/cli/validators/schema-sync.mjs +1 -1
- package/cli/validators/security.mjs +49 -1
- package/cli/validators/todo-tracking.mjs +41 -15
- package/extensions/spec-kit-docguard/extension.yml +6 -2
- package/extensions/spec-kit-docguard/skills/docguard-fix/SKILL.md +2 -1
- package/extensions/spec-kit-docguard/skills/docguard-guard/SKILL.md +9 -4
- package/extensions/spec-kit-docguard/skills/docguard-review/SKILL.md +5 -1
- package/extensions/spec-kit-docguard/skills/docguard-score/SKILL.md +2 -1
- package/package.json +1 -1
package/cli/commands/score.mjs
CHANGED
|
@@ -7,6 +7,7 @@ import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
|
7
7
|
import { resolve, join, extname } from 'node:path';
|
|
8
8
|
import { execSync } from 'node:child_process';
|
|
9
9
|
import { c } from '../shared.mjs';
|
|
10
|
+
import { validateSecurity } from '../validators/security.mjs';
|
|
10
11
|
|
|
11
12
|
const WEIGHTS = {
|
|
12
13
|
structure: 25, // Required files exist
|
|
@@ -410,15 +411,32 @@ function calcTestingScore(dir, config) {
|
|
|
410
411
|
const testDirs = ['tests', 'test', '__tests__', 'spec', 'e2e'];
|
|
411
412
|
const hasTopLevelTestDir = testDirs.some(d => existsSync(resolve(dir, d)));
|
|
412
413
|
|
|
413
|
-
// Check co-located tests:
|
|
414
|
+
// Check co-located tests: **/__tests__/ and **/*.test.* / **/*.spec.*
|
|
415
|
+
// Scan ALL common source roots — not just src/, also backend/, packages/, etc.
|
|
414
416
|
let hasColocatedTests = false;
|
|
415
417
|
if (!hasTopLevelTestDir) {
|
|
416
418
|
hasColocatedTests = findColocatedTests(dir);
|
|
417
419
|
}
|
|
418
420
|
|
|
421
|
+
// Check if testPatterns config points to existing test locations
|
|
422
|
+
let hasPatternTests = false;
|
|
423
|
+
if (!hasTopLevelTestDir && !hasColocatedTests) {
|
|
424
|
+
const patterns = config.testPatterns || [];
|
|
425
|
+
if (patterns.length > 0) {
|
|
426
|
+
for (const pattern of patterns) {
|
|
427
|
+
// Extract the root directory from the pattern
|
|
428
|
+
const rootDir = pattern.split('/')[0].split('*')[0];
|
|
429
|
+
if (rootDir && existsSync(resolve(dir, rootDir))) {
|
|
430
|
+
hasPatternTests = true;
|
|
431
|
+
break;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
419
437
|
// Check vitest/jest config for custom test patterns
|
|
420
438
|
let hasConfigTests = false;
|
|
421
|
-
if (!hasTopLevelTestDir && !hasColocatedTests) {
|
|
439
|
+
if (!hasTopLevelTestDir && !hasColocatedTests && !hasPatternTests) {
|
|
422
440
|
const testConfigs = ['vitest.config.ts', 'vitest.config.js', 'vitest.config.mts', 'jest.config.ts', 'jest.config.js'];
|
|
423
441
|
for (const cfgFile of testConfigs) {
|
|
424
442
|
const cfgPath = resolve(dir, cfgFile);
|
|
@@ -448,7 +466,7 @@ function calcTestingScore(dir, config) {
|
|
|
448
466
|
}
|
|
449
467
|
}
|
|
450
468
|
|
|
451
|
-
if (hasTopLevelTestDir || hasColocatedTests || hasConfigTests) score += 40;
|
|
469
|
+
if (hasTopLevelTestDir || hasColocatedTests || hasPatternTests || hasConfigTests) score += 40;
|
|
452
470
|
|
|
453
471
|
// ── Check 2: TEST-SPEC.md exists (30 pts) ──
|
|
454
472
|
if (existsSync(resolve(dir, 'docs-canonical/TEST-SPEC.md'))) score += 30;
|
|
@@ -490,7 +508,8 @@ function calcTestingScore(dir, config) {
|
|
|
490
508
|
*/
|
|
491
509
|
function findColocatedTests(dir) {
|
|
492
510
|
// Scan these common source roots for co-located tests
|
|
493
|
-
|
|
511
|
+
// Includes backend/, server/ for monorepo-style projects (e.g., backend/src/__tests__/)
|
|
512
|
+
const sourceRoots = ['src', 'app', 'lib', 'packages', 'modules', 'backend', 'server'];
|
|
494
513
|
const ignoreSet = new Set(['node_modules', '.git', 'dist', 'build', '.next', 'coverage', '.cache']);
|
|
495
514
|
|
|
496
515
|
for (const root of sourceRoots) {
|
|
@@ -534,25 +553,44 @@ function calcSecurityScore(dir, config) {
|
|
|
534
553
|
let score = 0;
|
|
535
554
|
const ptc = config.projectTypeConfig || {};
|
|
536
555
|
|
|
537
|
-
// SECURITY.md exists
|
|
538
|
-
if (existsSync(resolve(dir, 'docs-canonical/SECURITY.md'))) score +=
|
|
556
|
+
// SECURITY.md exists (25 pts)
|
|
557
|
+
if (existsSync(resolve(dir, 'docs-canonical/SECURITY.md'))) score += 25;
|
|
539
558
|
|
|
540
|
-
// .gitignore exists and includes .env
|
|
559
|
+
// .gitignore exists and includes .env (15 + 15 pts)
|
|
541
560
|
const gitignorePath = resolve(dir, '.gitignore');
|
|
542
561
|
if (existsSync(gitignorePath)) {
|
|
543
|
-
score +=
|
|
562
|
+
score += 15;
|
|
544
563
|
const content = readFileSync(gitignorePath, 'utf-8');
|
|
545
|
-
if (content.includes('.env')) score +=
|
|
564
|
+
if (content.includes('.env')) score += 15;
|
|
546
565
|
}
|
|
547
566
|
|
|
548
|
-
// No .env file committed (
|
|
549
|
-
if (!existsSync(resolve(dir, '.env')) || existsSync(gitignorePath)) score +=
|
|
567
|
+
// No .env file committed (10 pts)
|
|
568
|
+
if (!existsSync(resolve(dir, '.env')) || existsSync(gitignorePath)) score += 10;
|
|
550
569
|
|
|
551
|
-
// .env.example exists (safe template) — only check if project needs env vars
|
|
570
|
+
// .env.example exists (safe template) — only check if project needs env vars (10 pts)
|
|
552
571
|
if (ptc.needsEnvExample === false) {
|
|
553
|
-
score +=
|
|
572
|
+
score += 10; // Full marks — project doesn't need env vars
|
|
554
573
|
} else if (existsSync(resolve(dir, '.env.example'))) {
|
|
555
|
-
score +=
|
|
574
|
+
score += 10;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// No hardcoded secrets found by security validator (25 pts)
|
|
578
|
+
// Commands MAY compose validator results (Constitution IV, v1.1.0)
|
|
579
|
+
try {
|
|
580
|
+
const secResults = validateSecurity(dir, config);
|
|
581
|
+
if (secResults.errors.length === 0) {
|
|
582
|
+
score += 25;
|
|
583
|
+
} else {
|
|
584
|
+
// Partial credit: deduct proportionally, but give at least some credit
|
|
585
|
+
// if there are few findings relative to project size
|
|
586
|
+
const findingCount = secResults.errors.length;
|
|
587
|
+
if (findingCount <= 2) score += 15;
|
|
588
|
+
else if (findingCount <= 5) score += 5;
|
|
589
|
+
// 6+ findings = 0 pts for this check
|
|
590
|
+
}
|
|
591
|
+
} catch {
|
|
592
|
+
// If validator fails to run, give benefit of the doubt
|
|
593
|
+
score += 25;
|
|
556
594
|
}
|
|
557
595
|
|
|
558
596
|
return Math.min(100, score);
|
|
@@ -668,8 +706,12 @@ function getSuggestion(category, score, details) {
|
|
|
668
706
|
const suggestions = {
|
|
669
707
|
structure: 'Run `docguard init` to create missing documentation',
|
|
670
708
|
docQuality: 'Run `docguard fix` to get AI prompts for each doc that needs content',
|
|
671
|
-
testing:
|
|
672
|
-
|
|
709
|
+
testing: score < 40
|
|
710
|
+
? 'Add test files (tests/, src/**/__tests__/, or configure testPatterns in .docguard.json) and create TEST-SPEC.md'
|
|
711
|
+
: 'Configure TEST-SPEC.md and add CI test step → Run `docguard fix --doc test-spec`',
|
|
712
|
+
security: score < 50
|
|
713
|
+
? 'Create SECURITY.md and add .env to .gitignore → Run `docguard fix --doc security`'
|
|
714
|
+
: 'Review security findings with `docguard guard --verbose` — configure securityIgnore for false positives',
|
|
673
715
|
environment: 'Document env variables and create .env.example → Run `docguard fix --doc environment`',
|
|
674
716
|
drift: 'Create DRIFT-LOG.md and log any code deviations',
|
|
675
717
|
changelog: 'Maintain CHANGELOG.md with [Unreleased] section',
|
package/cli/commands/setup.mjs
CHANGED
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
* Each step shows current status (✅/⚠️) and offers to fix what's missing.
|
|
14
14
|
* Supports --skip-prompts for non-interactive CI mode.
|
|
15
15
|
*
|
|
16
|
-
* Zero dependencies — pure Node.js built-ins only.
|
|
16
|
+
* Zero NPM runtime dependencies — pure Node.js built-ins only.
|
|
17
|
+
* Framework dependency: spec-kit (convention, not code).
|
|
17
18
|
*/
|
|
18
19
|
|
|
19
20
|
import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync } from 'node:fs';
|
|
@@ -22,7 +23,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
22
23
|
import { createInterface } from 'node:readline';
|
|
23
24
|
import { execSync } from 'node:child_process';
|
|
24
25
|
import { c } from '../shared.mjs';
|
|
25
|
-
import { ensureSkills } from '../ensure-skills.mjs';
|
|
26
|
+
import { ensureSkills, detectAgentMode, isSpecKitInitialized, getDetectedAgent } from '../ensure-skills.mjs';
|
|
26
27
|
|
|
27
28
|
const __filename = fileURLToPath(import.meta.url);
|
|
28
29
|
const __dirname = dirname(__filename);
|
|
@@ -211,11 +212,17 @@ export async function runSetup(projectDir, config, flags) {
|
|
|
211
212
|
|
|
212
213
|
console.log(` ${c.bold}Step 3/7: AI Skills${c.reset}`);
|
|
213
214
|
|
|
214
|
-
const
|
|
215
|
+
const docguardSkills = ['docguard-guard', 'docguard-fix', 'docguard-review', 'docguard-score'];
|
|
216
|
+
const speckitSkills = [
|
|
217
|
+
'speckit-specify', 'speckit-plan', 'speckit-tasks', 'speckit-implement',
|
|
218
|
+
'speckit-analyze', 'speckit-clarify', 'speckit-checklist', 'speckit-constitution',
|
|
219
|
+
'speckit-taskstoissues',
|
|
220
|
+
];
|
|
221
|
+
const allSkillNames = [...docguardSkills, ...speckitSkills];
|
|
215
222
|
const skillsDest = resolve(projectDir, '.agent/skills');
|
|
216
223
|
let missingSkills = [];
|
|
217
224
|
|
|
218
|
-
for (const skill of
|
|
225
|
+
for (const skill of allSkillNames) {
|
|
219
226
|
const skillPath = resolve(skillsDest, skill, 'SKILL.md');
|
|
220
227
|
if (existsSync(skillPath)) {
|
|
221
228
|
console.log(` ${c.green}✅${c.reset} ${skill}`);
|
|
@@ -227,19 +234,28 @@ export async function runSetup(projectDir, config, flags) {
|
|
|
227
234
|
}
|
|
228
235
|
|
|
229
236
|
if (missingSkills.length > 0) {
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
: true;
|
|
237
|
+
const hasSpeckitMissing = missingSkills.some(s => s.startsWith('speckit-'));
|
|
238
|
+
const hasDocguardMissing = missingSkills.some(s => s.startsWith('docguard-'));
|
|
233
239
|
|
|
234
|
-
if (
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
240
|
+
if (hasSpeckitMissing) {
|
|
241
|
+
console.log(` ${c.dim} Spec-kit skills installed via: ${c.cyan}specify init --here --force --ai-skills${c.reset}`);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (hasDocguardMissing) {
|
|
245
|
+
const install = interactive
|
|
246
|
+
? await askYesNo(` → Install ${missingSkills.filter(s => s.startsWith('docguard-')).length} DocGuard skill(s) to .agent/skills/?`)
|
|
247
|
+
: true;
|
|
248
|
+
|
|
249
|
+
if (install) {
|
|
250
|
+
for (const skill of missingSkills.filter(s => s.startsWith('docguard-'))) {
|
|
251
|
+
const srcSkill = resolve(SKILLS_SOURCE, skill, 'SKILL.md');
|
|
252
|
+
const destDir = resolve(skillsDest, skill);
|
|
253
|
+
if (existsSync(srcSkill)) {
|
|
254
|
+
if (!existsSync(destDir)) mkdirSync(destDir, { recursive: true });
|
|
255
|
+
writeFileSync(resolve(destDir, 'SKILL.md'), readFileSync(srcSkill, 'utf-8'), 'utf-8');
|
|
256
|
+
console.log(` ${c.green}✅ Installed ${skill}${c.reset}`);
|
|
257
|
+
configured++;
|
|
258
|
+
}
|
|
243
259
|
}
|
|
244
260
|
}
|
|
245
261
|
}
|
|
@@ -354,16 +370,20 @@ export async function runSetup(projectDir, config, flags) {
|
|
|
354
370
|
|
|
355
371
|
console.log(` ${c.bold}Step 6/7: Integrations${c.reset}`);
|
|
356
372
|
|
|
357
|
-
// Check spec-kit framework
|
|
358
|
-
const
|
|
359
|
-
const
|
|
360
|
-
if (
|
|
361
|
-
|
|
373
|
+
// Check spec-kit framework (Extension-First: detect .specify/ directory)
|
|
374
|
+
const specKitInitialized = isSpecKitInitialized(projectDir);
|
|
375
|
+
const specifyDir = resolve(projectDir, '.specify');
|
|
376
|
+
if (specKitInitialized) {
|
|
377
|
+
const agent = getDetectedAgent(projectDir);
|
|
378
|
+
console.log(` ${c.green}✅${c.reset} spec-kit ${c.dim}(SDD configured${agent ? `, agent: ${agent}` : ''})${c.reset}`);
|
|
362
379
|
alreadyGood++;
|
|
380
|
+
} else if (existsSync(specifyDir)) {
|
|
381
|
+
console.log(` ${c.yellow}⚠️${c.reset} spec-kit ${c.dim}(.specify/ exists but not fully initialized)${c.reset}`);
|
|
382
|
+
console.log(` ${c.dim} Run: ${c.cyan}specify init --here --force --ai-skills${c.reset}`);
|
|
363
383
|
} else {
|
|
364
|
-
console.log(` ${c.dim}──${c.reset} spec-kit ${c.dim}(not configured —
|
|
365
|
-
console.log(` ${c.dim}
|
|
366
|
-
console.log(` ${c.dim}
|
|
384
|
+
console.log(` ${c.dim}──${c.reset} spec-kit ${c.dim}(not configured — recommended for full SDD+CDD workflow)${c.reset}`);
|
|
385
|
+
console.log(` ${c.dim} Install: ${c.cyan}uv tool install specify-cli --from git+https://github.com/github/spec-kit.git${c.reset}`);
|
|
386
|
+
console.log(` ${c.dim} Then: ${c.cyan}docguard init${c.reset} ${c.dim}(will auto-run specify init)${c.reset}`);
|
|
367
387
|
}
|
|
368
388
|
|
|
369
389
|
// Check for spec-kit extensions
|
|
@@ -421,8 +441,8 @@ export async function runSetup(projectDir, config, flags) {
|
|
|
421
441
|
if (!existsSync(hooksDir)) mkdirSync(hooksDir, { recursive: true });
|
|
422
442
|
|
|
423
443
|
const hookContent = existsSync(preCommitHook)
|
|
424
|
-
? readFileSync(preCommitHook, 'utf-8') + '\n\n# DocGuard CDD validation\nnpx docguard guard --fail-on-warning\n'
|
|
425
|
-
: '#!/bin/sh\n\n# DocGuard CDD validation\nnpx docguard guard --fail-on-warning\n';
|
|
444
|
+
? readFileSync(preCommitHook, 'utf-8') + '\n\n# DocGuard CDD validation\nnpx docguard-cli guard --fail-on-warning\n'
|
|
445
|
+
: '#!/bin/sh\n\n# DocGuard CDD validation\nnpx docguard-cli guard --fail-on-warning\n';
|
|
426
446
|
|
|
427
447
|
writeFileSync(preCommitHook, hookContent, { mode: 0o755 });
|
|
428
448
|
console.log(` ${c.green}✅ Pre-commit hook installed${c.reset}`);
|
|
@@ -447,9 +467,19 @@ export async function runSetup(projectDir, config, flags) {
|
|
|
447
467
|
console.log(` ${c.dim}No changes made.${c.reset}`);
|
|
448
468
|
}
|
|
449
469
|
|
|
450
|
-
|
|
451
|
-
console.log(
|
|
452
|
-
|
|
453
|
-
|
|
470
|
+
const agentMode = detectAgentMode(projectDir);
|
|
471
|
+
console.log(`\n ${c.bold}Next steps:${c.reset} ${c.dim}(${agentMode === 'llm' ? 'LLM mode' : 'CLI mode'})${c.reset}`);
|
|
472
|
+
if (agentMode === 'llm') {
|
|
473
|
+
if (!isSpecKitInitialized(projectDir)) {
|
|
474
|
+
console.log(` ${c.dim}Bootstrap:${c.reset} ${c.cyan}/speckit.constitution${c.reset}`);
|
|
475
|
+
}
|
|
476
|
+
console.log(` ${c.dim}Fill docs:${c.reset} ${c.cyan}/docguard.guard${c.reset}`);
|
|
477
|
+
console.log(` ${c.dim}Fix issues:${c.reset} ${c.cyan}/docguard.fix${c.reset}`);
|
|
478
|
+
console.log(` ${c.dim}Review:${c.reset} ${c.cyan}/docguard.review${c.reset}`);
|
|
479
|
+
} else {
|
|
480
|
+
console.log(` ${c.dim}Fill docs:${c.reset} ${c.cyan}docguard diagnose${c.reset}`);
|
|
481
|
+
console.log(` ${c.dim}Validate:${c.reset} ${c.cyan}docguard guard${c.reset}`);
|
|
482
|
+
console.log(` ${c.dim}Check score:${c.reset} ${c.cyan}docguard score${c.reset}`);
|
|
483
|
+
}
|
|
454
484
|
console.log('');
|
|
455
485
|
}
|
package/cli/docguard.mjs
CHANGED
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* DocGuard CLI — The enforcement tool for Canonical-Driven Development (CDD)
|
|
5
5
|
*
|
|
6
|
-
* Zero dependencies. Pure Node.js.
|
|
6
|
+
* Zero NPM runtime dependencies. Pure Node.js.
|
|
7
7
|
*
|
|
8
8
|
* Usage:
|
|
9
|
-
* npx docguard audit — Scan project, report what docs exist/missing
|
|
10
|
-
* npx docguard init — Initialize CDD docs from templates
|
|
11
|
-
* npx docguard guard — Validate project against its canonical docs
|
|
12
|
-
* npx docguard --help — Show help
|
|
9
|
+
* npx docguard-cli audit — Scan project, report what docs exist/missing
|
|
10
|
+
* npx docguard-cli init — Initialize CDD docs from templates
|
|
11
|
+
* npx docguard-cli guard — Validate project against its canonical docs
|
|
12
|
+
* npx docguard-cli --help — Show help
|
|
13
13
|
*
|
|
14
14
|
* @see https://github.com/raccioly/docguard
|
|
15
15
|
*/
|
|
@@ -128,6 +128,15 @@ export function loadConfig(projectDir) {
|
|
|
128
128
|
...getProjectTypeDefaults(merged.projectType),
|
|
129
129
|
...(merged.projectTypeConfig || {}),
|
|
130
130
|
};
|
|
131
|
+
// Normalize testPattern (string) → testPatterns (array) for backward compat
|
|
132
|
+
if (merged.testPattern && !merged.testPatterns) {
|
|
133
|
+
merged.testPatterns = [merged.testPattern];
|
|
134
|
+
} else if (merged.testPattern && merged.testPatterns) {
|
|
135
|
+
// Both set — merge, deduplicate
|
|
136
|
+
if (!merged.testPatterns.includes(merged.testPattern)) {
|
|
137
|
+
merged.testPatterns.push(merged.testPattern);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
131
140
|
return merged;
|
|
132
141
|
} catch (e) {
|
|
133
142
|
console.error(`${c.red}Error parsing .docguard.json: ${e.message}${c.reset}`);
|
package/cli/ensure-skills.mjs
CHANGED
|
@@ -4,12 +4,16 @@
|
|
|
4
4
|
* Called before every command execution. If skills or commands are missing,
|
|
5
5
|
* copies them from the package's bundled assets into the project directory.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
7
|
+
* Also provides agent mode detection (LLM vs CLI) and spec-kit availability.
|
|
8
|
+
*
|
|
9
|
+
* Zero npm dependencies — pure Node.js built-ins only.
|
|
10
|
+
* Framework dependency: spec-kit (convention, not code).
|
|
8
11
|
*/
|
|
9
12
|
|
|
10
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync
|
|
11
|
-
import { resolve, dirname
|
|
13
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync } from 'node:fs';
|
|
14
|
+
import { resolve, dirname } from 'node:path';
|
|
12
15
|
import { fileURLToPath } from 'node:url';
|
|
16
|
+
import { execSync } from 'node:child_process';
|
|
13
17
|
import { c } from './shared.mjs';
|
|
14
18
|
|
|
15
19
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -23,23 +27,224 @@ const COMMANDS_SOURCE = resolve(__dirname, '..', 'commands');
|
|
|
23
27
|
const SKILLS_DEST = '.agent/skills';
|
|
24
28
|
const COMMANDS_DEST = 'commands';
|
|
25
29
|
|
|
30
|
+
// ── Agent Mode Detection ────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Detect if user is in LLM mode (AI agent) or CLI mode (terminal).
|
|
34
|
+
* DocGuard is LLM-first — defaults to 'llm' when any agent signal detected.
|
|
35
|
+
*
|
|
36
|
+
* @param {string} projectDir - The project root directory
|
|
37
|
+
* @returns {'llm' | 'cli'}
|
|
38
|
+
*/
|
|
39
|
+
export function detectAgentMode(projectDir) {
|
|
40
|
+
// First check .specify/init-options.json for explicit AI agent selection
|
|
41
|
+
const initOptions = resolve(projectDir, '.specify', 'init-options.json');
|
|
42
|
+
if (existsSync(initOptions)) {
|
|
43
|
+
try {
|
|
44
|
+
const opts = JSON.parse(readFileSync(initOptions, 'utf-8'));
|
|
45
|
+
if (opts.ai) return 'llm'; // spec-kit was initialized with an AI agent
|
|
46
|
+
} catch { /* ignore */ }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Check for LLM signal directories/files
|
|
50
|
+
const llmSignals = [
|
|
51
|
+
'.agent/skills',
|
|
52
|
+
'.cursor',
|
|
53
|
+
'.claude',
|
|
54
|
+
'.specify',
|
|
55
|
+
'.github/copilot-instructions.md',
|
|
56
|
+
'CLAUDE.md',
|
|
57
|
+
'.gemini',
|
|
58
|
+
'.agents',
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
for (const signal of llmSignals) {
|
|
62
|
+
if (existsSync(resolve(projectDir, signal))) return 'llm';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return 'cli';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Get the detected AI agent name from spec-kit init options.
|
|
70
|
+
* Returns null if spec-kit hasn't been initialized.
|
|
71
|
+
*
|
|
72
|
+
* @param {string} projectDir - The project root directory
|
|
73
|
+
* @returns {string | null}
|
|
74
|
+
*/
|
|
75
|
+
export function getDetectedAgent(projectDir) {
|
|
76
|
+
const initOptions = resolve(projectDir, '.specify', 'init-options.json');
|
|
77
|
+
if (existsSync(initOptions)) {
|
|
78
|
+
try {
|
|
79
|
+
const opts = JSON.parse(readFileSync(initOptions, 'utf-8'));
|
|
80
|
+
return opts.ai || null;
|
|
81
|
+
} catch { /* ignore */ }
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Detect which AI agent is in use, returning the spec-kit --ai flag value.
|
|
88
|
+
* Matches spec-kit's supported agents: agy, claude, copilot, cursor-agent,
|
|
89
|
+
* gemini, windsurf, codex, roo, etc.
|
|
90
|
+
*
|
|
91
|
+
* Priority: .specify/init-options.json > filesystem signals > null
|
|
92
|
+
*
|
|
93
|
+
* @param {string} projectDir - The project root directory
|
|
94
|
+
* @returns {string | null} - spec-kit --ai flag value, or null if unknown
|
|
95
|
+
*/
|
|
96
|
+
export function detectAIAgent(projectDir) {
|
|
97
|
+
// 1. Check spec-kit init options (already initialized — trust it)
|
|
98
|
+
const existing = getDetectedAgent(projectDir);
|
|
99
|
+
if (existing) return existing;
|
|
100
|
+
|
|
101
|
+
// 2. Map filesystem signals to spec-kit agent IDs
|
|
102
|
+
// Order matters: more specific signals first
|
|
103
|
+
const agentSignals = [
|
|
104
|
+
{ signal: '.cursor', agent: 'cursor-agent' },
|
|
105
|
+
{ signal: '.claude', agent: 'claude' },
|
|
106
|
+
{ signal: 'CLAUDE.md', agent: 'claude' },
|
|
107
|
+
{ signal: '.gemini', agent: 'gemini' },
|
|
108
|
+
{ signal: '.agents', agent: 'agy' }, // Antigravity
|
|
109
|
+
{ signal: '.github/copilot-instructions.md', agent: 'copilot' },
|
|
110
|
+
{ signal: '.windsurf', agent: 'windsurf' },
|
|
111
|
+
{ signal: '.codex', agent: 'codex' },
|
|
112
|
+
{ signal: '.roo', agent: 'roo' },
|
|
113
|
+
{ signal: '.amp', agent: 'amp' },
|
|
114
|
+
{ signal: '.kiro', agent: 'kiro-cli' },
|
|
115
|
+
{ signal: '.tabnine', agent: 'tabnine' },
|
|
116
|
+
];
|
|
117
|
+
|
|
118
|
+
for (const { signal, agent } of agentSignals) {
|
|
119
|
+
if (existsSync(resolve(projectDir, signal))) return agent;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// 3. No signal found — return null (caller decides: interactive vs generic)
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Check if the specify CLI (spec-kit) is available on PATH.
|
|
128
|
+
*
|
|
129
|
+
* @returns {boolean}
|
|
130
|
+
*/
|
|
131
|
+
export function isSpecKitAvailable() {
|
|
132
|
+
try {
|
|
133
|
+
const cmd = process.platform === 'win32' ? 'where specify' : 'which specify';
|
|
134
|
+
execSync(cmd, { encoding: 'utf-8', stdio: 'pipe', timeout: 3000 });
|
|
135
|
+
return true;
|
|
136
|
+
} catch {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
26
141
|
/**
|
|
27
|
-
*
|
|
142
|
+
* Check if spec-kit has been initialized in this project.
|
|
143
|
+
*
|
|
144
|
+
* @param {string} projectDir - The project root directory
|
|
145
|
+
* @returns {boolean}
|
|
146
|
+
*/
|
|
147
|
+
export function isSpecKitInitialized(projectDir) {
|
|
148
|
+
return existsSync(resolve(projectDir, '.specify', 'init-options.json'));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ── Spec-Kit Integration Gate ───────────────────────────────────────────
|
|
152
|
+
|
|
153
|
+
// Read DocGuard package version (for skill auto-update)
|
|
154
|
+
const PKG_VERSION = (() => {
|
|
155
|
+
try {
|
|
156
|
+
const pkg = JSON.parse(readFileSync(resolve(__dirname, '..', 'package.json'), 'utf-8'));
|
|
157
|
+
return pkg.version || '0.0.0';
|
|
158
|
+
} catch { return '0.0.0'; }
|
|
159
|
+
})();
|
|
160
|
+
|
|
161
|
+
const SPEC_KIT_INSTALL_CMD = 'uv tool install specify-cli --from git+https://github.com/github/spec-kit.git';
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Ensure spec-kit is initialized in the project.
|
|
165
|
+
* Called on every command run — this is the persistent nudge.
|
|
166
|
+
*
|
|
167
|
+
* - If .specify/ exists → do nothing
|
|
168
|
+
* - If specify CLI available → auto-run specify init with detected agent
|
|
169
|
+
* - If specify CLI not available → show prominent install reminder (every time)
|
|
170
|
+
*
|
|
171
|
+
* @param {string} projectDir - The project root directory
|
|
172
|
+
* @param {object} flags - CLI flags
|
|
173
|
+
* @returns {{ specKitReady: boolean }}
|
|
174
|
+
*/
|
|
175
|
+
export function ensureSpecKit(projectDir, flags = {}) {
|
|
176
|
+
const silent = flags.format === 'json';
|
|
177
|
+
|
|
178
|
+
// Already initialized — nothing to do
|
|
179
|
+
if (isSpecKitInitialized(projectDir)) {
|
|
180
|
+
return { specKitReady: true };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Spec-kit CLI available — auto-initialize
|
|
184
|
+
if (isSpecKitAvailable()) {
|
|
185
|
+
if (!silent) {
|
|
186
|
+
console.log(` ${c.cyan}🌱 Spec Kit detected — auto-initializing SDD workflow...${c.reset}`);
|
|
187
|
+
}
|
|
188
|
+
try {
|
|
189
|
+
const detectedAgent = detectAIAgent(projectDir);
|
|
190
|
+
const aiFlag = detectedAgent
|
|
191
|
+
? `--ai ${detectedAgent}`
|
|
192
|
+
: '--ai generic --ai-commands-dir .agent/commands/';
|
|
193
|
+
const scriptFlag = process.platform === 'win32' ? '--script ps' : '--script sh';
|
|
194
|
+
execSync(
|
|
195
|
+
`specify init --here --force ${aiFlag} --ai-skills --ignore-agent-tools --no-git ${scriptFlag}`,
|
|
196
|
+
{ cwd: projectDir, encoding: 'utf-8', stdio: 'pipe', timeout: 30000 }
|
|
197
|
+
);
|
|
198
|
+
if (!silent) {
|
|
199
|
+
console.log(` ${c.green}✅ Spec Kit initialized${c.reset} ${c.dim}(agent: ${detectedAgent || 'generic'}, 9 skills installed)${c.reset}\n`);
|
|
200
|
+
}
|
|
201
|
+
return { specKitReady: true };
|
|
202
|
+
} catch {
|
|
203
|
+
// Failed silently — will show reminder instead
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// No specify CLI — show prominent reminder (every time, no dismiss)
|
|
208
|
+
if (!silent) {
|
|
209
|
+
console.log(` ${c.yellow}┌─────────────────────────────────────────────────────────┐${c.reset}`);
|
|
210
|
+
console.log(` ${c.yellow}│${c.reset} ${c.bold}💡 Spec Kit not installed${c.reset} ${c.yellow}│${c.reset}`);
|
|
211
|
+
console.log(` ${c.yellow}│${c.reset} ${c.yellow}│${c.reset}`);
|
|
212
|
+
console.log(` ${c.yellow}│${c.reset} DocGuard is a Spec Kit extension. Install Spec Kit ${c.yellow}│${c.reset}`);
|
|
213
|
+
console.log(` ${c.yellow}│${c.reset} for the full experience: 13 AI skills, SDD workflow, ${c.yellow}│${c.reset}`);
|
|
214
|
+
console.log(` ${c.yellow}│${c.reset} project constitution, and seamless agent integration. ${c.yellow}│${c.reset}`);
|
|
215
|
+
console.log(` ${c.yellow}│${c.reset} ${c.yellow}│${c.reset}`);
|
|
216
|
+
console.log(` ${c.yellow}│${c.reset} ${c.cyan}${SPEC_KIT_INSTALL_CMD}${c.reset}`);
|
|
217
|
+
console.log(` ${c.yellow}│${c.reset} ${c.yellow}│${c.reset}`);
|
|
218
|
+
console.log(` ${c.yellow}│${c.reset} ${c.dim}Then run: ${c.cyan}docguard init${c.reset} ${c.yellow}│${c.reset}`);
|
|
219
|
+
console.log(` ${c.yellow}└─────────────────────────────────────────────────────────┘${c.reset}\n`);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return { specKitReady: false };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ── Skill Installation ──────────────────────────────────────────────────
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Silently ensure DocGuard skills and commands are installed in the project.
|
|
229
|
+
* Also checks spec-kit integration and auto-updates stale skills.
|
|
28
230
|
*
|
|
29
231
|
* @param {string} projectDir - The project root directory
|
|
30
232
|
* @param {object} flags - CLI flags (format, etc.)
|
|
31
|
-
* @returns {{ skillsInstalled: boolean, commandsInstalled: boolean }}
|
|
233
|
+
* @returns {{ skillsInstalled: boolean, commandsInstalled: boolean, specKitReady: boolean }}
|
|
32
234
|
*/
|
|
33
235
|
export function ensureSkills(projectDir, flags = {}) {
|
|
34
|
-
const result = { skillsInstalled: false, commandsInstalled: false };
|
|
236
|
+
const result = { skillsInstalled: false, commandsInstalled: false, specKitReady: false };
|
|
35
237
|
const silent = flags.format === 'json';
|
|
36
238
|
|
|
37
|
-
// ──
|
|
38
|
-
const
|
|
39
|
-
|
|
239
|
+
// ── Spec-Kit Gate (runs on every command) ─────────────────────────────
|
|
240
|
+
const specKitResult = ensureSpecKit(projectDir, flags);
|
|
241
|
+
result.specKitReady = specKitResult.specKitReady;
|
|
242
|
+
|
|
243
|
+
// ── DocGuard Skills (install + auto-update) ───────────────────────────
|
|
244
|
+
if (existsSync(SKILLS_SOURCE)) {
|
|
40
245
|
try {
|
|
41
246
|
const skillDirs = readdirSync(SKILLS_SOURCE).filter(d =>
|
|
42
|
-
existsSync(resolve(SKILLS_SOURCE, d, 'SKILL.md'))
|
|
247
|
+
d.startsWith('docguard-') && existsSync(resolve(SKILLS_SOURCE, d, 'SKILL.md'))
|
|
43
248
|
);
|
|
44
249
|
|
|
45
250
|
for (const skillDir of skillDirs) {
|
|
@@ -49,14 +254,26 @@ export function ensureSkills(projectDir, flags = {}) {
|
|
|
49
254
|
}
|
|
50
255
|
const srcSkill = resolve(SKILLS_SOURCE, skillDir, 'SKILL.md');
|
|
51
256
|
const destSkill = resolve(destDir, 'SKILL.md');
|
|
257
|
+
|
|
52
258
|
if (!existsSync(destSkill)) {
|
|
259
|
+
// New install
|
|
53
260
|
writeFileSync(destSkill, readFileSync(srcSkill, 'utf-8'), 'utf-8');
|
|
261
|
+
result.skillsInstalled = true;
|
|
262
|
+
} else {
|
|
263
|
+
// Auto-update: check if package version is newer than installed
|
|
264
|
+
const installedContent = readFileSync(destSkill, 'utf-8');
|
|
265
|
+
const versionMatch = installedContent.match(/docguard:version:\s*(\S+)/);
|
|
266
|
+
const installedVersion = versionMatch ? versionMatch[1] : '0.0.0';
|
|
267
|
+
|
|
268
|
+
if (installedVersion !== PKG_VERSION) {
|
|
269
|
+
writeFileSync(destSkill, readFileSync(srcSkill, 'utf-8'), 'utf-8');
|
|
270
|
+
result.skillsInstalled = true;
|
|
271
|
+
}
|
|
54
272
|
}
|
|
55
273
|
}
|
|
56
274
|
|
|
57
|
-
result.skillsInstalled
|
|
58
|
-
|
|
59
|
-
console.log(` ${c.cyan}✨ DocGuard AI skills installed → ${SKILLS_DEST}/${c.reset}`);
|
|
275
|
+
if (result.skillsInstalled && !silent) {
|
|
276
|
+
console.log(` ${c.cyan}✨ DocGuard AI skills installed/updated → ${SKILLS_DEST}/${c.reset}`);
|
|
60
277
|
}
|
|
61
278
|
} catch {
|
|
62
279
|
// Silent failure — skills are optional enhancement
|
|
@@ -94,3 +311,4 @@ export function ensureSkills(projectDir, flags = {}) {
|
|
|
94
311
|
|
|
95
312
|
return result;
|
|
96
313
|
}
|
|
314
|
+
|
package/cli/scanners/speckit.mjs
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* Credit: Integration with GitHub's Spec Kit framework
|
|
20
20
|
* (github.com/github/spec-kit)
|
|
21
21
|
*
|
|
22
|
-
* Zero dependencies — pure Node.js built-ins only.
|
|
22
|
+
* Zero NPM runtime dependencies — pure Node.js built-ins only.
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
25
|
import { existsSync, readFileSync, readdirSync, statSync, copyFileSync, writeFileSync } from 'node:fs';
|