moai-adk 0.25.4__py3-none-any.whl โ†’ 0.32.8__py3-none-any.whl

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.

Potentially problematic release.


This version of moai-adk might be problematic. Click here for more details.

Files changed (378) hide show
  1. moai_adk/__init__.py +2 -5
  2. moai_adk/__main__.py +114 -82
  3. moai_adk/cli/__init__.py +6 -1
  4. moai_adk/cli/commands/__init__.py +1 -3
  5. moai_adk/cli/commands/analyze.py +5 -16
  6. moai_adk/cli/commands/doctor.py +6 -18
  7. moai_adk/cli/commands/init.py +56 -125
  8. moai_adk/cli/commands/language.py +14 -35
  9. moai_adk/cli/commands/status.py +9 -15
  10. moai_adk/cli/commands/update.py +1555 -190
  11. moai_adk/cli/prompts/init_prompts.py +112 -56
  12. moai_adk/cli/spec_status.py +263 -0
  13. moai_adk/cli/ui/__init__.py +44 -0
  14. moai_adk/cli/ui/progress.py +422 -0
  15. moai_adk/cli/ui/prompts.py +389 -0
  16. moai_adk/cli/ui/theme.py +129 -0
  17. moai_adk/cli/worktree/__init__.py +27 -0
  18. moai_adk/cli/worktree/__main__.py +31 -0
  19. moai_adk/cli/worktree/cli.py +672 -0
  20. moai_adk/cli/worktree/exceptions.py +89 -0
  21. moai_adk/cli/worktree/manager.py +490 -0
  22. moai_adk/cli/worktree/models.py +65 -0
  23. moai_adk/cli/worktree/registry.py +128 -0
  24. moai_adk/core/PHASE2_OPTIMIZATIONS.md +467 -0
  25. moai_adk/core/analysis/session_analyzer.py +17 -56
  26. moai_adk/core/claude_integration.py +26 -54
  27. moai_adk/core/command_helpers.py +10 -10
  28. moai_adk/core/comprehensive_monitoring_system.py +1183 -0
  29. moai_adk/core/config/auto_spec_config.py +5 -11
  30. moai_adk/core/config/migration.py +19 -9
  31. moai_adk/core/config/unified.py +436 -0
  32. moai_adk/core/context_manager.py +6 -12
  33. moai_adk/core/enterprise_features.py +1404 -0
  34. moai_adk/core/error_recovery_system.py +725 -112
  35. moai_adk/core/event_driven_hook_system.py +1371 -0
  36. moai_adk/core/git/__init__.py +8 -0
  37. moai_adk/core/git/branch_manager.py +3 -11
  38. moai_adk/core/git/checkpoint.py +1 -3
  39. moai_adk/core/git/conflict_detector.py +413 -0
  40. moai_adk/core/git/manager.py +91 -1
  41. moai_adk/core/hooks/post_tool_auto_spec_completion.py +56 -80
  42. moai_adk/core/input_validation_middleware.py +1006 -0
  43. moai_adk/core/integration/engine.py +6 -18
  44. moai_adk/core/integration/integration_tester.py +10 -9
  45. moai_adk/core/integration/utils.py +1 -1
  46. moai_adk/core/issue_creator.py +10 -28
  47. moai_adk/core/jit_context_loader.py +956 -0
  48. moai_adk/core/jit_enhanced_hook_manager.py +1987 -0
  49. moai_adk/core/language_config_resolver.py +485 -0
  50. moai_adk/core/language_validator.py +28 -41
  51. moai_adk/core/mcp/setup.py +15 -12
  52. moai_adk/core/merge/__init__.py +9 -0
  53. moai_adk/core/merge/analyzer.py +481 -0
  54. moai_adk/core/migration/alfred_to_moai_migrator.py +383 -0
  55. moai_adk/core/migration/backup_manager.py +78 -9
  56. moai_adk/core/migration/custom_element_scanner.py +358 -0
  57. moai_adk/core/migration/file_migrator.py +8 -17
  58. moai_adk/core/migration/interactive_checkbox_ui.py +488 -0
  59. moai_adk/core/migration/selective_restorer.py +470 -0
  60. moai_adk/core/migration/template_utils.py +74 -0
  61. moai_adk/core/migration/user_selection_ui.py +338 -0
  62. moai_adk/core/migration/version_detector.py +6 -10
  63. moai_adk/core/migration/version_migrator.py +3 -3
  64. moai_adk/core/performance/cache_system.py +8 -10
  65. moai_adk/core/phase_optimized_hook_scheduler.py +879 -0
  66. moai_adk/core/project/checker.py +2 -4
  67. moai_adk/core/project/detector.py +1 -3
  68. moai_adk/core/project/initializer.py +135 -23
  69. moai_adk/core/project/phase_executor.py +54 -81
  70. moai_adk/core/project/validator.py +6 -12
  71. moai_adk/core/quality/trust_checker.py +9 -27
  72. moai_adk/core/realtime_monitoring_dashboard.py +1724 -0
  73. moai_adk/core/robust_json_parser.py +611 -0
  74. moai_adk/core/rollback_manager.py +73 -148
  75. moai_adk/core/session_manager.py +10 -26
  76. moai_adk/core/skill_loading_system.py +579 -0
  77. moai_adk/core/spec/confidence_scoring.py +31 -100
  78. moai_adk/core/spec/ears_template_engine.py +351 -286
  79. moai_adk/core/spec/quality_validator.py +35 -69
  80. moai_adk/core/spec_status_manager.py +64 -74
  81. moai_adk/core/template/backup.py +45 -20
  82. moai_adk/core/template/config.py +112 -39
  83. moai_adk/core/template/merger.py +11 -19
  84. moai_adk/core/template/processor.py +253 -149
  85. moai_adk/core/template_engine.py +73 -40
  86. moai_adk/core/template_variable_synchronizer.py +417 -0
  87. moai_adk/core/unified_permission_manager.py +745 -0
  88. moai_adk/core/user_behavior_analytics.py +851 -0
  89. moai_adk/core/version_sync.py +429 -0
  90. moai_adk/foundation/__init__.py +56 -0
  91. moai_adk/foundation/backend.py +1027 -0
  92. moai_adk/foundation/database.py +1115 -0
  93. moai_adk/foundation/devops.py +1585 -0
  94. moai_adk/foundation/ears.py +431 -0
  95. moai_adk/foundation/frontend.py +870 -0
  96. moai_adk/foundation/git/commit_templates.py +4 -12
  97. moai_adk/foundation/git.py +376 -0
  98. moai_adk/foundation/langs.py +484 -0
  99. moai_adk/foundation/ml_ops.py +1162 -0
  100. moai_adk/foundation/testing.py +1524 -0
  101. moai_adk/foundation/trust/trust_principles.py +23 -72
  102. moai_adk/foundation/trust/validation_checklist.py +57 -162
  103. moai_adk/project/__init__.py +0 -0
  104. moai_adk/project/configuration.py +1084 -0
  105. moai_adk/project/documentation.py +566 -0
  106. moai_adk/project/schema.py +447 -0
  107. moai_adk/statusline/alfred_detector.py +1 -3
  108. moai_adk/statusline/config.py +13 -4
  109. moai_adk/statusline/enhanced_output_style_detector.py +23 -15
  110. moai_adk/statusline/main.py +51 -15
  111. moai_adk/statusline/renderer.py +104 -48
  112. moai_adk/statusline/update_checker.py +3 -9
  113. moai_adk/statusline/version_reader.py +140 -46
  114. moai_adk/templates/.claude/agents/moai/ai-nano-banana.md +549 -0
  115. moai_adk/templates/.claude/agents/moai/builder-agent.md +445 -0
  116. moai_adk/templates/.claude/agents/moai/builder-command.md +1132 -0
  117. moai_adk/templates/.claude/agents/moai/builder-skill.md +601 -0
  118. moai_adk/templates/.claude/agents/moai/expert-backend.md +831 -0
  119. moai_adk/templates/.claude/agents/moai/expert-database.md +774 -0
  120. moai_adk/templates/.claude/agents/moai/expert-debug.md +396 -0
  121. moai_adk/templates/.claude/agents/moai/expert-devops.md +711 -0
  122. moai_adk/templates/.claude/agents/moai/expert-frontend.md +666 -0
  123. moai_adk/templates/.claude/agents/moai/expert-security.md +474 -0
  124. moai_adk/templates/.claude/agents/moai/expert-uiux.md +1038 -0
  125. moai_adk/templates/.claude/agents/moai/manager-claude-code.md +429 -0
  126. moai_adk/templates/.claude/agents/moai/manager-docs.md +570 -0
  127. moai_adk/templates/.claude/agents/moai/manager-git.md +937 -0
  128. moai_adk/templates/.claude/agents/moai/manager-project.md +891 -0
  129. moai_adk/templates/.claude/agents/moai/manager-quality.md +598 -0
  130. moai_adk/templates/.claude/agents/moai/manager-spec.md +713 -0
  131. moai_adk/templates/.claude/agents/moai/manager-strategy.md +600 -0
  132. moai_adk/templates/.claude/agents/moai/manager-tdd.md +603 -0
  133. moai_adk/templates/.claude/agents/moai/mcp-context7.md +369 -0
  134. moai_adk/templates/.claude/agents/moai/mcp-figma.md +1567 -0
  135. moai_adk/templates/.claude/agents/moai/mcp-notion.md +749 -0
  136. moai_adk/templates/.claude/agents/moai/mcp-playwright.md +427 -0
  137. moai_adk/templates/.claude/agents/moai/mcp-sequential-thinking.md +994 -0
  138. moai_adk/templates/.claude/commands/moai/0-project.md +1143 -0
  139. moai_adk/templates/.claude/commands/moai/1-plan.md +1435 -0
  140. moai_adk/templates/.claude/commands/moai/2-run.md +883 -0
  141. moai_adk/templates/.claude/commands/moai/3-sync.md +993 -0
  142. moai_adk/templates/.claude/commands/moai/9-feedback.md +314 -0
  143. moai_adk/templates/.claude/hooks/__init__.py +8 -0
  144. moai_adk/templates/.claude/hooks/moai/__init__.py +8 -0
  145. moai_adk/templates/.claude/hooks/moai/lib/__init__.py +85 -0
  146. moai_adk/templates/.claude/hooks/moai/lib/checkpoint.py +244 -0
  147. moai_adk/templates/.claude/hooks/moai/lib/common.py +131 -0
  148. moai_adk/templates/.claude/hooks/moai/lib/config_manager.py +446 -0
  149. moai_adk/templates/.claude/hooks/moai/lib/config_validator.py +639 -0
  150. moai_adk/templates/.claude/hooks/moai/lib/example_config.json +104 -0
  151. moai_adk/templates/.claude/hooks/moai/lib/git_operations_manager.py +590 -0
  152. moai_adk/templates/.claude/hooks/moai/lib/language_validator.py +317 -0
  153. moai_adk/templates/.claude/hooks/moai/lib/models.py +102 -0
  154. moai_adk/templates/.claude/hooks/moai/lib/path_utils.py +28 -0
  155. moai_adk/templates/.claude/hooks/moai/lib/project.py +768 -0
  156. moai_adk/templates/.claude/hooks/moai/lib/test_hooks_improvements.py +443 -0
  157. moai_adk/templates/.claude/hooks/moai/lib/timeout.py +160 -0
  158. moai_adk/templates/.claude/hooks/moai/lib/unified_timeout_manager.py +530 -0
  159. moai_adk/templates/.claude/hooks/moai/session_end__auto_cleanup.py +862 -0
  160. moai_adk/templates/.claude/hooks/moai/session_start__show_project_info.py +921 -0
  161. moai_adk/templates/.claude/output-styles/moai/r2d2.md +380 -0
  162. moai_adk/templates/.claude/output-styles/moai/yoda.md +338 -0
  163. moai_adk/templates/.claude/settings.json +172 -0
  164. moai_adk/templates/.claude/skills/moai-docs-generation/SKILL.md +247 -0
  165. moai_adk/templates/.claude/skills/moai-docs-generation/modules/README.md +44 -0
  166. moai_adk/templates/.claude/skills/moai-docs-generation/modules/api-documentation.md +130 -0
  167. moai_adk/templates/.claude/skills/moai-docs-generation/modules/code-documentation.md +152 -0
  168. moai_adk/templates/.claude/skills/moai-docs-generation/modules/multi-format-output.md +178 -0
  169. moai_adk/templates/.claude/skills/moai-docs-generation/modules/user-guides.md +147 -0
  170. moai_adk/templates/.claude/skills/moai-domain-backend/SKILL.md +319 -0
  171. moai_adk/templates/.claude/skills/moai-domain-database/SKILL.md +320 -0
  172. moai_adk/templates/.claude/skills/moai-domain-database/modules/README.md +53 -0
  173. moai_adk/templates/.claude/skills/moai-domain-database/modules/mongodb.md +231 -0
  174. moai_adk/templates/.claude/skills/moai-domain-database/modules/postgresql.md +169 -0
  175. moai_adk/templates/.claude/skills/moai-domain-database/modules/redis.md +262 -0
  176. moai_adk/templates/.claude/skills/moai-domain-frontend/SKILL.md +496 -0
  177. moai_adk/templates/.claude/skills/moai-domain-uiux/SKILL.md +453 -0
  178. moai_adk/templates/.claude/skills/moai-domain-uiux/examples.md +560 -0
  179. moai_adk/templates/.claude/skills/moai-domain-uiux/modules/accessibility-wcag.md +260 -0
  180. moai_adk/templates/.claude/skills/moai-domain-uiux/modules/component-architecture.md +228 -0
  181. moai_adk/templates/.claude/skills/moai-domain-uiux/modules/design-system-tokens.md +405 -0
  182. moai_adk/templates/.claude/skills/moai-domain-uiux/modules/icon-libraries.md +401 -0
  183. moai_adk/templates/.claude/skills/moai-domain-uiux/modules/theming-system.md +373 -0
  184. moai_adk/templates/.claude/skills/moai-domain-uiux/reference.md +243 -0
  185. moai_adk/templates/.claude/skills/moai-formats-data/SKILL.md +491 -0
  186. moai_adk/templates/.claude/skills/moai-formats-data/modules/README.md +98 -0
  187. moai_adk/templates/.claude/skills/moai-formats-data/modules/SKILL-MODULARIZATION-TEMPLATE.md +278 -0
  188. moai_adk/templates/.claude/skills/moai-formats-data/modules/caching-performance.md +459 -0
  189. moai_adk/templates/.claude/skills/moai-formats-data/modules/data-validation.md +485 -0
  190. moai_adk/templates/.claude/skills/moai-formats-data/modules/json-optimization.md +374 -0
  191. moai_adk/templates/.claude/skills/moai-formats-data/modules/toon-encoding.md +308 -0
  192. moai_adk/templates/.claude/skills/moai-foundation-claude/SKILL.md +201 -0
  193. moai_adk/templates/.claude/skills/moai-foundation-claude/reference/best-practices-checklist.md +616 -0
  194. moai_adk/templates/.claude/skills/moai-foundation-claude/reference/claude-code-custom-slash-commands-official.md +729 -0
  195. moai_adk/templates/.claude/skills/moai-foundation-claude/reference/claude-code-hooks-official.md +560 -0
  196. moai_adk/templates/.claude/skills/moai-foundation-claude/reference/claude-code-iam-official.md +635 -0
  197. moai_adk/templates/.claude/skills/moai-foundation-claude/reference/claude-code-memory-official.md +543 -0
  198. moai_adk/templates/.claude/skills/moai-foundation-claude/reference/claude-code-settings-official.md +663 -0
  199. moai_adk/templates/.claude/skills/moai-foundation-claude/reference/claude-code-skills-official.md +113 -0
  200. moai_adk/templates/.claude/skills/moai-foundation-claude/reference/claude-code-sub-agents-official.md +238 -0
  201. moai_adk/templates/.claude/skills/moai-foundation-claude/reference/complete-configuration-guide.md +175 -0
  202. moai_adk/templates/.claude/skills/moai-foundation-claude/reference/skill-examples.md +1674 -0
  203. moai_adk/templates/.claude/skills/moai-foundation-claude/reference/skill-formatting-guide.md +729 -0
  204. moai_adk/templates/.claude/skills/moai-foundation-claude/reference/sub-agents/sub-agent-examples.md +1513 -0
  205. moai_adk/templates/.claude/skills/moai-foundation-claude/reference/sub-agents/sub-agent-formatting-guide.md +1086 -0
  206. moai_adk/templates/.claude/skills/moai-foundation-claude/reference/sub-agents/sub-agent-integration-patterns.md +1100 -0
  207. moai_adk/templates/.claude/skills/moai-foundation-context/SKILL.md +438 -0
  208. moai_adk/templates/.claude/skills/moai-foundation-core/SKILL.md +515 -0
  209. moai_adk/templates/.claude/skills/moai-foundation-core/modules/README.md +296 -0
  210. moai_adk/templates/.claude/skills/moai-foundation-core/modules/agents-reference.md +346 -0
  211. moai_adk/templates/.claude/skills/moai-foundation-core/modules/commands-reference.md +432 -0
  212. moai_adk/templates/.claude/skills/moai-foundation-core/modules/delegation-patterns.md +757 -0
  213. moai_adk/templates/.claude/skills/moai-foundation-core/modules/execution-rules.md +687 -0
  214. moai_adk/templates/.claude/skills/moai-foundation-core/modules/modular-system.md +665 -0
  215. moai_adk/templates/.claude/skills/moai-foundation-core/modules/progressive-disclosure.md +649 -0
  216. moai_adk/templates/.claude/skills/moai-foundation-core/modules/spec-first-tdd.md +864 -0
  217. moai_adk/templates/.claude/skills/moai-foundation-core/modules/token-optimization.md +708 -0
  218. moai_adk/templates/.claude/skills/moai-foundation-core/modules/trust-5-framework.md +981 -0
  219. moai_adk/templates/.claude/skills/moai-foundation-quality/SKILL.md +362 -0
  220. moai_adk/templates/.claude/skills/moai-foundation-quality/examples.md +1232 -0
  221. moai_adk/templates/.claude/skills/moai-foundation-quality/modules/best-practices.md +261 -0
  222. moai_adk/templates/.claude/skills/moai-foundation-quality/modules/integration-patterns.md +194 -0
  223. moai_adk/templates/.claude/skills/moai-foundation-quality/modules/proactive-analysis.md +229 -0
  224. moai_adk/templates/.claude/skills/moai-foundation-quality/modules/trust5-validation.md +169 -0
  225. moai_adk/templates/.claude/skills/moai-foundation-quality/reference.md +1266 -0
  226. moai_adk/templates/.claude/skills/moai-foundation-quality/scripts/quality-gate.sh +668 -0
  227. moai_adk/templates/.claude/skills/moai-foundation-quality/templates/github-actions-quality.yml +481 -0
  228. moai_adk/templates/.claude/skills/moai-foundation-quality/templates/quality-config.yaml +519 -0
  229. moai_adk/templates/.claude/skills/moai-integration-mcp/SKILL.md +352 -0
  230. moai_adk/templates/.claude/skills/moai-integration-mcp/modules/README.md +52 -0
  231. moai_adk/templates/.claude/skills/moai-integration-mcp/modules/error-handling.md +334 -0
  232. moai_adk/templates/.claude/skills/moai-integration-mcp/modules/integration-patterns.md +310 -0
  233. moai_adk/templates/.claude/skills/moai-integration-mcp/modules/security-authentication.md +256 -0
  234. moai_adk/templates/.claude/skills/moai-integration-mcp/modules/server-architecture.md +253 -0
  235. moai_adk/templates/.claude/skills/moai-lang-unified/README.md +133 -0
  236. moai_adk/templates/.claude/skills/moai-lang-unified/SKILL.md +296 -0
  237. moai_adk/templates/.claude/skills/moai-lang-unified/examples.md +1269 -0
  238. moai_adk/templates/.claude/skills/moai-lang-unified/reference.md +331 -0
  239. moai_adk/templates/.claude/skills/moai-library-mermaid/SKILL.md +298 -0
  240. moai_adk/templates/.claude/skills/moai-library-mermaid/advanced-patterns.md +465 -0
  241. moai_adk/templates/.claude/skills/moai-library-mermaid/examples.md +270 -0
  242. moai_adk/templates/.claude/skills/moai-library-mermaid/optimization.md +440 -0
  243. moai_adk/templates/.claude/skills/moai-library-mermaid/reference.md +228 -0
  244. moai_adk/templates/.claude/skills/moai-library-nextra/SKILL.md +316 -0
  245. moai_adk/templates/.claude/skills/moai-library-nextra/advanced-patterns.md +336 -0
  246. moai_adk/templates/.claude/skills/moai-library-nextra/modules/advanced-deployment-patterns.md +182 -0
  247. moai_adk/templates/.claude/skills/moai-library-nextra/modules/advanced-patterns.md +17 -0
  248. moai_adk/templates/.claude/skills/moai-library-nextra/modules/configuration.md +57 -0
  249. moai_adk/templates/.claude/skills/moai-library-nextra/modules/content-architecture-optimization.md +162 -0
  250. moai_adk/templates/.claude/skills/moai-library-nextra/modules/deployment.md +52 -0
  251. moai_adk/templates/.claude/skills/moai-library-nextra/modules/framework-core-configuration.md +186 -0
  252. moai_adk/templates/.claude/skills/moai-library-nextra/modules/i18n-setup.md +55 -0
  253. moai_adk/templates/.claude/skills/moai-library-nextra/modules/mdx-components.md +52 -0
  254. moai_adk/templates/.claude/skills/moai-library-nextra/optimization.md +303 -0
  255. moai_adk/templates/.claude/skills/moai-library-shadcn/SKILL.md +370 -0
  256. moai_adk/templates/.claude/skills/moai-library-shadcn/examples.md +575 -0
  257. moai_adk/templates/.claude/skills/moai-library-shadcn/modules/advanced-patterns.md +394 -0
  258. moai_adk/templates/.claude/skills/moai-library-shadcn/modules/optimization.md +278 -0
  259. moai_adk/templates/.claude/skills/moai-library-shadcn/modules/shadcn-components.md +457 -0
  260. moai_adk/templates/.claude/skills/moai-library-shadcn/modules/shadcn-theming.md +373 -0
  261. moai_adk/templates/.claude/skills/moai-library-shadcn/reference.md +74 -0
  262. moai_adk/templates/.claude/skills/moai-platform-baas/README.md +186 -0
  263. moai_adk/templates/.claude/skills/moai-platform-baas/SKILL.md +290 -0
  264. moai_adk/templates/.claude/skills/moai-platform-baas/examples.md +1225 -0
  265. moai_adk/templates/.claude/skills/moai-platform-baas/reference.md +567 -0
  266. moai_adk/templates/.claude/skills/moai-platform-baas/scripts/provider-selector.py +323 -0
  267. moai_adk/templates/.claude/skills/moai-platform-baas/templates/stack-config.yaml +204 -0
  268. moai_adk/templates/.claude/skills/moai-workflow-jit-docs/SKILL.md +446 -0
  269. moai_adk/templates/.claude/skills/moai-workflow-jit-docs/advanced-patterns.md +379 -0
  270. moai_adk/templates/.claude/skills/moai-workflow-jit-docs/optimization.md +286 -0
  271. moai_adk/templates/.claude/skills/moai-workflow-project/README.md +190 -0
  272. moai_adk/templates/.claude/skills/moai-workflow-project/SKILL.md +387 -0
  273. moai_adk/templates/.claude/skills/moai-workflow-project/__init__.py +520 -0
  274. moai_adk/templates/.claude/skills/moai-workflow-project/complete_workflow_demo_fixed.py +574 -0
  275. moai_adk/templates/.claude/skills/moai-workflow-project/examples/complete_project_setup.py +317 -0
  276. moai_adk/templates/.claude/skills/moai-workflow-project/examples/complete_workflow_demo.py +663 -0
  277. moai_adk/templates/.claude/skills/moai-workflow-project/examples/config-migration-example.json +190 -0
  278. moai_adk/templates/.claude/skills/moai-workflow-project/examples/question-examples.json +135 -0
  279. moai_adk/templates/.claude/skills/moai-workflow-project/examples/quick_start.py +196 -0
  280. moai_adk/templates/.claude/skills/moai-workflow-project/modules/__init__.py +17 -0
  281. moai_adk/templates/.claude/skills/moai-workflow-project/modules/advanced-patterns.md +158 -0
  282. moai_adk/templates/.claude/skills/moai-workflow-project/modules/ask_user_integration.py +340 -0
  283. moai_adk/templates/.claude/skills/moai-workflow-project/modules/batch_questions.py +713 -0
  284. moai_adk/templates/.claude/skills/moai-workflow-project/modules/config_manager.py +538 -0
  285. moai_adk/templates/.claude/skills/moai-workflow-project/modules/documentation_manager.py +1336 -0
  286. moai_adk/templates/.claude/skills/moai-workflow-project/modules/language_initializer.py +730 -0
  287. moai_adk/templates/.claude/skills/moai-workflow-project/modules/migration_manager.py +608 -0
  288. moai_adk/templates/.claude/skills/moai-workflow-project/modules/template_optimizer.py +1005 -0
  289. moai_adk/templates/.claude/skills/moai-workflow-project/schemas/config-schema.json +316 -0
  290. moai_adk/templates/.claude/skills/moai-workflow-project/schemas/tab_schema.json +1362 -0
  291. moai_adk/templates/.claude/skills/moai-workflow-project/templates/config-template.json +71 -0
  292. moai_adk/templates/.claude/skills/moai-workflow-project/templates/doc-templates/product-template.md +44 -0
  293. moai_adk/templates/.claude/skills/moai-workflow-project/templates/doc-templates/structure-template.md +48 -0
  294. moai_adk/templates/.claude/skills/moai-workflow-project/templates/doc-templates/tech-template.md +71 -0
  295. moai_adk/templates/.claude/skills/moai-workflow-project/templates/question-templates/config-manager-setup.json +109 -0
  296. moai_adk/templates/.claude/skills/moai-workflow-project/templates/question-templates/language-initializer.json +228 -0
  297. moai_adk/templates/.claude/skills/moai-workflow-project/templates/question-templates/menu-project-config.json +130 -0
  298. moai_adk/templates/.claude/skills/moai-workflow-project/templates/question-templates/project-batch-questions.json +97 -0
  299. moai_adk/templates/.claude/skills/moai-workflow-project/templates/question-templates/spec-workflow-setup.json +150 -0
  300. moai_adk/templates/.claude/skills/moai-workflow-project/test_integration_simple.py +436 -0
  301. moai_adk/templates/.claude/skills/moai-workflow-templates/SKILL.md +374 -0
  302. moai_adk/templates/.claude/skills/moai-workflow-templates/modules/code-templates.md +124 -0
  303. moai_adk/templates/.claude/skills/moai-workflow-templates/modules/feedback-templates.md +100 -0
  304. moai_adk/templates/.claude/skills/moai-workflow-templates/modules/template-optimizer.md +138 -0
  305. moai_adk/templates/.claude/skills/moai-workflow-testing/LICENSE.txt +202 -0
  306. moai_adk/templates/.claude/skills/moai-workflow-testing/SKILL.md +453 -0
  307. moai_adk/templates/.claude/skills/moai-workflow-testing/advanced-patterns.md +576 -0
  308. moai_adk/templates/.claude/skills/moai-workflow-testing/examples/ai-powered-testing.py +294 -0
  309. moai_adk/templates/.claude/skills/moai-workflow-testing/examples/console_logging.py +35 -0
  310. moai_adk/templates/.claude/skills/moai-workflow-testing/examples/element_discovery.py +40 -0
  311. moai_adk/templates/.claude/skills/moai-workflow-testing/examples/static_html_automation.py +34 -0
  312. moai_adk/templates/.claude/skills/moai-workflow-testing/modules/README.md +220 -0
  313. moai_adk/templates/.claude/skills/moai-workflow-testing/modules/ai-debugging.md +845 -0
  314. moai_adk/templates/.claude/skills/moai-workflow-testing/modules/automated-code-review.md +1416 -0
  315. moai_adk/templates/.claude/skills/moai-workflow-testing/modules/performance-optimization.md +1234 -0
  316. moai_adk/templates/.claude/skills/moai-workflow-testing/modules/smart-refactoring.md +1243 -0
  317. moai_adk/templates/.claude/skills/moai-workflow-testing/modules/tdd-context7.md +1260 -0
  318. moai_adk/templates/.claude/skills/moai-workflow-testing/optimization.md +505 -0
  319. moai_adk/templates/.claude/skills/moai-workflow-testing/reference/playwright-best-practices.md +57 -0
  320. moai_adk/templates/.claude/skills/moai-workflow-testing/scripts/with_server.py +218 -0
  321. moai_adk/templates/.claude/skills/moai-workflow-testing/templates/alfred-integration.md +376 -0
  322. moai_adk/templates/.claude/skills/moai-workflow-testing/workflows/enterprise-testing-workflow.py +571 -0
  323. moai_adk/templates/.claude/skills/moai-worktree/SKILL.md +410 -0
  324. moai_adk/templates/.claude/skills/moai-worktree/examples.md +606 -0
  325. moai_adk/templates/.claude/skills/moai-worktree/modules/integration-patterns.md +982 -0
  326. moai_adk/templates/.claude/skills/moai-worktree/modules/parallel-development.md +778 -0
  327. moai_adk/templates/.claude/skills/moai-worktree/modules/worktree-commands.md +646 -0
  328. moai_adk/templates/.claude/skills/moai-worktree/modules/worktree-management.md +782 -0
  329. moai_adk/templates/.claude/skills/moai-worktree/reference.md +357 -0
  330. moai_adk/templates/.git-hooks/pre-commit +103 -41
  331. moai_adk/templates/.git-hooks/pre-push +116 -21
  332. moai_adk/templates/.github/workflows/ci-universal.yml +513 -0
  333. moai_adk/templates/.github/workflows/security-secrets-check.yml +179 -0
  334. moai_adk/templates/.gitignore +184 -44
  335. moai_adk/templates/.mcp.json +7 -9
  336. moai_adk/templates/.moai/cache/personalization.json +10 -0
  337. moai_adk/templates/.moai/config/config.yaml +344 -0
  338. moai_adk/templates/.moai/config/presets/manual.yaml +28 -0
  339. moai_adk/templates/.moai/config/presets/personal.yaml +30 -0
  340. moai_adk/templates/.moai/config/presets/team.yaml +33 -0
  341. moai_adk/templates/.moai/config/questions/_schema.yaml +79 -0
  342. moai_adk/templates/.moai/config/questions/tab1-user.yaml +108 -0
  343. moai_adk/templates/.moai/config/questions/tab2-project.yaml +122 -0
  344. moai_adk/templates/.moai/config/questions/tab3-git.yaml +542 -0
  345. moai_adk/templates/.moai/config/questions/tab4-quality.yaml +167 -0
  346. moai_adk/templates/.moai/config/questions/tab5-system.yaml +152 -0
  347. moai_adk/templates/.moai/config/sections/git-strategy.yaml +40 -0
  348. moai_adk/templates/.moai/config/sections/language.yaml +11 -0
  349. moai_adk/templates/.moai/config/sections/project.yaml +13 -0
  350. moai_adk/templates/.moai/config/sections/quality.yaml +15 -0
  351. moai_adk/templates/.moai/config/sections/system.yaml +14 -0
  352. moai_adk/templates/.moai/config/sections/user.yaml +5 -0
  353. moai_adk/templates/.moai/config/statusline-config.yaml +86 -0
  354. moai_adk/templates/.moai/scripts/setup-glm.py +136 -0
  355. moai_adk/templates/CLAUDE.md +382 -501
  356. moai_adk/utils/__init__.py +24 -1
  357. moai_adk/utils/banner.py +7 -10
  358. moai_adk/utils/common.py +16 -30
  359. moai_adk/utils/link_validator.py +4 -12
  360. moai_adk/utils/safe_file_reader.py +2 -6
  361. moai_adk/utils/timeout.py +160 -0
  362. moai_adk/utils/toon_utils.py +256 -0
  363. moai_adk/version.py +22 -0
  364. moai_adk-0.32.8.dist-info/METADATA +2478 -0
  365. moai_adk-0.32.8.dist-info/RECORD +396 -0
  366. {moai_adk-0.25.4.dist-info โ†’ moai_adk-0.32.8.dist-info}/WHEEL +1 -1
  367. {moai_adk-0.25.4.dist-info โ†’ moai_adk-0.32.8.dist-info}/entry_points.txt +1 -0
  368. moai_adk/cli/commands/backup.py +0 -82
  369. moai_adk/cli/commands/improve_user_experience.py +0 -348
  370. moai_adk/cli/commands/migrate.py +0 -158
  371. moai_adk/cli/commands/validate_links.py +0 -118
  372. moai_adk/templates/.github/workflows/moai-gitflow.yml +0 -413
  373. moai_adk/templates/.github/workflows/moai-release-create.yml +0 -100
  374. moai_adk/templates/.github/workflows/moai-release-pipeline.yml +0 -188
  375. moai_adk/utils/user_experience.py +0 -531
  376. moai_adk-0.25.4.dist-info/METADATA +0 -2279
  377. moai_adk-0.25.4.dist-info/RECORD +0 -112
  378. {moai_adk-0.25.4.dist-info โ†’ moai_adk-0.32.8.dist-info}/licenses/LICENSE +0 -0
