docguard-cli 0.5.1

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.
Files changed (57) hide show
  1. package/LICENSE +21 -0
  2. package/PHILOSOPHY.md +150 -0
  3. package/README.md +309 -0
  4. package/STANDARD.md +751 -0
  5. package/cli/commands/agents.mjs +221 -0
  6. package/cli/commands/audit.mjs +92 -0
  7. package/cli/commands/badge.mjs +72 -0
  8. package/cli/commands/ci.mjs +80 -0
  9. package/cli/commands/diagnose.mjs +273 -0
  10. package/cli/commands/diff.mjs +360 -0
  11. package/cli/commands/fix.mjs +610 -0
  12. package/cli/commands/generate.mjs +842 -0
  13. package/cli/commands/guard.mjs +158 -0
  14. package/cli/commands/hooks.mjs +227 -0
  15. package/cli/commands/init.mjs +249 -0
  16. package/cli/commands/score.mjs +396 -0
  17. package/cli/commands/watch.mjs +143 -0
  18. package/cli/docguard.mjs +458 -0
  19. package/cli/validators/architecture.mjs +380 -0
  20. package/cli/validators/changelog.mjs +39 -0
  21. package/cli/validators/docs-sync.mjs +110 -0
  22. package/cli/validators/drift.mjs +101 -0
  23. package/cli/validators/environment.mjs +70 -0
  24. package/cli/validators/freshness.mjs +224 -0
  25. package/cli/validators/security.mjs +101 -0
  26. package/cli/validators/structure.mjs +88 -0
  27. package/cli/validators/test-spec.mjs +115 -0
  28. package/docs/ai-integration.md +179 -0
  29. package/docs/commands.md +239 -0
  30. package/docs/configuration.md +96 -0
  31. package/docs/faq.md +155 -0
  32. package/docs/installation.md +81 -0
  33. package/docs/profiles.md +103 -0
  34. package/docs/quickstart.md +79 -0
  35. package/package.json +57 -0
  36. package/templates/ADR.md.template +64 -0
  37. package/templates/AGENTS.md.template +88 -0
  38. package/templates/ARCHITECTURE.md.template +78 -0
  39. package/templates/CHANGELOG.md.template +16 -0
  40. package/templates/CURRENT-STATE.md.template +64 -0
  41. package/templates/DATA-MODEL.md.template +66 -0
  42. package/templates/DEPLOYMENT.md.template +66 -0
  43. package/templates/DRIFT-LOG.md.template +18 -0
  44. package/templates/ENVIRONMENT.md.template +43 -0
  45. package/templates/KNOWN-GOTCHAS.md.template +69 -0
  46. package/templates/ROADMAP.md.template +82 -0
  47. package/templates/RUNBOOKS.md.template +115 -0
  48. package/templates/SECURITY.md.template +42 -0
  49. package/templates/TEST-SPEC.md.template +55 -0
  50. package/templates/TROUBLESHOOTING.md.template +96 -0
  51. package/templates/VENDOR-BUGS.md.template +74 -0
  52. package/templates/ci/github-actions.yml +39 -0
  53. package/templates/commands/docguard.fix.md +65 -0
  54. package/templates/commands/docguard.guard.md +40 -0
  55. package/templates/commands/docguard.init.md +62 -0
  56. package/templates/commands/docguard.review.md +44 -0
  57. package/templates/commands/docguard.update.md +44 -0
