code-as-plan 2.0.0
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/README.ja-JP.md +834 -0
- package/README.ko-KR.md +823 -0
- package/README.md +1006 -0
- package/README.pt-BR.md +452 -0
- package/README.zh-CN.md +800 -0
- package/agents/cap-brainstormer.md +154 -0
- package/agents/cap-debugger.md +221 -0
- package/agents/cap-prototyper.md +170 -0
- package/agents/cap-reviewer.md +230 -0
- package/agents/cap-tester.md +193 -0
- package/bin/install.js +5002 -0
- package/cap/bin/gsd-tools.cjs +1141 -0
- package/cap/bin/lib/arc-scanner.cjs +341 -0
- package/cap/bin/lib/cap-feature-map.cjs +506 -0
- package/cap/bin/lib/cap-session.cjs +191 -0
- package/cap/bin/lib/cap-stack-docs.cjs +598 -0
- package/cap/bin/lib/cap-tag-scanner.cjs +458 -0
- package/cap/bin/lib/commands.cjs +959 -0
- package/cap/bin/lib/config.cjs +466 -0
- package/cap/bin/lib/convention-reader.cjs +180 -0
- package/cap/bin/lib/core.cjs +1230 -0
- package/cap/bin/lib/feature-aggregator.cjs +422 -0
- package/cap/bin/lib/frontmatter.cjs +336 -0
- package/cap/bin/lib/init.cjs +1442 -0
- package/cap/bin/lib/manifest-generator.cjs +381 -0
- package/cap/bin/lib/milestone.cjs +252 -0
- package/cap/bin/lib/model-profiles.cjs +68 -0
- package/cap/bin/lib/monorepo-context.cjs +224 -0
- package/cap/bin/lib/monorepo-migrator.cjs +507 -0
- package/cap/bin/lib/phase.cjs +888 -0
- package/cap/bin/lib/profile-output.cjs +952 -0
- package/cap/bin/lib/profile-pipeline.cjs +539 -0
- package/cap/bin/lib/roadmap.cjs +329 -0
- package/cap/bin/lib/security.cjs +382 -0
- package/cap/bin/lib/session-manager.cjs +290 -0
- package/cap/bin/lib/skeleton-generator.cjs +177 -0
- package/cap/bin/lib/state.cjs +1031 -0
- package/cap/bin/lib/template.cjs +222 -0
- package/cap/bin/lib/test-detector.cjs +61 -0
- package/cap/bin/lib/uat.cjs +282 -0
- package/cap/bin/lib/verify.cjs +888 -0
- package/cap/bin/lib/workspace-detector.cjs +369 -0
- package/cap/bin/lib/workstream.cjs +491 -0
- package/cap/commands/gsd/workstreams.md +63 -0
- package/cap/references/arc-standard.md +315 -0
- package/cap/references/cap-agent-architecture.md +102 -0
- package/cap/references/cap-gitignore-template +9 -0
- package/cap/references/cap-zero-deps.md +158 -0
- package/cap/references/checkpoints.md +778 -0
- package/cap/references/continuation-format.md +249 -0
- package/cap/references/decimal-phase-calculation.md +64 -0
- package/cap/references/feature-map-template.md +25 -0
- package/cap/references/git-integration.md +295 -0
- package/cap/references/git-planning-commit.md +38 -0
- package/cap/references/model-profile-resolution.md +36 -0
- package/cap/references/model-profiles.md +139 -0
- package/cap/references/phase-argument-parsing.md +61 -0
- package/cap/references/planning-config.md +202 -0
- package/cap/references/questioning.md +162 -0
- package/cap/references/session-template.json +8 -0
- package/cap/references/tdd.md +263 -0
- package/cap/references/ui-brand.md +160 -0
- package/cap/references/user-profiling.md +681 -0
- package/cap/references/verification-patterns.md +612 -0
- package/cap/references/workstream-flag.md +58 -0
- package/cap/templates/DEBUG.md +164 -0
- package/cap/templates/UAT.md +265 -0
- package/cap/templates/UI-SPEC.md +100 -0
- package/cap/templates/VALIDATION.md +76 -0
- package/cap/templates/claude-md.md +122 -0
- package/cap/templates/codebase/architecture.md +255 -0
- package/cap/templates/codebase/concerns.md +310 -0
- package/cap/templates/codebase/conventions.md +307 -0
- package/cap/templates/codebase/integrations.md +280 -0
- package/cap/templates/codebase/stack.md +186 -0
- package/cap/templates/codebase/structure.md +285 -0
- package/cap/templates/codebase/testing.md +480 -0
- package/cap/templates/config.json +44 -0
- package/cap/templates/context.md +352 -0
- package/cap/templates/continue-here.md +78 -0
- package/cap/templates/copilot-instructions.md +7 -0
- package/cap/templates/debug-subagent-prompt.md +91 -0
- package/cap/templates/dev-preferences.md +21 -0
- package/cap/templates/discovery.md +146 -0
- package/cap/templates/discussion-log.md +63 -0
- package/cap/templates/milestone-archive.md +123 -0
- package/cap/templates/milestone.md +115 -0
- package/cap/templates/phase-prompt.md +610 -0
- package/cap/templates/planner-subagent-prompt.md +117 -0
- package/cap/templates/project.md +186 -0
- package/cap/templates/requirements.md +231 -0
- package/cap/templates/research-project/ARCHITECTURE.md +204 -0
- package/cap/templates/research-project/FEATURES.md +147 -0
- package/cap/templates/research-project/PITFALLS.md +200 -0
- package/cap/templates/research-project/STACK.md +120 -0
- package/cap/templates/research-project/SUMMARY.md +170 -0
- package/cap/templates/research.md +552 -0
- package/cap/templates/retrospective.md +54 -0
- package/cap/templates/roadmap.md +202 -0
- package/cap/templates/state.md +176 -0
- package/cap/templates/summary-complex.md +59 -0
- package/cap/templates/summary-minimal.md +41 -0
- package/cap/templates/summary-standard.md +48 -0
- package/cap/templates/summary.md +248 -0
- package/cap/templates/user-profile.md +146 -0
- package/cap/templates/user-setup.md +311 -0
- package/cap/templates/verification-report.md +322 -0
- package/cap/workflows/add-phase.md +112 -0
- package/cap/workflows/add-tests.md +351 -0
- package/cap/workflows/add-todo.md +158 -0
- package/cap/workflows/audit-milestone.md +340 -0
- package/cap/workflows/audit-uat.md +109 -0
- package/cap/workflows/autonomous.md +891 -0
- package/cap/workflows/check-todos.md +177 -0
- package/cap/workflows/cleanup.md +152 -0
- package/cap/workflows/complete-milestone.md +767 -0
- package/cap/workflows/diagnose-issues.md +231 -0
- package/cap/workflows/discovery-phase.md +289 -0
- package/cap/workflows/discuss-phase-assumptions.md +653 -0
- package/cap/workflows/discuss-phase.md +1049 -0
- package/cap/workflows/do.md +104 -0
- package/cap/workflows/execute-phase.md +846 -0
- package/cap/workflows/execute-plan.md +514 -0
- package/cap/workflows/fast.md +105 -0
- package/cap/workflows/forensics.md +265 -0
- package/cap/workflows/health.md +181 -0
- package/cap/workflows/help.md +660 -0
- package/cap/workflows/insert-phase.md +130 -0
- package/cap/workflows/list-phase-assumptions.md +178 -0
- package/cap/workflows/list-workspaces.md +56 -0
- package/cap/workflows/manager.md +362 -0
- package/cap/workflows/map-codebase.md +377 -0
- package/cap/workflows/milestone-summary.md +223 -0
- package/cap/workflows/new-milestone.md +486 -0
- package/cap/workflows/new-project.md +1250 -0
- package/cap/workflows/new-workspace.md +237 -0
- package/cap/workflows/next.md +97 -0
- package/cap/workflows/node-repair.md +92 -0
- package/cap/workflows/note.md +156 -0
- package/cap/workflows/pause-work.md +176 -0
- package/cap/workflows/plan-milestone-gaps.md +273 -0
- package/cap/workflows/plan-phase.md +859 -0
- package/cap/workflows/plant-seed.md +169 -0
- package/cap/workflows/pr-branch.md +129 -0
- package/cap/workflows/profile-user.md +450 -0
- package/cap/workflows/progress.md +507 -0
- package/cap/workflows/quick.md +757 -0
- package/cap/workflows/remove-phase.md +155 -0
- package/cap/workflows/remove-workspace.md +90 -0
- package/cap/workflows/research-phase.md +82 -0
- package/cap/workflows/resume-project.md +326 -0
- package/cap/workflows/review.md +228 -0
- package/cap/workflows/session-report.md +146 -0
- package/cap/workflows/settings.md +283 -0
- package/cap/workflows/ship.md +228 -0
- package/cap/workflows/stats.md +60 -0
- package/cap/workflows/transition.md +671 -0
- package/cap/workflows/ui-phase.md +302 -0
- package/cap/workflows/ui-review.md +165 -0
- package/cap/workflows/update.md +323 -0
- package/cap/workflows/validate-phase.md +174 -0
- package/cap/workflows/verify-phase.md +254 -0
- package/cap/workflows/verify-work.md +637 -0
- package/commands/cap/annotate.md +165 -0
- package/commands/cap/brainstorm.md +238 -0
- package/commands/cap/debug.md +297 -0
- package/commands/cap/init.md +262 -0
- package/commands/cap/iterate.md +234 -0
- package/commands/cap/prototype.md +281 -0
- package/commands/cap/refresh-docs.md +37 -0
- package/commands/cap/review.md +272 -0
- package/commands/cap/scan.md +249 -0
- package/commands/cap/start.md +234 -0
- package/commands/cap/status.md +189 -0
- package/commands/cap/test.md +250 -0
- package/hooks/dist/gsd-check-update.js +114 -0
- package/hooks/dist/gsd-context-monitor.js +156 -0
- package/hooks/dist/gsd-prompt-guard.js +96 -0
- package/hooks/dist/gsd-statusline.js +119 -0
- package/hooks/dist/gsd-workflow-guard.js +94 -0
- package/package.json +51 -0
- package/scripts/base64-scan.sh +262 -0
- package/scripts/build-hooks.js +82 -0
- package/scripts/cap-removal-checklist.md +202 -0
- package/scripts/prompt-injection-scan.sh +198 -0
- package/scripts/run-tests.cjs +29 -0
- package/scripts/secret-scan.sh +227 -0
|
@@ -0,0 +1,598 @@
|
|
|
1
|
+
// @gsd-context CAP v2.0 stack docs manager -- wraps Context7 CLI for library documentation fetch and caching in .cap/stack-docs/.
|
|
2
|
+
// @gsd-decision Wraps npx ctx7@latest (not a direct API call) -- Context7 is already the user's standard tool per CLAUDE.md. This module provides programmatic access for agent workflows.
|
|
3
|
+
// @gsd-decision Docs cached as markdown files in .cap/stack-docs/{library-name}.md -- simple, readable, committable for offline use.
|
|
4
|
+
// @gsd-constraint Zero external dependencies at runtime -- Context7 is invoked via child_process.execSync (npx), not imported.
|
|
5
|
+
// @gsd-risk Context7 requires network access and may hit rate limits. Module must handle failures gracefully and report to caller.
|
|
6
|
+
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
const fs = require('node:fs');
|
|
10
|
+
const path = require('node:path');
|
|
11
|
+
const { execSync } = require('node:child_process');
|
|
12
|
+
|
|
13
|
+
const STACK_DOCS_DIR = '.cap/stack-docs';
|
|
14
|
+
|
|
15
|
+
// @gsd-todo(ref:AC-27) Tag scanner uses stack docs path for enrichment context
|
|
16
|
+
const FRESHNESS_DAYS = 7;
|
|
17
|
+
const FRESHNESS_HOURS = FRESHNESS_DAYS * 24; // 168 hours default freshness window
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @typedef {Object} LibraryInfo
|
|
21
|
+
* @property {string} id - Context7 library ID (e.g., "/vercel/next.js")
|
|
22
|
+
* @property {string} name - Display name
|
|
23
|
+
* @property {string} description - Library description
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @typedef {Object} FetchResult
|
|
28
|
+
* @property {boolean} success - Whether the fetch succeeded
|
|
29
|
+
* @property {string|null} filePath - Path to cached docs file (null on failure)
|
|
30
|
+
* @property {string|null} error - Error message on failure
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @typedef {Object} DependencyInfo
|
|
35
|
+
* @property {string[]} dependencies - Production dependency names
|
|
36
|
+
* @property {string[]} devDependencies - Dev dependency names
|
|
37
|
+
* @property {string} type - Project type: 'node', 'python', 'go', 'rust', 'unknown'
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
// @gsd-api detectDependencies(projectRoot) -- Reads package.json/requirements.txt/etc to discover project dependencies.
|
|
41
|
+
// Returns: DependencyInfo with categorized dependency lists and project type.
|
|
42
|
+
/**
|
|
43
|
+
* @param {string} projectRoot - Absolute path to project root
|
|
44
|
+
* @returns {DependencyInfo}
|
|
45
|
+
*/
|
|
46
|
+
function detectDependencies(projectRoot) {
|
|
47
|
+
const result = { dependencies: [], devDependencies: [], type: 'unknown' };
|
|
48
|
+
|
|
49
|
+
// Node.js: package.json
|
|
50
|
+
const pkgPath = path.join(projectRoot, 'package.json');
|
|
51
|
+
if (fs.existsSync(pkgPath)) {
|
|
52
|
+
try {
|
|
53
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
54
|
+
result.type = 'node';
|
|
55
|
+
if (pkg.dependencies) result.dependencies = Object.keys(pkg.dependencies);
|
|
56
|
+
if (pkg.devDependencies) result.devDependencies = Object.keys(pkg.devDependencies);
|
|
57
|
+
} catch (_e) {
|
|
58
|
+
// Malformed package.json -- continue to other detectors
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Python: requirements.txt
|
|
63
|
+
const reqPath = path.join(projectRoot, 'requirements.txt');
|
|
64
|
+
if (fs.existsSync(reqPath) && result.type === 'unknown') {
|
|
65
|
+
try {
|
|
66
|
+
const content = fs.readFileSync(reqPath, 'utf8');
|
|
67
|
+
const depRE = /^([a-zA-Z0-9_-]+)/gm;
|
|
68
|
+
let match;
|
|
69
|
+
result.type = 'python';
|
|
70
|
+
while ((match = depRE.exec(content)) !== null) {
|
|
71
|
+
result.dependencies.push(match[1]);
|
|
72
|
+
}
|
|
73
|
+
} catch (_e) {
|
|
74
|
+
// Ignore
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Python: pyproject.toml (basic extraction)
|
|
79
|
+
const pyprojectPath = path.join(projectRoot, 'pyproject.toml');
|
|
80
|
+
if (fs.existsSync(pyprojectPath) && result.type === 'unknown') {
|
|
81
|
+
try {
|
|
82
|
+
const content = fs.readFileSync(pyprojectPath, 'utf8');
|
|
83
|
+
result.type = 'python';
|
|
84
|
+
// Extract dependency names from [project.dependencies] or [tool.poetry.dependencies]
|
|
85
|
+
const depRE = /^\s*"?([a-zA-Z0-9_-]+)/gm;
|
|
86
|
+
const depsSection = content.match(/\[(?:project\.)?dependencies\]([\s\S]*?)(?:\[|$)/);
|
|
87
|
+
if (depsSection) {
|
|
88
|
+
let m;
|
|
89
|
+
while ((m = depRE.exec(depsSection[1])) !== null) {
|
|
90
|
+
if (m[1] !== 'python') result.dependencies.push(m[1]);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
} catch (_e) {
|
|
94
|
+
// Ignore
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Go: go.mod
|
|
99
|
+
const goModPath = path.join(projectRoot, 'go.mod');
|
|
100
|
+
if (fs.existsSync(goModPath) && result.type === 'unknown') {
|
|
101
|
+
try {
|
|
102
|
+
const content = fs.readFileSync(goModPath, 'utf8');
|
|
103
|
+
result.type = 'go';
|
|
104
|
+
const requireRE = /^\s+([^\s]+)/gm;
|
|
105
|
+
const requireBlock = content.match(/require\s*\(([\s\S]*?)\)/);
|
|
106
|
+
if (requireBlock) {
|
|
107
|
+
let m;
|
|
108
|
+
while ((m = requireRE.exec(requireBlock[1])) !== null) {
|
|
109
|
+
result.dependencies.push(m[1]);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
} catch (_e) {
|
|
113
|
+
// Ignore
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Rust: Cargo.toml
|
|
118
|
+
const cargoPath = path.join(projectRoot, 'Cargo.toml');
|
|
119
|
+
if (fs.existsSync(cargoPath) && result.type === 'unknown') {
|
|
120
|
+
try {
|
|
121
|
+
const content = fs.readFileSync(cargoPath, 'utf8');
|
|
122
|
+
result.type = 'rust';
|
|
123
|
+
const depSection = content.match(/\[dependencies\]([\s\S]*?)(?:\[|$)/);
|
|
124
|
+
if (depSection) {
|
|
125
|
+
const depRE = /^([a-zA-Z0-9_-]+)/gm;
|
|
126
|
+
let m;
|
|
127
|
+
while ((m = depRE.exec(depSection[1])) !== null) {
|
|
128
|
+
result.dependencies.push(m[1]);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
} catch (_e) {
|
|
132
|
+
// Ignore
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return result;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// @gsd-api resolveLibrary(libraryName, query) -- Resolves a library name to a Context7 library ID.
|
|
140
|
+
// Returns: LibraryInfo or null if not found.
|
|
141
|
+
/**
|
|
142
|
+
* @param {string} libraryName - Library name (e.g., "react", "express")
|
|
143
|
+
* @param {string} [query] - Optional query for better matching
|
|
144
|
+
* @returns {LibraryInfo|null}
|
|
145
|
+
*/
|
|
146
|
+
function resolveLibrary(libraryName, query) {
|
|
147
|
+
const queryStr = query ? `"${query}"` : `"${libraryName}"`;
|
|
148
|
+
try {
|
|
149
|
+
const output = execSync(
|
|
150
|
+
`npx ctx7@latest library ${libraryName} ${queryStr}`,
|
|
151
|
+
{ encoding: 'utf8', timeout: 30000, stdio: ['pipe', 'pipe', 'pipe'] }
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
// Parse the first result from ctx7 library output
|
|
155
|
+
// Expected format: lines with ID, name, description
|
|
156
|
+
const lines = output.trim().split('\n').filter(l => l.trim());
|
|
157
|
+
if (lines.length === 0) return null;
|
|
158
|
+
|
|
159
|
+
// ctx7 outputs a table or JSON-like structure -- extract the first match
|
|
160
|
+
// Look for a line containing a library ID in /org/project format
|
|
161
|
+
for (const line of lines) {
|
|
162
|
+
const idMatch = line.match(/\/[a-zA-Z0-9_-]+\/[a-zA-Z0-9._-]+/);
|
|
163
|
+
if (idMatch) {
|
|
164
|
+
return {
|
|
165
|
+
id: idMatch[0],
|
|
166
|
+
name: libraryName,
|
|
167
|
+
description: line.replace(idMatch[0], '').trim(),
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return null;
|
|
173
|
+
} catch (e) {
|
|
174
|
+
// ctx7 not available, network error, or timeout
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// @gsd-api fetchDocs(projectRoot, libraryId, query) -- Fetches library docs via Context7 and caches them.
|
|
180
|
+
// Returns: FetchResult with success status and cached file path.
|
|
181
|
+
/**
|
|
182
|
+
* @param {string} projectRoot - Absolute path to project root
|
|
183
|
+
* @param {string} libraryId - Context7 library ID (e.g., "/vercel/next.js")
|
|
184
|
+
* @param {string} [query] - Optional query to focus documentation
|
|
185
|
+
* @returns {FetchResult}
|
|
186
|
+
*/
|
|
187
|
+
function fetchDocs(projectRoot, libraryId, query) {
|
|
188
|
+
const docsDir = path.join(projectRoot, STACK_DOCS_DIR);
|
|
189
|
+
// Ensure .cap/stack-docs/ exists
|
|
190
|
+
fs.mkdirSync(docsDir, { recursive: true });
|
|
191
|
+
|
|
192
|
+
// Derive filename from library ID: /vercel/next.js -> next.js.md
|
|
193
|
+
const libName = libraryId.split('/').pop() || libraryId.replace(/\//g, '-');
|
|
194
|
+
const filePath = path.join(docsDir, `${libName}.md`);
|
|
195
|
+
|
|
196
|
+
const queryStr = query ? `"${query}"` : '""';
|
|
197
|
+
try {
|
|
198
|
+
const output = execSync(
|
|
199
|
+
`npx ctx7@latest docs ${libraryId} ${queryStr}`,
|
|
200
|
+
{ encoding: 'utf8', timeout: 60000, stdio: ['pipe', 'pipe', 'pipe'] }
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
if (!output || output.trim().length === 0) {
|
|
204
|
+
return { success: false, filePath: null, error: 'Empty response from Context7' };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Write docs with metadata header
|
|
208
|
+
const header = [
|
|
209
|
+
`<!-- CAP Stack Docs: ${libraryId} -->`,
|
|
210
|
+
`<!-- Fetched: ${new Date().toISOString()} -->`,
|
|
211
|
+
`<!-- Query: ${query || 'general'} -->`,
|
|
212
|
+
'',
|
|
213
|
+
].join('\n');
|
|
214
|
+
|
|
215
|
+
fs.writeFileSync(filePath, header + output, 'utf8');
|
|
216
|
+
return { success: true, filePath, error: null };
|
|
217
|
+
} catch (e) {
|
|
218
|
+
const errorMsg = e.message || 'Unknown error fetching docs';
|
|
219
|
+
return { success: false, filePath: null, error: errorMsg };
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// @gsd-api writeDocs(projectRoot, libraryName, content) -- Writes documentation content directly to .cap/stack-docs/.
|
|
224
|
+
// Returns: string -- path to written file.
|
|
225
|
+
/**
|
|
226
|
+
* @param {string} projectRoot - Absolute path to project root
|
|
227
|
+
* @param {string} libraryName - Library name for filename
|
|
228
|
+
* @param {string} content - Documentation content to write
|
|
229
|
+
* @returns {string}
|
|
230
|
+
*/
|
|
231
|
+
function writeDocs(projectRoot, libraryName, content) {
|
|
232
|
+
const docsDir = path.join(projectRoot, STACK_DOCS_DIR);
|
|
233
|
+
fs.mkdirSync(docsDir, { recursive: true });
|
|
234
|
+
|
|
235
|
+
const filePath = path.join(docsDir, `${libraryName}.md`);
|
|
236
|
+
const header = [
|
|
237
|
+
`<!-- CAP Stack Docs: ${libraryName} -->`,
|
|
238
|
+
`<!-- Written: ${new Date().toISOString()} -->`,
|
|
239
|
+
'',
|
|
240
|
+
].join('\n');
|
|
241
|
+
|
|
242
|
+
fs.writeFileSync(filePath, header + content, 'utf8');
|
|
243
|
+
return filePath;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// @gsd-api listCachedDocs(projectRoot) -- Lists all cached library docs.
|
|
247
|
+
// Returns: Array of { libraryName, filePath, lastModified }.
|
|
248
|
+
/**
|
|
249
|
+
* @param {string} projectRoot - Absolute path to project root
|
|
250
|
+
* @returns {Array<{libraryName: string, filePath: string, lastModified: Date}>}
|
|
251
|
+
*/
|
|
252
|
+
function listCachedDocs(projectRoot) {
|
|
253
|
+
const docsDir = path.join(projectRoot, STACK_DOCS_DIR);
|
|
254
|
+
if (!fs.existsSync(docsDir)) return [];
|
|
255
|
+
|
|
256
|
+
try {
|
|
257
|
+
const files = fs.readdirSync(docsDir).filter(f => f.endsWith('.md'));
|
|
258
|
+
return files.map(f => {
|
|
259
|
+
const filePath = path.join(docsDir, f);
|
|
260
|
+
const stat = fs.statSync(filePath);
|
|
261
|
+
return {
|
|
262
|
+
libraryName: f.replace(/\.md$/, ''),
|
|
263
|
+
filePath,
|
|
264
|
+
lastModified: stat.mtime,
|
|
265
|
+
};
|
|
266
|
+
});
|
|
267
|
+
} catch (_e) {
|
|
268
|
+
return [];
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// @gsd-api checkFreshness(projectRoot, libraryName, maxAgeHours) -- Checks if cached docs are still fresh.
|
|
273
|
+
// Returns: { fresh: boolean, ageHours: number | null, filePath: string | null }
|
|
274
|
+
/**
|
|
275
|
+
* @param {string} projectRoot - Absolute path to project root
|
|
276
|
+
* @param {string} libraryName - Library name
|
|
277
|
+
* @param {number} [maxAgeHours] - Maximum age in hours (default: 168 = 7 days)
|
|
278
|
+
* @returns {{ fresh: boolean, ageHours: number|null, filePath: string|null }}
|
|
279
|
+
*/
|
|
280
|
+
function checkFreshness(projectRoot, libraryName, maxAgeHours) {
|
|
281
|
+
const maxAge = maxAgeHours != null ? maxAgeHours : FRESHNESS_HOURS;
|
|
282
|
+
const filePath = getDocsPath(projectRoot, libraryName);
|
|
283
|
+
|
|
284
|
+
if (!fs.existsSync(filePath)) {
|
|
285
|
+
return { fresh: false, ageHours: null, filePath: null };
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
try {
|
|
289
|
+
const stat = fs.statSync(filePath);
|
|
290
|
+
const ageMs = Math.max(0, Date.now() - stat.mtime.getTime());
|
|
291
|
+
const ageHours = Math.floor(ageMs / (1000 * 60 * 60));
|
|
292
|
+
return {
|
|
293
|
+
fresh: ageHours <= maxAge,
|
|
294
|
+
ageHours,
|
|
295
|
+
filePath,
|
|
296
|
+
};
|
|
297
|
+
} catch (_e) {
|
|
298
|
+
return { fresh: false, ageHours: null, filePath: null };
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// @gsd-api getDocsPath(projectRoot, libraryName) -- Returns the expected path for a library's cached docs.
|
|
303
|
+
/**
|
|
304
|
+
* @param {string} projectRoot - Absolute path to project root
|
|
305
|
+
* @param {string} libraryName - Library name
|
|
306
|
+
* @returns {string}
|
|
307
|
+
*/
|
|
308
|
+
function getDocsPath(projectRoot, libraryName) {
|
|
309
|
+
return path.join(projectRoot, STACK_DOCS_DIR, `${libraryName}.md`);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// @gsd-api parseFreshnessFromContent(content) -- Extracts freshness date from doc file header comment.
|
|
313
|
+
// @gsd-todo(ref:AC-84) Stack-docs carry freshness marker (fetch date). Docs older than 7 days auto-refreshed.
|
|
314
|
+
/**
|
|
315
|
+
* Parse the fetch date from a stack doc file's header.
|
|
316
|
+
* Looks for: <!-- Fetched: ISO_DATE --> or <!-- Written: ISO_DATE -->
|
|
317
|
+
*
|
|
318
|
+
* @param {string} content - File content
|
|
319
|
+
* @returns {string|null} - ISO date string or null if not found
|
|
320
|
+
*/
|
|
321
|
+
function parseFreshnessFromContent(content) {
|
|
322
|
+
const match = content.match(/<!--\s*(?:Fetched|Written):\s*(\d{4}-\d{2}-\d{2}T[^\s>]+)\s*-->/);
|
|
323
|
+
return match ? match[1] : null;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// @gsd-api checkFreshnessEnhanced(projectRoot, libraryName, maxAgeDays) -- Checks freshness using embedded date marker.
|
|
327
|
+
/**
|
|
328
|
+
* @param {string} projectRoot - Absolute path to project root
|
|
329
|
+
* @param {string} libraryName - Library name
|
|
330
|
+
* @param {number} [maxAgeDays] - Maximum age in days (default: 7)
|
|
331
|
+
* @returns {{ fresh: boolean, ageHours: number|null, fetchDate: string|null, filePath: string|null }}
|
|
332
|
+
*/
|
|
333
|
+
function checkFreshnessEnhanced(projectRoot, libraryName, maxAgeDays) {
|
|
334
|
+
const maxDays = maxAgeDays != null ? maxAgeDays : FRESHNESS_DAYS;
|
|
335
|
+
const filePath = path.join(projectRoot, STACK_DOCS_DIR, `${libraryName}.md`);
|
|
336
|
+
|
|
337
|
+
if (!fs.existsSync(filePath)) {
|
|
338
|
+
return { fresh: false, ageHours: null, fetchDate: null, filePath: null };
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
try {
|
|
342
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
343
|
+
const fetchDate = parseFreshnessFromContent(content);
|
|
344
|
+
|
|
345
|
+
if (!fetchDate) {
|
|
346
|
+
// No freshness marker -- treat as stale, use file mtime as fallback
|
|
347
|
+
const stat = fs.statSync(filePath);
|
|
348
|
+
const ageMs = Math.max(0, Date.now() - stat.mtime.getTime());
|
|
349
|
+
const ageHours = Math.floor(ageMs / (1000 * 60 * 60));
|
|
350
|
+
return {
|
|
351
|
+
fresh: ageHours <= maxDays * 24,
|
|
352
|
+
ageHours,
|
|
353
|
+
fetchDate: null,
|
|
354
|
+
filePath,
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const fetchTime = new Date(fetchDate).getTime();
|
|
359
|
+
const ageMs = Math.max(0, Date.now() - fetchTime);
|
|
360
|
+
const ageHours = Math.floor(ageMs / (1000 * 60 * 60));
|
|
361
|
+
|
|
362
|
+
return {
|
|
363
|
+
fresh: ageHours <= maxDays * 24,
|
|
364
|
+
ageHours,
|
|
365
|
+
fetchDate,
|
|
366
|
+
filePath,
|
|
367
|
+
};
|
|
368
|
+
} catch (_e) {
|
|
369
|
+
return { fresh: false, ageHours: null, fetchDate: null, filePath: null };
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// @gsd-api fetchDocsWithFreshness(projectRoot, libraryId, query) -- Fetches docs with embedded freshness marker.
|
|
374
|
+
// @gsd-todo(ref:AC-82) Store fetched stack docs in .cap/stack-docs/{library-name}.md
|
|
375
|
+
/**
|
|
376
|
+
* @param {string} projectRoot - Absolute path to project root
|
|
377
|
+
* @param {string} libraryId - Context7 library ID (e.g., "/vercel/next.js")
|
|
378
|
+
* @param {string} [query] - Optional query to focus documentation
|
|
379
|
+
* @returns {{ success: boolean, filePath: string|null, error: string|null }}
|
|
380
|
+
*/
|
|
381
|
+
function fetchDocsWithFreshness(projectRoot, libraryId, query) {
|
|
382
|
+
const docsDir = path.join(projectRoot, STACK_DOCS_DIR);
|
|
383
|
+
fs.mkdirSync(docsDir, { recursive: true });
|
|
384
|
+
|
|
385
|
+
const libName = libraryId.split('/').pop() || libraryId.replace(/\//g, '-');
|
|
386
|
+
const filePath = path.join(docsDir, `${libName}.md`);
|
|
387
|
+
|
|
388
|
+
const queryStr = query ? `"${query}"` : '""';
|
|
389
|
+
try {
|
|
390
|
+
const output = execSync(
|
|
391
|
+
`npx ctx7@latest docs ${libraryId} ${queryStr}`,
|
|
392
|
+
{ encoding: 'utf8', timeout: 60000, stdio: ['pipe', 'pipe', 'pipe'] }
|
|
393
|
+
);
|
|
394
|
+
|
|
395
|
+
if (!output || output.trim().length === 0) {
|
|
396
|
+
return { success: false, filePath: null, error: 'Empty response from Context7' };
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Write docs with freshness metadata header
|
|
400
|
+
const now = new Date().toISOString();
|
|
401
|
+
const header = [
|
|
402
|
+
`<!-- CAP Stack Docs: ${libraryId} -->`,
|
|
403
|
+
`<!-- Fetched: ${now} -->`,
|
|
404
|
+
`<!-- Query: ${query || 'general'} -->`,
|
|
405
|
+
`<!-- Freshness: valid until ${new Date(Date.now() + FRESHNESS_DAYS * 24 * 60 * 60 * 1000).toISOString()} -->`,
|
|
406
|
+
'',
|
|
407
|
+
].join('\n');
|
|
408
|
+
|
|
409
|
+
fs.writeFileSync(filePath, header + output, 'utf8');
|
|
410
|
+
return { success: true, filePath, error: null };
|
|
411
|
+
} catch (e) {
|
|
412
|
+
return { success: false, filePath: null, error: e.message || 'Unknown error' };
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// @gsd-api batchFetchDocs(projectRoot, dependencies, options) -- Orchestrates batch fetch for /cap:init.
|
|
417
|
+
// @gsd-todo(ref:AC-85) Context7 fetching is MANDATORY at init. If unreachable, warning emitted and init continues.
|
|
418
|
+
/**
|
|
419
|
+
* Fetch stack docs for multiple dependencies. Skips already-fresh docs.
|
|
420
|
+
*
|
|
421
|
+
* @param {string} projectRoot - Absolute path to project root
|
|
422
|
+
* @param {string[]} dependencies - Array of dependency names to fetch
|
|
423
|
+
* @param {Object} [options]
|
|
424
|
+
* @param {number} [options.maxDeps] - Maximum number of deps to fetch (default: 15)
|
|
425
|
+
* @param {boolean} [options.force] - Force refresh even if fresh (default: false)
|
|
426
|
+
* @returns {{ total: number, fetched: number, failed: number, skipped: number, context7Available: boolean, errors: string[] }}
|
|
427
|
+
*/
|
|
428
|
+
function batchFetchDocs(projectRoot, dependencies, options = {}) {
|
|
429
|
+
const maxDeps = options.maxDeps || 15;
|
|
430
|
+
const force = options.force || false;
|
|
431
|
+
|
|
432
|
+
// Filter out internal/scoped packages that Context7 likely does not have
|
|
433
|
+
const fetchable = dependencies
|
|
434
|
+
.filter(dep => !dep.startsWith('@') || dep.startsWith('@angular/') || dep.startsWith('@nestjs/'))
|
|
435
|
+
.slice(0, maxDeps);
|
|
436
|
+
|
|
437
|
+
const result = {
|
|
438
|
+
total: fetchable.length,
|
|
439
|
+
fetched: 0,
|
|
440
|
+
failed: 0,
|
|
441
|
+
skipped: 0,
|
|
442
|
+
context7Available: false,
|
|
443
|
+
errors: [],
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
for (const dep of fetchable) {
|
|
447
|
+
// Check freshness first (skip if already fresh and not forced)
|
|
448
|
+
if (!force) {
|
|
449
|
+
const freshness = checkFreshnessEnhanced(projectRoot, dep);
|
|
450
|
+
if (freshness.fresh) {
|
|
451
|
+
result.skipped++;
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Resolve library in Context7
|
|
457
|
+
// @gsd-risk Context7 resolution may fail for less popular libraries. Graceful skip per dep.
|
|
458
|
+
try {
|
|
459
|
+
const lib = resolveLibrary(dep, 'API surface and configuration');
|
|
460
|
+
if (!lib) {
|
|
461
|
+
result.failed++;
|
|
462
|
+
result.errors.push(`${dep}: not found in Context7`);
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const fetchResult = fetchDocsWithFreshness(
|
|
467
|
+
projectRoot,
|
|
468
|
+
lib.id,
|
|
469
|
+
'API surface, configuration, breaking changes'
|
|
470
|
+
);
|
|
471
|
+
|
|
472
|
+
if (fetchResult.success) {
|
|
473
|
+
result.fetched++;
|
|
474
|
+
result.context7Available = true;
|
|
475
|
+
} else {
|
|
476
|
+
result.failed++;
|
|
477
|
+
result.errors.push(`${dep}: ${fetchResult.error}`);
|
|
478
|
+
}
|
|
479
|
+
} catch (e) {
|
|
480
|
+
result.failed++;
|
|
481
|
+
result.errors.push(`${dep}: ${e.message}`);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
return result;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// @gsd-api getStaleLibraries(projectRoot) -- Returns list of libraries with stale (>7 day) docs.
|
|
489
|
+
/**
|
|
490
|
+
* @param {string} projectRoot - Absolute path to project root
|
|
491
|
+
* @returns {Array<{libraryName: string, ageHours: number, fetchDate: string|null}>}
|
|
492
|
+
*/
|
|
493
|
+
function getStaleLibraries(projectRoot) {
|
|
494
|
+
const docsDir = path.join(projectRoot, STACK_DOCS_DIR);
|
|
495
|
+
if (!fs.existsSync(docsDir)) return [];
|
|
496
|
+
|
|
497
|
+
const stale = [];
|
|
498
|
+
try {
|
|
499
|
+
const files = fs.readdirSync(docsDir).filter(f => f.endsWith('.md'));
|
|
500
|
+
for (const f of files) {
|
|
501
|
+
const libName = f.replace(/\.md$/, '');
|
|
502
|
+
const freshness = checkFreshnessEnhanced(projectRoot, libName);
|
|
503
|
+
if (!freshness.fresh) {
|
|
504
|
+
stale.push({
|
|
505
|
+
libraryName: libName,
|
|
506
|
+
ageHours: freshness.ageHours,
|
|
507
|
+
fetchDate: freshness.fetchDate,
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
} catch (_e) {
|
|
512
|
+
// Ignore
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
return stale;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// @gsd-api detectWorkspacePackages(projectRoot) -- Detects monorepo workspace packages for cross-package scanning.
|
|
519
|
+
/**
|
|
520
|
+
* @param {string} projectRoot - Absolute path to project root
|
|
521
|
+
* @returns {{ isMonorepo: boolean, packages: string[] }}
|
|
522
|
+
*/
|
|
523
|
+
function detectWorkspacePackages(projectRoot) {
|
|
524
|
+
const result = { isMonorepo: false, packages: [] };
|
|
525
|
+
|
|
526
|
+
const pkgPath = path.join(projectRoot, 'package.json');
|
|
527
|
+
if (fs.existsSync(pkgPath)) {
|
|
528
|
+
try {
|
|
529
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
530
|
+
if (pkg.workspaces) {
|
|
531
|
+
result.isMonorepo = true;
|
|
532
|
+
const wsPatterns = Array.isArray(pkg.workspaces)
|
|
533
|
+
? pkg.workspaces
|
|
534
|
+
: (pkg.workspaces.packages || []);
|
|
535
|
+
|
|
536
|
+
for (const pattern of wsPatterns) {
|
|
537
|
+
const baseDir = pattern.replace(/\/\*.*$/, '');
|
|
538
|
+
const fullDir = path.join(projectRoot, baseDir);
|
|
539
|
+
if (fs.existsSync(fullDir) && fs.statSync(fullDir).isDirectory()) {
|
|
540
|
+
const entries = fs.readdirSync(fullDir, { withFileTypes: true });
|
|
541
|
+
for (const entry of entries) {
|
|
542
|
+
if (entry.isDirectory()) {
|
|
543
|
+
result.packages.push(path.join(baseDir, entry.name));
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
} catch (_e) {
|
|
550
|
+
// Ignore
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// Check lerna.json
|
|
555
|
+
const lernaPath = path.join(projectRoot, 'lerna.json');
|
|
556
|
+
if (!result.isMonorepo && fs.existsSync(lernaPath)) {
|
|
557
|
+
try {
|
|
558
|
+
const lerna = JSON.parse(fs.readFileSync(lernaPath, 'utf8'));
|
|
559
|
+
result.isMonorepo = true;
|
|
560
|
+
const patterns = lerna.packages || ['packages/*'];
|
|
561
|
+
for (const pattern of patterns) {
|
|
562
|
+
const baseDir = pattern.replace(/\/\*.*$/, '');
|
|
563
|
+
const fullDir = path.join(projectRoot, baseDir);
|
|
564
|
+
if (fs.existsSync(fullDir) && fs.statSync(fullDir).isDirectory()) {
|
|
565
|
+
const entries = fs.readdirSync(fullDir, { withFileTypes: true });
|
|
566
|
+
for (const entry of entries) {
|
|
567
|
+
if (entry.isDirectory()) {
|
|
568
|
+
result.packages.push(path.join(baseDir, entry.name));
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
} catch (_e) {
|
|
574
|
+
// Ignore
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
return result;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
module.exports = {
|
|
582
|
+
STACK_DOCS_DIR,
|
|
583
|
+
FRESHNESS_DAYS,
|
|
584
|
+
FRESHNESS_HOURS,
|
|
585
|
+
detectDependencies,
|
|
586
|
+
resolveLibrary,
|
|
587
|
+
fetchDocs,
|
|
588
|
+
writeDocs,
|
|
589
|
+
listCachedDocs,
|
|
590
|
+
checkFreshness,
|
|
591
|
+
getDocsPath,
|
|
592
|
+
parseFreshnessFromContent,
|
|
593
|
+
checkFreshnessEnhanced,
|
|
594
|
+
fetchDocsWithFreshness,
|
|
595
|
+
batchFetchDocs,
|
|
596
|
+
getStaleLibraries,
|
|
597
|
+
detectWorkspacePackages,
|
|
598
|
+
};
|