@@ -44,17 +44,26 @@ from __future__ import annotations
44
44
 
45
45
  import json
46
46
  import logging
47
+ import shutil
47
48
  import subprocess
48
49
  from datetime import datetime
49
50
  from pathlib import Path
50
- from typing import Any, cast
51
+ from typing import Any, Union, cast
51
52
 
52
53
  import click
54
+ import yaml
53
55
  from packaging import version
54
56
  from rich.console import Console
55
57
 
56
58
  from moai_adk import __version__
59
+ from moai_adk.core.merge import MergeAnalyzer
57
60
  from moai_adk.core.migration import VersionMigrator
61
+ from moai_adk.core.migration.alfred_to_moai_migrator import AlfredToMoaiMigrator
62
+
63
+ # Import new custom element restoration modules
64
+ from moai_adk.core.migration.custom_element_scanner import create_custom_element_scanner
65
+ from moai_adk.core.migration.selective_restorer import create_selective_restorer
66
+ from moai_adk.core.migration.user_selection_ui import create_user_selection_ui
58
67
  from moai_adk.core.template.processor import TemplateProcessor
59
68
 
60
69
  console = Console()
@@ -98,6 +107,45 @@ class TemplateSyncError(UpdateError):
98
107
  pass
99
108
 
100
109
 
