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,506 @@
|
|
|
1
|
+
// @gsd-context CAP v2.0 Feature Map reader/writer -- FEATURE-MAP.md is the single source of truth for all features, ACs, status, and dependencies.
|
|
2
|
+
// @gsd-decision Markdown format for Feature Map (not JSON/YAML) -- human-readable, diffable in git, editable in any text editor. Machine-readable via regex parsing of structured table rows.
|
|
3
|
+
// @gsd-decision Read and write are separate operations -- no in-memory mutation API. Read returns structured data, write takes structured data and serializes to markdown.
|
|
4
|
+
// @gsd-constraint Zero external dependencies -- uses only Node.js built-ins (fs, path).
|
|
5
|
+
// @gsd-pattern Feature Map is the bridge between all CAP workflows. Brainstorm writes entries, scan updates status, status reads for dashboard.
|
|
6
|
+
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
const fs = require('node:fs');
|
|
10
|
+
const path = require('node:path');
|
|
11
|
+
|
|
12
|
+
const FEATURE_MAP_FILE = 'FEATURE-MAP.md';
|
|
13
|
+
|
|
14
|
+
// @gsd-todo(ref:AC-9) Feature state lifecycle: planned -> prototyped -> tested -> shipped
|
|
15
|
+
const VALID_STATES = ['planned', 'prototyped', 'tested', 'shipped'];
|
|
16
|
+
const STATE_TRANSITIONS = {
|
|
17
|
+
planned: ['prototyped'],
|
|
18
|
+
prototyped: ['tested'],
|
|
19
|
+
tested: ['shipped'],
|
|
20
|
+
shipped: [],
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @typedef {Object} AcceptanceCriterion
|
|
25
|
+
* @property {string} id - AC identifier (e.g., "AC-1")
|
|
26
|
+
* @property {string} description - Imperative description text
|
|
27
|
+
* @property {'pending'|'implemented'|'tested'|'reviewed'} status - Current status
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @typedef {Object} Feature
|
|
32
|
+
* @property {string} id - Feature ID (e.g., "F-001")
|
|
33
|
+
* @property {string} title - Feature title (verb+object format)
|
|
34
|
+
* @property {'planned'|'prototyped'|'tested'|'shipped'} state - Feature lifecycle state
|
|
35
|
+
* @property {AcceptanceCriterion[]} acs - Acceptance criteria
|
|
36
|
+
* @property {string[]} files - File references linked to this feature
|
|
37
|
+
* @property {string[]} dependencies - Feature IDs this depends on
|
|
38
|
+
* @property {Object<string,string>} metadata - Additional key-value metadata
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @typedef {Object} FeatureMap
|
|
43
|
+
* @property {Feature[]} features - All features
|
|
44
|
+
* @property {string} lastScan - ISO timestamp of last scan
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
// @gsd-todo(ref:AC-7) Feature Map is a single Markdown file at the project root named FEATURE-MAP.md
|
|
48
|
+
|
|
49
|
+
// @gsd-todo(ref:AC-1) Generate empty FEATURE-MAP.md template with section headers (Features, Legend) and no feature entries
|
|
50
|
+
/**
|
|
51
|
+
* Generate the empty FEATURE-MAP.md template for /cap:init.
|
|
52
|
+
* @returns {string}
|
|
53
|
+
*/
|
|
54
|
+
function generateTemplate() {
|
|
55
|
+
return `# Feature Map
|
|
56
|
+
|
|
57
|
+
> Single source of truth for feature identity, state, acceptance criteria, and relationships.
|
|
58
|
+
> Auto-enriched by \`@cap-feature\` tags and dependency analysis.
|
|
59
|
+
|
|
60
|
+
## Features
|
|
61
|
+
|
|
62
|
+
<!-- No features yet. Run /cap:brainstorm or add features with addFeature(). -->
|
|
63
|
+
|
|
64
|
+
## Legend
|
|
65
|
+
|
|
66
|
+
| State | Meaning |
|
|
67
|
+
|-------|---------|
|
|
68
|
+
| planned | Feature identified, not yet implemented |
|
|
69
|
+
| prototyped | Initial implementation exists |
|
|
70
|
+
| tested | Tests written and passing |
|
|
71
|
+
| shipped | Deployed / merged to main |
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
*Last updated: ${new Date().toISOString()}*
|
|
75
|
+
`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// @gsd-api readFeatureMap(projectRoot) -- Reads and parses FEATURE-MAP.md from project root.
|
|
79
|
+
// Returns: FeatureMap object with features and lastScan timestamp.
|
|
80
|
+
// @gsd-todo(ref:AC-10) Feature Map is the single source of truth for feature identity, state, ACs, and relationships
|
|
81
|
+
/**
|
|
82
|
+
* @param {string} projectRoot - Absolute path to project root
|
|
83
|
+
* @returns {FeatureMap}
|
|
84
|
+
*/
|
|
85
|
+
function readFeatureMap(projectRoot) {
|
|
86
|
+
const filePath = path.join(projectRoot, FEATURE_MAP_FILE);
|
|
87
|
+
if (!fs.existsSync(filePath)) {
|
|
88
|
+
return { features: [], lastScan: null };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
92
|
+
return parseFeatureMapContent(content);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// @gsd-todo(ref:AC-8) Each feature entry contains: feature ID, title, state, ACs, and file references
|
|
96
|
+
// @gsd-todo(ref:AC-14) Feature Map scales to 80-120 features in a single file
|
|
97
|
+
/**
|
|
98
|
+
* Parse FEATURE-MAP.md content into structured data.
|
|
99
|
+
* @param {string} content - Raw markdown content
|
|
100
|
+
* @returns {FeatureMap}
|
|
101
|
+
*/
|
|
102
|
+
function parseFeatureMapContent(content) {
|
|
103
|
+
const features = [];
|
|
104
|
+
const lines = content.split('\n');
|
|
105
|
+
|
|
106
|
+
// Match feature headers: ### F-001: Title text [state]
|
|
107
|
+
const featureHeaderRE = /^###\s+(F-\d{3}):\s+(.+?)\s+\[(\w+)\]\s*$/;
|
|
108
|
+
// Match AC rows: | AC-N | status | description |
|
|
109
|
+
const acRowRE = /^\|\s*(AC-\d+)\s*\|\s*(\w+)\s*\|\s*(.+?)\s*\|/;
|
|
110
|
+
// Match file refs: - `path/to/file`
|
|
111
|
+
const fileRefRE = /^-\s+`(.+?)`/;
|
|
112
|
+
// Match dependencies: **Depends on:** F-001, F-002
|
|
113
|
+
const depsRE = /^\*\*Depends on:\*\*\s*(.+)/;
|
|
114
|
+
// Match lastScan in footer
|
|
115
|
+
const lastScanRE = /^\*Last updated:\s*(.+?)\*$/;
|
|
116
|
+
|
|
117
|
+
let currentFeature = null;
|
|
118
|
+
let inAcTable = false;
|
|
119
|
+
let inFileRefs = false;
|
|
120
|
+
let lastScan = null;
|
|
121
|
+
|
|
122
|
+
for (const line of lines) {
|
|
123
|
+
const headerMatch = line.match(featureHeaderRE);
|
|
124
|
+
if (headerMatch) {
|
|
125
|
+
if (currentFeature) features.push(currentFeature);
|
|
126
|
+
currentFeature = {
|
|
127
|
+
id: headerMatch[1],
|
|
128
|
+
title: headerMatch[2],
|
|
129
|
+
state: headerMatch[3],
|
|
130
|
+
acs: [],
|
|
131
|
+
files: [],
|
|
132
|
+
dependencies: [],
|
|
133
|
+
metadata: {},
|
|
134
|
+
};
|
|
135
|
+
inAcTable = false;
|
|
136
|
+
inFileRefs = false;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (!currentFeature) {
|
|
141
|
+
const scanMatch = line.match(lastScanRE);
|
|
142
|
+
if (scanMatch) lastScan = scanMatch[1].trim();
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Detect AC table start
|
|
147
|
+
if (line.startsWith('| AC') && line.includes('Status')) {
|
|
148
|
+
inAcTable = true;
|
|
149
|
+
inFileRefs = false;
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
// Skip table separator
|
|
153
|
+
if (line.match(/^\|[\s-]+\|/)) continue;
|
|
154
|
+
|
|
155
|
+
const acMatch = line.match(acRowRE);
|
|
156
|
+
if (acMatch && inAcTable) {
|
|
157
|
+
currentFeature.acs.push({
|
|
158
|
+
id: acMatch[1],
|
|
159
|
+
description: acMatch[3].trim(),
|
|
160
|
+
status: acMatch[2].toLowerCase(),
|
|
161
|
+
});
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// File references section
|
|
166
|
+
if (line.startsWith('**Files:**')) {
|
|
167
|
+
inFileRefs = true;
|
|
168
|
+
inAcTable = false;
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (inFileRefs) {
|
|
173
|
+
const refMatch = line.match(fileRefRE);
|
|
174
|
+
if (refMatch) {
|
|
175
|
+
currentFeature.files.push(refMatch[1]);
|
|
176
|
+
continue;
|
|
177
|
+
} else if (line.trim() === '') {
|
|
178
|
+
inFileRefs = false;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Dependencies
|
|
183
|
+
const depsMatch = line.match(depsRE);
|
|
184
|
+
if (depsMatch) {
|
|
185
|
+
currentFeature.dependencies = depsMatch[1].split(',').map(d => d.trim()).filter(Boolean);
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const scanMatch = line.match(lastScanRE);
|
|
190
|
+
if (scanMatch) lastScan = scanMatch[1].trim();
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (currentFeature) features.push(currentFeature);
|
|
194
|
+
|
|
195
|
+
return { features, lastScan };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// @gsd-api writeFeatureMap(projectRoot, featureMap) -- Serializes FeatureMap to FEATURE-MAP.md.
|
|
199
|
+
// Side effect: overwrites FEATURE-MAP.md at project root.
|
|
200
|
+
/**
|
|
201
|
+
* @param {string} projectRoot - Absolute path to project root
|
|
202
|
+
* @param {FeatureMap} featureMap - Structured feature map data
|
|
203
|
+
*/
|
|
204
|
+
function writeFeatureMap(projectRoot, featureMap) {
|
|
205
|
+
const filePath = path.join(projectRoot, FEATURE_MAP_FILE);
|
|
206
|
+
const content = serializeFeatureMap(featureMap);
|
|
207
|
+
fs.writeFileSync(filePath, content, 'utf8');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Serialize FeatureMap to markdown string.
|
|
212
|
+
* @param {FeatureMap} featureMap
|
|
213
|
+
* @returns {string}
|
|
214
|
+
*/
|
|
215
|
+
function serializeFeatureMap(featureMap) {
|
|
216
|
+
const lines = [
|
|
217
|
+
'# Feature Map',
|
|
218
|
+
'',
|
|
219
|
+
'> Single source of truth for feature identity, state, acceptance criteria, and relationships.',
|
|
220
|
+
'> Auto-enriched by `@cap-feature` tags and dependency analysis.',
|
|
221
|
+
'',
|
|
222
|
+
'## Features',
|
|
223
|
+
'',
|
|
224
|
+
];
|
|
225
|
+
|
|
226
|
+
for (const feature of featureMap.features) {
|
|
227
|
+
lines.push(`### ${feature.id}: ${feature.title} [${feature.state}]`);
|
|
228
|
+
lines.push('');
|
|
229
|
+
|
|
230
|
+
if (feature.dependencies.length > 0) {
|
|
231
|
+
lines.push(`**Depends on:** ${feature.dependencies.join(', ')}`);
|
|
232
|
+
lines.push('');
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (feature.acs.length > 0) {
|
|
236
|
+
lines.push('| AC | Status | Description |');
|
|
237
|
+
lines.push('|----|--------|-------------|');
|
|
238
|
+
for (const ac of feature.acs) {
|
|
239
|
+
lines.push(`| ${ac.id} | ${ac.status} | ${ac.description} |`);
|
|
240
|
+
}
|
|
241
|
+
lines.push('');
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (feature.files.length > 0) {
|
|
245
|
+
lines.push('**Files:**');
|
|
246
|
+
for (const file of feature.files) {
|
|
247
|
+
lines.push(`- \`${file}\``);
|
|
248
|
+
}
|
|
249
|
+
lines.push('');
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (featureMap.features.length === 0) {
|
|
254
|
+
lines.push('<!-- No features yet. Run /cap:brainstorm or add features with addFeature(). -->');
|
|
255
|
+
lines.push('');
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
lines.push('## Legend');
|
|
259
|
+
lines.push('');
|
|
260
|
+
lines.push('| State | Meaning |');
|
|
261
|
+
lines.push('|-------|---------|');
|
|
262
|
+
lines.push('| planned | Feature identified, not yet implemented |');
|
|
263
|
+
lines.push('| prototyped | Initial implementation exists |');
|
|
264
|
+
lines.push('| tested | Tests written and passing |');
|
|
265
|
+
lines.push('| shipped | Deployed / merged to main |');
|
|
266
|
+
lines.push('');
|
|
267
|
+
lines.push('---');
|
|
268
|
+
lines.push(`*Last updated: ${new Date().toISOString()}*`);
|
|
269
|
+
lines.push('');
|
|
270
|
+
|
|
271
|
+
return lines.join('\n');
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// @gsd-api addFeature(projectRoot, feature) -- Add a new feature entry to FEATURE-MAP.md.
|
|
275
|
+
/**
|
|
276
|
+
* @param {string} projectRoot - Absolute path to project root
|
|
277
|
+
* @param {{ title: string, acs?: AcceptanceCriterion[], dependencies?: string[], metadata?: Object }} feature - Feature data (ID auto-generated)
|
|
278
|
+
* @returns {Feature} - The added feature with generated ID
|
|
279
|
+
*/
|
|
280
|
+
function addFeature(projectRoot, feature) {
|
|
281
|
+
const featureMap = readFeatureMap(projectRoot);
|
|
282
|
+
const id = getNextFeatureId(featureMap.features);
|
|
283
|
+
const newFeature = {
|
|
284
|
+
id,
|
|
285
|
+
title: feature.title,
|
|
286
|
+
state: 'planned',
|
|
287
|
+
acs: feature.acs || [],
|
|
288
|
+
files: [],
|
|
289
|
+
dependencies: feature.dependencies || [],
|
|
290
|
+
metadata: feature.metadata || {},
|
|
291
|
+
};
|
|
292
|
+
featureMap.features.push(newFeature);
|
|
293
|
+
writeFeatureMap(projectRoot, featureMap);
|
|
294
|
+
return newFeature;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// @gsd-api updateFeatureState(projectRoot, featureId, newState) -- Transition feature state.
|
|
298
|
+
// @gsd-todo(ref:AC-9) Enforce valid state transitions: planned->prototyped->tested->shipped
|
|
299
|
+
/**
|
|
300
|
+
* @param {string} projectRoot - Absolute path to project root
|
|
301
|
+
* @param {string} featureId - Feature ID (e.g., "F-001")
|
|
302
|
+
* @param {string} newState - Target state
|
|
303
|
+
* @returns {boolean} - True if transition was valid and applied
|
|
304
|
+
*/
|
|
305
|
+
function updateFeatureState(projectRoot, featureId, newState) {
|
|
306
|
+
if (!VALID_STATES.includes(newState)) return false;
|
|
307
|
+
|
|
308
|
+
const featureMap = readFeatureMap(projectRoot);
|
|
309
|
+
const feature = featureMap.features.find(f => f.id === featureId);
|
|
310
|
+
if (!feature) return false;
|
|
311
|
+
|
|
312
|
+
const allowed = STATE_TRANSITIONS[feature.state];
|
|
313
|
+
if (!allowed || !allowed.includes(newState)) return false;
|
|
314
|
+
|
|
315
|
+
feature.state = newState;
|
|
316
|
+
writeFeatureMap(projectRoot, featureMap);
|
|
317
|
+
return true;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// @gsd-api enrichFromTags(projectRoot, scanResults) -- Update file references from tag scan.
|
|
321
|
+
// @gsd-todo(ref:AC-12) Feature Map auto-enriched from @cap-feature tags found in source code
|
|
322
|
+
/**
|
|
323
|
+
* @param {string} projectRoot - Absolute path to project root
|
|
324
|
+
* @param {import('./cap-tag-scanner.cjs').CapTag[]} scanResults - Tags from cap-tag-scanner
|
|
325
|
+
* @returns {FeatureMap}
|
|
326
|
+
*/
|
|
327
|
+
function enrichFromTags(projectRoot, scanResults) {
|
|
328
|
+
const featureMap = readFeatureMap(projectRoot);
|
|
329
|
+
|
|
330
|
+
for (const tag of scanResults) {
|
|
331
|
+
if (tag.type !== 'feature') continue;
|
|
332
|
+
const featureId = tag.metadata.feature;
|
|
333
|
+
if (!featureId) continue;
|
|
334
|
+
|
|
335
|
+
const feature = featureMap.features.find(f => f.id === featureId);
|
|
336
|
+
if (!feature) continue;
|
|
337
|
+
|
|
338
|
+
// Add file reference if not already present
|
|
339
|
+
if (!feature.files.includes(tag.file)) {
|
|
340
|
+
feature.files.push(tag.file);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
writeFeatureMap(projectRoot, featureMap);
|
|
345
|
+
return featureMap;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// @gsd-api enrichFromDeps(projectRoot) -- Read package.json, detect imports, add dependency info to features.
|
|
349
|
+
// @gsd-todo(ref:AC-13) Feature Map auto-enriched from dependency graph analysis, env vars, package.json
|
|
350
|
+
/**
|
|
351
|
+
* @param {string} projectRoot - Absolute path to project root
|
|
352
|
+
* @returns {{ dependencies: string[], devDependencies: string[], envVars: string[] }}
|
|
353
|
+
*/
|
|
354
|
+
function enrichFromDeps(projectRoot) {
|
|
355
|
+
const result = { dependencies: [], devDependencies: [], envVars: [] };
|
|
356
|
+
|
|
357
|
+
const pkgPath = path.join(projectRoot, 'package.json');
|
|
358
|
+
if (fs.existsSync(pkgPath)) {
|
|
359
|
+
try {
|
|
360
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
361
|
+
if (pkg.dependencies) result.dependencies = Object.keys(pkg.dependencies);
|
|
362
|
+
if (pkg.devDependencies) result.devDependencies = Object.keys(pkg.devDependencies);
|
|
363
|
+
} catch (_e) {
|
|
364
|
+
// Malformed package.json
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Scan for .env file to detect environment variables
|
|
369
|
+
const envPath = path.join(projectRoot, '.env');
|
|
370
|
+
if (fs.existsSync(envPath)) {
|
|
371
|
+
try {
|
|
372
|
+
const envContent = fs.readFileSync(envPath, 'utf8');
|
|
373
|
+
const envRE = /^([A-Z_][A-Z0-9_]*)=/gm;
|
|
374
|
+
let match;
|
|
375
|
+
while ((match = envRE.exec(envContent)) !== null) {
|
|
376
|
+
result.envVars.push(match[1]);
|
|
377
|
+
}
|
|
378
|
+
} catch (_e) {
|
|
379
|
+
// Ignore
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return result;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// @gsd-api getNextFeatureId(features) -- Generate next F-NNN ID.
|
|
387
|
+
/**
|
|
388
|
+
* @param {Feature[]} features - Existing features
|
|
389
|
+
* @returns {string} - Next feature ID (e.g., "F-001")
|
|
390
|
+
*/
|
|
391
|
+
function getNextFeatureId(features) {
|
|
392
|
+
if (!features || features.length === 0) return 'F-001';
|
|
393
|
+
|
|
394
|
+
let maxNum = 0;
|
|
395
|
+
for (const f of features) {
|
|
396
|
+
const match = f.id.match(/^F-(\d+)$/);
|
|
397
|
+
if (match) {
|
|
398
|
+
const num = parseInt(match[1], 10);
|
|
399
|
+
if (num > maxNum) maxNum = num;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return `F-${String(maxNum + 1).padStart(3, '0')}`;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// @gsd-api enrichFromScan(featureMap, tags) -- Updates Feature Map status from tag scan results.
|
|
407
|
+
// Returns: updated FeatureMap with AC statuses reflecting code annotations.
|
|
408
|
+
/**
|
|
409
|
+
* @param {FeatureMap} featureMap - Current feature map data
|
|
410
|
+
* @param {import('./cap-tag-scanner.cjs').CapTag[]} tags - Tags from cap-tag-scanner
|
|
411
|
+
* @returns {FeatureMap}
|
|
412
|
+
*/
|
|
413
|
+
function enrichFromScan(featureMap, tags) {
|
|
414
|
+
for (const tag of tags) {
|
|
415
|
+
if (tag.type !== 'feature') continue;
|
|
416
|
+
const featureId = tag.metadata.feature;
|
|
417
|
+
if (!featureId) continue;
|
|
418
|
+
|
|
419
|
+
const feature = featureMap.features.find(f => f.id === featureId);
|
|
420
|
+
if (!feature) continue;
|
|
421
|
+
|
|
422
|
+
// Add file reference
|
|
423
|
+
if (!feature.files.includes(tag.file)) {
|
|
424
|
+
feature.files.push(tag.file);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// If AC reference in metadata, mark it as implemented
|
|
428
|
+
const acRef = tag.metadata.ac;
|
|
429
|
+
if (acRef) {
|
|
430
|
+
const ac = feature.acs.find(a => a.id === acRef);
|
|
431
|
+
if (ac && ac.status === 'pending') {
|
|
432
|
+
ac.status = 'implemented';
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
return featureMap;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// @gsd-api addFeatures(featureMap, newFeatures) -- Adds new features to an existing Feature Map (from brainstorm).
|
|
441
|
+
// @gsd-todo(ref:AC-11) Feature Map supports auto-derivation from brainstorm output
|
|
442
|
+
/**
|
|
443
|
+
* @param {FeatureMap} featureMap - Current feature map data
|
|
444
|
+
* @param {Feature[]} newFeatures - Features to add
|
|
445
|
+
* @returns {FeatureMap}
|
|
446
|
+
*/
|
|
447
|
+
function addFeatures(featureMap, newFeatures) {
|
|
448
|
+
const existingIds = new Set(featureMap.features.map(f => f.id));
|
|
449
|
+
const existingTitles = new Set(featureMap.features.map(f => f.title.toLowerCase()));
|
|
450
|
+
|
|
451
|
+
for (const nf of newFeatures) {
|
|
452
|
+
// Skip duplicates by ID or title
|
|
453
|
+
if (existingIds.has(nf.id)) continue;
|
|
454
|
+
if (existingTitles.has(nf.title.toLowerCase())) continue;
|
|
455
|
+
|
|
456
|
+
featureMap.features.push(nf);
|
|
457
|
+
existingIds.add(nf.id);
|
|
458
|
+
existingTitles.add(nf.title.toLowerCase());
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
return featureMap;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// @gsd-api getStatus(featureMap) -- Computes aggregate project status from Feature Map.
|
|
465
|
+
/**
|
|
466
|
+
* @param {FeatureMap} featureMap
|
|
467
|
+
* @returns {{ totalFeatures: number, completedFeatures: number, totalACs: number, implementedACs: number, testedACs: number, reviewedACs: number }}
|
|
468
|
+
*/
|
|
469
|
+
function getStatus(featureMap) {
|
|
470
|
+
let totalFeatures = featureMap.features.length;
|
|
471
|
+
let completedFeatures = featureMap.features.filter(f => f.state === 'shipped').length;
|
|
472
|
+
let totalACs = 0;
|
|
473
|
+
let implementedACs = 0;
|
|
474
|
+
let testedACs = 0;
|
|
475
|
+
let reviewedACs = 0;
|
|
476
|
+
|
|
477
|
+
for (const f of featureMap.features) {
|
|
478
|
+
totalACs += f.acs.length;
|
|
479
|
+
for (const ac of f.acs) {
|
|
480
|
+
if (ac.status === 'implemented') implementedACs++;
|
|
481
|
+
if (ac.status === 'tested') testedACs++;
|
|
482
|
+
if (ac.status === 'reviewed') reviewedACs++;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
return { totalFeatures, completedFeatures, totalACs, implementedACs, testedACs, reviewedACs };
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
module.exports = {
|
|
490
|
+
FEATURE_MAP_FILE,
|
|
491
|
+
VALID_STATES,
|
|
492
|
+
STATE_TRANSITIONS,
|
|
493
|
+
generateTemplate,
|
|
494
|
+
readFeatureMap,
|
|
495
|
+
writeFeatureMap,
|
|
496
|
+
parseFeatureMapContent,
|
|
497
|
+
serializeFeatureMap,
|
|
498
|
+
addFeature,
|
|
499
|
+
updateFeatureState,
|
|
500
|
+
enrichFromTags,
|
|
501
|
+
enrichFromDeps,
|
|
502
|
+
getNextFeatureId,
|
|
503
|
+
enrichFromScan,
|
|
504
|
+
addFeatures,
|
|
505
|
+
getStatus,
|
|
506
|
+
};
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
// @gsd-context CAP v2.0 session manager -- manages .cap/SESSION.json for cross-conversation workflow state.
|
|
2
|
+
// @gsd-decision SESSION.json is ephemeral (gitignored) -- it tracks the current developer's workflow state, not project state. Project state lives in FEATURE-MAP.md.
|
|
3
|
+
// @gsd-decision JSON format (not markdown) -- session state is machine-consumed, not human-read. JSON is faster to parse and type-safe.
|
|
4
|
+
// @gsd-constraint Zero external dependencies -- uses only Node.js built-ins (fs, path).
|
|
5
|
+
// @gsd-pattern All session reads/writes go through this module -- no direct fs.readFileSync of SESSION.json elsewhere.
|
|
6
|
+
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
const fs = require('node:fs');
|
|
10
|
+
const path = require('node:path');
|
|
11
|
+
|
|
12
|
+
// @gsd-decision Session schema is flat and extensible -- new workflow commands can add keys without schema migration.
|
|
13
|
+
// @gsd-todo(ref:AC-16) SESSION.json tracks ephemeral workflow state: active feature ID, current workflow step, session timestamps
|
|
14
|
+
/**
|
|
15
|
+
* @typedef {Object} CapSession
|
|
16
|
+
* @property {string} version - Session schema version (e.g., "2.0.0")
|
|
17
|
+
* @property {string|null} lastCommand - Last /cap: command executed
|
|
18
|
+
* @property {string|null} lastCommandTimestamp - ISO timestamp of last command
|
|
19
|
+
* @property {string|null} activeFeature - Currently focused feature ID
|
|
20
|
+
* @property {string|null} step - Current workflow step
|
|
21
|
+
* @property {string|null} startedAt - ISO timestamp of when session started
|
|
22
|
+
* @property {string|null} activeDebugSession - Active debug session ID
|
|
23
|
+
* @property {Object<string,string>} metadata - Extensible key-value metadata
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const CAP_DIR = '.cap';
|
|
27
|
+
const SESSION_FILE = 'SESSION.json';
|
|
28
|
+
|
|
29
|
+
// @gsd-todo(ref:AC-3) .cap/.gitignore ignores SESSION.json (ephemeral state shall not be committed)
|
|
30
|
+
const GITIGNORE_CONTENT = `# CAP ephemeral state -- do not commit
|
|
31
|
+
SESSION.json
|
|
32
|
+
debug/
|
|
33
|
+
`;
|
|
34
|
+
|
|
35
|
+
// @gsd-api getDefaultSession() -- Returns a fresh default session object.
|
|
36
|
+
// @gsd-todo(ref:AC-2) SESSION.json with valid JSON structure: { active_feature: null, step: null, started_at: null }
|
|
37
|
+
/**
|
|
38
|
+
* @returns {CapSession}
|
|
39
|
+
*/
|
|
40
|
+
function getDefaultSession() {
|
|
41
|
+
return {
|
|
42
|
+
version: '2.0.0',
|
|
43
|
+
lastCommand: null,
|
|
44
|
+
lastCommandTimestamp: null,
|
|
45
|
+
activeFeature: null,
|
|
46
|
+
step: null,
|
|
47
|
+
startedAt: null,
|
|
48
|
+
activeDebugSession: null,
|
|
49
|
+
metadata: {},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// @gsd-api loadSession(projectRoot) -- Loads .cap/SESSION.json. Returns default session if file missing or corrupt.
|
|
54
|
+
// @gsd-todo(ref:AC-19) SESSION.json is the only mutable session artifact
|
|
55
|
+
/**
|
|
56
|
+
* @param {string} projectRoot - Absolute path to project root
|
|
57
|
+
* @returns {CapSession}
|
|
58
|
+
*/
|
|
59
|
+
function loadSession(projectRoot) {
|
|
60
|
+
const sessionPath = path.join(projectRoot, CAP_DIR, SESSION_FILE);
|
|
61
|
+
try {
|
|
62
|
+
if (!fs.existsSync(sessionPath)) return getDefaultSession();
|
|
63
|
+
const content = fs.readFileSync(sessionPath, 'utf8');
|
|
64
|
+
const parsed = JSON.parse(content);
|
|
65
|
+
// Merge with defaults to handle missing keys from older versions
|
|
66
|
+
return { ...getDefaultSession(), ...parsed };
|
|
67
|
+
} catch (_e) {
|
|
68
|
+
// Corrupt JSON -- return default
|
|
69
|
+
return getDefaultSession();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// @gsd-api saveSession(projectRoot, session) -- Writes .cap/SESSION.json. Creates .cap/ directory if needed.
|
|
74
|
+
// @gsd-todo(ref:AC-18) SESSION.json shall not be committed to version control (enforced by .cap/.gitignore)
|
|
75
|
+
/**
|
|
76
|
+
* @param {string} projectRoot - Absolute path to project root
|
|
77
|
+
* @param {CapSession} session - Session data to persist
|
|
78
|
+
*/
|
|
79
|
+
function saveSession(projectRoot, session) {
|
|
80
|
+
const capDir = path.join(projectRoot, CAP_DIR);
|
|
81
|
+
if (!fs.existsSync(capDir)) {
|
|
82
|
+
fs.mkdirSync(capDir, { recursive: true });
|
|
83
|
+
}
|
|
84
|
+
const sessionPath = path.join(capDir, SESSION_FILE);
|
|
85
|
+
fs.writeFileSync(sessionPath, JSON.stringify(session, null, 2) + '\n', 'utf8');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// @gsd-api updateSession(projectRoot, updates) -- Partial update to session (merge, not overwrite).
|
|
89
|
+
/**
|
|
90
|
+
* @param {string} projectRoot - Absolute path to project root
|
|
91
|
+
* @param {Partial<CapSession>} updates - Fields to merge into current session
|
|
92
|
+
* @returns {CapSession} - The updated session
|
|
93
|
+
*/
|
|
94
|
+
function updateSession(projectRoot, updates) {
|
|
95
|
+
const session = loadSession(projectRoot);
|
|
96
|
+
// Shallow merge -- metadata gets replaced if present in updates
|
|
97
|
+
const updated = { ...session, ...updates };
|
|
98
|
+
saveSession(projectRoot, updated);
|
|
99
|
+
return updated;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// @gsd-api startSession(projectRoot, featureId, step) -- Set active feature and step with timestamp.
|
|
103
|
+
// @gsd-todo(ref:AC-17) SESSION.json connects to FEATURE-MAP.md only via feature IDs (loose coupling)
|
|
104
|
+
/**
|
|
105
|
+
* @param {string} projectRoot - Absolute path to project root
|
|
106
|
+
* @param {string} featureId - Feature ID to focus on (e.g., "F-001")
|
|
107
|
+
* @param {string} step - Current workflow step name
|
|
108
|
+
* @returns {CapSession}
|
|
109
|
+
*/
|
|
110
|
+
function startSession(projectRoot, featureId, step) {
|
|
111
|
+
return updateSession(projectRoot, {
|
|
112
|
+
activeFeature: featureId,
|
|
113
|
+
step,
|
|
114
|
+
startedAt: new Date().toISOString(),
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// @gsd-api updateStep(projectRoot, step) -- Update current workflow step.
|
|
119
|
+
/**
|
|
120
|
+
* @param {string} projectRoot - Absolute path to project root
|
|
121
|
+
* @param {string} step - New workflow step name
|
|
122
|
+
* @returns {CapSession}
|
|
123
|
+
*/
|
|
124
|
+
function updateStep(projectRoot, step) {
|
|
125
|
+
return updateSession(projectRoot, { step });
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// @gsd-api endSession(projectRoot) -- Clear active feature and step.
|
|
129
|
+
/**
|
|
130
|
+
* @param {string} projectRoot - Absolute path to project root
|
|
131
|
+
* @returns {CapSession}
|
|
132
|
+
*/
|
|
133
|
+
function endSession(projectRoot) {
|
|
134
|
+
return updateSession(projectRoot, {
|
|
135
|
+
activeFeature: null,
|
|
136
|
+
step: null,
|
|
137
|
+
startedAt: null,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// @gsd-api isInitialized(projectRoot) -- Check if .cap/ exists.
|
|
142
|
+
/**
|
|
143
|
+
* @param {string} projectRoot - Absolute path to project root
|
|
144
|
+
* @returns {boolean}
|
|
145
|
+
*/
|
|
146
|
+
function isInitialized(projectRoot) {
|
|
147
|
+
return fs.existsSync(path.join(projectRoot, CAP_DIR));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// @gsd-api initCapDirectory(projectRoot) -- Creates .cap/ directory structure and .gitignore. Idempotent.
|
|
151
|
+
// @gsd-todo(ref:AC-4) No prompts, questions, wizards, or configuration forms
|
|
152
|
+
// @gsd-todo(ref:AC-5) Completes in a single invocation with no follow-up steps
|
|
153
|
+
// @gsd-todo(ref:AC-6) Idempotent -- running on already-initialized project shall not overwrite existing content
|
|
154
|
+
/**
|
|
155
|
+
* @param {string} projectRoot - Absolute path to project root
|
|
156
|
+
*/
|
|
157
|
+
function initCapDirectory(projectRoot) {
|
|
158
|
+
const capDir = path.join(projectRoot, CAP_DIR);
|
|
159
|
+
const stackDocsDir = path.join(capDir, 'stack-docs');
|
|
160
|
+
const debugDir = path.join(capDir, 'debug');
|
|
161
|
+
const gitignorePath = path.join(capDir, '.gitignore');
|
|
162
|
+
const sessionPath = path.join(capDir, SESSION_FILE);
|
|
163
|
+
|
|
164
|
+
// Create directories (idempotent via recursive:true)
|
|
165
|
+
fs.mkdirSync(capDir, { recursive: true });
|
|
166
|
+
fs.mkdirSync(stackDocsDir, { recursive: true });
|
|
167
|
+
fs.mkdirSync(debugDir, { recursive: true });
|
|
168
|
+
|
|
169
|
+
// Write .gitignore (always overwrite -- it's infrastructure, not user content)
|
|
170
|
+
fs.writeFileSync(gitignorePath, GITIGNORE_CONTENT, 'utf8');
|
|
171
|
+
|
|
172
|
+
// Write SESSION.json only if it doesn't exist (preserve existing session)
|
|
173
|
+
if (!fs.existsSync(sessionPath)) {
|
|
174
|
+
saveSession(projectRoot, getDefaultSession());
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
module.exports = {
|
|
179
|
+
CAP_DIR,
|
|
180
|
+
SESSION_FILE,
|
|
181
|
+
GITIGNORE_CONTENT,
|
|
182
|
+
loadSession,
|
|
183
|
+
saveSession,
|
|
184
|
+
updateSession,
|
|
185
|
+
getDefaultSession,
|
|
186
|
+
startSession,
|
|
187
|
+
updateStep,
|
|
188
|
+
endSession,
|
|
189
|
+
isInitialized,
|
|
190
|
+
initCapDirectory,
|
|
191
|
+
};
|