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,507 @@
|
|
|
1
|
+
// @gsd-context Monorepo migration module -- audits existing .planning/ directories in apps, supports archive/replace/keep per app, analyzes root .planning/ for global vs app-specific split, regenerates scoped CODE-INVENTORY.md
|
|
2
|
+
// @gsd-decision Separate module from monorepo-context.cjs -- migration is a one-time destructive operation; context assembly is read-only and ongoing
|
|
3
|
+
// @gsd-constraint Zero external dependencies -- uses only Node.js built-ins (fs, path)
|
|
4
|
+
// @gsd-pattern Migration functions return audit/result objects -- callers (commands) handle user interaction and confirmation
|
|
5
|
+
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
const fs = require('node:fs');
|
|
9
|
+
const path = require('node:path');
|
|
10
|
+
const arcScanner = require('./arc-scanner.cjs');
|
|
11
|
+
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Constants
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
// @gsd-decision Standard .planning/ files recognized during audit -- matches the set that monorepo-context.cjs and extract-plan produce
|
|
17
|
+
const KNOWN_PLANNING_FILES = [
|
|
18
|
+
'PROJECT.md',
|
|
19
|
+
'ROADMAP.md',
|
|
20
|
+
'REQUIREMENTS.md',
|
|
21
|
+
'PRD.md',
|
|
22
|
+
'FEATURES.md',
|
|
23
|
+
'STATE.md',
|
|
24
|
+
'MILESTONES.md',
|
|
25
|
+
'RETROSPECTIVE.md',
|
|
26
|
+
'BRAINSTORM-LEDGER.md',
|
|
27
|
+
'config.json',
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
const KNOWN_PLANNING_DIRS = [
|
|
31
|
+
'prototype',
|
|
32
|
+
'phases',
|
|
33
|
+
'milestones',
|
|
34
|
+
'manifests',
|
|
35
|
+
'research',
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
// Audit types
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @typedef {Object} PlanningAuditEntry
|
|
44
|
+
* @property {string} appPath - Relative path to the app (e.g., 'apps/dashboard')
|
|
45
|
+
* @property {string} planningDir - Absolute path to app's .planning/
|
|
46
|
+
* @property {boolean} exists - Whether .planning/ exists in this app
|
|
47
|
+
* @property {string[]} files - Filenames found in .planning/
|
|
48
|
+
* @property {string[]} directories - Subdirectory names found in .planning/
|
|
49
|
+
* @property {boolean} hasCodeInventory - Whether prototype/CODE-INVENTORY.md exists
|
|
50
|
+
* @property {boolean} hasPrd - Whether PRD.md exists
|
|
51
|
+
*/
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @typedef {Object} MigrationAudit
|
|
55
|
+
* @property {PlanningAuditEntry[]} apps - Audit entry per app
|
|
56
|
+
* @property {number} appsWithPlanning - Count of apps that have .planning/
|
|
57
|
+
* @property {number} appsWithoutPlanning - Count of apps that lack .planning/
|
|
58
|
+
* @property {RootAnalysis} rootAnalysis - Analysis of root .planning/ contents
|
|
59
|
+
*/
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* @typedef {Object} RootAnalysis
|
|
63
|
+
* @property {string[]} globalFiles - Files that belong at root (PROJECT.md, ROADMAP.md, etc.)
|
|
64
|
+
* @property {string[]} appSpecificFiles - Files that likely belong to a specific app (PRD.md with app refs, etc.)
|
|
65
|
+
* @property {string[]} ambiguousFiles - Files that need user decision
|
|
66
|
+
* @property {string} rootPlanningDir - Absolute path to root .planning/
|
|
67
|
+
*/
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* @typedef {Object} MigrationAction
|
|
71
|
+
* @property {string} appPath - Relative path to app
|
|
72
|
+
* @property {'keep'|'archive'|'replace'} action - What to do with existing .planning/
|
|
73
|
+
*/
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* @typedef {Object} MigrationResult
|
|
77
|
+
* @property {string} appPath - Relative path to app
|
|
78
|
+
* @property {string} action - Action taken
|
|
79
|
+
* @property {boolean} success - Whether it succeeded
|
|
80
|
+
* @property {string|null} archivePath - Path to archive if action was 'archive'
|
|
81
|
+
* @property {string|null} error - Error message if failed
|
|
82
|
+
*/
|
|
83
|
+
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
// Audit functions
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
// @gsd-todo(ref:AC-9) Implement full audit: scan all app directories for existing .planning/ folders and report what exists where
|
|
89
|
+
// @gsd-api auditAppPlanning(rootPath, apps) -- returns MigrationAudit describing existing .planning/ state across all apps
|
|
90
|
+
/**
|
|
91
|
+
* Scan all app directories for existing .planning/ folders.
|
|
92
|
+
* Produces a structured audit of what exists where.
|
|
93
|
+
*
|
|
94
|
+
* @param {string} rootPath - Monorepo root
|
|
95
|
+
* @param {Array<{path: string, name: string}>} apps - Apps from workspace detection
|
|
96
|
+
* @returns {MigrationAudit}
|
|
97
|
+
*/
|
|
98
|
+
function auditAppPlanning(rootPath, apps) {
|
|
99
|
+
const entries = [];
|
|
100
|
+
let withPlanning = 0;
|
|
101
|
+
let withoutPlanning = 0;
|
|
102
|
+
|
|
103
|
+
for (const app of apps) {
|
|
104
|
+
const planningDir = path.join(rootPath, app.path, '.planning');
|
|
105
|
+
const exists = fs.existsSync(planningDir);
|
|
106
|
+
|
|
107
|
+
if (exists) {
|
|
108
|
+
withPlanning++;
|
|
109
|
+
const contents = safeReaddir(planningDir);
|
|
110
|
+
const files = contents.filter(name => {
|
|
111
|
+
const fullPath = path.join(planningDir, name);
|
|
112
|
+
return safeStat(fullPath, 'isFile');
|
|
113
|
+
});
|
|
114
|
+
const directories = contents.filter(name => {
|
|
115
|
+
const fullPath = path.join(planningDir, name);
|
|
116
|
+
return safeStat(fullPath, 'isDirectory');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
entries.push({
|
|
120
|
+
appPath: app.path,
|
|
121
|
+
planningDir,
|
|
122
|
+
exists: true,
|
|
123
|
+
files,
|
|
124
|
+
directories,
|
|
125
|
+
hasCodeInventory: fs.existsSync(path.join(planningDir, 'prototype', 'CODE-INVENTORY.md')),
|
|
126
|
+
hasPrd: files.includes('PRD.md'),
|
|
127
|
+
});
|
|
128
|
+
} else {
|
|
129
|
+
withoutPlanning++;
|
|
130
|
+
entries.push({
|
|
131
|
+
appPath: app.path,
|
|
132
|
+
planningDir,
|
|
133
|
+
exists: false,
|
|
134
|
+
files: [],
|
|
135
|
+
directories: [],
|
|
136
|
+
hasCodeInventory: false,
|
|
137
|
+
hasPrd: false,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const rootAnalysis = analyzeRootPlanning(rootPath);
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
apps: entries,
|
|
146
|
+
appsWithPlanning: withPlanning,
|
|
147
|
+
appsWithoutPlanning: withoutPlanning,
|
|
148
|
+
rootAnalysis,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ---------------------------------------------------------------------------
|
|
153
|
+
// Root analysis
|
|
154
|
+
// ---------------------------------------------------------------------------
|
|
155
|
+
|
|
156
|
+
// @gsd-todo(ref:AC-11) Implement root .planning/ analysis: classify files as global vs app-specific and guide user to split
|
|
157
|
+
// @gsd-api analyzeRootPlanning(rootPath) -- returns RootAnalysis classifying root .planning/ contents
|
|
158
|
+
/**
|
|
159
|
+
* Analyze root .planning/ contents and classify them as global or app-specific.
|
|
160
|
+
* Global: PROJECT.md, ROADMAP.md, REQUIREMENTS.md, manifests/
|
|
161
|
+
* App-specific: PRD.md that references a single app, app-named subdirectories
|
|
162
|
+
* Ambiguous: everything else -- user decides
|
|
163
|
+
*
|
|
164
|
+
* @param {string} rootPath - Monorepo root
|
|
165
|
+
* @returns {RootAnalysis}
|
|
166
|
+
*/
|
|
167
|
+
function analyzeRootPlanning(rootPath) {
|
|
168
|
+
const rootPlanningDir = path.join(rootPath, '.planning');
|
|
169
|
+
const globalFiles = [];
|
|
170
|
+
const appSpecificFiles = [];
|
|
171
|
+
const ambiguousFiles = [];
|
|
172
|
+
|
|
173
|
+
if (!fs.existsSync(rootPlanningDir)) {
|
|
174
|
+
return { globalFiles, appSpecificFiles, ambiguousFiles, rootPlanningDir };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// @gsd-decision Classify root files by name convention -- PROJECT.md, ROADMAP.md, REQUIREMENTS.md are always global; PRD.md and FEATURES.md at root are ambiguous in a monorepo
|
|
178
|
+
const GLOBAL_FILES = new Set(['PROJECT.md', 'ROADMAP.md', 'REQUIREMENTS.md', 'MILESTONES.md', 'config.json']);
|
|
179
|
+
const AMBIGUOUS_FILES = new Set(['PRD.md', 'FEATURES.md', 'STATE.md', 'BRAINSTORM-LEDGER.md', 'RETROSPECTIVE.md']);
|
|
180
|
+
|
|
181
|
+
const contents = safeReaddir(rootPlanningDir);
|
|
182
|
+
|
|
183
|
+
for (const name of contents) {
|
|
184
|
+
const fullPath = path.join(rootPlanningDir, name);
|
|
185
|
+
|
|
186
|
+
if (safeStat(fullPath, 'isDirectory')) {
|
|
187
|
+
// manifests/ is global; prototype/ with monolithic inventory is ambiguous
|
|
188
|
+
if (name === 'manifests') {
|
|
189
|
+
globalFiles.push(name + '/');
|
|
190
|
+
} else if (name === 'prototype') {
|
|
191
|
+
// @gsd-risk Root prototype/ may contain monolithic CODE-INVENTORY.md that should be split per app
|
|
192
|
+
ambiguousFiles.push(name + '/');
|
|
193
|
+
} else {
|
|
194
|
+
ambiguousFiles.push(name + '/');
|
|
195
|
+
}
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (GLOBAL_FILES.has(name)) {
|
|
200
|
+
globalFiles.push(name);
|
|
201
|
+
} else if (AMBIGUOUS_FILES.has(name)) {
|
|
202
|
+
// Further check: does the file reference a specific app?
|
|
203
|
+
const content = safeReadFile(fullPath);
|
|
204
|
+
if (content && looksAppSpecific(content)) {
|
|
205
|
+
appSpecificFiles.push(name);
|
|
206
|
+
} else {
|
|
207
|
+
ambiguousFiles.push(name);
|
|
208
|
+
}
|
|
209
|
+
} else {
|
|
210
|
+
ambiguousFiles.push(name);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return { globalFiles, appSpecificFiles, ambiguousFiles, rootPlanningDir };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Heuristic: does a planning file's content reference a single app?
|
|
219
|
+
* Checks for app-like path references (apps/something) in the first 20 lines.
|
|
220
|
+
*
|
|
221
|
+
* @param {string} content - File content
|
|
222
|
+
* @returns {boolean}
|
|
223
|
+
*/
|
|
224
|
+
function looksAppSpecific(content) {
|
|
225
|
+
// @gsd-risk Heuristic detection of app-specific content may produce false positives -- user confirmation is always required
|
|
226
|
+
const lines = content.split('\n').slice(0, 20);
|
|
227
|
+
const appRefs = lines.filter(line => /\bapps\/\w+/.test(line));
|
|
228
|
+
return appRefs.length >= 2;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// ---------------------------------------------------------------------------
|
|
232
|
+
// Migration actions
|
|
233
|
+
// ---------------------------------------------------------------------------
|
|
234
|
+
|
|
235
|
+
// @gsd-todo(ref:AC-10) Implement per-app keep/archive/replace: user chooses action for each app's existing .planning/
|
|
236
|
+
// @gsd-api executeAppMigration(rootPath, action) -- performs keep, archive, or replace on one app's .planning/
|
|
237
|
+
/**
|
|
238
|
+
* Execute a migration action on a single app's .planning/ directory.
|
|
239
|
+
*
|
|
240
|
+
* @param {string} rootPath - Monorepo root
|
|
241
|
+
* @param {MigrationAction} action - Action to perform
|
|
242
|
+
* @returns {MigrationResult}
|
|
243
|
+
*/
|
|
244
|
+
function executeAppMigration(rootPath, action) {
|
|
245
|
+
const planningDir = path.join(rootPath, action.appPath, '.planning');
|
|
246
|
+
|
|
247
|
+
try {
|
|
248
|
+
switch (action.action) {
|
|
249
|
+
case 'keep':
|
|
250
|
+
return { appPath: action.appPath, action: 'keep', success: true, archivePath: null, error: null };
|
|
251
|
+
|
|
252
|
+
case 'archive':
|
|
253
|
+
return archiveAppPlanning(rootPath, action.appPath);
|
|
254
|
+
|
|
255
|
+
case 'replace':
|
|
256
|
+
return replaceAppPlanning(rootPath, action.appPath);
|
|
257
|
+
|
|
258
|
+
default:
|
|
259
|
+
return { appPath: action.appPath, action: action.action, success: false, archivePath: null, error: `Unknown action: ${action.action}` };
|
|
260
|
+
}
|
|
261
|
+
} catch (err) {
|
|
262
|
+
return { appPath: action.appPath, action: action.action, success: false, archivePath: null, error: err.message };
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// @gsd-pattern Archive uses timestamp-based directory naming (legacy-{timestamp}) -- ensures idempotent re-runs never collide
|
|
267
|
+
/**
|
|
268
|
+
* Archive an app's existing .planning/ to .planning/legacy-{timestamp}/.
|
|
269
|
+
*
|
|
270
|
+
* @param {string} rootPath - Monorepo root
|
|
271
|
+
* @param {string} appPath - Relative path to app
|
|
272
|
+
* @returns {MigrationResult}
|
|
273
|
+
*/
|
|
274
|
+
function archiveAppPlanning(rootPath, appPath) {
|
|
275
|
+
const planningDir = path.join(rootPath, appPath, '.planning');
|
|
276
|
+
|
|
277
|
+
if (!fs.existsSync(planningDir)) {
|
|
278
|
+
return { appPath, action: 'archive', success: true, archivePath: null, error: null };
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// @gsd-decision Archive to legacy-{timestamp} inside the app's .planning/ -- keeps history co-located with the app
|
|
282
|
+
const timestamp = Date.now();
|
|
283
|
+
const archiveDir = path.join(planningDir, `legacy-${timestamp}`);
|
|
284
|
+
fs.mkdirSync(archiveDir, { recursive: true });
|
|
285
|
+
|
|
286
|
+
const contents = safeReaddir(planningDir);
|
|
287
|
+
for (const name of contents) {
|
|
288
|
+
if (name.startsWith('legacy-')) continue; // Don't archive previous archives
|
|
289
|
+
const src = path.join(planningDir, name);
|
|
290
|
+
const dest = path.join(archiveDir, name);
|
|
291
|
+
// @gsd-decision Use cpSync+rmSync instead of renameSync -- cross-device safe for monorepos spanning mounts
|
|
292
|
+
fs.cpSync(src, dest, { recursive: true });
|
|
293
|
+
fs.rmSync(src, { recursive: true, force: true });
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return { appPath, action: 'archive', success: true, archivePath: archiveDir, error: null };
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Replace an app's existing .planning/ with a fresh structure.
|
|
301
|
+
* Archives first, then creates fresh stubs.
|
|
302
|
+
*
|
|
303
|
+
* @param {string} rootPath - Monorepo root
|
|
304
|
+
* @param {string} appPath - Relative path to app
|
|
305
|
+
* @returns {MigrationResult}
|
|
306
|
+
*/
|
|
307
|
+
function replaceAppPlanning(rootPath, appPath) {
|
|
308
|
+
// Archive first so nothing is lost
|
|
309
|
+
const archiveResult = archiveAppPlanning(rootPath, appPath);
|
|
310
|
+
if (!archiveResult.success) return archiveResult;
|
|
311
|
+
|
|
312
|
+
// Create fresh structure using monorepo-context.cjs pattern
|
|
313
|
+
const planningDir = path.join(rootPath, appPath, '.planning');
|
|
314
|
+
const appName = path.basename(appPath);
|
|
315
|
+
fs.mkdirSync(planningDir, { recursive: true });
|
|
316
|
+
fs.mkdirSync(path.join(planningDir, 'prototype'), { recursive: true });
|
|
317
|
+
|
|
318
|
+
fs.writeFileSync(
|
|
319
|
+
path.join(planningDir, 'PRD.md'),
|
|
320
|
+
`# ${appName} -- PRD\n\nApp-scoped PRD. Generated by monorepo-init --migrate.\n`,
|
|
321
|
+
'utf-8'
|
|
322
|
+
);
|
|
323
|
+
fs.writeFileSync(
|
|
324
|
+
path.join(planningDir, 'FEATURES.md'),
|
|
325
|
+
`# ${appName} -- Feature Map\n\nRun extract-plan --app ${appPath} to populate.\n`,
|
|
326
|
+
'utf-8'
|
|
327
|
+
);
|
|
328
|
+
fs.writeFileSync(
|
|
329
|
+
path.join(planningDir, 'prototype', 'CODE-INVENTORY.md'),
|
|
330
|
+
`# CODE-INVENTORY.md\n\nRun /gsd:extract-plan --app ${appPath} to populate.\n`,
|
|
331
|
+
'utf-8'
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
return { appPath, action: 'replace', success: true, archivePath: archiveResult.archivePath, error: null };
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// ---------------------------------------------------------------------------
|
|
338
|
+
// Scoped CODE-INVENTORY regeneration
|
|
339
|
+
// ---------------------------------------------------------------------------
|
|
340
|
+
|
|
341
|
+
// @gsd-todo(ref:AC-12) Implement scoped CODE-INVENTORY.md regeneration per app after migration (replacing monolithic version)
|
|
342
|
+
// @gsd-api regenerateScopedInventories(rootPath, apps) -- triggers extract-tags per app to produce scoped CODE-INVENTORY.md files
|
|
343
|
+
/**
|
|
344
|
+
* Regenerate CODE-INVENTORY.md for each app by invoking the tag scanner
|
|
345
|
+
* scoped to each app directory.
|
|
346
|
+
*
|
|
347
|
+
* Note: This is a scaffold -- actual implementation calls extract-tags
|
|
348
|
+
* via the CLI or imports tag-scanner.cjs directly.
|
|
349
|
+
*
|
|
350
|
+
* @param {string} rootPath - Monorepo root
|
|
351
|
+
* @param {Array<{path: string}>} apps - Apps to regenerate inventories for
|
|
352
|
+
* @returns {Array<{appPath: string, inventoryPath: string, success: boolean, error: string|null}>}
|
|
353
|
+
*/
|
|
354
|
+
function regenerateScopedInventories(rootPath, apps) {
|
|
355
|
+
// @gsd-constraint Must use existing tag-scanner.cjs for extraction -- no reimplementation of scanning logic
|
|
356
|
+
const results = [];
|
|
357
|
+
|
|
358
|
+
for (const app of apps) {
|
|
359
|
+
const appAbsPath = path.join(rootPath, app.path);
|
|
360
|
+
const planningDir = path.join(appAbsPath, '.planning');
|
|
361
|
+
const inventoryPath = path.join(planningDir, 'prototype', 'CODE-INVENTORY.md');
|
|
362
|
+
|
|
363
|
+
try {
|
|
364
|
+
// Ensure directory exists
|
|
365
|
+
fs.mkdirSync(path.join(planningDir, 'prototype'), { recursive: true });
|
|
366
|
+
|
|
367
|
+
// Delegate to arc-scanner for actual tag extraction scoped to this app
|
|
368
|
+
arcScanner.cmdExtractTags(appAbsPath, appAbsPath, {
|
|
369
|
+
format: 'md',
|
|
370
|
+
outputFile: inventoryPath,
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
results.push({
|
|
374
|
+
appPath: app.path,
|
|
375
|
+
inventoryPath,
|
|
376
|
+
success: true,
|
|
377
|
+
error: null,
|
|
378
|
+
});
|
|
379
|
+
} catch (err) {
|
|
380
|
+
results.push({
|
|
381
|
+
appPath: app.path,
|
|
382
|
+
inventoryPath,
|
|
383
|
+
success: false,
|
|
384
|
+
error: err.message,
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
return results;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// ---------------------------------------------------------------------------
|
|
393
|
+
// Batch migration
|
|
394
|
+
// ---------------------------------------------------------------------------
|
|
395
|
+
|
|
396
|
+
// @gsd-api executeMigration(rootPath, apps, actions) -- runs all migration actions and returns aggregate results
|
|
397
|
+
/**
|
|
398
|
+
* Execute a full migration given user-selected actions per app.
|
|
399
|
+
*
|
|
400
|
+
* @param {string} rootPath - Monorepo root
|
|
401
|
+
* @param {Array<{path: string, name: string}>} apps - All apps from workspace detection
|
|
402
|
+
* @param {MigrationAction[]} actions - User-selected action per app
|
|
403
|
+
* @returns {{results: MigrationResult[], regeneration: Array}}
|
|
404
|
+
*/
|
|
405
|
+
function executeMigration(rootPath, apps, actions) {
|
|
406
|
+
const results = [];
|
|
407
|
+
|
|
408
|
+
for (const action of actions) {
|
|
409
|
+
results.push(executeAppMigration(rootPath, action));
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// After migration, regenerate scoped inventories for all apps
|
|
413
|
+
const regeneration = regenerateScopedInventories(rootPath, apps);
|
|
414
|
+
|
|
415
|
+
return { results, regeneration };
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// ---------------------------------------------------------------------------
|
|
419
|
+
// Formatting helpers (for command output)
|
|
420
|
+
// ---------------------------------------------------------------------------
|
|
421
|
+
|
|
422
|
+
// @gsd-api formatAuditReport(audit) -- returns human-readable string summarizing the migration audit
|
|
423
|
+
/**
|
|
424
|
+
* Format the audit into a human-readable report for display.
|
|
425
|
+
*
|
|
426
|
+
* @param {MigrationAudit} audit
|
|
427
|
+
* @returns {string}
|
|
428
|
+
*/
|
|
429
|
+
function formatAuditReport(audit) {
|
|
430
|
+
const lines = [];
|
|
431
|
+
lines.push('## Migration Audit\n');
|
|
432
|
+
lines.push(`Apps with existing .planning/: ${audit.appsWithPlanning}`);
|
|
433
|
+
lines.push(`Apps without .planning/: ${audit.appsWithoutPlanning}\n`);
|
|
434
|
+
|
|
435
|
+
for (const entry of audit.apps) {
|
|
436
|
+
if (entry.exists) {
|
|
437
|
+
lines.push(`### ${entry.appPath}`);
|
|
438
|
+
lines.push(` Files: ${entry.files.join(', ') || '(none)'}`);
|
|
439
|
+
lines.push(` Directories: ${entry.directories.join(', ') || '(none)'}`);
|
|
440
|
+
lines.push(` Has CODE-INVENTORY: ${entry.hasCodeInventory ? 'yes' : 'no'}`);
|
|
441
|
+
lines.push(` Has PRD: ${entry.hasPrd ? 'yes' : 'no'}`);
|
|
442
|
+
lines.push('');
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
if (audit.rootAnalysis.globalFiles.length > 0 || audit.rootAnalysis.appSpecificFiles.length > 0 || audit.rootAnalysis.ambiguousFiles.length > 0) {
|
|
447
|
+
lines.push('### Root .planning/ Analysis\n');
|
|
448
|
+
if (audit.rootAnalysis.globalFiles.length > 0) {
|
|
449
|
+
lines.push(`Global (keep at root): ${audit.rootAnalysis.globalFiles.join(', ')}`);
|
|
450
|
+
}
|
|
451
|
+
if (audit.rootAnalysis.appSpecificFiles.length > 0) {
|
|
452
|
+
lines.push(`App-specific (move to app): ${audit.rootAnalysis.appSpecificFiles.join(', ')}`);
|
|
453
|
+
}
|
|
454
|
+
if (audit.rootAnalysis.ambiguousFiles.length > 0) {
|
|
455
|
+
lines.push(`Needs review: ${audit.rootAnalysis.ambiguousFiles.join(', ')}`);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
return lines.join('\n');
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// ---------------------------------------------------------------------------
|
|
463
|
+
// Utility helpers
|
|
464
|
+
// ---------------------------------------------------------------------------
|
|
465
|
+
|
|
466
|
+
function safeReaddir(dirPath) {
|
|
467
|
+
try {
|
|
468
|
+
return fs.readdirSync(dirPath);
|
|
469
|
+
} catch {
|
|
470
|
+
return [];
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function safeStat(filePath, check) {
|
|
475
|
+
try {
|
|
476
|
+
const stat = fs.statSync(filePath);
|
|
477
|
+
return stat[check]();
|
|
478
|
+
} catch {
|
|
479
|
+
return false;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function safeReadFile(filePath) {
|
|
484
|
+
try {
|
|
485
|
+
return fs.readFileSync(filePath, 'utf-8');
|
|
486
|
+
} catch {
|
|
487
|
+
return null;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// ---------------------------------------------------------------------------
|
|
492
|
+
// Exports
|
|
493
|
+
// ---------------------------------------------------------------------------
|
|
494
|
+
|
|
495
|
+
module.exports = {
|
|
496
|
+
auditAppPlanning,
|
|
497
|
+
analyzeRootPlanning,
|
|
498
|
+
executeAppMigration,
|
|
499
|
+
archiveAppPlanning,
|
|
500
|
+
replaceAppPlanning,
|
|
501
|
+
regenerateScopedInventories,
|
|
502
|
+
executeMigration,
|
|
503
|
+
formatAuditReport,
|
|
504
|
+
looksAppSpecific,
|
|
505
|
+
KNOWN_PLANNING_FILES,
|
|
506
|
+
KNOWN_PLANNING_DIRS,
|
|
507
|
+
};
|