docguard-cli 0.9.6 → 0.9.7

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.
@@ -157,7 +157,12 @@ function diffEntities(dir) {
157
157
  'EntityName', 'Entity', 'metadata', 'tbd', 'cascade', 'fields',
158
158
  'purpose', 'version', 'author', 'example', 'TODO', 'Overview',
159
159
  'Revision', 'History', 'Entities', 'Relationships', 'Indexes',
160
- 'Migration', 'Strategy',
160
+ 'Migration', 'Strategy', 'Trade-offs', 'Tradeoffs', 'Notes',
161
+ 'Summary', 'Details', 'Configuration', 'Setup', 'Reference',
162
+ 'Appendix', 'Glossary', 'FAQ', 'Introduction', 'Background',
163
+ 'Prerequisites', 'Requirements', 'Assumptions', 'Constraints',
164
+ 'Dependencies', 'Architecture', 'Design', 'Implementation',
165
+ 'Testing', 'Deployment', 'Monitoring', 'Operations', 'Security',
161
166
  ]);
162
167
 
163
168
  const headerRegex = /^### (\S+)/gm;
@@ -165,9 +170,11 @@ function diffEntities(dir) {
165
170
  while ((match = headerRegex.exec(content)) !== null) {
166
171
  const name = match[1].replace(/[`*]/g, '');
167
172
  // Skip template placeholders (<!-- ... -->) and noise words
168
- if (name.startsWith('<!--') || name.length <= 1 || HEADER_NOISE.has(name) || HEADER_NOISE.has(name.toLowerCase())) {
173
+ if (name.startsWith('<!--') || name.length <= 2 || HEADER_NOISE.has(name) || HEADER_NOISE.has(name.toLowerCase())) {
169
174
  continue;
170
175
  }
176
+ // Skip hyphenated words (e.g., 'Trade-offs', 'Set-up') — these are section titles, not entities
177
+ if (name.includes('-')) continue;
171
178
  docEntities.add(name.toLowerCase());
172
179
  }
173
180
 
@@ -194,10 +201,16 @@ function diffEntities(dir) {
194
201
  // Common table headers and template words
195
202
  'true', 'false', 'header', 'checks', 'project', 'count', 'grade',
196
203
  'breakdown', 'issuecount', 'autofixable', 'projectname', 'projecttype',
204
+ // Common doc section words (not entity names)
205
+ 'trade', 'offs', 'tradeoffs', 'setup', 'overview', 'summary',
206
+ 'details', 'configuration', 'reference', 'pattern', 'patterns',
207
+ 'strategy', 'approach', 'impact', 'benefit', 'risk', 'concern',
208
+ 'action', 'result', 'outcome', 'inverted', 'composite', 'secondary',
197
209
  ]);
198
210
  while ((match = tableRegex.exec(content)) !== null) {
199
211
  const name = match[1];
200
- if (name.length > 2 && !TABLE_NOISE.has(name.toLowerCase())) {
212
+ // Skip short names (<=3 chars) and noise words
213
+ if (name.length > 3 && !TABLE_NOISE.has(name.toLowerCase())) {
201
214
  docEntities.add(name.toLowerCase());
202
215
  }
203
216
  }
@@ -8,6 +8,7 @@ import { resolve, dirname } from 'node:path';
8
8
  import { fileURLToPath } from 'node:url';
9
9
  import { createInterface } from 'node:readline';
10
10
  import { c, PROFILES } from '../shared.mjs';
11
+ import { ensureSkills } from '../ensure-skills.mjs';
11
12
 
12
13
  function detectProjectType(dir) {
13
14
  const pkgPath = resolve(dir, 'package.json');
@@ -285,4 +286,7 @@ export async function runInit(projectDir, config, flags) {
285
286
  } else {
286
287
  console.log(`\n ${c.dim}Run${c.reset} ${c.cyan}docguard diagnose${c.reset} ${c.dim}to check for issues.${c.reset}\n`);
287
288
  }
289
+
290
+ // Auto-install skills and commands
291
+ ensureSkills(projectDir, flags);
288
292
  }
@@ -0,0 +1,455 @@
1
+ /**
2
+ * Setup Command — Interactive onboarding wizard for DocGuard
3
+ *
4
+ * Walks through 7 steps to ensure DocGuard is fully configured:
5
+ * 1. Project detection & config
6
+ * 2. Canonical docs
7
+ * 3. AI skills
8
+ * 4. Slash commands
9
+ * 5. Agent configs
10
+ * 6. External integrations (spec-kit, understanding)
11
+ * 7. Git hooks
12
+ *
13
+ * Each step shows current status (✅/⚠️) and offers to fix what's missing.
14
+ * Supports --skip-prompts for non-interactive CI mode.
15
+ *
16
+ * Zero dependencies — pure Node.js built-ins only.
17
+ */
18
+
19
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync } from 'node:fs';
20
+ import { resolve, dirname, basename } from 'node:path';
21
+ import { fileURLToPath } from 'node:url';
22
+ import { createInterface } from 'node:readline';
23
+ import { execSync } from 'node:child_process';
24
+ import { c } from '../shared.mjs';
25
+ import { ensureSkills } from '../ensure-skills.mjs';
26
+
27
+ const __filename = fileURLToPath(import.meta.url);
28
+ const __dirname = dirname(__filename);
29
+ const TEMPLATES_DIR = resolve(__dirname, '../../templates');
30
+ const SKILLS_SOURCE = resolve(__dirname, '../../extensions/spec-kit-docguard/skills');
31
+ const COMMANDS_SOURCE = resolve(__dirname, '../../commands');
32
+
33
+ // ── Readline Helper ─────────────────────────────────────────────────────
34
+
35
+ function askQuestion(prompt) {
36
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
37
+ return new Promise(res => {
38
+ rl.question(prompt, answer => {
39
+ rl.close();
40
+ res(answer.trim().toLowerCase());
41
+ });
42
+ });
43
+ }
44
+
45
+ async function askYesNo(prompt, defaultYes = true) {
46
+ const label = defaultYes ? 'Y/n' : 'y/N';
47
+ const answer = await askQuestion(`${prompt} [${label}]: `);
48
+ if (answer === '') return defaultYes;
49
+ return answer === 'y' || answer === 'yes';
50
+ }
51
+
52
+ // ── Project Type Detection ──────────────────────────────────────────────
53
+
54
+ function detectProjectType(dir) {
55
+ const pkgPath = resolve(dir, 'package.json');
56
+ if (existsSync(pkgPath)) {
57
+ try {
58
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
59
+ const allDeps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
60
+ if (pkg.bin) return 'cli';
61
+ if (allDeps.next || allDeps.react || allDeps.vue || allDeps['@angular/core'] ||
62
+ allDeps.svelte || allDeps.nuxt) return 'webapp';
63
+ if (allDeps.express || allDeps.fastify || allDeps.hono || allDeps.koa) return 'api';
64
+ if (pkg.main || pkg.exports || pkg.module) return 'library';
65
+ } catch { /* fall through */ }
66
+ }
67
+ if (existsSync(resolve(dir, 'manage.py'))) return 'webapp';
68
+ if (existsSync(resolve(dir, 'setup.py')) || existsSync(resolve(dir, 'pyproject.toml'))) return 'library';
69
+ return 'unknown';
70
+ }
71
+
72
+ // ── CLI Detection ───────────────────────────────────────────────────────
73
+
74
+ function isCliAvailable(name) {
75
+ try {
76
+ const cmd = process.platform === 'win32' ? `where ${name}` : `which ${name}`;
77
+ execSync(`${cmd} 2>/dev/null`, { encoding: 'utf-8', timeout: 3000 });
78
+ return true;
79
+ } catch {
80
+ return false;
81
+ }
82
+ }
83
+
84
+ function detectAgentDirs(projectDir) {
85
+ const agentDirs = [
86
+ { name: 'GitHub Copilot', dir: '.github', commandsPath: '.github/commands' },
87
+ { name: 'Cursor', dir: '.cursor', commandsPath: '.cursor/rules' },
88
+ { name: 'Google Gemini', dir: '.gemini', commandsPath: '.gemini/commands' },
89
+ { name: 'Claude Code', dir: '.claude', commandsPath: '.claude/commands' },
90
+ { name: 'Antigravity', dir: '.agents', commandsPath: '.agents/workflows' },
91
+ ];
92
+
93
+ return agentDirs.filter(a => existsSync(resolve(projectDir, a.dir)));
94
+ }
95
+
96
+ // ── Main Setup Wizard ───────────────────────────────────────────────────
97
+
98
+ export async function runSetup(projectDir, config, flags) {
99
+ console.log(`${c.bold}🧙 DocGuard Setup Wizard${c.reset}`);
100
+ console.log(`${c.dim} Directory: ${projectDir}${c.reset}\n`);
101
+
102
+ const interactive = !flags.skipPrompts;
103
+ let configured = 0;
104
+ let alreadyGood = 0;
105
+
106
+ // ── Step 1: Project Detection & Config ──────────────────────────────
107
+
108
+ console.log(` ${c.bold}Step 1/7: Project Detection${c.reset}`);
109
+
110
+ const detectedType = detectProjectType(projectDir);
111
+ console.log(` ${c.green}✅${c.reset} Project type: ${c.cyan}${detectedType}${c.reset}`);
112
+
113
+ const configPath = resolve(projectDir, '.docguard.json');
114
+ if (existsSync(configPath)) {
115
+ const cfg = JSON.parse(readFileSync(configPath, 'utf-8'));
116
+ console.log(` ${c.green}✅${c.reset} .docguard.json exists (profile: ${c.cyan}${cfg.profile || 'standard'}${c.reset})`);
117
+ alreadyGood++;
118
+ } else {
119
+ console.log(` ${c.yellow}⚠️${c.reset} .docguard.json missing`);
120
+ const create = interactive
121
+ ? await askYesNo(` → Create config file?`)
122
+ : true;
123
+
124
+ if (create) {
125
+ const typeDefaults = {
126
+ cli: { needsEnvVars: false, needsEnvExample: false, needsE2E: false, needsDatabase: false },
127
+ library: { needsEnvVars: false, needsEnvExample: false, needsE2E: false, needsDatabase: false },
128
+ webapp: { needsEnvVars: true, needsEnvExample: true, needsE2E: true, needsDatabase: true },
129
+ api: { needsEnvVars: true, needsEnvExample: true, needsE2E: false, needsDatabase: true },
130
+ unknown: { needsEnvVars: true, needsEnvExample: true, needsE2E: false, needsDatabase: true },
131
+ };
132
+
133
+ const defaultConfig = {
134
+ projectName: config.projectName,
135
+ version: '0.4',
136
+ profile: 'standard',
137
+ projectType: detectedType,
138
+ projectTypeConfig: typeDefaults[detectedType] || typeDefaults.unknown,
139
+ };
140
+
141
+ writeFileSync(configPath, JSON.stringify(defaultConfig, null, 2) + '\n', 'utf-8');
142
+ console.log(` ${c.green}✅ Created .docguard.json${c.reset}`);
143
+ configured++;
144
+ }
145
+ }
146
+
147
+ console.log('');
148
+
149
+ // ── Step 2: Canonical Docs ──────────────────────────────────────────
150
+
151
+ console.log(` ${c.bold}Step 2/7: Canonical Docs${c.reset}`);
152
+
153
+ const canonicalDocs = [
154
+ { file: 'docs-canonical/ARCHITECTURE.md', template: 'ARCHITECTURE.md.template', label: 'Architecture', defaultYes: true },
155
+ { file: 'docs-canonical/DATA-MODEL.md', template: 'DATA-MODEL.md.template', label: 'Data Model', defaultYes: ['webapp', 'api'].includes(detectedType) },
156
+ { file: 'docs-canonical/SECURITY.md', template: 'SECURITY.md.template', label: 'Security', defaultYes: ['webapp', 'api'].includes(detectedType) },
157
+ { file: 'docs-canonical/TEST-SPEC.md', template: 'TEST-SPEC.md.template', label: 'Test Spec', defaultYes: true },
158
+ { file: 'docs-canonical/ENVIRONMENT.md', template: 'ENVIRONMENT.md.template', label: 'Environment', defaultYes: ['webapp', 'api'].includes(detectedType) },
159
+ { file: 'docs-canonical/REQUIREMENTS.md', template: 'REQUIREMENTS.md.template', label: 'Requirements', defaultYes: true },
160
+ ];
161
+
162
+ const trackingFiles = [
163
+ { file: 'AGENTS.md', template: 'AGENTS.md.template', label: 'Agent Instructions' },
164
+ { file: 'CHANGELOG.md', template: 'CHANGELOG.md.template', label: 'Changelog' },
165
+ { file: 'DRIFT-LOG.md', template: 'DRIFT-LOG.md.template', label: 'Drift Log' },
166
+ ];
167
+
168
+ let missingDocs = [];
169
+
170
+ // Check canonical docs
171
+ for (const doc of [...canonicalDocs, ...trackingFiles]) {
172
+ const fullPath = resolve(projectDir, doc.file);
173
+ if (existsSync(fullPath)) {
174
+ console.log(` ${c.green}✅${c.reset} ${doc.file}`);
175
+ alreadyGood++;
176
+ } else {
177
+ console.log(` ${c.yellow}⚠️${c.reset} ${doc.file} ${c.dim}(missing)${c.reset}`);
178
+ missingDocs.push(doc);
179
+ }
180
+ }
181
+
182
+ if (missingDocs.length > 0) {
183
+ const create = interactive
184
+ ? await askYesNo(` → Create ${missingDocs.length} missing doc(s) from templates?`)
185
+ : true;
186
+
187
+ if (create) {
188
+ const today = new Date().toISOString().split('T')[0];
189
+ for (const doc of missingDocs) {
190
+ const destPath = resolve(projectDir, doc.file);
191
+ const templatePath = resolve(TEMPLATES_DIR, doc.template);
192
+
193
+ const destDir = dirname(destPath);
194
+ if (!existsSync(destDir)) {
195
+ mkdirSync(destDir, { recursive: true });
196
+ }
197
+
198
+ if (existsSync(templatePath)) {
199
+ const content = readFileSync(templatePath, 'utf-8').replace(/YYYY-MM-DD/g, today);
200
+ writeFileSync(destPath, content, 'utf-8');
201
+ console.log(` ${c.green}✅ Created ${doc.file}${c.reset}`);
202
+ configured++;
203
+ }
204
+ }
205
+ }
206
+ }
207
+
208
+ console.log('');
209
+
210
+ // ── Step 3: AI Skills ──────────────────────────────────────────────
211
+
212
+ console.log(` ${c.bold}Step 3/7: AI Skills${c.reset}`);
213
+
214
+ const skillNames = ['docguard-guard', 'docguard-fix', 'docguard-review', 'docguard-score'];
215
+ const skillsDest = resolve(projectDir, '.agent/skills');
216
+ let missingSkills = [];
217
+
218
+ for (const skill of skillNames) {
219
+ const skillPath = resolve(skillsDest, skill, 'SKILL.md');
220
+ if (existsSync(skillPath)) {
221
+ console.log(` ${c.green}✅${c.reset} ${skill}`);
222
+ alreadyGood++;
223
+ } else {
224
+ console.log(` ${c.yellow}⚠️${c.reset} ${skill} ${c.dim}(not installed)${c.reset}`);
225
+ missingSkills.push(skill);
226
+ }
227
+ }
228
+
229
+ if (missingSkills.length > 0) {
230
+ const install = interactive
231
+ ? await askYesNo(` → Install ${missingSkills.length} AI skill(s) to .agent/skills/?`)
232
+ : true;
233
+
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++;
243
+ }
244
+ }
245
+ }
246
+ }
247
+
248
+ console.log('');
249
+
250
+ // ── Step 4: Slash Commands ─────────────────────────────────────────
251
+
252
+ console.log(` ${c.bold}Step 4/7: Slash Commands${c.reset}`);
253
+
254
+ // Check root commands/ dir
255
+ const rootCommandsDir = resolve(projectDir, 'commands');
256
+ const rootCommandsExist = existsSync(resolve(rootCommandsDir, 'docguard.guard.md'));
257
+
258
+ if (rootCommandsExist) {
259
+ console.log(` ${c.green}✅${c.reset} commands/ ${c.dim}(root)${c.reset}`);
260
+ alreadyGood++;
261
+ } else {
262
+ console.log(` ${c.yellow}⚠️${c.reset} commands/ ${c.dim}(not installed)${c.reset}`);
263
+ }
264
+
265
+ // Detect agent directories and sync commands
266
+ const detectedAgents = detectAgentDirs(projectDir);
267
+ let unsyncedAgents = [];
268
+
269
+ for (const agent of detectedAgents) {
270
+ const agentCommandCheck = resolve(projectDir, agent.commandsPath, 'docguard.guard.md');
271
+ if (existsSync(agentCommandCheck)) {
272
+ console.log(` ${c.green}✅${c.reset} ${agent.commandsPath}/ ${c.dim}(${agent.name})${c.reset}`);
273
+ alreadyGood++;
274
+ } else {
275
+ console.log(` ${c.yellow}⚠️${c.reset} ${agent.commandsPath}/ ${c.dim}(${agent.name} — not synced)${c.reset}`);
276
+ unsyncedAgents.push(agent);
277
+ }
278
+ }
279
+
280
+ const needsCommands = !rootCommandsExist || unsyncedAgents.length > 0;
281
+
282
+ if (needsCommands && existsSync(COMMANDS_SOURCE)) {
283
+ const install = interactive
284
+ ? await askYesNo(` → Install/sync slash commands?`)
285
+ : true;
286
+
287
+ if (install) {
288
+ const commandFiles = readdirSync(COMMANDS_SOURCE).filter(f => f.endsWith('.md'));
289
+
290
+ // Install to root commands/
291
+ if (!rootCommandsExist) {
292
+ if (!existsSync(rootCommandsDir)) mkdirSync(rootCommandsDir, { recursive: true });
293
+ for (const file of commandFiles) {
294
+ const destPath = resolve(rootCommandsDir, file);
295
+ if (!existsSync(destPath)) {
296
+ writeFileSync(destPath, readFileSync(resolve(COMMANDS_SOURCE, file), 'utf-8'), 'utf-8');
297
+ }
298
+ }
299
+ console.log(` ${c.green}✅ Installed to commands/${c.reset}`);
300
+ configured++;
301
+ }
302
+
303
+ // Sync to agent-specific dirs
304
+ for (const agent of unsyncedAgents) {
305
+ const destDir = resolve(projectDir, agent.commandsPath);
306
+ if (!existsSync(destDir)) mkdirSync(destDir, { recursive: true });
307
+ for (const file of commandFiles) {
308
+ const destPath = resolve(destDir, file);
309
+ if (!existsSync(destPath)) {
310
+ writeFileSync(destPath, readFileSync(resolve(COMMANDS_SOURCE, file), 'utf-8'), 'utf-8');
311
+ }
312
+ }
313
+ console.log(` ${c.green}✅ Synced to ${agent.commandsPath}/ (${agent.name})${c.reset}`);
314
+ configured++;
315
+ }
316
+ }
317
+ }
318
+
319
+ console.log('');
320
+
321
+ // ── Step 5: Agent Configs ──────────────────────────────────────────
322
+
323
+ console.log(` ${c.bold}Step 5/7: Agent Configs${c.reset}`);
324
+
325
+ const agentConfigs = [
326
+ { file: 'AGENTS.md', label: 'Agent Instructions' },
327
+ { file: 'CLAUDE.md', label: 'Claude Code' },
328
+ { file: '.cursor/rules/cdd.mdc', label: 'Cursor' },
329
+ { file: '.github/copilot-instructions.md', label: 'GitHub Copilot' },
330
+ ];
331
+
332
+ let missingConfigs = [];
333
+ for (const cfg of agentConfigs) {
334
+ const fullPath = resolve(projectDir, cfg.file);
335
+ if (existsSync(fullPath)) {
336
+ console.log(` ${c.green}✅${c.reset} ${cfg.file} ${c.dim}(${cfg.label})${c.reset}`);
337
+ alreadyGood++;
338
+ } else {
339
+ // AGENTS.md is handled in step 2, skip it here
340
+ if (cfg.file !== 'AGENTS.md') {
341
+ console.log(` ${c.dim}──${c.reset} ${cfg.file} ${c.dim}(${cfg.label} — not generated)${c.reset}`);
342
+ missingConfigs.push(cfg);
343
+ }
344
+ }
345
+ }
346
+
347
+ if (missingConfigs.length > 0) {
348
+ console.log(` ${c.dim} Run ${c.cyan}docguard agents${c.dim} to generate agent-specific configs${c.reset}`);
349
+ }
350
+
351
+ console.log('');
352
+
353
+ // ── Step 6: Integrations ───────────────────────────────────────────
354
+
355
+ console.log(` ${c.bold}Step 6/7: Integrations${c.reset}`);
356
+
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}`);
362
+ alreadyGood++;
363
+ } 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}`);
367
+ }
368
+
369
+ // Check for spec-kit extensions
370
+ const extensionsDir = resolve(projectDir, 'extensions');
371
+
372
+ // DocGuard extension (this project IS DocGuard, so check if extension is bundled)
373
+ const docguardExt = resolve(extensionsDir, 'spec-kit-docguard', 'extension.yml');
374
+ if (existsSync(docguardExt)) {
375
+ console.log(` ${c.green}✅${c.reset} docguard extension ${c.dim}(spec-kit CDD enforcement)${c.reset}`);
376
+ alreadyGood++;
377
+ } else {
378
+ // DocGuard is installed as a CLI, not necessarily as a spec-kit extension
379
+ console.log(` ${c.green}✅${c.reset} docguard CLI ${c.dim}(standalone — 19 validators + 31 quality metrics)${c.reset}`);
380
+ alreadyGood++;
381
+ }
382
+
383
+ // Understanding extension (spec-kit community extension)
384
+ const understandingExt = resolve(extensionsDir, 'spec-kit-understanding', 'extension.yml');
385
+ if (existsSync(understandingExt)) {
386
+ console.log(` ${c.green}✅${c.reset} understanding ${c.dim}(spec-kit deep doc analysis)${c.reset}`);
387
+ alreadyGood++;
388
+ } else {
389
+ console.log(` ${c.dim}──${c.reset} understanding ${c.dim}(spec-kit extension — optional)${c.reset}`);
390
+ console.log(` ${c.dim} Install via spec-kit: ${c.cyan}https://github.com/github/spec-kit/tree/main/extensions${c.reset}`);
391
+ }
392
+
393
+ console.log('');
394
+
395
+ // ── Step 7: Git Hooks ──────────────────────────────────────────────
396
+
397
+ console.log(` ${c.bold}Step 7/7: Git Hooks${c.reset}`);
398
+
399
+ const gitDir = resolve(projectDir, '.git');
400
+ if (!existsSync(gitDir)) {
401
+ console.log(` ${c.dim}──${c.reset} No .git directory ${c.dim}(not a git repo)${c.reset}`);
402
+ } else {
403
+ const preCommitHook = resolve(gitDir, 'hooks', 'pre-commit');
404
+ let hasDocguardHook = false;
405
+
406
+ if (existsSync(preCommitHook)) {
407
+ const content = readFileSync(preCommitHook, 'utf-8');
408
+ hasDocguardHook = content.includes('docguard');
409
+ }
410
+
411
+ if (hasDocguardHook) {
412
+ console.log(` ${c.green}✅${c.reset} pre-commit hook ${c.dim}(docguard guard)${c.reset}`);
413
+ alreadyGood++;
414
+ } else {
415
+ console.log(` ${c.dim}──${c.reset} pre-commit hook ${c.dim}(not installed)${c.reset}`);
416
+ if (interactive) {
417
+ const install = await askYesNo(` → Install docguard guard as pre-commit hook?`, false);
418
+ if (install) {
419
+ try {
420
+ const hooksDir = resolve(gitDir, 'hooks');
421
+ if (!existsSync(hooksDir)) mkdirSync(hooksDir, { recursive: true });
422
+
423
+ 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';
426
+
427
+ writeFileSync(preCommitHook, hookContent, { mode: 0o755 });
428
+ console.log(` ${c.green}✅ Pre-commit hook installed${c.reset}`);
429
+ configured++;
430
+ } catch (e) {
431
+ console.log(` ${c.yellow}⚠️ Failed to install hook: ${e.message}${c.reset}`);
432
+ }
433
+ }
434
+ }
435
+ }
436
+ }
437
+
438
+ // ── Summary ────────────────────────────────────────────────────────
439
+
440
+ console.log(`\n ${c.bold}─────────────────────────────────────${c.reset}`);
441
+
442
+ if (configured > 0) {
443
+ console.log(` ${c.green}✅ Setup complete!${c.reset} ${configured} item(s) configured, ${alreadyGood} already good.`);
444
+ } else if (alreadyGood > 0) {
445
+ console.log(` ${c.green}✅ Everything is set up!${c.reset} ${alreadyGood} item(s) verified.`);
446
+ } else {
447
+ console.log(` ${c.dim}No changes made.${c.reset}`);
448
+ }
449
+
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}`);
454
+ console.log('');
455
+ }
package/cli/docguard.mjs CHANGED
@@ -38,6 +38,8 @@ import { runDiagnose } from './commands/diagnose.mjs';
38
38
  import { runPublish } from './commands/publish.mjs';
39
39
  import { runTrace } from './commands/trace.mjs';
40
40
  import { runLlms } from './commands/llms.mjs';
41
+ import { runSetup } from './commands/setup.mjs';
42
+ import { ensureSkills } from './ensure-skills.mjs';
41
43
 
42
44
  // ── Shared constants (imported to break circular dependencies) ──────────
43
45
  import { c, PROFILES } from './shared.mjs';
@@ -218,6 +220,7 @@ function printHelp() {
218
220
 
219
221
  ${c.bold}Getting Started:${c.reset}
220
222
  ${c.green}init${c.reset} Initialize CDD docs (interactive setup)
223
+ ${c.green}setup${c.reset} Full onboarding wizard (skills, integrations, hooks)
221
224
  ${c.green}generate${c.reset} Reverse-engineer canonical docs from existing code
222
225
 
223
226
  ${c.bold}Enforcement:${c.reset}
@@ -376,6 +379,11 @@ async function main() {
376
379
 
377
380
  const config = loadConfig(projectDir);
378
381
 
382
+ // Silent auto-check: install skills/commands if missing
383
+ if (command !== 'setup' && command !== 'init') {
384
+ ensureSkills(projectDir, flags);
385
+ }
386
+
379
387
  switch (command) {
380
388
  case 'audit':
381
389
  // audit is an alias for guard — guard does everything the old audit did + 50 more checks
@@ -384,6 +392,10 @@ async function main() {
384
392
  case 'init':
385
393
  await runInit(projectDir, config, flags);
386
394
  break;
395
+ case 'setup':
396
+ case 'onboard':
397
+ await runSetup(projectDir, config, flags);
398
+ break;
387
399
  case 'guard':
388
400
  runGuard(projectDir, config, flags);
389
401
  break;
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Ensure Skills — Silent auto-check for DocGuard AI skills and commands
3
+ *
4
+ * Called before every command execution. If skills or commands are missing,
5
+ * copies them from the package's bundled assets into the project directory.
6
+ *
7
+ * Zero dependencies — pure Node.js built-ins only.
8
+ */
9
+
10
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, cpSync } from 'node:fs';
11
+ import { resolve, dirname, join } from 'node:path';
12
+ import { fileURLToPath } from 'node:url';
13
+ import { c } from './shared.mjs';
14
+
15
+ const __filename = fileURLToPath(import.meta.url);
16
+ const __dirname = dirname(__filename);
17
+
18
+ // Source locations in the npm package
19
+ const SKILLS_SOURCE = resolve(__dirname, '..', 'extensions', 'spec-kit-docguard', 'skills');
20
+ const COMMANDS_SOURCE = resolve(__dirname, '..', 'commands');
21
+
22
+ // Destination in the user's project
23
+ const SKILLS_DEST = '.agent/skills';
24
+ const COMMANDS_DEST = 'commands';
25
+
26
+ /**
27
+ * Silently ensure skills and commands are installed in the project.
28
+ *
29
+ * @param {string} projectDir - The project root directory
30
+ * @param {object} flags - CLI flags (format, etc.)
31
+ * @returns {{ skillsInstalled: boolean, commandsInstalled: boolean }}
32
+ */
33
+ export function ensureSkills(projectDir, flags = {}) {
34
+ const result = { skillsInstalled: false, commandsInstalled: false };
35
+ const silent = flags.format === 'json';
36
+
37
+ // ── Skills ────────────────────────────────────────────────────────────
38
+ const skillsCheck = resolve(projectDir, SKILLS_DEST, 'docguard-guard', 'SKILL.md');
39
+ if (!existsSync(skillsCheck) && existsSync(SKILLS_SOURCE)) {
40
+ try {
41
+ const skillDirs = readdirSync(SKILLS_SOURCE).filter(d =>
42
+ existsSync(resolve(SKILLS_SOURCE, d, 'SKILL.md'))
43
+ );
44
+
45
+ for (const skillDir of skillDirs) {
46
+ const destDir = resolve(projectDir, SKILLS_DEST, skillDir);
47
+ if (!existsSync(destDir)) {
48
+ mkdirSync(destDir, { recursive: true });
49
+ }
50
+ const srcSkill = resolve(SKILLS_SOURCE, skillDir, 'SKILL.md');
51
+ const destSkill = resolve(destDir, 'SKILL.md');
52
+ if (!existsSync(destSkill)) {
53
+ writeFileSync(destSkill, readFileSync(srcSkill, 'utf-8'), 'utf-8');
54
+ }
55
+ }
56
+
57
+ result.skillsInstalled = true;
58
+ if (!silent) {
59
+ console.log(` ${c.cyan}✨ DocGuard AI skills installed → ${SKILLS_DEST}/${c.reset}`);
60
+ }
61
+ } catch {
62
+ // Silent failure — skills are optional enhancement
63
+ }
64
+ }
65
+
66
+ // ── Slash Commands ────────────────────────────────────────────────────
67
+ const commandsCheck = resolve(projectDir, COMMANDS_DEST, 'docguard.guard.md');
68
+ if (!existsSync(commandsCheck) && existsSync(COMMANDS_SOURCE)) {
69
+ try {
70
+ const commandFiles = readdirSync(COMMANDS_SOURCE).filter(f => f.endsWith('.md'));
71
+
72
+ if (commandFiles.length > 0) {
73
+ const destDir = resolve(projectDir, COMMANDS_DEST);
74
+ if (!existsSync(destDir)) {
75
+ mkdirSync(destDir, { recursive: true });
76
+ }
77
+
78
+ for (const file of commandFiles) {
79
+ const destPath = resolve(destDir, file);
80
+ if (!existsSync(destPath)) {
81
+ writeFileSync(destPath, readFileSync(resolve(COMMANDS_SOURCE, file), 'utf-8'), 'utf-8');
82
+ }
83
+ }
84
+
85
+ result.commandsInstalled = true;
86
+ if (!silent) {
87
+ console.log(` ${c.cyan}✨ DocGuard slash commands installed → ${COMMANDS_DEST}/${c.reset}`);
88
+ }
89
+ }
90
+ } catch {
91
+ // Silent failure — commands are optional enhancement
92
+ }
93
+ }
94
+
95
+ return result;
96
+ }
@@ -33,8 +33,8 @@ const THRESHOLDS = {
33
33
  passiveVoiceRatio: { warn: 0.25, label: 'Passive voice ratio' }, // >25% passive = warn
34
34
  ambiguousPronounRatio: { warn: 0.15, label: 'Ambiguous pronoun ratio' }, // >15% ambiguous pronouns = warn
35
35
  atomicityScore: { warn: 0.35, label: 'Non-atomic sentence ratio' }, // >35% compound sentences = warn
36
- fleschReadingEase: { warn: 15, label: 'Flesch reading ease' }, // <15 = truly unreadable prose
37
- fleschKincaidGrade: { warn: 18, label: 'Flesch-Kincaid grade' }, // >18 = graduate level+
36
+ fleschReadingEase: { warn: 5, label: 'Flesch reading ease' }, // <5 = truly unreadable prose (tech docs typically score 10-30)
37
+ fleschKincaidGrade: { warn: 22, label: 'Flesch-Kincaid grade' }, // >22 = PhD level+ (tech docs typically 14-20)
38
38
  avgSentenceLength: { warn: 30, label: 'Avg sentence length' }, // >30 words = too long
39
39
  negationLoad: { warn: 0.20, label: 'Negation load' }, // >20% sentences with negation = warn
40
40
  conditionalLoad: { warn: 0.30, label: 'Conditional load' }, // >30% sentences conditional = warn
@@ -118,18 +118,53 @@ export function validateDocsSync(projectDir, config) {
118
118
  if (!['.ts', '.js', '.mjs'].includes(ext)) continue;
119
119
 
120
120
  // Skip index/middleware files
121
- const name = basename(file, ext).toLowerCase();
122
- if (name === 'index' || name === 'middleware' || name.startsWith('_')) continue;
121
+ const rawName = basename(file, ext).toLowerCase();
122
+ if (rawName === 'index' || rawName === 'middleware' || rawName.startsWith('_')) continue;
123
123
 
124
124
  results.total++;
125
125
 
126
- // Check if a likely route path exists in the OpenAPI spec
127
- // Route file "users.ts" → check for "/users" in spec
128
- if (openapiContent.includes(`/${name}`) || openapiContent.includes(`"${name}"`)) {
126
+ // Strategy 1: Parse the route file for actual route paths
127
+ // Look for router.get('/path'), app.post('/path'), etc.
128
+ let routeFileContent = '';
129
+ try { routeFileContent = readFileSync(file, 'utf-8').toLowerCase(); } catch { /* skip */ }
130
+
131
+ const actualRoutes = [];
132
+ const routeDefRegex = /(?:router|app|route)\s*\.\s*(?:get|post|put|delete|patch|all|use)\s*\(\s*['"`](\/[^'"`]*)['"`]/gi;
133
+ let routeMatch;
134
+ while ((routeMatch = routeDefRegex.exec(routeFileContent)) !== null) {
135
+ actualRoutes.push(routeMatch[1]);
136
+ }
137
+
138
+ let matched = false;
139
+
140
+ if (actualRoutes.length > 0) {
141
+ // Check if ANY of the actual route paths appear in the OpenAPI spec
142
+ matched = actualRoutes.some(route => {
143
+ // Normalize: /api/conversations/:id → /api/conversations
144
+ const basePath = route.replace(/\/:[^/]+/g, '').replace(/\/{[^}]+}/g, '');
145
+ return openapiContent.includes(basePath) || openapiContent.includes(route);
146
+ });
147
+ } else {
148
+ // Strategy 2 (fallback): Strip common suffixes and check filename
149
+ // userRoutes.ts → 'user', conversationRoutes.ts → 'conversation'
150
+ const cleanName = rawName
151
+ .replace(/routes?$/i, '')
152
+ .replace(/controllers?$/i, '')
153
+ .replace(/handlers?$/i, '')
154
+ .replace(/router$/i, '');
155
+
156
+ if (cleanName.length > 0) {
157
+ matched = openapiContent.includes(`/${cleanName}`) ||
158
+ openapiContent.includes(`"${cleanName}"`) ||
159
+ openapiContent.includes(`'${cleanName}'`);
160
+ }
161
+ }
162
+
163
+ if (matched) {
129
164
  results.passed++;
130
165
  } else {
131
166
  results.warnings.push(
132
- `Route file ${basename(file)} exists but no /${name} path found in ${openapiFile}. ` +
167
+ `Route file ${basename(file)} exists but no matching paths found in ${openapiFile}. ` +
133
168
  `Run your spec generator (e.g., zod-to-openapi) to update the API spec`
134
169
  );
135
170
  }
@@ -110,14 +110,19 @@ function checkSkippedTests(projectDir) {
110
110
  const isSkipped = SKIP_PATTERNS.some(p => p.test(line));
111
111
  if (!isSkipped) continue;
112
112
 
113
- // Check surrounding lines (1 above, 1 below, and inline) for explanation
114
- const prevLine = i > 0 ? lines[i - 1] : '';
115
- const nextLine = i < lines.length - 1 ? lines[i + 1] : '';
113
+ // Check surrounding lines (3 above, 1 below, and inline) for explanation
114
+ // Developers commonly place block comments above the skip call
115
+ const surroundingLines = [];
116
+ for (let j = Math.max(0, i - 3); j <= Math.min(lines.length - 1, i + 1); j++) {
117
+ surroundingLines.push(lines[j]);
118
+ }
119
+
120
+ // Also check for block comment pattern: /* REASON: ... */ or /** ... REASON: ... */
121
+ const blockCommentPattern = /\/\*[\s\S]*?(REASON|SKIP|TODO|FIXME|NOTE|WHY)\s*:/i;
116
122
 
117
123
  const hasReason =
118
- SKIP_REASON_PATTERN.test(prevLine) ||
119
- SKIP_REASON_PATTERN.test(line) ||
120
- SKIP_REASON_PATTERN.test(nextLine);
124
+ surroundingLines.some(l => SKIP_REASON_PATTERN.test(l)) ||
125
+ blockCommentPattern.test(surroundingLines.join('\n'));
121
126
 
122
127
  if (hasReason) {
123
128
  skippedWithReason++;
@@ -8,7 +8,7 @@ Enterprise-grade Canonical-Driven Development (CDD) enforcement for [Spec Kit](h
8
8
  - **4 AI Skills** — Enterprise-grade AI behavior protocols (not just step-lists)
9
9
  - **3 Bash Scripts** — JSON-output orchestration for AI consumption
10
10
  - **Workflow Chaining** — YAML handoffs enable guard → fix → review → score flows
11
- - **Spec Kit Hooks** — Mandatory quality gate after `/speckit.implement`
11
+ - **Spec Kit Hooks** — Quality gate integrations at implement, tasks, and review phases
12
12
  - **Zero Dependencies** — Pure Node.js built-ins only
13
13
 
14
14
  ## Installation
@@ -40,16 +40,14 @@ docguard score
40
40
 
41
41
  ## Commands
42
42
 
43
- | Command | Purpose |
44
- |---------|---------|
45
- | `docguard.guard` | Run 19-validator quality gate with severity triage |
46
- | `docguard.fix` | AI-driven documentation repair with codebase research |
47
- | `docguard.review` | Cross-document semantic consistency analysis (read-only) |
48
- | `docguard.score` | CDD maturity score with ROI improvement roadmap |
49
- | `docguard.diagnose` | Diagnose issues + generate multi-perspective AI prompts |
50
- | `docguard.generate` | Reverse-engineer canonical docs from codebase |
51
- | `docguard.init` | Initialize CDD structure in a project |
52
- | `docguard.trace` | Generate requirements traceability matrix |
43
+ | Command | Alias | Purpose |
44
+ |---------|-------|---------|
45
+ | `speckit.docguard.guard` | `docguard.guard` | Run 19-validator quality gate with severity triage |
46
+ | `speckit.docguard.fix` | `docguard.fix` | AI-driven documentation repair with codebase research |
47
+ | `speckit.docguard.review` | `docguard.review` | Cross-document semantic consistency analysis (read-only) |
48
+ | `speckit.docguard.score` | `docguard.score` | CDD maturity score with ROI improvement roadmap |
49
+ | `speckit.docguard.diagnose` | — | Diagnose issues + generate multi-perspective AI prompts |
50
+ | `speckit.docguard.generate` | — | Reverse-engineer canonical docs from codebase |
53
51
 
54
52
  ## AI Skills
55
53
 
@@ -72,12 +70,12 @@ DocGuard integrates into the spec-kit workflow through hooks:
72
70
 
73
71
  ```yaml
74
72
  hooks:
75
- after_implement: # Mandatoryalways runs after /speckit.implement
76
- command: docguard.guard
73
+ after_implement: # Optionalquality gate after /speckit.implement
74
+ command: speckit.docguard.guard
77
75
  before_tasks: # Optional — review docs before task generation
78
- command: docguard.review
76
+ command: speckit.docguard.review
79
77
  after_tasks: # Optional — show score after tasks
80
- command: docguard.score
78
+ command: speckit.docguard.score
81
79
  ```
82
80
 
83
81
  ### Workflow Chaining
@@ -3,7 +3,7 @@ schema_version: "1.0"
3
3
  extension:
4
4
  id: "docguard"
5
5
  name: "DocGuard — CDD Enforcement"
6
- version: "0.9.6"
6
+ version: "0.9.7"
7
7
  description: "Canonical-Driven Development enforcement with enterprise-grade AI skills. 19 automated validators, structured research workflows, quality validation loops, and spec-kit integration hooks. Zero dependencies."
8
8
  author: "Ricardo Accioly"
9
9
  repository: "https://github.com/raccioly/docguard"
@@ -21,78 +21,62 @@ requires:
21
21
 
22
22
  provides:
23
23
  commands:
24
- - name: "docguard.guard"
24
+ - name: "speckit.docguard.guard"
25
25
  file: "commands/guard.md"
26
26
  description: "Run 19-validator quality gate with severity triage and remediation plan"
27
- aliases: ["speckit.docguard.guard"]
27
+ aliases: ["docguard.guard"]
28
28
 
29
- - name: "docguard.fix"
30
- file: "commands/fix.md"
29
+ - name: "speckit.docguard.fix"
30
+ file: "commands/generate.md"
31
31
  description: "AI-driven documentation repair with codebase research and validation loops"
32
- aliases: ["speckit.docguard.fix"]
32
+ aliases: ["docguard.fix"]
33
33
 
34
- - name: "docguard.review"
35
- file: "commands/review.md"
34
+ - name: "speckit.docguard.review"
35
+ file: "commands/diagnose.md"
36
36
  description: "Cross-document semantic consistency analysis (read-only)"
37
- aliases: ["speckit.docguard.review"]
37
+ aliases: ["docguard.review"]
38
38
 
39
- - name: "docguard.score"
39
+ - name: "speckit.docguard.score"
40
40
  file: "commands/score.md"
41
41
  description: "CDD maturity score with ROI-based improvement roadmap"
42
- aliases: ["speckit.docguard.score"]
42
+ aliases: ["docguard.score"]
43
43
 
44
44
  - name: "speckit.docguard.diagnose"
45
- file: "commands/review.md"
45
+ file: "commands/diagnose.md"
46
46
  description: "Diagnose all issues and generate a complete AI remediation plan"
47
47
 
48
48
  - name: "speckit.docguard.generate"
49
- file: "commands/fix.md"
49
+ file: "commands/generate.md"
50
50
  description: "Reverse-engineer canonical docs from existing codebase"
51
51
 
52
- skills:
53
- - name: "docguard-guard"
54
- path: "skills/docguard-guard/SKILL.md"
55
- description: "19-validator quality gate with severity triage and structured reporting"
56
-
57
- - name: "docguard-fix"
58
- path: "skills/docguard-fix/SKILL.md"
59
- description: "AI-driven documentation repair with research workflow and validation loops"
60
-
61
- - name: "docguard-review"
62
- path: "skills/docguard-review/SKILL.md"
63
- description: "Cross-document semantic consistency analysis with quality scoring"
64
-
65
- - name: "docguard-score"
66
- path: "skills/docguard-score/SKILL.md"
67
- description: "CDD maturity assessment with ROI-based improvement roadmap"
68
-
52
+ # Helper scripts for CI/CD and automation
69
53
  scripts:
70
54
  - name: "docguard-check-docs"
71
- path: "scripts/bash/docguard-check-docs.sh"
55
+ file: "scripts/bash/docguard-check-docs.sh"
72
56
  description: "Discover project docs, return JSON inventory with metadata"
73
57
 
74
58
  - name: "docguard-suggest-fix"
75
- path: "scripts/bash/docguard-suggest-fix.sh"
59
+ file: "scripts/bash/docguard-suggest-fix.sh"
76
60
  description: "Run guard, parse results, output prioritized fix suggestions"
77
61
 
78
62
  - name: "docguard-init-doc"
79
- path: "scripts/bash/docguard-init-doc.sh"
63
+ file: "scripts/bash/docguard-init-doc.sh"
80
64
  description: "Initialize a new canonical document with metadata header"
81
65
 
82
66
  hooks:
83
67
  after_implement:
84
- command: "docguard.guard"
85
- optional: false
86
- prompt: "Run DocGuard validation after implementation"
87
- description: "Mandatory quality gate — ensures docs stay in sync with code"
68
+ command: "speckit.docguard.guard"
69
+ optional: true
70
+ prompt: "Run DocGuard validation after implementation?"
71
+ description: "Quality gate — ensures docs stay in sync with code"
88
72
 
89
73
  before_tasks:
90
- command: "docguard.review"
74
+ command: "speckit.docguard.review"
91
75
  optional: true
92
76
  prompt: "Review documentation consistency before generating tasks?"
93
77
 
94
78
  after_tasks:
95
- command: "docguard.score"
79
+ command: "speckit.docguard.score"
96
80
  optional: true
97
81
  prompt: "Show CDD maturity score after task generation?"
98
82
 
@@ -106,4 +90,3 @@ tags:
106
90
  - "ai-agents"
107
91
  - "enforcement"
108
92
  - "spec-kit"
109
- - "skills"
@@ -14,16 +14,16 @@ hooks:
14
14
  # This ensures documentation stays in sync with code changes
15
15
  after_implement:
16
16
  - extension: docguard
17
- command: docguard.guard
17
+ command: speckit.docguard.guard
18
18
  description: "Validate documentation passes CDD standards after implementation"
19
19
  enabled: true
20
- optional: false # Mandatory — always runs after implement
21
- prompt: "Run DocGuard guard to verify documentation quality after implementation changes"
20
+ optional: true
21
+ prompt: "Run DocGuard guard to verify documentation quality after implementation changes?"
22
22
 
23
23
  # Run DocGuard review before /speckit.tasks to catch doc drift early
24
24
  before_tasks:
25
25
  - extension: docguard
26
- command: docguard.review
26
+ command: speckit.docguard.review
27
27
  description: "Review documentation consistency before generating tasks"
28
28
  enabled: true
29
29
  optional: true # Optional — user can skip
@@ -32,7 +32,7 @@ hooks:
32
32
  # Run DocGuard guard after /speckit.tasks to validate doc coverage
33
33
  after_tasks:
34
34
  - extension: docguard
35
- command: docguard.score
35
+ command: speckit.docguard.score
36
36
  description: "Show CDD maturity score after task generation"
37
37
  enabled: true
38
38
  optional: true
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "docguard-cli",
3
- "version": "0.9.6",
3
+ "version": "0.9.7",
4
4
  "description": "The enforcement tool for Canonical-Driven Development (CDD). Audit, generate, and guard your project documentation.",
5
5
  "type": "module",
6
6
  "bin": {