110
+ def _get_config_path(project_path: Path) -> tuple[Path, bool]:
111
+ """Get config file path, preferring YAML over JSON.
112
+
113
+ Returns:
114
+ Tuple of (config_path, is_yaml)
115
+ """
116
+ yaml_path = project_path / ".moai" / "config" / "config.yaml"
117
+ json_path = project_path / ".moai" / "config" / "config.json"
118
+
119
+ if yaml_path.exists():
120
+ return yaml_path, True
121
+ return json_path, False
122
+
123
+
124
+ def _load_config(config_path: Path) -> dict[str, Any]:
125
+ """Load config from YAML or JSON file."""
126
+ if not config_path.exists():
127
+ return {}
128
+
129
+ is_yaml = config_path.suffix in (".yaml", ".yml")
130
+ content = config_path.read_text(encoding="utf-8")
131
+
132
+ if is_yaml:
133
+ return yaml.safe_load(content) or {}
134
+ return json.loads(content)
135
+
136
+
137
+ def _save_config(config_path: Path, config_data: dict[str, Any]) -> None:
138
+ """Save config to YAML or JSON file."""
139
+ is_yaml = config_path.suffix in (".yaml", ".yml")
140
+
141
+ if is_yaml:
142
+ content = yaml.safe_dump(config_data, default_flow_style=False, allow_unicode=True, sort_keys=False)
143
+ else:
144
+ content = json.dumps(config_data, indent=2, ensure_ascii=False) + "\n"
145
+
146
+ config_path.write_text(content, encoding="utf-8")
147
+
148
+
101
149
  def _is_installed_via_uv_tool() -> bool:
102
150
  """Check if moai-adk installed via uv tool.
103
151
 
@@ -220,9 +268,7 @@ def _get_latest_version() -> str:
220
268
  import urllib.request
221
269
 
222
270
  url = "https://pypi.org/pypi/moai-adk/json"
223
- with urllib.request.urlopen(
224
- url, timeout=5
225
- ) as response: # nosec B310 - URL is hardcoded HTTPS to PyPI API, no user input
271
+ with urllib.request.urlopen(url, timeout=5) as response: # nosec B310 - URL is hardcoded HTTPS to PyPI API, no user input
226
272
  data = json.loads(response.read().decode("utf-8"))
227
273
  return cast(str, data["info"]["version"])
228
274
  except (urllib.error.URLError, json.JSONDecodeError, KeyError, TimeoutError) as e:
@@ -280,42 +326,398 @@ def _get_project_config_version(project_path: Path) -> str:
280
326
  Returns "0.0.0" if template_version field not found (indicates no prior sync)
281
327
 
282
328
  Raises:
283
- ValueError: If config.json exists but cannot be parsed
329
+ ValueError: If config file exists but cannot be parsed
284
330
  """
285
331
 
286
- def _is_placeholder(value: str) -> bool:
332
+ def _is_placeholder_val(value: str) -> bool:
287
333
  """Check if value contains unsubstituted template placeholders."""
288
- return (
289
- isinstance(value, str) and value.startswith("{{") and value.endswith("}}")
290
- )
334
+ return isinstance(value, str) and value.startswith("{{") and value.endswith("}}")
291
335
 
292
- config_path = project_path / ".moai" / "config" / "config.json"
336
+ config_path, _ = _get_config_path(project_path)
293
337
 
294
338
  if not config_path.exists():
295
339
  # No config yet, treat as version 0.0.0 (needs initial sync)
296
340
  return "0.0.0"
297
341
 
298
342
  try:
299
- config_data = json.loads(config_path.read_text(encoding="utf-8"))
343
+ config_data = _load_config(config_path)
300
344
  # Check for template_version in project section
301
345
  template_version = config_data.get("project", {}).get("template_version")
302
- if template_version and not _is_placeholder(template_version):
346
+ if template_version and not _is_placeholder_val(template_version):
303
347
  return template_version
304
348
 
305
349
  # Fallback to moai version if no template_version exists
306
350
  moai_version = config_data.get("moai", {}).get("version")
307
- if moai_version and not _is_placeholder(moai_version):
351
+ if moai_version and not _is_placeholder_val(moai_version):
308
352
  return moai_version
309
353
 
310
354
  # If values are placeholders or don't exist, treat as uninitialized (0.0.0 triggers sync)
311
355
  return "0.0.0"
312
- except json.JSONDecodeError as e:
313
- raise ValueError(f"Failed to parse project config.json: {e}") from e
356
+ except (json.JSONDecodeError, yaml.YAMLError) as e:
357
+ raise ValueError(f"Failed to parse project config: {e}") from e
314
358
 
315
359
 
316
- def _detect_stale_cache(
317
- upgrade_output: str, current_version: str, latest_version: str
318
- ) -> bool:
360
+ def _ask_merge_strategy(yes: bool = False) -> str:
361
+ """
362
+ Ask user to choose merge strategy via CLI prompt.
363
+
364
+ Args:
365
+ yes: If True, auto-select "auto" (for --yes flag)
366
+
367
+ Returns:
368
+ "auto" or "manual"
369
+ """
370
+ if yes:
371
+ return "auto"
372
+
373
+ console.print("\n[cyan]๐Ÿ”€ Choose merge strategy:[/cyan]")
374
+ console.print("[cyan] [1] Auto-merge (default)[/cyan]")
375
+ console.print("[dim] โ†’ Template installs fresh + user changes preserved + minimal conflicts[/dim]")
376
+ console.print("[cyan] [2] Manual merge[/cyan]")
377
+ console.print("[dim] โ†’ Backup preserved + merge guide generated + you control merging[/dim]")
378
+
379
+ response = click.prompt("Select [1 or 2]", default="1")
380
+ if response == "2":
381
+ return "manual"
382
+ return "auto"
383
+
384
+
385
+ def _generate_manual_merge_guide(backup_path: Path, template_path: Path, project_path: Path) -> Path:
386
+ """
387
+ Generate comprehensive merge guide for manual merging.
388
+
389
+ Args:
390
+ backup_path: Path to backup directory
391
+ template_path: Path to template directory
392
+ project_path: Project root path
393
+
394
+ Returns:
395
+ Path to generated merge guide
396
+ """
397
+ guide_dir = project_path / ".moai" / "guides"
398
+ guide_dir.mkdir(parents=True, exist_ok=True)
399
+
400
+ guide_path = guide_dir / "merge-guide.md"
401
+
402
+ # Find changed files
403
+ changed_files = []
404
+ backup_claude = backup_path / ".claude"
405
+ backup_path / ".moai"
406
+
407
+ # Compare .claude/
408
+ if backup_claude.exists():
409
+ for file in backup_claude.rglob("*"):
410
+ if file.is_file():
411
+ rel_path = file.relative_to(backup_path)
412
+ current_file = project_path / rel_path
413
+ if current_file.exists():
414
+ if file.read_text(encoding="utf-8", errors="ignore") != current_file.read_text(
415
+ encoding="utf-8", errors="ignore"
416
+ ):
417
+ changed_files.append(f" - {rel_path}")
418
+ else:
419
+ changed_files.append(f" - {rel_path} (new)")
420
+
421
+ # Generate guide
422
+ timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
423
+ guide_content = f"""# Merge Guide - Manual Merge Mode
424
+
425
+ **Generated**: {timestamp}
426
+ **Backup Location**: `{backup_path.relative_to(project_path)}/`
427
+
428
+ ## Summary
429
+
430
+ During this update, the following files were changed:
431
+
432
+ {chr(10).join(changed_files) if changed_files else " (No changes detected)"}
433
+
434
+ ## How to Merge
435
+
436
+ ### Option 1: Using diff (Terminal)
437
+
438
+ ```bash
439
+ # Compare specific files
440
+ diff {backup_path.name}/.claude/settings.json .claude/settings.json
441
+
442
+ # View all differences
443
+ diff -r {backup_path.name}/ .
444
+ ```
445
+
446
+ ### Option 2: Using Visual Merge Tool
447
+
448
+ ```bash
449
+ # macOS/Linux - Using meld
450
+ meld {backup_path.relative_to(project_path)}/ .
451
+
452
+ # Using VSCode
453
+ code --diff {backup_path.relative_to(project_path)}/.claude/settings.json .claude/settings.json
454
+ ```
455
+
456
+ ### Option 3: Manual Line-by-Line
457
+
458
+ 1. Open backup file in your editor
459
+ 2. Open current file side-by-side
460
+ 3. Manually copy your customizations
461
+
462
+ ## Key Files to Review
463
+
464
+ ### .claude/settings.json
465
+ - Contains MCP servers, hooks, environment variables
466
+ - **Action**: Restore any custom MCP servers and environment variables
467
+ - **Location**: {backup_path.relative_to(project_path)}/.claude/settings.json
468
+
469
+ ### .moai/config/config.json
470
+ - Contains project configuration and metadata
471
+ - **Action**: Verify user-specific settings are preserved
472
+ - **Location**: {backup_path.relative_to(project_path)}/.moai/config/config.json
473
+
474
+ ### .claude/commands/, .claude/agents/, .claude/hooks/
475
+ - Contains custom scripts and automation
476
+ - **Action**: Restore any custom scripts outside of /moai/ folders
477
+ - **Location**: {backup_path.relative_to(project_path)}/.claude/
478
+
479
+ ## Migration Checklist
480
+
481
+ - [ ] Compare `.claude/settings.json`
482
+ - [ ] Restore custom MCP servers
483
+ - [ ] Restore environment variables
484
+ - [ ] Verify hooks are properly configured
485
+
486
+ - [ ] Review `.moai/config/config.json`
487
+ - [ ] Check version was updated
488
+ - [ ] Verify user settings preserved
489
+
490
+ - [ ] Restore custom scripts
491
+ - [ ] Any custom commands outside /moai/
492
+ - [ ] Any custom agents outside /moai/
493
+ - [ ] Any custom hooks outside /moai/
494
+
495
+ - [ ] Run tests
496
+ ```bash
497
+ uv run pytest
498
+ moai-adk validate
499
+ ```
500
+
501
+ - [ ] Commit changes
502
+ ```bash
503
+ git add .
504
+ git commit -m "merge: Update templates with manual merge"
505
+ ```
506
+
507
+ ## Rollback if Needed
508
+
509
+ If you want to cancel and restore the backup:
510
+
511
+ ```bash
512
+ # Restore everything from backup
513
+ cp -r {backup_path.relative_to(project_path)}/.claude .
514
+ cp -r {backup_path.relative_to(project_path)}/.moai .
515
+ cp {backup_path.relative_to(project_path)}/CLAUDE.md .
516
+
517
+ # Or restore specific files
518
+ cp {backup_path.relative_to(project_path)}/.claude/settings.json .claude/
519
+ ```
520
+
521
+ ## Questions?
522
+
523
+ If you encounter merge conflicts or issues:
524
+
525
+ 1. Check the backup folder for original files
526
+ 2. Compare line-by-line using diff tools
527
+ 3. Consult documentation: https://adk.mo.ai.kr/update-merge
528
+
529
+ ---
530
+
531
+ **Backup**: `{backup_path}/`
532
+ **Generated**: {timestamp}
533
+ """
534
+
535
+ guide_path.write_text(guide_content, encoding="utf-8")
536
+ logger.info(f"โœ… Merge guide created: {guide_path}")
537
+ return guide_path
538
+
539
+
540
+ def _migrate_legacy_logs(project_path: Path, dry_run: bool = False) -> bool:
541
+ """Migrate legacy log files to unified directory structure.
542
+
543
+ Creates new unified directory structure (.moai/docs/, .moai/logs/archive/) and
544
+ migrates files from legacy locations to new unified structure:
545
+ - .moai/memory/last-session-state.json โ†’ .moai/logs/sessions/
546
+ - .moai/error_logs/ โ†’ .moai/logs/errors/
547
+ - .moai/reports/ โ†’ .moai/docs/reports/
548
+
549
+ Args:
550
+ project_path: Project directory path (absolute)
551
+ dry_run: If True, only simulate migration without making changes
552
+
553
+ Returns:
554
+ True if migration succeeded or no migration needed, False otherwise
555
+
556
+ Raises:
557
+ Exception: If migration fails during actual execution
558
+ """
559
+ try:
560
+ # Define source and target directories
561
+ legacy_memory = project_path / ".moai" / "memory"
562
+ legacy_error_logs = project_path / ".moai" / "error_logs"
563
+ legacy_reports = project_path / ".moai" / "reports"
564
+
565
+ # Create new unified directory structure
566
+ new_logs_dir = project_path / ".moai" / "logs"
567
+ new_docs_dir = project_path / ".moai" / "docs"
568
+ new_sessions_dir = new_logs_dir / "sessions"
569
+ new_errors_dir = new_logs_dir / "errors"
570
+ new_archive_dir = new_logs_dir / "archive"
571
+ new_docs_reports_dir = new_docs_dir / "reports"
572
+
573
+ migration_log = []
574
+ files_migrated = 0
575
+ files_skipped = 0
576
+
577
+ # Check if any legacy directories exist
578
+ has_legacy_files = legacy_memory.exists() or legacy_error_logs.exists() or legacy_reports.exists()
579
+
580
+ if not has_legacy_files:
581
+ if not dry_run:
582
+ # Create new directory structure anyway for consistency
583
+ new_logs_dir.mkdir(parents=True, exist_ok=True)
584
+ new_docs_dir.mkdir(parents=True, exist_ok=True)
585
+ new_sessions_dir.mkdir(parents=True, exist_ok=True)
586
+ new_errors_dir.mkdir(parents=True, exist_ok=True)
587
+ new_archive_dir.mkdir(parents=True, exist_ok=True)
588
+ new_docs_reports_dir.mkdir(parents=True, exist_ok=True)
589
+ return True
590
+
591
+ if dry_run:
592
+ console.print("[cyan]๐Ÿ” Legacy log migration (dry run):[/cyan]")
593
+
594
+ # Create new directories if not dry run
595
+ if not dry_run:
596
+ new_logs_dir.mkdir(parents=True, exist_ok=True)
597
+ new_docs_dir.mkdir(parents=True, exist_ok=True)
598
+ new_sessions_dir.mkdir(parents=True, exist_ok=True)
599
+ new_errors_dir.mkdir(parents=True, exist_ok=True)
600
+ new_archive_dir.mkdir(parents=True, exist_ok=True)
601
+ new_docs_reports_dir.mkdir(parents=True, exist_ok=True)
602
+
603
+ # Migration 1: .moai/memory/last-session-state.json โ†’ .moai/logs/sessions/
604
+ if legacy_memory.exists():
605
+ session_file = legacy_memory / "last-session-state.json"
606
+ if session_file.exists():
607
+ target_file = new_sessions_dir / "last-session-state.json"
608
+
609
+ if target_file.exists():
610
+ files_skipped += 1
611
+ migration_log.append(f"Skipped: {session_file.relative_to(project_path)} (target already exists)")
612
+ else:
613
+ if not dry_run:
614
+ shutil.copy2(session_file, target_file)
615
+ # Preserve original timestamp
616
+ shutil.copystat(session_file, target_file)
617
+ src_path = session_file.relative_to(project_path)
618
+ dst_path = target_file.relative_to(project_path)
619
+ migration_log.append(f"Migrated: {src_path} โ†’ {dst_path}")
620
+ else:
621
+ src_path = session_file.relative_to(project_path)
622
+ dst_path = target_file.relative_to(project_path)
623
+ migration_log.append(f"Would migrate: {src_path} โ†’ {dst_path}")
624
+ files_migrated += 1
625
+
626
+ # Migration 2: .moai/error_logs/ โ†’ .moai/logs/errors/
627
+ if legacy_error_logs.exists() and legacy_error_logs.is_dir():
628
+ for error_file in legacy_error_logs.rglob("*"):
629
+ if error_file.is_file():
630
+ relative_path = error_file.relative_to(legacy_error_logs)
631
+ target_file = new_errors_dir / relative_path
632
+
633
+ # Ensure target directory exists
634
+ if not dry_run:
635
+ target_file.parent.mkdir(parents=True, exist_ok=True)
636
+
637
+ if target_file.exists():
638
+ files_skipped += 1
639
+ error_path = error_file.relative_to(project_path)
640
+ migration_log.append(f"Skipped: {error_path} (target already exists)")
641
+ else:
642
+ if not dry_run:
643
+ shutil.copy2(error_file, target_file)
644
+ shutil.copystat(error_file, target_file)
645
+ error_path = error_file.relative_to(project_path)
646
+ target_path = target_file.relative_to(project_path)
647
+ migration_log.append(f"Migrated: {error_path} โ†’ {target_path}")
648
+ else:
649
+ error_path = error_file.relative_to(project_path)
650
+ target_path = target_file.relative_to(project_path)
651
+ migration_log.append(f"Would migrate: {error_path} โ†’ {target_path}")
652
+ files_migrated += 1
653
+
654
+ # Migration 3: .moai/reports/ โ†’ .moai/docs/reports/
655
+ if legacy_reports.exists() and legacy_reports.is_dir():
656
+ for report_file in legacy_reports.rglob("*"):
657
+ if report_file.is_file():
658
+ relative_path = report_file.relative_to(legacy_reports)
659
+ target_file = new_docs_reports_dir / relative_path
660
+
661
+ # Ensure target directory exists
662
+ if not dry_run:
663
+ target_file.parent.mkdir(parents=True, exist_ok=True)
664
+
665
+ if target_file.exists():
666
+ files_skipped += 1
667
+ report_path = report_file.relative_to(project_path)
668
+ migration_log.append(f"Skipped: {report_path} (target already exists)")
669
+ else:
670
+ if not dry_run:
671
+ shutil.copy2(report_file, target_file)
672
+ shutil.copystat(report_file, target_file)
673
+ report_path = report_file.relative_to(project_path)
674
+ target_path = target_file.relative_to(project_path)
675
+ migration_log.append(f"Migrated: {report_path} โ†’ {target_path}")
676
+ else:
677
+ report_path = report_file.relative_to(project_path)
678
+ target_path = target_file.relative_to(project_path)
679
+ migration_log.append(f"Would migrate: {report_path} โ†’ {target_path}")
680
+ files_migrated += 1
681
+
682
+ # Create migration log
683
+ migration_log_path = new_logs_dir / "migration-log.json"
684
+ if not dry_run and files_migrated > 0:
685
+ migration_data = {
686
+ "migration_timestamp": datetime.now().isoformat(),
687
+ "moai_adk_version": __version__,
688
+ "files_migrated": files_migrated,
689
+ "files_skipped": files_skipped,
690
+ "migration_log": migration_log,
691
+ "legacy_directories_found": [
692
+ str(d.relative_to(project_path))
693
+ for d in [legacy_memory, legacy_error_logs, legacy_reports]
694
+ if d.exists()
695
+ ],
696
+ }
697
+ json_content = json.dumps(migration_data, indent=2, ensure_ascii=False)
698
+ migration_log_path.write_text(json_content + "\n", encoding="utf-8")
699
+
700
+ # Display results
701
+ if files_migrated > 0 or files_skipped > 0:
702
+ if dry_run:
703
+ console.print(f" [yellow]Would migrate {files_migrated} files, skip {files_skipped} files[/yellow]")
704
+ else:
705
+ console.print(f" [green]โœ“ Migrated {files_migrated} legacy log files[/green]")
706
+ if files_skipped > 0:
707
+ console.print(f" [yellow]โš  Skipped {files_skipped} files (already exist)[/yellow]")
708
+ console.print(f" [dim] Migration log: {migration_log_path.relative_to(project_path)}[/dim]")
709
+ elif has_legacy_files:
710
+ console.print(" [dim] No files to migrate[/dim]")
711
+
712
+ return True
713
+
714
+ except Exception as e:
715
+ console.print(f" [red]โœ— Log migration failed: {e}[/red]")
716
+ logger.error(f"Legacy log migration failed: {e}", exc_info=True)
717
+ return False
718
+
719
+
720
+ def _detect_stale_cache(upgrade_output: str, current_version: str, latest_version: str) -> bool:
319
721
  """
320
722
  Detect if uv cache is stale by comparing versions.
321
723
 
@@ -410,9 +812,7 @@ def _clear_uv_package_cache(package_name: str = "moai-adk") -> bool:
410
812
  return False
411
813
 
412
814
 
413
- def _execute_upgrade_with_retry(
414
- installer_cmd: list[str], package_name: str = "moai-adk"
415
- ) -> bool:
815
+ def _execute_upgrade_with_retry(installer_cmd: list[str], package_name: str = "moai-adk") -> bool:
416
816
  """
