@pennixrv/trellis 0.7.1-beta.8 → 0.7.1-beta.9

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.
Files changed (56) hide show
  1. package/README.md +10 -7
  2. package/dist/cli/index.d.ts.map +1 -1
  3. package/dist/cli/index.js +2 -0
  4. package/dist/cli/index.js.map +1 -1
  5. package/dist/commands/ablate.d.ts.map +1 -1
  6. package/dist/commands/ablate.js +2 -0
  7. package/dist/commands/ablate.js.map +1 -1
  8. package/dist/commands/backup.d.ts +4 -0
  9. package/dist/commands/backup.d.ts.map +1 -0
  10. package/dist/commands/backup.js +75 -0
  11. package/dist/commands/backup.js.map +1 -0
  12. package/dist/commands/init.d.ts.map +1 -1
  13. package/dist/commands/init.js +675 -672
  14. package/dist/commands/init.js.map +1 -1
  15. package/dist/commands/mem.d.ts.map +1 -1
  16. package/dist/commands/mem.js +49 -7
  17. package/dist/commands/mem.js.map +1 -1
  18. package/dist/commands/uninstall.d.ts.map +1 -1
  19. package/dist/commands/uninstall.js +92 -84
  20. package/dist/commands/uninstall.js.map +1 -1
  21. package/dist/commands/update.d.ts.map +1 -1
  22. package/dist/commands/update.js +587 -606
  23. package/dist/commands/update.js.map +1 -1
  24. package/dist/commands/workflow.d.ts.map +1 -1
  25. package/dist/commands/workflow.js +94 -89
  26. package/dist/commands/workflow.js.map +1 -1
  27. package/dist/configurators/shared.js +2 -2
  28. package/dist/configurators/shared.js.map +1 -1
  29. package/dist/migrations/manifests/0.7.1-beta.9.json +191 -0
  30. package/dist/templates/common/bundled-skills/trellis-decision-space-construction-and-merging/workflow.md +2 -0
  31. package/dist/templates/common/bundled-skills/trellis-goal-exploration-and-requirements/workflow.md +2 -0
  32. package/dist/templates/common/bundled-skills/trellis-session-insight/references/cli-quick-reference.md +2 -1
  33. package/dist/templates/common/bundled-skills/trellis-session-insight/references/triggering-patterns.md +2 -2
  34. package/dist/templates/common/bundled-skills/trellis-solution-design-and-convergence/workflow.md +4 -0
  35. package/dist/templates/common/bundled-skills/trellis-spec-bootstrap/references/mcp-setup.md +4 -2
  36. package/dist/templates/common/bundled-skills/trellis-spec-bootstrap/references/spec-task-planning.md +2 -2
  37. package/dist/templates/common/bundled-skills/trellis-teamwork/SKILL.md +1 -1
  38. package/dist/templates/common/commands/start.md +2 -2
  39. package/dist/templates/common/skills/before-dev.md +1 -4
  40. package/dist/templates/common/skills/break-loop.md +7 -18
  41. package/dist/templates/common/skills/check.md +7 -11
  42. package/dist/templates/common/skills/{brainstorm.md → request-intake.md} +5 -3
  43. package/dist/templates/common/skills/update-spec.md +5 -7
  44. package/dist/utils/ablation-store.d.ts +1 -1
  45. package/dist/utils/ablation-store.d.ts.map +1 -1
  46. package/dist/utils/ablation-store.js +58 -24
  47. package/dist/utils/ablation-store.js.map +1 -1
  48. package/dist/utils/asset-backup.d.ts +138 -0
  49. package/dist/utils/asset-backup.d.ts.map +1 -0
  50. package/dist/utils/asset-backup.js +507 -0
  51. package/dist/utils/asset-backup.js.map +1 -0
  52. package/dist/utils/atomic-write.d.ts +1 -1
  53. package/dist/utils/atomic-write.d.ts.map +1 -1
  54. package/dist/utils/atomic-write.js +6 -3
  55. package/dist/utils/atomic-write.js.map +1 -1
  56. package/package.json +2 -2
@@ -11,13 +11,14 @@ import { compareVersions } from "../utils/compare-versions.js";
11
11
  import { toPosix } from "../utils/posix.js";
12
12
  import { setupProxy } from "../utils/proxy.js";
13
13
  import { emptyTaskJson } from "../utils/task-json.js";
14
+ import { assetBackupScope, bindAssetUpdate, createAssetBackup, withAssetMaintenance, } from "../utils/asset-backup.js";
14
15
  // Import templates for comparison
15
16
  import { getAllScripts, getAllAgents,
16
17
  // Configuration
17
18
  configYamlTemplate, gitignoreTemplate, workflowMdTemplate, } from "../templates/trellis/index.js";
18
19
  import { agentsMdContent } from "../templates/markdown/index.js";
19
20
  import { COPILOT_INSTRUCTIONS_BLOCK_END, COPILOT_INSTRUCTIONS_BLOCK_START, COPILOT_INSTRUCTIONS_PATH, getCopilotInstructions, } from "../templates/copilot/index.js";
20
- import { ALL_MANAGED_DIRS, getConfiguredPlatforms, collectPlatformTemplates, } from "../configurators/index.js";
21
+ import { getConfiguredPlatforms, collectPlatformTemplates, } from "../configurators/index.js";
21
22
  import { replacePythonCommandLiterals } from "../configurators/shared.js";
22
23
  import { preserveCodexAgentModelKeys } from "../configurators/codex.js";
23
24
  import { printZcodeSetupHint } from "../configurators/zcode.js";
@@ -886,30 +887,6 @@ async function promptConflictResolution(file, options, applyToAll) {
886
887
  }
887
888
  return action;
888
889
  }
889
- /**
890
- * Create a timestamped backup directory path
891
- */
892
- function createBackupDirPath(cwd) {
893
- const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
894
- return path.join(cwd, DIR_NAMES.WORKFLOW, `.backup-${timestamp}`);
895
- }
896
- /**
897
- * Backup a single file to the backup directory
898
- */
899
- function backupFile(cwd, backupDir, relativePath) {
900
- const srcPath = path.join(cwd, relativePath);
901
- if (!fs.existsSync(srcPath))
902
- return;
903
- const backupPath = path.join(backupDir, relativePath);
904
- fs.mkdirSync(path.dirname(backupPath), { recursive: true });
905
- fs.copyFileSync(srcPath, backupPath);
906
- }
907
- /**
908
- * Directories to backup as complete snapshot (derived from platform registry)
909
- */
910
- const BACKUP_DIRS = ALL_MANAGED_DIRS;
911
- /** Root-level managed files to include in update backups. */
912
- const BACKUP_FILES = [FILE_NAMES.AGENTS];
913
890
  /**
914
891
  * Patterns to exclude from backup (user data that shouldn't be backed up)
915
892
  */
