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.
@@ -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: src/**/__tests__/ and src/**/*.test.* / src/**/*.spec.*
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
- const sourceRoots = ['src', 'app', 'lib', 'packages', 'modules'];
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 += 30;
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 += 20;
562
+ score += 15;
544
563
  const content = readFileSync(gitignorePath, 'utf-8');
545
- if (content.includes('.env')) score += 20;
564
+ if (content.includes('.env')) score += 15;
546
565
  }
547
566
 
548
- // No .env file committed (check if .env exists but .gitignore covers it)
549
- if (!existsSync(resolve(dir, '.env')) || existsSync(gitignorePath)) score += 15;
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 += 15; // Full marks — project doesn't need env vars
572
+ score += 10; // Full marks — project doesn't need env vars
554
573
  } else if (existsSync(resolve(dir, '.env.example'))) {
555
- score += 15;
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: 'Add tests/ directory and configure TEST-SPEC.md',
672
- security: 'Create SECURITY.md and add .env to .gitignore Run `docguard fix --doc security`',
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',
@@ -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 skillNames = ['docguard-guard', 'docguard-fix', 'docguard-review', 'docguard-score'];
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 skillNames) {
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 install = interactive
231
- ? await askYesNo(` → Install ${missingSkills.length} AI skill(s) to .agent/skills/?`)
232
- : true;
237
+ const hasSpeckitMissing = missingSkills.some(s => s.startsWith('speckit-'));
238
+ const hasDocguardMissing = missingSkills.some(s => s.startsWith('docguard-'));
233
239
 
234
- if (install) {
235
- for (const skill of missingSkills) {
236
- const srcSkill = resolve(SKILLS_SOURCE, skill, 'SKILL.md');
237
- const destDir = resolve(skillsDest, skill);
238
- if (existsSync(srcSkill)) {
239
- if (!existsSync(destDir)) mkdirSync(destDir, { recursive: true });
240
- writeFileSync(resolve(destDir, 'SKILL.md'), readFileSync(srcSkill, 'utf-8'), 'utf-8');
241
- console.log(` ${c.green}✅ Installed ${skill}${c.reset}`);
242
- configured++;
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 speckitDir = resolve(projectDir, '.speckit');
359
- const hasSpeckit = existsSync(speckitDir) || existsSync(resolve(projectDir, 'spec.md'));
360
- if (hasSpeckit) {
361
- console.log(` ${c.green}✅${c.reset} spec-kit ${c.dim}(spec-driven development configured)${c.reset}`);
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 — optional)${c.reset}`);
365
- console.log(` ${c.dim} Spec Kit enables spec-driven development with AI agents${c.reset}`);
366
- console.log(` ${c.dim} See: ${c.cyan}https://github.com/github/spec-kit${c.reset}`);
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
- console.log(`\n ${c.bold}Next steps:${c.reset}`);
451
- console.log(` ${c.dim}Fill docs:${c.reset} ${c.cyan}docguard diagnose${c.reset}`);
452
- console.log(` ${c.dim}Validate:${c.reset} ${c.cyan}docguard guard${c.reset}`);
453
- console.log(` ${c.dim}Check score:${c.reset} ${c.cyan}docguard score${c.reset}`);
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}`);
@@ -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
- * Zero dependencies pure Node.js built-ins only.
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, cpSync } from 'node:fs';
11
- import { resolve, dirname, join } from 'node:path';
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
- * Silently ensure skills and commands are installed in the project.
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
- // ── Skills ────────────────────────────────────────────────────────────
38
- const skillsCheck = resolve(projectDir, SKILLS_DEST, 'docguard-guard', 'SKILL.md');
39
- if (!existsSync(skillsCheck) && existsSync(SKILLS_SOURCE)) {
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 = true;
58
- if (!silent) {
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
+
@@ -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';