417
817
  Execute upgrade with automatic cache retry on stale detection.
418
818
 
@@ -459,9 +859,7 @@ def _execute_upgrade_with_retry(
459
859
  """
460
860
  # Stage 1: First upgrade attempt
461
861
  try:
462
- result = subprocess.run(
463
- installer_cmd, capture_output=True, text=True, timeout=60, check=False
464
- )
862
+ result = subprocess.run(installer_cmd, capture_output=True, text=True, timeout=60, check=False)
465
863
  except subprocess.TimeoutExpired:
466
864
  raise # Re-raise timeout for caller to handle
467
865
  except Exception:
@@ -530,9 +928,7 @@ def _execute_upgrade(installer_cmd: list[str]) -> bool:
530
928
  subprocess.TimeoutExpired: If upgrade times out
531
929
  """
532
930
  try:
533
- result = subprocess.run(
534
- installer_cmd, capture_output=True, text=True, timeout=60, check=False
535
- )
931
+ result = subprocess.run(installer_cmd, capture_output=True, text=True, timeout=60, check=False)
536
932
  return result.returncode == 0
537
933
  except subprocess.TimeoutExpired:
538
934
  raise # Re-raise timeout for caller to handle
@@ -540,12 +936,539 @@ def _execute_upgrade(installer_cmd: list[str]) -> bool:
540
936
  return False
541
937
 
542
938
 
543
- def _sync_templates(project_path: Path, force: bool = False) -> bool:
939
+ def _preserve_user_settings(project_path: Path) -> dict[str, Path | None]:
940
+ """Back up user-specific settings files before template sync.
941
+
942
+ Args:
943
+ project_path: Project directory path
944
+
945
+ Returns:
946
+ Dictionary with backup paths of preserved files
947
+ """
948
+ preserved = {}
949
+ claude_dir = project_path / ".claude"
950
+
951
+ # Preserve settings.local.json (user MCP and GLM configuration)
952
+ settings_local = claude_dir / "settings.local.json"
953
+ if settings_local.exists():
954
+ try:
955
+ backup_dir = project_path / ".moai-backups" / "settings-backup"
956
+ backup_dir.mkdir(parents=True, exist_ok=True)
957
+ backup_path = backup_dir / "settings.local.json"
958
+ backup_path.write_text(settings_local.read_text(encoding="utf-8"))
959
+ preserved["settings.local.json"] = backup_path
960
+ console.print(" [cyan]๐Ÿ’พ Backed up user settings[/cyan]")
961
+ except Exception as e:
962
+ logger.warning(f"Failed to backup settings.local.json: {e}")
963
+ preserved["settings.local.json"] = None
964
+ else:
965
+ preserved["settings.local.json"] = None
966
+
967
+ return preserved
968
+
969
+
970
+ def _restore_user_settings(project_path: Path, preserved: dict[str, Path | None]) -> bool:
971
+ """Restore user-specific settings files after template sync.
972
+
973
+ Args:
974
+ project_path: Project directory path
975
+ preserved: Dictionary of backup paths from _preserve_user_settings()
976
+
977
+ Returns:
978
+ True if restoration succeeded, False otherwise
979
+ """
980
+ claude_dir = project_path / ".claude"
981
+ claude_dir.mkdir(parents=True, exist_ok=True)
982
+
983
+ success = True
984
+
985
+ # Restore settings.local.json
986
+ if preserved.get("settings.local.json"):
987
+ try:
988
+ backup_path = preserved["settings.local.json"]
989
+ settings_local = claude_dir / "settings.local.json"
990
+ settings_local.write_text(backup_path.read_text(encoding="utf-8"))
991
+ console.print(" [cyan]โœ“ Restored user settings[/cyan]")
992
+ except Exception as e:
993
+ console.print(f" [yellow]โš ๏ธ Failed to restore settings.local.json: {e}[/yellow]")
994
+ logger.warning(f"Failed to restore settings.local.json: {e}")
995
+ success = False
996
+
997
+ return success
998
+
999
+
1000
+ def _get_template_skill_names() -> set[str]:
1001
+ """Get set of skill folder names from installed template.
1002
+
1003
+ Returns:
1004
+ Set of skill folder names that are part of the template package.
1005
+ """
1006
+ template_path = Path(__file__).parent.parent.parent / "templates"
1007
+ skills_path = template_path / ".claude" / "skills"
1008
+
1009
+ if not skills_path.exists():
1010
+ return set()
1011
+
1012
+ return {d.name for d in skills_path.iterdir() if d.is_dir()}
1013
+
1014
+
1015
+ def _get_template_command_names() -> set[str]:
1016
+ """Get set of command file names from installed template.
1017
+
1018
+ Returns:
1019
+ Set of .md command file names from .claude/commands/moai/ in template.
1020
+ """
1021
+ template_path = Path(__file__).parent.parent.parent / "templates"
1022
+ commands_path = template_path / ".claude" / "commands" / "moai"
1023
+
1024
+ if not commands_path.exists():
1025
+ return set()
1026
+
1027
+ return {f.name for f in commands_path.iterdir() if f.is_file() and f.suffix == ".md"}
1028
+
1029
+
1030
+ def _get_template_agent_names() -> set[str]:
1031
+ """Get set of agent file names from installed template.
1032
+
1033
+ Returns:
1034
+ Set of agent file names from .claude/agents/ in template.
1035
+ """
1036
+ template_path = Path(__file__).parent.parent.parent / "templates"
1037
+ agents_path = template_path / ".claude" / "agents"
1038
+
1039
+ if not agents_path.exists():
1040
+ return set()
1041
+
1042
+ return {f.name for f in agents_path.iterdir() if f.is_file()}
1043
+
1044
+
1045
+ def _get_template_hook_names() -> set[str]:
1046
+ """Get set of hook file names from installed template.
1047
+
1048
+ Returns:
1049
+ Set of .py hook file names from .claude/hooks/moai/ in template.
1050
+ """
1051
+ template_path = Path(__file__).parent.parent.parent / "templates"
1052
+ hooks_path = template_path / ".claude" / "hooks" / "moai"
1053
+
1054
+ if not hooks_path.exists():
1055
+ return set()
1056
+
1057
+ return {f.name for f in hooks_path.iterdir() if f.is_file() and f.suffix == ".py"}
1058
+
1059
+
1060
+ def _detect_custom_commands(project_path: Path, template_commands: set[str]) -> list[str]:
1061
+ """Detect custom commands NOT in template (user-created).
1062
+
1063
+ Args:
1064
+ project_path: Project path (absolute)
1065
+ template_commands: Set of template command file names
1066
+
1067
+ Returns:
1068
+ Sorted list of custom command file names.
1069
+ """
1070
+ commands_path = project_path / ".claude" / "commands" / "moai"
1071
+
1072
+ if not commands_path.exists():
1073
+ return []
1074
+
1075
+ project_commands = {f.name for f in commands_path.iterdir() if f.is_file() and f.suffix == ".md"}
1076
+ custom_commands = project_commands - template_commands
1077
+
1078
+ return sorted(custom_commands)
1079
+
1080
+
1081
+ def _detect_custom_agents(project_path: Path, template_agents: set[str]) -> list[str]:
1082
+ """Detect custom agents NOT in template (user-created).
1083
+
1084
+ Args:
1085
+ project_path: Project path (absolute)
1086
+ template_agents: Set of template agent file names
1087
+
1088
+ Returns:
1089
+ Sorted list of custom agent file names.
1090
+ """
1091
+ agents_path = project_path / ".claude" / "agents"
1092
+
1093
+ if not agents_path.exists():
1094
+ return []
1095
+
1096
+ project_agents = {f.name for f in agents_path.iterdir() if f.is_file()}
1097
+ custom_agents = project_agents - template_agents
1098
+
1099
+ return sorted(custom_agents)
1100
+
1101
+
1102
+ def _detect_custom_hooks(project_path: Path, template_hooks: set[str]) -> list[str]:
1103
+ """Detect custom hooks NOT in template (user-created).
1104
+
1105
+ Args:
1106
+ project_path: Project path (absolute)
1107
+ template_hooks: Set of template hook file names
1108
+
1109
+ Returns:
1110
+ Sorted list of custom hook file names.
1111
+ """
1112
+ hooks_path = project_path / ".claude" / "hooks" / "moai"
1113
+
1114
+ if not hooks_path.exists():
1115
+ return []
1116
+
1117
+ project_hooks = {f.name for f in hooks_path.iterdir() if f.is_file() and f.suffix == ".py"}
1118
+ custom_hooks = project_hooks - template_hooks
1119
+
1120
+ return sorted(custom_hooks)
1121
+
1122
+
1123
+ def _group_custom_files_by_type(
1124
+ custom_commands: list[str],
1125
+ custom_agents: list[str],
1126
+ custom_hooks: list[str],
1127
+ ) -> dict[str, list[str]]:
1128
+ """Group custom files by type for UI display.
1129
+
1130
+ Args:
1131
+ custom_commands: List of custom command file names
1132
+ custom_agents: List of custom agent file names
1133
+ custom_hooks: List of custom hook file names
1134
+
1135
+ Returns:
1136
+ Dictionary with keys: commands, agents, hooks
1137
+ """
1138
+ return {
1139
+ "commands": custom_commands,
1140
+ "agents": custom_agents,
1141
+ "hooks": custom_hooks,
1142
+ }
1143
+
1144
+
1145
+ def _prompt_custom_files_restore(
1146
+ custom_commands: list[str],
1147
+ custom_agents: list[str],
1148
+ custom_hooks: list[str],
1149
+ yes: bool = False,
1150
+ ) -> dict[str, list[str]]:
1151
+ """Interactive fuzzy checkbox for custom files restore with search support.
1152
+
1153
+ Args:
1154
+ custom_commands: List of custom command file names
1155
+ custom_agents: List of custom agent file names
1156
+ custom_hooks: List of custom hook file names
1157
+ yes: Auto-confirm flag (skips restoration in CI/CD mode)
1158
+
1159
+ Returns:
1160
+ Dictionary with selected files grouped by type.
1161
+ """
1162
+ # If no custom files, skip UI
1163
+ if not (custom_commands or custom_agents or custom_hooks):
1164
+ return {
1165
+ "commands": [],
1166
+ "agents": [],
1167
+ "hooks": [],
1168
+ }
1169
+
1170
+ # In --yes mode, skip restoration (safest default)
1171
+ if yes:
1172
+ console.print("\n[dim] Skipping custom files restoration (--yes mode)[/dim]\n")
1173
+ return {
1174
+ "commands": [],
1175
+ "agents": [],
1176
+ "hooks": [],
1177
+ }
1178
+
1179
+ # Try to use new UI, fallback to questionary if import fails
1180
+ try:
1181
+ from moai_adk.cli.ui.prompts import create_grouped_choices, fuzzy_checkbox
1182
+
1183
+ # Build grouped choices for fuzzy checkbox
1184
+ groups: dict[str, list[dict[str, str]]] = {}
1185
+
1186
+ if custom_commands:
1187
+ groups["Commands (.claude/commands/moai/)"] = [
1188
+ {"name": cmd, "value": f"cmd:{cmd}"} for cmd in custom_commands
1189
+ ]
1190
+
1191
+ if custom_agents:
1192
+ groups["Agents (.claude/agents/)"] = [{"name": agent, "value": f"agent:{agent}"} for agent in custom_agents]
1193
+
1194
+ if custom_hooks:
1195
+ groups["Hooks (.claude/hooks/moai/)"] = [{"name": hook, "value": f"hook:{hook}"} for hook in custom_hooks]
1196
+
1197
+ choices = create_grouped_choices(groups)
1198
+
1199
+ console.print("\n[#DA7756]๐Ÿ“ฆ Custom files detected in backup:[/#DA7756]")
1200
+ console.print("[dim] Use fuzzy search to find files quickly[/dim]\n")
1201
+
1202
+ selected = fuzzy_checkbox(
1203
+ "Select custom files to restore:",
1204
+ choices=choices,
1205
+ instruction="[Space] Toggle [Tab] All [Enter] Confirm [Type to search]",
1206
+ )
1207
+
1208
+ except ImportError:
1209
+ # Fallback to questionary if new UI not available
1210
+ import questionary
1211
+ from questionary import Choice, Separator
1212
+
1213
+ choices_legacy: list[Union[Separator, Choice]] = []
1214
+
1215
+ if custom_commands:
1216
+ choices_legacy.append(Separator("Commands (.claude/commands/moai/)"))
1217
+ for cmd in custom_commands:
1218
+ choices_legacy.append(Choice(title=cmd, value=f"cmd:{cmd}"))
1219
+
1220
+ if custom_agents:
1221
+ choices_legacy.append(Separator("Agents (.claude/agents/)"))
1222
+ for agent in custom_agents:
1223
+ choices_legacy.append(Choice(title=agent, value=f"agent:{agent}"))
1224
+
1225
+ if custom_hooks:
1226
+ choices_legacy.append(Separator("Hooks (.claude/hooks/moai/)"))
1227
+ for hook in custom_hooks:
1228
+ choices_legacy.append(Choice(title=hook, value=f"hook:{hook}"))
1229
+
1230
+ console.print("\n[cyan]๐Ÿ“ฆ Custom files detected in backup:[/cyan]")
1231
+ console.print("[dim] Select files to restore (none selected by default)[/dim]\n")
1232
+
1233
+ selected = questionary.checkbox(
1234
+ "Select custom files to restore:",
1235
+ choices=choices_legacy,
1236
+ ).ask()
1237
+
1238
+ # Parse results
1239
+ result_commands = []
1240
+ result_agents = []
1241
+ result_hooks = []
1242
+
1243
+ if selected:
1244
+ for item in selected:
1245
+ if item.startswith("cmd:"):
1246
+ result_commands.append(item[4:])
1247
+ elif item.startswith("agent:"):
1248
+ result_agents.append(item[6:])
1249
+ elif item.startswith("hook:"):
1250
+ result_hooks.append(item[5:])
1251
+
1252
+ return {
1253
+ "commands": result_commands,
1254
+ "agents": result_agents,
1255
+ "hooks": result_hooks,
1256
+ }
1257
+
1258
+
1259
+ def _restore_custom_files(
1260
+ project_path: Path,
1261
+ backup_path: Path,
1262
+ selected_commands: list[str],
1263
+ selected_agents: list[str],
1264
+ selected_hooks: list[str],
1265
+ ) -> bool:
1266
+ """Restore selected custom files from backup to project.
1267
+
1268
+ Args:
1269
+ project_path: Project directory path
1270
+ backup_path: Backup directory path
1271
+ selected_commands: List of command files to restore
1272
+ selected_agents: List of agent files to restore
1273
+ selected_hooks: List of hook files to restore
1274
+
1275
+ Returns:
1276
+ True if all restorations succeeded, False otherwise.
1277
+ """
1278
+ import shutil
1279
+
1280
+ success = True
1281
+
1282
+ # Restore commands
1283
+ if selected_commands:
1284
+ commands_dst = project_path / ".claude" / "commands" / "moai"
1285
+ commands_dst.mkdir(parents=True, exist_ok=True)
1286
+
1287
+ for cmd_file in selected_commands:
1288
+ src = backup_path / ".claude" / "commands" / "moai" / cmd_file
1289
+ dst = commands_dst / cmd_file
1290
+
1291
+ if src.exists():
1292
+ try:
1293
+ shutil.copy2(src, dst)
1294
+ except Exception as e:
1295
+ logger.warning(f"Failed to restore command {cmd_file}: {e}")
1296
+ success = False
1297
+ else:
1298
+ logger.warning(f"Command file not in backup: {cmd_file}")
1299
+ success = False
1300
+
1301
+ # Restore agents
1302
+ if selected_agents:
1303
+ agents_dst = project_path / ".claude" / "agents"
1304
+ agents_dst.mkdir(parents=True, exist_ok=True)
1305
+
1306
+ for agent_file in selected_agents:
1307
+ src = backup_path / ".claude" / "agents" / agent_file
1308
+ dst = agents_dst / agent_file
1309
+
1310
+ if src.exists():
1311
+ try:
1312
+ shutil.copy2(src, dst)
1313
+ except Exception as e:
1314
+ logger.warning(f"Failed to restore agent {agent_file}: {e}")
1315
+ success = False
1316
+ else:
1317
+ logger.warning(f"Agent file not in backup: {agent_file}")
1318
+ success = False
1319
+
1320
+ # Restore hooks
1321
+ if selected_hooks:
1322
+ hooks_dst = project_path / ".claude" / "hooks" / "moai"
1323
+ hooks_dst.mkdir(parents=True, exist_ok=True)
1324
+
1325
+ for hook_file in selected_hooks:
1326
+ src = backup_path / ".claude" / "hooks" / "moai" / hook_file
1327
+ dst = hooks_dst / hook_file
1328
+
1329
+ if src.exists():
1330
+ try:
1331
+ shutil.copy2(src, dst)
1332
+ except Exception as e:
1333
+ logger.warning(f"Failed to restore hook {hook_file}: {e}")
1334
+ success = False
1335
+ else:
1336
+ logger.warning(f"Hook file not in backup: {hook_file}")
1337
+ success = False
1338
+
1339
+ return success
1340
+
1341
+
1342
+ def _detect_custom_skills(project_path: Path, template_skills: set[str]) -> list[str]:
1343
+ """Detect skills NOT in template (user-created).
1344
+
1345
+ Args:
1346
+ project_path: Project path (absolute)
1347
+ template_skills: Set of template skill names
1348
+
1349
+ Returns:
1350
+ Sorted list of custom skill names.
1351
+ """
1352
+ skills_path = project_path / ".claude" / "skills"
1353
+
1354
+ if not skills_path.exists():
1355
+ return []
1356
+
1357
+ project_skills = {d.name for d in skills_path.iterdir() if d.is_dir()}
1358
+ custom_skills = project_skills - template_skills
1359
+
1360
+ return sorted(custom_skills)
1361
+
1362
+
1363
+ def _prompt_skill_restore(custom_skills: list[str], yes: bool = False) -> list[str]:
1364
+ """Interactive fuzzy checkbox for skill restore with search support.
1365
+
1366
+ Args:
1367
+ custom_skills: List of custom skill names
1368
+ yes: Auto-confirm flag (skips restoration in CI/CD mode)
1369
+
1370
+ Returns:
1371
+ List of skills user selected to restore.
1372
+ """
1373
+ if not custom_skills:
1374
+ return []
1375
+
1376
+ console.print("\n[#DA7756]๐Ÿ“ฆ Custom skills detected in backup:[/#DA7756]")
1377
+ for skill in custom_skills:
1378
+ console.print(f" โ€ข {skill}")
1379
+ console.print()
1380
+
1381
+ if yes:
1382
+ console.print("[dim] Skipping restoration (--yes mode)[/dim]\n")
1383
+ return []
1384
+
1385
+ # Try new UI, fallback to questionary
1386
+ try:
1387
+ from moai_adk.cli.ui.prompts import fuzzy_checkbox
1388
+
1389
+ choices = [{"name": skill, "value": skill} for skill in custom_skills]
1390
+
1391
+ selected = fuzzy_checkbox(
1392
+ "Select skills to restore (type to search):",
1393
+ choices=choices,
1394
+ instruction="[Space] Toggle [Tab] All [Enter] Confirm [Type to search]",
1395
+ )
1396
+
1397
+ except ImportError:
1398
+ import questionary
1399
+
1400
+ selected = questionary.checkbox(
1401
+ "Select skills to restore (none selected by default):",
1402
+ choices=[questionary.Choice(title=skill, checked=False) for skill in custom_skills],
1403
+ ).ask()
1404
+
1405
+ return selected if selected else []
1406
+
1407
+
1408
+ def _restore_selected_skills(skills: list[str], backup_path: Path, project_path: Path) -> bool:
1409
+ """Restore selected skills from backup.
1410
+
1411
+ Args:
1412
+ skills: List of skill names to restore
1413
+ backup_path: Backup directory path
1414
+ project_path: Project path (absolute)
1415
+
1416
+ Returns:
1417
+ True if all restorations succeeded.
1418
+ """
1419
+ import shutil
1420
+
1421
+ if not skills:
1422
+ return True
1423
+
1424
+ console.print("\n[cyan]๐Ÿ“ฅ Restoring selected skills...[/cyan]")
1425
+ skills_dst = project_path / ".claude" / "skills"
1426
+ skills_dst.mkdir(parents=True, exist_ok=True)
1427
+
1428
+ success = True
1429
+ for skill_name in skills:
1430
+ src = backup_path / ".claude" / "skills" / skill_name
1431
+ dst = skills_dst / skill_name
1432
+
1433
+ if src.exists():
1434
+ try:
1435
+ shutil.copytree(src, dst, dirs_exist_ok=True)
1436
+ console.print(f" [green]โœ“ Restored: {skill_name}[/green]")
1437
+ except Exception as e:
1438
+ console.print(f" [red]โœ— Failed: {skill_name} - {e}[/red]")
1439
+ success = False
1440
+ else:
1441
+ console.print(f" [yellow]โš  Not in backup: {skill_name}[/yellow]")
1442
+ success = False
1443
+
1444
+ return success
1445
+
1446
+
1447
+ def _show_post_update_guidance(backup_path: Path) -> None:
1448
+ """Show post-update guidance for running /moai:0-project update.
1449
+
1450
+ Args:
1451
+ backup_path: Backup directory path for reference
1452
+ """
1453
+ console.print("\n" + "[cyan]" + "=" * 60 + "[/cyan]")
1454
+ console.print("[green]โœ… Update complete![/green]")
1455
+ console.print("\n[yellow]๐Ÿ“ IMPORTANT - Next step:[/yellow]")
1456
+ console.print(" Run [cyan]/moai:0-project update[/cyan] in Claude Code")
1457
+ console.print("\n This will:")
1458
+ console.print(" โ€ข Merge your settings with new templates")
1459
+ console.print(" โ€ข Validate configuration compatibility")
1460
+ console.print("\n[dim]๐Ÿ’ก Personal instructions should go in CLAUDE.local.md[/dim]")
1461
+ console.print(f"[dim]๐Ÿ“‚ Backup location: {backup_path}[/dim]")
1462
+ console.print("[cyan]" + "=" * 60 + "[/cyan]\n")
1463
+
1464
+
1465
+ def _sync_templates(project_path: Path, force: bool = False, yes: bool = False) -> bool:
544
1466
  """Sync templates to project with rollback mechanism.
545
1467
 
546
1468
  Args:
547
1469
  project_path: Project path (absolute)
548
1470
  force: Force update without backup
1471
+ yes: Auto-confirm flag (skips interactive prompts)
549
1472
 
550
1473
  Returns:
551
1474
  True if sync succeeded, False otherwise
@@ -554,6 +1477,20 @@ def _sync_templates(project_path: Path, force: bool = False) -> bool:
554
1477
 
555
1478
  backup_path = None
556
1479
  try:
1480
+ # NEW: Detect custom files and skills BEFORE backup/sync
1481
+ template_skills = _get_template_skill_names()
1482
+ _detect_custom_skills(project_path, template_skills)
1483
+
1484
+ # Detect custom commands, agents, and hooks
1485
+ template_commands = _get_template_command_names()
1486
+ _detect_custom_commands(project_path, template_commands)
1487
+
1488
+ template_agents = _get_template_agent_names()
1489
+ _detect_custom_agents(project_path, template_agents)
1490
+
1491
+ template_hooks = _get_template_hook_names()
1492
+ _detect_custom_hooks(project_path, template_hooks)
1493
+
557
1494
  processor = TemplateProcessor(project_path)
558
1495
 
559
1496
  # Create pre-sync backup for rollback
@@ -563,6 +1500,26 @@ def _sync_templates(project_path: Path, force: bool = False) -> bool:
563
1500
  backup_path = backup.create_backup()
564
1501
  console.print(f"๐Ÿ’พ Created backup: {backup_path.name}")
565
1502
 
1503
+ # Merge analysis using Claude Code headless mode
1504
+ try:
1505
+ analyzer = MergeAnalyzer(project_path)
1506
+ # Template source path from installed package
1507
+ template_path = Path(__file__).parent.parent.parent / "templates"
1508
+
1509
+ console.print("\n[cyan]๐Ÿ” Starting merge analysis (max 2 mins)...[/cyan]")
1510
+ console.print("[dim] Analyzing intelligent merge with Claude Code.[/dim]")
1511
+ console.print("[dim] Please wait...[/dim]\n")
1512
+ analysis = analyzer.analyze_merge(backup_path, template_path)
1513
+
1514
+ # Ask user confirmation
1515
+ if not analyzer.ask_user_confirmation(analysis):
1516
+ console.print("[yellow]โš ๏ธ User cancelled the update.[/yellow]")
1517
+ backup.restore_backup(backup_path)
1518
+ return False
1519
+ except Exception as e:
1520
+ console.print(f"[yellow]โš ๏ธ Merge analysis failed: {e}[/yellow]")
1521
+ console.print("[yellow]Proceeding with automatic merge.[/yellow]")
1522
+
566
1523
  # Load existing config
567
1524
  existing_config = _load_existing_config(project_path)
568
1525
 
@@ -571,18 +1528,34 @@ def _sync_templates(project_path: Path, force: bool = False) -> bool:
571
1528
  if context:
572
1529
  processor.set_context(context)
573
1530
 
574
- # Copy templates
1531
+ # Copy templates (including moai folder)
575
1532
  processor.copy_templates(backup=False, silent=True)
576
1533
 
1534
+ # Stage 1.5: Alfred โ†’ Moai migration (AFTER template sync)
1535
+ # Execute migration after template copy (moai folders must exist first)
1536
+ migrator = AlfredToMoaiMigrator(project_path)
1537
+ if migrator.needs_migration():
1538
+ console.print("\n[cyan]๐Ÿ”„ Migrating folder structure: Alfred โ†’ Moai[/cyan]")
1539
+ try:
1540
+ if not migrator.execute_migration(backup_path):
1541
+ console.print("[red]โŒ Alfred โ†’ Moai migration failed[/red]")
1542
+ if backup_path:
1543
+ console.print("[yellow]๐Ÿ”„ Restoring from backup...[/yellow]")
1544
+ backup = TemplateBackup(project_path)
1545
+ backup.restore_backup(backup_path)
1546
+ return False
1547
+ except Exception as e:
1548
+ console.print(f"[red]โŒ Error during migration: {e}[/red]")
1549
+ if backup_path:
1550
+ backup = TemplateBackup(project_path)
1551
+ backup.restore_backup(backup_path)
1552
+ return False
1553
+
577
1554
  # Validate template substitution
578
- validation_passed = _validate_template_substitution_with_rollback(
579
- project_path, backup_path
580
- )
1555
+ validation_passed = _validate_template_substitution_with_rollback(project_path, backup_path)
581
1556
  if not validation_passed:
582
1557
  if backup_path:
583
- console.print(
584
- f"[yellow]๐Ÿ”„ Rolling back to backup: {backup_path.name}[/yellow]"
585
- )
1558
+ console.print(f"[yellow]๐Ÿ”„ Rolling back to backup: {backup_path.name}[/yellow]")
586
1559
  backup.restore_backup(backup_path)
587
1560
  return False
588
1561
 
@@ -593,13 +1566,50 @@ def _sync_templates(project_path: Path, force: bool = False) -> bool:
593
1566
  # Set optimized=false
594
1567
  set_optimized_false(project_path)
595
1568
 
1569
+ # Update companyAnnouncements in settings.local.json
1570
+ try:
1571
+ import sys
1572
+
1573
+ utils_dir = (
1574
+ Path(__file__).parent.parent.parent / "templates" / ".claude" / "hooks" / "moai" / "shared" / "utils"
1575
+ )
1576
+
1577
+ if utils_dir.exists():
1578
+ sys.path.insert(0, str(utils_dir))
1579
+ try:
1580
+ from announcement_translator import auto_translate_and_update
1581
+
1582
+ console.print("[cyan]Updating announcements...[/cyan]")
1583
+ auto_translate_and_update(project_path)
1584
+ console.print("[green]โœ“ Announcements updated[/green]")
1585
+ except Exception as e:
1586
+ console.print(f"[yellow]โš ๏ธ Announcement update failed: {e}[/yellow]")
1587
+ finally:
1588
+ sys.path.remove(str(utils_dir))
1589
+
1590
+ except Exception as e:
1591
+ console.print(f"[yellow]โš ๏ธ Announcement module not available: {e}[/yellow]")
1592
+
1593
+ # NEW: Interactive custom element restore using new system
1594
+ _handle_custom_element_restoration(project_path, backup_path, yes)
1595
+
1596
+ # NEW: Migrate legacy logs to unified structure
1597
+ console.print("\n[cyan]๐Ÿ“ Migrating legacy log files...[/cyan]")
1598
+ if not _migrate_legacy_logs(project_path):
1599
+ console.print("[yellow]โš ๏ธ Legacy log migration failed, but update continuing[/yellow]")
1600
+
1601
+ # Clean up legacy presets directory
1602
+ _cleanup_legacy_presets(project_path)
1603
+
1604
+ # NEW: Show post-update guidance
1605
+ if backup_path:
1606
+ _show_post_update_guidance(backup_path)
1607
+
596
1608
  return True
597
1609
  except Exception as e:
598
1610
  console.print(f"[red]โœ— Template sync failed: {e}[/red]")
599
1611
  if backup_path:
600
- console.print(
601
- f"[yellow]๐Ÿ”„ Rolling back to backup: {backup_path.name}[/yellow]"
602
- )
1612
+ console.print(f"[yellow]๐Ÿ”„ Rolling back to backup: {backup_path.name}[/yellow]")
603
1613
  try:
604
1614
  backup = TemplateBackup(project_path)
605
1615
  backup.restore_backup(backup_path)
@@ -626,47 +1636,38 @@ def get_latest_version() -> str | None:
626
1636
 
627
1637
 
628
1638
  def set_optimized_false(project_path: Path) -> None:
629
- """Set config.json's optimized field to false.
1639
+ """Set config's optimized field to false.
630
1640
 
631
1641
  Args:
632
1642
  project_path: Project path (absolute).
633
1643
  """
634
- config_path = project_path / ".moai" / "config" / "config.json"
1644
+ config_path, _ = _get_config_path(project_path)
635
1645
  if not config_path.exists():
636
1646
  return
637
1647
 
638
1648
  try:
639
- config_data = json.loads(config_path.read_text(encoding="utf-8"))
1649
+ config_data = _load_config(config_path)
640
1650
  config_data.setdefault("project", {})["optimized"] = False
641
- config_path.write_text(
642
- json.dumps(config_data, indent=2, ensure_ascii=False) + "\n",
643
- encoding="utf-8",
644
- )
645
- except (json.JSONDecodeError, KeyError):
646
- # Ignore errors if config.json is invalid
1651
+ _save_config(config_path, config_data)
1652
+ except (json.JSONDecodeError, yaml.YAMLError, KeyError):
1653
+ # Ignore errors if config is invalid
647
1654
  pass
648
1655
 
649
1656
 
650
1657
  def _load_existing_config(project_path: Path) -> dict[str, Any]:
651
- """Load existing config.json if available."""
652
- config_path = project_path / ".moai" / "config" / "config.json"
1658
+ """Load existing config (YAML or JSON) if available."""
1659
+ config_path, _ = _get_config_path(project_path)
653
1660
  if config_path.exists():
654
1661
  try:
655
- return json.loads(config_path.read_text(encoding="utf-8"))
656
- except json.JSONDecodeError:
657
- console.print(
658
- "[yellow]โš  Existing config.json could not be parsed. Proceeding with defaults.[/yellow]"
659
- )
1662
+ return _load_config(config_path)
1663
+ except (json.JSONDecodeError, yaml.YAMLError):
1664
+ console.print("[yellow]โš  Existing config could not be parsed. Proceeding with defaults.[/yellow]")
660
1665
  return {}
661
1666
 
662
1667
 
663
1668
  def _is_placeholder(value: Any) -> bool:
664
1669
  """Check if a string value is an unsubstituted template placeholder."""
665
- return (
666
- isinstance(value, str)
667
- and value.strip().startswith("{{")
668
- and value.strip().endswith("}}")
669
- )
1670
+ return isinstance(value, str) and value.strip().startswith("{{") and value.strip().endswith("}}")
670
1671
 
671
1672
 
672
1673
  def _coalesce(*values: Any, default: str = "") -> str:
@@ -730,19 +1731,82 @@ def _build_template_context(
730
1731
  )
731
1732
 
732
1733
  # Detect OS for cross-platform Hook path configuration
733
- hook_project_dir = (
734
- "%CLAUDE_PROJECT_DIR%"
735
- if platform.system() == "Windows"
736
- else "$CLAUDE_PROJECT_DIR"
737
- )
1734
+ hook_project_dir = "%CLAUDE_PROJECT_DIR%" if platform.system() == "Windows" else "$CLAUDE_PROJECT_DIR"
738
1735
 
739
- # Extract language configuration
740
- language_config = existing_config.get("language", {})
741
- if not isinstance(language_config, dict):
742
- language_config = {}
1736
+ # Extract and resolve language configuration using centralized resolver
1737
+ try:
1738
+ from moai_adk.core.language_config_resolver import get_resolver
1739
+
1740
+ # Use language resolver to get complete configuration
1741
+ resolver = get_resolver(str(project_path))
1742
+ resolved_config = resolver.resolve_config()
1743
+
1744
+ # Extract language configuration with environment variable priority
1745
+ language_config = {
1746
+ "conversation_language": resolved_config.get("conversation_language", "en"),
1747
+ "conversation_language_name": resolved_config.get("conversation_language_name", "English"),
1748
+ "agent_prompt_language": resolved_config.get("agent_prompt_language", "en"),
1749
+ }
1750
+
1751
+ # Extract user personalization
1752
+ user_name = resolved_config.get("user_name", "")
1753
+ personalized_greeting = resolver.get_personalized_greeting(resolved_config)
1754
+ config_source = resolved_config.get("config_source", "config_file")
1755
+
1756
+ except ImportError:
1757
+ # Fallback to basic language config extraction if resolver not available
1758
+ language_config = existing_config.get("language", {})
1759
+ if not isinstance(language_config, dict):
1760
+ language_config = {}
1761
+
1762
+ user_name = existing_config.get("user", {}).get("name", "")
1763
+ conv_lang = language_config.get("conversation_language")
1764
+ personalized_greeting = f"{user_name}๋‹˜" if user_name and conv_lang == "ko" else user_name
1765
+ config_source = "config_file"
1766
+
1767
+ # Enhanced version formatting (matches TemplateProcessor.get_enhanced_version_context)
1768
+ def format_short_version(v: str) -> str:
1769
+ """Remove 'v' prefix if present."""
1770
+ return v[1:] if v.startswith("v") else v
1771
+
1772
+ def format_display_version(v: str) -> str:
1773
+ """Format display version with proper formatting."""
1774
+ if v == "unknown":
1775
+ return "MoAI-ADK unknown version"
1776
+ elif v.startswith("v"):
1777
+ return f"MoAI-ADK {v}"
1778
+ else:
1779
+ return f"MoAI-ADK v{v}"
1780
+
1781
+ def format_trimmed_version(v: str, max_length: int = 10) -> str:
1782
+ """Format version with maximum length for UI displays."""
1783
+ if v == "unknown":
1784
+ return "unknown"
1785
+ clean_version = v[1:] if v.startswith("v") else v
1786
+ if len(clean_version) > max_length:
1787
+ return clean_version[:max_length]
1788
+ return clean_version
1789
+
1790
+ def format_semver_version(v: str) -> str:
1791
+ """Format version as semantic version."""
1792
+ if v == "unknown":
1793
+ return "0.0.0"
1794
+ clean_version = v[1:] if v.startswith("v") else v
1795
+ import re
1796
+
1797
+ semver_match = re.match(r"^(\d+\.\d+\.\d+)", clean_version)
1798
+ if semver_match:
1799
+ return semver_match.group(1)
1800
+ return "0.0.0"
743
1801
 
744
1802
  return {
745
1803
  "MOAI_VERSION": version_for_config,
1804
+ "MOAI_VERSION_SHORT": format_short_version(version_for_config),
1805
+ "MOAI_VERSION_DISPLAY": format_display_version(version_for_config),
1806
+ "MOAI_VERSION_TRIMMED": format_trimmed_version(version_for_config),
1807
+ "MOAI_VERSION_SEMVER": format_semver_version(version_for_config),
1808
+ "MOAI_VERSION_VALID": "true" if version_for_config != "unknown" else "false",
1809
+ "MOAI_VERSION_SOURCE": "config_cached",
746
1810
  "PROJECT_NAME": project_name,
747
1811
  "PROJECT_MODE": project_mode,
748
1812
  "PROJECT_DESCRIPTION": project_description,
@@ -750,9 +1814,19 @@ def _build_template_context(
750
1814
  "CREATION_TIMESTAMP": created_at,
751
1815
  "PROJECT_DIR": hook_project_dir,
752
1816
  "CONVERSATION_LANGUAGE": language_config.get("conversation_language", "en"),
753
- "CONVERSATION_LANGUAGE_NAME": language_config.get(
754
- "conversation_language_name", "English"
1817
+ "CONVERSATION_LANGUAGE_NAME": language_config.get("conversation_language_name", "English"),
1818
+ "AGENT_PROMPT_LANGUAGE": language_config.get("agent_prompt_language", "en"),
1819
+ "GIT_COMMIT_MESSAGES_LANGUAGE": language_config.get("git_commit_messages", "en"),
1820
+ "CODE_COMMENTS_LANGUAGE": language_config.get("code_comments", "en"),
1821
+ "DOCUMENTATION_LANGUAGE": language_config.get(
1822
+ "documentation", language_config.get("conversation_language", "en")
1823
+ ),
1824
+ "ERROR_MESSAGES_LANGUAGE": language_config.get(
1825
+ "error_messages", language_config.get("conversation_language", "en")
755
1826
  ),
1827
+ "USER_NAME": user_name,
1828
+ "PERSONALIZED_GREETING": personalized_greeting,
1829
+ "LANGUAGE_CONFIG_SOURCE": config_source,
756
1830
  "CODEBASE_LANGUAGE": project_section.get("language", "generic"),
757
1831
  "PROJECT_OWNER": project_section.get("author", "@user"),
758
1832
  "AUTHOR": project_section.get("author", "@user"),
@@ -765,18 +1839,18 @@ def _preserve_project_metadata(
765
1839
  existing_config: dict[str, Any],
766
1840
  version_for_config: str,
767
1841
  ) -> None:
768
- """Restore project-specific metadata in the new config.json.
1842
+ """Restore project-specific metadata in the new config (YAML or JSON).
769
1843
 
770
1844
  Also updates template_version to track which template version is synchronized.
771
1845
  """
772
- config_path = project_path / ".moai" / "config" / "config.json"
1846
+ config_path, _ = _get_config_path(project_path)
773
1847
  if not config_path.exists():
774
1848
  return
775
1849
 
776
1850
  try:
777
- config_data = json.loads(config_path.read_text(encoding="utf-8"))
778
- except json.JSONDecodeError:
779
- console.print("[red]โœ— Failed to parse config.json after template copy[/red]")
1851
+ config_data = _load_config(config_path)
1852
+ except (json.JSONDecodeError, yaml.YAMLError):
1853
+ console.print("[red]โœ— Failed to parse config after template copy[/red]")
780
1854
  return
781
1855
 
782
1856
  project_data = config_data.setdefault("project", {})
@@ -796,9 +1870,7 @@ def _preserve_project_metadata(
796
1870
  if locale:
797
1871
  project_data["locale"] = locale
798
1872
 
799
- language = _coalesce(
800
- existing_project.get("language"), existing_config.get("language")
801
- )
1873
+ language = _coalesce(existing_project.get("language"), existing_config.get("language"))
802
1874
  if language:
803
1875
  project_data["language"] = language
804
1876
 
@@ -808,9 +1880,7 @@ def _preserve_project_metadata(
808
1880
  # This allows Stage 2 to compare package vs project template versions
809
1881
  project_data["template_version"] = version_for_config
810
1882
 
811
- config_path.write_text(
812
- json.dumps(config_data, indent=2, ensure_ascii=False) + "\n", encoding="utf-8"
813
- )
1883
+ _save_config(config_path, config_data)
814
1884
 
815
1885
 
816
1886
  def _apply_context_to_file(processor: TemplateProcessor, target_path: Path) -> None:
@@ -823,9 +1893,7 @@ def _apply_context_to_file(processor: TemplateProcessor, target_path: Path) -> N
823
1893
  except UnicodeDecodeError:
824
1894
  return
825
1895
 
826
- substituted, warnings = processor._substitute_variables(
827
- content
828
- ) # pylint: disable=protected-access
1896
+ substituted, warnings = processor._substitute_variables(content) # pylint: disable=protected-access
829
1897
  if warnings:
830
1898
  console.print("[yellow]โš  Template warnings:[/yellow]")
831
1899
  for warning in warnings:
@@ -856,28 +1924,20 @@ def _validate_template_substitution(project_path: Path) -> None:
856
1924
  unsubstituted = re.findall(r"\{\{([A-Z_]+)\}\}", content)
857
1925
  if unsubstituted:
858
1926
  unique_vars = sorted(set(unsubstituted))
859
- issues_found.append(
860
- f"{file_path.relative_to(project_path)}: {', '.join(unique_vars)}"
861
- )
1927
+ issues_found.append(f"{file_path.relative_to(project_path)}: {', '.join(unique_vars)}")
862
1928
  except Exception as e:
863
- console.print(
864
- f"[yellow]โš ๏ธ Could not validate {file_path.relative_to(project_path)}: {e}[/yellow]"
865
- )
1929
+ console.print(f"[yellow]โš ๏ธ Could not validate {file_path.relative_to(project_path)}: {e}[/yellow]")
866
1930
 
867
1931
  if issues_found:
868
1932
  console.print("[red]โœ— Template substitution validation failed:[/red]")
869
1933
  for issue in issues_found:
870
1934
  console.print(f" {issue}")
871
- console.print(
872
- "[yellow]๐Ÿ’ก Run '/alfred:0-project' to fix template variables[/yellow]"
873
- )
1935
+ console.print("[yellow]๐Ÿ’ก Run '/moai:0-project' to fix template variables[/yellow]")
874
1936
  else:
875
1937
  console.print("[green]โœ… Template substitution validation passed[/green]")
876
1938
 
877
1939
 
878
- def _validate_template_substitution_with_rollback(
879
- project_path: Path, backup_path: Path | None
880
- ) -> bool:
1940
+ def _validate_template_substitution_with_rollback(project_path: Path, backup_path: Path | None) -> bool:
881
1941
  """Validate template substitution with rollback capability.
882
1942
 
883
1943
  Returns:
@@ -903,13 +1963,9 @@ def _validate_template_substitution_with_rollback(
903
1963
  unsubstituted = re.findall(r"\{\{([A-Z_]+)\}\}", content)
904
1964
  if unsubstituted:
905
1965
  unique_vars = sorted(set(unsubstituted))
906
- issues_found.append(
907
- f"{file_path.relative_to(project_path)}: {', '.join(unique_vars)}"
908
- )
1966
+ issues_found.append(f"{file_path.relative_to(project_path)}: {', '.join(unique_vars)}")
909
1967
  except Exception as e:
910
- console.print(
911
- f"[yellow]โš ๏ธ Could not validate {file_path.relative_to(project_path)}: {e}[/yellow]"
912
- )
1968
+ console.print(f"[yellow]โš ๏ธ Could not validate {file_path.relative_to(project_path)}: {e}[/yellow]")
913
1969
 
914
1970
  if issues_found:
915
1971
  console.print("[red]โœ— Template substitution validation failed:[/red]")
@@ -917,13 +1973,9 @@ def _validate_template_substitution_with_rollback(
917
1973
  console.print(f" {issue}")
918
1974
 
919
1975
  if backup_path:
920
- console.print(
921
- "[yellow]๐Ÿ”„ Rolling back due to validation failure...[/yellow]"
922
- )
1976
+ console.print("[yellow]๐Ÿ”„ Rolling back due to validation failure...[/yellow]")
923
1977
  else:
924
- console.print(
925
- "[yellow]๐Ÿ’ก Run '/alfred:0-project' to fix template variables[/yellow]"
926
- )
1978
+ console.print("[yellow]๐Ÿ’ก Run '/moai:0-project' to fix template variables[/yellow]")
927
1979
  console.print("[red]โš ๏ธ No backup available - manual fix required[/red]")
928
1980
 
929
1981
  return False
@@ -978,18 +2030,14 @@ def _show_network_error_help() -> None:
978
2030
  console.print("Options:")
979
2031
  console.print(" 1. Check network connection")
980
2032
  console.print(" 2. Try again with: [cyan]moai-adk update --force[/cyan]")
981
- console.print(
982
- " 3. Skip version check: [cyan]moai-adk update --templates-only[/cyan]"
983
- )
2033
+ console.print(" 3. Skip version check: [cyan]moai-adk update --templates-only[/cyan]")
984
2034
 
985
2035
 
986
2036
  def _show_template_sync_failure_help() -> None:
987
2037
  """Show help when template sync fails."""
988
2038
  console.print("[yellow]โš ๏ธ Template sync failed[/yellow]\n")
989
2039
  console.print("Rollback options:")
990
- console.print(
991
- " 1. Restore from backup: [cyan]cp -r .moai-backups/TIMESTAMP .moai/[/cyan]"
992
- )
2040
+ console.print(" 1. Restore from backup: [cyan]cp -r .moai-backups/TIMESTAMP .moai/[/cyan]")
993
2041
  console.print(" 2. Skip backup and retry: [cyan]moai-adk update --force[/cyan]")
994
2042
  console.print(" 3. Report issue: https://github.com/modu-ai/moai-adk/issues")
995
2043
 
@@ -1027,24 +2075,16 @@ def _execute_migration_if_needed(project_path: Path, yes: bool = False) -> bool:
1027
2075
  console.print()
1028
2076
  console.print(" This will migrate configuration files to new locations:")
1029
2077
  console.print(" โ€ข .moai/config.json โ†’ .moai/config/config.json")
1030
- console.print(
1031
- " โ€ข .claude/statusline-config.yaml โ†’ .moai/config/statusline-config.yaml"
1032
- )
2078
+ console.print(" โ€ข .claude/statusline-config.yaml โ†’ .moai/config/statusline-config.yaml")
1033
2079
  console.print()
1034
2080
  console.print(" A backup will be created automatically.")
1035
2081
  console.print()
1036
2082
 
1037
2083
  # Confirm with user (unless --yes)
1038
2084
  if not yes:
1039
- if not click.confirm(
1040
- "Do you want to proceed with migration?", default=True
1041
- ):
1042
- console.print(
1043
- "[yellow]โš ๏ธ Migration skipped. Some features may not work correctly.[/yellow]"
1044
- )
1045
- console.print(
1046
- "[cyan]๐Ÿ’ก Run 'moai-adk migrate' manually when ready[/cyan]"
1047
- )
2085
+ if not click.confirm("Do you want to proceed with migration?", default=True):
2086
+ console.print("[yellow]โš ๏ธ Migration skipped. Some features may not work correctly.[/yellow]")
2087
+ console.print("[cyan]๐Ÿ’ก Run 'moai-adk migrate' manually when ready[/cyan]")
1048
2088
  return False
1049
2089
 
1050
2090
  # Execute migration
@@ -1056,9 +2096,7 @@ def _execute_migration_if_needed(project_path: Path, yes: bool = False) -> bool:
1056
2096
  return True
1057
2097
  else:
1058
2098
  console.print("[red]โŒ Migration failed[/red]")
1059
- console.print(
1060
- "[cyan]๐Ÿ’ก Use 'moai-adk migrate --rollback' to restore from backup[/cyan]"
1061
- )
2099
+ console.print("[cyan]๐Ÿ’ก Use 'moai-adk migrate --rollback' to restore from backup[/cyan]")
1062
2100
  return False
1063
2101
 
1064
2102
  except Exception as e:
@@ -1076,14 +2114,29 @@ def _execute_migration_if_needed(project_path: Path, yes: bool = False) -> bool:
1076
2114
  )
1077
2115
  @click.option("--force", is_flag=True, help="Skip backup and force the update")
1078
2116
  @click.option("--check", is_flag=True, help="Only check version (do not update)")
2117
+ @click.option("--templates-only", is_flag=True, help="Skip package upgrade, sync templates only")
2118
+ @click.option("--yes", is_flag=True, help="Auto-confirm all prompts (CI/CD mode)")
1079
2119
  @click.option(
1080
- "--templates-only", is_flag=True, help="Skip package upgrade, sync templates only"
2120
+ "--merge",
2121
+ "merge_strategy",
2122
+ flag_value="auto",
2123
+ help="Auto-merge: Apply template + preserve user changes",
2124
+ )
2125
+ @click.option(
2126
+ "--manual",
2127
+ "merge_strategy",
2128
+ flag_value="manual",
2129
+ help="Manual merge: Preserve backup, generate merge guide",
1081
2130
  )
1082
- @click.option("--yes", is_flag=True, help="Auto-confirm all prompts (CI/CD mode)")
1083
2131
  def update(
1084
- path: str, force: bool, check: bool, templates_only: bool, yes: bool
2132
+ path: str,
2133
+ force: bool,
2134
+ check: bool,
2135
+ templates_only: bool,
2136
+ yes: bool,
2137
+ merge_strategy: str | None,
1085
2138
  ) -> None:
1086
- """Update command with 3-stage workflow (v0.6.3+).
2139
+ """Update command with 3-stage workflow + merge strategy selection (v0.26.0+).
1087
2140
 
1088
2141
  Stage 1 (Package Version Check):
1089
2142
  - Fetches current and latest versions from PyPI
@@ -1095,18 +2148,34 @@ def update(
1095
2148
  - If versions match: skips Stage 3 (already up-to-date)
1096
2149
  - Performance improvement: 70-80% faster for unchanged projects (3-4s vs 12-18s)
1097
2150
 
1098
- Stage 3 (Template Sync):
2151
+ Stage 3 (Template Sync with Merge Strategy - NEW in v0.26.0):
1099
2152
  - Syncs templates only if versions differ
2153
+ - User chooses merge strategy:
2154
+ * Auto-merge (default): Template + preserved user changes
2155
+ * Manual merge: Backup + comprehensive merge guide (full control)
1100
2156
  - Updates .claude/, .moai/, CLAUDE.md, config.json
1101
2157
  - Preserves specs and reports
1102
2158
  - Saves new template_version to config.json
1103
2159
 
1104
2160
  Examples:
1105
- python -m moai_adk update # auto 3-stage workflow
1106
- python -m moai_adk update --force # force template sync
2161
+ python -m moai_adk update # interactive merge strategy selection
2162
+ python -m moai_adk update --merge # auto-merge (template + user changes)
2163
+ python -m moai_adk update --manual # manual merge (backup + guide)
2164
+ python -m moai_adk update --force # force template sync (no backup)
1107
2165
  python -m moai_adk update --check # check version only
1108
2166
  python -m moai_adk update --templates-only # skip package upgrade
1109
- python -m moai_adk update --yes # CI/CD mode (auto-confirm)
2167
+ python -m moai_adk update --yes # CI/CD mode (auto-confirm + auto-merge)
2168
+
2169
+ Merge Strategies:
2170
+ --merge: Auto-merge applies template + preserves your changes (default)
2171
+ Generated files: backup, merge report
2172
+ --manual: Manual merge preserves backup + generates comprehensive guide
2173
+ Generated files: backup, merge guide
2174
+
2175
+ Generated Files:
2176
+ - Backup: .moai-backups/pre-update-backup_{timestamp}/
2177
+ - Report: .moai/reports/merge-report.md (auto-merge only)
2178
+ - Guide: .moai/guides/merge-guide.md (manual merge only)
1110
2179
  """
1111
2180
  try:
1112
2181
  project_path = Path(path).resolve()
@@ -1120,14 +2189,24 @@ def update(
1120
2189
  # Note: If --check is used, always fetch versions even if --templates-only is also present
1121
2190
  if check or not templates_only:
1122
2191
  try:
1123
- current = _get_current_version()
1124
- latest = _get_latest_version()
2192
+ # Try to use new spinner UI
2193
+ try:
2194
+ from moai_adk.cli.ui.progress import SpinnerContext
2195
+
2196
+ with SpinnerContext("Checking for updates...") as spinner:
2197
+ current = _get_current_version()
2198
+ spinner.update("Fetching latest version from PyPI...")
2199
+ latest = _get_latest_version()
2200
+ spinner.success("Version check complete")
2201
+ except ImportError:
2202
+ # Fallback to simple console output
2203
+ console.print("[dim]Checking for updates...[/dim]")
2204
+ current = _get_current_version()
2205
+ latest = _get_latest_version()
1125
2206
  except RuntimeError as e:
1126
2207
  console.print(f"[red]Error: {e}[/red]")
1127
2208
  if not force:
1128
- console.print(
1129
- "[yellow]โš  Cannot check for updates. Use --force to update anyway.[/yellow]"
1130
- )
2209
+ console.print("[yellow]โš  Cannot check for updates. Use --force to update anyway.[/yellow]")
1131
2210
  raise click.Abort()
1132
2211
  # With --force, proceed to Stage 2 even if version check fails
1133
2212
  current = __version__
@@ -1139,23 +2218,24 @@ def update(
1139
2218
  if check:
1140
2219
  comparison = _compare_versions(current, latest)
1141
2220
  if comparison < 0:
1142
- console.print(
1143
- f"\n[yellow]๐Ÿ“ฆ Update available: {current} โ†’ {latest}[/yellow]"
1144
- )
2221
+ console.print(f"\n[yellow]๐Ÿ“ฆ Update available: {current} โ†’ {latest}[/yellow]")
1145
2222
  console.print(" Run 'moai-adk update' to upgrade")
1146
2223
  elif comparison == 0:
1147
2224
  console.print(f"[green]โœ“ Already up to date ({current})[/green]")
1148
2225
  else:
1149
- console.print(
1150
- f"[cyan]โ„น๏ธ Dev version: {current} (latest: {latest})[/cyan]"
1151
- )
2226
+ console.print(f"[cyan]โ„น๏ธ Dev version: {current} (latest: {latest})[/cyan]")
1152
2227
  return
1153
2228
 
1154
2229
  # Step 2: Handle --templates-only (skip upgrade, go straight to sync)
1155
2230
  if templates_only:
1156
2231
  console.print("[cyan]๐Ÿ“„ Syncing templates only...[/cyan]")
2232
+
2233
+ # Preserve user-specific settings before sync
2234
+ console.print(" [cyan]๐Ÿ’พ Preserving user settings...[/cyan]")
2235
+ preserved_settings = _preserve_user_settings(project_path)
2236
+
1157
2237
  try:
1158
- if not _sync_templates(project_path, force):
2238
+ if not _sync_templates(project_path, force, yes):
1159
2239
  raise TemplateSyncError("Template sync returned False")
1160
2240
  except TemplateSyncError:
1161
2241
  console.print("[red]Error: Template sync failed[/red]")
@@ -1166,10 +2246,11 @@ def update(
1166
2246
  _show_template_sync_failure_help()
1167
2247
  raise click.Abort()
1168
2248
 
2249
+ # Restore user-specific settings after sync
2250
+ _restore_user_settings(project_path, preserved_settings)
2251
+
1169
2252
  console.print(" [green]โœ… .claude/ update complete[/green]")
1170
- console.print(
1171
- " [green]โœ… .moai/ update complete (specs/reports preserved)[/green]"
1172
- )
2253
+ console.print(" [green]โœ… .moai/ update complete (specs/reports preserved)[/green]")
1173
2254
  console.print(" [green]๐Ÿ”„ CLAUDE.md merge complete[/green]")
1174
2255
  console.print(" [green]๐Ÿ”„ config.json merge complete[/green]")
1175
2256
  console.print("\n[green]โœ“ Template sync complete![/green]")
@@ -1204,9 +2285,7 @@ def update(
1204
2285
  try:
1205
2286
  upgrade_result = _execute_upgrade(installer_cmd)
1206
2287
  if not upgrade_result:
1207
- raise UpgradeError(
1208
- f"Upgrade command failed: {' '.join(installer_cmd)}"
1209
- )
2288
+ raise UpgradeError(f"Upgrade command failed: {' '.join(installer_cmd)}")
1210
2289
  except subprocess.TimeoutExpired:
1211
2290
  _show_timeout_error_help()
1212
2291
  raise click.Abort()
@@ -1216,9 +2295,7 @@ def update(
1216
2295
 
1217
2296
  # Prompt re-run
1218
2297
  console.print("\n[green]โœ“ Upgrade complete![/green]")
1219
- console.print(
1220
- "[cyan]๐Ÿ“ข Run 'moai-adk update' again to sync templates[/cyan]"
1221
- )
2298
+ console.print("[cyan]๐Ÿ“ข Run 'moai-adk update' again to sync templates[/cyan]")
1222
2299
  return
1223
2300
 
1224
2301
  # Stage 1.5: Migration Check (NEW in v0.24.0)
@@ -1227,9 +2304,12 @@ def update(
1227
2304
  # Execute migration if needed
1228
2305
  if not _execute_migration_if_needed(project_path, yes):
1229
2306
  console.print("[yellow]โš ๏ธ Update continuing without migration[/yellow]")
1230
- console.print(
1231
- "[cyan]๐Ÿ’ก Some features may require migration to work correctly[/cyan]"
1232
- )
2307
+ console.print("[cyan]๐Ÿ’ก Some features may require migration to work correctly[/cyan]")
2308
+
2309
+ # Migrate config.json โ†’ config.yaml (v0.32.0+)
2310
+ console.print("\n[cyan]๐Ÿ” Checking for config format migration...[/cyan]")
2311
+ if not _migrate_config_json_to_yaml(project_path):
2312
+ console.print("[yellow]โš ๏ธ Config migration failed, continuing with existing format[/yellow]")
1233
2313
 
1234
2314
  # Stage 2: Config Version Comparison
1235
2315
  try:
@@ -1246,32 +2326,66 @@ def update(
1246
2326
  console.print(f" Project config: {project_config_version}")
1247
2327
 
1248
2328
  try:
1249
- config_comparison = _compare_versions(
1250
- package_config_version, project_config_version
1251
- )
2329
+ config_comparison = _compare_versions(package_config_version, project_config_version)
1252
2330
  except version.InvalidVersion as e:
1253
2331
  # Handle invalid version strings (e.g., unsubstituted template placeholders, corrupted configs)
1254
2332
  console.print(f"[yellow]โš  Invalid version format in config: {e}[/yellow]")
1255
- console.print(
1256
- "[cyan]โ„น๏ธ Forcing template sync to repair configuration...[/cyan]"
1257
- )
2333
+ console.print("[cyan]โ„น๏ธ Forcing template sync to repair configuration...[/cyan]")
1258
2334
  # Force template sync by treating project version as outdated
1259
2335
  config_comparison = 1 # package_config_version > project_config_version
1260
2336
 
1261
2337
  # If versions are equal, no sync needed
1262
2338
  if config_comparison <= 0:
1263
- console.print(
1264
- f"\n[green]โœ“ Project already has latest template version ({project_config_version})[/green]"
1265
- )
1266
- console.print(
1267
- "[cyan]โ„น๏ธ Templates are up to date! No changes needed.[/cyan]"
1268
- )
2339
+ console.print(f"\n[green]โœ“ Project already has latest template version ({project_config_version})[/green]")
2340
+ console.print("[cyan]โ„น๏ธ Templates are up to date! No changes needed.[/cyan]")
1269
2341
  return
1270
2342
 
1271
2343
  # Stage 3: Template Sync (Only if package_config_version > project_config_version)
1272
- console.print(
1273
- f"\n[cyan]๐Ÿ“„ Syncing templates ({project_config_version} โ†’ {package_config_version})...[/cyan]"
1274
- )
2344
+ console.print(f"\n[cyan]๐Ÿ“„ Syncing templates ({project_config_version} โ†’ {package_config_version})...[/cyan]")
2345
+
2346
+ # Determine merge strategy (default: auto-merge)
2347
+ final_merge_strategy = merge_strategy or "auto"
2348
+
2349
+ # Handle merge strategy
2350
+ if final_merge_strategy == "manual":
2351
+ # Manual merge mode: Create full backup + generate guide, no template sync
2352
+ console.print("\n[cyan]๐Ÿ”€ Manual merge mode selected[/cyan]")
2353
+
2354
+ # Create full project backup
2355
+ console.print(" [cyan]๐Ÿ’พ Creating full project backup...[/cyan]")
2356
+ try:
2357
+ from moai_adk.core.migration.backup_manager import BackupManager
2358
+
2359
+ backup_manager = BackupManager(project_path)
2360
+ full_backup_path = backup_manager.create_full_project_backup(description="pre-update-backup")
2361
+ console.print(f" [green]โœ“ Backup: {full_backup_path.relative_to(project_path)}/[/green]")
2362
+
2363
+ # Generate merge guide
2364
+ console.print(" [cyan]๐Ÿ“‹ Generating merge guide...[/cyan]")
2365
+ template_path = Path(__file__).parent.parent.parent / "templates"
2366
+ guide_path = _generate_manual_merge_guide(full_backup_path, template_path, project_path)
2367
+ console.print(f" [green]โœ“ Guide: {guide_path.relative_to(project_path)}[/green]")
2368
+
2369
+ # Summary
2370
+ console.print("\n[green]โœ“ Manual merge setup complete![/green]")
2371
+ console.print(f"[cyan]๐Ÿ“ Backup location: {full_backup_path.relative_to(project_path)}/[/cyan]")
2372
+ console.print(f"[cyan]๐Ÿ“‹ Merge guide: {guide_path.relative_to(project_path)}[/cyan]")
2373
+ console.print("\n[yellow]โš ๏ธ Next steps:[/yellow]")
2374
+ console.print("[yellow] 1. Review the merge guide[/yellow]")
2375
+ console.print("[yellow] 2. Compare files using diff or visual tools[/yellow]")
2376
+ console.print("[yellow] 3. Manually merge your customizations[/yellow]")
2377
+ console.print("[yellow] 4. Test and commit changes[/yellow]")
2378
+
2379
+ except Exception as e:
2380
+ console.print(f"[red]Error: Manual merge setup failed - {e}[/red]")
2381
+ raise click.Abort()
2382
+
2383
+ return
2384
+
2385
+ # Auto merge mode: Preserve user-specific settings before sync
2386
+ console.print("\n[cyan]๐Ÿ”€ Auto-merge mode selected[/cyan]")
2387
+ console.print(" [cyan]๐Ÿ’พ Preserving user settings...[/cyan]")
2388
+ preserved_settings = _preserve_user_settings(project_path)
1275
2389
 
1276
2390
  # Create backup unless --force
1277
2391
  if not force:
@@ -1279,19 +2393,21 @@ def update(
1279
2393
  try:
1280
2394
  processor = TemplateProcessor(project_path)
1281
2395
  backup_path = processor.create_backup()
1282
- console.print(
1283
- f" [green]โœ“ Backup: {backup_path.relative_to(project_path)}/[/green]"
1284
- )
2396
+ console.print(f" [green]โœ“ Backup: {backup_path.relative_to(project_path)}/[/green]")
1285
2397
  except Exception as e:
1286
2398
  console.print(f" [yellow]โš  Backup failed: {e}[/yellow]")
1287
2399
  console.print(" [yellow]โš  Continuing without backup...[/yellow]")
1288
2400
  else:
1289
2401
  console.print(" [yellow]โš  Skipping backup (--force)[/yellow]")
1290
2402
 
1291
- # Sync templates
2403
+ # Sync templates (NO spinner - user interaction may be required)
2404
+ # SpinnerContext blocks stdin, causing hang when click.confirm() is called
1292
2405
  try:
1293
- if not _sync_templates(project_path, force):
2406
+ console.print(" [cyan]Syncing templates...[/cyan]")
2407
+ if not _sync_templates(project_path, force, yes):
1294
2408
  raise TemplateSyncError("Template sync returned False")
2409
+ _restore_user_settings(project_path, preserved_settings)
2410
+ console.print(" [green]โœ“ Template sync complete[/green]")
1295
2411
  except TemplateSyncError:
1296
2412
  console.print("[red]Error: Template sync failed[/red]")
1297
2413
  _show_template_sync_failure_help()
@@ -1302,20 +2418,269 @@ def update(
1302
2418
  raise click.Abort()
1303
2419
 
1304
2420
  console.print(" [green]โœ… .claude/ update complete[/green]")
1305
- console.print(
1306
- " [green]โœ… .moai/ update complete (specs/reports preserved)[/green]"
1307
- )
2421
+ console.print(" [green]โœ… .moai/ update complete (specs/reports preserved)[/green]")
1308
2422
  console.print(" [green]๐Ÿ”„ CLAUDE.md merge complete[/green]")
1309
2423
  console.print(" [green]๐Ÿ”„ config.json merge complete[/green]")
1310
- console.print(
1311
- " [yellow]โš™๏ธ Set optimized=false (optimization needed)[/yellow]"
1312
- )
2424
+ console.print(" [yellow]โš™๏ธ Set optimized=false (optimization needed)[/yellow]")
1313
2425
 
1314
2426
  console.print("\n[green]โœ“ Update complete![/green]")
1315
- console.print(
1316
- "[cyan]โ„น๏ธ Next step: Run /alfred:0-project update to optimize template changes[/cyan]"
1317
- )
2427
+ console.print("[cyan]โ„น๏ธ Next step: Run /moai:0-project update to optimize template changes[/cyan]")
1318
2428
 
1319
2429
  except Exception as e:
1320
2430
  console.print(f"[red]โœ— Update failed: {e}[/red]")
1321
2431
  raise click.ClickException(str(e)) from e
2432
+
2433
+
2434
+ def _handle_custom_element_restoration(project_path: Path, backup_path: Path | None, yes: bool = False) -> None:
2435
+ """Handle custom element restoration using the enhanced system.
2436
+
2437
+ This function provides an improved interface for restoring user-created custom elements
2438
+ (agents, commands, skills, hooks) from backup during MoAI-ADK updates.
2439
+
2440
+ Key improvements:
2441
+ - Preserves unselected elements (fixes disappearing issue)
2442
+ - Only overwrites/creates selected elements from backup
2443
+ - Interactive checkbox selection with arrow key navigation
2444
+ - Includes all categories (Agents, Commands, Skills, Hooks)
2445
+
2446
+ Args:
2447
+ project_path: Path to the MoAI-ADK project directory
2448
+ backup_path: Path to the backup directory (None if no backup)
2449
+ yes: Whether to automatically accept defaults (non-interactive mode)
2450
+ """
2451
+ if not backup_path:
2452
+ # No backup available, cannot restore
2453
+ return
2454
+
2455
+ try:
2456
+ # Create scanner to find custom elements in backup (not current project)
2457
+ backup_scanner = create_custom_element_scanner(backup_path)
2458
+
2459
+ # Get count of custom elements in backup
2460
+ backup_element_count = backup_scanner.get_element_count()
2461
+
2462
+ if backup_element_count == 0:
2463
+ # No custom elements found in backup
2464
+ console.print("[green]โœ“ No custom elements found in backup to restore[/green]")
2465
+ return
2466
+
2467
+ # Create enhanced user selection UI
2468
+ ui = create_user_selection_ui(project_path)
2469
+
2470
+ console.print(f"\n[cyan]๐Ÿ” Found {backup_element_count} custom elements in backup[/cyan]")
2471
+
2472
+ # If yes mode is enabled, restore all elements automatically
2473
+ if yes:
2474
+ console.print(f"[cyan]๐Ÿ”„ Auto-restoring {backup_element_count} custom elements...[/cyan]")
2475
+ backup_custom_elements = backup_scanner.scan_custom_elements()
2476
+ selected_elements = []
2477
+
2478
+ # Collect all element paths from backup
2479
+ for element_type, elements in backup_custom_elements.items():
2480
+ if element_type == "skills":
2481
+ for skill in elements:
2482
+ selected_elements.append(str(skill.path))
2483
+ else:
2484
+ for element_path in elements:
2485
+ selected_elements.append(str(element_path))
2486
+ else:
2487
+ # Interactive mode - prompt user for selection using enhanced UI
2488
+ selected_elements = ui.prompt_user_selection(backup_available=True)
2489
+
2490
+ if not selected_elements:
2491
+ console.print("[yellow]โš  No elements selected for restoration[/yellow]")
2492
+ console.print("[green]โœ“ All existing custom elements will be preserved[/green]")
2493
+ return
2494
+
2495
+ # Confirm selection
2496
+ if not ui.confirm_selection(selected_elements):
2497
+ console.print("[yellow]โš  Restoration cancelled by user[/yellow]")
2498
+ console.print("[green]โœ“ All existing custom elements will be preserved[/green]")
2499
+ return
2500
+
2501
+ # Perform selective restoration - ONLY restore selected elements
2502
+ if selected_elements:
2503
+ console.print(f"[cyan]๐Ÿ”„ Restoring {len(selected_elements)} selected elements from backup...[/cyan]")
2504
+ restorer = create_selective_restorer(project_path, backup_path)
2505
+ success, stats = restorer.restore_elements(selected_elements)
2506
+
2507
+ if success:
2508
+ console.print(f"[green]โœ… Successfully restored {stats['success']} custom elements[/green]")
2509
+ console.print("[green]โœ“ All unselected elements remain preserved[/green]")
2510
+ else:
2511
+ console.print(f"[yellow]โš ๏ธ Partial restoration: {stats['success']}/{stats['total']} elements[/yellow]")
2512
+ if stats["failed"] > 0:
2513
+ console.print(f"[red]โŒ Failed to restore {stats['failed']} elements[/red]")
2514
+ console.print("[yellow]โš ๏ธ All other elements remain preserved[/yellow]")
2515
+ else:
2516
+ console.print("[green]โœ“ No elements selected, all custom elements preserved[/green]")
2517
+
2518
+ except Exception as e:
2519
+ console.print(f"[yellow]โš ๏ธ Custom element restoration failed: {e}[/yellow]")
2520
+ logger.warning(f"Custom element restoration error: {e}")
2521
+ console.print("[yellow]โš ๏ธ All existing custom elements remain as-is[/yellow]")
2522
+ # Don't fail the entire update process, just log the error
2523
+ pass
2524
+
2525
+
2526
+ def _cleanup_legacy_presets(project_path: Path) -> None:
2527
+ """Remove legacy JSON preset files (not the entire directory).
2528
+
2529
+ This function cleans up obsolete .json preset files in .moai/config/presets/
2530
+ that may exist from previous versions of MoAI-ADK. The directory itself
2531
+ and .yaml files are preserved since they are part of the current template.
2532
+
2533
+ Args:
2534
+ project_path: Project directory path (absolute)
2535
+ """
2536
+ presets_dir = project_path / ".moai" / "config" / "presets"
2537
+
2538
+ if not presets_dir.exists() or not presets_dir.is_dir():
2539
+ return
2540
+
2541
+ # Only remove legacy .json files, preserve .yaml files and the directory
2542
+ json_files = list(presets_dir.glob("*.json"))
2543
+ if not json_files:
2544
+ return
2545
+
2546
+ removed_count = 0
2547
+ for json_file in json_files:
2548
+ try:
2549
+ json_file.unlink()
2550
+ removed_count += 1
2551
+ logger.debug(f"Removed legacy preset file: {json_file}")
2552
+ except Exception as e:
2553
+ logger.warning(f"Failed to remove legacy preset {json_file}: {e}")
2554
+
2555
+ if removed_count > 0:
2556
+ console.print(f" [cyan]๐Ÿงน Cleaned up {removed_count} legacy JSON preset files[/cyan]")
2557
+ logger.info(f"Removed {removed_count} legacy JSON preset files from {presets_dir}")
2558
+
2559
+
2560
+ def _migrate_config_json_to_yaml(project_path: Path) -> bool:
2561
+ """Migrate legacy config.json to config.yaml format.
2562
+
2563
+ This function:
2564
+ 1. Checks if config.json exists
2565
+ 2. Converts it to config.yaml using YAML format
2566
+ 3. Removes the old config.json file
2567
+ 4. Also migrates preset files from JSON to YAML
2568
+
2569
+ Args:
2570
+ project_path: Project directory path (absolute)
2571
+
2572
+ Returns:
2573
+ bool: True if migration successful or not needed, False on error
2574
+ """
2575
+ try:
2576
+ import yaml
2577
+ except ImportError:
2578
+ console.print(" [yellow]โš ๏ธ PyYAML not available, skipping config migration[/yellow]")
2579
+ return True # Not a critical error
2580
+
2581
+ config_dir = project_path / ".moai" / "config"
2582
+ json_path = config_dir / "config.json"
2583
+ yaml_path = config_dir / "config.yaml"
2584
+
2585
+ # Check if migration needed
2586
+ if not json_path.exists():
2587
+ # No JSON file, migration not needed
2588
+ return True
2589
+
2590
+ if yaml_path.exists():
2591
+ # YAML already exists, just remove JSON
2592
+ try:
2593
+ json_path.unlink()
2594
+ console.print(" [cyan]๐Ÿ”„ Removed legacy config.json (YAML version exists)[/cyan]")
2595
+ logger.info(f"Removed legacy config.json: {json_path}")
2596
+ return True
2597
+ except Exception as e:
2598
+ console.print(f" [yellow]โš ๏ธ Failed to remove legacy config.json: {e}[/yellow]")
2599
+ logger.warning(f"Failed to remove {json_path}: {e}")
2600
+ return True # Not critical
2601
+
2602
+ # Perform migration
2603
+ try:
2604
+ # Read JSON config
2605
+ with open(json_path, "r", encoding="utf-8") as f:
2606
+ config_data = json.load(f)
2607
+
2608
+ # Write YAML config
2609
+ with open(yaml_path, "w", encoding="utf-8") as f:
2610
+ yaml.safe_dump(
2611
+ config_data,
2612
+ f,
2613
+ default_flow_style=False,
2614
+ allow_unicode=True,
2615
+ sort_keys=False,
2616
+ )
2617
+
2618
+ # Remove old JSON file
2619
+ json_path.unlink()
2620
+
2621
+ console.print(" [green]โœ“ Migrated config.json โ†’ config.yaml[/green]")
2622
+ logger.info(f"Migrated config from JSON to YAML: {json_path} โ†’ {yaml_path}")
2623
+
2624
+ # Migrate preset files if they exist
2625
+ _migrate_preset_files_to_yaml(config_dir)
2626
+
2627
+ return True
2628
+
2629
+ except Exception as e:
2630
+ console.print(f" [red]โœ— Config migration failed: {e}[/red]")
2631
+ logger.error(f"Failed to migrate config.json to YAML: {e}")
2632
+ return False
2633
+
2634
+
2635
+ def _migrate_preset_files_to_yaml(config_dir: Path) -> None:
2636
+ """Migrate preset files from JSON to YAML format.
2637
+
2638
+ Args:
2639
+ config_dir: .moai/config directory path
2640
+ """
2641
+ try:
2642
+ import yaml
2643
+ except ImportError:
2644
+ return
2645
+
2646
+ presets_dir = config_dir / "presets"
2647
+ if not presets_dir.exists():
2648
+ return
2649
+
2650
+ migrated_count = 0
2651
+ for json_file in presets_dir.glob("*.json"):
2652
+ yaml_file = json_file.with_suffix(".yaml")
2653
+
2654
+ # Skip if YAML already exists
2655
+ if yaml_file.exists():
2656
+ # Just remove the JSON file
2657
+ try:
2658
+ json_file.unlink()
2659
+ migrated_count += 1
2660
+ except Exception as e:
2661
+ logger.warning(f"Failed to remove {json_file}: {e}")
2662
+ continue
2663
+
2664
+ # Migrate JSON โ†’ YAML
2665
+ try:
2666
+ with open(json_file, "r", encoding="utf-8") as f:
2667
+ preset_data = json.load(f)
2668
+
2669
+ with open(yaml_file, "w", encoding="utf-8") as f:
2670
+ yaml.safe_dump(
2671
+ preset_data,
2672
+ f,
2673
+ default_flow_style=False,
2674
+ allow_unicode=True,
2675
+ sort_keys=False,
2676
+ )
2677
+
2678
+ json_file.unlink()
2679
+ migrated_count += 1
2680
+
2681
+ except Exception as e:
2682
+ logger.warning(f"Failed to migrate preset {json_file}: {e}")
2683
+
2684
+ if migrated_count > 0:
2685
+ console.print(f" [cyan]๐Ÿ”„ Migrated {migrated_count} preset file(s) to YAML[/cyan]")
2686
+ logger.info(f"Migrated {migrated_count} preset files to YAML")