@ktpartners/dgs-platform 2.7.4 → 2.8.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/CHANGELOG.md +25 -0
- package/agents/dgs-executor.md +0 -52
- package/deliver-great-systems/bin/dgs-tools.cjs +66 -10
- package/deliver-great-systems/bin/lib/commands.cjs +1 -8
- package/deliver-great-systems/bin/lib/config.cjs +9 -90
- package/deliver-great-systems/bin/lib/context.cjs +2 -2
- package/deliver-great-systems/bin/lib/context.test.cjs +100 -100
- package/deliver-great-systems/bin/lib/core.cjs +17 -57
- package/deliver-great-systems/bin/lib/core.test.cjs +166 -170
- package/deliver-great-systems/bin/lib/docs.cjs +3 -3
- package/deliver-great-systems/bin/lib/docs.test.cjs +14 -7
- package/deliver-great-systems/bin/lib/execution.cjs +2 -2
- package/deliver-great-systems/bin/lib/execution.test.cjs +65 -67
- package/deliver-great-systems/bin/lib/ideas.cjs +4 -4
- package/deliver-great-systems/bin/lib/ideas.test.cjs +45 -44
- package/deliver-great-systems/bin/lib/init.cjs +9 -4
- package/deliver-great-systems/bin/lib/init.test.cjs +242 -175
- package/deliver-great-systems/bin/lib/jobs.cjs +1 -1
- package/deliver-great-systems/bin/lib/jobs.test.cjs +203 -202
- package/deliver-great-systems/bin/lib/migration.cjs +256 -281
- package/deliver-great-systems/bin/lib/migration.test.cjs +385 -440
- package/deliver-great-systems/bin/lib/milestone.cjs +1 -1
- package/deliver-great-systems/bin/lib/overlap.cjs +4 -4
- package/deliver-great-systems/bin/lib/overlap.test.cjs +45 -44
- package/deliver-great-systems/bin/lib/path-audit.test.cjs +16 -22
- package/deliver-great-systems/bin/lib/paths.cjs +60 -59
- package/deliver-great-systems/bin/lib/paths.test.cjs +192 -225
- package/deliver-great-systems/bin/lib/phase.cjs +5 -4
- package/deliver-great-systems/bin/lib/projects.cjs +8 -8
- package/deliver-great-systems/bin/lib/projects.test.cjs +75 -74
- package/deliver-great-systems/bin/lib/repos.cjs +94 -230
- package/deliver-great-systems/bin/lib/repos.test.cjs +84 -75
- package/deliver-great-systems/bin/lib/search.cjs +4 -4
- package/deliver-great-systems/bin/lib/specs.cjs +2 -2
- package/deliver-great-systems/bin/lib/sync.cjs +1 -1
- package/deliver-great-systems/bin/lib/template.cjs +3 -3
- package/deliver-great-systems/bin/lib/test-helpers.cjs +59 -162
- package/deliver-great-systems/bin/lib/verify.cjs +3 -3
- package/deliver-great-systems/references/planning-config.md +7 -8
- package/deliver-great-systems/workflows/add-tests.md +1 -1
- package/deliver-great-systems/workflows/approve-spec.md +1 -11
- package/deliver-great-systems/workflows/complete-milestone.md +2 -2
- package/deliver-great-systems/workflows/consolidate-ideas.md +1 -1
- package/deliver-great-systems/workflows/create-milestone-job.md +2 -2
- package/deliver-great-systems/workflows/discuss-phase.md +2 -2
- package/deliver-great-systems/workflows/execute-phase.md +63 -4
- package/deliver-great-systems/workflows/execute-plan.md +0 -51
- package/deliver-great-systems/workflows/find-related-ideas.md +1 -1
- package/deliver-great-systems/workflows/help.md +25 -58
- package/deliver-great-systems/workflows/init-product.md +14 -451
- package/deliver-great-systems/workflows/map-codebase.md +109 -0
- package/deliver-great-systems/workflows/new-project.md +0 -1
- package/deliver-great-systems/workflows/quick.md +6 -7
- package/deliver-great-systems/workflows/run-job.md +56 -0
- package/deliver-great-systems/workflows/settings.md +30 -0
- package/package.json +5 -1
|
@@ -1,352 +1,327 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Migration
|
|
2
|
+
* Migration -- .planning/ to root layout migration engine
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
4
|
+
* Provides migrateDotPlanningToRoot() which moves all planning files from
|
|
5
|
+
* .planning/ to repo root. Handles conflict detection (abort on different
|
|
6
|
+
* content, resolve identical content), partial migration, .gitignore
|
|
7
|
+
* rewriting, config.local.json cleanup, dgs.config.json rename, git mv /
|
|
8
|
+
* fs.rename duality, atomic commit, rollback on failure, and dry-run
|
|
9
|
+
* data collection.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
const fs = require('fs');
|
|
13
13
|
const path = require('path');
|
|
14
|
-
const {
|
|
15
|
-
const { writeReposMd } = require('./repos.cjs');
|
|
16
|
-
const { regenerateProjectsMd } = require('./projects.cjs');
|
|
17
|
-
const { writeConfigField } = require('./config.cjs');
|
|
18
|
-
const { getPlanningRoot, PROJECTS_DIR } = require('./paths.cjs');
|
|
14
|
+
const { execGit, safeReadFile, error } = require('./core.cjs');
|
|
19
15
|
|
|
20
|
-
// ───
|
|
16
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
21
17
|
|
|
22
18
|
/**
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* v1 marker: .planning/PROJECT.md exists at root WITHOUT PROJECTS.md or REPOS.md.
|
|
26
|
-
* If v1 detected, extracts project name from heading and generates slug.
|
|
19
|
+
* Recursively collects all file paths under `dir`, returning paths
|
|
20
|
+
* relative to `base`. Skips .git directories.
|
|
27
21
|
*
|
|
28
|
-
* @param {string}
|
|
29
|
-
* @
|
|
22
|
+
* @param {string} dir - Directory to walk
|
|
23
|
+
* @param {string} base - Base directory for relative path calculation
|
|
24
|
+
* @returns {string[]} Array of relative file paths
|
|
30
25
|
*/
|
|
31
|
-
function
|
|
32
|
-
|
|
33
|
-
const
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// Check v2 markers — if either exists, it's already v2 (or partially migrated)
|
|
44
|
-
const projectsMdContent = safeReadFile(projectsMdPath);
|
|
45
|
-
if (projectsMdContent && projectsMdContent.startsWith('# Projects')) {
|
|
46
|
-
return { isV1: false };
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const reposMdContent = safeReadFile(reposMdPath);
|
|
50
|
-
if (reposMdContent && reposMdContent.startsWith('# Repos')) {
|
|
51
|
-
return { isV1: false };
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// It's v1 — extract project name from heading
|
|
55
|
-
let projectName = null;
|
|
56
|
-
|
|
57
|
-
// Try "# Project: Name" format first
|
|
58
|
-
const colonMatch = projectContent.match(/^#\s+Project:\s+(.+)$/m);
|
|
59
|
-
if (colonMatch) {
|
|
60
|
-
projectName = colonMatch[1].trim();
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// Try "# Project Name" format (no colon)
|
|
64
|
-
if (!projectName) {
|
|
65
|
-
const headingMatch = projectContent.match(/^#\s+(.+)$/m);
|
|
66
|
-
if (headingMatch) {
|
|
67
|
-
projectName = headingMatch[1].trim();
|
|
26
|
+
function collectFiles(dir, base) {
|
|
27
|
+
const results = [];
|
|
28
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
29
|
+
for (const entry of entries) {
|
|
30
|
+
if (entry.name === '.git') continue;
|
|
31
|
+
const fullPath = path.join(dir, entry.name);
|
|
32
|
+
if (entry.isDirectory()) {
|
|
33
|
+
results.push(...collectFiles(fullPath, base));
|
|
34
|
+
} else {
|
|
35
|
+
results.push(path.relative(base, fullPath));
|
|
68
36
|
}
|
|
69
37
|
}
|
|
70
|
-
|
|
71
|
-
// Fall back to directory name
|
|
72
|
-
if (!projectName) {
|
|
73
|
-
projectName = path.basename(cwd);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const slug = generateSlugInternal(projectName) || path.basename(cwd).toLowerCase().replace(/[^a-z0-9]+/g, '-');
|
|
77
|
-
|
|
78
|
-
return { isV1: true, projectName, slug };
|
|
38
|
+
return results;
|
|
79
39
|
}
|
|
80
40
|
|
|
81
|
-
// ─── Precondition Validation ─────────────────────────────────────────────────
|
|
82
|
-
|
|
83
41
|
/**
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
* Checks:
|
|
87
|
-
* 1. Not already a v2 install
|
|
88
|
-
* 2. .planning/PROJECT.md exists (v1 marker)
|
|
89
|
-
* 3. Git working tree is clean (no uncommitted changes)
|
|
42
|
+
* Returns true if `cwd` is inside a git working tree.
|
|
90
43
|
*
|
|
91
44
|
* @param {string} cwd - Working directory
|
|
92
|
-
* @returns {
|
|
45
|
+
* @returns {boolean}
|
|
93
46
|
*/
|
|
94
|
-
function
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
return { valid: false, reason: 'Already a v2 install. Migration not needed.' };
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// Check if v1 marker exists — v1 always uses .planning/
|
|
101
|
-
const projectMdPath = path.join(cwd, '.planning', 'PROJECT.md');
|
|
102
|
-
if (!safeReadFile(projectMdPath)) {
|
|
103
|
-
return { valid: false, reason: 'No .planning/PROJECT.md found. Not a v1 install.' };
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// Check git working tree is clean
|
|
107
|
-
const gitStatus = execGit(cwd, ['status', '--porcelain']);
|
|
108
|
-
if (gitStatus.exitCode !== 0) {
|
|
109
|
-
return { valid: false, reason: 'Not a git repository or git command failed.' };
|
|
110
|
-
}
|
|
111
|
-
if (gitStatus.stdout.trim().length > 0) {
|
|
112
|
-
return { valid: false, reason: 'Working tree has uncommitted changes. Please commit or stash before migration.' };
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
return { valid: true };
|
|
47
|
+
function isGitRepo(cwd) {
|
|
48
|
+
const result = execGit(cwd, ['rev-parse', '--is-inside-work-tree']);
|
|
49
|
+
return result.exitCode === 0;
|
|
116
50
|
}
|
|
117
51
|
|
|
118
|
-
// ─── Backup Tag ──────────────────────────────────────────────────────────────
|
|
119
|
-
|
|
120
52
|
/**
|
|
121
|
-
*
|
|
53
|
+
* Returns true if the git working tree at `cwd` has no uncommitted changes.
|
|
122
54
|
*
|
|
123
|
-
* @param {string} cwd - Working directory
|
|
124
|
-
* @returns {
|
|
55
|
+
* @param {string} cwd - Working directory (should be repo root)
|
|
56
|
+
* @returns {boolean}
|
|
125
57
|
*/
|
|
126
|
-
function
|
|
127
|
-
const result = execGit(cwd, ['
|
|
128
|
-
|
|
129
|
-
// Check if tag already exists or no git
|
|
130
|
-
if (result.stderr.includes('already exists')) {
|
|
131
|
-
return { tagged: false, reason: 'Tag dgs-pre-v2-migration already exists.' };
|
|
132
|
-
}
|
|
133
|
-
return { tagged: false, reason: result.stderr || 'Failed to create tag.' };
|
|
134
|
-
}
|
|
135
|
-
return { tagged: true };
|
|
58
|
+
function isCleanWorkingTree(cwd) {
|
|
59
|
+
const result = execGit(cwd, ['status', '--porcelain']);
|
|
60
|
+
return result.stdout === '';
|
|
136
61
|
}
|
|
137
62
|
|
|
138
|
-
// ─── Migration
|
|
139
|
-
|
|
140
|
-
// Directories to move (project-level, order: directories first)
|
|
141
|
-
const DIRS_TO_MOVE = ['phases', 'research', 'quick', 'debug'];
|
|
142
|
-
|
|
143
|
-
// Files to move (project-level)
|
|
144
|
-
const FILES_TO_MOVE = ['PROJECT.md', 'REQUIREMENTS.md', 'ROADMAP.md', 'STATE.md'];
|
|
63
|
+
// ─── Main Migration Function ──────────────────────────────────────────────────
|
|
145
64
|
|
|
146
65
|
/**
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
* Returns an array of { relSource, relTarget, isDir } for all items that exist
|
|
150
|
-
* and need moving. Directories are listed before files to maximize git rename detection.
|
|
66
|
+
* Migrates all planning files from .planning/ to repo root.
|
|
151
67
|
*
|
|
152
|
-
* @param {string} cwd - Working directory
|
|
153
|
-
* @param {
|
|
154
|
-
* @
|
|
68
|
+
* @param {string} [cwd] - Working directory (defaults to process.cwd())
|
|
69
|
+
* @param {Object} [options] - Migration options
|
|
70
|
+
* @param {boolean} [options.dryRun=false] - If true, return actions without modifying files
|
|
71
|
+
* @returns {{
|
|
72
|
+
* migrated: boolean,
|
|
73
|
+
* filesMoved: number,
|
|
74
|
+
* identicalResolved: number,
|
|
75
|
+
* commitHash: string|null,
|
|
76
|
+
* dryRun: boolean,
|
|
77
|
+
* actions: Array<{type: string, from: string, to: string}>
|
|
78
|
+
* }}
|
|
155
79
|
*/
|
|
156
|
-
function
|
|
157
|
-
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
80
|
+
function migrateDotPlanningToRoot(cwd, options) {
|
|
81
|
+
const opts = options || {};
|
|
82
|
+
const dryRun = opts.dryRun || false;
|
|
83
|
+
|
|
84
|
+
// 1. Resolve paths
|
|
85
|
+
const inGit = isGitRepo(cwd || process.cwd());
|
|
86
|
+
let root;
|
|
87
|
+
if (inGit) {
|
|
88
|
+
root = execGit(cwd || process.cwd(), ['rev-parse', '--show-toplevel']).stdout;
|
|
89
|
+
} else {
|
|
90
|
+
root = path.resolve(cwd || process.cwd());
|
|
91
|
+
}
|
|
161
92
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
93
|
+
const dotPlanning = path.join(root, '.planning');
|
|
94
|
+
|
|
95
|
+
// 2. Idempotency check (MIG-07)
|
|
96
|
+
if (!fs.existsSync(dotPlanning) || !fs.statSync(dotPlanning).isDirectory()) {
|
|
97
|
+
return {
|
|
98
|
+
migrated: false,
|
|
99
|
+
filesMoved: 0,
|
|
100
|
+
identicalResolved: 0,
|
|
101
|
+
commitHash: null,
|
|
102
|
+
dryRun: dryRun,
|
|
103
|
+
actions: [],
|
|
104
|
+
};
|
|
172
105
|
}
|
|
173
106
|
|
|
174
|
-
//
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
if (fs.existsSync(sourcePath)) {
|
|
178
|
-
moves.push({
|
|
179
|
-
relSource: path.join('.planning', file),
|
|
180
|
-
relTarget: path.join('.planning', PROJECTS_DIR, slug, file),
|
|
181
|
-
isDir: false,
|
|
182
|
-
});
|
|
183
|
-
}
|
|
107
|
+
// 3. Clean working tree check (git only)
|
|
108
|
+
if (inGit && !isCleanWorkingTree(root)) {
|
|
109
|
+
error('Migration requires a clean working tree. Commit or stash your changes first, then re-run.');
|
|
184
110
|
}
|
|
185
111
|
|
|
186
|
-
|
|
187
|
-
|
|
112
|
+
// 4. Collect files in .planning/
|
|
113
|
+
const planningFiles = collectFiles(dotPlanning, dotPlanning);
|
|
188
114
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
* - Each source exists on disk
|
|
194
|
-
* - Each target does NOT already exist (would cause git mv to fail)
|
|
195
|
-
*
|
|
196
|
-
* @param {string} cwd - Working directory
|
|
197
|
-
* @param {Array<{relSource: string, relTarget: string, isDir: boolean}>} moves - Planned moves
|
|
198
|
-
* @returns {{ valid: boolean, errors: string[] }}
|
|
199
|
-
*/
|
|
200
|
-
function validateMoves(cwd, moves) {
|
|
201
|
-
const errors = [];
|
|
115
|
+
// 5. Pre-flight conflict scan (MIG-02, MIG-03, MIG-04)
|
|
116
|
+
const moves = [];
|
|
117
|
+
const identical = [];
|
|
118
|
+
const conflicts = [];
|
|
202
119
|
|
|
203
|
-
for (const
|
|
204
|
-
const
|
|
205
|
-
if (!fs.existsSync(absSource)) {
|
|
206
|
-
errors.push('Source does not exist: ' + move.relSource);
|
|
207
|
-
}
|
|
120
|
+
for (const relPath of planningFiles) {
|
|
121
|
+
const srcPath = path.join(dotPlanning, relPath);
|
|
208
122
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
123
|
+
// 6. Handle dgs.config.json rename (MIG-10)
|
|
124
|
+
let destRelPath = relPath;
|
|
125
|
+
if (relPath === 'dgs.config.json') {
|
|
126
|
+
destRelPath = 'config.json';
|
|
212
127
|
}
|
|
213
|
-
}
|
|
214
128
|
|
|
215
|
-
|
|
216
|
-
}
|
|
129
|
+
const destPath = path.join(root, destRelPath);
|
|
217
130
|
|
|
218
|
-
|
|
131
|
+
if (fs.existsSync(destPath)) {
|
|
132
|
+
const srcContent = fs.readFileSync(srcPath, 'utf-8');
|
|
133
|
+
const destContent = fs.readFileSync(destPath, 'utf-8');
|
|
134
|
+
if (srcContent === destContent) {
|
|
135
|
+
identical.push({ relPath, destRelPath, srcPath, destPath });
|
|
136
|
+
} else {
|
|
137
|
+
conflicts.push({ relPath, destRelPath, srcPath, destPath });
|
|
138
|
+
}
|
|
139
|
+
} else {
|
|
140
|
+
moves.push({ relPath, destRelPath, srcPath, destPath });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
219
143
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
144
|
+
// Also check if dgs.config.json exists at root (not in .planning) and needs rename
|
|
145
|
+
const rootDgsConfig = path.join(root, 'dgs.config.json');
|
|
146
|
+
const rootConfig = path.join(root, 'config.json');
|
|
147
|
+
if (fs.existsSync(rootDgsConfig) && !fs.existsSync(rootConfig)) {
|
|
148
|
+
// Only if not already handled via .planning/dgs.config.json
|
|
149
|
+
const alreadyHandled = moves.some(m => m.relPath === 'dgs.config.json') ||
|
|
150
|
+
identical.some(m => m.relPath === 'dgs.config.json') ||
|
|
151
|
+
conflicts.some(m => m.relPath === 'dgs.config.json');
|
|
152
|
+
if (!alreadyHandled) {
|
|
153
|
+
moves.push({
|
|
154
|
+
relPath: 'dgs.config.json',
|
|
155
|
+
destRelPath: 'config.json',
|
|
156
|
+
srcPath: rootDgsConfig,
|
|
157
|
+
destPath: rootConfig,
|
|
158
|
+
isRootRename: true,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
237
162
|
|
|
238
|
-
//
|
|
239
|
-
|
|
163
|
+
// 7. Abort on conflicts (MIG-02)
|
|
164
|
+
if (conflicts.length > 0) {
|
|
165
|
+
const fileList = conflicts.map(c => ' - ' + c.relPath + ' (.planning/' + c.relPath + ' vs ./' + c.destRelPath + ')').join('\n');
|
|
166
|
+
error(
|
|
167
|
+
'Migration aborted: ' + conflicts.length + ' file(s) have different content in .planning/ and root.\n' +
|
|
168
|
+
'Resolve these conflicts manually, then re-run:\n' +
|
|
169
|
+
fileList
|
|
170
|
+
);
|
|
171
|
+
}
|
|
240
172
|
|
|
241
|
-
//
|
|
242
|
-
|
|
173
|
+
// 8. Build actions array
|
|
174
|
+
const actions = [];
|
|
175
|
+
for (const m of moves) {
|
|
176
|
+
actions.push({ type: 'move', from: m.isRootRename ? 'dgs.config.json' : '.planning/' + m.relPath, to: m.destRelPath });
|
|
177
|
+
}
|
|
178
|
+
for (const i of identical) {
|
|
179
|
+
actions.push({ type: 'identical', from: '.planning/' + i.relPath, to: i.destRelPath });
|
|
180
|
+
}
|
|
243
181
|
|
|
244
|
-
//
|
|
245
|
-
|
|
246
|
-
if (!validation.valid) {
|
|
247
|
-
// Clean up created target directory
|
|
248
|
-
try { fs.rmdirSync(targetDir); } catch (_e) { /* ignore */ }
|
|
182
|
+
// 9. Dry-run exit (OPT-02)
|
|
183
|
+
if (dryRun) {
|
|
249
184
|
return {
|
|
250
185
|
migrated: false,
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
186
|
+
filesMoved: moves.length,
|
|
187
|
+
identicalResolved: identical.length,
|
|
188
|
+
commitHash: null,
|
|
189
|
+
dryRun: true,
|
|
190
|
+
actions,
|
|
256
191
|
};
|
|
257
192
|
}
|
|
258
193
|
|
|
259
|
-
//
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
194
|
+
// 10. Execute file moves
|
|
195
|
+
for (const m of moves) {
|
|
196
|
+
if (m.isRootRename) {
|
|
197
|
+
// Root-level dgs.config.json -> config.json rename
|
|
198
|
+
if (inGit) {
|
|
199
|
+
execGit(root, ['mv', 'dgs.config.json', 'config.json']);
|
|
200
|
+
} else {
|
|
201
|
+
fs.renameSync(m.srcPath, m.destPath);
|
|
202
|
+
}
|
|
203
|
+
} else {
|
|
204
|
+
// .planning/ file -> root
|
|
205
|
+
fs.mkdirSync(path.dirname(m.destPath), { recursive: true });
|
|
206
|
+
if (inGit) {
|
|
207
|
+
execGit(root, ['-c', 'diff.renameLimit=1000', 'mv', '.planning/' + m.relPath, m.destRelPath]);
|
|
208
|
+
} else {
|
|
209
|
+
fs.renameSync(m.srcPath, m.destPath);
|
|
267
210
|
}
|
|
268
|
-
filesMoved.push(move.isDir ? path.basename(move.relSource) + '/' : path.basename(move.relSource));
|
|
269
211
|
}
|
|
270
|
-
} catch (err) {
|
|
271
|
-
// Rollback: restore all files to pre-move state
|
|
272
|
-
execGit(cwd, ['checkout', 'HEAD', '--', '.']);
|
|
273
|
-
execGit(cwd, ['clean', '-fd', path.join('.planning', PROJECTS_DIR, slug)]);
|
|
274
|
-
return {
|
|
275
|
-
migrated: false,
|
|
276
|
-
slug,
|
|
277
|
-
files_moved: [],
|
|
278
|
-
repos_created: false,
|
|
279
|
-
project_created: false,
|
|
280
|
-
error: 'Migration failed: ' + err.message,
|
|
281
|
-
rollback: true,
|
|
282
|
-
};
|
|
283
212
|
}
|
|
284
213
|
|
|
285
|
-
//
|
|
286
|
-
|
|
287
|
-
|
|
214
|
+
// 11. Resolve identical conflicts (MIG-03)
|
|
215
|
+
for (const i of identical) {
|
|
216
|
+
if (inGit) {
|
|
217
|
+
execGit(root, ['rm', '-f', '.planning/' + i.relPath]);
|
|
218
|
+
} else {
|
|
219
|
+
fs.unlinkSync(i.srcPath);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
288
222
|
|
|
289
|
-
//
|
|
290
|
-
const
|
|
291
|
-
|
|
223
|
+
// 12. Rewrite .gitignore (MIG-05)
|
|
224
|
+
const gitignorePath = path.join(root, '.gitignore');
|
|
225
|
+
if (fs.existsSync(gitignorePath)) {
|
|
226
|
+
const content = fs.readFileSync(gitignorePath, 'utf-8');
|
|
227
|
+
const rewritten = content
|
|
228
|
+
.split('\n')
|
|
229
|
+
.map(line => {
|
|
230
|
+
// Handle negation patterns: !.planning/X -> !X
|
|
231
|
+
if (line.startsWith('!.planning/')) {
|
|
232
|
+
return '!' + line.slice('!.planning/'.length);
|
|
233
|
+
}
|
|
234
|
+
// Handle normal patterns: .planning/X -> X
|
|
235
|
+
if (line.startsWith('.planning/')) {
|
|
236
|
+
return line.slice('.planning/'.length);
|
|
237
|
+
}
|
|
238
|
+
return line;
|
|
239
|
+
})
|
|
240
|
+
.join('\n');
|
|
241
|
+
fs.writeFileSync(gitignorePath, rewritten);
|
|
242
|
+
}
|
|
292
243
|
|
|
293
|
-
//
|
|
294
|
-
|
|
244
|
+
// 13. Clean config.local.json (MIG-06)
|
|
245
|
+
const configLocalPath = path.join(root, 'config.local.json');
|
|
246
|
+
if (fs.existsSync(configLocalPath)) {
|
|
247
|
+
try {
|
|
248
|
+
const configContent = fs.readFileSync(configLocalPath, 'utf-8');
|
|
249
|
+
const configObj = JSON.parse(configContent);
|
|
250
|
+
if ('planningRoot' in configObj) {
|
|
251
|
+
delete configObj.planningRoot;
|
|
252
|
+
fs.writeFileSync(configLocalPath, JSON.stringify(configObj, null, 2) + '\n');
|
|
253
|
+
}
|
|
254
|
+
} catch {
|
|
255
|
+
// If config.local.json is not valid JSON, skip cleanup
|
|
256
|
+
}
|
|
257
|
+
}
|
|
295
258
|
|
|
296
|
-
//
|
|
297
|
-
|
|
259
|
+
// 14. Remove empty .planning/ directory
|
|
260
|
+
if (fs.existsSync(dotPlanning)) {
|
|
261
|
+
fs.rmSync(dotPlanning, { recursive: true, force: true });
|
|
262
|
+
}
|
|
298
263
|
|
|
299
|
-
//
|
|
300
|
-
|
|
301
|
-
|
|
264
|
+
// 15. Git commit (MIG-11)
|
|
265
|
+
let commitHash = null;
|
|
266
|
+
if (inGit) {
|
|
267
|
+
execGit(root, ['add', '-A']);
|
|
268
|
+
const commitResult = execGit(root, ['commit', '-m', 'chore: migrate .planning/ to root layout']);
|
|
269
|
+
if (commitResult.exitCode !== 0) {
|
|
270
|
+
// Rollback (OPT-03): restore from git on commit failure
|
|
271
|
+
execGit(root, ['checkout', 'HEAD', '--', '.']);
|
|
272
|
+
error('Migration commit failed. Original .planning/ layout restored. Error: ' + commitResult.stderr);
|
|
273
|
+
}
|
|
274
|
+
const hashResult = execGit(root, ['rev-parse', '--short', 'HEAD']);
|
|
275
|
+
commitHash = hashResult.stdout;
|
|
276
|
+
}
|
|
302
277
|
|
|
278
|
+
// 16. Return result
|
|
303
279
|
return {
|
|
304
280
|
migrated: true,
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
281
|
+
filesMoved: moves.length,
|
|
282
|
+
identicalResolved: identical.length,
|
|
283
|
+
commitHash: inGit ? commitHash : null,
|
|
284
|
+
dryRun: false,
|
|
285
|
+
actions,
|
|
309
286
|
};
|
|
310
287
|
}
|
|
311
288
|
|
|
312
|
-
// ───
|
|
289
|
+
// ─── V1 Rejection ────────────────────────────────────────────────────────────
|
|
313
290
|
|
|
314
291
|
/**
|
|
315
|
-
*
|
|
316
|
-
*
|
|
292
|
+
* Reject V1 single-repo layout installations.
|
|
293
|
+
* Detects .planning/PROJECT.md without PROJECTS.md or REPOS.md markers.
|
|
294
|
+
* Writes a persistent V1_UNSUPPORTED.md guidance file and exits with code 1.
|
|
317
295
|
*
|
|
318
|
-
*
|
|
319
|
-
*
|
|
320
|
-
* @param {
|
|
296
|
+
* Called from dgs-tools.cjs router -- single call site at CLI entry.
|
|
297
|
+
*
|
|
298
|
+
* @param {string} [cwd] - Working directory (defaults to process.cwd())
|
|
321
299
|
*/
|
|
322
|
-
function
|
|
323
|
-
|
|
324
|
-
const
|
|
325
|
-
|
|
326
|
-
|
|
300
|
+
function rejectV1Install(cwd) {
|
|
301
|
+
const resolved = path.resolve(cwd || process.cwd());
|
|
302
|
+
const dotPlanning = path.join(resolved, '.planning');
|
|
303
|
+
const v1Marker = path.join(dotPlanning, 'PROJECT.md');
|
|
304
|
+
if (!fs.existsSync(v1Marker)) return; // No V1 marker, nothing to do
|
|
305
|
+
const hasProjectsMd = fs.existsSync(path.join(resolved, 'PROJECTS.md')) ||
|
|
306
|
+
fs.existsSync(path.join(dotPlanning, 'PROJECTS.md'));
|
|
307
|
+
const hasReposMd = fs.existsSync(path.join(resolved, 'REPOS.md')) ||
|
|
308
|
+
fs.existsSync(path.join(dotPlanning, 'REPOS.md'));
|
|
309
|
+
if (!hasProjectsMd && !hasReposMd) {
|
|
310
|
+
const guidancePath = path.join(resolved, 'V1_UNSUPPORTED.md');
|
|
311
|
+
const guidanceContent = [
|
|
312
|
+
'# V1 Install Detected', '',
|
|
313
|
+
'This repository has a DGS V1 (single-repo) layout which is no longer supported.', '',
|
|
314
|
+
'DGS now requires the V2 multi-project layout. To continue using DGS,',
|
|
315
|
+
'install an older version that supports V1, or set up a fresh V2 install',
|
|
316
|
+
'with `/dgs:init-product`.', '',
|
|
317
|
+
'See the DGS changelog for migration details.', '',
|
|
318
|
+
].join('\n');
|
|
319
|
+
try { fs.writeFileSync(guidancePath, guidanceContent); } catch { /* best effort */ }
|
|
320
|
+
process.stderr.write('Error: V1 single-repo layout is no longer supported. Install an older DGS version or run /dgs:init-product for a fresh V2 setup. See V1_UNSUPPORTED.md for details.\n');
|
|
321
|
+
process.exit(1);
|
|
327
322
|
}
|
|
328
|
-
|
|
329
|
-
// Create backup tag
|
|
330
|
-
const tag = createBackupTag(cwd);
|
|
331
|
-
|
|
332
|
-
// Perform migration
|
|
333
|
-
const result = migrateV1ToV2(cwd, slug);
|
|
334
|
-
|
|
335
|
-
output({
|
|
336
|
-
...result,
|
|
337
|
-
backup_tag: tag.tagged ? 'dgs-pre-v2-migration' : null,
|
|
338
|
-
backup_tag_note: tag.tagged ? null : tag.reason,
|
|
339
|
-
}, raw);
|
|
340
323
|
}
|
|
341
324
|
|
|
342
|
-
// ─── Exports
|
|
343
|
-
|
|
344
|
-
module.exports = {
|
|
345
|
-
detectV1Install,
|
|
346
|
-
validateMigrationPreconditions,
|
|
347
|
-
createBackupTag,
|
|
348
|
-
collectMigrationMoves,
|
|
349
|
-
validateMoves,
|
|
350
|
-
migrateV1ToV2,
|
|
351
|
-
cmdMigrateV1ToV2,
|
|
352
|
-
};
|
|
325
|
+
// ─── Exports ──────────────────────────────────────────────────────────────────
|
|
326
|
+
|
|
327
|
+
module.exports = { migrateDotPlanningToRoot, rejectV1Install };
|