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