@pennixrv/trellis 0.7.1-beta.0 → 0.7.1-beta.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -7
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +6 -4
- package/dist/cli/index.js.map +1 -1
- package/dist/commands/ablate.d.ts.map +1 -1
- package/dist/commands/ablate.js +2 -0
- package/dist/commands/ablate.js.map +1 -1
- package/dist/commands/backup.d.ts +4 -0
- package/dist/commands/backup.d.ts.map +1 -0
- package/dist/commands/backup.js +75 -0
- package/dist/commands/backup.js.map +1 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +687 -676
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/mem.d.ts.map +1 -1
- package/dist/commands/mem.js +49 -7
- package/dist/commands/mem.js.map +1 -1
- package/dist/commands/task-progress.d.ts +1 -0
- package/dist/commands/task-progress.d.ts.map +1 -1
- package/dist/commands/task-progress.js +67 -25
- package/dist/commands/task-progress.js.map +1 -1
- package/dist/commands/teamwork/spawn.d.ts.map +1 -1
- package/dist/commands/teamwork/spawn.js +5 -2
- package/dist/commands/teamwork/spawn.js.map +1 -1
- package/dist/commands/teamwork/teammate-adapters.d.ts +0 -2
- package/dist/commands/teamwork/teammate-adapters.d.ts.map +1 -1
- package/dist/commands/teamwork/teammate-adapters.js +0 -2
- package/dist/commands/teamwork/teammate-adapters.js.map +1 -1
- package/dist/commands/teamwork/teammate-launch.d.ts +1 -2
- package/dist/commands/teamwork/teammate-launch.d.ts.map +1 -1
- package/dist/commands/teamwork/teammate-launch.js +0 -2
- package/dist/commands/teamwork/teammate-launch.js.map +1 -1
- package/dist/commands/uninstall.d.ts.map +1 -1
- package/dist/commands/uninstall.js +92 -84
- package/dist/commands/uninstall.js.map +1 -1
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +633 -591
- package/dist/commands/update.js.map +1 -1
- package/dist/commands/workflow.d.ts.map +1 -1
- package/dist/commands/workflow.js +94 -89
- package/dist/commands/workflow.js.map +1 -1
- package/dist/configurators/shared.js +2 -2
- package/dist/configurators/shared.js.map +1 -1
- package/dist/constants/paths.d.ts +3 -10
- package/dist/constants/paths.d.ts.map +1 -1
- package/dist/constants/paths.js +4 -13
- package/dist/constants/paths.js.map +1 -1
- package/dist/migrations/manifests/0.7.1-beta.1.json +186 -0
- package/dist/migrations/manifests/0.7.1-beta.10.json +9 -0
- package/dist/migrations/manifests/0.7.1-beta.2.json +522 -0
- package/dist/migrations/manifests/0.7.1-beta.3.json +9 -0
- package/dist/migrations/manifests/0.7.1-beta.4.json +9 -0
- package/dist/migrations/manifests/0.7.1-beta.5.json +9 -0
- package/dist/migrations/manifests/0.7.1-beta.6.json +10 -0
- package/dist/migrations/manifests/0.7.1-beta.7.json +8 -0
- package/dist/migrations/manifests/0.7.1-beta.8.json +481 -0
- package/dist/migrations/manifests/0.7.1-beta.9.json +191 -0
- package/dist/templates/claude/hooks/statusline.py +9 -4
- package/dist/templates/common/bundled-skills/trellis-decision-space-construction-and-merging/workflow.md +2 -0
- package/dist/templates/common/bundled-skills/trellis-goal-exploration-and-requirements/workflow.md +2 -0
- package/dist/templates/common/bundled-skills/trellis-meta/SKILL.md +1 -1
- package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/add-project-local-conventions.md +1 -1
- package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-context-loading.md +5 -5
- package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-task-lifecycle.md +3 -2
- package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/overview.md +1 -1
- package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/context-injection.md +1 -1
- package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/task-system.md +8 -2
- package/dist/templates/common/bundled-skills/trellis-session-insight/references/cli-quick-reference.md +2 -1
- package/dist/templates/common/bundled-skills/trellis-session-insight/references/triggering-patterns.md +2 -2
- package/dist/templates/common/bundled-skills/trellis-solution-design-and-convergence/workflow.md +4 -0
- package/dist/templates/common/bundled-skills/trellis-spec-bootstrap/references/mcp-setup.md +4 -2
- package/dist/templates/common/bundled-skills/trellis-spec-bootstrap/references/spec-task-planning.md +2 -2
- package/dist/templates/common/bundled-skills/trellis-teamwork/SKILL.md +7 -2
- package/dist/templates/common/bundled-skills/trellis-teamwork/references/coordination-modes.md +3 -1
- package/dist/templates/common/bundled-skills/trellis-teamwork/references/teammate-configuration-matrix.md +11 -6
- package/dist/templates/common/bundled-skills/trellis-teamwork/references/teammate-processes.md +50 -6
- package/dist/templates/common/bundled-skills/trellis-teamwork/references/teamwork-plan-construction.md +49 -9
- package/dist/templates/common/commands/start.md +2 -2
- package/dist/templates/common/skills/before-dev.md +1 -4
- package/dist/templates/common/skills/break-loop.md +7 -18
- package/dist/templates/common/skills/check.md +7 -11
- package/dist/templates/common/skills/{brainstorm.md → request-intake.md} +5 -3
- package/dist/templates/common/skills/update-spec.md +5 -7
- package/dist/templates/extract.js +4 -0
- package/dist/templates/extract.js.map +1 -1
- package/dist/templates/omp/extensions/trellis/index.ts.txt +1 -1
- package/dist/templates/opencode/lib/session-utils.js +301 -209
- package/dist/templates/snow/SNOW.md +1 -1
- package/dist/templates/trellis/agents/teammate.md +16 -5
- package/dist/templates/trellis/index.d.ts +3 -0
- package/dist/templates/trellis/index.d.ts.map +1 -1
- package/dist/templates/trellis/index.js +6 -0
- package/dist/templates/trellis/index.js.map +1 -1
- package/dist/templates/trellis/scripts/add_session.py +1 -1
- package/dist/templates/trellis/scripts/common/active_task.py +3 -47
- package/dist/templates/trellis/scripts/common/paths.py +9 -25
- package/dist/templates/trellis/scripts/common/safe_commit.py +8 -8
- package/dist/templates/trellis/scripts/common/task_context.py +11 -51
- package/dist/templates/trellis/scripts/common/task_continuity.py +53 -45
- package/dist/templates/trellis/scripts/common/task_layout.py +80 -0
- package/dist/templates/trellis/scripts/common/task_move.py +345 -0
- package/dist/templates/trellis/scripts/common/task_store.py +28 -55
- package/dist/templates/trellis/scripts/common/task_utils.py +23 -19
- package/dist/templates/trellis/scripts/common/task_workflow.py +72 -31
- package/dist/templates/trellis/scripts/common/tasks.py +6 -5
- package/dist/templates/trellis/scripts/hooks/linear_sync.py +13 -3
- package/dist/templates/trellis/scripts/task.py +41 -23
- package/dist/templates/trellis/workflow.md +4 -2
- package/dist/utils/ablation-store.d.ts +1 -1
- package/dist/utils/ablation-store.d.ts.map +1 -1
- package/dist/utils/ablation-store.js +58 -24
- package/dist/utils/ablation-store.js.map +1 -1
- package/dist/utils/asset-backup.d.ts +138 -0
- package/dist/utils/asset-backup.d.ts.map +1 -0
- package/dist/utils/asset-backup.js +514 -0
- package/dist/utils/asset-backup.js.map +1 -0
- package/dist/utils/atomic-write.d.ts +1 -1
- package/dist/utils/atomic-write.d.ts.map +1 -1
- package/dist/utils/atomic-write.js +6 -3
- package/dist/utils/atomic-write.js.map +1 -1
- package/dist/utils/manifest-prune.d.ts +4 -1
- package/dist/utils/manifest-prune.d.ts.map +1 -1
- package/dist/utils/manifest-prune.js +17 -6
- package/dist/utils/manifest-prune.js.map +1 -1
- package/dist/utils/template-hash.d.ts +5 -4
- package/dist/utils/template-hash.d.ts.map +1 -1
- package/dist/utils/template-hash.js +24 -17
- package/dist/utils/template-hash.js.map +1 -1
- package/package.json +2 -2
package/dist/commands/update.js
CHANGED
|
@@ -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 {
|
|
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";
|
|
@@ -187,6 +188,50 @@ bypassUpdateSkip = false) {
|
|
|
187
188
|
results.push({ item, action: "skip-missing" });
|
|
188
189
|
}
|
|
189
190
|
}
|
|
191
|
+
for (const { item, action } of [...results]) {
|
|
192
|
+
if (!item.from.startsWith(".trellis/scripts/") ||
|
|
193
|
+
!item.from.endsWith(".py") ||
|
|
194
|
+
!item.allowed_hashes?.length ||
|
|
195
|
+
(action !== "delete" &&
|
|
196
|
+
!(action === "skip-missing" && !fs.existsSync(path.join(cwd, item.from)))))
|
|
197
|
+
continue;
|
|
198
|
+
const cacheDir = path.join(path.dirname(item.from), "__pycache__");
|
|
199
|
+
try {
|
|
200
|
+
const fullCacheDir = path.join(cwd, cacheDir);
|
|
201
|
+
if (fs.lstatSync(fullCacheDir).isSymbolicLink())
|
|
202
|
+
continue;
|
|
203
|
+
if (fs.realpathSync(fullCacheDir) !== path.resolve(fullCacheDir))
|
|
204
|
+
continue;
|
|
205
|
+
for (const entry of fs.readdirSync(fullCacheDir, {
|
|
206
|
+
withFileTypes: true,
|
|
207
|
+
})) {
|
|
208
|
+
const parts = entry.name.split(".");
|
|
209
|
+
if (!entry.isFile() ||
|
|
210
|
+
parts[0] !== path.basename(item.from, ".py") ||
|
|
211
|
+
!/^cpython-\d+$/.test(parts[1] ?? "") ||
|
|
212
|
+
!((parts.length === 3 && parts[2] === "pyc") ||
|
|
213
|
+
(parts.length === 4 &&
|
|
214
|
+
/^opt-[12]$/.test(parts[2] ?? "") &&
|
|
215
|
+
parts[3] === "pyc")))
|
|
216
|
+
continue;
|
|
217
|
+
const from = toPosix(path.join(cacheDir, entry.name));
|
|
218
|
+
const skipped = !bypassUpdateSkip &&
|
|
219
|
+
skipPaths.some((skip) => [item.from, from].some((file) => file === skip ||
|
|
220
|
+
file.startsWith(skip.endsWith("/") ? skip : skip + "/")));
|
|
221
|
+
results.push({
|
|
222
|
+
item: {
|
|
223
|
+
type: "safe-file-delete",
|
|
224
|
+
from,
|
|
225
|
+
description: "Bytecode cache of a retired generated script",
|
|
226
|
+
},
|
|
227
|
+
action: skipped ? "skip-update-skip" : "delete",
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
// Missing or unreadable caches are not cleanup candidates.
|
|
233
|
+
}
|
|
234
|
+
}
|
|
190
235
|
return results;
|
|
191
236
|
}
|
|
192
237
|
/**
|
|
@@ -842,30 +887,6 @@ async function promptConflictResolution(file, options, applyToAll) {
|
|
|
842
887
|
}
|
|
843
888
|
return action;
|
|
844
889
|
}
|
|
845
|
-
/**
|
|
846
|
-
* Create a timestamped backup directory path
|
|
847
|
-
*/
|
|
848
|
-
function createBackupDirPath(cwd) {
|
|
849
|
-
const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
850
|
-
return path.join(cwd, DIR_NAMES.WORKFLOW, `.backup-${timestamp}`);
|
|
851
|
-
}
|
|
852
|
-
/**
|
|
853
|
-
* Backup a single file to the backup directory
|
|
854
|
-
*/
|
|
855
|
-
function backupFile(cwd, backupDir, relativePath) {
|
|
856
|
-
const srcPath = path.join(cwd, relativePath);
|
|
857
|
-
if (!fs.existsSync(srcPath))
|
|
858
|
-
return;
|
|
859
|
-
const backupPath = path.join(backupDir, relativePath);
|
|
860
|
-
fs.mkdirSync(path.dirname(backupPath), { recursive: true });
|
|
861
|
-
fs.copyFileSync(srcPath, backupPath);
|
|
862
|
-
}
|
|
863
|
-
/**
|
|
864
|
-
* Directories to backup as complete snapshot (derived from platform registry)
|
|
865
|
-
*/
|
|
866
|
-
const BACKUP_DIRS = ALL_MANAGED_DIRS;
|
|
867
|
-
/** Root-level managed files to include in update backups. */
|
|
868
|
-
const BACKUP_FILES = [FILE_NAMES.AGENTS];
|
|
869
890
|
/**
|
|
870
891
|
* Patterns to exclude from backup (user data that shouldn't be backed up)
|
|
871
892
|
*/
|
|
@@ -877,6 +898,8 @@ const BACKUP_EXCLUDE_PATTERNS = [
|
|
|
877
898
|
"/spec/", // Spec files (user-customized content)
|
|
878
899
|
"/backlog/", // Backlog data (user data)
|
|
879
900
|
"/agent-traces/", // Agent traces (user data, legacy name)
|
|
901
|
+
"/session-handoffs/",
|
|
902
|
+
"/sessions/",
|
|
880
903
|
// Platform-native worktree dirs — these are full sub-repos the CLI
|
|
881
904
|
// spawns for parallel sessions. Backing them up on every update would
|
|
882
905
|
// snapshot the entire nested working tree. Confirmed conventions:
|
|
@@ -908,44 +931,11 @@ export function shouldExcludeFromBackup(relativePath) {
|
|
|
908
931
|
return false;
|
|
909
932
|
}
|
|
910
933
|
/**
|
|
911
|
-
*
|
|
912
|
-
* Backs up all managed platform/workflow directories entirely
|
|
913
|
-
* (excluding user data like workspace/, tasks/, backlog/)
|
|
934
|
+
* Snapshot only positively owned assets and the current update scope.
|
|
914
935
|
*/
|
|
915
|
-
function createFullBackup(cwd) {
|
|
916
|
-
const
|
|
917
|
-
|
|
918
|
-
for (const dir of BACKUP_DIRS) {
|
|
919
|
-
const dirPath = path.join(cwd, dir);
|
|
920
|
-
if (!fs.existsSync(dirPath))
|
|
921
|
-
continue;
|
|
922
|
-
const files = collectAllFiles(dirPath, cwd);
|
|
923
|
-
for (const fullPath of files) {
|
|
924
|
-
const relativePath = path.relative(cwd, fullPath);
|
|
925
|
-
// Skip excluded paths
|
|
926
|
-
if (shouldExcludeFromBackup(relativePath))
|
|
927
|
-
continue;
|
|
928
|
-
// Create backup
|
|
929
|
-
if (!hasFiles) {
|
|
930
|
-
fs.mkdirSync(backupDir, { recursive: true });
|
|
931
|
-
hasFiles = true;
|
|
932
|
-
}
|
|
933
|
-
backupFile(cwd, backupDir, relativePath);
|
|
934
|
-
}
|
|
935
|
-
}
|
|
936
|
-
for (const relativePath of BACKUP_FILES) {
|
|
937
|
-
const fullPath = path.join(cwd, relativePath);
|
|
938
|
-
if (!fs.existsSync(fullPath))
|
|
939
|
-
continue;
|
|
940
|
-
if (shouldExcludeFromBackup(relativePath))
|
|
941
|
-
continue;
|
|
942
|
-
if (!hasFiles) {
|
|
943
|
-
fs.mkdirSync(backupDir, { recursive: true });
|
|
944
|
-
hasFiles = true;
|
|
945
|
-
}
|
|
946
|
-
backupFile(cwd, backupDir, relativePath);
|
|
947
|
-
}
|
|
948
|
-
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);
|
|
949
939
|
}
|
|
950
940
|
/**
|
|
951
941
|
* Update version file
|
|
@@ -1584,580 +1574,632 @@ export function renameTracesToJournal(workspaceDir) {
|
|
|
1584
1574
|
*/
|
|
1585
1575
|
export async function update(options) {
|
|
1586
1576
|
const cwd = process.cwd();
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
}
|
|
1593
|
-
console.log(chalk.cyan("\nTrellis Update"));
|
|
1594
|
-
console.log(chalk.cyan("══════════════\n"));
|
|
1595
|
-
// Set up proxy before any network calls (npm version check)
|
|
1596
|
-
setupProxy();
|
|
1597
|
-
// Get versions
|
|
1598
|
-
const projectVersion = getInstalledVersion(cwd);
|
|
1599
|
-
const cliVersion = VERSION;
|
|
1600
|
-
const latestNpmVersion = await getLatestNpmVersion();
|
|
1601
|
-
// Version comparison
|
|
1602
|
-
const cliVsProject = compareVersions(cliVersion, projectVersion);
|
|
1603
|
-
const cliVsNpm = latestNpmVersion
|
|
1604
|
-
? compareVersions(cliVersion, latestNpmVersion)
|
|
1605
|
-
: 0;
|
|
1606
|
-
// Display versions with context
|
|
1607
|
-
console.log(`Project version: ${chalk.white(projectVersion)}`);
|
|
1608
|
-
console.log(`CLI version: ${chalk.white(cliVersion)}`);
|
|
1609
|
-
if (latestNpmVersion) {
|
|
1610
|
-
console.log(`Latest on npm: ${chalk.white(latestNpmVersion)}`);
|
|
1611
|
-
}
|
|
1612
|
-
else {
|
|
1613
|
-
console.log(chalk.gray("Latest on npm: (unable to fetch)"));
|
|
1614
|
-
}
|
|
1615
|
-
console.log("");
|
|
1616
|
-
// Check if CLI is outdated compared to npm
|
|
1617
|
-
if (cliVsNpm < 0 && latestNpmVersion) {
|
|
1618
|
-
console.log(chalk.yellow(`⚠️ Your CLI (${cliVersion}) is behind npm (${latestNpmVersion}).`));
|
|
1619
|
-
console.log(chalk.yellow(` Run: trellis upgrade\n`));
|
|
1620
|
-
}
|
|
1621
|
-
// Check for downgrade situation
|
|
1622
|
-
if (cliVsProject < 0) {
|
|
1623
|
-
console.log(chalk.red(`❌ Cannot update: CLI version (${cliVersion}) < project version (${projectVersion})`));
|
|
1624
|
-
console.log(chalk.red(` This would DOWNGRADE your project!\n`));
|
|
1625
|
-
if (!options.allowDowngrade) {
|
|
1626
|
-
console.log(chalk.gray("Solutions:"));
|
|
1627
|
-
console.log(chalk.gray(` 1. Update your CLI: trellis upgrade`));
|
|
1628
|
-
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."));
|
|
1629
1582
|
return;
|
|
1630
1583
|
}
|
|
1631
|
-
console.log(chalk.
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
console.log(chalk.yellow(" Legacy Codex detected: .agents/skills/ tracked without .codex/ — will create .codex/ directory"));
|
|
1653
|
-
}
|
|
1654
|
-
// Self-heal poisoned manifests: prune entries that no current platform
|
|
1655
|
-
// configurator owns. This silently removes user-owned paths that early
|
|
1656
|
-
// buggy versions of `trellis init` over-hashed (e.g. .codex/sessions/*).
|
|
1657
|
-
// Include codex in known-platforms when codexUpgradeNeeded so legacy Codex
|
|
1658
|
-
// markers under .agents/skills/ survive into the upgrade flow.
|
|
1659
|
-
{
|
|
1660
|
-
const configuredPlatforms = new Set(getConfiguredPlatforms(cwd));
|
|
1661
|
-
if (codexUpgradeNeeded)
|
|
1662
|
-
configuredPlatforms.add("codex");
|
|
1663
|
-
const prune = pruneOrphanManifestKeys(cwd, [...configuredPlatforms], hashes);
|
|
1664
|
-
if (prune.pruned.length > 0) {
|
|
1665
|
-
console.log(chalk.gray(` Pruned ${prune.pruned.length} orphan manifest entries from .template-hashes.json`));
|
|
1666
|
-
hashes = prune.hashes;
|
|
1667
|
-
}
|
|
1668
|
-
}
|
|
1669
|
-
// For breaking releases with recommendMigrate + --migrate, bypass update.skip
|
|
1670
|
-
// across the board (safe-file-delete, new file writes, template updates).
|
|
1671
|
-
// Why: honoring skip here leaves users forever half-migrated — old deprecated
|
|
1672
|
-
// files persist under skip-protected paths, new commands like `continue.md`
|
|
1673
|
-
// never land, and every future update re-flags the same mess. Rename
|
|
1674
|
-
// migrations already ignore update.skip; this makes the rest consistent
|
|
1675
|
-
// during a breaking upgrade. User customizations are still guarded by the
|
|
1676
|
-
// per-file conflict prompt ("Modified by you") at write time.
|
|
1677
|
-
const breakingBypass = options.migrate === true &&
|
|
1678
|
-
cliVsProject > 0 &&
|
|
1679
|
-
projectVersion !== "unknown" &&
|
|
1680
|
-
(() => {
|
|
1681
|
-
const md = getMigrationMetadata(projectVersion, cliVersion);
|
|
1682
|
-
return md.breaking && md.recommendMigrate;
|
|
1683
|
-
})();
|
|
1684
|
-
// Collect templates (used for both migration classification and change analysis)
|
|
1685
|
-
const templates = await collectTemplateFiles(cwd, codexUpgradeNeeded ? new Set(["codex"]) : undefined, breakingBypass);
|
|
1686
|
-
// Load update.skip paths (used for both safe-file-delete and template collection)
|
|
1687
|
-
const skipPaths = loadUpdateSkipPaths(cwd);
|
|
1688
|
-
// Collect safe-file-delete items from ALL manifests (hash match is the safety net)
|
|
1689
|
-
// This runs regardless of version — unknown version still gets safe cleanup
|
|
1690
|
-
const allMigrations = getAllMigrations();
|
|
1691
|
-
const safeFileDeletes = collectSafeFileDeletes(allMigrations, cwd, skipPaths, new Set(templates.keys()), breakingBypass);
|
|
1692
|
-
const hasSafeDeletes = safeFileDeletes.filter((c) => c.action === "delete").length > 0;
|
|
1693
|
-
// Check for pending regular migrations (skip if unknown version)
|
|
1694
|
-
let pendingMigrations = isUnknownVersion
|
|
1695
|
-
? []
|
|
1696
|
-
: getMigrationsForVersion(projectVersion, cliVersion);
|
|
1697
|
-
// Also check for "orphaned" migrations - where source still exists but version says we shouldn't migrate
|
|
1698
|
-
// This handles cases where version was updated but migrations weren't applied
|
|
1699
|
-
const orphanedMigrations = allMigrations.filter((item) => {
|
|
1700
|
-
// Only check rename and rename-dir migrations
|
|
1701
|
-
if (item.type !== "rename" && item.type !== "rename-dir")
|
|
1702
|
-
return false;
|
|
1703
|
-
if (!item.from || !item.to)
|
|
1704
|
-
return false;
|
|
1705
|
-
const oldPath = path.join(cwd, item.from);
|
|
1706
|
-
const newPath = path.join(cwd, item.to);
|
|
1707
|
-
// Orphaned if: source exists AND target doesn't exist
|
|
1708
|
-
// AND this migration isn't already in pendingMigrations
|
|
1709
|
-
const sourceExists = fs.existsSync(oldPath);
|
|
1710
|
-
const targetExists = fs.existsSync(newPath);
|
|
1711
|
-
const alreadyPending = pendingMigrations.some((m) => m.from === item.from && m.to === item.to);
|
|
1712
|
-
return sourceExists && !targetExists && !alreadyPending;
|
|
1713
|
-
});
|
|
1714
|
-
// Add orphaned migrations to pending (they need to be applied)
|
|
1715
|
-
if (orphanedMigrations.length > 0) {
|
|
1716
|
-
console.log(chalk.yellow("⚠️ Detected incomplete migrations from previous updates:"));
|
|
1717
|
-
for (const item of orphanedMigrations) {
|
|
1718
|
-
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)"));
|
|
1719
1605
|
}
|
|
1720
1606
|
console.log("");
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
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;
|
|
1753
1658
|
}
|
|
1754
1659
|
}
|
|
1755
|
-
//
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
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.`));
|
|
1765
1737
|
console.log("");
|
|
1738
|
+
console.log(chalk.yellow(` Run: trellis update --migrate`));
|
|
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);
|
|
1766
1744
|
}
|
|
1767
1745
|
}
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
console.log(chalk.cyan("ℹ️ First update with hash tracking enabled."));
|
|
1782
|
-
console.log(chalk.gray(" Changed files shown above may not be actual user modifications."));
|
|
1783
|
-
console.log(chalk.gray(" After this update, hash tracking will accurately detect changes.\n"));
|
|
1784
|
-
}
|
|
1785
|
-
// Ensure project-root .gitattributes carries the journal merge=union rule.
|
|
1786
|
-
// Additive-only (see ensureGitattributes) — runs regardless of whether
|
|
1787
|
-
// other template files changed, so it must sit before the "nothing to do"
|
|
1788
|
-
// early-return below. Never touches disk in --dry-run.
|
|
1789
|
-
if (!options.dryRun) {
|
|
1790
|
-
ensureGitattributes(cwd);
|
|
1791
|
-
}
|
|
1792
|
-
// Check if there's anything to do
|
|
1793
|
-
const isUpgrade = cliVsProject > 0;
|
|
1794
|
-
const isDowngrade = cliVsProject < 0;
|
|
1795
|
-
const isSameVersion = cliVsProject === 0;
|
|
1796
|
-
// Check if we have pending migrations that need to be applied
|
|
1797
|
-
const hasPendingMigrations = options.migrate &&
|
|
1798
|
-
classifiedMigrations &&
|
|
1799
|
-
(classifiedMigrations.auto.length > 0 ||
|
|
1800
|
-
classifiedMigrations.confirm.length > 0);
|
|
1801
|
-
if (changes.newFiles.length === 0 &&
|
|
1802
|
-
changes.autoUpdateFiles.length === 0 &&
|
|
1803
|
-
changes.changedFiles.length === 0 &&
|
|
1804
|
-
!hasPendingMigrations &&
|
|
1805
|
-
!hasSafeDeletes) {
|
|
1806
|
-
// The "already up to date" exit still has to repair the receipt: this is
|
|
1807
|
-
// exactly the clean tree where every file is `unchanged`, so it is the run
|
|
1808
|
-
// where a wrong entry would otherwise be skipped again.
|
|
1809
|
-
if (!options.dryRun && unchangedFileHashRepairs.size > 0) {
|
|
1810
|
-
updateHashes(cwd, unchangedFileHashRepairs);
|
|
1811
|
-
}
|
|
1812
|
-
if (isSameVersion) {
|
|
1813
|
-
console.log(chalk.green("✓ Already up to date!"));
|
|
1814
|
-
}
|
|
1815
|
-
else {
|
|
1816
|
-
// Version changed but no file changes needed — still update the version stamp
|
|
1817
|
-
updateVersionFile(cwd);
|
|
1818
|
-
if (isUpgrade) {
|
|
1819
|
-
console.log(chalk.green(`✓ No file changes needed for ${projectVersion} → ${cliVersion}`));
|
|
1820
|
-
}
|
|
1821
|
-
else if (isDowngrade) {
|
|
1822
|
-
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
|
+
}
|
|
1823
1759
|
}
|
|
1824
1760
|
}
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
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);
|
|
1846
1802
|
}
|
|
1847
|
-
if (
|
|
1848
|
-
console.log("");
|
|
1849
|
-
console.log(chalk.bgGreen.black.bold(" 💡 RECOMMENDED ") +
|
|
1850
|
-
chalk.green.bold(" Run with --migrate to complete the migration"));
|
|
1803
|
+
if (isSameVersion) {
|
|
1804
|
+
console.log(chalk.green("✓ Already up to date!"));
|
|
1851
1805
|
}
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
if (
|
|
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
|
+
}
|
|
1815
|
+
}
|
|
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) {
|
|
1859
1835
|
console.log("");
|
|
1860
|
-
console.log(chalk.
|
|
1861
|
-
chalk.yellow.bold(` Breaking release — ${willBypass.length.toString()} file(s) under your update.skip paths will be cleaned up.`));
|
|
1862
|
-
console.log(chalk.gray(" Hash-verified: only files matching known Trellis templates are deleted. Your local customizations (hash mismatch) are still preserved."));
|
|
1836
|
+
console.log(chalk.white(preConfirmMetadata.changelog[0]));
|
|
1863
1837
|
}
|
|
1838
|
+
if (preConfirmMetadata.recommendMigrate && !options.migrate) {
|
|
1839
|
+
console.log("");
|
|
1840
|
+
console.log(chalk.bgGreen.black.bold(" 💡 RECOMMENDED ") +
|
|
1841
|
+
chalk.green.bold(" Run with --migrate to complete the migration"));
|
|
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("");
|
|
1864
1858
|
}
|
|
1865
|
-
console.log(chalk.cyan("═".repeat(60)));
|
|
1866
|
-
console.log("");
|
|
1867
1859
|
}
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
console.log(chalk.gray("[Dry run] No changes made."));
|
|
1872
|
-
return;
|
|
1873
|
-
}
|
|
1874
|
-
// Batch-resolution flags are explicit consent for non-interactive runs.
|
|
1875
|
-
// Prompting here breaks CI and `node ... update --force --migrate` smoke tests.
|
|
1876
|
-
if (!options.force && !options.skipAll && !options.createNew) {
|
|
1877
|
-
const { proceed } = await inquirer.prompt([
|
|
1878
|
-
{
|
|
1879
|
-
type: "confirm",
|
|
1880
|
-
name: "proceed",
|
|
1881
|
-
message: "Proceed?",
|
|
1882
|
-
default: true,
|
|
1883
|
-
},
|
|
1884
|
-
]);
|
|
1885
|
-
if (!proceed) {
|
|
1886
|
-
console.log(chalk.yellow("Update cancelled."));
|
|
1860
|
+
// Dry run mode
|
|
1861
|
+
if (options.dryRun) {
|
|
1862
|
+
console.log(chalk.gray("[Dry run] No changes made."));
|
|
1887
1863
|
return;
|
|
1888
1864
|
}
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
// traces-*.md files are in .trellis/workspace/{developer}/ with variable developer names
|
|
1905
|
-
// and variable file numbers (traces-1.md, traces-2.md, etc.), so we can't enumerate them
|
|
1906
|
-
// in the migration manifest. This is a one-time migration for the 0.2.0 naming redesign.
|
|
1907
|
-
const workspaceDir = path.join(cwd, PATHS.WORKSPACE);
|
|
1908
|
-
const { renamed: journalRenamed, skipped: journalSkipped } = renameTracesToJournal(workspaceDir);
|
|
1909
|
-
if (journalRenamed > 0) {
|
|
1910
|
-
console.log(chalk.cyan(`Renamed ${journalRenamed} traces file(s) to journal`));
|
|
1911
|
-
}
|
|
1912
|
-
for (const oldPath of journalSkipped) {
|
|
1913
|
-
console.warn(chalk.yellow(`Kept ${path.relative(cwd, oldPath)}: its journal target already exists`));
|
|
1914
|
-
}
|
|
1915
|
-
}
|
|
1916
|
-
// Execute safe-file-delete (after backup, before template writes)
|
|
1917
|
-
let safeDeleted = 0;
|
|
1918
|
-
if (hasSafeDeletes) {
|
|
1919
|
-
safeDeleted = executeSafeFileDeletes(safeFileDeletes, cwd);
|
|
1920
|
-
if (safeDeleted > 0) {
|
|
1921
|
-
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
|
+
}
|
|
1922
1880
|
}
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
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`));
|
|
1941
1937
|
}
|
|
1942
|
-
console.log(chalk.green(` + ${file.relativePath}`));
|
|
1943
|
-
added++;
|
|
1944
1938
|
}
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
// Make scripts executable
|
|
1952
|
-
if (file.relativePath.endsWith(".sh") ||
|
|
1953
|
-
file.relativePath.endsWith(".py")) {
|
|
1954
|
-
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)`));
|
|
1955
1945
|
}
|
|
1956
|
-
console.log(chalk.cyan(` ↑ ${file.relativePath}`));
|
|
1957
|
-
autoUpdated++;
|
|
1958
1946
|
}
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
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 });
|
|
1967
1959
|
fs.writeFileSync(file.path, file.newContent);
|
|
1960
|
+
// Make scripts executable
|
|
1968
1961
|
if (file.relativePath.endsWith(".sh") ||
|
|
1969
1962
|
file.relativePath.endsWith(".py")) {
|
|
1970
1963
|
fs.chmodSync(file.path, "755");
|
|
1971
1964
|
}
|
|
1972
|
-
console.log(chalk.
|
|
1973
|
-
|
|
1965
|
+
console.log(chalk.green(` + ${file.relativePath}`));
|
|
1966
|
+
added++;
|
|
1974
1967
|
}
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
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++;
|
|
1980
1981
|
}
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
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
|
+
}
|
|
1984
2008
|
}
|
|
1985
2009
|
}
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
configSectionsAppended = appended;
|
|
1996
|
-
}
|
|
1997
|
-
}
|
|
1998
|
-
// Update version file
|
|
1999
|
-
updateVersionFile(cwd);
|
|
2000
|
-
// Update template hashes for new, auto-updated, and overwritten files
|
|
2001
|
-
const filesToHash = new Map(unchangedFileHashRepairs);
|
|
2002
|
-
for (const file of changes.newFiles) {
|
|
2003
|
-
filesToHash.set(file.relativePath, file.newContent);
|
|
2004
|
-
}
|
|
2005
|
-
// Auto-updated files always get new hash
|
|
2006
|
-
for (const file of changes.autoUpdateFiles) {
|
|
2007
|
-
filesToHash.set(file.relativePath, file.newContent);
|
|
2008
|
-
}
|
|
2009
|
-
// Only hash overwritten files (not skipped or .new copies)
|
|
2010
|
-
for (const file of changes.changedFiles) {
|
|
2011
|
-
const fullPath = path.join(cwd, file.relativePath);
|
|
2012
|
-
if (fs.existsSync(fullPath)) {
|
|
2013
|
-
const content = fs.readFileSync(fullPath, "utf-8");
|
|
2014
|
-
if (content === file.newContent) {
|
|
2015
|
-
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;
|
|
2016
2019
|
}
|
|
2017
2020
|
}
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
console.log(
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
if (
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
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);
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
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
|
+
}
|
|
2083
2103
|
}
|
|
2084
2104
|
}
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
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;
|
|
2118
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
|
+
}
|
|
2119
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."));
|
|
2120
2170
|
}
|
|
2121
|
-
// Write PRD
|
|
2122
|
-
const prdPath = path.join(taskDir, "prd.md");
|
|
2123
|
-
fs.writeFileSync(prdPath, prdContent);
|
|
2124
|
-
console.log("");
|
|
2125
|
-
console.log(chalk.bgCyan.black.bold(" 📋 MIGRATION TASK CREATED "));
|
|
2126
|
-
console.log(chalk.cyan(`A task has been created to help you complete the migration:`));
|
|
2127
|
-
console.log(chalk.white(` ${DIR_NAMES.WORKFLOW}/${DIR_NAMES.TASKS}/${taskDirName}/`));
|
|
2128
|
-
console.log("");
|
|
2129
|
-
console.log(chalk.gray("Use AI to help: Ask Claude/Cursor to read the task and fix your custom files."));
|
|
2130
2171
|
}
|
|
2131
2172
|
}
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
console.log("");
|
|
2140
|
-
console.log(chalk.cyan("═".repeat(60)));
|
|
2141
|
-
if (finalMetadata.breaking) {
|
|
2142
|
-
console.log(chalk.bgRed.white.bold(" ⚠️ BREAKING CHANGES ") +
|
|
2143
|
-
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) {
|
|
2144
2180
|
console.log("");
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
console.log(
|
|
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("");
|
|
2150
2186
|
}
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
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)));
|
|
2158
2201
|
}
|
|
2159
|
-
console.log(chalk.cyan("═".repeat(60)));
|
|
2160
2202
|
}
|
|
2161
|
-
}
|
|
2203
|
+
}, options.dryRun);
|
|
2162
2204
|
}
|
|
2163
2205
|
//# sourceMappingURL=update.js.map
|