@@ -0,0 +1,158 @@
1
+ /**
2
+ * Guard Command — Validate project against its canonical documentation
3
+ * Runs all enabled validators and reports results.
4
+ *
5
+ * Two modes:
6
+ * runGuard() → prints to console, exits with code
7
+ * runGuardInternal() → returns data, no side effects (for diagnose, ci)
8
+ */
9
+
10
+ import { c } from '../docguard.mjs';
11
+ import { validateStructure, validateDocSections } from '../validators/structure.mjs';
12
+ import { validateDrift } from '../validators/drift.mjs';
13
+ import { validateChangelog } from '../validators/changelog.mjs';
14
+ import { validateTestSpec } from '../validators/test-spec.mjs';
15
+ import { validateEnvironment } from '../validators/environment.mjs';
16
+ import { validateSecurity } from '../validators/security.mjs';
17
+ import { validateDocsSync } from '../validators/docs-sync.mjs';
18
+ import { validateArchitecture } from '../validators/architecture.mjs';
19
+ import { validateFreshness } from '../validators/freshness.mjs';
20
+
21
+ /**
22
+ * Internal guard — returns structured data, no console output, no process.exit.
23
+ * Used by diagnose, ci, and guard --format json.
24
+ */
25
+ export function runGuardInternal(projectDir, config) {
26
+ const validators = config.validators || {};
27
+ const results = [];
28
+
29
+ const validatorMap = [
30
+ { key: 'structure', name: 'Structure', fn: () => validateStructure(projectDir, config) },
31
+ { key: 'structure', name: 'Doc Sections', fn: () => validateDocSections(projectDir, config) },
32
+ { key: 'docsSync', name: 'Docs-Sync', fn: () => validateDocsSync(projectDir, config) },
33
+ { key: 'drift', name: 'Drift', fn: () => validateDrift(projectDir, config) },
34
+ { key: 'changelog', name: 'Changelog', fn: () => validateChangelog(projectDir, config) },
35
+ { key: 'testSpec', name: 'Test-Spec', fn: () => validateTestSpec(projectDir, config) },
36
+ { key: 'environment', name: 'Environment', fn: () => validateEnvironment(projectDir, config) },
37
+ { key: 'security', name: 'Security', fn: () => validateSecurity(projectDir, config) },
38
+ { key: 'architecture', name: 'Architecture', fn: () => validateArchitecture(projectDir, config) },
39
+ { key: 'freshness', name: 'Freshness', fn: () => {
40
+ const freshnessResults = validateFreshness(projectDir, config);
41
+ const errors = [];
42
+ const warnings = [];
43
+ let passed = 0;
44
+ for (const r of freshnessResults) {
45
+ if (r.status === 'pass') passed++;
46
+ else if (r.status === 'warn') warnings.push(r.message);
47
+ else if (r.status === 'fail') errors.push(r.message);
48
+ }
49
+ return { errors, warnings, passed, total: passed + warnings.length + errors.length };
50
+ }},
51
+ ];
52
+
53
+ for (const { key, name, fn } of validatorMap) {
54
+ if (validators[key] === false) {
55
+ results.push({ name, key, status: 'skipped', errors: [], warnings: [], passed: 0, total: 0 });
56
+ continue;
57
+ }
58
+
59
+ try {
60
+ const result = fn();
61
+ const hasErrors = result.errors.length > 0;
62
+ const hasWarnings = result.warnings.length > 0;
63
+ const status = hasErrors ? 'fail' : hasWarnings ? 'warn' : 'pass';
64
+ results.push({ ...result, name, key, status });
65
+ } catch (err) {
66
+ results.push({ name, key, status: 'fail', errors: [err.message], warnings: [], passed: 0, total: 1 });
67
+ }
68
+ }
69
+
70
+ const activeResults = results.filter(r => r.status !== 'skipped');
71
+ const totalErrors = activeResults.reduce((sum, r) => sum + r.errors.length, 0);
72
+ const totalWarnings = activeResults.reduce((sum, r) => sum + r.warnings.length, 0);
73
+ const totalPassed = activeResults.reduce((sum, r) => sum + r.passed, 0);
74
+ const totalChecks = activeResults.reduce((sum, r) => sum + r.total, 0);
75
+
76
+ const overallStatus = totalErrors > 0 ? 'FAIL' : totalWarnings > 0 ? 'WARN' : 'PASS';
77
+
78
+ return {
79
+ project: config.projectName,
80
+ profile: config.profile || 'standard',
81
+ status: overallStatus,
82
+ passed: totalPassed,
83
+ total: totalChecks,
84
+ errors: totalErrors,
85
+ warnings: totalWarnings,
86
+ validators: results,
87
+ timestamp: new Date().toISOString(),
88
+ };
89
+ }
90
+
91
+ /**
92
+ * Public guard — prints results and exits.
93
+ */
94
+ export function runGuard(projectDir, config, flags) {
95
+ const data = runGuardInternal(projectDir, config);
96
+
97
+ // ── JSON output ──
98
+ if (flags.format === 'json') {
99
+ console.log(JSON.stringify(data, null, 2));
100
+ if (data.errors > 0) process.exit(1);
101
+ if (data.warnings > 0) process.exit(2);
102
+ process.exit(0);
103
+ }
104
+
105
+ // ── Text output ──
106
+ console.log(`${c.bold}🛡️ DocGuard Guard — ${config.projectName}${c.reset}`);
107
+ console.log(`${c.dim} Directory: ${projectDir}${c.reset}\n`);
108
+
109
+ for (const v of data.validators) {
110
+ if (v.status === 'skipped') {
111
+ if (flags.verbose) {
112
+ console.log(` ${c.dim}⏭️ ${v.name} (disabled)${c.reset}`);
113
+ }
114
+ continue;
115
+ }
116
+
117
+ if (v.status === 'pass') {
118
+ console.log(` ${c.green}✅ ${v.name}${c.reset}${c.dim} ${v.passed}/${v.total} checks passed${c.reset}`);
119
+ } else if (v.status === 'fail') {
120
+ console.log(` ${c.red}❌ ${v.name}${c.reset}${c.dim} ${v.passed}/${v.total} checks passed${c.reset}`);
121
+ } else {
122
+ console.log(` ${c.yellow}⚠️ ${v.name}${c.reset}${c.dim} ${v.passed}/${v.total} checks passed${c.reset}`);
123
+ }
124
+
125
+ if (flags.verbose || v.status === 'fail') {
126
+ for (const err of v.errors) {
127
+ console.log(` ${c.red}✗ ${err}${c.reset}`);
128
+ }
129
+ }
130
+ if (flags.verbose || v.status === 'warn') {
131
+ for (const warn of v.warnings) {
132
+ console.log(` ${c.yellow}⚠ ${warn}${c.reset}`);
133
+ }
134
+ }
135
+ }
136
+
137
+ // Summary
138
+ console.log(`\n${c.bold} ─────────────────────────────────────${c.reset}`);
139
+
140
+ if (data.status === 'PASS') {
141
+ console.log(` ${c.green}${c.bold}✅ PASS${c.reset} ${c.green}— All ${data.total} checks passed${c.reset}`);
142
+ } else if (data.status === 'WARN') {
143
+ console.log(` ${c.yellow}${c.bold}⚠️ WARN${c.reset} ${c.yellow}— ${data.passed}/${data.total} passed, ${data.warnings} warning(s)${c.reset}`);
144
+ } else {
145
+ console.log(` ${c.red}${c.bold}❌ FAIL${c.reset} ${c.red}— ${data.passed}/${data.total} passed, ${data.errors} error(s), ${data.warnings} warning(s)${c.reset}`);
146
+ }
147
+
148
+ // Next step hint — always point to diagnose when issues exist
149
+ if (data.status !== 'PASS') {
150
+ console.log(` ${c.dim}Run ${c.cyan}docguard diagnose${c.dim} to get AI fix prompts.${c.reset}`);
151
+ }
152
+
153
+ console.log('');
154
+
155
+ if (data.errors > 0) process.exit(1);
156
+ if (data.warnings > 0) process.exit(2);
157
+ process.exit(0);
158
+ }
@@ -0,0 +1,227 @@
1
+ /**
2
+ * Hooks Command — Generate pre-commit/pre-push hooks for DocGuard
3
+ * Creates git hooks that run guard/score before commits.
4
+ */
5
+
6
+ import { existsSync, writeFileSync, mkdirSync, chmodSync, readFileSync, unlinkSync } from 'node:fs';
7
+ import { resolve } from 'node:path';
8
+ import { c } from '../docguard.mjs';
9
+
10
+ const HOOKS = {
11
+ 'pre-commit': {
12
+ description: 'Run docguard guard before every commit',
13
+ content: `#!/bin/sh
14
+ # DocGuard pre-commit hook
15
+ # Validates CDD compliance before allowing commits
16
+ # Install: docguard hooks --type pre-commit
17
+ # Remove: rm .git/hooks/pre-commit
18
+
19
+ echo "🛡️ Running DocGuard guard..."
20
+
21
+ # Check if docguard is available
22
+ if command -v npx &> /dev/null; then
23
+ npx docguard guard
24
+ EXIT_CODE=$?
25
+ elif command -v docguard &> /dev/null; then
26
+ docguard guard
27
+ EXIT_CODE=$?
28
+ else
29
+ echo "⚠️ DocGuard not found. Skipping guard check."
30
+ echo " Install: npm install -g docguard"
31
+ exit 0
32
+ fi
33
+
34
+ if [ $EXIT_CODE -eq 1 ]; then
35
+ echo ""
36
+ echo "❌ DocGuard guard FAILED — commit blocked"
37
+ echo " Fix the errors above, then try again."
38
+ echo " To skip: git commit --no-verify"
39
+ exit 1
40
+ elif [ $EXIT_CODE -eq 2 ]; then
41
+ echo ""
42
+ echo "⚠️ DocGuard guard found warnings — commit allowed"
43
+ fi
44
+
45
+ exit 0
46
+ `,
47
+ },
48
+
49
+ 'pre-push': {
50
+ description: 'Run docguard score check before push (enforce minimum score)',
51
+ content: `#!/bin/sh
52
+ # DocGuard pre-push hook
53
+ # Enforces minimum CDD score before allowing push
54
+ # Install: docguard hooks --type pre-push
55
+ # Remove: rm .git/hooks/pre-push
56
+
57
+ MIN_SCORE=60
58
+
59
+ echo "📊 Running DocGuard score check (minimum: $MIN_SCORE)..."
60
+
61
+ # Get score as JSON
62
+ if command -v npx &> /dev/null; then
63
+ RESULT=$(npx docguard score --format json 2>/dev/null)
64
+ elif command -v docguard &> /dev/null; then
65
+ RESULT=$(docguard score --format json 2>/dev/null)
66
+ else
67
+ echo "⚠️ DocGuard not found. Skipping score check."
68
+ exit 0
69
+ fi
70
+
71
+ # Parse score from JSON
72
+ SCORE=$(echo "$RESULT" | grep -o '"score":[0-9]*' | head -1 | cut -d: -f2)
73
+
74
+ if [ -z "$SCORE" ]; then
75
+ echo "⚠️ Could not determine CDD score. Push allowed."
76
+ exit 0
77
+ fi
78
+
79
+ echo " CDD Score: $SCORE/100"
80
+
81
+ if [ "$SCORE" -lt "$MIN_SCORE" ]; then
82
+ echo ""
83
+ echo "❌ CDD score $SCORE is below minimum $MIN_SCORE — push blocked"
84
+ echo " Run: docguard score (for details)"
85
+ echo " To skip: git push --no-verify"
86
+ exit 1
87
+ fi
88
+
89
+ echo " ✅ Score meets minimum threshold"
90
+ exit 0
91
+ `,
92
+ },
93
+
94
+ 'commit-msg': {
95
+ description: 'Validate commit message format (conventional commits)',
96
+ content: `#!/bin/sh
97
+ # DocGuard commit-msg hook
98
+ # Validates conventional commit message format
99
+ # Install: docguard hooks --type commit-msg
100
+ # Remove: rm .git/hooks/commit-msg
101
+
102
+ COMMIT_MSG_FILE=$1
103
+ COMMIT_MSG=$(cat "$COMMIT_MSG_FILE")
104
+
105
+ # Conventional commit regex
106
+ PATTERN="^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert|release)(\\(.+\\))?: .{1,72}"
107
+
108
+ if ! echo "$COMMIT_MSG" | head -1 | grep -qE "$PATTERN"; then
109
+ echo ""
110
+ echo "❌ Commit message does not follow Conventional Commits format"
111
+ echo ""
112
+ echo " Expected: type(scope): description"
113
+ echo " Types: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert, release"
114
+ echo ""
115
+ echo " Examples:"
116
+ echo " feat: add user authentication"
117
+ echo " fix(api): resolve timeout on large requests"
118
+ echo " docs: update ARCHITECTURE.md layer boundaries"
119
+ echo ""
120
+ echo " Your message: $(head -1 "$COMMIT_MSG_FILE")"
121
+ echo ""
122
+ echo " To skip: git commit --no-verify"
123
+ exit 1
124
+ fi
125
+
126
+ exit 0
127
+ `,
128
+ },
129
+ };
130
+
131
+ export function runHooks(projectDir, config, flags) {
132
+ console.log(`${c.bold}🪝 DocGuard Hooks — ${config.projectName}${c.reset}`);
133
+ console.log(`${c.dim} Directory: ${projectDir}${c.reset}\n`);
134
+
135
+ // Check if .git exists
136
+ const gitDir = resolve(projectDir, '.git');
137
+ if (!existsSync(gitDir)) {
138
+ console.log(` ${c.red}❌ Not a git repository. Run ${c.cyan}git init${c.red} first.${c.reset}\n`);
139
+ process.exit(1);
140
+ }
141
+
142
+ const hooksDir = resolve(gitDir, 'hooks');
143
+ if (!existsSync(hooksDir)) {
144
+ mkdirSync(hooksDir, { recursive: true });
145
+ }
146
+
147
+ // Determine which hooks to install
148
+ let hookTypes = Object.keys(HOOKS);
149
+ if (flags.type) {
150
+ if (!HOOKS[flags.type]) {
151
+ console.log(` ${c.red}Unknown hook type: ${flags.type}${c.reset}`);
152
+ console.log(` Available: ${Object.keys(HOOKS).join(', ')}\n`);
153
+ process.exit(1);
154
+ }
155
+ hookTypes = [flags.type];
156
+ }
157
+
158
+ // List mode
159
+ if (flags.list) {
160
+ console.log(` ${c.bold}Available hooks:${c.reset}\n`);
161
+ for (const [name, hook] of Object.entries(HOOKS)) {
162
+ const installed = existsSync(resolve(hooksDir, name));
163
+ const status = installed ? `${c.green}✅ installed${c.reset}` : `${c.dim}not installed${c.reset}`;
164
+ console.log(` ${c.cyan}${name}${c.reset}: ${hook.description} [${status}]`);
165
+ }
166
+ console.log(`\n ${c.dim}Install: docguard hooks --type <name>${c.reset}`);
167
+ console.log(` ${c.dim}Install all: docguard hooks${c.reset}\n`);
168
+ return;
169
+ }
170
+
171
+ // Remove mode
172
+ if (flags.remove) {
173
+ let removed = 0;
174
+ for (const name of hookTypes) {
175
+ const hookPath = resolve(hooksDir, name);
176
+ if (existsSync(hookPath)) {
177
+ const content = readFileSync(hookPath, 'utf-8');
178
+ if (content.includes('DocGuard')) {
179
+ unlinkSync(hookPath);
180
+ console.log(` ${c.yellow}🗑️ Removed: ${name}${c.reset}`);
181
+ removed++;
182
+ } else {
183
+ console.log(` ${c.dim}⏭️ ${name}: not a DocGuard hook (skipped)${c.reset}`);
184
+ }
185
+ }
186
+ }
187
+ console.log(`\n Removed: ${removed}\n`);
188
+ return;
189
+ }
190
+
191
+ // Install mode
192
+ let installed = 0;
193
+ let skipped = 0;
194
+
195
+ for (const name of hookTypes) {
196
+ const hookPath = resolve(hooksDir, name);
197
+
198
+ if (existsSync(hookPath) && !flags.force) {
199
+ // Check if it's already a DocGuard hook
200
+ const existing = readFileSync(hookPath, 'utf-8');
201
+ if (existing.includes('DocGuard')) {
202
+ console.log(` ${c.dim}⏭️ ${name} (DocGuard hook already installed)${c.reset}`);
203
+ skipped++;
204
+ continue;
205
+ }
206
+ console.log(` ${c.yellow}⚠️ ${name}: existing hook found (use --force to overwrite)${c.reset}`);
207
+ skipped++;
208
+ continue;
209
+ }
210
+
211
+ writeFileSync(hookPath, HOOKS[name].content, 'utf-8');
212
+ chmodSync(hookPath, 0o755); // Make executable
213
+ console.log(` ${c.green}✅ ${name}${c.reset}: ${HOOKS[name].description}`);
214
+ installed++;
215
+ }
216
+
217
+ console.log(`\n${c.bold} ─────────────────────────────────────${c.reset}`);
218
+ console.log(` Installed: ${installed} Skipped: ${skipped}`);
219
+
220
+ if (installed > 0) {
221
+ console.log(`\n ${c.dim}Hooks run automatically on git operations.${c.reset}`);
222
+ console.log(` ${c.dim}Skip with: git commit --no-verify${c.reset}`);
223
+ console.log(` ${c.dim}Remove with: docguard hooks --remove${c.reset}`);
224
+ }
225
+
226
+ console.log('');
227
+ }
@@ -0,0 +1,249 @@
1
+ /**
2
+ * Init Command — Initialize CDD documentation from templates
3
+ */
4
+
5
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync } from 'node:fs';
6
+ import { resolve, dirname, join } from 'node:path';
7
+ import { fileURLToPath } from 'node:url';
8
+ import { c, PROFILES } from '../docguard.mjs';
9
+
10
+ function detectProjectType(dir) {
11
+ const pkgPath = resolve(dir, 'package.json');
12
+ if (existsSync(pkgPath)) {
13
+ try {
14
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
15
+ const allDeps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
16
+ if (pkg.bin) return 'cli';
17
+ if (allDeps.next || allDeps.react || allDeps.vue || allDeps['@angular/core'] ||
18
+ allDeps.svelte || allDeps.nuxt) return 'webapp';
19
+ if (allDeps.express || allDeps.fastify || allDeps.hono || allDeps.koa) return 'api';
20
+ if (pkg.main || pkg.exports || pkg.module) return 'library';
21
+ } catch { /* fall through */ }
22
+ }
23
+ if (existsSync(resolve(dir, 'manage.py'))) return 'webapp';
24
+ if (existsSync(resolve(dir, 'setup.py')) || existsSync(resolve(dir, 'pyproject.toml'))) return 'library';
25
+ return 'unknown';
26
+ }
27
+
28
+ const __filename = fileURLToPath(import.meta.url);
29
+ const __dirname = dirname(__filename);
30
+ const TEMPLATES_DIR = resolve(__dirname, '../../templates');
31
+
32
+ export function runInit(projectDir, config, flags) {
33
+ const profileName = flags.profile || 'standard';
34
+ const profile = PROFILES[profileName];
35
+
36
+ if (!profile) {
37
+ console.error(`${c.red}Unknown profile: ${profileName}${c.reset}`);
38
+ console.log(`Available profiles: ${Object.keys(PROFILES).join(', ')}`);
39
+ process.exit(1);
40
+ }
41
+
42
+ console.log(`${c.bold}🏗️ DocGuard Init — ${config.projectName}${c.reset}`);
43
+ console.log(`${c.dim} Directory: ${projectDir}${c.reset}`);
44
+ console.log(`${c.dim} Profile: ${profileName} — ${profile.description}${c.reset}\n`);
45
+
46
+ const created = [];
47
+ const skipped = [];
48
+
49
+ // Map template files to their destinations
50
+ const allMappings = [
51
+ { template: 'ARCHITECTURE.md.template', dest: 'docs-canonical/ARCHITECTURE.md' },
52
+ { template: 'DATA-MODEL.md.template', dest: 'docs-canonical/DATA-MODEL.md' },
53
+ { template: 'SECURITY.md.template', dest: 'docs-canonical/SECURITY.md' },
54
+ { template: 'TEST-SPEC.md.template', dest: 'docs-canonical/TEST-SPEC.md' },
55
+ { template: 'ENVIRONMENT.md.template', dest: 'docs-canonical/ENVIRONMENT.md' },
56
+ { template: 'AGENTS.md.template', dest: 'AGENTS.md' },
57
+ { template: 'CHANGELOG.md.template', dest: 'CHANGELOG.md' },
58
+ { template: 'DRIFT-LOG.md.template', dest: 'DRIFT-LOG.md' },
59
+ ];
60
+
61
+ // Filter based on profile — starter only gets required files
62
+ const profileRequiredFiles = profile.requiredFiles
63
+ ? new Set([...profile.requiredFiles.canonical, profile.requiredFiles.changelog, profile.requiredFiles.driftLog, ...profile.requiredFiles.agentFile])
64
+ : null;
65
+
66
+ const fileMappings = profileRequiredFiles
67
+ ? allMappings.filter(m => profileRequiredFiles.has(m.dest))
68
+ : allMappings;
69
+
70
+ for (const mapping of fileMappings) {
71
+ const destPath = resolve(projectDir, mapping.dest);
72
+ const templatePath = resolve(TEMPLATES_DIR, mapping.template);
73
+
74
+ if (existsSync(destPath)) {
75
+ skipped.push(mapping.dest);
76
+ console.log(` ${c.yellow}⏭️${c.reset} ${mapping.dest} ${c.dim}(already exists)${c.reset}`);
77
+ continue;
78
+ }
79
+
80
+ // Ensure directory exists
81
+ const destDir = dirname(destPath);
82
+ if (!existsSync(destDir)) {
83
+ mkdirSync(destDir, { recursive: true });
84
+ }
85
+
86
+ // Read template and write
87
+ if (existsSync(templatePath)) {
88
+ const content = readFileSync(templatePath, 'utf-8');
89
+ // Replace template date placeholder with today's date
90
+ const today = new Date().toISOString().split('T')[0];
91
+ const processed = content.replace(/YYYY-MM-DD/g, today);
92
+ writeFileSync(destPath, processed, 'utf-8');
93
+ created.push(mapping.dest);
94
+ console.log(` ${c.green}✅${c.reset} Created: ${c.cyan}${mapping.dest}${c.reset}`);
95
+ } else {
96
+ console.log(
97
+ ` ${c.red}❌${c.reset} Template not found: ${mapping.template}`
98
+ );
99
+ }
100
+ }
101
+
102
+ // Create .docguard.json if it doesn't exist — auto-detect project type
103
+ const configPath = resolve(projectDir, '.docguard.json');
104
+ if (!existsSync(configPath)) {
105
+ // Detect project type from package.json
106
+ const detectedType = detectProjectType(projectDir);
107
+
108
+ // Get appropriate defaults for this project type
109
+ const typeDefaults = {
110
+ cli: { needsEnvVars: false, needsEnvExample: false, needsE2E: false, needsDatabase: false },
111
+ library: { needsEnvVars: false, needsEnvExample: false, needsE2E: false, needsDatabase: false },
112
+ webapp: { needsEnvVars: true, needsEnvExample: true, needsE2E: true, needsDatabase: true },
113
+ api: { needsEnvVars: true, needsEnvExample: true, needsE2E: false, needsDatabase: true },
114
+ unknown: { needsEnvVars: true, needsEnvExample: true, needsE2E: false, needsDatabase: true },
115
+ };
116
+
117
+ const ptc = typeDefaults[detectedType] || typeDefaults.unknown;
118
+
119
+ const defaultConfig = {
120
+ projectName: config.projectName,
121
+ version: '0.4',
122
+ profile: profileName,
123
+ projectType: detectedType,
124
+ projectTypeConfig: ptc,
125
+ validators: profile.validators || {
126
+ structure: true,
127
+ docsSync: true,
128
+ drift: true,
129
+ changelog: true,
130
+ architecture: false,
131
+ testSpec: true,
132
+ security: false,
133
+ environment: true,
134
+ freshness: true,
135
+ },
136
+ };
137
+
138
+ writeFileSync(configPath, JSON.stringify(defaultConfig, null, 2) + '\n', 'utf-8');
139
+ created.push('.docguard.json');
140
+ console.log(` ${c.green}✅${c.reset} Created: ${c.cyan}.docguard.json${c.reset} ${c.dim}(auto-detected: ${detectedType})${c.reset}`);
141
+ } else {
142
+ skipped.push('.docguard.json');
143
+ console.log(` ${c.yellow}⏭️${c.reset} .docguard.json ${c.dim}(already exists)${c.reset}`);
144
+ }
145
+
146
+ // Install slash commands for AI agents — detect which agents are in use
147
+ const commandsSourceDir = resolve(TEMPLATES_DIR, 'commands');
148
+ if (existsSync(commandsSourceDir)) {
149
+ const commandFiles = readdirSync(commandsSourceDir).filter(f => f.endsWith('.md'));
150
+
151
+ // Detect which AI agent directories exist in the project
152
+ const agentDirs = [
153
+ { name: 'GitHub Copilot', path: '.github/commands' },
154
+ { name: 'Cursor', path: '.cursor/rules' },
155
+ { name: 'Google Gemini', path: '.gemini/commands' },
156
+ { name: 'Claude Code', path: '.claude/commands' },
157
+ { name: 'Antigravity', path: '.agents/workflows' },
158
+ ];
159
+
160
+ // Find which agent dirs already exist in the project
161
+ const detected = agentDirs.filter(a =>
162
+ existsSync(resolve(projectDir, a.path.split('/')[0])) // check parent dir exists
163
+ );
164
+
165
+ // If none detected, default to .github/commands (most universal)
166
+ const targets = detected.length > 0
167
+ ? detected
168
+ : [{ name: 'GitHub (default)', path: '.github/commands' }];
169
+
170
+ let totalCreated = 0;
171
+ const installedLocations = [];
172
+
173
+ for (const target of targets) {
174
+ const destDir = resolve(projectDir, target.path);
175
+ if (!existsSync(destDir)) {
176
+ mkdirSync(destDir, { recursive: true });
177
+ }
178
+
179
+ let dirCreated = 0;
180
+ for (const file of commandFiles) {
181
+ const destPath = resolve(destDir, file);
182
+ if (!existsSync(destPath)) {
183
+ const content = readFileSync(resolve(commandsSourceDir, file), 'utf-8');
184
+ writeFileSync(destPath, content, 'utf-8');
185
+ dirCreated++;
186
+ }
187
+ }
188
+
189
+ if (dirCreated > 0) {
190
+ totalCreated += dirCreated;
191
+ installedLocations.push(`${target.path}/ (${target.name})`);
192
+ }
193
+ }
194
+
195
+ if (totalCreated > 0) {
196
+ created.push(`slash commands (${installedLocations.length} location(s))`);
197
+ console.log(` ${c.green}✅${c.reset} Installed ${c.cyan}slash commands${c.reset} for AI agents:`);
198
+ for (const loc of installedLocations) {
199
+ console.log(` ${c.dim}→ ${loc}${c.reset}`);
200
+ }
201
+ } else {
202
+ console.log(` ${c.yellow}⏭️${c.reset} Slash commands ${c.dim}(already installed)${c.reset}`);
203
+ }
204
+ }
205
+
206
+ // Summary
207
+ console.log(`\n${c.bold} ─────────────────────────────────────${c.reset}`);
208
+ console.log(` ${c.green}Created:${c.reset} ${created.length} files`);
209
+ if (skipped.length > 0) {
210
+ console.log(` ${c.yellow}Skipped:${c.reset} ${skipped.length} files (already exist)`);
211
+ }
212
+
213
+ if (flags.skipPrompts) {
214
+ // Simple instructions, no AI prompts
215
+ console.log(`\n ${c.bold}Next steps:${c.reset}`);
216
+ console.log(` ${c.dim}Run${c.reset} ${c.cyan}docguard diagnose${c.reset} ${c.dim}to get AI prompts for filling docs.${c.reset}`);
217
+ console.log(` ${c.dim}Then verify:${c.reset} ${c.cyan}docguard guard${c.reset}\n`);
218
+ } else {
219
+ // Auto-populate: output AI research prompts for each created canonical doc
220
+ const createdDocs = created.filter(f => f.startsWith('docs-canonical/'));
221
+
222
+ if (createdDocs.length > 0) {
223
+ console.log(`\n ${c.bold}🤖 AI Auto-Populate${c.reset}`);
224
+ console.log(` ${c.dim}The files above are skeleton templates. Your AI agent should fill them.${c.reset}`);
225
+ console.log(` ${c.dim}Run this single command to get a full remediation plan:${c.reset}\n`);
226
+ console.log(` ${c.cyan}${c.bold}docguard diagnose${c.reset}\n`);
227
+ console.log(` ${c.dim}Or generate prompts for individual docs:${c.reset}`);
228
+
229
+ const docNameMap = {
230
+ 'docs-canonical/ARCHITECTURE.md': 'architecture',
231
+ 'docs-canonical/DATA-MODEL.md': 'data-model',
232
+ 'docs-canonical/SECURITY.md': 'security',
233
+ 'docs-canonical/TEST-SPEC.md': 'test-spec',
234
+ 'docs-canonical/ENVIRONMENT.md': 'environment',
235
+ };
236
+
237
+ for (const doc of createdDocs) {
238
+ const target = docNameMap[doc];
239
+ if (target) {
240
+ console.log(` ${c.cyan}docguard fix --doc ${target}${c.reset}`);
241
+ }
242
+ }
243
+ console.log(`\n ${c.dim}Then verify:${c.reset} ${c.cyan}docguard guard${c.reset}\n`);
244
+ } else {
245
+ console.log(`\n ${c.dim}Run${c.reset} ${c.cyan}docguard diagnose${c.reset} ${c.dim}to check for issues.${c.reset}\n`);
246
+ }
247
+ }
248
+ }
249
+