@fifine/aim-studio 0.0.1
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/LICENSE +21 -0
- package/README.md +159 -0
- package/bin/aim.js +3 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +89 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/commands/init.d.ts +13 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +513 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/update.d.ts +27 -0
- package/dist/commands/update.d.ts.map +1 -0
- package/dist/commands/update.js +1275 -0
- package/dist/commands/update.js.map +1 -0
- package/dist/configurators/claude.d.ts +32 -0
- package/dist/configurators/claude.d.ts.map +1 -0
- package/dist/configurators/claude.js +98 -0
- package/dist/configurators/claude.js.map +1 -0
- package/dist/configurators/index.d.ts +51 -0
- package/dist/configurators/index.d.ts.map +1 -0
- package/dist/configurators/index.js +113 -0
- package/dist/configurators/index.js.map +1 -0
- package/dist/configurators/shared.d.ts +12 -0
- package/dist/configurators/shared.d.ts.map +1 -0
- package/dist/configurators/shared.js +21 -0
- package/dist/configurators/shared.js.map +1 -0
- package/dist/configurators/workflow.d.ts +28 -0
- package/dist/configurators/workflow.d.ts.map +1 -0
- package/dist/configurators/workflow.js +147 -0
- package/dist/configurators/workflow.js.map +1 -0
- package/dist/constants/paths.d.ts +68 -0
- package/dist/constants/paths.d.ts.map +1 -0
- package/dist/constants/paths.js +77 -0
- package/dist/constants/paths.js.map +1 -0
- package/dist/constants/version.d.ts +9 -0
- package/dist/constants/version.d.ts.map +1 -0
- package/dist/constants/version.js +15 -0
- package/dist/constants/version.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/migrations/index.d.ts +54 -0
- package/dist/migrations/index.d.ts.map +1 -0
- package/dist/migrations/index.js +160 -0
- package/dist/migrations/index.js.map +1 -0
- package/dist/migrations/manifests/0.0.1.json +9 -0
- package/dist/migrations/manifests/0.1.9.json +30 -0
- package/dist/migrations/manifests/0.2.0.json +49 -0
- package/dist/migrations/manifests/0.2.12.json +9 -0
- package/dist/migrations/manifests/0.2.13.json +9 -0
- package/dist/migrations/manifests/0.2.14.json +175 -0
- package/dist/migrations/manifests/0.2.15.json +33 -0
- package/dist/migrations/manifests/0.3.0-beta.0.json +278 -0
- package/dist/migrations/manifests/0.3.0-beta.1.json +9 -0
- package/dist/migrations/manifests/0.3.0-beta.10.json +9 -0
- package/dist/migrations/manifests/0.3.0-beta.11.json +9 -0
- package/dist/migrations/manifests/0.3.0-beta.12.json +9 -0
- package/dist/migrations/manifests/0.3.0-beta.13.json +9 -0
- package/dist/migrations/manifests/0.3.0-beta.14.json +9 -0
- package/dist/migrations/manifests/0.3.0-beta.15.json +9 -0
- package/dist/migrations/manifests/0.3.0-beta.16.json +9 -0
- package/dist/migrations/manifests/0.3.0-beta.2.json +9 -0
- package/dist/migrations/manifests/0.3.0-beta.3.json +9 -0
- package/dist/migrations/manifests/0.3.0-beta.4.json +9 -0
- package/dist/migrations/manifests/0.3.0-beta.5.json +9 -0
- package/dist/migrations/manifests/0.3.0-beta.6.json +9 -0
- package/dist/migrations/manifests/0.3.0-beta.7.json +11 -0
- package/dist/migrations/manifests/0.3.0-beta.8.json +9 -0
- package/dist/migrations/manifests/0.3.0-beta.9.json +9 -0
- package/dist/migrations/manifests/0.3.0-rc.0.json +9 -0
- package/dist/migrations/manifests/0.3.0-rc.1.json +9 -0
- package/dist/migrations/manifests/0.3.0-rc.2.json +9 -0
- package/dist/templates/CLAUDE.md +71 -0
- package/dist/templates/aim/gitignore.txt +29 -0
- package/dist/templates/aim/index.d.ts +49 -0
- package/dist/templates/aim/index.d.ts.map +1 -0
- package/dist/templates/aim/index.js +92 -0
- package/dist/templates/aim/index.js.map +1 -0
- package/dist/templates/aim/scripts/__init__.py +5 -0
- package/dist/templates/aim/scripts/add_session.py +392 -0
- package/dist/templates/aim/scripts/common/__init__.py +80 -0
- package/dist/templates/aim/scripts/common/cli_adapter.py +435 -0
- package/dist/templates/aim/scripts/common/developer.py +190 -0
- package/dist/templates/aim/scripts/common/git_context.py +383 -0
- package/dist/templates/aim/scripts/common/paths.py +347 -0
- package/dist/templates/aim/scripts/common/phase.py +253 -0
- package/dist/templates/aim/scripts/common/registry.py +366 -0
- package/dist/templates/aim/scripts/common/task_queue.py +255 -0
- package/dist/templates/aim/scripts/common/task_utils.py +178 -0
- package/dist/templates/aim/scripts/common/worktree.py +219 -0
- package/dist/templates/aim/scripts/create_bootstrap.py +290 -0
- package/dist/templates/aim/scripts/get_context.py +16 -0
- package/dist/templates/aim/scripts/get_developer.py +26 -0
- package/dist/templates/aim/scripts/init_developer.py +51 -0
- package/dist/templates/aim/scripts/multi_agent/__init__.py +5 -0
- package/dist/templates/aim/scripts/multi_agent/cleanup.py +403 -0
- package/dist/templates/aim/scripts/multi_agent/create_pr.py +329 -0
- package/dist/templates/aim/scripts/multi_agent/plan.py +233 -0
- package/dist/templates/aim/scripts/multi_agent/start.py +461 -0
- package/dist/templates/aim/scripts/multi_agent/status.py +817 -0
- package/dist/templates/aim/scripts/task.py +1068 -0
- package/dist/templates/aim/scripts-shell-archive/add-session.sh +384 -0
- package/dist/templates/aim/scripts-shell-archive/common/developer.sh +129 -0
- package/dist/templates/aim/scripts-shell-archive/common/git-context.sh +263 -0
- package/dist/templates/aim/scripts-shell-archive/common/paths.sh +208 -0
- package/dist/templates/aim/scripts-shell-archive/common/phase.sh +150 -0
- package/dist/templates/aim/scripts-shell-archive/common/registry.sh +247 -0
- package/dist/templates/aim/scripts-shell-archive/common/task-queue.sh +142 -0
- package/dist/templates/aim/scripts-shell-archive/common/task-utils.sh +151 -0
- package/dist/templates/aim/scripts-shell-archive/common/worktree.sh +128 -0
- package/dist/templates/aim/scripts-shell-archive/create-bootstrap.sh +299 -0
- package/dist/templates/aim/scripts-shell-archive/get-context.sh +7 -0
- package/dist/templates/aim/scripts-shell-archive/get-developer.sh +15 -0
- package/dist/templates/aim/scripts-shell-archive/init-developer.sh +34 -0
- package/dist/templates/aim/scripts-shell-archive/multi-agent/cleanup.sh +396 -0
- package/dist/templates/aim/scripts-shell-archive/multi-agent/create-pr.sh +241 -0
- package/dist/templates/aim/scripts-shell-archive/multi-agent/plan.sh +207 -0
- package/dist/templates/aim/scripts-shell-archive/multi-agent/start.sh +317 -0
- package/dist/templates/aim/scripts-shell-archive/multi-agent/status.sh +828 -0
- package/dist/templates/aim/scripts-shell-archive/task.sh +1204 -0
- package/dist/templates/aim/tasks/.gitkeep +0 -0
- package/dist/templates/aim/workflow.md +258 -0
- package/dist/templates/aim/worktree.yaml +47 -0
- package/dist/templates/claude/agents/check.md +122 -0
- package/dist/templates/claude/agents/debug.md +106 -0
- package/dist/templates/claude/agents/dispatch.md +230 -0
- package/dist/templates/claude/agents/implement.md +96 -0
- package/dist/templates/claude/agents/plan.md +396 -0
- package/dist/templates/claude/agents/research.md +120 -0
- package/dist/templates/claude/agents/story.md +53 -0
- package/dist/templates/claude/commands/aim/before-backend-dev.md +13 -0
- package/dist/templates/claude/commands/aim/before-frontend-dev.md +13 -0
- package/dist/templates/claude/commands/aim/break-loop.md +153 -0
- package/dist/templates/claude/commands/aim/check-backend.md +13 -0
- package/dist/templates/claude/commands/aim/check-cross-layer.md +153 -0
- package/dist/templates/claude/commands/aim/check-frontend.md +13 -0
- package/dist/templates/claude/commands/aim/check-story.md +59 -0
- package/dist/templates/claude/commands/aim/create-command.md +154 -0
- package/dist/templates/claude/commands/aim/export.md +187 -0
- package/dist/templates/claude/commands/aim/finish-work.md +104 -0
- package/dist/templates/claude/commands/aim/integrate-skill.md +219 -0
- package/dist/templates/claude/commands/aim/onboard.md +358 -0
- package/dist/templates/claude/commands/aim/parallel.md +217 -0
- package/dist/templates/claude/commands/aim/portrait.md +170 -0
- package/dist/templates/claude/commands/aim/record-session.md +92 -0
- package/dist/templates/claude/commands/aim/start.md +112 -0
- package/dist/templates/claude/commands/aim/story.md +140 -0
- package/dist/templates/claude/commands/aim/update-spec.md +285 -0
- package/dist/templates/claude/commands/aim/visualize.md +182 -0
- package/dist/templates/claude/commands/trellis/before-backend-dev.md +13 -0
- package/dist/templates/claude/commands/trellis/before-frontend-dev.md +13 -0
- package/dist/templates/claude/commands/trellis/break-loop.md +125 -0
- package/dist/templates/claude/commands/trellis/check-backend.md +13 -0
- package/dist/templates/claude/commands/trellis/check-cross-layer.md +153 -0
- package/dist/templates/claude/commands/trellis/check-frontend.md +13 -0
- package/dist/templates/claude/commands/trellis/create-command.md +154 -0
- package/dist/templates/claude/commands/trellis/finish-work.md +129 -0
- package/dist/templates/claude/commands/trellis/integrate-skill.md +219 -0
- package/dist/templates/claude/commands/trellis/onboard.md +358 -0
- package/dist/templates/claude/commands/trellis/parallel.md +193 -0
- package/dist/templates/claude/commands/trellis/record-session.md +62 -0
- package/dist/templates/claude/commands/trellis/start.md +280 -0
- package/dist/templates/claude/commands/trellis/update-spec.md +285 -0
- package/dist/templates/claude/hooks/inject-subagent-context.py +772 -0
- package/dist/templates/claude/hooks/ralph-loop.py +388 -0
- package/dist/templates/claude/hooks/session-start.py +142 -0
- package/dist/templates/claude/index.d.ts +54 -0
- package/dist/templates/claude/index.d.ts.map +1 -0
- package/dist/templates/claude/index.js +85 -0
- package/dist/templates/claude/index.js.map +1 -0
- package/dist/templates/claude/settings.json +41 -0
- package/dist/templates/extract.d.ts +68 -0
- package/dist/templates/extract.d.ts.map +1 -0
- package/dist/templates/extract.js +128 -0
- package/dist/templates/extract.js.map +1 -0
- package/dist/templates/markdown/agents.md +25 -0
- package/dist/templates/markdown/gitignore.txt +12 -0
- package/dist/templates/markdown/index.d.ts +32 -0
- package/dist/templates/markdown/index.d.ts.map +1 -0
- package/dist/templates/markdown/index.js +58 -0
- package/dist/templates/markdown/index.js.map +1 -0
- package/dist/templates/markdown/spec/backend/database-guidelines.md.txt +51 -0
- package/dist/templates/markdown/spec/backend/directory-structure.md.txt +54 -0
- package/dist/templates/markdown/spec/backend/error-handling.md.txt +51 -0
- package/dist/templates/markdown/spec/backend/index.md +40 -0
- package/dist/templates/markdown/spec/backend/index.md.txt +38 -0
- package/dist/templates/markdown/spec/backend/logging-guidelines.md.txt +51 -0
- package/dist/templates/markdown/spec/backend/quality-guidelines.md.txt +51 -0
- package/dist/templates/markdown/spec/backend/script-conventions.md +467 -0
- package/dist/templates/markdown/spec/frontend/component-guidelines.md.txt +59 -0
- package/dist/templates/markdown/spec/frontend/directory-structure.md.txt +54 -0
- package/dist/templates/markdown/spec/frontend/hook-guidelines.md.txt +51 -0
- package/dist/templates/markdown/spec/frontend/index.md.txt +39 -0
- package/dist/templates/markdown/spec/frontend/quality-guidelines.md.txt +51 -0
- package/dist/templates/markdown/spec/frontend/state-management.md.txt +51 -0
- package/dist/templates/markdown/spec/frontend/type-safety.md.txt +51 -0
- package/dist/templates/markdown/spec/guides/code-reuse-thinking-guide.md +118 -0
- package/dist/templates/markdown/spec/guides/code-reuse-thinking-guide.md.txt +92 -0
- package/dist/templates/markdown/spec/guides/cross-layer-thinking-guide.md.txt +94 -0
- package/dist/templates/markdown/spec/guides/cross-platform-thinking-guide.md +394 -0
- package/dist/templates/markdown/spec/guides/cross-platform-thinking-guide.md.txt +319 -0
- package/dist/templates/markdown/spec/guides/index.md.txt +89 -0
- package/dist/templates/markdown/spec/story/character.md.txt +95 -0
- package/dist/templates/markdown/spec/story/index.md.txt +31 -0
- package/dist/templates/markdown/spec/story/script.md.txt +313 -0
- package/dist/templates/markdown/spec/story/world.md.txt +92 -0
- package/dist/templates/markdown/workspace-index.md +123 -0
- package/dist/templates/markdown/worktree.yaml.txt +58 -0
- package/dist/templates/trellis/gitignore.txt +29 -0
- package/dist/templates/trellis/index.d.ts +49 -0
- package/dist/templates/trellis/index.d.ts.map +1 -0
- package/dist/templates/trellis/index.js +92 -0
- package/dist/templates/trellis/index.js.map +1 -0
- package/dist/templates/trellis/scripts/__init__.py +5 -0
- package/dist/templates/trellis/scripts/add_session.py +392 -0
- package/dist/templates/trellis/scripts/common/__init__.py +80 -0
- package/dist/templates/trellis/scripts/common/cli_adapter.py +435 -0
- package/dist/templates/trellis/scripts/common/developer.py +190 -0
- package/dist/templates/trellis/scripts/common/git_context.py +383 -0
- package/dist/templates/trellis/scripts/common/paths.py +347 -0
- package/dist/templates/trellis/scripts/common/phase.py +253 -0
- package/dist/templates/trellis/scripts/common/registry.py +366 -0
- package/dist/templates/trellis/scripts/common/task_queue.py +255 -0
- package/dist/templates/trellis/scripts/common/task_utils.py +178 -0
- package/dist/templates/trellis/scripts/common/worktree.py +219 -0
- package/dist/templates/trellis/scripts/create_bootstrap.py +290 -0
- package/dist/templates/trellis/scripts/get_context.py +16 -0
- package/dist/templates/trellis/scripts/get_developer.py +26 -0
- package/dist/templates/trellis/scripts/init_developer.py +51 -0
- package/dist/templates/trellis/scripts/multi_agent/__init__.py +5 -0
- package/dist/templates/trellis/scripts/multi_agent/cleanup.py +403 -0
- package/dist/templates/trellis/scripts/multi_agent/create_pr.py +329 -0
- package/dist/templates/trellis/scripts/multi_agent/plan.py +233 -0
- package/dist/templates/trellis/scripts/multi_agent/start.py +461 -0
- package/dist/templates/trellis/scripts/multi_agent/status.py +817 -0
- package/dist/templates/trellis/scripts/task.py +1056 -0
- package/dist/templates/trellis/scripts-shell-archive/add-session.sh +384 -0
- package/dist/templates/trellis/scripts-shell-archive/common/developer.sh +129 -0
- package/dist/templates/trellis/scripts-shell-archive/common/git-context.sh +263 -0
- package/dist/templates/trellis/scripts-shell-archive/common/paths.sh +208 -0
- package/dist/templates/trellis/scripts-shell-archive/common/phase.sh +150 -0
- package/dist/templates/trellis/scripts-shell-archive/common/registry.sh +247 -0
- package/dist/templates/trellis/scripts-shell-archive/common/task-queue.sh +142 -0
- package/dist/templates/trellis/scripts-shell-archive/common/task-utils.sh +151 -0
- package/dist/templates/trellis/scripts-shell-archive/common/worktree.sh +128 -0
- package/dist/templates/trellis/scripts-shell-archive/create-bootstrap.sh +299 -0
- package/dist/templates/trellis/scripts-shell-archive/get-context.sh +7 -0
- package/dist/templates/trellis/scripts-shell-archive/get-developer.sh +15 -0
- package/dist/templates/trellis/scripts-shell-archive/init-developer.sh +34 -0
- package/dist/templates/trellis/scripts-shell-archive/multi-agent/cleanup.sh +396 -0
- package/dist/templates/trellis/scripts-shell-archive/multi-agent/create-pr.sh +241 -0
- package/dist/templates/trellis/scripts-shell-archive/multi-agent/plan.sh +207 -0
- package/dist/templates/trellis/scripts-shell-archive/multi-agent/start.sh +317 -0
- package/dist/templates/trellis/scripts-shell-archive/multi-agent/status.sh +828 -0
- package/dist/templates/trellis/scripts-shell-archive/task.sh +1204 -0
- package/dist/templates/trellis/tasks/.gitkeep +0 -0
- package/dist/templates/trellis/workflow.md +416 -0
- package/dist/templates/trellis/worktree.yaml +47 -0
- package/dist/types/ai-tools.d.ts +48 -0
- package/dist/types/ai-tools.d.ts.map +1 -0
- package/dist/types/ai-tools.js +32 -0
- package/dist/types/ai-tools.js.map +1 -0
- package/dist/types/migration.d.ts +86 -0
- package/dist/types/migration.d.ts.map +1 -0
- package/dist/types/migration.js +8 -0
- package/dist/types/migration.js.map +1 -0
- package/dist/utils/compare-versions.d.ts +12 -0
- package/dist/utils/compare-versions.d.ts.map +1 -0
- package/dist/utils/compare-versions.js +76 -0
- package/dist/utils/compare-versions.js.map +1 -0
- package/dist/utils/file-writer.d.ts +23 -0
- package/dist/utils/file-writer.d.ts.map +1 -0
- package/dist/utils/file-writer.js +140 -0
- package/dist/utils/file-writer.js.map +1 -0
- package/dist/utils/project-detector.d.ts +16 -0
- package/dist/utils/project-detector.d.ts.map +1 -0
- package/dist/utils/project-detector.js +188 -0
- package/dist/utils/project-detector.js.map +1 -0
- package/dist/utils/template-fetcher.d.ts +51 -0
- package/dist/utils/template-fetcher.d.ts.map +1 -0
- package/dist/utils/template-fetcher.js +174 -0
- package/dist/utils/template-fetcher.js.map +1 -0
- package/dist/utils/template-hash.d.ts +78 -0
- package/dist/utils/template-hash.d.ts.map +1 -0
- package/dist/utils/template-hash.js +239 -0
- package/dist/utils/template-hash.js.map +1 -0
- package/package.json +87 -0
|
@@ -0,0 +1,1275 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import inquirer from "inquirer";
|
|
5
|
+
import { PATHS, DIR_NAMES } from "../constants/paths.js";
|
|
6
|
+
import { VERSION, PACKAGE_NAME } from "../constants/version.js";
|
|
7
|
+
import { getMigrationsForVersion, getAllMigrations, getMigrationMetadata, } from "../migrations/index.js";
|
|
8
|
+
import { loadHashes, saveHashes, updateHashes, isTemplateModified, removeHash, renameHash, computeHash, } from "../utils/template-hash.js";
|
|
9
|
+
import { compareVersions } from "../utils/compare-versions.js";
|
|
10
|
+
// Import templates for comparison
|
|
11
|
+
import {
|
|
12
|
+
// Python scripts - package init
|
|
13
|
+
scriptsInit,
|
|
14
|
+
// Python scripts - common
|
|
15
|
+
commonInit, commonPaths, commonDeveloper, commonGitContext, commonWorktree, commonTaskQueue, commonTaskUtils, commonPhase, commonRegistry, commonCliAdapter,
|
|
16
|
+
// Python scripts - multi_agent
|
|
17
|
+
multiAgentInit, multiAgentStart, multiAgentCleanup, multiAgentStatus, multiAgentCreatePr, multiAgentPlan,
|
|
18
|
+
// Python scripts - main
|
|
19
|
+
getDeveloperScript, initDeveloperScript, taskScript, getContextScript, addSessionScript, createBootstrapScript,
|
|
20
|
+
// Configuration
|
|
21
|
+
worktreeYamlTemplate, workflowMdTemplate, gitignoreTemplate, } from "../templates/aim/index.js";
|
|
22
|
+
import { guidesIndexContent, guidesCrossLayerThinkingGuideContent, guidesCodeReuseThinkingGuideContent,
|
|
23
|
+
// Backend structure (multi-doc)
|
|
24
|
+
backendIndexContent, backendDirectoryStructureContent, backendDatabaseGuidelinesContent, backendLoggingGuidelinesContent, backendQualityGuidelinesContent, backendErrorHandlingContent,
|
|
25
|
+
// Frontend structure (multi-doc)
|
|
26
|
+
frontendIndexContent, frontendDirectoryStructureContent, frontendTypeSafetyContent, frontendHookGuidelinesContent, frontendComponentGuidelinesContent, frontendQualityGuidelinesContent, frontendStateManagementContent,
|
|
27
|
+
// Workspace
|
|
28
|
+
workspaceIndexContent, } from "../templates/markdown/index.js";
|
|
29
|
+
import { ALL_MANAGED_DIRS, getConfiguredPlatforms, collectPlatformTemplates, isManagedPath, isManagedRootDir, } from "../configurators/index.js";
|
|
30
|
+
// Paths that should never be touched (true user data)
|
|
31
|
+
// Note: frontend/backend spec dirs removed - they should be created if missing,
|
|
32
|
+
// and existing files are protected by hash-based modification tracking
|
|
33
|
+
const PROTECTED_PATHS = [
|
|
34
|
+
`${DIR_NAMES.WORKFLOW}/${DIR_NAMES.WORKSPACE}`, // workspace/
|
|
35
|
+
`${DIR_NAMES.WORKFLOW}/${DIR_NAMES.TASKS}`, // tasks/
|
|
36
|
+
`${DIR_NAMES.WORKFLOW}/.developer`,
|
|
37
|
+
`${DIR_NAMES.WORKFLOW}/.current-task`,
|
|
38
|
+
];
|
|
39
|
+
/**
|
|
40
|
+
* Collect all template files that should be managed by update
|
|
41
|
+
* Only collects templates for platforms that are already configured (have directories)
|
|
42
|
+
*/
|
|
43
|
+
function collectTemplateFiles(cwd) {
|
|
44
|
+
const files = new Map();
|
|
45
|
+
const platforms = getConfiguredPlatforms(cwd);
|
|
46
|
+
// Python scripts - package init
|
|
47
|
+
files.set(`${PATHS.SCRIPTS}/__init__.py`, scriptsInit);
|
|
48
|
+
// Python scripts - common
|
|
49
|
+
files.set(`${PATHS.SCRIPTS}/common/__init__.py`, commonInit);
|
|
50
|
+
files.set(`${PATHS.SCRIPTS}/common/paths.py`, commonPaths);
|
|
51
|
+
files.set(`${PATHS.SCRIPTS}/common/developer.py`, commonDeveloper);
|
|
52
|
+
files.set(`${PATHS.SCRIPTS}/common/git_context.py`, commonGitContext);
|
|
53
|
+
files.set(`${PATHS.SCRIPTS}/common/worktree.py`, commonWorktree);
|
|
54
|
+
files.set(`${PATHS.SCRIPTS}/common/task_queue.py`, commonTaskQueue);
|
|
55
|
+
files.set(`${PATHS.SCRIPTS}/common/task_utils.py`, commonTaskUtils);
|
|
56
|
+
files.set(`${PATHS.SCRIPTS}/common/phase.py`, commonPhase);
|
|
57
|
+
files.set(`${PATHS.SCRIPTS}/common/registry.py`, commonRegistry);
|
|
58
|
+
files.set(`${PATHS.SCRIPTS}/common/cli_adapter.py`, commonCliAdapter);
|
|
59
|
+
// Python scripts - multi_agent
|
|
60
|
+
files.set(`${PATHS.SCRIPTS}/multi_agent/__init__.py`, multiAgentInit);
|
|
61
|
+
files.set(`${PATHS.SCRIPTS}/multi_agent/start.py`, multiAgentStart);
|
|
62
|
+
files.set(`${PATHS.SCRIPTS}/multi_agent/cleanup.py`, multiAgentCleanup);
|
|
63
|
+
files.set(`${PATHS.SCRIPTS}/multi_agent/status.py`, multiAgentStatus);
|
|
64
|
+
files.set(`${PATHS.SCRIPTS}/multi_agent/create_pr.py`, multiAgentCreatePr);
|
|
65
|
+
files.set(`${PATHS.SCRIPTS}/multi_agent/plan.py`, multiAgentPlan);
|
|
66
|
+
// Python scripts - main
|
|
67
|
+
files.set(`${PATHS.SCRIPTS}/init_developer.py`, initDeveloperScript);
|
|
68
|
+
files.set(`${PATHS.SCRIPTS}/get_developer.py`, getDeveloperScript);
|
|
69
|
+
files.set(`${PATHS.SCRIPTS}/task.py`, taskScript);
|
|
70
|
+
files.set(`${PATHS.SCRIPTS}/get_context.py`, getContextScript);
|
|
71
|
+
files.set(`${PATHS.SCRIPTS}/add_session.py`, addSessionScript);
|
|
72
|
+
files.set(`${PATHS.SCRIPTS}/create_bootstrap.py`, createBootstrapScript);
|
|
73
|
+
// Configuration
|
|
74
|
+
files.set(`${DIR_NAMES.WORKFLOW}/worktree.yaml`, worktreeYamlTemplate);
|
|
75
|
+
files.set(`${DIR_NAMES.WORKFLOW}/.gitignore`, gitignoreTemplate);
|
|
76
|
+
files.set(PATHS.WORKFLOW_GUIDE_FILE, workflowMdTemplate);
|
|
77
|
+
// Workspace index (template file, not user data)
|
|
78
|
+
files.set(`${PATHS.WORKSPACE}/index.md`, workspaceIndexContent);
|
|
79
|
+
// Spec - guides
|
|
80
|
+
files.set(`${PATHS.SPEC}/guides/index.md`, guidesIndexContent);
|
|
81
|
+
files.set(`${PATHS.SPEC}/guides/cross-layer-thinking-guide.md`, guidesCrossLayerThinkingGuideContent);
|
|
82
|
+
files.set(`${PATHS.SPEC}/guides/code-reuse-thinking-guide.md`, guidesCodeReuseThinkingGuideContent);
|
|
83
|
+
// Spec - backend (created if missing, protected by hash tracking if modified)
|
|
84
|
+
files.set(`${PATHS.SPEC}/backend/index.md`, backendIndexContent);
|
|
85
|
+
files.set(`${PATHS.SPEC}/backend/directory-structure.md`, backendDirectoryStructureContent);
|
|
86
|
+
files.set(`${PATHS.SPEC}/backend/database-guidelines.md`, backendDatabaseGuidelinesContent);
|
|
87
|
+
files.set(`${PATHS.SPEC}/backend/logging-guidelines.md`, backendLoggingGuidelinesContent);
|
|
88
|
+
files.set(`${PATHS.SPEC}/backend/quality-guidelines.md`, backendQualityGuidelinesContent);
|
|
89
|
+
files.set(`${PATHS.SPEC}/backend/error-handling.md`, backendErrorHandlingContent);
|
|
90
|
+
// Spec - frontend (created if missing, protected by hash tracking if modified)
|
|
91
|
+
files.set(`${PATHS.SPEC}/frontend/index.md`, frontendIndexContent);
|
|
92
|
+
files.set(`${PATHS.SPEC}/frontend/directory-structure.md`, frontendDirectoryStructureContent);
|
|
93
|
+
files.set(`${PATHS.SPEC}/frontend/type-safety.md`, frontendTypeSafetyContent);
|
|
94
|
+
files.set(`${PATHS.SPEC}/frontend/hook-guidelines.md`, frontendHookGuidelinesContent);
|
|
95
|
+
files.set(`${PATHS.SPEC}/frontend/component-guidelines.md`, frontendComponentGuidelinesContent);
|
|
96
|
+
files.set(`${PATHS.SPEC}/frontend/quality-guidelines.md`, frontendQualityGuidelinesContent);
|
|
97
|
+
files.set(`${PATHS.SPEC}/frontend/state-management.md`, frontendStateManagementContent);
|
|
98
|
+
// Platform-specific templates (only for configured platforms)
|
|
99
|
+
for (const platformId of platforms) {
|
|
100
|
+
const platformFiles = collectPlatformTemplates(platformId);
|
|
101
|
+
if (platformFiles) {
|
|
102
|
+
for (const [filePath, content] of platformFiles) {
|
|
103
|
+
files.set(filePath, content);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return files;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Analyze changes between current files and templates
|
|
111
|
+
*
|
|
112
|
+
* Uses hash tracking to distinguish between:
|
|
113
|
+
* - User didn't modify + template same = skip (unchangedFiles)
|
|
114
|
+
* - User didn't modify + template updated = auto-update (autoUpdateFiles)
|
|
115
|
+
* - User modified = needs confirmation (changedFiles)
|
|
116
|
+
*/
|
|
117
|
+
function analyzeChanges(cwd, hashes, templates) {
|
|
118
|
+
const result = {
|
|
119
|
+
newFiles: [],
|
|
120
|
+
unchangedFiles: [],
|
|
121
|
+
autoUpdateFiles: [],
|
|
122
|
+
changedFiles: [],
|
|
123
|
+
protectedPaths: PROTECTED_PATHS,
|
|
124
|
+
};
|
|
125
|
+
for (const [relativePath, newContent] of templates) {
|
|
126
|
+
const fullPath = path.join(cwd, relativePath);
|
|
127
|
+
const exists = fs.existsSync(fullPath);
|
|
128
|
+
const change = {
|
|
129
|
+
path: fullPath,
|
|
130
|
+
relativePath,
|
|
131
|
+
newContent,
|
|
132
|
+
status: "new",
|
|
133
|
+
};
|
|
134
|
+
if (!exists) {
|
|
135
|
+
change.status = "new";
|
|
136
|
+
result.newFiles.push(change);
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
const existingContent = fs.readFileSync(fullPath, "utf-8");
|
|
140
|
+
if (existingContent === newContent) {
|
|
141
|
+
// Content same as template - already up to date
|
|
142
|
+
change.status = "unchanged";
|
|
143
|
+
result.unchangedFiles.push(change);
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
// Content differs - check if user modified or template updated
|
|
147
|
+
const storedHash = hashes[relativePath];
|
|
148
|
+
const currentHash = computeHash(existingContent);
|
|
149
|
+
if (storedHash && storedHash === currentHash) {
|
|
150
|
+
// Hash matches stored hash - user didn't modify, template was updated
|
|
151
|
+
// Safe to auto-update
|
|
152
|
+
change.status = "changed";
|
|
153
|
+
result.autoUpdateFiles.push(change);
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
// Hash differs (or no stored hash) - user modified the file
|
|
157
|
+
// Needs confirmation
|
|
158
|
+
change.status = "changed";
|
|
159
|
+
result.changedFiles.push(change);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return result;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Print change summary
|
|
168
|
+
*/
|
|
169
|
+
function printChangeSummary(changes) {
|
|
170
|
+
console.log("\nScanning for changes...\n");
|
|
171
|
+
if (changes.newFiles.length > 0) {
|
|
172
|
+
console.log(chalk.green(" New files (will add):"));
|
|
173
|
+
for (const file of changes.newFiles) {
|
|
174
|
+
console.log(chalk.green(` + ${file.relativePath}`));
|
|
175
|
+
}
|
|
176
|
+
console.log("");
|
|
177
|
+
}
|
|
178
|
+
if (changes.autoUpdateFiles.length > 0) {
|
|
179
|
+
console.log(chalk.cyan(" Template updated (will auto-update):"));
|
|
180
|
+
for (const file of changes.autoUpdateFiles) {
|
|
181
|
+
console.log(chalk.cyan(` ā ${file.relativePath}`));
|
|
182
|
+
}
|
|
183
|
+
console.log("");
|
|
184
|
+
}
|
|
185
|
+
if (changes.unchangedFiles.length > 0) {
|
|
186
|
+
console.log(chalk.gray(" Unchanged files (will skip):"));
|
|
187
|
+
for (const file of changes.unchangedFiles.slice(0, 5)) {
|
|
188
|
+
console.log(chalk.gray(` ā ${file.relativePath}`));
|
|
189
|
+
}
|
|
190
|
+
if (changes.unchangedFiles.length > 5) {
|
|
191
|
+
console.log(chalk.gray(` ... and ${changes.unchangedFiles.length - 5} more`));
|
|
192
|
+
}
|
|
193
|
+
console.log("");
|
|
194
|
+
}
|
|
195
|
+
if (changes.changedFiles.length > 0) {
|
|
196
|
+
console.log(chalk.yellow(" Modified by you (need your decision):"));
|
|
197
|
+
for (const file of changes.changedFiles) {
|
|
198
|
+
console.log(chalk.yellow(` ? ${file.relativePath}`));
|
|
199
|
+
}
|
|
200
|
+
console.log("");
|
|
201
|
+
}
|
|
202
|
+
// Only show protected paths that actually exist
|
|
203
|
+
const existingProtectedPaths = changes.protectedPaths.filter((p) => {
|
|
204
|
+
const fullPath = path.join(process.cwd(), p);
|
|
205
|
+
return fs.existsSync(fullPath);
|
|
206
|
+
});
|
|
207
|
+
if (existingProtectedPaths.length > 0) {
|
|
208
|
+
console.log(chalk.gray(" User data (preserved):"));
|
|
209
|
+
for (const protectedPath of existingProtectedPaths) {
|
|
210
|
+
console.log(chalk.gray(` ā ${protectedPath}/`));
|
|
211
|
+
}
|
|
212
|
+
console.log("");
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Prompt user for conflict resolution
|
|
217
|
+
*/
|
|
218
|
+
async function promptConflictResolution(file, options, applyToAll) {
|
|
219
|
+
// If we have a batch action, use it
|
|
220
|
+
if (applyToAll.action) {
|
|
221
|
+
return applyToAll.action;
|
|
222
|
+
}
|
|
223
|
+
// Check command-line options
|
|
224
|
+
if (options.force) {
|
|
225
|
+
return "overwrite";
|
|
226
|
+
}
|
|
227
|
+
if (options.skipAll) {
|
|
228
|
+
return "skip";
|
|
229
|
+
}
|
|
230
|
+
if (options.createNew) {
|
|
231
|
+
return "create-new";
|
|
232
|
+
}
|
|
233
|
+
// Interactive prompt
|
|
234
|
+
const { action } = await inquirer.prompt([
|
|
235
|
+
{
|
|
236
|
+
type: "list",
|
|
237
|
+
name: "action",
|
|
238
|
+
message: `${file.relativePath} has changes.`,
|
|
239
|
+
choices: [
|
|
240
|
+
{
|
|
241
|
+
name: "[1] Overwrite - Replace with new version",
|
|
242
|
+
value: "overwrite",
|
|
243
|
+
},
|
|
244
|
+
{ name: "[2] Skip - Keep your current version", value: "skip" },
|
|
245
|
+
{
|
|
246
|
+
name: "[3] Create copy - Save new version as .new",
|
|
247
|
+
value: "create-new",
|
|
248
|
+
},
|
|
249
|
+
{ name: "[a] Apply Overwrite to all", value: "overwrite-all" },
|
|
250
|
+
{ name: "[s] Apply Skip to all", value: "skip-all" },
|
|
251
|
+
{ name: "[n] Apply Create copy to all", value: "create-new-all" },
|
|
252
|
+
],
|
|
253
|
+
default: "skip",
|
|
254
|
+
},
|
|
255
|
+
]);
|
|
256
|
+
if (action === "overwrite-all") {
|
|
257
|
+
applyToAll.action = "overwrite";
|
|
258
|
+
return "overwrite";
|
|
259
|
+
}
|
|
260
|
+
if (action === "skip-all") {
|
|
261
|
+
applyToAll.action = "skip";
|
|
262
|
+
return "skip";
|
|
263
|
+
}
|
|
264
|
+
if (action === "create-new-all") {
|
|
265
|
+
applyToAll.action = "create-new";
|
|
266
|
+
return "create-new";
|
|
267
|
+
}
|
|
268
|
+
return action;
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Create a timestamped backup directory path
|
|
272
|
+
*/
|
|
273
|
+
function createBackupDirPath(cwd) {
|
|
274
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
275
|
+
return path.join(cwd, DIR_NAMES.WORKFLOW, `.backup-${timestamp}`);
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Backup a single file to the backup directory
|
|
279
|
+
*/
|
|
280
|
+
function backupFile(cwd, backupDir, relativePath) {
|
|
281
|
+
const srcPath = path.join(cwd, relativePath);
|
|
282
|
+
if (!fs.existsSync(srcPath))
|
|
283
|
+
return;
|
|
284
|
+
const backupPath = path.join(backupDir, relativePath);
|
|
285
|
+
fs.mkdirSync(path.dirname(backupPath), { recursive: true });
|
|
286
|
+
fs.copyFileSync(srcPath, backupPath);
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Directories to backup as complete snapshot (derived from platform registry)
|
|
290
|
+
*/
|
|
291
|
+
const BACKUP_DIRS = ALL_MANAGED_DIRS;
|
|
292
|
+
/**
|
|
293
|
+
* Patterns to exclude from backup (user data that shouldn't be backed up)
|
|
294
|
+
*/
|
|
295
|
+
const BACKUP_EXCLUDE_PATTERNS = [
|
|
296
|
+
".backup-", // Previous backups
|
|
297
|
+
"/workspace/", // Developer workspace (user data)
|
|
298
|
+
"/tasks/", // Task data (user data)
|
|
299
|
+
"/backlog/", // Backlog data (user data)
|
|
300
|
+
"/agent-traces/", // Agent traces (user data, legacy name)
|
|
301
|
+
];
|
|
302
|
+
/**
|
|
303
|
+
* Check if a path should be excluded from backup
|
|
304
|
+
*/
|
|
305
|
+
function shouldExcludeFromBackup(relativePath) {
|
|
306
|
+
for (const pattern of BACKUP_EXCLUDE_PATTERNS) {
|
|
307
|
+
if (relativePath.includes(pattern)) {
|
|
308
|
+
return true;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Create complete snapshot backup of all managed directories
|
|
315
|
+
* Backs up .aim-studio, .claude, .cursor, .iflow, .opencode directories entirely
|
|
316
|
+
* (excluding user data like workspace/, tasks/, backlog/)
|
|
317
|
+
*/
|
|
318
|
+
function createFullBackup(cwd) {
|
|
319
|
+
const backupDir = createBackupDirPath(cwd);
|
|
320
|
+
let hasFiles = false;
|
|
321
|
+
for (const dir of BACKUP_DIRS) {
|
|
322
|
+
const dirPath = path.join(cwd, dir);
|
|
323
|
+
if (!fs.existsSync(dirPath))
|
|
324
|
+
continue;
|
|
325
|
+
const files = collectAllFiles(dirPath);
|
|
326
|
+
for (const fullPath of files) {
|
|
327
|
+
const relativePath = path.relative(cwd, fullPath);
|
|
328
|
+
// Skip excluded paths
|
|
329
|
+
if (shouldExcludeFromBackup(relativePath))
|
|
330
|
+
continue;
|
|
331
|
+
// Create backup
|
|
332
|
+
if (!hasFiles) {
|
|
333
|
+
fs.mkdirSync(backupDir, { recursive: true });
|
|
334
|
+
hasFiles = true;
|
|
335
|
+
}
|
|
336
|
+
backupFile(cwd, backupDir, relativePath);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return hasFiles ? backupDir : null;
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Update version file
|
|
343
|
+
*/
|
|
344
|
+
function updateVersionFile(cwd) {
|
|
345
|
+
const versionPath = path.join(cwd, DIR_NAMES.WORKFLOW, ".version");
|
|
346
|
+
fs.writeFileSync(versionPath, VERSION);
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Get current installed version
|
|
350
|
+
*/
|
|
351
|
+
function getInstalledVersion(cwd) {
|
|
352
|
+
const versionPath = path.join(cwd, DIR_NAMES.WORKFLOW, ".version");
|
|
353
|
+
if (fs.existsSync(versionPath)) {
|
|
354
|
+
return fs.readFileSync(versionPath, "utf-8").trim();
|
|
355
|
+
}
|
|
356
|
+
return "unknown";
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Fetch latest version from npm registry
|
|
360
|
+
*/
|
|
361
|
+
async function getLatestNpmVersion() {
|
|
362
|
+
try {
|
|
363
|
+
const response = await fetch(`https://registry.npmjs.org/${PACKAGE_NAME}/latest`);
|
|
364
|
+
if (!response.ok) {
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
const data = (await response.json());
|
|
368
|
+
return data.version ?? null;
|
|
369
|
+
}
|
|
370
|
+
catch {
|
|
371
|
+
return null;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Recursively collect all files in a directory
|
|
376
|
+
*/
|
|
377
|
+
function collectAllFiles(dirPath) {
|
|
378
|
+
if (!fs.existsSync(dirPath))
|
|
379
|
+
return [];
|
|
380
|
+
const files = [];
|
|
381
|
+
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
382
|
+
for (const entry of entries) {
|
|
383
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
384
|
+
if (entry.isDirectory()) {
|
|
385
|
+
files.push(...collectAllFiles(fullPath));
|
|
386
|
+
}
|
|
387
|
+
else if (entry.isFile()) {
|
|
388
|
+
files.push(fullPath);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
return files;
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Check if a directory only contains unmodified template files
|
|
395
|
+
* Returns true if safe to delete:
|
|
396
|
+
* - All files are tracked and unmodified, OR
|
|
397
|
+
* - All files match current template content (even if not tracked)
|
|
398
|
+
*/
|
|
399
|
+
function isDirectorySafeToReplace(cwd, dirRelativePath, hashes, templates) {
|
|
400
|
+
const dirFullPath = path.join(cwd, dirRelativePath);
|
|
401
|
+
if (!fs.existsSync(dirFullPath))
|
|
402
|
+
return true;
|
|
403
|
+
const files = collectAllFiles(dirFullPath);
|
|
404
|
+
if (files.length === 0)
|
|
405
|
+
return true; // Empty directory is safe
|
|
406
|
+
for (const fullPath of files) {
|
|
407
|
+
const relativePath = path.relative(cwd, fullPath);
|
|
408
|
+
const storedHash = hashes[relativePath];
|
|
409
|
+
const templateContent = templates.get(relativePath);
|
|
410
|
+
// Check if file matches template content (handles untracked files)
|
|
411
|
+
if (templateContent) {
|
|
412
|
+
const currentContent = fs.readFileSync(fullPath, "utf-8");
|
|
413
|
+
if (currentContent === templateContent) {
|
|
414
|
+
// File matches template - safe
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
// Check if file is tracked and unmodified
|
|
419
|
+
if (storedHash && !isTemplateModified(cwd, relativePath, hashes)) {
|
|
420
|
+
// Tracked and unmodified - safe
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
// File is either user-created or user-modified - not safe
|
|
424
|
+
return false;
|
|
425
|
+
}
|
|
426
|
+
return true;
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Recursively delete a directory
|
|
430
|
+
*/
|
|
431
|
+
function removeDirectoryRecursive(dirPath) {
|
|
432
|
+
if (!fs.existsSync(dirPath))
|
|
433
|
+
return;
|
|
434
|
+
fs.rmSync(dirPath, { recursive: true, force: true });
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Check if a file is safe to overwrite (matches template content)
|
|
438
|
+
*/
|
|
439
|
+
function isFileSafeToReplace(cwd, relativePath, templates) {
|
|
440
|
+
const fullPath = path.join(cwd, relativePath);
|
|
441
|
+
if (!fs.existsSync(fullPath))
|
|
442
|
+
return true;
|
|
443
|
+
const templateContent = templates.get(relativePath);
|
|
444
|
+
if (!templateContent)
|
|
445
|
+
return false; // Not a template file
|
|
446
|
+
const currentContent = fs.readFileSync(fullPath, "utf-8");
|
|
447
|
+
return currentContent === templateContent;
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Classify migrations based on file state and user modifications
|
|
451
|
+
*/
|
|
452
|
+
function classifyMigrations(migrations, cwd, hashes, templates) {
|
|
453
|
+
const result = {
|
|
454
|
+
auto: [],
|
|
455
|
+
confirm: [],
|
|
456
|
+
conflict: [],
|
|
457
|
+
skip: [],
|
|
458
|
+
};
|
|
459
|
+
for (const item of migrations) {
|
|
460
|
+
const oldPath = path.join(cwd, item.from);
|
|
461
|
+
const oldExists = fs.existsSync(oldPath);
|
|
462
|
+
if (!oldExists) {
|
|
463
|
+
// Old file doesn't exist, nothing to migrate
|
|
464
|
+
result.skip.push(item);
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
if (item.type === "rename" && item.to) {
|
|
468
|
+
const newPath = path.join(cwd, item.to);
|
|
469
|
+
const newExists = fs.existsSync(newPath);
|
|
470
|
+
if (newExists) {
|
|
471
|
+
// Both exist - check if new file matches template (safe to overwrite)
|
|
472
|
+
if (isFileSafeToReplace(cwd, item.to, templates)) {
|
|
473
|
+
// New file is just template content - safe to delete and rename
|
|
474
|
+
result.auto.push(item);
|
|
475
|
+
}
|
|
476
|
+
else {
|
|
477
|
+
// New file has user content - conflict
|
|
478
|
+
result.conflict.push(item);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
else if (isTemplateModified(cwd, item.from, hashes)) {
|
|
482
|
+
// User has modified the file - needs confirmation
|
|
483
|
+
result.confirm.push(item);
|
|
484
|
+
}
|
|
485
|
+
else {
|
|
486
|
+
// Unmodified template - safe to auto-migrate
|
|
487
|
+
result.auto.push(item);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
else if (item.type === "rename-dir" && item.to) {
|
|
491
|
+
const newPath = path.join(cwd, item.to);
|
|
492
|
+
const newExists = fs.existsSync(newPath);
|
|
493
|
+
if (newExists) {
|
|
494
|
+
// Target exists - check if it only contains unmodified template files
|
|
495
|
+
if (isDirectorySafeToReplace(cwd, item.to, hashes, templates)) {
|
|
496
|
+
// Safe to delete target and rename source
|
|
497
|
+
result.auto.push(item);
|
|
498
|
+
}
|
|
499
|
+
else {
|
|
500
|
+
// Target has user modifications - conflict
|
|
501
|
+
result.conflict.push(item);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
else {
|
|
505
|
+
// Directory rename - always auto (includes user files)
|
|
506
|
+
result.auto.push(item);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
else if (item.type === "delete") {
|
|
510
|
+
if (isTemplateModified(cwd, item.from, hashes)) {
|
|
511
|
+
// User has modified - needs confirmation before delete
|
|
512
|
+
result.confirm.push(item);
|
|
513
|
+
}
|
|
514
|
+
else {
|
|
515
|
+
// Unmodified - safe to auto-delete
|
|
516
|
+
result.auto.push(item);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
return result;
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* Print migration summary
|
|
524
|
+
*/
|
|
525
|
+
function printMigrationSummary(classified) {
|
|
526
|
+
const total = classified.auto.length +
|
|
527
|
+
classified.confirm.length +
|
|
528
|
+
classified.conflict.length +
|
|
529
|
+
classified.skip.length;
|
|
530
|
+
if (total === 0) {
|
|
531
|
+
console.log(chalk.gray(" No migrations to apply.\n"));
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
if (classified.auto.length > 0) {
|
|
535
|
+
console.log(chalk.green(" ā Auto-migrate (unmodified):"));
|
|
536
|
+
for (const item of classified.auto) {
|
|
537
|
+
if (item.type === "rename") {
|
|
538
|
+
console.log(chalk.green(` ${item.from} ā ${item.to}`));
|
|
539
|
+
}
|
|
540
|
+
else if (item.type === "rename-dir") {
|
|
541
|
+
console.log(chalk.green(` [dir] ${item.from}/ ā ${item.to}/`));
|
|
542
|
+
}
|
|
543
|
+
else {
|
|
544
|
+
console.log(chalk.green(` ā ${item.from}`));
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
console.log("");
|
|
548
|
+
}
|
|
549
|
+
if (classified.confirm.length > 0) {
|
|
550
|
+
console.log(chalk.yellow(" ā Requires confirmation (modified by user):"));
|
|
551
|
+
for (const item of classified.confirm) {
|
|
552
|
+
if (item.type === "rename") {
|
|
553
|
+
console.log(chalk.yellow(` ${item.from} ā ${item.to}`));
|
|
554
|
+
}
|
|
555
|
+
else {
|
|
556
|
+
console.log(chalk.yellow(` ā ${item.from}`));
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
console.log("");
|
|
560
|
+
}
|
|
561
|
+
if (classified.conflict.length > 0) {
|
|
562
|
+
console.log(chalk.red(" ā Conflict (both old and new exist):"));
|
|
563
|
+
for (const item of classified.conflict) {
|
|
564
|
+
if (item.type === "rename-dir") {
|
|
565
|
+
console.log(chalk.red(` [dir] ${item.from}/ ā ${item.to}/`));
|
|
566
|
+
}
|
|
567
|
+
else {
|
|
568
|
+
console.log(chalk.red(` ${item.from} ā ${item.to}`));
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
console.log(chalk.gray(" ā Resolve manually: merge or delete one, then re-run update"));
|
|
572
|
+
console.log("");
|
|
573
|
+
}
|
|
574
|
+
if (classified.skip.length > 0) {
|
|
575
|
+
console.log(chalk.gray(" ā Skipping (old file not found):"));
|
|
576
|
+
for (const item of classified.skip.slice(0, 3)) {
|
|
577
|
+
console.log(chalk.gray(` ${item.from}`));
|
|
578
|
+
}
|
|
579
|
+
if (classified.skip.length > 3) {
|
|
580
|
+
console.log(chalk.gray(` ... and ${classified.skip.length - 3} more`));
|
|
581
|
+
}
|
|
582
|
+
console.log("");
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
/**
|
|
586
|
+
* Prompt user for migration action on a single item
|
|
587
|
+
*/
|
|
588
|
+
async function promptMigrationAction(item) {
|
|
589
|
+
const action = item.type === "rename"
|
|
590
|
+
? `${item.from} ā ${item.to}`
|
|
591
|
+
: `Delete ${item.from}`;
|
|
592
|
+
const { choice } = await inquirer.prompt([
|
|
593
|
+
{
|
|
594
|
+
type: "list",
|
|
595
|
+
name: "choice",
|
|
596
|
+
message: `${action}\nThis file has been modified. What would you like to do?`,
|
|
597
|
+
choices: [
|
|
598
|
+
{
|
|
599
|
+
name: item.type === "rename" ? "[r] Rename anyway" : "[d] Delete anyway",
|
|
600
|
+
value: "rename",
|
|
601
|
+
},
|
|
602
|
+
{
|
|
603
|
+
name: "[b] Backup original, then proceed",
|
|
604
|
+
value: "backup-rename",
|
|
605
|
+
},
|
|
606
|
+
{
|
|
607
|
+
name: "[s] Skip this migration",
|
|
608
|
+
value: "skip",
|
|
609
|
+
},
|
|
610
|
+
],
|
|
611
|
+
default: "skip",
|
|
612
|
+
},
|
|
613
|
+
]);
|
|
614
|
+
return choice;
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* Clean up empty directories after file migration
|
|
618
|
+
* Recursively removes empty parent directories up to .aim-studio root
|
|
619
|
+
*/
|
|
620
|
+
/** @internal Exported for testing only */
|
|
621
|
+
export function cleanupEmptyDirs(cwd, dirPath) {
|
|
622
|
+
const fullPath = path.join(cwd, dirPath);
|
|
623
|
+
// Safety: don't delete outside of managed directories
|
|
624
|
+
if (!isManagedPath(dirPath)) {
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
// Safety: never delete managed root directories themselves (e.g., .claude, .aim-studio)
|
|
628
|
+
if (isManagedRootDir(dirPath)) {
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
// Check if directory exists and is empty
|
|
632
|
+
if (!fs.existsSync(fullPath))
|
|
633
|
+
return;
|
|
634
|
+
try {
|
|
635
|
+
const stat = fs.statSync(fullPath);
|
|
636
|
+
if (!stat.isDirectory())
|
|
637
|
+
return;
|
|
638
|
+
const contents = fs.readdirSync(fullPath);
|
|
639
|
+
if (contents.length === 0) {
|
|
640
|
+
fs.rmdirSync(fullPath);
|
|
641
|
+
// Recursively check parent (but stop at root directories)
|
|
642
|
+
const parent = path.dirname(dirPath);
|
|
643
|
+
if (parent !== "." &&
|
|
644
|
+
parent !== dirPath &&
|
|
645
|
+
!isManagedRootDir(parent)) {
|
|
646
|
+
cleanupEmptyDirs(cwd, parent);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
catch {
|
|
651
|
+
// Ignore errors (permission issues, etc.)
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
/**
|
|
655
|
+
* Sort migrations for safe execution order
|
|
656
|
+
* - rename-dir with deeper paths first (to handle nested directories)
|
|
657
|
+
* - rename-dir before rename/delete
|
|
658
|
+
*/
|
|
659
|
+
/** @internal Exported for testing only */
|
|
660
|
+
export function sortMigrationsForExecution(migrations) {
|
|
661
|
+
return [...migrations].sort((a, b) => {
|
|
662
|
+
// rename-dir should be sorted by path depth (deeper first)
|
|
663
|
+
if (a.type === "rename-dir" && b.type === "rename-dir") {
|
|
664
|
+
const aDepth = a.from.split("/").length;
|
|
665
|
+
const bDepth = b.from.split("/").length;
|
|
666
|
+
return bDepth - aDepth; // Deeper paths first
|
|
667
|
+
}
|
|
668
|
+
// rename-dir before rename/delete (directories first)
|
|
669
|
+
if (a.type === "rename-dir" && b.type !== "rename-dir")
|
|
670
|
+
return -1;
|
|
671
|
+
if (a.type !== "rename-dir" && b.type === "rename-dir")
|
|
672
|
+
return 1;
|
|
673
|
+
return 0;
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
/**
|
|
677
|
+
* Execute classified migrations
|
|
678
|
+
*
|
|
679
|
+
* @param options.force - Force migrate modified files without asking
|
|
680
|
+
* @param options.skipAll - Skip all modified files without asking
|
|
681
|
+
* If neither is set, prompts interactively for modified files
|
|
682
|
+
*/
|
|
683
|
+
async function executeMigrations(classified, cwd, options) {
|
|
684
|
+
const result = {
|
|
685
|
+
renamed: 0,
|
|
686
|
+
deleted: 0,
|
|
687
|
+
skipped: 0,
|
|
688
|
+
conflicts: classified.conflict.length,
|
|
689
|
+
};
|
|
690
|
+
// Sort migrations for safe execution order
|
|
691
|
+
const sortedAuto = sortMigrationsForExecution(classified.auto);
|
|
692
|
+
// 1. Execute auto migrations (unmodified files and directories)
|
|
693
|
+
for (const item of sortedAuto) {
|
|
694
|
+
if (item.type === "rename" && item.to) {
|
|
695
|
+
const oldPath = path.join(cwd, item.from);
|
|
696
|
+
const newPath = path.join(cwd, item.to);
|
|
697
|
+
// Ensure target directory exists
|
|
698
|
+
fs.mkdirSync(path.dirname(newPath), { recursive: true });
|
|
699
|
+
fs.renameSync(oldPath, newPath);
|
|
700
|
+
// Update hash tracking
|
|
701
|
+
renameHash(cwd, item.from, item.to);
|
|
702
|
+
// Make executable if it's a script
|
|
703
|
+
if (item.to.endsWith(".sh") || item.to.endsWith(".py")) {
|
|
704
|
+
fs.chmodSync(newPath, "755");
|
|
705
|
+
}
|
|
706
|
+
// Clean up empty source directory
|
|
707
|
+
cleanupEmptyDirs(cwd, path.dirname(item.from));
|
|
708
|
+
result.renamed++;
|
|
709
|
+
}
|
|
710
|
+
else if (item.type === "rename-dir" && item.to) {
|
|
711
|
+
const oldPath = path.join(cwd, item.from);
|
|
712
|
+
const newPath = path.join(cwd, item.to);
|
|
713
|
+
// If target exists (safe to replace, already checked in classification)
|
|
714
|
+
// delete it first before renaming
|
|
715
|
+
if (fs.existsSync(newPath)) {
|
|
716
|
+
removeDirectoryRecursive(newPath);
|
|
717
|
+
}
|
|
718
|
+
// Ensure parent directory exists
|
|
719
|
+
fs.mkdirSync(path.dirname(newPath), { recursive: true });
|
|
720
|
+
// Rename the entire directory (includes all user files)
|
|
721
|
+
fs.renameSync(oldPath, newPath);
|
|
722
|
+
// Batch update hash tracking for all files in the directory
|
|
723
|
+
const hashes = loadHashes(cwd);
|
|
724
|
+
const oldPrefix = item.from.endsWith("/") ? item.from : item.from + "/";
|
|
725
|
+
const newPrefix = item.to.endsWith("/") ? item.to : item.to + "/";
|
|
726
|
+
const updatedHashes = {};
|
|
727
|
+
for (const [hashPath, hashValue] of Object.entries(hashes)) {
|
|
728
|
+
if (hashPath.startsWith(oldPrefix)) {
|
|
729
|
+
// Rename path: old prefix -> new prefix
|
|
730
|
+
const newHashPath = newPrefix + hashPath.slice(oldPrefix.length);
|
|
731
|
+
updatedHashes[newHashPath] = hashValue;
|
|
732
|
+
}
|
|
733
|
+
else if (hashPath.startsWith(newPrefix)) {
|
|
734
|
+
// Skip old hashes from deleted target directory
|
|
735
|
+
// (they will be replaced by renamed source files)
|
|
736
|
+
continue;
|
|
737
|
+
}
|
|
738
|
+
else {
|
|
739
|
+
// Keep unchanged
|
|
740
|
+
updatedHashes[hashPath] = hashValue;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
saveHashes(cwd, updatedHashes);
|
|
744
|
+
result.renamed++;
|
|
745
|
+
}
|
|
746
|
+
else if (item.type === "delete") {
|
|
747
|
+
const filePath = path.join(cwd, item.from);
|
|
748
|
+
fs.unlinkSync(filePath);
|
|
749
|
+
// Remove from hash tracking
|
|
750
|
+
removeHash(cwd, item.from);
|
|
751
|
+
// Clean up empty directory
|
|
752
|
+
cleanupEmptyDirs(cwd, path.dirname(item.from));
|
|
753
|
+
result.deleted++;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
// 2. Handle confirm items (modified files)
|
|
757
|
+
// Note: All files are already backed up by createMigrationBackup before execution
|
|
758
|
+
for (const item of classified.confirm) {
|
|
759
|
+
let action;
|
|
760
|
+
if (options.force) {
|
|
761
|
+
// Force mode: proceed (already backed up)
|
|
762
|
+
action = "rename";
|
|
763
|
+
}
|
|
764
|
+
else if (options.skipAll) {
|
|
765
|
+
// Skip mode: skip all modified files
|
|
766
|
+
action = "skip";
|
|
767
|
+
}
|
|
768
|
+
else {
|
|
769
|
+
// Default: interactive prompt
|
|
770
|
+
action = await promptMigrationAction(item);
|
|
771
|
+
}
|
|
772
|
+
if (action === "skip") {
|
|
773
|
+
result.skipped++;
|
|
774
|
+
continue;
|
|
775
|
+
}
|
|
776
|
+
// For backup-rename, just proceed (backup already done)
|
|
777
|
+
// Proceed with rename or delete
|
|
778
|
+
if (item.type === "rename" && item.to) {
|
|
779
|
+
const oldPath = path.join(cwd, item.from);
|
|
780
|
+
const newPath = path.join(cwd, item.to);
|
|
781
|
+
fs.mkdirSync(path.dirname(newPath), { recursive: true });
|
|
782
|
+
fs.renameSync(oldPath, newPath);
|
|
783
|
+
renameHash(cwd, item.from, item.to);
|
|
784
|
+
if (item.to.endsWith(".sh") || item.to.endsWith(".py")) {
|
|
785
|
+
fs.chmodSync(newPath, "755");
|
|
786
|
+
}
|
|
787
|
+
// Clean up empty source directory
|
|
788
|
+
cleanupEmptyDirs(cwd, path.dirname(item.from));
|
|
789
|
+
result.renamed++;
|
|
790
|
+
}
|
|
791
|
+
else if (item.type === "delete") {
|
|
792
|
+
const filePath = path.join(cwd, item.from);
|
|
793
|
+
fs.unlinkSync(filePath);
|
|
794
|
+
removeHash(cwd, item.from);
|
|
795
|
+
// Clean up empty directory
|
|
796
|
+
cleanupEmptyDirs(cwd, path.dirname(item.from));
|
|
797
|
+
result.deleted++;
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
// 3. Skip count already tracked (old files not found)
|
|
801
|
+
result.skipped += classified.skip.length;
|
|
802
|
+
return result;
|
|
803
|
+
}
|
|
804
|
+
/**
|
|
805
|
+
* Print migration result summary
|
|
806
|
+
*/
|
|
807
|
+
function printMigrationResult(result) {
|
|
808
|
+
const parts = [];
|
|
809
|
+
if (result.renamed > 0) {
|
|
810
|
+
parts.push(`${result.renamed} renamed`);
|
|
811
|
+
}
|
|
812
|
+
if (result.deleted > 0) {
|
|
813
|
+
parts.push(`${result.deleted} deleted`);
|
|
814
|
+
}
|
|
815
|
+
if (result.skipped > 0) {
|
|
816
|
+
parts.push(`${result.skipped} skipped`);
|
|
817
|
+
}
|
|
818
|
+
if (result.conflicts > 0) {
|
|
819
|
+
parts.push(`${result.conflicts} conflict${result.conflicts > 1 ? "s" : ""}`);
|
|
820
|
+
}
|
|
821
|
+
if (parts.length > 0) {
|
|
822
|
+
console.log(chalk.cyan(`Migration complete: ${parts.join(", ")}`));
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
/**
|
|
826
|
+
* Main update command
|
|
827
|
+
*/
|
|
828
|
+
export async function update(options) {
|
|
829
|
+
const cwd = process.cwd();
|
|
830
|
+
// Check if AIM Studio is initialized
|
|
831
|
+
if (!fs.existsSync(path.join(cwd, DIR_NAMES.WORKFLOW))) {
|
|
832
|
+
console.log(chalk.red("Error: AIM Studio not initialized in this directory."));
|
|
833
|
+
console.log(chalk.gray("Run 'aim init' first."));
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
console.log(chalk.cyan("\nAIM Studio Update"));
|
|
837
|
+
console.log(chalk.cyan("āāāāāāāāāāāāāāāāā\n"));
|
|
838
|
+
// Get versions
|
|
839
|
+
const projectVersion = getInstalledVersion(cwd);
|
|
840
|
+
const cliVersion = VERSION;
|
|
841
|
+
const latestNpmVersion = await getLatestNpmVersion();
|
|
842
|
+
// Version comparison
|
|
843
|
+
const cliVsProject = compareVersions(cliVersion, projectVersion);
|
|
844
|
+
const cliVsNpm = latestNpmVersion
|
|
845
|
+
? compareVersions(cliVersion, latestNpmVersion)
|
|
846
|
+
: 0;
|
|
847
|
+
// Display versions with context
|
|
848
|
+
console.log(`Project version: ${chalk.white(projectVersion)}`);
|
|
849
|
+
console.log(`CLI version: ${chalk.white(cliVersion)}`);
|
|
850
|
+
if (latestNpmVersion) {
|
|
851
|
+
console.log(`Latest on npm: ${chalk.white(latestNpmVersion)}`);
|
|
852
|
+
}
|
|
853
|
+
else {
|
|
854
|
+
console.log(chalk.gray("Latest on npm: (unable to fetch)"));
|
|
855
|
+
}
|
|
856
|
+
console.log("");
|
|
857
|
+
// Check if CLI is outdated compared to npm
|
|
858
|
+
if (cliVsNpm < 0 && latestNpmVersion) {
|
|
859
|
+
console.log(chalk.yellow(`ā ļø Your CLI (${cliVersion}) is behind npm (${latestNpmVersion}).`));
|
|
860
|
+
console.log(chalk.yellow(` Run: npm install -g ${PACKAGE_NAME}\n`));
|
|
861
|
+
}
|
|
862
|
+
// Check for downgrade situation
|
|
863
|
+
if (cliVsProject < 0) {
|
|
864
|
+
console.log(chalk.red(`ā Cannot update: CLI version (${cliVersion}) < project version (${projectVersion})`));
|
|
865
|
+
console.log(chalk.red(` This would DOWNGRADE your project!\n`));
|
|
866
|
+
if (!options.allowDowngrade) {
|
|
867
|
+
console.log(chalk.gray("Solutions:"));
|
|
868
|
+
console.log(chalk.gray(` 1. Update your CLI: npm install -g ${PACKAGE_NAME}`));
|
|
869
|
+
console.log(chalk.gray(` 2. Force downgrade: aim update --allow-downgrade\n`));
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
872
|
+
console.log(chalk.yellow("ā ļø --allow-downgrade flag set. Proceeding with downgrade...\n"));
|
|
873
|
+
}
|
|
874
|
+
// Migration metadata is displayed at the end to prevent scrolling off screen
|
|
875
|
+
// Load template hashes for modification detection
|
|
876
|
+
const hashes = loadHashes(cwd);
|
|
877
|
+
const isFirstHashTracking = Object.keys(hashes).length === 0;
|
|
878
|
+
// Handle unknown version - skip migrations but continue with template updates
|
|
879
|
+
const isUnknownVersion = projectVersion === "unknown";
|
|
880
|
+
if (isUnknownVersion) {
|
|
881
|
+
console.log(chalk.yellow("ā ļø No version file found. Skipping migrations."));
|
|
882
|
+
console.log(chalk.gray(" Template updates will still be applied."));
|
|
883
|
+
console.log(chalk.gray(" If your project used old file paths, you may need to rename them manually.\n"));
|
|
884
|
+
}
|
|
885
|
+
// Collect templates (used for both migration classification and change analysis)
|
|
886
|
+
const templates = collectTemplateFiles(cwd);
|
|
887
|
+
// Check for pending migrations (skip if unknown version)
|
|
888
|
+
let pendingMigrations = isUnknownVersion
|
|
889
|
+
? []
|
|
890
|
+
: getMigrationsForVersion(projectVersion, cliVersion);
|
|
891
|
+
// Also check for "orphaned" migrations - where source still exists but version says we shouldn't migrate
|
|
892
|
+
// This handles cases where version was updated but migrations weren't applied
|
|
893
|
+
const allMigrations = getAllMigrations();
|
|
894
|
+
const orphanedMigrations = allMigrations.filter((item) => {
|
|
895
|
+
// Only check rename and rename-dir migrations
|
|
896
|
+
if (item.type !== "rename" && item.type !== "rename-dir")
|
|
897
|
+
return false;
|
|
898
|
+
if (!item.from || !item.to)
|
|
899
|
+
return false;
|
|
900
|
+
const oldPath = path.join(cwd, item.from);
|
|
901
|
+
const newPath = path.join(cwd, item.to);
|
|
902
|
+
// Orphaned if: source exists AND target doesn't exist
|
|
903
|
+
// AND this migration isn't already in pendingMigrations
|
|
904
|
+
const sourceExists = fs.existsSync(oldPath);
|
|
905
|
+
const targetExists = fs.existsSync(newPath);
|
|
906
|
+
const alreadyPending = pendingMigrations.some((m) => m.from === item.from && m.to === item.to);
|
|
907
|
+
return sourceExists && !targetExists && !alreadyPending;
|
|
908
|
+
});
|
|
909
|
+
// Add orphaned migrations to pending (they need to be applied)
|
|
910
|
+
if (orphanedMigrations.length > 0) {
|
|
911
|
+
console.log(chalk.yellow("ā ļø Detected incomplete migrations from previous updates:"));
|
|
912
|
+
for (const item of orphanedMigrations) {
|
|
913
|
+
console.log(chalk.yellow(` ${item.from} ā ${item.to}`));
|
|
914
|
+
}
|
|
915
|
+
console.log("");
|
|
916
|
+
pendingMigrations = [...pendingMigrations, ...orphanedMigrations];
|
|
917
|
+
}
|
|
918
|
+
const hasMigrations = pendingMigrations.length > 0;
|
|
919
|
+
// Classify migrations (stored for later backup creation)
|
|
920
|
+
let classifiedMigrations = null;
|
|
921
|
+
if (hasMigrations) {
|
|
922
|
+
console.log(chalk.cyan("Analyzing migrations...\n"));
|
|
923
|
+
classifiedMigrations = classifyMigrations(pendingMigrations, cwd, hashes, templates);
|
|
924
|
+
printMigrationSummary(classifiedMigrations);
|
|
925
|
+
// Show hint about --migrate flag (execution happens later after backup)
|
|
926
|
+
if (!options.migrate) {
|
|
927
|
+
const autoCount = classifiedMigrations.auto.length;
|
|
928
|
+
const confirmCount = classifiedMigrations.confirm.length;
|
|
929
|
+
if (autoCount > 0 || confirmCount > 0) {
|
|
930
|
+
console.log(chalk.gray(`Tip: Use --migrate to apply migrations (prompts for modified files).`));
|
|
931
|
+
if (confirmCount > 0) {
|
|
932
|
+
console.log(chalk.gray(` Use --migrate -f to force all, or --migrate -s to skip modified.\n`));
|
|
933
|
+
}
|
|
934
|
+
else {
|
|
935
|
+
console.log("");
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
// Analyze changes (pass hashes for modification detection)
|
|
941
|
+
const changes = analyzeChanges(cwd, hashes, templates);
|
|
942
|
+
// Print summary
|
|
943
|
+
printChangeSummary(changes);
|
|
944
|
+
// First-time hash tracking hint
|
|
945
|
+
if (isFirstHashTracking && changes.changedFiles.length > 0) {
|
|
946
|
+
console.log(chalk.cyan("ā¹ļø First update with hash tracking enabled."));
|
|
947
|
+
console.log(chalk.gray(" Changed files shown above may not be actual user modifications."));
|
|
948
|
+
console.log(chalk.gray(" After this update, hash tracking will accurately detect changes.\n"));
|
|
949
|
+
}
|
|
950
|
+
// Check if there's anything to do
|
|
951
|
+
const isUpgrade = cliVsProject > 0;
|
|
952
|
+
const isDowngrade = cliVsProject < 0;
|
|
953
|
+
const isSameVersion = cliVsProject === 0;
|
|
954
|
+
// Check if we have pending migrations that need to be applied
|
|
955
|
+
const hasPendingMigrations = options.migrate &&
|
|
956
|
+
classifiedMigrations &&
|
|
957
|
+
(classifiedMigrations.auto.length > 0 ||
|
|
958
|
+
classifiedMigrations.confirm.length > 0);
|
|
959
|
+
if (changes.newFiles.length === 0 &&
|
|
960
|
+
changes.autoUpdateFiles.length === 0 &&
|
|
961
|
+
changes.changedFiles.length === 0 &&
|
|
962
|
+
!hasPendingMigrations) {
|
|
963
|
+
if (isSameVersion) {
|
|
964
|
+
console.log(chalk.green("ā Already up to date!"));
|
|
965
|
+
}
|
|
966
|
+
else if (isUpgrade) {
|
|
967
|
+
console.log(chalk.green(`ā No file changes needed for ${projectVersion} ā ${cliVersion}`));
|
|
968
|
+
}
|
|
969
|
+
return;
|
|
970
|
+
}
|
|
971
|
+
// Show what this operation will do
|
|
972
|
+
if (isUpgrade) {
|
|
973
|
+
console.log(chalk.green(`This will UPGRADE: ${projectVersion} ā ${cliVersion}\n`));
|
|
974
|
+
}
|
|
975
|
+
else if (isDowngrade) {
|
|
976
|
+
console.log(chalk.red(`ā ļø This will DOWNGRADE: ${projectVersion} ā ${cliVersion}\n`));
|
|
977
|
+
}
|
|
978
|
+
// Show breaking change warning before confirm
|
|
979
|
+
if (cliVsProject > 0 && projectVersion !== "unknown") {
|
|
980
|
+
const preConfirmMetadata = getMigrationMetadata(projectVersion, cliVersion);
|
|
981
|
+
if (preConfirmMetadata.breaking) {
|
|
982
|
+
console.log(chalk.cyan("ā".repeat(60)));
|
|
983
|
+
console.log(chalk.bgRed.white.bold(" ā ļø BREAKING CHANGES ") +
|
|
984
|
+
chalk.red.bold(" Review the changes above carefully!"));
|
|
985
|
+
if (preConfirmMetadata.changelog.length > 0) {
|
|
986
|
+
console.log("");
|
|
987
|
+
console.log(chalk.white(preConfirmMetadata.changelog[0]));
|
|
988
|
+
}
|
|
989
|
+
if (preConfirmMetadata.recommendMigrate && !options.migrate) {
|
|
990
|
+
console.log("");
|
|
991
|
+
console.log(chalk.bgGreen.black.bold(" š” RECOMMENDED ") +
|
|
992
|
+
chalk.green.bold(" Run with --migrate to complete the migration"));
|
|
993
|
+
}
|
|
994
|
+
console.log(chalk.cyan("ā".repeat(60)));
|
|
995
|
+
console.log("");
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
// Dry run mode
|
|
999
|
+
if (options.dryRun) {
|
|
1000
|
+
console.log(chalk.gray("[Dry run] No changes made."));
|
|
1001
|
+
return;
|
|
1002
|
+
}
|
|
1003
|
+
// Confirm
|
|
1004
|
+
const { proceed } = await inquirer.prompt([
|
|
1005
|
+
{
|
|
1006
|
+
type: "confirm",
|
|
1007
|
+
name: "proceed",
|
|
1008
|
+
message: "Proceed?",
|
|
1009
|
+
default: true,
|
|
1010
|
+
},
|
|
1011
|
+
]);
|
|
1012
|
+
if (!proceed) {
|
|
1013
|
+
console.log(chalk.yellow("Update cancelled."));
|
|
1014
|
+
return;
|
|
1015
|
+
}
|
|
1016
|
+
// Create complete backup of .aim-studio, .claude, .cursor, .iflow, .opencode directories
|
|
1017
|
+
const backupDir = createFullBackup(cwd);
|
|
1018
|
+
if (backupDir) {
|
|
1019
|
+
console.log(chalk.gray(`\nBackup created: ${path.relative(cwd, backupDir)}/`));
|
|
1020
|
+
}
|
|
1021
|
+
// Execute migrations if --migrate flag is set
|
|
1022
|
+
if (options.migrate && classifiedMigrations) {
|
|
1023
|
+
const migrationResult = await executeMigrations(classifiedMigrations, cwd, {
|
|
1024
|
+
force: options.force,
|
|
1025
|
+
skipAll: options.skipAll,
|
|
1026
|
+
});
|
|
1027
|
+
printMigrationResult(migrationResult);
|
|
1028
|
+
// Hardcoded: Rename traces-*.md to journal-*.md in workspace directories
|
|
1029
|
+
// Why hardcoded: The migration system only supports fixed path renames, not pattern-based.
|
|
1030
|
+
// traces-*.md files are in .aim-studio/workspace/{developer}/ with variable developer names
|
|
1031
|
+
// and variable file numbers (traces-1.md, traces-2.md, etc.), so we can't enumerate them
|
|
1032
|
+
// in the migration manifest. This is a one-time migration for the 0.2.0 naming redesign.
|
|
1033
|
+
const workspaceDir = path.join(cwd, PATHS.WORKSPACE);
|
|
1034
|
+
if (fs.existsSync(workspaceDir)) {
|
|
1035
|
+
let journalRenamed = 0;
|
|
1036
|
+
const devDirs = fs.readdirSync(workspaceDir);
|
|
1037
|
+
for (const dev of devDirs) {
|
|
1038
|
+
const devPath = path.join(workspaceDir, dev);
|
|
1039
|
+
if (!fs.statSync(devPath).isDirectory())
|
|
1040
|
+
continue;
|
|
1041
|
+
const files = fs.readdirSync(devPath);
|
|
1042
|
+
for (const file of files) {
|
|
1043
|
+
if (file.startsWith("traces-") && file.endsWith(".md")) {
|
|
1044
|
+
const oldPath = path.join(devPath, file);
|
|
1045
|
+
const newFile = file.replace("traces-", "journal-");
|
|
1046
|
+
const newPath = path.join(devPath, newFile);
|
|
1047
|
+
fs.renameSync(oldPath, newPath);
|
|
1048
|
+
journalRenamed++;
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
if (journalRenamed > 0) {
|
|
1053
|
+
console.log(chalk.cyan(`Renamed ${journalRenamed} traces file(s) to journal`));
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
// Track results
|
|
1058
|
+
let added = 0;
|
|
1059
|
+
let autoUpdated = 0;
|
|
1060
|
+
let updated = 0;
|
|
1061
|
+
let skipped = 0;
|
|
1062
|
+
let createdNew = 0;
|
|
1063
|
+
// Add new files
|
|
1064
|
+
if (changes.newFiles.length > 0) {
|
|
1065
|
+
console.log(chalk.blue("\nAdding new files..."));
|
|
1066
|
+
for (const file of changes.newFiles) {
|
|
1067
|
+
const dir = path.dirname(file.path);
|
|
1068
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
1069
|
+
fs.writeFileSync(file.path, file.newContent);
|
|
1070
|
+
// Make scripts executable
|
|
1071
|
+
if (file.relativePath.endsWith(".sh") || file.relativePath.endsWith(".py")) {
|
|
1072
|
+
fs.chmodSync(file.path, "755");
|
|
1073
|
+
}
|
|
1074
|
+
console.log(chalk.green(` + ${file.relativePath}`));
|
|
1075
|
+
added++;
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
// Auto-update files (template updated, user didn't modify)
|
|
1079
|
+
if (changes.autoUpdateFiles.length > 0) {
|
|
1080
|
+
console.log(chalk.blue("\nAuto-updating template files..."));
|
|
1081
|
+
for (const file of changes.autoUpdateFiles) {
|
|
1082
|
+
fs.writeFileSync(file.path, file.newContent);
|
|
1083
|
+
// Make scripts executable
|
|
1084
|
+
if (file.relativePath.endsWith(".sh") || file.relativePath.endsWith(".py")) {
|
|
1085
|
+
fs.chmodSync(file.path, "755");
|
|
1086
|
+
}
|
|
1087
|
+
console.log(chalk.cyan(` ā ${file.relativePath}`));
|
|
1088
|
+
autoUpdated++;
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
// Handle changed files
|
|
1092
|
+
if (changes.changedFiles.length > 0) {
|
|
1093
|
+
console.log(chalk.blue("\n--- Resolving conflicts ---\n"));
|
|
1094
|
+
const applyToAll = { action: null };
|
|
1095
|
+
for (const file of changes.changedFiles) {
|
|
1096
|
+
const action = await promptConflictResolution(file, options, applyToAll);
|
|
1097
|
+
if (action === "overwrite") {
|
|
1098
|
+
fs.writeFileSync(file.path, file.newContent);
|
|
1099
|
+
if (file.relativePath.endsWith(".sh") || file.relativePath.endsWith(".py")) {
|
|
1100
|
+
fs.chmodSync(file.path, "755");
|
|
1101
|
+
}
|
|
1102
|
+
console.log(chalk.yellow(` ā Overwritten: ${file.relativePath}`));
|
|
1103
|
+
updated++;
|
|
1104
|
+
}
|
|
1105
|
+
else if (action === "create-new") {
|
|
1106
|
+
const newPath = file.path + ".new";
|
|
1107
|
+
fs.writeFileSync(newPath, file.newContent);
|
|
1108
|
+
console.log(chalk.blue(` ā Created: ${file.relativePath}.new`));
|
|
1109
|
+
createdNew++;
|
|
1110
|
+
}
|
|
1111
|
+
else {
|
|
1112
|
+
console.log(chalk.gray(` ā Skipped: ${file.relativePath}`));
|
|
1113
|
+
skipped++;
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
// Update version file
|
|
1118
|
+
updateVersionFile(cwd);
|
|
1119
|
+
// Update template hashes for new, auto-updated, and overwritten files
|
|
1120
|
+
const filesToHash = new Map();
|
|
1121
|
+
for (const file of changes.newFiles) {
|
|
1122
|
+
filesToHash.set(file.relativePath, file.newContent);
|
|
1123
|
+
}
|
|
1124
|
+
// Auto-updated files always get new hash
|
|
1125
|
+
for (const file of changes.autoUpdateFiles) {
|
|
1126
|
+
filesToHash.set(file.relativePath, file.newContent);
|
|
1127
|
+
}
|
|
1128
|
+
// Only hash overwritten files (not skipped or .new copies)
|
|
1129
|
+
for (const file of changes.changedFiles) {
|
|
1130
|
+
const fullPath = path.join(cwd, file.relativePath);
|
|
1131
|
+
if (fs.existsSync(fullPath)) {
|
|
1132
|
+
const content = fs.readFileSync(fullPath, "utf-8");
|
|
1133
|
+
if (content === file.newContent) {
|
|
1134
|
+
filesToHash.set(file.relativePath, file.newContent);
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
if (filesToHash.size > 0) {
|
|
1139
|
+
updateHashes(cwd, filesToHash);
|
|
1140
|
+
}
|
|
1141
|
+
// Print summary
|
|
1142
|
+
console.log(chalk.cyan("\n--- Summary ---\n"));
|
|
1143
|
+
if (added > 0) {
|
|
1144
|
+
console.log(` Added: ${added} file(s)`);
|
|
1145
|
+
}
|
|
1146
|
+
if (autoUpdated > 0) {
|
|
1147
|
+
console.log(` Auto-updated: ${autoUpdated} file(s)`);
|
|
1148
|
+
}
|
|
1149
|
+
if (updated > 0) {
|
|
1150
|
+
console.log(` Updated: ${updated} file(s)`);
|
|
1151
|
+
}
|
|
1152
|
+
if (skipped > 0) {
|
|
1153
|
+
console.log(` Skipped: ${skipped} file(s)`);
|
|
1154
|
+
}
|
|
1155
|
+
if (createdNew > 0) {
|
|
1156
|
+
console.log(` Created .new copies: ${createdNew} file(s)`);
|
|
1157
|
+
}
|
|
1158
|
+
if (backupDir) {
|
|
1159
|
+
console.log(` Backup: ${path.relative(cwd, backupDir)}/`);
|
|
1160
|
+
}
|
|
1161
|
+
const actionWord = isDowngrade ? "Downgrade" : "Update";
|
|
1162
|
+
console.log(chalk.green(`\nā
${actionWord} complete! (${projectVersion} ā ${cliVersion})`));
|
|
1163
|
+
if (createdNew > 0) {
|
|
1164
|
+
console.log(chalk.gray("\nTip: Review .new files and merge changes manually if needed."));
|
|
1165
|
+
}
|
|
1166
|
+
// Create migration task if there are breaking changes with migration guides
|
|
1167
|
+
if (cliVsProject > 0 && projectVersion !== "unknown") {
|
|
1168
|
+
const metadata = getMigrationMetadata(projectVersion, cliVersion);
|
|
1169
|
+
if (metadata.breaking && metadata.migrationGuides.length > 0) {
|
|
1170
|
+
// Create task directory
|
|
1171
|
+
const today = new Date();
|
|
1172
|
+
const monthDay = `${String(today.getMonth() + 1).padStart(2, "0")}-${String(today.getDate()).padStart(2, "0")}`;
|
|
1173
|
+
const taskSlug = `migrate-to-${cliVersion}`;
|
|
1174
|
+
const taskDirName = `${monthDay}-${taskSlug}`;
|
|
1175
|
+
const tasksDir = path.join(cwd, DIR_NAMES.WORKFLOW, DIR_NAMES.TASKS);
|
|
1176
|
+
const taskDir = path.join(tasksDir, taskDirName);
|
|
1177
|
+
// Check if task already exists
|
|
1178
|
+
if (!fs.existsSync(taskDir)) {
|
|
1179
|
+
fs.mkdirSync(taskDir, { recursive: true });
|
|
1180
|
+
// Get current developer for assignee
|
|
1181
|
+
const developerFile = path.join(cwd, DIR_NAMES.WORKFLOW, ".developer");
|
|
1182
|
+
let currentDeveloper = "unknown";
|
|
1183
|
+
if (fs.existsSync(developerFile)) {
|
|
1184
|
+
currentDeveloper = fs.readFileSync(developerFile, "utf-8").trim();
|
|
1185
|
+
}
|
|
1186
|
+
// Build task.json
|
|
1187
|
+
const taskTitle = `Migrate to v${cliVersion}`;
|
|
1188
|
+
const todayStr = today.toISOString().split("T")[0];
|
|
1189
|
+
const taskJson = {
|
|
1190
|
+
title: taskTitle,
|
|
1191
|
+
description: `Breaking change migration from v${projectVersion} to v${cliVersion}`,
|
|
1192
|
+
status: "planning",
|
|
1193
|
+
dev_type: null,
|
|
1194
|
+
scope: "migration",
|
|
1195
|
+
priority: "P1",
|
|
1196
|
+
creator: "trellis-update",
|
|
1197
|
+
assignee: currentDeveloper,
|
|
1198
|
+
createdAt: todayStr,
|
|
1199
|
+
completedAt: null,
|
|
1200
|
+
branch: null,
|
|
1201
|
+
base_branch: null,
|
|
1202
|
+
worktree_path: null,
|
|
1203
|
+
current_phase: 0,
|
|
1204
|
+
next_action: [
|
|
1205
|
+
{ phase: 1, action: "review-guide" },
|
|
1206
|
+
{ phase: 2, action: "update-files" },
|
|
1207
|
+
{ phase: 3, action: "run-migrate" },
|
|
1208
|
+
{ phase: 4, action: "test" },
|
|
1209
|
+
],
|
|
1210
|
+
commit: null,
|
|
1211
|
+
pr_url: null,
|
|
1212
|
+
subtasks: [],
|
|
1213
|
+
};
|
|
1214
|
+
// Write task.json
|
|
1215
|
+
const taskJsonPath = path.join(taskDir, "task.json");
|
|
1216
|
+
fs.writeFileSync(taskJsonPath, JSON.stringify(taskJson, null, 2));
|
|
1217
|
+
// Build PRD content
|
|
1218
|
+
let prdContent = `# Migration Task: Upgrade to v${cliVersion}\n\n`;
|
|
1219
|
+
prdContent += `**Created**: ${todayStr}\n`;
|
|
1220
|
+
prdContent += `**From Version**: ${projectVersion}\n`;
|
|
1221
|
+
prdContent += `**To Version**: ${cliVersion}\n`;
|
|
1222
|
+
prdContent += `**Assignee**: ${currentDeveloper}\n\n`;
|
|
1223
|
+
prdContent += `## Status\n\n- [ ] Review migration guide\n- [ ] Update custom files\n- [ ] Run \`trellis update --migrate\`\n- [ ] Test workflows\n\n`;
|
|
1224
|
+
for (const { version, guide, aiInstructions } of metadata.migrationGuides) {
|
|
1225
|
+
prdContent += `---\n\n## v${version} Migration Guide\n\n`;
|
|
1226
|
+
prdContent += guide;
|
|
1227
|
+
prdContent += "\n\n";
|
|
1228
|
+
if (aiInstructions) {
|
|
1229
|
+
prdContent += `### AI Assistant Instructions\n\n`;
|
|
1230
|
+
prdContent += `When helping with this migration:\n\n`;
|
|
1231
|
+
prdContent += aiInstructions;
|
|
1232
|
+
prdContent += "\n\n";
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
// Write PRD
|
|
1236
|
+
const prdPath = path.join(taskDir, "prd.md");
|
|
1237
|
+
fs.writeFileSync(prdPath, prdContent);
|
|
1238
|
+
console.log("");
|
|
1239
|
+
console.log(chalk.bgCyan.black.bold(" š MIGRATION TASK CREATED "));
|
|
1240
|
+
console.log(chalk.cyan(`A task has been created to help you complete the migration:`));
|
|
1241
|
+
console.log(chalk.white(` ${DIR_NAMES.WORKFLOW}/${DIR_NAMES.TASKS}/${taskDirName}/`));
|
|
1242
|
+
console.log("");
|
|
1243
|
+
console.log(chalk.gray("Use AI to help: Ask Claude/Cursor to read the task and fix your custom files."));
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
// Display breaking change warnings at the very end (so they don't scroll off screen)
|
|
1248
|
+
if (cliVsProject > 0 && projectVersion !== "unknown") {
|
|
1249
|
+
const finalMetadata = getMigrationMetadata(projectVersion, cliVersion);
|
|
1250
|
+
if (finalMetadata.breaking || finalMetadata.changelog.length > 0) {
|
|
1251
|
+
console.log("");
|
|
1252
|
+
console.log(chalk.cyan("ā".repeat(60)));
|
|
1253
|
+
if (finalMetadata.breaking) {
|
|
1254
|
+
console.log(chalk.bgRed.white.bold(" ā ļø BREAKING CHANGES ") +
|
|
1255
|
+
chalk.red.bold(" This update contains breaking changes!"));
|
|
1256
|
+
console.log("");
|
|
1257
|
+
}
|
|
1258
|
+
if (finalMetadata.changelog.length > 0) {
|
|
1259
|
+
console.log(chalk.cyan.bold("š What's Changed:"));
|
|
1260
|
+
for (const entry of finalMetadata.changelog) {
|
|
1261
|
+
console.log(chalk.white(` ${entry}`));
|
|
1262
|
+
}
|
|
1263
|
+
console.log("");
|
|
1264
|
+
}
|
|
1265
|
+
if (finalMetadata.recommendMigrate && !options.migrate) {
|
|
1266
|
+
console.log(chalk.bgGreen.black.bold(" š” RECOMMENDED ") +
|
|
1267
|
+
chalk.green.bold(" Run with --migrate to complete the migration"));
|
|
1268
|
+
console.log(chalk.gray(" This will remove legacy files and apply all changes."));
|
|
1269
|
+
console.log("");
|
|
1270
|
+
}
|
|
1271
|
+
console.log(chalk.cyan("ā".repeat(60)));
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
//# sourceMappingURL=update.js.map
|