docguard-cli 0.9.5 → 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.
- package/README.md +281 -203
- package/cli/commands/diff.mjs +16 -3
- package/cli/commands/init.mjs +4 -0
- package/cli/commands/setup.mjs +455 -0
- package/cli/docguard.mjs +12 -0
- package/cli/ensure-skills.mjs +96 -0
- package/cli/validators/doc-quality.mjs +2 -2
- package/cli/validators/docs-sync.mjs +41 -6
- package/cli/validators/metrics-consistency.mjs +2 -1
- package/cli/validators/todo-tracking.mjs +11 -6
- package/commands/docguard.fix.md +37 -17
- package/commands/docguard.guard.md +45 -12
- package/commands/docguard.review.md +37 -19
- package/commands/docguard.score.md +36 -17
- package/docs/installation.md +37 -19
- package/docs/quickstart.md +21 -6
- package/extensions/spec-kit-docguard/LICENSE +21 -0
- package/extensions/spec-kit-docguard/README.md +103 -0
- package/extensions/spec-kit-docguard/commands/diagnose.md +43 -0
- package/extensions/spec-kit-docguard/commands/generate.md +50 -0
- package/extensions/spec-kit-docguard/commands/guard.md +73 -0
- package/extensions/spec-kit-docguard/commands/init.md +38 -0
- package/extensions/spec-kit-docguard/commands/score.md +53 -0
- package/extensions/spec-kit-docguard/commands/trace.md +56 -0
- package/extensions/spec-kit-docguard/extension.yml +92 -0
- package/extensions/spec-kit-docguard/scripts/bash/common.sh +106 -0
- package/extensions/spec-kit-docguard/scripts/bash/docguard-check-docs.sh +153 -0
- package/extensions/spec-kit-docguard/scripts/bash/docguard-init-doc.sh +153 -0
- package/extensions/spec-kit-docguard/scripts/bash/docguard-suggest-fix.sh +107 -0
- package/extensions/spec-kit-docguard/skills/docguard-fix/SKILL.md +218 -0
- package/extensions/spec-kit-docguard/skills/docguard-guard/SKILL.md +167 -0
- package/extensions/spec-kit-docguard/skills/docguard-review/SKILL.md +182 -0
- package/extensions/spec-kit-docguard/skills/docguard-score/SKILL.md +178 -0
- package/extensions/spec-kit-docguard/templates/extensions.yml +39 -0
- package/package.json +2 -1
- package/templates/commands/docguard.fix.md +35 -39
- package/templates/commands/docguard.guard.md +26 -13
- package/templates/commands/docguard.init.md +35 -28
- package/templates/commands/docguard.review.md +33 -23
- package/templates/commands/docguard.update.md +15 -4
package/cli/commands/init.mjs
CHANGED
|
@@ -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:
|
|
37
|
-
fleschKincaidGrade: { warn:
|
|
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
|
|
122
|
-
if (
|
|
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
|
-
//
|
|
127
|
-
//
|
|
128
|
-
|
|
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
|
|
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
|
}
|