@mindfoldhq/trellis 0.1.9 → 0.2.2
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/dist/cli/index.js +2 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +12 -6
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/update.d.ts +1 -0
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +684 -42
- package/dist/commands/update.js.map +1 -1
- package/dist/configurators/opencode.js +1 -1
- package/dist/configurators/opencode.js.map +1 -1
- package/dist/configurators/workflow.d.ts +4 -3
- package/dist/configurators/workflow.d.ts.map +1 -1
- package/dist/configurators/workflow.js +23 -20
- package/dist/configurators/workflow.js.map +1 -1
- package/dist/constants/paths.d.ts +29 -30
- package/dist/constants/paths.d.ts.map +1 -1
- package/dist/constants/paths.js +32 -35
- package/dist/constants/paths.js.map +1 -1
- package/dist/migrations/index.d.ts +35 -0
- package/dist/migrations/index.d.ts.map +1 -0
- package/dist/migrations/index.js +124 -0
- package/dist/migrations/index.js.map +1 -0
- package/dist/migrations/manifests/0.1.9.json +30 -0
- package/dist/migrations/manifests/0.2.0.json +43 -0
- package/dist/templates/claude/agents/check.md +3 -3
- package/dist/templates/claude/agents/debug.md +1 -1
- package/dist/templates/claude/agents/dispatch.md +12 -12
- package/dist/templates/claude/agents/implement.md +6 -6
- package/dist/templates/claude/agents/plan.md +37 -37
- package/dist/templates/claude/agents/research.md +1 -1
- package/dist/templates/claude/commands/before-backend-dev.md +5 -5
- package/dist/templates/claude/commands/before-frontend-dev.md +5 -5
- package/dist/templates/claude/commands/break-loop.md +2 -2
- package/dist/templates/claude/commands/check-backend.md +6 -6
- package/dist/templates/claude/commands/check-cross-layer.md +5 -5
- package/dist/templates/claude/commands/check-frontend.md +6 -6
- package/dist/templates/claude/commands/create-command.md +3 -3
- package/dist/templates/claude/commands/finish-work.md +6 -6
- package/dist/templates/claude/commands/integrate-skill.md +11 -11
- package/dist/templates/claude/commands/{onboard-developer.md → onboard.md} +31 -28
- package/dist/templates/claude/commands/parallel.md +17 -17
- package/dist/templates/claude/commands/{record-agent-flow.md → record-session.md} +7 -7
- package/dist/templates/claude/commands/start.md +36 -36
- package/dist/templates/claude/hooks/inject-subagent-context.py +77 -76
- package/dist/templates/claude/hooks/ralph-loop.py +18 -18
- package/dist/templates/claude/hooks/session-start.py +4 -4
- package/dist/templates/cursor/commands/before-backend-dev.md +5 -5
- package/dist/templates/cursor/commands/before-frontend-dev.md +5 -5
- package/dist/templates/cursor/commands/break-loop.md +2 -2
- package/dist/templates/cursor/commands/check-backend.md +6 -6
- package/dist/templates/cursor/commands/check-cross-layer.md +5 -5
- package/dist/templates/cursor/commands/check-frontend.md +6 -6
- package/dist/templates/cursor/commands/create-command.md +3 -3
- package/dist/templates/cursor/commands/finish-work.md +6 -6
- package/dist/templates/cursor/commands/integrate-skill.md +11 -11
- package/dist/templates/cursor/commands/{onboard-developer.md → onboard.md} +31 -28
- package/dist/templates/cursor/commands/{record-agent-flow.md → record-session.md} +7 -7
- package/dist/templates/cursor/commands/start.md +25 -25
- package/dist/templates/extract.d.ts +2 -2
- package/dist/templates/extract.js +2 -2
- package/dist/templates/markdown/agents.md +2 -2
- package/dist/templates/markdown/gitignore.txt +2 -2
- package/dist/templates/markdown/index.d.ts +1 -0
- package/dist/templates/markdown/index.d.ts.map +1 -1
- package/dist/templates/markdown/index.js +4 -2
- package/dist/templates/markdown/index.js.map +1 -1
- package/dist/templates/markdown/{agent-traces-index.md → workspace-index.md} +14 -14
- package/dist/templates/trellis/index.d.ts +7 -1
- package/dist/templates/trellis/index.d.ts.map +1 -1
- package/dist/templates/trellis/index.js +14 -2
- package/dist/templates/trellis/index.js.map +1 -1
- package/dist/templates/trellis/scripts/add-session.sh +26 -26
- package/dist/templates/trellis/scripts/common/developer.sh +20 -21
- package/dist/templates/trellis/scripts/common/git-context.sh +90 -115
- package/dist/templates/trellis/scripts/common/paths.sh +53 -63
- package/dist/templates/trellis/scripts/common/phase.sh +40 -40
- package/dist/templates/trellis/scripts/common/registry.sh +13 -13
- package/dist/templates/trellis/scripts/common/task-queue.sh +142 -0
- package/dist/templates/trellis/scripts/common/task-utils.sh +151 -0
- package/dist/templates/trellis/scripts/common/worktree.sh +3 -3
- package/dist/templates/trellis/scripts/create-bootstrap.sh +43 -42
- package/dist/templates/trellis/scripts/init-developer.sh +1 -1
- package/dist/templates/trellis/scripts/multi-agent/cleanup.sh +33 -33
- package/dist/templates/trellis/scripts/multi-agent/create-pr.sh +30 -30
- package/dist/templates/trellis/scripts/multi-agent/plan.sh +28 -28
- package/dist/templates/trellis/scripts/multi-agent/start.sh +56 -56
- package/dist/templates/trellis/scripts/multi-agent/status.sh +59 -59
- package/dist/templates/trellis/scripts/{feature.sh → task.sh} +235 -185
- package/dist/templates/trellis/workflow.md +71 -74
- package/dist/types/migration.d.ts +74 -0
- package/dist/types/migration.d.ts.map +1 -0
- package/dist/types/migration.js +8 -0
- package/dist/types/migration.js.map +1 -0
- package/dist/utils/template-hash.d.ts +78 -0
- package/dist/utils/template-hash.d.ts.map +1 -0
- package/dist/utils/template-hash.js +234 -0
- package/dist/utils/template-hash.js.map +1 -0
- package/package.json +1 -1
- package/dist/templates/trellis/scripts/common/backlog.sh +0 -220
- package/dist/templates/trellis/scripts/common/feature-utils.sh +0 -194
- /package/dist/templates/trellis/{backlog → tasks}/.gitkeep +0 -0
package/dist/commands/update.js
CHANGED
|
@@ -4,18 +4,21 @@ import chalk from "chalk";
|
|
|
4
4
|
import inquirer from "inquirer";
|
|
5
5
|
import { PATHS, DIR_NAMES } from "../constants/paths.js";
|
|
6
6
|
import { VERSION, PACKAGE_NAME } from "../cli/index.js";
|
|
7
|
+
import { getMigrationsForVersion } from "../migrations/index.js";
|
|
8
|
+
import { loadHashes, saveHashes, updateHashes, isTemplateModified, removeHash, renameHash, computeHash, } from "../utils/template-hash.js";
|
|
7
9
|
// Import templates for comparison
|
|
8
|
-
import { commonPathsScript, commonDeveloperScript, commonGitContextScript, commonWorktreeScript, multiAgentStartScript, multiAgentCleanupScript, multiAgentStatusScript, worktreeYamlTemplate, workflowMdTemplate, gitignoreTemplate, initDeveloperScript, getDeveloperScript,
|
|
10
|
+
import { commonPathsScript, commonDeveloperScript, commonGitContextScript, commonWorktreeScript, commonTaskQueueScript, commonTaskUtilsScript, commonPhaseScript, commonRegistryScript, multiAgentStartScript, multiAgentCleanupScript, multiAgentStatusScript, multiAgentCreatePrScript, multiAgentPlanScript, worktreeYamlTemplate, workflowMdTemplate, gitignoreTemplate, initDeveloperScript, getDeveloperScript, taskScript, getContextScript, addSessionScript, createBootstrapScript, } from "../templates/trellis/index.js";
|
|
9
11
|
import { guidesIndexContent, guidesCrossLayerThinkingGuideContent, guidesCodeReuseThinkingGuideContent, } from "../templates/markdown/index.js";
|
|
10
12
|
import { getCommandTemplates } from "../configurators/templates.js";
|
|
11
13
|
import { getAllAgents, getAllHooks, getSettingsTemplate, } from "../templates/claude/index.js";
|
|
12
14
|
// Paths that should never be touched
|
|
13
15
|
const PROTECTED_PATHS = [
|
|
14
|
-
`${DIR_NAMES.WORKFLOW}/${DIR_NAMES.
|
|
16
|
+
`${DIR_NAMES.WORKFLOW}/${DIR_NAMES.WORKSPACE}`, // workspace/
|
|
17
|
+
`${DIR_NAMES.WORKFLOW}/${DIR_NAMES.TASKS}`, // tasks/
|
|
15
18
|
`${DIR_NAMES.WORKFLOW}/.developer`,
|
|
16
|
-
`${DIR_NAMES.WORKFLOW}/.current-
|
|
17
|
-
`${PATHS.
|
|
18
|
-
`${PATHS.
|
|
19
|
+
`${DIR_NAMES.WORKFLOW}/.current-task`,
|
|
20
|
+
`${PATHS.SPEC}/frontend`,
|
|
21
|
+
`${PATHS.SPEC}/backend`,
|
|
19
22
|
];
|
|
20
23
|
/**
|
|
21
24
|
* Collect all template files that should be managed by update
|
|
@@ -27,14 +30,20 @@ function collectTemplateFiles(_cwd) {
|
|
|
27
30
|
files.set(`${PATHS.SCRIPTS}/common/developer.sh`, commonDeveloperScript);
|
|
28
31
|
files.set(`${PATHS.SCRIPTS}/common/git-context.sh`, commonGitContextScript);
|
|
29
32
|
files.set(`${PATHS.SCRIPTS}/common/worktree.sh`, commonWorktreeScript);
|
|
33
|
+
files.set(`${PATHS.SCRIPTS}/common/task-queue.sh`, commonTaskQueueScript);
|
|
34
|
+
files.set(`${PATHS.SCRIPTS}/common/task-utils.sh`, commonTaskUtilsScript);
|
|
35
|
+
files.set(`${PATHS.SCRIPTS}/common/phase.sh`, commonPhaseScript);
|
|
36
|
+
files.set(`${PATHS.SCRIPTS}/common/registry.sh`, commonRegistryScript);
|
|
30
37
|
// Scripts - multi-agent
|
|
31
38
|
files.set(`${PATHS.SCRIPTS}/multi-agent/start.sh`, multiAgentStartScript);
|
|
32
39
|
files.set(`${PATHS.SCRIPTS}/multi-agent/cleanup.sh`, multiAgentCleanupScript);
|
|
33
40
|
files.set(`${PATHS.SCRIPTS}/multi-agent/status.sh`, multiAgentStatusScript);
|
|
41
|
+
files.set(`${PATHS.SCRIPTS}/multi-agent/create-pr.sh`, multiAgentCreatePrScript);
|
|
42
|
+
files.set(`${PATHS.SCRIPTS}/multi-agent/plan.sh`, multiAgentPlanScript);
|
|
34
43
|
// Scripts - main
|
|
35
44
|
files.set(`${PATHS.SCRIPTS}/init-developer.sh`, initDeveloperScript);
|
|
36
45
|
files.set(`${PATHS.SCRIPTS}/get-developer.sh`, getDeveloperScript);
|
|
37
|
-
files.set(`${PATHS.SCRIPTS}/
|
|
46
|
+
files.set(`${PATHS.SCRIPTS}/task.sh`, taskScript);
|
|
38
47
|
files.set(`${PATHS.SCRIPTS}/get-context.sh`, getContextScript);
|
|
39
48
|
files.set(`${PATHS.SCRIPTS}/add-session.sh`, addSessionScript);
|
|
40
49
|
files.set(`${PATHS.SCRIPTS}/create-bootstrap.sh`, createBootstrapScript);
|
|
@@ -42,10 +51,10 @@ function collectTemplateFiles(_cwd) {
|
|
|
42
51
|
files.set(`${DIR_NAMES.WORKFLOW}/worktree.yaml`, worktreeYamlTemplate);
|
|
43
52
|
files.set(`${DIR_NAMES.WORKFLOW}/.gitignore`, gitignoreTemplate);
|
|
44
53
|
files.set(PATHS.WORKFLOW_GUIDE_FILE, workflowMdTemplate);
|
|
45
|
-
//
|
|
46
|
-
files.set(`${PATHS.
|
|
47
|
-
files.set(`${PATHS.
|
|
48
|
-
files.set(`${PATHS.
|
|
54
|
+
// Spec - guides only (frontend/backend are protected)
|
|
55
|
+
files.set(`${PATHS.SPEC}/guides/index.md`, guidesIndexContent);
|
|
56
|
+
files.set(`${PATHS.SPEC}/guides/cross-layer-thinking-guide.md`, guidesCrossLayerThinkingGuideContent);
|
|
57
|
+
files.set(`${PATHS.SPEC}/guides/code-reuse-thinking-guide.md`, guidesCodeReuseThinkingGuideContent);
|
|
49
58
|
// Claude commands
|
|
50
59
|
const claudeCommands = getCommandTemplates("claude-code");
|
|
51
60
|
for (const [name, content] of Object.entries(claudeCommands)) {
|
|
@@ -73,12 +82,17 @@ function collectTemplateFiles(_cwd) {
|
|
|
73
82
|
}
|
|
74
83
|
/**
|
|
75
84
|
* Analyze changes between current files and templates
|
|
85
|
+
*
|
|
86
|
+
* Uses hash tracking to distinguish between:
|
|
87
|
+
* - User didn't modify + template same = skip (unchangedFiles)
|
|
88
|
+
* - User didn't modify + template updated = auto-update (autoUpdateFiles)
|
|
89
|
+
* - User modified = needs confirmation (changedFiles)
|
|
76
90
|
*/
|
|
77
|
-
function analyzeChanges(cwd) {
|
|
78
|
-
const templates = collectTemplateFiles(cwd);
|
|
91
|
+
function analyzeChanges(cwd, hashes, templates) {
|
|
79
92
|
const result = {
|
|
80
93
|
newFiles: [],
|
|
81
94
|
unchangedFiles: [],
|
|
95
|
+
autoUpdateFiles: [],
|
|
82
96
|
changedFiles: [],
|
|
83
97
|
protectedPaths: PROTECTED_PATHS,
|
|
84
98
|
};
|
|
@@ -98,12 +112,26 @@ function analyzeChanges(cwd) {
|
|
|
98
112
|
else {
|
|
99
113
|
const existingContent = fs.readFileSync(fullPath, "utf-8");
|
|
100
114
|
if (existingContent === newContent) {
|
|
115
|
+
// Content same as template - already up to date
|
|
101
116
|
change.status = "unchanged";
|
|
102
117
|
result.unchangedFiles.push(change);
|
|
103
118
|
}
|
|
104
119
|
else {
|
|
105
|
-
|
|
106
|
-
|
|
120
|
+
// Content differs - check if user modified or template updated
|
|
121
|
+
const storedHash = hashes[relativePath];
|
|
122
|
+
const currentHash = computeHash(existingContent);
|
|
123
|
+
if (storedHash && storedHash === currentHash) {
|
|
124
|
+
// Hash matches stored hash - user didn't modify, template was updated
|
|
125
|
+
// Safe to auto-update
|
|
126
|
+
change.status = "changed";
|
|
127
|
+
result.autoUpdateFiles.push(change);
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
// Hash differs (or no stored hash) - user modified the file
|
|
131
|
+
// Needs confirmation
|
|
132
|
+
change.status = "changed";
|
|
133
|
+
result.changedFiles.push(change);
|
|
134
|
+
}
|
|
107
135
|
}
|
|
108
136
|
}
|
|
109
137
|
}
|
|
@@ -121,6 +149,13 @@ function printChangeSummary(changes) {
|
|
|
121
149
|
}
|
|
122
150
|
console.log("");
|
|
123
151
|
}
|
|
152
|
+
if (changes.autoUpdateFiles.length > 0) {
|
|
153
|
+
console.log(chalk.cyan(" Template updated (will auto-update):"));
|
|
154
|
+
for (const file of changes.autoUpdateFiles) {
|
|
155
|
+
console.log(chalk.cyan(` ↑ ${file.relativePath}`));
|
|
156
|
+
}
|
|
157
|
+
console.log("");
|
|
158
|
+
}
|
|
124
159
|
if (changes.unchangedFiles.length > 0) {
|
|
125
160
|
console.log(chalk.gray(" Unchanged files (will skip):"));
|
|
126
161
|
for (const file of changes.unchangedFiles.slice(0, 5)) {
|
|
@@ -132,17 +167,24 @@ function printChangeSummary(changes) {
|
|
|
132
167
|
console.log("");
|
|
133
168
|
}
|
|
134
169
|
if (changes.changedFiles.length > 0) {
|
|
135
|
-
console.log(chalk.yellow("
|
|
170
|
+
console.log(chalk.yellow(" Modified by you (need your decision):"));
|
|
136
171
|
for (const file of changes.changedFiles) {
|
|
137
172
|
console.log(chalk.yellow(` ? ${file.relativePath}`));
|
|
138
173
|
}
|
|
139
174
|
console.log("");
|
|
140
175
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
176
|
+
// Only show protected paths that actually exist
|
|
177
|
+
const existingProtectedPaths = changes.protectedPaths.filter((p) => {
|
|
178
|
+
const fullPath = path.join(process.cwd(), p);
|
|
179
|
+
return fs.existsSync(fullPath);
|
|
180
|
+
});
|
|
181
|
+
if (existingProtectedPaths.length > 0) {
|
|
182
|
+
console.log(chalk.gray(" User data (preserved):"));
|
|
183
|
+
for (const protectedPath of existingProtectedPaths) {
|
|
184
|
+
console.log(chalk.gray(` ○ ${protectedPath}/`));
|
|
185
|
+
}
|
|
186
|
+
console.log("");
|
|
144
187
|
}
|
|
145
|
-
console.log("");
|
|
146
188
|
}
|
|
147
189
|
/**
|
|
148
190
|
* Prompt user for conflict resolution
|
|
@@ -200,26 +242,88 @@ async function promptConflictResolution(file, options, applyToAll) {
|
|
|
200
242
|
return action;
|
|
201
243
|
}
|
|
202
244
|
/**
|
|
203
|
-
* Create
|
|
245
|
+
* Create a timestamped backup directory path
|
|
204
246
|
*/
|
|
205
|
-
function
|
|
206
|
-
const filesToBackup = changes.changedFiles;
|
|
207
|
-
if (filesToBackup.length === 0) {
|
|
208
|
-
return null;
|
|
209
|
-
}
|
|
247
|
+
function createBackupDirPath(cwd) {
|
|
210
248
|
const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
}
|
|
222
|
-
|
|
249
|
+
return path.join(cwd, DIR_NAMES.WORKFLOW, `.backup-${timestamp}`);
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Backup a single file to the backup directory
|
|
253
|
+
*/
|
|
254
|
+
function backupFile(cwd, backupDir, relativePath) {
|
|
255
|
+
const srcPath = path.join(cwd, relativePath);
|
|
256
|
+
if (!fs.existsSync(srcPath))
|
|
257
|
+
return;
|
|
258
|
+
const backupPath = path.join(backupDir, relativePath);
|
|
259
|
+
fs.mkdirSync(path.dirname(backupPath), { recursive: true });
|
|
260
|
+
fs.copyFileSync(srcPath, backupPath);
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Backup an entire directory recursively
|
|
264
|
+
*/
|
|
265
|
+
function backupDirectory(cwd, backupDir, relativeDirPath) {
|
|
266
|
+
const srcDir = path.join(cwd, relativeDirPath);
|
|
267
|
+
if (!fs.existsSync(srcDir))
|
|
268
|
+
return;
|
|
269
|
+
const files = collectAllFiles(srcDir);
|
|
270
|
+
for (const fullPath of files) {
|
|
271
|
+
const relativePath = path.relative(cwd, fullPath);
|
|
272
|
+
backupFile(cwd, backupDir, relativePath);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Directories to backup as complete snapshot
|
|
277
|
+
*/
|
|
278
|
+
const BACKUP_DIRS = [".trellis", ".claude", ".cursor"];
|
|
279
|
+
/**
|
|
280
|
+
* Patterns to exclude from backup (user data that shouldn't be backed up)
|
|
281
|
+
*/
|
|
282
|
+
const BACKUP_EXCLUDE_PATTERNS = [
|
|
283
|
+
".backup-", // Previous backups
|
|
284
|
+
"/workspace/", // Developer workspace (user data)
|
|
285
|
+
"/tasks/", // Task data (user data)
|
|
286
|
+
"/backlog/", // Backlog data (user data)
|
|
287
|
+
"/agent-traces/", // Agent traces (user data, legacy name)
|
|
288
|
+
];
|
|
289
|
+
/**
|
|
290
|
+
* Check if a path should be excluded from backup
|
|
291
|
+
*/
|
|
292
|
+
function shouldExcludeFromBackup(relativePath) {
|
|
293
|
+
for (const pattern of BACKUP_EXCLUDE_PATTERNS) {
|
|
294
|
+
if (relativePath.includes(pattern)) {
|
|
295
|
+
return true;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Create complete snapshot backup of all managed directories
|
|
302
|
+
* Backs up .trellis, .claude, .cursor directories entirely
|
|
303
|
+
* (excluding user data like workspace/, tasks/, backlog/)
|
|
304
|
+
*/
|
|
305
|
+
function createFullBackup(cwd) {
|
|
306
|
+
const backupDir = createBackupDirPath(cwd);
|
|
307
|
+
let hasFiles = false;
|
|
308
|
+
for (const dir of BACKUP_DIRS) {
|
|
309
|
+
const dirPath = path.join(cwd, dir);
|
|
310
|
+
if (!fs.existsSync(dirPath))
|
|
311
|
+
continue;
|
|
312
|
+
const files = collectAllFiles(dirPath);
|
|
313
|
+
for (const fullPath of files) {
|
|
314
|
+
const relativePath = path.relative(cwd, fullPath);
|
|
315
|
+
// Skip excluded paths
|
|
316
|
+
if (shouldExcludeFromBackup(relativePath))
|
|
317
|
+
continue;
|
|
318
|
+
// Create backup
|
|
319
|
+
if (!hasFiles) {
|
|
320
|
+
fs.mkdirSync(backupDir, { recursive: true });
|
|
321
|
+
hasFiles = true;
|
|
322
|
+
}
|
|
323
|
+
backupFile(cwd, backupDir, relativePath);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return hasFiles ? backupDir : null;
|
|
223
327
|
}
|
|
224
328
|
/**
|
|
225
329
|
* Update version file
|
|
@@ -273,6 +377,447 @@ function compareVersions(a, b) {
|
|
|
273
377
|
}
|
|
274
378
|
return 0;
|
|
275
379
|
}
|
|
380
|
+
/**
|
|
381
|
+
* Recursively collect all files in a directory
|
|
382
|
+
*/
|
|
383
|
+
function collectAllFiles(dirPath) {
|
|
384
|
+
if (!fs.existsSync(dirPath))
|
|
385
|
+
return [];
|
|
386
|
+
const files = [];
|
|
387
|
+
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
388
|
+
for (const entry of entries) {
|
|
389
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
390
|
+
if (entry.isDirectory()) {
|
|
391
|
+
files.push(...collectAllFiles(fullPath));
|
|
392
|
+
}
|
|
393
|
+
else if (entry.isFile()) {
|
|
394
|
+
files.push(fullPath);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
return files;
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Check if a directory only contains unmodified template files
|
|
401
|
+
* Returns true if safe to delete:
|
|
402
|
+
* - All files are tracked and unmodified, OR
|
|
403
|
+
* - All files match current template content (even if not tracked)
|
|
404
|
+
*/
|
|
405
|
+
function isDirectorySafeToReplace(cwd, dirRelativePath, hashes, templates) {
|
|
406
|
+
const dirFullPath = path.join(cwd, dirRelativePath);
|
|
407
|
+
if (!fs.existsSync(dirFullPath))
|
|
408
|
+
return true;
|
|
409
|
+
const files = collectAllFiles(dirFullPath);
|
|
410
|
+
if (files.length === 0)
|
|
411
|
+
return true; // Empty directory is safe
|
|
412
|
+
for (const fullPath of files) {
|
|
413
|
+
const relativePath = path.relative(cwd, fullPath);
|
|
414
|
+
const storedHash = hashes[relativePath];
|
|
415
|
+
const templateContent = templates.get(relativePath);
|
|
416
|
+
// Check if file matches template content (handles untracked files)
|
|
417
|
+
if (templateContent) {
|
|
418
|
+
const currentContent = fs.readFileSync(fullPath, "utf-8");
|
|
419
|
+
if (currentContent === templateContent) {
|
|
420
|
+
// File matches template - safe
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
// Check if file is tracked and unmodified
|
|
425
|
+
if (storedHash && !isTemplateModified(cwd, relativePath, hashes)) {
|
|
426
|
+
// Tracked and unmodified - safe
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
// File is either user-created or user-modified - not safe
|
|
430
|
+
return false;
|
|
431
|
+
}
|
|
432
|
+
return true;
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* Recursively delete a directory
|
|
436
|
+
*/
|
|
437
|
+
function removeDirectoryRecursive(dirPath) {
|
|
438
|
+
if (!fs.existsSync(dirPath))
|
|
439
|
+
return;
|
|
440
|
+
fs.rmSync(dirPath, { recursive: true, force: true });
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* Check if a file is safe to overwrite (matches template content)
|
|
444
|
+
*/
|
|
445
|
+
function isFileSafeToReplace(cwd, relativePath, templates) {
|
|
446
|
+
const fullPath = path.join(cwd, relativePath);
|
|
447
|
+
if (!fs.existsSync(fullPath))
|
|
448
|
+
return true;
|
|
449
|
+
const templateContent = templates.get(relativePath);
|
|
450
|
+
if (!templateContent)
|
|
451
|
+
return false; // Not a template file
|
|
452
|
+
const currentContent = fs.readFileSync(fullPath, "utf-8");
|
|
453
|
+
return currentContent === templateContent;
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Classify migrations based on file state and user modifications
|
|
457
|
+
*/
|
|
458
|
+
function classifyMigrations(migrations, cwd, hashes, templates) {
|
|
459
|
+
const result = {
|
|
460
|
+
auto: [],
|
|
461
|
+
confirm: [],
|
|
462
|
+
conflict: [],
|
|
463
|
+
skip: [],
|
|
464
|
+
};
|
|
465
|
+
for (const item of migrations) {
|
|
466
|
+
const oldPath = path.join(cwd, item.from);
|
|
467
|
+
const oldExists = fs.existsSync(oldPath);
|
|
468
|
+
if (!oldExists) {
|
|
469
|
+
// Old file doesn't exist, nothing to migrate
|
|
470
|
+
result.skip.push(item);
|
|
471
|
+
continue;
|
|
472
|
+
}
|
|
473
|
+
if (item.type === "rename" && item.to) {
|
|
474
|
+
const newPath = path.join(cwd, item.to);
|
|
475
|
+
const newExists = fs.existsSync(newPath);
|
|
476
|
+
if (newExists) {
|
|
477
|
+
// Both exist - check if new file matches template (safe to overwrite)
|
|
478
|
+
if (isFileSafeToReplace(cwd, item.to, templates)) {
|
|
479
|
+
// New file is just template content - safe to delete and rename
|
|
480
|
+
result.auto.push(item);
|
|
481
|
+
}
|
|
482
|
+
else {
|
|
483
|
+
// New file has user content - conflict
|
|
484
|
+
result.conflict.push(item);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
else if (isTemplateModified(cwd, item.from, hashes)) {
|
|
488
|
+
// User has modified the file - needs confirmation
|
|
489
|
+
result.confirm.push(item);
|
|
490
|
+
}
|
|
491
|
+
else {
|
|
492
|
+
// Unmodified template - safe to auto-migrate
|
|
493
|
+
result.auto.push(item);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
else if (item.type === "rename-dir" && item.to) {
|
|
497
|
+
const newPath = path.join(cwd, item.to);
|
|
498
|
+
const newExists = fs.existsSync(newPath);
|
|
499
|
+
if (newExists) {
|
|
500
|
+
// Target exists - check if it only contains unmodified template files
|
|
501
|
+
if (isDirectorySafeToReplace(cwd, item.to, hashes, templates)) {
|
|
502
|
+
// Safe to delete target and rename source
|
|
503
|
+
result.auto.push(item);
|
|
504
|
+
}
|
|
505
|
+
else {
|
|
506
|
+
// Target has user modifications - conflict
|
|
507
|
+
result.conflict.push(item);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
else {
|
|
511
|
+
// Directory rename - always auto (includes user files)
|
|
512
|
+
result.auto.push(item);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
else if (item.type === "delete") {
|
|
516
|
+
if (isTemplateModified(cwd, item.from, hashes)) {
|
|
517
|
+
// User has modified - needs confirmation before delete
|
|
518
|
+
result.confirm.push(item);
|
|
519
|
+
}
|
|
520
|
+
else {
|
|
521
|
+
// Unmodified - safe to auto-delete
|
|
522
|
+
result.auto.push(item);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
return result;
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* Print migration summary
|
|
530
|
+
*/
|
|
531
|
+
function printMigrationSummary(classified) {
|
|
532
|
+
const total = classified.auto.length +
|
|
533
|
+
classified.confirm.length +
|
|
534
|
+
classified.conflict.length +
|
|
535
|
+
classified.skip.length;
|
|
536
|
+
if (total === 0) {
|
|
537
|
+
console.log(chalk.gray(" No migrations to apply.\n"));
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
if (classified.auto.length > 0) {
|
|
541
|
+
console.log(chalk.green(" ✓ Auto-migrate (unmodified):"));
|
|
542
|
+
for (const item of classified.auto) {
|
|
543
|
+
if (item.type === "rename") {
|
|
544
|
+
console.log(chalk.green(` ${item.from} → ${item.to}`));
|
|
545
|
+
}
|
|
546
|
+
else if (item.type === "rename-dir") {
|
|
547
|
+
console.log(chalk.green(` [dir] ${item.from}/ → ${item.to}/`));
|
|
548
|
+
}
|
|
549
|
+
else {
|
|
550
|
+
console.log(chalk.green(` ✕ ${item.from}`));
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
console.log("");
|
|
554
|
+
}
|
|
555
|
+
if (classified.confirm.length > 0) {
|
|
556
|
+
console.log(chalk.yellow(" ⚠ Requires confirmation (modified by user):"));
|
|
557
|
+
for (const item of classified.confirm) {
|
|
558
|
+
if (item.type === "rename") {
|
|
559
|
+
console.log(chalk.yellow(` ${item.from} → ${item.to}`));
|
|
560
|
+
}
|
|
561
|
+
else {
|
|
562
|
+
console.log(chalk.yellow(` ✕ ${item.from}`));
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
console.log("");
|
|
566
|
+
}
|
|
567
|
+
if (classified.conflict.length > 0) {
|
|
568
|
+
console.log(chalk.red(" ⊘ Conflict (both old and new exist):"));
|
|
569
|
+
for (const item of classified.conflict) {
|
|
570
|
+
if (item.type === "rename-dir") {
|
|
571
|
+
console.log(chalk.red(` [dir] ${item.from}/ ↔ ${item.to}/`));
|
|
572
|
+
}
|
|
573
|
+
else {
|
|
574
|
+
console.log(chalk.red(` ${item.from} ↔ ${item.to}`));
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
console.log(chalk.gray(" → Resolve manually: merge or delete one, then re-run update"));
|
|
578
|
+
console.log("");
|
|
579
|
+
}
|
|
580
|
+
if (classified.skip.length > 0) {
|
|
581
|
+
console.log(chalk.gray(" ○ Skipping (old file not found):"));
|
|
582
|
+
for (const item of classified.skip.slice(0, 3)) {
|
|
583
|
+
console.log(chalk.gray(` ${item.from}`));
|
|
584
|
+
}
|
|
585
|
+
if (classified.skip.length > 3) {
|
|
586
|
+
console.log(chalk.gray(` ... and ${classified.skip.length - 3} more`));
|
|
587
|
+
}
|
|
588
|
+
console.log("");
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
/**
|
|
592
|
+
* Prompt user for migration action on a single item
|
|
593
|
+
*/
|
|
594
|
+
async function promptMigrationAction(item) {
|
|
595
|
+
const action = item.type === "rename" ? `${item.from} → ${item.to}` : `Delete ${item.from}`;
|
|
596
|
+
const { choice } = await inquirer.prompt([
|
|
597
|
+
{
|
|
598
|
+
type: "list",
|
|
599
|
+
name: "choice",
|
|
600
|
+
message: `${action}\nThis file has been modified. What would you like to do?`,
|
|
601
|
+
choices: [
|
|
602
|
+
{
|
|
603
|
+
name: item.type === "rename" ? "[r] Rename anyway" : "[d] Delete anyway",
|
|
604
|
+
value: "rename",
|
|
605
|
+
},
|
|
606
|
+
{
|
|
607
|
+
name: "[b] Backup original, then proceed",
|
|
608
|
+
value: "backup-rename",
|
|
609
|
+
},
|
|
610
|
+
{
|
|
611
|
+
name: "[s] Skip this migration",
|
|
612
|
+
value: "skip",
|
|
613
|
+
},
|
|
614
|
+
],
|
|
615
|
+
default: "skip",
|
|
616
|
+
},
|
|
617
|
+
]);
|
|
618
|
+
return choice;
|
|
619
|
+
}
|
|
620
|
+
/**
|
|
621
|
+
* Clean up empty directories after file migration
|
|
622
|
+
* Recursively removes empty parent directories up to .trellis root
|
|
623
|
+
*/
|
|
624
|
+
function cleanupEmptyDirs(cwd, dirPath) {
|
|
625
|
+
const fullPath = path.join(cwd, dirPath);
|
|
626
|
+
// Safety: don't delete outside of expected directories
|
|
627
|
+
if (!dirPath.startsWith(".trellis/") && !dirPath.startsWith(".claude/") && !dirPath.startsWith(".cursor/")) {
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
// Check if directory exists and is empty
|
|
631
|
+
if (!fs.existsSync(fullPath))
|
|
632
|
+
return;
|
|
633
|
+
try {
|
|
634
|
+
const stat = fs.statSync(fullPath);
|
|
635
|
+
if (!stat.isDirectory())
|
|
636
|
+
return;
|
|
637
|
+
const contents = fs.readdirSync(fullPath);
|
|
638
|
+
if (contents.length === 0) {
|
|
639
|
+
fs.rmdirSync(fullPath);
|
|
640
|
+
// Recursively check parent (but stop at root directories)
|
|
641
|
+
const parent = path.dirname(dirPath);
|
|
642
|
+
if (parent !== "." && parent !== dirPath && parent !== ".trellis" && parent !== ".claude" && parent !== ".cursor") {
|
|
643
|
+
cleanupEmptyDirs(cwd, parent);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
catch {
|
|
648
|
+
// Ignore errors (permission issues, etc.)
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
/**
|
|
652
|
+
* Sort migrations for safe execution order
|
|
653
|
+
* - rename-dir with deeper paths first (to handle nested directories)
|
|
654
|
+
* - rename-dir before rename/delete
|
|
655
|
+
*/
|
|
656
|
+
function sortMigrationsForExecution(migrations) {
|
|
657
|
+
return [...migrations].sort((a, b) => {
|
|
658
|
+
// rename-dir should be sorted by path depth (deeper first)
|
|
659
|
+
if (a.type === "rename-dir" && b.type === "rename-dir") {
|
|
660
|
+
const aDepth = a.from.split("/").length;
|
|
661
|
+
const bDepth = b.from.split("/").length;
|
|
662
|
+
return bDepth - aDepth; // Deeper paths first
|
|
663
|
+
}
|
|
664
|
+
// rename-dir before rename/delete (directories first)
|
|
665
|
+
if (a.type === "rename-dir" && b.type !== "rename-dir")
|
|
666
|
+
return -1;
|
|
667
|
+
if (a.type !== "rename-dir" && b.type === "rename-dir")
|
|
668
|
+
return 1;
|
|
669
|
+
return 0;
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
/**
|
|
673
|
+
* Execute classified migrations
|
|
674
|
+
*
|
|
675
|
+
* @param options.force - Force migrate modified files without asking
|
|
676
|
+
* @param options.skipAll - Skip all modified files without asking
|
|
677
|
+
* If neither is set, prompts interactively for modified files
|
|
678
|
+
*/
|
|
679
|
+
async function executeMigrations(classified, cwd, options) {
|
|
680
|
+
const result = {
|
|
681
|
+
renamed: 0,
|
|
682
|
+
deleted: 0,
|
|
683
|
+
skipped: 0,
|
|
684
|
+
conflicts: classified.conflict.length,
|
|
685
|
+
};
|
|
686
|
+
// Sort migrations for safe execution order
|
|
687
|
+
const sortedAuto = sortMigrationsForExecution(classified.auto);
|
|
688
|
+
// 1. Execute auto migrations (unmodified files and directories)
|
|
689
|
+
for (const item of sortedAuto) {
|
|
690
|
+
if (item.type === "rename" && item.to) {
|
|
691
|
+
const oldPath = path.join(cwd, item.from);
|
|
692
|
+
const newPath = path.join(cwd, item.to);
|
|
693
|
+
// Ensure target directory exists
|
|
694
|
+
fs.mkdirSync(path.dirname(newPath), { recursive: true });
|
|
695
|
+
fs.renameSync(oldPath, newPath);
|
|
696
|
+
// Update hash tracking
|
|
697
|
+
renameHash(cwd, item.from, item.to);
|
|
698
|
+
// Make executable if it's a script
|
|
699
|
+
if (item.to.endsWith(".sh")) {
|
|
700
|
+
fs.chmodSync(newPath, "755");
|
|
701
|
+
}
|
|
702
|
+
// Clean up empty source directory
|
|
703
|
+
cleanupEmptyDirs(cwd, path.dirname(item.from));
|
|
704
|
+
result.renamed++;
|
|
705
|
+
}
|
|
706
|
+
else if (item.type === "rename-dir" && item.to) {
|
|
707
|
+
const oldPath = path.join(cwd, item.from);
|
|
708
|
+
const newPath = path.join(cwd, item.to);
|
|
709
|
+
// If target exists (safe to replace, already checked in classification)
|
|
710
|
+
// delete it first before renaming
|
|
711
|
+
if (fs.existsSync(newPath)) {
|
|
712
|
+
removeDirectoryRecursive(newPath);
|
|
713
|
+
}
|
|
714
|
+
// Ensure parent directory exists
|
|
715
|
+
fs.mkdirSync(path.dirname(newPath), { recursive: true });
|
|
716
|
+
// Rename the entire directory (includes all user files)
|
|
717
|
+
fs.renameSync(oldPath, newPath);
|
|
718
|
+
// Batch update hash tracking for all files in the directory
|
|
719
|
+
const hashes = loadHashes(cwd);
|
|
720
|
+
const oldPrefix = item.from.endsWith("/") ? item.from : item.from + "/";
|
|
721
|
+
const newPrefix = item.to.endsWith("/") ? item.to : item.to + "/";
|
|
722
|
+
const updatedHashes = {};
|
|
723
|
+
for (const [hashPath, hashValue] of Object.entries(hashes)) {
|
|
724
|
+
if (hashPath.startsWith(oldPrefix)) {
|
|
725
|
+
// Rename path: old prefix -> new prefix
|
|
726
|
+
const newHashPath = newPrefix + hashPath.slice(oldPrefix.length);
|
|
727
|
+
updatedHashes[newHashPath] = hashValue;
|
|
728
|
+
}
|
|
729
|
+
else if (hashPath.startsWith(newPrefix)) {
|
|
730
|
+
// Skip old hashes from deleted target directory
|
|
731
|
+
// (they will be replaced by renamed source files)
|
|
732
|
+
continue;
|
|
733
|
+
}
|
|
734
|
+
else {
|
|
735
|
+
// Keep unchanged
|
|
736
|
+
updatedHashes[hashPath] = hashValue;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
saveHashes(cwd, updatedHashes);
|
|
740
|
+
result.renamed++;
|
|
741
|
+
}
|
|
742
|
+
else if (item.type === "delete") {
|
|
743
|
+
const filePath = path.join(cwd, item.from);
|
|
744
|
+
fs.unlinkSync(filePath);
|
|
745
|
+
// Remove from hash tracking
|
|
746
|
+
removeHash(cwd, item.from);
|
|
747
|
+
// Clean up empty directory
|
|
748
|
+
cleanupEmptyDirs(cwd, path.dirname(item.from));
|
|
749
|
+
result.deleted++;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
// 2. Handle confirm items (modified files)
|
|
753
|
+
// Note: All files are already backed up by createMigrationBackup before execution
|
|
754
|
+
for (const item of classified.confirm) {
|
|
755
|
+
let action;
|
|
756
|
+
if (options.force) {
|
|
757
|
+
// Force mode: proceed (already backed up)
|
|
758
|
+
action = "rename";
|
|
759
|
+
}
|
|
760
|
+
else if (options.skipAll) {
|
|
761
|
+
// Skip mode: skip all modified files
|
|
762
|
+
action = "skip";
|
|
763
|
+
}
|
|
764
|
+
else {
|
|
765
|
+
// Default: interactive prompt
|
|
766
|
+
action = await promptMigrationAction(item);
|
|
767
|
+
}
|
|
768
|
+
if (action === "skip") {
|
|
769
|
+
result.skipped++;
|
|
770
|
+
continue;
|
|
771
|
+
}
|
|
772
|
+
// For backup-rename, just proceed (backup already done)
|
|
773
|
+
// Proceed with rename or delete
|
|
774
|
+
if (item.type === "rename" && item.to) {
|
|
775
|
+
const oldPath = path.join(cwd, item.from);
|
|
776
|
+
const newPath = path.join(cwd, item.to);
|
|
777
|
+
fs.mkdirSync(path.dirname(newPath), { recursive: true });
|
|
778
|
+
fs.renameSync(oldPath, newPath);
|
|
779
|
+
renameHash(cwd, item.from, item.to);
|
|
780
|
+
if (item.to.endsWith(".sh")) {
|
|
781
|
+
fs.chmodSync(newPath, "755");
|
|
782
|
+
}
|
|
783
|
+
// Clean up empty source directory
|
|
784
|
+
cleanupEmptyDirs(cwd, path.dirname(item.from));
|
|
785
|
+
result.renamed++;
|
|
786
|
+
}
|
|
787
|
+
else if (item.type === "delete") {
|
|
788
|
+
const filePath = path.join(cwd, item.from);
|
|
789
|
+
fs.unlinkSync(filePath);
|
|
790
|
+
removeHash(cwd, item.from);
|
|
791
|
+
// Clean up empty directory
|
|
792
|
+
cleanupEmptyDirs(cwd, path.dirname(item.from));
|
|
793
|
+
result.deleted++;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
// 3. Skip count already tracked (old files not found)
|
|
797
|
+
result.skipped += classified.skip.length;
|
|
798
|
+
return result;
|
|
799
|
+
}
|
|
800
|
+
/**
|
|
801
|
+
* Print migration result summary
|
|
802
|
+
*/
|
|
803
|
+
function printMigrationResult(result) {
|
|
804
|
+
const parts = [];
|
|
805
|
+
if (result.renamed > 0) {
|
|
806
|
+
parts.push(`${result.renamed} renamed`);
|
|
807
|
+
}
|
|
808
|
+
if (result.deleted > 0) {
|
|
809
|
+
parts.push(`${result.deleted} deleted`);
|
|
810
|
+
}
|
|
811
|
+
if (result.skipped > 0) {
|
|
812
|
+
parts.push(`${result.skipped} skipped`);
|
|
813
|
+
}
|
|
814
|
+
if (result.conflicts > 0) {
|
|
815
|
+
parts.push(`${result.conflicts} conflict${result.conflicts > 1 ? "s" : ""}`);
|
|
816
|
+
}
|
|
817
|
+
if (parts.length > 0) {
|
|
818
|
+
console.log(chalk.cyan(`Migration complete: ${parts.join(", ")}`));
|
|
819
|
+
}
|
|
820
|
+
}
|
|
276
821
|
/**
|
|
277
822
|
* Main update command
|
|
278
823
|
*/
|
|
@@ -322,15 +867,65 @@ export async function update(options) {
|
|
|
322
867
|
}
|
|
323
868
|
console.log(chalk.yellow("⚠️ --allow-downgrade flag set. Proceeding with downgrade...\n"));
|
|
324
869
|
}
|
|
325
|
-
//
|
|
326
|
-
const
|
|
870
|
+
// Load template hashes for modification detection
|
|
871
|
+
const hashes = loadHashes(cwd);
|
|
872
|
+
const isFirstHashTracking = Object.keys(hashes).length === 0;
|
|
873
|
+
// Handle unknown version - skip migrations but continue with template updates
|
|
874
|
+
const isUnknownVersion = projectVersion === "unknown";
|
|
875
|
+
if (isUnknownVersion) {
|
|
876
|
+
console.log(chalk.yellow("⚠️ No version file found. Skipping migrations."));
|
|
877
|
+
console.log(chalk.gray(" Template updates will still be applied."));
|
|
878
|
+
console.log(chalk.gray(" If your project used old file paths, you may need to rename them manually.\n"));
|
|
879
|
+
}
|
|
880
|
+
// Collect templates (used for both migration classification and change analysis)
|
|
881
|
+
const templates = collectTemplateFiles(cwd);
|
|
882
|
+
// Check for pending migrations (skip if unknown version)
|
|
883
|
+
const pendingMigrations = isUnknownVersion
|
|
884
|
+
? []
|
|
885
|
+
: getMigrationsForVersion(projectVersion, cliVersion);
|
|
886
|
+
const hasMigrations = pendingMigrations.length > 0;
|
|
887
|
+
// Classify migrations (stored for later backup creation)
|
|
888
|
+
let classifiedMigrations = null;
|
|
889
|
+
if (hasMigrations) {
|
|
890
|
+
console.log(chalk.cyan("Analyzing migrations...\n"));
|
|
891
|
+
classifiedMigrations = classifyMigrations(pendingMigrations, cwd, hashes, templates);
|
|
892
|
+
printMigrationSummary(classifiedMigrations);
|
|
893
|
+
// Show hint about --migrate flag (execution happens later after backup)
|
|
894
|
+
if (!options.migrate) {
|
|
895
|
+
const autoCount = classifiedMigrations.auto.length;
|
|
896
|
+
const confirmCount = classifiedMigrations.confirm.length;
|
|
897
|
+
if (autoCount > 0 || confirmCount > 0) {
|
|
898
|
+
console.log(chalk.gray(`Tip: Use --migrate to apply migrations (prompts for modified files).`));
|
|
899
|
+
if (confirmCount > 0) {
|
|
900
|
+
console.log(chalk.gray(` Use --migrate -f to force all, or --migrate -s to skip modified.\n`));
|
|
901
|
+
}
|
|
902
|
+
else {
|
|
903
|
+
console.log("");
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
// Analyze changes (pass hashes for modification detection)
|
|
909
|
+
const changes = analyzeChanges(cwd, hashes, templates);
|
|
327
910
|
// Print summary
|
|
328
911
|
printChangeSummary(changes);
|
|
912
|
+
// First-time hash tracking hint
|
|
913
|
+
if (isFirstHashTracking && changes.changedFiles.length > 0) {
|
|
914
|
+
console.log(chalk.cyan("ℹ️ First update with hash tracking enabled."));
|
|
915
|
+
console.log(chalk.gray(" Changed files shown above may not be actual user modifications."));
|
|
916
|
+
console.log(chalk.gray(" After this update, hash tracking will accurately detect changes.\n"));
|
|
917
|
+
}
|
|
329
918
|
// Check if there's anything to do
|
|
330
919
|
const isUpgrade = cliVsProject > 0;
|
|
331
920
|
const isDowngrade = cliVsProject < 0;
|
|
332
921
|
const isSameVersion = cliVsProject === 0;
|
|
333
|
-
if
|
|
922
|
+
// Check if we have pending migrations that need to be applied
|
|
923
|
+
const hasPendingMigrations = options.migrate && classifiedMigrations && (classifiedMigrations.auto.length > 0 ||
|
|
924
|
+
classifiedMigrations.confirm.length > 0);
|
|
925
|
+
if (changes.newFiles.length === 0 &&
|
|
926
|
+
changes.autoUpdateFiles.length === 0 &&
|
|
927
|
+
changes.changedFiles.length === 0 &&
|
|
928
|
+
!hasPendingMigrations) {
|
|
334
929
|
if (isSameVersion) {
|
|
335
930
|
console.log(chalk.green("✓ Already up to date!"));
|
|
336
931
|
}
|
|
@@ -364,10 +959,19 @@ export async function update(options) {
|
|
|
364
959
|
console.log(chalk.yellow("Update cancelled."));
|
|
365
960
|
return;
|
|
366
961
|
}
|
|
367
|
-
// Create backup
|
|
368
|
-
const backupDir =
|
|
962
|
+
// Create complete backup of .trellis, .claude, .cursor directories
|
|
963
|
+
const backupDir = createFullBackup(cwd);
|
|
964
|
+
if (backupDir) {
|
|
965
|
+
console.log(chalk.gray(`\nBackup created: ${path.relative(cwd, backupDir)}/`));
|
|
966
|
+
}
|
|
967
|
+
// Execute migrations if --migrate flag is set
|
|
968
|
+
if (options.migrate && classifiedMigrations) {
|
|
969
|
+
const migrationResult = await executeMigrations(classifiedMigrations, cwd, { force: options.force, skipAll: options.skipAll });
|
|
970
|
+
printMigrationResult(migrationResult);
|
|
971
|
+
}
|
|
369
972
|
// Track results
|
|
370
973
|
let added = 0;
|
|
974
|
+
let autoUpdated = 0;
|
|
371
975
|
let updated = 0;
|
|
372
976
|
let skipped = 0;
|
|
373
977
|
let createdNew = 0;
|
|
@@ -386,6 +990,19 @@ export async function update(options) {
|
|
|
386
990
|
added++;
|
|
387
991
|
}
|
|
388
992
|
}
|
|
993
|
+
// Auto-update files (template updated, user didn't modify)
|
|
994
|
+
if (changes.autoUpdateFiles.length > 0) {
|
|
995
|
+
console.log(chalk.blue("\nAuto-updating template files..."));
|
|
996
|
+
for (const file of changes.autoUpdateFiles) {
|
|
997
|
+
fs.writeFileSync(file.path, file.newContent);
|
|
998
|
+
// Make scripts executable
|
|
999
|
+
if (file.relativePath.endsWith(".sh")) {
|
|
1000
|
+
fs.chmodSync(file.path, "755");
|
|
1001
|
+
}
|
|
1002
|
+
console.log(chalk.cyan(` ↑ ${file.relativePath}`));
|
|
1003
|
+
autoUpdated++;
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
389
1006
|
// Handle changed files
|
|
390
1007
|
if (changes.changedFiles.length > 0) {
|
|
391
1008
|
console.log(chalk.blue("\n--- Resolving conflicts ---\n"));
|
|
@@ -414,11 +1031,36 @@ export async function update(options) {
|
|
|
414
1031
|
}
|
|
415
1032
|
// Update version file
|
|
416
1033
|
updateVersionFile(cwd);
|
|
1034
|
+
// Update template hashes for new, auto-updated, and overwritten files
|
|
1035
|
+
const filesToHash = new Map();
|
|
1036
|
+
for (const file of changes.newFiles) {
|
|
1037
|
+
filesToHash.set(file.relativePath, file.newContent);
|
|
1038
|
+
}
|
|
1039
|
+
// Auto-updated files always get new hash
|
|
1040
|
+
for (const file of changes.autoUpdateFiles) {
|
|
1041
|
+
filesToHash.set(file.relativePath, file.newContent);
|
|
1042
|
+
}
|
|
1043
|
+
// Only hash overwritten files (not skipped or .new copies)
|
|
1044
|
+
for (const file of changes.changedFiles) {
|
|
1045
|
+
const fullPath = path.join(cwd, file.relativePath);
|
|
1046
|
+
if (fs.existsSync(fullPath)) {
|
|
1047
|
+
const content = fs.readFileSync(fullPath, "utf-8");
|
|
1048
|
+
if (content === file.newContent) {
|
|
1049
|
+
filesToHash.set(file.relativePath, file.newContent);
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
if (filesToHash.size > 0) {
|
|
1054
|
+
updateHashes(cwd, filesToHash);
|
|
1055
|
+
}
|
|
417
1056
|
// Print summary
|
|
418
1057
|
console.log(chalk.cyan("\n--- Summary ---\n"));
|
|
419
1058
|
if (added > 0) {
|
|
420
1059
|
console.log(` Added: ${added} file(s)`);
|
|
421
1060
|
}
|
|
1061
|
+
if (autoUpdated > 0) {
|
|
1062
|
+
console.log(` Auto-updated: ${autoUpdated} file(s)`);
|
|
1063
|
+
}
|
|
422
1064
|
if (updated > 0) {
|
|
423
1065
|
console.log(` Updated: ${updated} file(s)`);
|
|
424
1066
|
}
|