@pennixrv/trellis 0.7.0-beta.47 → 0.7.0-beta.49

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.
@@ -4,6 +4,7 @@ import path from "node:path";
4
4
  import chalk from "chalk";
5
5
  import inquirer from "inquirer";
6
6
  import { DIR_NAMES, FILE_NAMES, PATHS } from "../constants/paths.js";
7
+ import { BACKUP_COMPLETE_MARKER, BACKUP_PRUNE_LOCK } from "./backup.js";
7
8
  import { VERSION, PACKAGE_NAME } from "../constants/version.js";
8
9
  import { getMigrationsForVersion, getAllMigrations, getMigrationMetadata, getConfigSectionsAddedBetween, } from "../migrations/index.js";
9
10
  import { loadHashes, saveHashes, updateHashes, isTemplateModified, removeHash, renameHash, computeHash, shouldExcludeFromHash, } from "../utils/template-hash.js";
@@ -1028,6 +1029,7 @@ export function shouldExcludeFromBackup(relativePath) {
1028
1029
  */
1029
1030
  function createFullBackup(cwd) {
1030
1031
  const backupDir = createBackupDirPath(cwd);
1032
+ const stagingDir = `${backupDir}.incomplete`;
1031
1033
  let hasFiles = false;
1032
1034
  for (const dir of BACKUP_DIRS) {
1033
1035
  const dirPath = path.join(cwd, dir);
@@ -1041,10 +1043,10 @@ function createFullBackup(cwd) {
1041
1043
  continue;
1042
1044
  // Create backup
1043
1045
  if (!hasFiles) {
1044
- fs.mkdirSync(backupDir, { recursive: true });
1046
+ fs.mkdirSync(stagingDir, { recursive: true });
1045
1047
  hasFiles = true;
1046
1048
  }
1047
- backupFile(cwd, backupDir, relativePath);
1049
+ backupFile(cwd, stagingDir, relativePath);
1048
1050
  }
1049
1051
  }
1050
1052
  for (const relativePath of BACKUP_FILES) {
@@ -1054,10 +1056,14 @@ function createFullBackup(cwd) {
1054
1056
  if (shouldExcludeFromBackup(relativePath))
1055
1057
  continue;
1056
1058
  if (!hasFiles) {
1057
- fs.mkdirSync(backupDir, { recursive: true });
1059
+ fs.mkdirSync(stagingDir, { recursive: true });
1058
1060
  hasFiles = true;
1059
1061
  }
1060
- backupFile(cwd, backupDir, relativePath);
1062
+ backupFile(cwd, stagingDir, relativePath);
1063
+ }
1064
+ if (hasFiles) {
1065
+ fs.writeFileSync(path.join(stagingDir, BACKUP_COMPLETE_MARKER), VERSION, "utf8");
1066
+ fs.renameSync(stagingDir, backupDir);
1061
1067
  }
1062
1068
  return hasFiles ? backupDir : null;
1063
1069
  }
@@ -2011,292 +2017,301 @@ export async function update(options) {
2011
2017
  return;
2012
2018
  }
2013
2019
  }
2014
- // Create complete backup of all managed platform/workflow directories
2015
- const backupDir = createFullBackup(cwd);
2016
- if (backupDir) {
2017
- console.log(chalk.gray(`\nBackup created: ${path.relative(cwd, backupDir)}/`));
2018
- }
2019
- // Execute migrations if --migrate flag is set
2020
- if (options.migrate && classifiedMigrations) {
2021
- const migrationResult = await executeMigrations(classifiedMigrations, cwd, {
2022
- force: options.force,
2023
- skipAll: options.skipAll,
2024
- }, templates);
2025
- printMigrationResult(migrationResult);
2026
- // Hardcoded: Rename traces-*.md to journal-*.md in workspace directories
2027
- // Why hardcoded: The migration system only supports fixed path renames, not pattern-based.
2028
- // traces-*.md files are in .trellis/workspace/{developer}/ with variable developer names
2029
- // and variable file numbers (traces-1.md, traces-2.md, etc.), so we can't enumerate them
2030
- // in the migration manifest. This is a one-time migration for the 0.2.0 naming redesign.
2031
- const workspaceDir = path.join(cwd, PATHS.WORKSPACE);
2032
- const { renamed: journalRenamed, skipped: journalSkipped } = renameTracesToJournal(workspaceDir);
2033
- if (journalRenamed > 0) {
2034
- console.log(chalk.cyan(`Renamed ${journalRenamed} traces file(s) to journal`));
2035
- }
2036
- for (const oldPath of journalSkipped) {
2037
- console.warn(chalk.yellow(`Kept ${path.relative(cwd, oldPath)}: its journal target already exists`));
2038
- }
2039
- }
2040
- // Execute safe-file-delete (after backup, before template writes)
2041
- let safeDeleted = 0;
2042
- if (hasSafeDeletes) {
2043
- safeDeleted = executeSafeFileDeletes(safeFileDeletes, cwd);
2044
- if (safeDeleted > 0) {
2045
- console.log(chalk.cyan(`\nCleaned up ${safeDeleted} deprecated command file(s)`));
2046
- }
2047
- }
2048
- // Track results
2049
- let added = 0;
2050
- let autoUpdated = 0;
2051
- let updated = 0;
2052
- let skipped = 0;
2053
- let createdNew = 0;
2054
- // Add new files
2055
- if (changes.newFiles.length > 0) {
2056
- console.log(chalk.blue("\nAdding new files..."));
2057
- for (const file of changes.newFiles) {
2058
- const dir = path.dirname(file.path);
2059
- fs.mkdirSync(dir, { recursive: true });
2060
- fs.writeFileSync(file.path, file.newContent);
2061
- // Make scripts executable
2062
- if (file.relativePath.endsWith(".sh") ||
2063
- file.relativePath.endsWith(".py")) {
2064
- fs.chmodSync(file.path, "755");
2020
+ // Keep the rollback snapshot protected until all update writes finish.
2021
+ const backupLockPath = path.join(cwd, DIR_NAMES.WORKFLOW, BACKUP_PRUNE_LOCK);
2022
+ const backupLock = fs.openSync(backupLockPath, "wx");
2023
+ try {
2024
+ // Create complete backup of all managed platform/workflow directories
2025
+ const backupDir = createFullBackup(cwd);
2026
+ if (backupDir) {
2027
+ console.log(chalk.gray(`\nBackup created: ${path.relative(cwd, backupDir)}/`));
2028
+ }
2029
+ // Execute migrations if --migrate flag is set
2030
+ if (options.migrate && classifiedMigrations) {
2031
+ const migrationResult = await executeMigrations(classifiedMigrations, cwd, {
2032
+ force: options.force,
2033
+ skipAll: options.skipAll,
2034
+ }, templates);
2035
+ printMigrationResult(migrationResult);
2036
+ // Hardcoded: Rename traces-*.md to journal-*.md in workspace directories
2037
+ // Why hardcoded: The migration system only supports fixed path renames, not pattern-based.
2038
+ // traces-*.md files are in .trellis/workspace/{developer}/ with variable developer names
2039
+ // and variable file numbers (traces-1.md, traces-2.md, etc.), so we can't enumerate them
2040
+ // in the migration manifest. This is a one-time migration for the 0.2.0 naming redesign.
2041
+ const workspaceDir = path.join(cwd, PATHS.WORKSPACE);
2042
+ const { renamed: journalRenamed, skipped: journalSkipped } = renameTracesToJournal(workspaceDir);
2043
+ if (journalRenamed > 0) {
2044
+ console.log(chalk.cyan(`Renamed ${journalRenamed} traces file(s) to journal`));
2045
+ }
2046
+ for (const oldPath of journalSkipped) {
2047
+ console.warn(chalk.yellow(`Kept ${path.relative(cwd, oldPath)}: its journal target already exists`));
2065
2048
  }
2066
- console.log(chalk.green(` + ${file.relativePath}`));
2067
- added++;
2068
2049
  }
2069
- }
2070
- // Auto-update files (template updated, user didn't modify)
2071
- if (changes.autoUpdateFiles.length > 0) {
2072
- console.log(chalk.blue("\nAuto-updating template files..."));
2073
- for (const file of changes.autoUpdateFiles) {
2074
- fs.writeFileSync(file.path, file.newContent);
2075
- // Make scripts executable
2076
- if (file.relativePath.endsWith(".sh") ||
2077
- file.relativePath.endsWith(".py")) {
2078
- fs.chmodSync(file.path, "755");
2050
+ // Execute safe-file-delete (after backup, before template writes)
2051
+ let safeDeleted = 0;
2052
+ if (hasSafeDeletes) {
2053
+ safeDeleted = executeSafeFileDeletes(safeFileDeletes, cwd);
2054
+ if (safeDeleted > 0) {
2055
+ console.log(chalk.cyan(`\nCleaned up ${safeDeleted} deprecated command file(s)`));
2079
2056
  }
2080
- console.log(chalk.cyan(` ↑ ${file.relativePath}`));
2081
- autoUpdated++;
2082
2057
  }
2083
- }
2084
- // Handle changed files
2085
- if (changes.changedFiles.length > 0) {
2086
- console.log(chalk.blue("\n--- Resolving conflicts ---\n"));
2087
- const applyToAll = { action: null };
2088
- for (const file of changes.changedFiles) {
2089
- const action = await promptConflictResolution(file, options, applyToAll);
2090
- if (action === "overwrite") {
2091
- const markers = file.relativePath === FILE_NAMES.AGENTS
2092
- ? [TRELLIS_BLOCK_START, TRELLIS_BLOCK_END]
2093
- : file.relativePath === COPILOT_INSTRUCTIONS_PATH
2094
- ? [
2095
- COPILOT_INSTRUCTIONS_BLOCK_START,
2096
- COPILOT_INSTRUCTIONS_BLOCK_END,
2097
- ]
2098
- : null;
2099
- if (markers &&
2100
- mergeManagedMarkdownBlock(fs.readFileSync(file.path, "utf-8"), file.newContent, markers[0], markers[1]) === null) {
2101
- throw new Error("Refusing ambiguous managed Markdown markers in " +
2102
- file.relativePath +
2103
- "; use --create-new or repair the markers.");
2058
+ // Track results
2059
+ let added = 0;
2060
+ let autoUpdated = 0;
2061
+ let updated = 0;
2062
+ let skipped = 0;
2063
+ let createdNew = 0;
2064
+ // Add new files
2065
+ if (changes.newFiles.length > 0) {
2066
+ console.log(chalk.blue("\nAdding new files..."));
2067
+ for (const file of changes.newFiles) {
2068
+ const dir = path.dirname(file.path);
2069
+ fs.mkdirSync(dir, { recursive: true });
2070
+ fs.writeFileSync(file.path, file.newContent);
2071
+ // Make scripts executable
2072
+ if (file.relativePath.endsWith(".sh") ||
2073
+ file.relativePath.endsWith(".py")) {
2074
+ fs.chmodSync(file.path, "755");
2104
2075
  }
2076
+ console.log(chalk.green(` + ${file.relativePath}`));
2077
+ added++;
2078
+ }
2079
+ }
2080
+ // Auto-update files (template updated, user didn't modify)
2081
+ if (changes.autoUpdateFiles.length > 0) {
2082
+ console.log(chalk.blue("\nAuto-updating template files..."));
2083
+ for (const file of changes.autoUpdateFiles) {
2105
2084
  fs.writeFileSync(file.path, file.newContent);
2085
+ // Make scripts executable
2106
2086
  if (file.relativePath.endsWith(".sh") ||
2107
2087
  file.relativePath.endsWith(".py")) {
2108
2088
  fs.chmodSync(file.path, "755");
2109
2089
  }
2110
- console.log(chalk.yellow(` ✓ Overwritten: ${file.relativePath}`));
2111
- updated++;
2090
+ console.log(chalk.cyan(` ↑ ${file.relativePath}`));
2091
+ autoUpdated++;
2112
2092
  }
2113
- else if (action === "create-new") {
2114
- const newPath = file.path + ".new";
2115
- fs.writeFileSync(newPath, file.newContent);
2116
- console.log(chalk.blue(` ✓ Created: ${file.relativePath}.new`));
2117
- createdNew++;
2093
+ }
2094
+ // Handle changed files
2095
+ if (changes.changedFiles.length > 0) {
2096
+ console.log(chalk.blue("\n--- Resolving conflicts ---\n"));
2097
+ const applyToAll = { action: null };
2098
+ for (const file of changes.changedFiles) {
2099
+ const action = await promptConflictResolution(file, options, applyToAll);
2100
+ if (action === "overwrite") {
2101
+ const markers = file.relativePath === FILE_NAMES.AGENTS
2102
+ ? [TRELLIS_BLOCK_START, TRELLIS_BLOCK_END]
2103
+ : file.relativePath === COPILOT_INSTRUCTIONS_PATH
2104
+ ? [
2105
+ COPILOT_INSTRUCTIONS_BLOCK_START,
2106
+ COPILOT_INSTRUCTIONS_BLOCK_END,
2107
+ ]
2108
+ : null;
2109
+ if (markers &&
2110
+ mergeManagedMarkdownBlock(fs.readFileSync(file.path, "utf-8"), file.newContent, markers[0], markers[1]) === null) {
2111
+ throw new Error("Refusing ambiguous managed Markdown markers in " +
2112
+ file.relativePath +
2113
+ "; use --create-new or repair the markers.");
2114
+ }
2115
+ fs.writeFileSync(file.path, file.newContent);
2116
+ if (file.relativePath.endsWith(".sh") ||
2117
+ file.relativePath.endsWith(".py")) {
2118
+ fs.chmodSync(file.path, "755");
2119
+ }
2120
+ console.log(chalk.yellow(` ✓ Overwritten: ${file.relativePath}`));
2121
+ updated++;
2122
+ }
2123
+ else if (action === "create-new") {
2124
+ const newPath = file.path + ".new";
2125
+ fs.writeFileSync(newPath, file.newContent);
2126
+ console.log(chalk.blue(` ✓ Created: ${file.relativePath}.new`));
2127
+ createdNew++;
2128
+ }
2129
+ else {
2130
+ console.log(chalk.gray(` ○ Skipped: ${file.relativePath}`));
2131
+ skipped++;
2132
+ }
2118
2133
  }
2119
- else {
2120
- console.log(chalk.gray(` ○ Skipped: ${file.relativePath}`));
2121
- skipped++;
2134
+ }
2135
+ // Append additive config.yaml sections introduced between versions.
2136
+ // Sentinel-gated, so users keep their customizations and re-running update
2137
+ // on already-migrated files is a no-op. Skipped on unknown / downgrade.
2138
+ let configSectionsAppended = 0;
2139
+ if (cliVsProject > 0 && projectVersion !== "unknown") {
2140
+ const sectionEntries = getConfigSectionsAddedBetween(projectVersion, cliVersion);
2141
+ if (sectionEntries.length > 0) {
2142
+ const { appended } = applyConfigSectionsAdded(sectionEntries, cwd, templates);
2143
+ configSectionsAppended = appended;
2122
2144
  }
2123
2145
  }
2124
- }
2125
- // Append additive config.yaml sections introduced between versions.
2126
- // Sentinel-gated, so users keep their customizations and re-running update
2127
- // on already-migrated files is a no-op. Skipped on unknown / downgrade.
2128
- let configSectionsAppended = 0;
2129
- if (cliVsProject > 0 && projectVersion !== "unknown") {
2130
- const sectionEntries = getConfigSectionsAddedBetween(projectVersion, cliVersion);
2131
- if (sectionEntries.length > 0) {
2132
- const { appended } = applyConfigSectionsAdded(sectionEntries, cwd, templates);
2133
- configSectionsAppended = appended;
2134
- }
2135
- }
2136
- // Update version file
2137
- updateVersionFile(cwd);
2138
- refreshNativeWorkflowProvenance(cwd);
2139
- // Update template hashes for new, auto-updated, and overwritten files
2140
- const filesToHash = new Map(unchangedFileHashRepairs);
2141
- for (const file of changes.newFiles) {
2142
- filesToHash.set(file.relativePath, file.newContent);
2143
- }
2144
- // Auto-updated files always get new hash
2145
- for (const file of changes.autoUpdateFiles) {
2146
- filesToHash.set(file.relativePath, file.newContent);
2147
- }
2148
- // Only hash overwritten files (not skipped or .new copies)
2149
- for (const file of changes.changedFiles) {
2150
- const fullPath = path.join(cwd, file.relativePath);
2151
- if (fs.existsSync(fullPath)) {
2152
- const content = fs.readFileSync(fullPath, "utf-8");
2153
- if (content === file.newContent) {
2154
- filesToHash.set(file.relativePath, file.newContent);
2146
+ // Update version file
2147
+ updateVersionFile(cwd);
2148
+ refreshNativeWorkflowProvenance(cwd);
2149
+ // Update template hashes for new, auto-updated, and overwritten files
2150
+ const filesToHash = new Map(unchangedFileHashRepairs);
2151
+ for (const file of changes.newFiles) {
2152
+ filesToHash.set(file.relativePath, file.newContent);
2153
+ }
2154
+ // Auto-updated files always get new hash
2155
+ for (const file of changes.autoUpdateFiles) {
2156
+ filesToHash.set(file.relativePath, file.newContent);
2157
+ }
2158
+ // Only hash overwritten files (not skipped or .new copies)
2159
+ for (const file of changes.changedFiles) {
2160
+ const fullPath = path.join(cwd, file.relativePath);
2161
+ if (fs.existsSync(fullPath)) {
2162
+ const content = fs.readFileSync(fullPath, "utf-8");
2163
+ if (content === file.newContent) {
2164
+ filesToHash.set(file.relativePath, file.newContent);
2165
+ }
2155
2166
  }
2156
2167
  }
2157
- }
2158
- if (filesToHash.size > 0) {
2159
- updateHashes(cwd, filesToHash);
2160
- }
2161
- // Print summary
2162
- console.log(chalk.cyan("\n--- Summary ---\n"));
2163
- if (added > 0) {
2164
- console.log(` Added: ${added} file(s)`);
2165
- }
2166
- if (autoUpdated > 0) {
2167
- console.log(` Auto-updated: ${autoUpdated} file(s)`);
2168
- }
2169
- if (updated > 0) {
2170
- console.log(` Updated: ${updated} file(s)`);
2171
- }
2172
- if (skipped > 0) {
2173
- console.log(` Skipped: ${skipped} file(s)`);
2174
- }
2175
- if (createdNew > 0) {
2176
- console.log(` Created .new copies: ${createdNew} file(s)`);
2177
- }
2178
- if (safeDeleted > 0) {
2179
- console.log(` Cleaned up: ${safeDeleted} deprecated file(s)`);
2180
- }
2181
- if (configSectionsAppended > 0) {
2182
- console.log(` Config sections added: ${configSectionsAppended}`);
2183
- }
2184
- if (backupDir) {
2185
- console.log(` Backup: ${path.relative(cwd, backupDir)}/`);
2186
- }
2187
- const actionWord = isDowngrade ? "Downgrade" : "Update";
2188
- console.log(chalk.green(`\n✅ ${actionWord} complete! (${projectVersion} → ${cliVersion})`));
2189
- if (createdNew > 0) {
2190
- console.log(chalk.gray("\nTip: Review .new files and merge changes manually if needed."));
2191
- }
2192
- // Create migration task if there are breaking changes with migration guides
2193
- if (cliVsProject > 0 && projectVersion !== "unknown") {
2194
- const metadata = getMigrationMetadata(projectVersion, cliVersion);
2195
- if (metadata.breaking && metadata.migrationGuides.length > 0) {
2196
- // Create task directory
2197
- const today = new Date();
2198
- const monthDay = `${String(today.getMonth() + 1).padStart(2, "0")}-${String(today.getDate()).padStart(2, "0")}`;
2199
- const taskSlug = `migrate-to-${cliVersion}`;
2200
- const taskDirName = `${monthDay}-${taskSlug}`;
2201
- const tasksDir = path.join(cwd, DIR_NAMES.WORKFLOW, DIR_NAMES.TASKS);
2202
- const taskDir = path.join(tasksDir, taskDirName);
2203
- // Check if task already exists
2204
- if (!fs.existsSync(taskDir)) {
2205
- fs.mkdirSync(taskDir, { recursive: true });
2206
- // Get current developer for assignee.
2207
- // `.developer` is a key=value file (written by init_developer.py):
2208
- // name=<developer-name>
2209
- // initialized_at=<iso8601>
2210
- // Reading it raw and .trim()-ing embeds the entire file contents
2211
- // (including the `name=` prefix and the `initialized_at` line) into
2212
- // the assignee field, producing bogus assignees like
2213
- // "name=suyuan\ninitialized_at=2026-04-07T23:41:21.978312" that
2214
- // later break session-start task rendering.
2215
- const developerFile = path.join(cwd, DIR_NAMES.WORKFLOW, ".developer");
2216
- let currentDeveloper = "unknown";
2217
- if (fs.existsSync(developerFile)) {
2218
- const raw = fs.readFileSync(developerFile, "utf-8");
2219
- const nameMatch = raw.match(/^\s*name\s*=\s*(.+?)\s*$/m);
2220
- if (nameMatch) {
2221
- currentDeveloper = nameMatch[1];
2168
+ if (filesToHash.size > 0) {
2169
+ updateHashes(cwd, filesToHash);
2170
+ }
2171
+ // Print summary
2172
+ console.log(chalk.cyan("\n--- Summary ---\n"));
2173
+ if (added > 0) {
2174
+ console.log(` Added: ${added} file(s)`);
2175
+ }
2176
+ if (autoUpdated > 0) {
2177
+ console.log(` Auto-updated: ${autoUpdated} file(s)`);
2178
+ }
2179
+ if (updated > 0) {
2180
+ console.log(` Updated: ${updated} file(s)`);
2181
+ }
2182
+ if (skipped > 0) {
2183
+ console.log(` Skipped: ${skipped} file(s)`);
2184
+ }
2185
+ if (createdNew > 0) {
2186
+ console.log(` Created .new copies: ${createdNew} file(s)`);
2187
+ }
2188
+ if (safeDeleted > 0) {
2189
+ console.log(` Cleaned up: ${safeDeleted} deprecated file(s)`);
2190
+ }
2191
+ if (configSectionsAppended > 0) {
2192
+ console.log(` Config sections added: ${configSectionsAppended}`);
2193
+ }
2194
+ if (backupDir) {
2195
+ console.log(` Backup: ${path.relative(cwd, backupDir)}/`);
2196
+ }
2197
+ const actionWord = isDowngrade ? "Downgrade" : "Update";
2198
+ console.log(chalk.green(`\n✅ ${actionWord} complete! (${projectVersion} → ${cliVersion})`));
2199
+ if (createdNew > 0) {
2200
+ console.log(chalk.gray("\nTip: Review .new files and merge changes manually if needed."));
2201
+ }
2202
+ // Create migration task if there are breaking changes with migration guides
2203
+ if (cliVsProject > 0 && projectVersion !== "unknown") {
2204
+ const metadata = getMigrationMetadata(projectVersion, cliVersion);
2205
+ if (metadata.breaking && metadata.migrationGuides.length > 0) {
2206
+ // Create task directory
2207
+ const today = new Date();
2208
+ const monthDay = `${String(today.getMonth() + 1).padStart(2, "0")}-${String(today.getDate()).padStart(2, "0")}`;
2209
+ const taskSlug = `migrate-to-${cliVersion}`;
2210
+ const taskDirName = `${monthDay}-${taskSlug}`;
2211
+ const tasksDir = path.join(cwd, DIR_NAMES.WORKFLOW, DIR_NAMES.TASKS);
2212
+ const taskDir = path.join(tasksDir, taskDirName);
2213
+ // Check if task already exists
2214
+ if (!fs.existsSync(taskDir)) {
2215
+ fs.mkdirSync(taskDir, { recursive: true });
2216
+ // Get current developer for assignee.
2217
+ // `.developer` is a key=value file (written by init_developer.py):
2218
+ // name=<developer-name>
2219
+ // initialized_at=<iso8601>
2220
+ // Reading it raw and .trim()-ing embeds the entire file contents
2221
+ // (including the `name=` prefix and the `initialized_at` line) into
2222
+ // the assignee field, producing bogus assignees like
2223
+ // "name=suyuan\ninitialized_at=2026-04-07T23:41:21.978312" that
2224
+ // later break session-start task rendering.
2225
+ const developerFile = path.join(cwd, DIR_NAMES.WORKFLOW, ".developer");
2226
+ let currentDeveloper = "unknown";
2227
+ if (fs.existsSync(developerFile)) {
2228
+ const raw = fs.readFileSync(developerFile, "utf-8");
2229
+ const nameMatch = raw.match(/^\s*name\s*=\s*(.+?)\s*$/m);
2230
+ if (nameMatch) {
2231
+ currentDeveloper = nameMatch[1];
2232
+ }
2222
2233
  }
2223
- }
2224
- // Build task.json — canonical 24-field shape via shared factory.
2225
- const taskTitle = `Migrate to v${cliVersion}`;
2226
- const todayStr = today.toISOString().split("T")[0];
2227
- const taskJson = emptyTaskJson({
2228
- id: taskSlug,
2229
- name: taskSlug,
2230
- title: taskTitle,
2231
- description: `Breaking change migration from v${projectVersion} to v${cliVersion}`,
2232
- status: "planning",
2233
- scope: "migration",
2234
- priority: "P1",
2235
- creator: "trellis-update",
2236
- assignee: currentDeveloper,
2237
- createdAt: todayStr,
2238
- });
2239
- // Write task.json
2240
- const taskJsonPath = path.join(taskDir, "task.json");
2241
- fs.writeFileSync(taskJsonPath, JSON.stringify(taskJson, null, 2));
2242
- // Build PRD content
2243
- let prdContent = `# Migration Task: Upgrade to v${cliVersion}\n\n`;
2244
- prdContent += `**Created**: ${todayStr}\n`;
2245
- prdContent += `**From Version**: ${projectVersion}\n`;
2246
- prdContent += `**To Version**: ${cliVersion}\n`;
2247
- prdContent += `**Assignee**: ${currentDeveloper}\n\n`;
2248
- prdContent += `## Status\n\n- [ ] Review migration guide\n- [ ] Update custom files\n- [ ] Run \`trellis update --migrate\`\n- [ ] Test workflows\n\n`;
2249
- for (const { version, guide, aiInstructions, } of metadata.migrationGuides) {
2250
- prdContent += `---\n\n## v${version} Migration Guide\n\n`;
2251
- prdContent += guide;
2252
- prdContent += "\n\n";
2253
- if (aiInstructions) {
2254
- prdContent += `### AI Assistant Instructions\n\n`;
2255
- prdContent += `When helping with this migration:\n\n`;
2256
- prdContent += aiInstructions;
2234
+ // Build task.json — canonical 24-field shape via shared factory.
2235
+ const taskTitle = `Migrate to v${cliVersion}`;
2236
+ const todayStr = today.toISOString().split("T")[0];
2237
+ const taskJson = emptyTaskJson({
2238
+ id: taskSlug,
2239
+ name: taskSlug,
2240
+ title: taskTitle,
2241
+ description: `Breaking change migration from v${projectVersion} to v${cliVersion}`,
2242
+ status: "planning",
2243
+ scope: "migration",
2244
+ priority: "P1",
2245
+ creator: "trellis-update",
2246
+ assignee: currentDeveloper,
2247
+ createdAt: todayStr,
2248
+ });
2249
+ // Write task.json
2250
+ const taskJsonPath = path.join(taskDir, "task.json");
2251
+ fs.writeFileSync(taskJsonPath, JSON.stringify(taskJson, null, 2));
2252
+ // Build PRD content
2253
+ let prdContent = `# Migration Task: Upgrade to v${cliVersion}\n\n`;
2254
+ prdContent += `**Created**: ${todayStr}\n`;
2255
+ prdContent += `**From Version**: ${projectVersion}\n`;
2256
+ prdContent += `**To Version**: ${cliVersion}\n`;
2257
+ prdContent += `**Assignee**: ${currentDeveloper}\n\n`;
2258
+ prdContent += `## Status\n\n- [ ] Review migration guide\n- [ ] Update custom files\n- [ ] Run \`trellis update --migrate\`\n- [ ] Test workflows\n\n`;
2259
+ for (const { version, guide, aiInstructions, } of metadata.migrationGuides) {
2260
+ prdContent += `---\n\n## v${version} Migration Guide\n\n`;
2261
+ prdContent += guide;
2257
2262
  prdContent += "\n\n";
2263
+ if (aiInstructions) {
2264
+ prdContent += `### AI Assistant Instructions\n\n`;
2265
+ prdContent += `When helping with this migration:\n\n`;
2266
+ prdContent += aiInstructions;
2267
+ prdContent += "\n\n";
2268
+ }
2258
2269
  }
2270
+ // Write PRD
2271
+ const prdPath = path.join(taskDir, "prd.md");
2272
+ fs.writeFileSync(prdPath, prdContent);
2273
+ console.log("");
2274
+ console.log(chalk.bgCyan.black.bold(" 📋 MIGRATION TASK CREATED "));
2275
+ console.log(chalk.cyan(`A task has been created to help you complete the migration:`));
2276
+ console.log(chalk.white(` ${DIR_NAMES.WORKFLOW}/${DIR_NAMES.TASKS}/${taskDirName}/`));
2277
+ console.log("");
2278
+ console.log(chalk.gray("Use AI to help: Ask Claude/Cursor to read the task and fix your custom files."));
2259
2279
  }
2260
- // Write PRD
2261
- const prdPath = path.join(taskDir, "prd.md");
2262
- fs.writeFileSync(prdPath, prdContent);
2263
- console.log("");
2264
- console.log(chalk.bgCyan.black.bold(" 📋 MIGRATION TASK CREATED "));
2265
- console.log(chalk.cyan(`A task has been created to help you complete the migration:`));
2266
- console.log(chalk.white(` ${DIR_NAMES.WORKFLOW}/${DIR_NAMES.TASKS}/${taskDirName}/`));
2267
- console.log("");
2268
- console.log(chalk.gray("Use AI to help: Ask Claude/Cursor to read the task and fix your custom files."));
2269
2280
  }
2270
2281
  }
2271
- }
2272
- if (zcodeConfigured)
2273
- printZcodeSetupHint();
2274
- // Display breaking change warnings at the very end (so they don't scroll off screen)
2275
- if (cliVsProject > 0 && projectVersion !== "unknown") {
2276
- const finalMetadata = getMigrationMetadata(projectVersion, cliVersion);
2277
- if (finalMetadata.breaking || finalMetadata.changelog.length > 0) {
2278
- console.log("");
2279
- console.log(chalk.cyan("═".repeat(60)));
2280
- if (finalMetadata.breaking) {
2281
- console.log(chalk.bgRed.white.bold(" ⚠️ BREAKING CHANGES ") +
2282
- chalk.red.bold(" This update contains breaking changes!"));
2282
+ if (zcodeConfigured)
2283
+ printZcodeSetupHint();
2284
+ // Display breaking change warnings at the very end (so they don't scroll off screen)
2285
+ if (cliVsProject > 0 && projectVersion !== "unknown") {
2286
+ const finalMetadata = getMigrationMetadata(projectVersion, cliVersion);
2287
+ if (finalMetadata.breaking || finalMetadata.changelog.length > 0) {
2283
2288
  console.log("");
2284
- }
2285
- if (finalMetadata.changelog.length > 0) {
2286
- console.log(chalk.cyan.bold("📋 What's Changed:"));
2287
- for (const entry of finalMetadata.changelog) {
2288
- console.log(chalk.white(` ${entry}`));
2289
+ console.log(chalk.cyan("═".repeat(60)));
2290
+ if (finalMetadata.breaking) {
2291
+ console.log(chalk.bgRed.white.bold(" ⚠️ BREAKING CHANGES ") +
2292
+ chalk.red.bold(" This update contains breaking changes!"));
2293
+ console.log("");
2289
2294
  }
2290
- console.log("");
2291
- }
2292
- if (finalMetadata.recommendMigrate && !options.migrate) {
2293
- console.log(chalk.bgGreen.black.bold(" 💡 RECOMMENDED ") +
2294
- chalk.green.bold(" Run with --migrate to complete the migration"));
2295
- console.log(chalk.gray(" This will remove legacy files and apply all changes."));
2296
- console.log("");
2295
+ if (finalMetadata.changelog.length > 0) {
2296
+ console.log(chalk.cyan.bold("📋 What's Changed:"));
2297
+ for (const entry of finalMetadata.changelog) {
2298
+ console.log(chalk.white(` ${entry}`));
2299
+ }
2300
+ console.log("");
2301
+ }
2302
+ if (finalMetadata.recommendMigrate && !options.migrate) {
2303
+ console.log(chalk.bgGreen.black.bold(" 💡 RECOMMENDED ") +
2304
+ chalk.green.bold(" Run with --migrate to complete the migration"));
2305
+ console.log(chalk.gray(" This will remove legacy files and apply all changes."));
2306
+ console.log("");
2307
+ }
2308
+ console.log(chalk.cyan("═".repeat(60)));
2297
2309
  }
2298
- console.log(chalk.cyan("═".repeat(60)));
2299
2310
  }
2300
2311
  }
2312
+ finally {
2313
+ fs.closeSync(backupLock);
2314
+ fs.rmSync(backupLockPath, { force: true });
2315
+ }
2301
2316
  }
2302
2317
  //# sourceMappingURL=update.js.map