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.
- package/LICENSE +21 -0
- package/PHILOSOPHY.md +150 -0
- package/README.md +309 -0
- package/STANDARD.md +751 -0
- package/cli/commands/agents.mjs +221 -0
- package/cli/commands/audit.mjs +92 -0
- package/cli/commands/badge.mjs +72 -0
- package/cli/commands/ci.mjs +80 -0
- package/cli/commands/diagnose.mjs +273 -0
- package/cli/commands/diff.mjs +360 -0
- package/cli/commands/fix.mjs +610 -0
- package/cli/commands/generate.mjs +842 -0
- package/cli/commands/guard.mjs +158 -0
- package/cli/commands/hooks.mjs +227 -0
- package/cli/commands/init.mjs +249 -0
- package/cli/commands/score.mjs +396 -0
- package/cli/commands/watch.mjs +143 -0
- package/cli/docguard.mjs +458 -0
- package/cli/validators/architecture.mjs +380 -0
- package/cli/validators/changelog.mjs +39 -0
- package/cli/validators/docs-sync.mjs +110 -0
- package/cli/validators/drift.mjs +101 -0
- package/cli/validators/environment.mjs +70 -0
- package/cli/validators/freshness.mjs +224 -0
- package/cli/validators/security.mjs +101 -0
- package/cli/validators/structure.mjs +88 -0
- package/cli/validators/test-spec.mjs +115 -0
- package/docs/ai-integration.md +179 -0
- package/docs/commands.md +239 -0
- package/docs/configuration.md +96 -0
- package/docs/faq.md +155 -0
- package/docs/installation.md +81 -0
- package/docs/profiles.md +103 -0
- package/docs/quickstart.md +79 -0
- package/package.json +57 -0
- package/templates/ADR.md.template +64 -0
- package/templates/AGENTS.md.template +88 -0
- package/templates/ARCHITECTURE.md.template +78 -0
- package/templates/CHANGELOG.md.template +16 -0
- package/templates/CURRENT-STATE.md.template +64 -0
- package/templates/DATA-MODEL.md.template +66 -0
- package/templates/DEPLOYMENT.md.template +66 -0
- package/templates/DRIFT-LOG.md.template +18 -0
- package/templates/ENVIRONMENT.md.template +43 -0
- package/templates/KNOWN-GOTCHAS.md.template +69 -0
- package/templates/ROADMAP.md.template +82 -0
- package/templates/RUNBOOKS.md.template +115 -0
- package/templates/SECURITY.md.template +42 -0
- package/templates/TEST-SPEC.md.template +55 -0
- package/templates/TROUBLESHOOTING.md.template +96 -0
- package/templates/VENDOR-BUGS.md.template +74 -0
- package/templates/ci/github-actions.yml +39 -0
- package/templates/commands/docguard.fix.md +65 -0
- package/templates/commands/docguard.guard.md +40 -0
- package/templates/commands/docguard.init.md +62 -0
- package/templates/commands/docguard.review.md +44 -0
- 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
|
+
|