@@ -921,6 +898,8 @@ const BACKUP_EXCLUDE_PATTERNS = [
921
898
  "/spec/", // Spec files (user-customized content)
922
899
  "/backlog/", // Backlog data (user data)
923
900
  "/agent-traces/", // Agent traces (user data, legacy name)
901
+ "/session-handoffs/",
902
+ "/sessions/",
924
903
  // Platform-native worktree dirs — these are full sub-repos the CLI
925
904
  // spawns for parallel sessions. Backing them up on every update would
926
905
  // snapshot the entire nested working tree. Confirmed conventions:
@@ -952,44 +931,11 @@ export function shouldExcludeFromBackup(relativePath) {
952
931
  return false;
953
932
  }
954
933
  /**
955
- * Create complete snapshot backup of all managed directories
956
- * Backs up all managed platform/workflow directories entirely
957
- * (excluding user data like workspace/, tasks/, backlog/)
934
+ * Snapshot only positively owned assets and the current update scope.
958
935
  */
959
- function createFullBackup(cwd) {
960
- const backupDir = createBackupDirPath(cwd);
961
- let hasFiles = false;
962
- for (const dir of BACKUP_DIRS) {
963
- const dirPath = path.join(cwd, dir);
964
- if (!fs.existsSync(dirPath))
965
- continue;
966
- const files = collectAllFiles(dirPath, cwd);
967
- for (const fullPath of files) {
968
- const relativePath = path.relative(cwd, fullPath);
969
- // Skip excluded paths
970
- if (shouldExcludeFromBackup(relativePath))
971
- continue;
972
- // Create backup
973
- if (!hasFiles) {
974
- fs.mkdirSync(backupDir, { recursive: true });
975
- hasFiles = true;
976
- }
977
- backupFile(cwd, backupDir, relativePath);
978
- }
979
- }
980
- for (const relativePath of BACKUP_FILES) {
981
- const fullPath = path.join(cwd, relativePath);
982
- if (!fs.existsSync(fullPath))
983
- continue;
984
- if (shouldExcludeFromBackup(relativePath))
985
- continue;
986
- if (!hasFiles) {
987
- fs.mkdirSync(backupDir, { recursive: true });
988
- hasFiles = true;
989
- }
990
- backupFile(cwd, backupDir, relativePath);
991
- }
992
- return hasFiles ? backupDir : null;
936
+ function createFullBackup(cwd, scope) {
937
+ const backup = createAssetBackup(cwd, "update", assetBackupScope(cwd, scope));
938
+ return path.join(cwd, DIR_NAMES.WORKFLOW, backup.id);
993
939
  }
994
940
  /**
995
941
  * Update version file
@@ -1628,597 +1574,632 @@ export function renameTracesToJournal(workspaceDir) {
1628
1574
  */
1629
1575
  export async function update(options) {
1630
1576
  const cwd = process.cwd();
1631
- // Check if Trellis is initialized
1632
- if (!fs.existsSync(path.join(cwd, DIR_NAMES.WORKFLOW))) {
1633
- console.log(chalk.red("Error: Trellis not initialized in this directory."));
1634
- console.log(chalk.gray("Run 'trellis init' first."));
1635
- return;
1636
- }
1637
- console.log(chalk.cyan("\nTrellis Update"));
1638
- console.log(chalk.cyan("══════════════\n"));
1639
- // Set up proxy before any network calls (npm version check)
1640
- setupProxy();
1641
- // Get versions
1642
- const projectVersion = getInstalledVersion(cwd);
1643
- const cliVersion = VERSION;
1644
- const latestNpmVersion = await getLatestNpmVersion();
1645
- // Version comparison
1646
- const cliVsProject = compareVersions(cliVersion, projectVersion);
1647
- const cliVsNpm = latestNpmVersion
1648
- ? compareVersions(cliVersion, latestNpmVersion)
1649
- : 0;
1650
- // Display versions with context
1651
- console.log(`Project version: ${chalk.white(projectVersion)}`);
1652
- console.log(`CLI version: ${chalk.white(cliVersion)}`);
1653
- if (latestNpmVersion) {
1654
- console.log(`Latest on npm: ${chalk.white(latestNpmVersion)}`);
1655
- }
1656
- else {
1657
- console.log(chalk.gray("Latest on npm: (unable to fetch)"));
1658
- }
1659
- console.log("");
1660
- // Check if CLI is outdated compared to npm
1661
- if (cliVsNpm < 0 && latestNpmVersion) {
1662
- console.log(chalk.yellow(`⚠️ Your CLI (${cliVersion}) is behind npm (${latestNpmVersion}).`));
1663
- console.log(chalk.yellow(` Run: trellis upgrade\n`));
1664
- }
1665
- // Check for downgrade situation
1666
- if (cliVsProject < 0) {
1667
- console.log(chalk.red(`❌ Cannot update: CLI version (${cliVersion}) < project version (${projectVersion})`));
1668
- console.log(chalk.red(` This would DOWNGRADE your project!\n`));
1669
- if (!options.allowDowngrade) {
1670
- console.log(chalk.gray("Solutions:"));
1671
- console.log(chalk.gray(` 1. Update your CLI: trellis upgrade`));
1672
- console.log(chalk.gray(` 2. Force downgrade: trellis update --allow-downgrade\n`));
1577
+ return withAssetMaintenance(cwd, async () => {
1578
+ // Check if Trellis is initialized
1579
+ if (!fs.existsSync(path.join(cwd, DIR_NAMES.WORKFLOW))) {
1580
+ console.log(chalk.red("Error: Trellis not initialized in this directory."));
1581
+ console.log(chalk.gray("Run 'trellis init' first."));
1673
1582
  return;
1674
1583
  }
1675
- console.log(chalk.yellow("⚠️ --allow-downgrade flag set. Proceeding with downgrade...\n"));
1676
- }
1677
- // Migration metadata is displayed at the end to prevent scrolling off screen
1678
- // Load template hashes for modification detection
1679
- let hashes = loadHashes(cwd);
1680
- const zcodeConfigured = getConfiguredPlatforms(cwd).has("zcode");
1681
- const isFirstHashTracking = Object.keys(hashes).length === 0;
1682
- // Handle unknown version - skip regular migrations but safe-file-delete still runs
1683
- const isUnknownVersion = projectVersion === "unknown";
1684
- if (isUnknownVersion) {
1685
- console.log(chalk.yellow("⚠️ No version file found. Skipping migrations — run trellis init to fix."));
1686
- console.log(chalk.gray(" Template updates will still be applied."));
1687
- console.log(chalk.gray(" Safe file cleanup will still run (hash-verified).\n"));
1688
- }
1689
- // Detect legacy Codex (has .agents/skills/ tracked by Trellis but no .codex/)
1690
- // NOTE: this MUST happen before pruneOrphanManifestKeys below, since the
1691
- // detector reads the raw manifest looking for .agents/skills/ markers that
1692
- // the prune step would otherwise consider orphans (codex hasn't been added
1693
- // to configuredPlatforms yet at this point).
1694
- const codexUpgradeNeeded = needsCodexUpgrade(cwd);
1695
- if (codexUpgradeNeeded) {
1696
- console.log(chalk.yellow(" Legacy Codex detected: .agents/skills/ tracked without .codex/ — will create .codex/ directory"));
1697
- }
1698
- // Self-heal poisoned manifests: prune entries that no current platform
1699
- // configurator owns. This silently removes user-owned paths that early
1700
- // buggy versions of `trellis init` over-hashed (e.g. .codex/sessions/*).
1701
- // Include codex in known-platforms when codexUpgradeNeeded so legacy Codex
1702
- // markers under .agents/skills/ survive into the upgrade flow.
1703
- {
1704
- const configuredPlatforms = new Set(getConfiguredPlatforms(cwd));
1705
- if (codexUpgradeNeeded)
1706
- configuredPlatforms.add("codex");
1707
- const prune = pruneOrphanManifestKeys(cwd, [...configuredPlatforms], hashes, { persist: !options.dryRun });
1708
- if (prune.pruned.length > 0) {
1709
- console.log(chalk.gray(` Pruned ${prune.pruned.length} orphan manifest entries from .template-hashes.json`));
1710
- hashes = prune.hashes;
1711
- }
1712
- }
1713
- // For breaking releases with recommendMigrate + --migrate, bypass update.skip
1714
- // across the board (safe-file-delete, new file writes, template updates).
1715
- // Why: honoring skip here leaves users forever half-migrated — old deprecated
1716
- // files persist under skip-protected paths, new commands like `continue.md`
1717
- // never land, and every future update re-flags the same mess. Rename
1718
- // migrations already ignore update.skip; this makes the rest consistent
1719
- // during a breaking upgrade. User customizations are still guarded by the
1720
- // per-file conflict prompt ("Modified by you") at write time.
1721
- const breakingBypass = options.migrate === true &&
1722
- cliVsProject > 0 &&
1723
- projectVersion !== "unknown" &&
1724
- (() => {
1725
- const md = getMigrationMetadata(projectVersion, cliVersion);
1726
- return md.breaking && md.recommendMigrate;
1727
- })();
1728
- // Collect templates (used for both migration classification and change analysis)
1729
- const templates = await collectTemplateFiles(cwd, codexUpgradeNeeded ? new Set(["codex"]) : undefined, breakingBypass);
1730
- // Load update.skip paths (used for both safe-file-delete and template collection)
1731
- const skipPaths = loadUpdateSkipPaths(cwd);
1732
- // Collect safe-file-delete items from ALL manifests (hash match is the safety net)
1733
- // This runs regardless of version — unknown version still gets safe cleanup
1734
- const allMigrations = getAllMigrations();
1735
- const safeFileDeletes = collectSafeFileDeletes(allMigrations, cwd, skipPaths, new Set(templates.keys()), breakingBypass);
1736
- const hasSafeDeletes = safeFileDeletes.filter((c) => c.action === "delete").length > 0;
1737
- // Check for pending regular migrations (skip if unknown version)
1738
- let pendingMigrations = isUnknownVersion
1739
- ? []
1740
- : getMigrationsForVersion(projectVersion, cliVersion);
1741
- // Also check for "orphaned" migrations - where source still exists but version says we shouldn't migrate
1742
- // This handles cases where version was updated but migrations weren't applied
1743
- const orphanedMigrations = allMigrations.filter((item) => {
1744
- // Only check rename and rename-dir migrations
1745
- if (item.type !== "rename" && item.type !== "rename-dir")
1746
- return false;
1747
- if (!item.from || !item.to)
1748
- return false;
1749
- const oldPath = path.join(cwd, item.from);
1750
- const newPath = path.join(cwd, item.to);
1751
- // Orphaned if: source exists AND target doesn't exist
1752
- // AND this migration isn't already in pendingMigrations
1753
- const sourceExists = fs.existsSync(oldPath);
1754
- const targetExists = fs.existsSync(newPath);
1755
- const alreadyPending = pendingMigrations.some((m) => m.from === item.from && m.to === item.to);
1756
- return sourceExists && !targetExists && !alreadyPending;
1757
- });
1758
- // Add orphaned migrations to pending (they need to be applied)
1759
- if (orphanedMigrations.length > 0) {
1760
- console.log(chalk.yellow("⚠️ Detected incomplete migrations from previous updates:"));
1761
- for (const item of orphanedMigrations) {
1762
- console.log(chalk.yellow(` ${item.from} → ${item.to}`));
1584
+ console.log(chalk.cyan("\nTrellis Update"));
1585
+ console.log(chalk.cyan("══════════════\n"));
1586
+ // Set up proxy before any network calls (npm version check)
1587
+ setupProxy();
1588
+ // Get versions
1589
+ const projectVersion = getInstalledVersion(cwd);
1590
+ const cliVersion = VERSION;
1591
+ const latestNpmVersion = await getLatestNpmVersion();
1592
+ // Version comparison
1593
+ const cliVsProject = compareVersions(cliVersion, projectVersion);
1594
+ const cliVsNpm = latestNpmVersion
1595
+ ? compareVersions(cliVersion, latestNpmVersion)
1596
+ : 0;
1597
+ // Display versions with context
1598
+ console.log(`Project version: ${chalk.white(projectVersion)}`);
1599
+ console.log(`CLI version: ${chalk.white(cliVersion)}`);
1600
+ if (latestNpmVersion) {
1601
+ console.log(`Latest on npm: ${chalk.white(latestNpmVersion)}`);
1602
+ }
1603
+ else {
1604
+ console.log(chalk.gray("Latest on npm: (unable to fetch)"));
1763
1605
  }
1764
1606
  console.log("");
1765
- pendingMigrations = [...pendingMigrations, ...orphanedMigrations];
1766
- }
1767
- const hasMigrations = pendingMigrations.length > 0;
1768
- // Classify migrations (stored for later backup creation)
1769
- let classifiedMigrations = null;
1770
- if (hasMigrations) {
1771
- console.log(chalk.cyan("Analyzing migrations...\n"));
1772
- classifiedMigrations = classifyMigrations(pendingMigrations, cwd, hashes, templates);
1773
- printMigrationSummary(classifiedMigrations);
1774
- // Hard-stop: pending rename/delete work from a breaking release requires --migrate.
1775
- // Why: without --migrate, those entries are skipped and update()'s later path silently
1776
- // bumps the version stamp, leaving old paths orphaned next to new templates. Force
1777
- // explicit opt-in so the user can't half-migrate by accident.
1778
- const pendingMigrationCount = classifiedMigrations.auto.length +
1779
- classifiedMigrations.confirm.length +
1780
- classifiedMigrations.conflict.length;
1781
- if (pendingMigrationCount > 0 &&
1782
- !options.migrate &&
1783
- !options.dryRun &&
1784
- cliVsProject > 0 &&
1785
- projectVersion !== "unknown") {
1786
- const gateMetadata = getMigrationMetadata(projectVersion, cliVersion);
1787
- if (gateMetadata.breaking && gateMetadata.recommendMigrate) {
1788
- console.log(chalk.bgRed.white.bold(" ✖ MIGRATION REQUIRED ") +
1789
- chalk.red(` Breaking changes between ${projectVersion} → ${cliVersion} require --migrate.`));
1790
- console.log("");
1791
- console.log(chalk.yellow(` Run: trellis update --migrate`));
1792
- console.log("");
1793
- console.log(chalk.gray(" Without --migrate, renamed/relocated files from breaking releases aren't moved,\n" +
1794
- " leaving your project with stale paths alongside new templates.\n" +
1795
- " Use --dry-run to preview what --migrate will do."));
1796
- process.exit(1);
1607
+ // Check if CLI is outdated compared to npm
1608
+ if (cliVsNpm < 0 && latestNpmVersion) {
1609
+ console.log(chalk.yellow(`⚠️ Your CLI (${cliVersion}) is behind npm (${latestNpmVersion}).`));
1610
+ console.log(chalk.yellow(` Run: trellis upgrade\n`));
1611
+ }
1612
+ // Check for downgrade situation
1613
+ if (cliVsProject < 0) {
1614
+ console.log(chalk.red(`❌ Cannot update: CLI version (${cliVersion}) < project version (${projectVersion})`));
1615
+ console.log(chalk.red(` This would DOWNGRADE your project!\n`));
1616
+ if (!options.allowDowngrade) {
1617
+ console.log(chalk.gray("Solutions:"));
1618
+ console.log(chalk.gray(` 1. Update your CLI: trellis upgrade`));
1619
+ console.log(chalk.gray(` 2. Force downgrade: trellis update --allow-downgrade\n`));
1620
+ return;
1621
+ }
1622
+ console.log(chalk.yellow("⚠️ --allow-downgrade flag set. Proceeding with downgrade...\n"));
1623
+ }
1624
+ // Migration metadata is displayed at the end to prevent scrolling off screen
1625
+ // Load template hashes for modification detection
1626
+ let hashes = loadHashes(cwd);
1627
+ const zcodeConfigured = getConfiguredPlatforms(cwd).has("zcode");
1628
+ const isFirstHashTracking = Object.keys(hashes).length === 0;
1629
+ // Handle unknown version - skip regular migrations but safe-file-delete still runs
1630
+ const isUnknownVersion = projectVersion === "unknown";
1631
+ if (isUnknownVersion) {
1632
+ console.log(chalk.yellow("⚠️ No version file found. Skipping migrations — run trellis init to fix."));
1633
+ console.log(chalk.gray(" Template updates will still be applied."));
1634
+ console.log(chalk.gray(" Safe file cleanup will still run (hash-verified).\n"));
1635
+ }
1636
+ // Detect legacy Codex (has .agents/skills/ tracked by Trellis but no .codex/)
1637
+ // NOTE: this MUST happen before pruneOrphanManifestKeys below, since the
1638
+ // detector reads the raw manifest looking for .agents/skills/ markers that
1639
+ // the prune step would otherwise consider orphans (codex hasn't been added
1640
+ // to configuredPlatforms yet at this point).
1641
+ const codexUpgradeNeeded = needsCodexUpgrade(cwd);
1642
+ if (codexUpgradeNeeded) {
1643
+ console.log(chalk.yellow(" Legacy Codex detected: .agents/skills/ tracked without .codex/ — will create .codex/ directory"));
1644
+ }
1645
+ // Self-heal poisoned manifests: prune entries that no current platform
1646
+ // configurator owns. This silently removes user-owned paths that early
1647
+ // buggy versions of `trellis init` over-hashed (e.g. .codex/sessions/*).
1648
+ // Include codex in known-platforms when codexUpgradeNeeded so legacy Codex
1649
+ // markers under .agents/skills/ survive into the upgrade flow.
1650
+ {
1651
+ const configuredPlatforms = new Set(getConfiguredPlatforms(cwd));
1652
+ if (codexUpgradeNeeded)
1653
+ configuredPlatforms.add("codex");
1654
+ const prune = pruneOrphanManifestKeys(cwd, [...configuredPlatforms], hashes, { persist: !options.dryRun });
1655
+ if (prune.pruned.length > 0) {
1656
+ console.log(chalk.gray(` Pruned ${prune.pruned.length} orphan manifest entries from .template-hashes.json`));
1657
+ hashes = prune.hashes;
1797
1658
  }
1798
1659
  }
1799
- // Soft hint: non-breaking migrations or projects that chose not to set recommendMigrate
1800
- if (!options.migrate) {
1801
- const autoCount = classifiedMigrations.auto.length;
1802
- const confirmCount = classifiedMigrations.confirm.length;
1803
- if (autoCount > 0 || confirmCount > 0) {
1804
- console.log(chalk.gray(`Tip: Use --migrate to apply migrations (prompts for modified files).`));
1805
- if (confirmCount > 0) {
1806
- console.log(chalk.gray(` Use --migrate -f to force all, or --migrate -s to skip modified.\n`));
1807
- }
1808
- else {
1660
+ // For breaking releases with recommendMigrate + --migrate, bypass update.skip
1661
+ // across the board (safe-file-delete, new file writes, template updates).
1662
+ // Why: honoring skip here leaves users forever half-migrated — old deprecated
1663
+ // files persist under skip-protected paths, new commands like `continue.md`
1664
+ // never land, and every future update re-flags the same mess. Rename
1665
+ // migrations already ignore update.skip; this makes the rest consistent
1666
+ // during a breaking upgrade. User customizations are still guarded by the
1667
+ // per-file conflict prompt ("Modified by you") at write time.
1668
+ const breakingBypass = options.migrate === true &&
1669
+ cliVsProject > 0 &&
1670
+ projectVersion !== "unknown" &&
1671
+ (() => {
1672
+ const md = getMigrationMetadata(projectVersion, cliVersion);
1673
+ return md.breaking && md.recommendMigrate;
1674
+ })();
1675
+ // Collect templates (used for both migration classification and change analysis)
1676
+ const templates = await collectTemplateFiles(cwd, codexUpgradeNeeded ? new Set(["codex"]) : undefined, breakingBypass);
1677
+ // Load update.skip paths (used for both safe-file-delete and template collection)
1678
+ const skipPaths = loadUpdateSkipPaths(cwd);
1679
+ // Collect safe-file-delete items from ALL manifests (hash match is the safety net)
1680
+ // This runs regardless of version — unknown version still gets safe cleanup
1681
+ const allMigrations = getAllMigrations();
1682
+ const safeFileDeletes = collectSafeFileDeletes(allMigrations, cwd, skipPaths, new Set(templates.keys()), breakingBypass);
1683
+ const hasSafeDeletes = safeFileDeletes.filter((c) => c.action === "delete").length > 0;
1684
+ // Check for pending regular migrations (skip if unknown version)
1685
+ let pendingMigrations = isUnknownVersion
1686
+ ? []
1687
+ : getMigrationsForVersion(projectVersion, cliVersion);
1688
+ // Also check for "orphaned" migrations - where source still exists but version says we shouldn't migrate
1689
+ // This handles cases where version was updated but migrations weren't applied
1690
+ const orphanedMigrations = allMigrations.filter((item) => {
1691
+ // Only check rename and rename-dir migrations
1692
+ if (item.type !== "rename" && item.type !== "rename-dir")
1693
+ return false;
1694
+ if (!item.from || !item.to)
1695
+ return false;
1696
+ const oldPath = path.join(cwd, item.from);
1697
+ const newPath = path.join(cwd, item.to);
1698
+ // Orphaned if: source exists AND target doesn't exist
1699
+ // AND this migration isn't already in pendingMigrations
1700
+ const sourceExists = fs.existsSync(oldPath);
1701
+ const targetExists = fs.existsSync(newPath);
1702
+ const alreadyPending = pendingMigrations.some((m) => m.from === item.from && m.to === item.to);
1703
+ return sourceExists && !targetExists && !alreadyPending;
1704
+ });
1705
+ // Add orphaned migrations to pending (they need to be applied)
1706
+ if (orphanedMigrations.length > 0) {
1707
+ console.log(chalk.yellow("⚠️ Detected incomplete migrations from previous updates:"));
1708
+ for (const item of orphanedMigrations) {
1709
+ console.log(chalk.yellow(` ${item.from} → ${item.to}`));
1710
+ }
1711
+ console.log("");
1712
+ pendingMigrations = [...pendingMigrations, ...orphanedMigrations];
1713
+ }
1714
+ const hasMigrations = pendingMigrations.length > 0;
1715
+ // Classify migrations (stored for later backup creation)
1716
+ let classifiedMigrations = null;
1717
+ if (hasMigrations) {
1718
+ console.log(chalk.cyan("Analyzing migrations...\n"));
1719
+ classifiedMigrations = classifyMigrations(pendingMigrations, cwd, hashes, templates);
1720
+ printMigrationSummary(classifiedMigrations);
1721
+ // Hard-stop: pending rename/delete work from a breaking release requires --migrate.
1722
+ // Why: without --migrate, those entries are skipped and update()'s later path silently
1723
+ // bumps the version stamp, leaving old paths orphaned next to new templates. Force
1724
+ // explicit opt-in so the user can't half-migrate by accident.
1725
+ const pendingMigrationCount = classifiedMigrations.auto.length +
1726
+ classifiedMigrations.confirm.length +
1727
+ classifiedMigrations.conflict.length;
1728
+ if (pendingMigrationCount > 0 &&
1729
+ !options.migrate &&
1730
+ !options.dryRun &&
1731
+ cliVsProject > 0 &&
1732
+ projectVersion !== "unknown") {
1733
+ const gateMetadata = getMigrationMetadata(projectVersion, cliVersion);
1734
+ if (gateMetadata.breaking && gateMetadata.recommendMigrate) {
1735
+ console.log(chalk.bgRed.white.bold(" ✖ MIGRATION REQUIRED ") +
1736
+ chalk.red(` Breaking changes between ${projectVersion} → ${cliVersion} require --migrate.`));
1737
+ console.log("");
1738
+ console.log(chalk.yellow(` Run: trellis update --migrate`));
1809
1739
  console.log("");
1740
+ console.log(chalk.gray(" Without --migrate, renamed/relocated files from breaking releases aren't moved,\n" +
1741
+ " leaving your project with stale paths alongside new templates.\n" +
1742
+ " Use --dry-run to preview what --migrate will do."));
1743
+ process.exit(1);
1810
1744
  }
1811
1745
  }
1812
- }
1813
- }
1814
- // Print safe-file-delete summary (always shown, runs without --migrate)
1815
- if (safeFileDeletes.length > 0) {
1816
- printSafeFileDeleteSummary(safeFileDeletes);
1817
- }
1818
- // Analyze changes (pass hashes for modification detection)
1819
- const changes = analyzeChanges(cwd, hashes, templates);
1820
- const unchangedFileHashRepairs = collectUnchangedFileHashRepairs(changes, hashes);
1821
- // Print summary
1822
- printChangeSummary(changes);
1823
- // First-time hash tracking hint
1824
- if (isFirstHashTracking && changes.changedFiles.length > 0) {
1825
- console.log(chalk.cyan("ℹ️ First update with hash tracking enabled."));
1826
- console.log(chalk.gray(" Changed files shown above may not be actual user modifications."));
1827
- console.log(chalk.gray(" After this update, hash tracking will accurately detect changes.\n"));
1828
- }
1829
- // Ensure project-root .gitattributes carries the journal merge=union rule.
1830
- // Additive-only (see ensureGitattributes) — runs regardless of whether
1831
- // other template files changed, so it must sit before the "nothing to do"
1832
- // early-return below. Never touches disk in --dry-run.
1833
- if (!options.dryRun) {
1834
- ensureGitattributes(cwd);
1835
- }
1836
- // Check if there's anything to do
1837
- const isUpgrade = cliVsProject > 0;
1838
- const isDowngrade = cliVsProject < 0;
1839
- const isSameVersion = cliVsProject === 0;
1840
- // Check if we have pending migrations that need to be applied
1841
- const hasPendingMigrations = options.migrate &&
1842
- classifiedMigrations &&
1843
- (classifiedMigrations.auto.length > 0 ||
1844
- classifiedMigrations.confirm.length > 0);
1845
- if (changes.newFiles.length === 0 &&
1846
- changes.autoUpdateFiles.length === 0 &&
1847
- changes.changedFiles.length === 0 &&
1848
- !hasPendingMigrations &&
1849
- !hasSafeDeletes) {
1850
- // The "already up to date" exit still has to repair the receipt: this is
1851
- // exactly the clean tree where every file is `unchanged`, so it is the run
1852
- // where a wrong entry would otherwise be skipped again.
1853
- if (!options.dryRun && unchangedFileHashRepairs.size > 0) {
1854
- updateHashes(cwd, unchangedFileHashRepairs);
1855
- }
1856
- if (isSameVersion) {
1857
- console.log(chalk.green("✓ Already up to date!"));
1858
- }
1859
- else {
1860
- // Version changed but no file changes needed — still update the version stamp
1861
- updateVersionFile(cwd);
1862
- if (isUpgrade) {
1863
- console.log(chalk.green(`✓ No file changes needed for ${projectVersion} → ${cliVersion}`));
1864
- }
1865
- else if (isDowngrade) {
1866
- console.log(chalk.green(`✓ No file changes needed for ${projectVersion} → ${cliVersion} (downgrade)`));
1746
+ // Soft hint: non-breaking migrations or projects that chose not to set recommendMigrate
1747
+ if (!options.migrate) {
1748
+ const autoCount = classifiedMigrations.auto.length;
1749
+ const confirmCount = classifiedMigrations.confirm.length;
1750
+ if (autoCount > 0 || confirmCount > 0) {
1751
+ console.log(chalk.gray(`Tip: Use --migrate to apply migrations (prompts for modified files).`));
1752
+ if (confirmCount > 0) {
1753
+ console.log(chalk.gray(` Use --migrate -f to force all, or --migrate -s to skip modified.\n`));
1754
+ }
1755
+ else {
1756
+ console.log("");
1757
+ }
1758
+ }
1867
1759
  }
1868
1760
  }
1869
- if (zcodeConfigured)
1870
- printZcodeSetupHint();
1871
- return;
1872
- }
1873
- // Show what this operation will do
1874
- if (isUpgrade) {
1875
- console.log(chalk.green(`This will UPGRADE: ${projectVersion} → ${cliVersion}\n`));
1876
- }
1877
- else if (isDowngrade) {
1878
- console.log(chalk.red(`⚠️ This will DOWNGRADE: ${projectVersion} → ${cliVersion}\n`));
1879
- }
1880
- // Show breaking change warning before confirm
1881
- if (cliVsProject > 0 && projectVersion !== "unknown") {
1882
- const preConfirmMetadata = getMigrationMetadata(projectVersion, cliVersion);
1883
- if (preConfirmMetadata.breaking) {
1884
- console.log(chalk.cyan("═".repeat(60)));
1885
- console.log(chalk.bgRed.white.bold(" ⚠️ BREAKING CHANGES ") +
1886
- chalk.red.bold(" Review the changes above carefully!"));
1887
- if (preConfirmMetadata.changelog.length > 0) {
1888
- console.log("");
1889
- console.log(chalk.white(preConfirmMetadata.changelog[0]));
1761
+ // Print safe-file-delete summary (always shown, runs without --migrate)
1762
+ if (safeFileDeletes.length > 0) {
1763
+ printSafeFileDeleteSummary(safeFileDeletes);
1764
+ }
1765
+ // Analyze changes (pass hashes for modification detection)
1766
+ const changes = analyzeChanges(cwd, hashes, templates);
1767
+ const unchangedFileHashRepairs = collectUnchangedFileHashRepairs(changes, hashes);
1768
+ // Print summary
1769
+ printChangeSummary(changes);
1770
+ // First-time hash tracking hint
1771
+ if (isFirstHashTracking && changes.changedFiles.length > 0) {
1772
+ console.log(chalk.cyan("ℹ️ First update with hash tracking enabled."));
1773
+ console.log(chalk.gray(" Changed files shown above may not be actual user modifications."));
1774
+ console.log(chalk.gray(" After this update, hash tracking will accurately detect changes.\n"));
1775
+ }
1776
+ // Ensure project-root .gitattributes carries the journal merge=union rule.
1777
+ // Additive-only (see ensureGitattributes) — runs regardless of whether
1778
+ // other template files changed, so it must sit before the "nothing to do"
1779
+ // early-return below. Never touches disk in --dry-run.
1780
+ if (!options.dryRun) {
1781
+ ensureGitattributes(cwd);
1782
+ }
1783
+ // Check if there's anything to do
1784
+ const isUpgrade = cliVsProject > 0;
1785
+ const isDowngrade = cliVsProject < 0;
1786
+ const isSameVersion = cliVsProject === 0;
1787
+ // Check if we have pending migrations that need to be applied
1788
+ const hasPendingMigrations = options.migrate &&
1789
+ classifiedMigrations &&
1790
+ (classifiedMigrations.auto.length > 0 ||
1791
+ classifiedMigrations.confirm.length > 0);
1792
+ if (changes.newFiles.length === 0 &&
1793
+ changes.autoUpdateFiles.length === 0 &&
1794
+ changes.changedFiles.length === 0 &&
1795
+ !hasPendingMigrations &&
1796
+ !hasSafeDeletes) {
1797
+ // The "already up to date" exit still has to repair the receipt: this is
1798
+ // exactly the clean tree where every file is `unchanged`, so it is the run
1799
+ // where a wrong entry would otherwise be skipped again.
1800
+ if (!options.dryRun && unchangedFileHashRepairs.size > 0) {
1801
+ updateHashes(cwd, unchangedFileHashRepairs);
1890
1802
  }
1891
- if (preConfirmMetadata.recommendMigrate && !options.migrate) {
1892
- console.log("");
1893
- console.log(chalk.bgGreen.black.bold(" 💡 RECOMMENDED ") +
1894
- chalk.green.bold(" Run with --migrate to complete the migration"));
1803
+ if (isSameVersion) {
1804
+ console.log(chalk.green("✓ Already up to date!"));
1805
+ }
1806
+ else {
1807
+ // Version changed but no file changes needed — still update the version stamp
1808
+ updateVersionFile(cwd);
1809
+ if (isUpgrade) {
1810
+ console.log(chalk.green(`✓ No file changes needed for ${projectVersion} → ${cliVersion}`));
1811
+ }
1812
+ else if (isDowngrade) {
1813
+ console.log(chalk.green(`✓ No file changes needed for ${projectVersion} → ${cliVersion} (downgrade)`));
1814
+ }
1895
1815
  }
1896
- // Notice when update.skip is bypassed so user isn't surprised when
1897
- // skipPaths-protected files get cleaned up during this breaking upgrade.
1898
- if (breakingBypass && skipPaths.length > 0) {
1899
- const willBypass = safeFileDeletes.filter((c) => c.action === "delete" &&
1900
- skipPaths.some((skip) => c.item.from === skip ||
1901
- c.item.from.startsWith(skip.endsWith("/") ? skip : skip + "/")));
1902
- if (willBypass.length > 0) {
1816
+ if (zcodeConfigured)
1817
+ printZcodeSetupHint();
1818
+ return;
1819
+ }
1820
+ // Show what this operation will do
1821
+ if (isUpgrade) {
1822
+ console.log(chalk.green(`This will UPGRADE: ${projectVersion} → ${cliVersion}\n`));
1823
+ }
1824
+ else if (isDowngrade) {
1825
+ console.log(chalk.red(`⚠️ This will DOWNGRADE: ${projectVersion} → ${cliVersion}\n`));
1826
+ }
1827
+ // Show breaking change warning before confirm
1828
+ if (cliVsProject > 0 && projectVersion !== "unknown") {
1829
+ const preConfirmMetadata = getMigrationMetadata(projectVersion, cliVersion);
1830
+ if (preConfirmMetadata.breaking) {
1831
+ console.log(chalk.cyan("═".repeat(60)));
1832
+ console.log(chalk.bgRed.white.bold(" ⚠️ BREAKING CHANGES ") +
1833
+ chalk.red.bold(" Review the changes above carefully!"));
1834
+ if (preConfirmMetadata.changelog.length > 0) {
1835
+ console.log("");
1836
+ console.log(chalk.white(preConfirmMetadata.changelog[0]));
1837
+ }
1838
+ if (preConfirmMetadata.recommendMigrate && !options.migrate) {
1903
1839
  console.log("");
1904
- console.log(chalk.bgYellow.black.bold(" ⚠ update.skip BYPASSED ") +
1905
- chalk.yellow.bold(` Breaking release — ${willBypass.length.toString()} file(s) under your update.skip paths will be cleaned up.`));
1906
- console.log(chalk.gray(" Hash-verified: only files matching known Trellis templates are deleted. Your local customizations (hash mismatch) are still preserved."));
1840
+ console.log(chalk.bgGreen.black.bold(" 💡 RECOMMENDED ") +
1841
+ chalk.green.bold(" Run with --migrate to complete the migration"));
1907
1842
  }
1843
+ // Notice when update.skip is bypassed so user isn't surprised when
1844
+ // skipPaths-protected files get cleaned up during this breaking upgrade.
1845
+ if (breakingBypass && skipPaths.length > 0) {
1846
+ const willBypass = safeFileDeletes.filter((c) => c.action === "delete" &&
1847
+ skipPaths.some((skip) => c.item.from === skip ||
1848
+ c.item.from.startsWith(skip.endsWith("/") ? skip : skip + "/")));
1849
+ if (willBypass.length > 0) {
1850
+ console.log("");
1851
+ console.log(chalk.bgYellow.black.bold(" ⚠ update.skip BYPASSED ") +
1852
+ chalk.yellow.bold(` Breaking release — ${willBypass.length.toString()} file(s) under your update.skip paths will be cleaned up.`));
1853
+ console.log(chalk.gray(" Hash-verified: only files matching known Trellis templates are deleted. Your local customizations (hash mismatch) are still preserved."));
1854
+ }
1855
+ }
1856
+ console.log(chalk.cyan("═".repeat(60)));
1857
+ console.log("");
1908
1858
  }
1909
- console.log(chalk.cyan("═".repeat(60)));
1910
- console.log("");
1911
1859
  }
1912
- }
1913
- // Dry run mode
1914
- if (options.dryRun) {
1915
- console.log(chalk.gray("[Dry run] No changes made."));
1916
- return;
1917
- }
1918
- // Batch-resolution flags are explicit consent for non-interactive runs.
1919
- // Prompting here breaks CI and `node ... update --force --migrate` smoke tests.
1920
- if (!options.force && !options.skipAll && !options.createNew) {
1921
- const { proceed } = await inquirer.prompt([
1922
- {
1923
- type: "confirm",
1924
- name: "proceed",
1925
- message: "Proceed?",
1926
- default: true,
1927
- },
1928
- ]);
1929
- if (!proceed) {
1930
- console.log(chalk.yellow("Update cancelled."));
1860
+ // Dry run mode
1861
+ if (options.dryRun) {
1862
+ console.log(chalk.gray("[Dry run] No changes made."));
1931
1863
  return;
1932
1864
  }
1933
- }
1934
- // Create complete backup of all managed platform/workflow directories
1935
- const backupDir = createFullBackup(cwd);
1936
- if (backupDir) {
1937
- console.log(chalk.gray(`\nBackup created: ${path.relative(cwd, backupDir)}/`));
1938
- }
1939
- // Execute migrations if --migrate flag is set
1940
- if (options.migrate && classifiedMigrations) {
1941
- const migrationResult = await executeMigrations(classifiedMigrations, cwd, {
1942
- force: options.force,
1943
- skipAll: options.skipAll,
1944
- }, templates);
1945
- printMigrationResult(migrationResult);
1946
- // Hardcoded: Rename traces-*.md to journal-*.md in workspace directories
1947
- // Why hardcoded: The migration system only supports fixed path renames, not pattern-based.
1948
- // traces-*.md files are in .trellis/workspace/{developer}/ with variable developer names
1949
- // and variable file numbers (traces-1.md, traces-2.md, etc.), so we can't enumerate them
1950
- // in the migration manifest. This is a one-time migration for the 0.2.0 naming redesign.
1951
- const workspaceDir = path.join(cwd, PATHS.WORKSPACE);
1952
- const { renamed: journalRenamed, skipped: journalSkipped } = renameTracesToJournal(workspaceDir);
1953
- if (journalRenamed > 0) {
1954
- console.log(chalk.cyan(`Renamed ${journalRenamed} traces file(s) to journal`));
1955
- }
1956
- for (const oldPath of journalSkipped) {
1957
- console.warn(chalk.yellow(`Kept ${path.relative(cwd, oldPath)}: its journal target already exists`));
1958
- }
1959
- }
1960
- // Execute safe-file-delete (after backup, before template writes)
1961
- let safeDeleted = 0;
1962
- if (hasSafeDeletes) {
1963
- safeDeleted = executeSafeFileDeletes(safeFileDeletes, cwd);
1964
- if (safeDeleted > 0) {
1965
- console.log(chalk.cyan(`\nCleaned up ${safeDeleted} deprecated command file(s)`));
1865
+ // Batch-resolution flags are explicit consent for non-interactive runs.
1866
+ // Prompting here breaks CI and `node ... update --force --migrate` smoke tests.
1867
+ if (!options.force && !options.skipAll && !options.createNew) {
1868
+ const { proceed } = await inquirer.prompt([
1869
+ {
1870
+ type: "confirm",
1871
+ name: "proceed",
1872
+ message: "Proceed?",
1873
+ default: true,
1874
+ },
1875
+ ]);
1876
+ if (!proceed) {
1877
+ console.log(chalk.yellow("Update cancelled."));
1878
+ return;
1879
+ }
1966
1880
  }
1967
- }
1968
- // Track results
1969
- let added = 0;
1970
- let autoUpdated = 0;
1971
- let updated = 0;
1972
- let skipped = 0;
1973
- let createdNew = 0;
1974
- // Add new files
1975
- if (changes.newFiles.length > 0) {
1976
- console.log(chalk.blue("\nAdding new files..."));
1977
- for (const file of changes.newFiles) {
1978
- const dir = path.dirname(file.path);
1979
- fs.mkdirSync(dir, { recursive: true });
1980
- fs.writeFileSync(file.path, file.newContent);
1981
- // Make scripts executable
1982
- if (file.relativePath.endsWith(".sh") ||
1983
- file.relativePath.endsWith(".py")) {
1984
- fs.chmodSync(file.path, "755");
1881
+ const assetMigrations = options.migrate && classifiedMigrations
1882
+ ? [
1883
+ ...classifiedMigrations.auto,
1884
+ ...classifiedMigrations.confirm,
1885
+ ...classifiedMigrations.conflict,
1886
+ ]
1887
+ : [];
1888
+ const migrationScope = assetMigrations.flatMap((item) => {
1889
+ if (item.type !== "rename-dir")
1890
+ return [
1891
+ item.from,
1892
+ `${item.from}.backup`,
1893
+ ...(item.to ? [item.to, `${item.to}.backup`] : []),
1894
+ ];
1895
+ const source = collectAllFiles(path.join(cwd, item.from), cwd).map((file) => toPosix(path.relative(cwd, file)));
1896
+ const target = item.to;
1897
+ return [
1898
+ ...source,
1899
+ ...(target
1900
+ ? source.map((file) => `${target}${file.slice(item.from.length)}`)
1901
+ : []),
1902
+ ...(target
1903
+ ? collectAllFiles(path.join(cwd, target), cwd).map((file) => toPosix(path.relative(cwd, file)))
1904
+ : []),
1905
+ ];
1906
+ });
1907
+ const backupDir = createFullBackup(cwd, [
1908
+ ...templates.keys(),
1909
+ ...changes.changedFiles.map((file) => `${file.relativePath}.new`),
1910
+ ...migrationScope,
1911
+ ...safeFileDeletes
1912
+ .filter((item) => item.action === "delete")
1913
+ .map((item) => item.item.from),
1914
+ ]);
1915
+ if (backupDir) {
1916
+ console.log(chalk.gray(`\nBackup created: ${path.relative(cwd, backupDir)}/`));
1917
+ }
1918
+ // Execute migrations if --migrate flag is set
1919
+ if (options.migrate && classifiedMigrations) {
1920
+ const migrationResult = await executeMigrations(classifiedMigrations, cwd, {
1921
+ force: options.force,
1922
+ skipAll: options.skipAll,
1923
+ }, templates);
1924
+ printMigrationResult(migrationResult);
1925
+ // Hardcoded: Rename traces-*.md to journal-*.md in workspace directories
1926
+ // Why hardcoded: The migration system only supports fixed path renames, not pattern-based.
1927
+ // traces-*.md files are in .trellis/workspace/{developer}/ with variable developer names
1928
+ // and variable file numbers (traces-1.md, traces-2.md, etc.), so we can't enumerate them
1929
+ // in the migration manifest. This is a one-time migration for the 0.2.0 naming redesign.
1930
+ const workspaceDir = path.join(cwd, PATHS.WORKSPACE);
1931
+ const { renamed: journalRenamed, skipped: journalSkipped } = renameTracesToJournal(workspaceDir);
1932
+ if (journalRenamed > 0) {
1933
+ console.log(chalk.cyan(`Renamed ${journalRenamed} traces file(s) to journal`));
1934
+ }
1935
+ for (const oldPath of journalSkipped) {
1936
+ console.warn(chalk.yellow(`Kept ${path.relative(cwd, oldPath)}: its journal target already exists`));
1985
1937
  }
1986
- console.log(chalk.green(` + ${file.relativePath}`));
1987
- added++;
1988
1938
  }
1989
- }
1990
- // Auto-update files (template updated, user didn't modify)
1991
- if (changes.autoUpdateFiles.length > 0) {
1992
- console.log(chalk.blue("\nAuto-updating template files..."));
1993
- for (const file of changes.autoUpdateFiles) {
1994
- fs.writeFileSync(file.path, file.newContent);
1995
- // Make scripts executable
1996
- if (file.relativePath.endsWith(".sh") ||
1997
- file.relativePath.endsWith(".py")) {
1998
- fs.chmodSync(file.path, "755");
1939
+ // Execute safe-file-delete (after backup, before template writes)
1940
+ let safeDeleted = 0;
1941
+ if (hasSafeDeletes) {
1942
+ safeDeleted = executeSafeFileDeletes(safeFileDeletes, cwd);
1943
+ if (safeDeleted > 0) {
1944
+ console.log(chalk.cyan(`\nCleaned up ${safeDeleted} deprecated command file(s)`));
1999
1945
  }
2000
- console.log(chalk.cyan(` ↑ ${file.relativePath}`));
2001
- autoUpdated++;
2002
1946
  }
2003
- }
2004
- // Handle changed files
2005
- if (changes.changedFiles.length > 0) {
2006
- console.log(chalk.blue("\n--- Resolving conflicts ---\n"));
2007
- const applyToAll = { action: null };
2008
- for (const file of changes.changedFiles) {
2009
- const action = await promptConflictResolution(file, options, applyToAll);
2010
- if (action === "overwrite") {
1947
+ // Track results
1948
+ let added = 0;
1949
+ let autoUpdated = 0;
1950
+ let updated = 0;
1951
+ let skipped = 0;
1952
+ let createdNew = 0;
1953
+ // Add new files
1954
+ if (changes.newFiles.length > 0) {
1955
+ console.log(chalk.blue("\nAdding new files..."));
1956
+ for (const file of changes.newFiles) {
1957
+ const dir = path.dirname(file.path);
1958
+ fs.mkdirSync(dir, { recursive: true });
2011
1959
  fs.writeFileSync(file.path, file.newContent);
1960
+ // Make scripts executable
2012
1961
  if (file.relativePath.endsWith(".sh") ||
2013
1962
  file.relativePath.endsWith(".py")) {
2014
1963
  fs.chmodSync(file.path, "755");
2015
1964
  }
2016
- console.log(chalk.yellow(` ✓ Overwritten: ${file.relativePath}`));
2017
- updated++;
1965
+ console.log(chalk.green(` + ${file.relativePath}`));
1966
+ added++;
2018
1967
  }
2019
- else if (action === "create-new") {
2020
- const newPath = file.path + ".new";
2021
- fs.writeFileSync(newPath, file.newContent);
2022
- console.log(chalk.blue(` ✓ Created: ${file.relativePath}.new`));
2023
- createdNew++;
1968
+ }
1969
+ // Auto-update files (template updated, user didn't modify)
1970
+ if (changes.autoUpdateFiles.length > 0) {
1971
+ console.log(chalk.blue("\nAuto-updating template files..."));
1972
+ for (const file of changes.autoUpdateFiles) {
1973
+ fs.writeFileSync(file.path, file.newContent);
1974
+ // Make scripts executable
1975
+ if (file.relativePath.endsWith(".sh") ||
1976
+ file.relativePath.endsWith(".py")) {
1977
+ fs.chmodSync(file.path, "755");
1978
+ }
1979
+ console.log(chalk.cyan(` ↑ ${file.relativePath}`));
1980
+ autoUpdated++;
2024
1981
  }
2025
- else {
2026
- console.log(chalk.gray(` ○ Skipped: ${file.relativePath}`));
2027
- skipped++;
1982
+ }
1983
+ // Handle changed files
1984
+ if (changes.changedFiles.length > 0) {
1985
+ console.log(chalk.blue("\n--- Resolving conflicts ---\n"));
1986
+ const applyToAll = { action: null };
1987
+ for (const file of changes.changedFiles) {
1988
+ const action = await promptConflictResolution(file, options, applyToAll);
1989
+ if (action === "overwrite") {
1990
+ fs.writeFileSync(file.path, file.newContent);
1991
+ if (file.relativePath.endsWith(".sh") ||
1992
+ file.relativePath.endsWith(".py")) {
1993
+ fs.chmodSync(file.path, "755");
1994
+ }
1995
+ console.log(chalk.yellow(` ✓ Overwritten: ${file.relativePath}`));
1996
+ updated++;
1997
+ }
1998
+ else if (action === "create-new") {
1999
+ const newPath = file.path + ".new";
2000
+ fs.writeFileSync(newPath, file.newContent);
2001
+ console.log(chalk.blue(` ✓ Created: ${file.relativePath}.new`));
2002
+ createdNew++;
2003
+ }
2004
+ else {
2005
+ console.log(chalk.gray(` ○ Skipped: ${file.relativePath}`));
2006
+ skipped++;
2007
+ }
2028
2008
  }
2029
2009
  }
2030
- }
2031
- // Append additive config.yaml sections introduced between versions.
2032
- // Sentinel-gated, so users keep their customizations and re-running update
2033
- // on already-migrated files is a no-op. Skipped on unknown / downgrade.
2034
- let configSectionsAppended = 0;
2035
- if (cliVsProject > 0 && projectVersion !== "unknown") {
2036
- const sectionEntries = getConfigSectionsAddedBetween(projectVersion, cliVersion);
2037
- if (sectionEntries.length > 0) {
2038
- const { appended } = applyConfigSectionsAdded(sectionEntries, cwd, templates);
2039
- configSectionsAppended = appended;
2040
- }
2041
- }
2042
- // Update version file
2043
- updateVersionFile(cwd);
2044
- // Update template hashes for new, auto-updated, and overwritten files
2045
- const filesToHash = new Map(unchangedFileHashRepairs);
2046
- for (const file of changes.newFiles) {
2047
- filesToHash.set(file.relativePath, file.newContent);
2048
- }
2049
- // Auto-updated files always get new hash
2050
- for (const file of changes.autoUpdateFiles) {
2051
- filesToHash.set(file.relativePath, file.newContent);
2052
- }
2053
- // Only hash overwritten files (not skipped or .new copies)
2054
- for (const file of changes.changedFiles) {
2055
- const fullPath = path.join(cwd, file.relativePath);
2056
- if (fs.existsSync(fullPath)) {
2057
- const content = fs.readFileSync(fullPath, "utf-8");
2058
- if (content === file.newContent) {
2059
- filesToHash.set(file.relativePath, file.newContent);
2010
+ // Append additive config.yaml sections introduced between versions.
2011
+ // Sentinel-gated, so users keep their customizations and re-running update
2012
+ // on already-migrated files is a no-op. Skipped on unknown / downgrade.
2013
+ let configSectionsAppended = 0;
2014
+ if (cliVsProject > 0 && projectVersion !== "unknown") {
2015
+ const sectionEntries = getConfigSectionsAddedBetween(projectVersion, cliVersion);
2016
+ if (sectionEntries.length > 0) {
2017
+ const { appended } = applyConfigSectionsAdded(sectionEntries, cwd, templates);
2018
+ configSectionsAppended = appended;
2060
2019
  }
2061
2020
  }
2062
- }
2063
- if (filesToHash.size > 0) {
2064
- updateHashes(cwd, filesToHash);
2065
- }
2066
- // Print summary
2067
- console.log(chalk.cyan("\n--- Summary ---\n"));
2068
- if (added > 0) {
2069
- console.log(` Added: ${added} file(s)`);
2070
- }
2071
- if (autoUpdated > 0) {
2072
- console.log(` Auto-updated: ${autoUpdated} file(s)`);
2073
- }
2074
- if (updated > 0) {
2075
- console.log(` Updated: ${updated} file(s)`);
2076
- }
2077
- if (skipped > 0) {
2078
- console.log(` Skipped: ${skipped} file(s)`);
2079
- }
2080
- if (createdNew > 0) {
2081
- console.log(` Created .new copies: ${createdNew} file(s)`);
2082
- }
2083
- if (safeDeleted > 0) {
2084
- console.log(` Cleaned up: ${safeDeleted} deprecated file(s)`);
2085
- }
2086
- if (configSectionsAppended > 0) {
2087
- console.log(` Config sections added: ${configSectionsAppended}`);
2088
- }
2089
- if (backupDir) {
2090
- console.log(` Backup: ${path.relative(cwd, backupDir)}/`);
2091
- }
2092
- const actionWord = isDowngrade ? "Downgrade" : "Update";
2093
- console.log(chalk.green(`\n✅ ${actionWord} complete! (${projectVersion} → ${cliVersion})`));
2094
- if (createdNew > 0) {
2095
- console.log(chalk.gray("\nTip: Review .new files and merge changes manually if needed."));
2096
- }
2097
- // Create migration task if there are breaking changes with migration guides
2098
- if (cliVsProject > 0 && projectVersion !== "unknown") {
2099
- const metadata = getMigrationMetadata(projectVersion, cliVersion);
2100
- if (metadata.breaking && metadata.migrationGuides.length > 0) {
2101
- // Create task directory
2102
- const today = new Date();
2103
- const monthDay = `${String(today.getMonth() + 1).padStart(2, "0")}-${String(today.getDate()).padStart(2, "0")}`;
2104
- const taskSlug = `migrate-to-${cliVersion}`;
2105
- const taskDirName = `${monthDay}-${taskSlug}`;
2106
- const tasksDir = path.join(cwd, DIR_NAMES.WORKFLOW, DIR_NAMES.TASKS);
2107
- const taskDir = path.join(tasksDir, "planning", taskDirName);
2108
- // A prior migration task may have moved to another lifecycle directory.
2109
- const taskLocations = [
2110
- tasksDir,
2111
- ...["planning", "in_progress", "cancelled"].map((status) => path.join(tasksDir, status)),
2112
- ];
2113
- for (const status of ["completed", "archive"]) {
2114
- const statusDir = path.join(tasksDir, status);
2115
- if (!fs.existsSync(statusDir))
2116
- continue;
2117
- for (const month of fs.readdirSync(statusDir, {
2118
- withFileTypes: true,
2119
- })) {
2120
- if (month.isDirectory() &&
2121
- /^\d{4}-(0[1-9]|1[0-2])$/.test(month.name)) {
2122
- taskLocations.push(path.join(statusDir, month.name));
2123
- }
2021
+ // Update version file
2022
+ updateVersionFile(cwd);
2023
+ // Update template hashes for new, auto-updated, and overwritten files
2024
+ const filesToHash = new Map(unchangedFileHashRepairs);
2025
+ for (const file of changes.newFiles) {
2026
+ filesToHash.set(file.relativePath, file.newContent);
2027
+ }
2028
+ // Auto-updated files always get new hash
2029
+ for (const file of changes.autoUpdateFiles) {
2030
+ filesToHash.set(file.relativePath, file.newContent);
2031
+ }
2032
+ // Only hash overwritten files (not skipped or .new copies)
2033
+ for (const file of changes.changedFiles) {
2034
+ const fullPath = path.join(cwd, file.relativePath);
2035
+ if (fs.existsSync(fullPath)) {
2036
+ const content = fs.readFileSync(fullPath, "utf-8");
2037
+ if (content === file.newContent) {
2038
+ filesToHash.set(file.relativePath, file.newContent);
2124
2039
  }
2125
2040
  }
2126
- if (!taskLocations.some((directory) => fs.existsSync(path.join(directory, taskDirName)))) {
2127
- fs.mkdirSync(taskDir, { recursive: true });
2128
- // Get current developer for assignee.
2129
- // `.developer` is a key=value file (written by init_developer.py):
2130
- // name=<developer-name>
2131
- // initialized_at=<iso8601>
2132
- // Reading it raw and .trim()-ing embeds the entire file contents
2133
- // (including the `name=` prefix and the `initialized_at` line) into
2134
- // the assignee field, producing bogus assignees like
2135
- // "name=suyuan\ninitialized_at=2026-04-07T23:41:21.978312" that
2136
- // later break session-start task rendering.
2137
- const developerFile = path.join(cwd, DIR_NAMES.WORKFLOW, ".developer");
2138
- let currentDeveloper = "unknown";
2139
- if (fs.existsSync(developerFile)) {
2140
- const raw = fs.readFileSync(developerFile, "utf-8");
2141
- const nameMatch = raw.match(/^\s*name\s*=\s*(.+?)\s*$/m);
2142
- if (nameMatch) {
2143
- currentDeveloper = nameMatch[1];
2041
+ }
2042
+ if (filesToHash.size > 0) {
2043
+ updateHashes(cwd, filesToHash);
2044
+ }
2045
+ // Print summary
2046
+ console.log(chalk.cyan("\n--- Summary ---\n"));
2047
+ if (added > 0) {
2048
+ console.log(` Added: ${added} file(s)`);
2049
+ }
2050
+ if (autoUpdated > 0) {
2051
+ console.log(` Auto-updated: ${autoUpdated} file(s)`);
2052
+ }
2053
+ if (updated > 0) {
2054
+ console.log(` Updated: ${updated} file(s)`);
2055
+ }
2056
+ if (skipped > 0) {
2057
+ console.log(` Skipped: ${skipped} file(s)`);
2058
+ }
2059
+ if (createdNew > 0) {
2060
+ console.log(` Created .new copies: ${createdNew} file(s)`);
2061
+ }
2062
+ if (safeDeleted > 0) {
2063
+ console.log(` Cleaned up: ${safeDeleted} deprecated file(s)`);
2064
+ }
2065
+ if (configSectionsAppended > 0) {
2066
+ console.log(` Config sections added: ${configSectionsAppended}`);
2067
+ }
2068
+ if (backupDir) {
2069
+ console.log(` Backup: ${path.relative(cwd, backupDir)}/`);
2070
+ }
2071
+ const actionWord = isDowngrade ? "Downgrade" : "Update";
2072
+ console.log(chalk.green(`\n✅ ${actionWord} complete! (${projectVersion} → ${cliVersion})`));
2073
+ if (createdNew > 0) {
2074
+ console.log(chalk.gray("\nTip: Review .new files and merge changes manually if needed."));
2075
+ }
2076
+ // Create migration task if there are breaking changes with migration guides
2077
+ if (cliVsProject > 0 && projectVersion !== "unknown") {
2078
+ const metadata = getMigrationMetadata(projectVersion, cliVersion);
2079
+ if (metadata.breaking && metadata.migrationGuides.length > 0) {
2080
+ // Create task directory
2081
+ const today = new Date();
2082
+ const monthDay = `${String(today.getMonth() + 1).padStart(2, "0")}-${String(today.getDate()).padStart(2, "0")}`;
2083
+ const taskSlug = `migrate-to-${cliVersion}`;
2084
+ const taskDirName = `${monthDay}-${taskSlug}`;
2085
+ const tasksDir = path.join(cwd, DIR_NAMES.WORKFLOW, DIR_NAMES.TASKS);
2086
+ const taskDir = path.join(tasksDir, "planning", taskDirName);
2087
+ // A prior migration task may have moved to another lifecycle directory.
2088
+ const taskLocations = [
2089
+ tasksDir,
2090
+ ...["planning", "in_progress", "cancelled"].map((status) => path.join(tasksDir, status)),
2091
+ ];
2092
+ for (const status of ["completed", "archive"]) {
2093
+ const statusDir = path.join(tasksDir, status);
2094
+ if (!fs.existsSync(statusDir))
2095
+ continue;
2096
+ for (const month of fs.readdirSync(statusDir, {
2097
+ withFileTypes: true,
2098
+ })) {
2099
+ if (month.isDirectory() &&
2100
+ /^\d{4}-(0[1-9]|1[0-2])$/.test(month.name)) {
2101
+ taskLocations.push(path.join(statusDir, month.name));
2102
+ }
2144
2103
  }
2145
2104
  }
2146
- // Build task.json — canonical 24-field shape via shared factory.
2147
- const taskTitle = `Migrate to v${cliVersion}`;
2148
- const todayStr = today.toISOString().split("T")[0];
2149
- const taskJson = emptyTaskJson({
2150
- id: taskSlug,
2151
- name: taskSlug,
2152
- title: taskTitle,
2153
- description: `Breaking change migration from v${projectVersion} to v${cliVersion}`,
2154
- status: "planning",
2155
- scope: "migration",
2156
- priority: "P1",
2157
- creator: "trellis-update",
2158
- assignee: currentDeveloper,
2159
- createdAt: todayStr,
2160
- });
2161
- // Write task.json
2162
- const taskJsonPath = path.join(taskDir, "task.json");
2163
- fs.writeFileSync(taskJsonPath, JSON.stringify(taskJson, null, 2));
2164
- // Build PRD content
2165
- let prdContent = `# Migration Task: Upgrade to v${cliVersion}\n\n`;
2166
- prdContent += `**Created**: ${todayStr}\n`;
2167
- prdContent += `**From Version**: ${projectVersion}\n`;
2168
- prdContent += `**To Version**: ${cliVersion}\n`;
2169
- prdContent += `**Assignee**: ${currentDeveloper}\n\n`;
2170
- prdContent += `## Status\n\n- [ ] Review migration guide\n- [ ] Update custom files\n- [ ] Run \`trellis update --migrate\`\n- [ ] Test workflows\n\n`;
2171
- for (const { version, guide, aiInstructions, } of metadata.migrationGuides) {
2172
- prdContent += `---\n\n## v${version} Migration Guide\n\n`;
2173
- prdContent += guide;
2174
- prdContent += "\n\n";
2175
- if (aiInstructions) {
2176
- prdContent += `### AI Assistant Instructions\n\n`;
2177
- prdContent += `When helping with this migration:\n\n`;
2178
- prdContent += aiInstructions;
2105
+ if (!taskLocations.some((directory) => fs.existsSync(path.join(directory, taskDirName)))) {
2106
+ fs.mkdirSync(taskDir, { recursive: true });
2107
+ // Get current developer for assignee.
2108
+ // `.developer` is a key=value file (written by init_developer.py):
2109
+ // name=<developer-name>
2110
+ // initialized_at=<iso8601>
2111
+ // Reading it raw and .trim()-ing embeds the entire file contents
2112
+ // (including the `name=` prefix and the `initialized_at` line) into
2113
+ // the assignee field, producing bogus assignees like
2114
+ // "name=suyuan\ninitialized_at=2026-04-07T23:41:21.978312" that
2115
+ // later break session-start task rendering.
2116
+ const developerFile = path.join(cwd, DIR_NAMES.WORKFLOW, ".developer");
2117
+ let currentDeveloper = "unknown";
2118
+ if (fs.existsSync(developerFile)) {
2119
+ const raw = fs.readFileSync(developerFile, "utf-8");
2120
+ const nameMatch = raw.match(/^\s*name\s*=\s*(.+?)\s*$/m);
2121
+ if (nameMatch) {
2122
+ currentDeveloper = nameMatch[1];
2123
+ }
2124
+ }
2125
+ // Build task.json — canonical 24-field shape via shared factory.
2126
+ const taskTitle = `Migrate to v${cliVersion}`;
2127
+ const todayStr = today.toISOString().split("T")[0];
2128
+ const taskJson = emptyTaskJson({
2129
+ id: taskSlug,
2130
+ name: taskSlug,
2131
+ title: taskTitle,
2132
+ description: `Breaking change migration from v${projectVersion} to v${cliVersion}`,
2133
+ status: "planning",
2134
+ scope: "migration",
2135
+ priority: "P1",
2136
+ creator: "trellis-update",
2137
+ assignee: currentDeveloper,
2138
+ createdAt: todayStr,
2139
+ });
2140
+ // Write task.json
2141
+ const taskJsonPath = path.join(taskDir, "task.json");
2142
+ fs.writeFileSync(taskJsonPath, JSON.stringify(taskJson, null, 2));
2143
+ // Build PRD content
2144
+ let prdContent = `# Migration Task: Upgrade to v${cliVersion}\n\n`;
2145
+ prdContent += `**Created**: ${todayStr}\n`;
2146
+ prdContent += `**From Version**: ${projectVersion}\n`;
2147
+ prdContent += `**To Version**: ${cliVersion}\n`;
2148
+ prdContent += `**Assignee**: ${currentDeveloper}\n\n`;
2149
+ prdContent += `## Status\n\n- [ ] Review migration guide\n- [ ] Update custom files\n- [ ] Run \`trellis update --migrate\`\n- [ ] Test workflows\n\n`;
2150
+ for (const { version, guide, aiInstructions, } of metadata.migrationGuides) {
2151
+ prdContent += `---\n\n## v${version} Migration Guide\n\n`;
2152
+ prdContent += guide;
2179
2153
  prdContent += "\n\n";
2154
+ if (aiInstructions) {
2155
+ prdContent += `### AI Assistant Instructions\n\n`;
2156
+ prdContent += `When helping with this migration:\n\n`;
2157
+ prdContent += aiInstructions;
2158
+ prdContent += "\n\n";
2159
+ }
2180
2160
  }
2161
+ // Write PRD
2162
+ const prdPath = path.join(taskDir, "prd.md");
2163
+ fs.writeFileSync(prdPath, prdContent);
2164
+ console.log("");
2165
+ console.log(chalk.bgCyan.black.bold(" 📋 MIGRATION TASK CREATED "));
2166
+ console.log(chalk.cyan(`A task has been created to help you complete the migration:`));
2167
+ console.log(chalk.white(` ${DIR_NAMES.WORKFLOW}/${DIR_NAMES.TASKS}/planning/${taskDirName}/`));
2168
+ console.log("");
2169
+ console.log(chalk.gray("Use AI to help: Ask Claude/Cursor to read the task and fix your custom files."));
2181
2170
  }
2182
- // Write PRD
2183
- const prdPath = path.join(taskDir, "prd.md");
2184
- fs.writeFileSync(prdPath, prdContent);
2185
- console.log("");
2186
- console.log(chalk.bgCyan.black.bold(" 📋 MIGRATION TASK CREATED "));
2187
- console.log(chalk.cyan(`A task has been created to help you complete the migration:`));
2188
- console.log(chalk.white(` ${DIR_NAMES.WORKFLOW}/${DIR_NAMES.TASKS}/planning/${taskDirName}/`));
2189
- console.log("");
2190
- console.log(chalk.gray("Use AI to help: Ask Claude/Cursor to read the task and fix your custom files."));
2191
2171
  }
2192
2172
  }
2193
- }
2194
- if (zcodeConfigured)
2195
- printZcodeSetupHint();
2196
- // Display breaking change warnings at the very end (so they don't scroll off screen)
2197
- if (cliVsProject > 0 && projectVersion !== "unknown") {
2198
- const finalMetadata = getMigrationMetadata(projectVersion, cliVersion);
2199
- if (finalMetadata.breaking || finalMetadata.changelog.length > 0) {
2200
- console.log("");
2201
- console.log(chalk.cyan("═".repeat(60)));
2202
- if (finalMetadata.breaking) {
2203
- console.log(chalk.bgRed.white.bold(" ⚠️ BREAKING CHANGES ") +
2204
- chalk.red.bold(" This update contains breaking changes!"));
2173
+ bindAssetUpdate(cwd, path.basename(backupDir));
2174
+ if (zcodeConfigured)
2175
+ printZcodeSetupHint();
2176
+ // Display breaking change warnings at the very end (so they don't scroll off screen)
2177
+ if (cliVsProject > 0 && projectVersion !== "unknown") {
2178
+ const finalMetadata = getMigrationMetadata(projectVersion, cliVersion);
2179
+ if (finalMetadata.breaking || finalMetadata.changelog.length > 0) {
2205
2180
  console.log("");
2206
- }
2207
- if (finalMetadata.changelog.length > 0) {
2208
- console.log(chalk.cyan.bold("📋 What's Changed:"));
2209
- for (const entry of finalMetadata.changelog) {
2210
- console.log(chalk.white(` ${entry}`));
2181
+ console.log(chalk.cyan("═".repeat(60)));
2182
+ if (finalMetadata.breaking) {
2183
+ console.log(chalk.bgRed.white.bold(" ⚠️ BREAKING CHANGES ") +
2184
+ chalk.red.bold(" This update contains breaking changes!"));
2185
+ console.log("");
2211
2186
  }
2212
- console.log("");
2213
- }
2214
- if (finalMetadata.recommendMigrate && !options.migrate) {
2215
- console.log(chalk.bgGreen.black.bold(" 💡 RECOMMENDED ") +
2216
- chalk.green.bold(" Run with --migrate to complete the migration"));
2217
- console.log(chalk.gray(" This will remove legacy files and apply all changes."));
2218
- console.log("");
2187
+ if (finalMetadata.changelog.length > 0) {
2188
+ console.log(chalk.cyan.bold("📋 What's Changed:"));
2189
+ for (const entry of finalMetadata.changelog) {
2190
+ console.log(chalk.white(` ${entry}`));
2191
+ }
2192
+ console.log("");
2193
+ }
2194
+ if (finalMetadata.recommendMigrate && !options.migrate) {
2195
+ console.log(chalk.bgGreen.black.bold(" 💡 RECOMMENDED ") +
2196
+ chalk.green.bold(" Run with --migrate to complete the migration"));
2197
+ console.log(chalk.gray(" This will remove legacy files and apply all changes."));
2198
+ console.log("");
2199
+ }
2200
+ console.log(chalk.cyan("═".repeat(60)));
2219
2201
  }
2220
- console.log(chalk.cyan("═".repeat(60)));
2221
2202
  }
2222
- }
2203
+ }, options.dryRun);
2223
2204
  }
2224
2205
  //# sourceMappingURL=update.js.map