moai-adk 0.35.1__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 (502) hide show
  1. moai_adk/__init__.py +10 -0
  2. moai_adk/__main__.py +199 -0
  3. moai_adk/cli/__init__.py +6 -0
  4. moai_adk/cli/commands/__init__.py +17 -0
  5. moai_adk/cli/commands/analyze.py +116 -0
  6. moai_adk/cli/commands/doctor.py +272 -0
  7. moai_adk/cli/commands/init.py +372 -0
  8. moai_adk/cli/commands/language.py +248 -0
  9. moai_adk/cli/commands/status.py +104 -0
  10. moai_adk/cli/commands/update.py +2686 -0
  11. moai_adk/cli/main.py +13 -0
  12. moai_adk/cli/prompts/__init__.py +5 -0
  13. moai_adk/cli/prompts/init_prompts.py +219 -0
  14. moai_adk/cli/spec_status.py +263 -0
  15. moai_adk/cli/ui/__init__.py +44 -0
  16. moai_adk/cli/ui/progress.py +422 -0
  17. moai_adk/cli/ui/prompts.py +389 -0
  18. moai_adk/cli/ui/theme.py +129 -0
  19. moai_adk/cli/worktree/__init__.py +27 -0
  20. moai_adk/cli/worktree/__main__.py +31 -0
  21. moai_adk/cli/worktree/cli.py +683 -0
  22. moai_adk/cli/worktree/exceptions.py +89 -0
  23. moai_adk/cli/worktree/manager.py +493 -0
  24. moai_adk/cli/worktree/models.py +65 -0
  25. moai_adk/cli/worktree/registry.py +422 -0
  26. moai_adk/core/PHASE2_OPTIMIZATIONS.md +467 -0
  27. moai_adk/core/__init__.py +1 -0
  28. moai_adk/core/analysis/__init__.py +9 -0
  29. moai_adk/core/analysis/session_analyzer.py +400 -0
  30. moai_adk/core/claude_integration.py +393 -0
  31. moai_adk/core/command_helpers.py +270 -0
  32. moai_adk/core/comprehensive_monitoring_system.py +1183 -0
  33. moai_adk/core/config/__init__.py +19 -0
  34. moai_adk/core/config/auto_spec_config.py +340 -0
  35. moai_adk/core/config/migration.py +244 -0
  36. moai_adk/core/config/unified.py +436 -0
  37. moai_adk/core/context_manager.py +273 -0
  38. moai_adk/core/diagnostics/__init__.py +19 -0
  39. moai_adk/core/diagnostics/slash_commands.py +159 -0
  40. moai_adk/core/enterprise_features.py +1404 -0
  41. moai_adk/core/error_recovery_system.py +1902 -0
  42. moai_adk/core/event_driven_hook_system.py +1371 -0
  43. moai_adk/core/git/__init__.py +31 -0
  44. moai_adk/core/git/branch.py +25 -0
  45. moai_adk/core/git/branch_manager.py +129 -0
  46. moai_adk/core/git/checkpoint.py +134 -0
  47. moai_adk/core/git/commit.py +67 -0
  48. moai_adk/core/git/conflict_detector.py +413 -0
  49. moai_adk/core/git/event_detector.py +79 -0
  50. moai_adk/core/git/manager.py +216 -0
  51. moai_adk/core/hooks/post_tool_auto_spec_completion.py +901 -0
  52. moai_adk/core/input_validation_middleware.py +1006 -0
  53. moai_adk/core/integration/__init__.py +22 -0
  54. moai_adk/core/integration/engine.py +157 -0
  55. moai_adk/core/integration/integration_tester.py +226 -0
  56. moai_adk/core/integration/models.py +88 -0
  57. moai_adk/core/integration/utils.py +211 -0
  58. moai_adk/core/issue_creator.py +305 -0
  59. moai_adk/core/jit_context_loader.py +956 -0
  60. moai_adk/core/jit_enhanced_hook_manager.py +1987 -0
  61. moai_adk/core/language_config.py +202 -0
  62. moai_adk/core/language_config_resolver.py +572 -0
  63. moai_adk/core/language_validator.py +543 -0
  64. moai_adk/core/mcp/setup.py +116 -0
  65. moai_adk/core/merge/__init__.py +9 -0
  66. moai_adk/core/merge/analyzer.py +605 -0
  67. moai_adk/core/migration/__init__.py +18 -0
  68. moai_adk/core/migration/alfred_to_moai_migrator.py +383 -0
  69. moai_adk/core/migration/backup_manager.py +277 -0
  70. moai_adk/core/migration/custom_element_scanner.py +358 -0
  71. moai_adk/core/migration/file_migrator.py +209 -0
  72. moai_adk/core/migration/interactive_checkbox_ui.py +488 -0
  73. moai_adk/core/migration/selective_restorer.py +470 -0
  74. moai_adk/core/migration/template_utils.py +74 -0
  75. moai_adk/core/migration/user_selection_ui.py +338 -0
  76. moai_adk/core/migration/version_detector.py +139 -0
  77. moai_adk/core/migration/version_migrator.py +228 -0
  78. moai_adk/core/performance/__init__.py +6 -0
  79. moai_adk/core/performance/cache_system.py +316 -0
  80. moai_adk/core/performance/parallel_processor.py +116 -0
  81. moai_adk/core/phase_optimized_hook_scheduler.py +879 -0
  82. moai_adk/core/project/__init__.py +1 -0
  83. moai_adk/core/project/backup_utils.py +70 -0
  84. moai_adk/core/project/checker.py +300 -0
  85. moai_adk/core/project/detector.py +293 -0
  86. moai_adk/core/project/initializer.py +387 -0
  87. moai_adk/core/project/phase_executor.py +716 -0
  88. moai_adk/core/project/validator.py +139 -0
  89. moai_adk/core/quality/__init__.py +6 -0
  90. moai_adk/core/quality/trust_checker.py +377 -0
  91. moai_adk/core/quality/validators/__init__.py +6 -0
  92. moai_adk/core/quality/validators/base_validator.py +19 -0
  93. moai_adk/core/realtime_monitoring_dashboard.py +1724 -0
  94. moai_adk/core/robust_json_parser.py +611 -0
  95. moai_adk/core/rollback_manager.py +918 -0
  96. moai_adk/core/session_manager.py +651 -0
  97. moai_adk/core/skill_loading_system.py +579 -0
  98. moai_adk/core/spec/confidence_scoring.py +680 -0
  99. moai_adk/core/spec/ears_template_engine.py +1247 -0
  100. moai_adk/core/spec/quality_validator.py +687 -0
  101. moai_adk/core/spec_status_manager.py +478 -0
  102. moai_adk/core/template/__init__.py +7 -0
  103. moai_adk/core/template/backup.py +174 -0
  104. moai_adk/core/template/config.py +191 -0
  105. moai_adk/core/template/languages.py +43 -0
  106. moai_adk/core/template/merger.py +233 -0
  107. moai_adk/core/template/processor.py +1200 -0
  108. moai_adk/core/template_engine.py +310 -0
  109. moai_adk/core/template_variable_synchronizer.py +417 -0
  110. moai_adk/core/unified_permission_manager.py +745 -0
  111. moai_adk/core/user_behavior_analytics.py +851 -0
  112. moai_adk/core/version_sync.py +429 -0
  113. moai_adk/foundation/__init__.py +56 -0
  114. moai_adk/foundation/backend.py +1027 -0
  115. moai_adk/foundation/database.py +1115 -0
  116. moai_adk/foundation/devops.py +1585 -0
  117. moai_adk/foundation/ears.py +431 -0
  118. moai_adk/foundation/frontend.py +870 -0
  119. moai_adk/foundation/git/commit_templates.py +557 -0
  120. moai_adk/foundation/git.py +376 -0
  121. moai_adk/foundation/langs.py +484 -0
  122. moai_adk/foundation/ml_ops.py +1162 -0
  123. moai_adk/foundation/testing.py +1524 -0
  124. moai_adk/foundation/trust/trust_principles.py +676 -0
  125. moai_adk/foundation/trust/validation_checklist.py +1573 -0
  126. moai_adk/project/__init__.py +0 -0
  127. moai_adk/project/configuration.py +1084 -0
  128. moai_adk/project/documentation.py +566 -0
  129. moai_adk/project/schema.py +447 -0
  130. moai_adk/statusline/__init__.py +38 -0
  131. moai_adk/statusline/alfred_detector.py +105 -0
  132. moai_adk/statusline/config.py +376 -0
  133. moai_adk/statusline/enhanced_output_style_detector.py +372 -0
  134. moai_adk/statusline/git_collector.py +190 -0
  135. moai_adk/statusline/main.py +322 -0
  136. moai_adk/statusline/metrics_tracker.py +78 -0
  137. moai_adk/statusline/renderer.py +343 -0
  138. moai_adk/statusline/update_checker.py +129 -0
  139. moai_adk/statusline/version_reader.py +741 -0
  140. moai_adk/templates/.claude/agents/moai/ai-nano-banana.md +714 -0
  141. moai_adk/templates/.claude/agents/moai/builder-agent.md +474 -0
  142. moai_adk/templates/.claude/agents/moai/builder-command.md +1172 -0
  143. moai_adk/templates/.claude/agents/moai/builder-plugin.md +637 -0
  144. moai_adk/templates/.claude/agents/moai/builder-skill.md +666 -0
  145. moai_adk/templates/.claude/agents/moai/expert-backend.md +899 -0
  146. moai_adk/templates/.claude/agents/moai/expert-database.md +777 -0
  147. moai_adk/templates/.claude/agents/moai/expert-debug.md +401 -0
  148. moai_adk/templates/.claude/agents/moai/expert-devops.md +720 -0
  149. moai_adk/templates/.claude/agents/moai/expert-frontend.md +734 -0
  150. moai_adk/templates/.claude/agents/moai/expert-performance.md +657 -0
  151. moai_adk/templates/.claude/agents/moai/expert-security.md +513 -0
  152. moai_adk/templates/.claude/agents/moai/expert-testing.md +733 -0
  153. moai_adk/templates/.claude/agents/moai/expert-uiux.md +1041 -0
  154. moai_adk/templates/.claude/agents/moai/manager-claude-code.md +432 -0
  155. moai_adk/templates/.claude/agents/moai/manager-docs.md +573 -0
  156. moai_adk/templates/.claude/agents/moai/manager-git.md +1060 -0
  157. moai_adk/templates/.claude/agents/moai/manager-project.md +891 -0
  158. moai_adk/templates/.claude/agents/moai/manager-quality.md +624 -0
  159. moai_adk/templates/.claude/agents/moai/manager-spec.md +809 -0
  160. moai_adk/templates/.claude/agents/moai/manager-strategy.md +780 -0
  161. moai_adk/templates/.claude/agents/moai/manager-tdd.md +784 -0
  162. moai_adk/templates/.claude/agents/moai/mcp-context7.md +458 -0
  163. moai_adk/templates/.claude/agents/moai/mcp-figma.md +1607 -0
  164. moai_adk/templates/.claude/agents/moai/mcp-notion.md +789 -0
  165. moai_adk/templates/.claude/agents/moai/mcp-playwright.md +469 -0
  166. moai_adk/templates/.claude/agents/moai/mcp-sequential-thinking.md +1032 -0
  167. moai_adk/templates/.claude/commands/moai/0-project.md +1386 -0
  168. moai_adk/templates/.claude/commands/moai/1-plan.md +1427 -0
  169. moai_adk/templates/.claude/commands/moai/2-run.md +943 -0
  170. moai_adk/templates/.claude/commands/moai/3-sync.md +1324 -0
  171. moai_adk/templates/.claude/commands/moai/9-feedback.md +314 -0
  172. moai_adk/templates/.claude/hooks/__init__.py +8 -0
  173. moai_adk/templates/.claude/hooks/moai/__init__.py +8 -0
  174. moai_adk/templates/.claude/hooks/moai/lib/__init__.py +85 -0
  175. moai_adk/templates/.claude/hooks/moai/lib/checkpoint.py +244 -0
  176. moai_adk/templates/.claude/hooks/moai/lib/common.py +131 -0
  177. moai_adk/templates/.claude/hooks/moai/lib/config_manager.py +446 -0
  178. moai_adk/templates/.claude/hooks/moai/lib/config_validator.py +639 -0
  179. moai_adk/templates/.claude/hooks/moai/lib/example_config.json +104 -0
  180. moai_adk/templates/.claude/hooks/moai/lib/git_operations_manager.py +590 -0
  181. moai_adk/templates/.claude/hooks/moai/lib/language_validator.py +317 -0
  182. moai_adk/templates/.claude/hooks/moai/lib/models.py +102 -0
  183. moai_adk/templates/.claude/hooks/moai/lib/path_utils.py +28 -0
  184. moai_adk/templates/.claude/hooks/moai/lib/project.py +768 -0
  185. moai_adk/templates/.claude/hooks/moai/lib/test_hooks_improvements.py +443 -0
  186. moai_adk/templates/.claude/hooks/moai/lib/timeout.py +160 -0
  187. moai_adk/templates/.claude/hooks/moai/lib/unified_timeout_manager.py +530 -0
  188. moai_adk/templates/.claude/hooks/moai/session_end__auto_cleanup.py +862 -0
  189. moai_adk/templates/.claude/hooks/moai/session_start__show_project_info.py +1083 -0
  190. moai_adk/templates/.claude/output-styles/moai/r2d2.md +560 -0
  191. moai_adk/templates/.claude/output-styles/moai/yoda.md +359 -0
  192. moai_adk/templates/.claude/settings.json +172 -0
  193. moai_adk/templates/.claude/skills/moai-ai-nano-banana/SKILL.md +307 -0
  194. moai_adk/templates/.claude/skills/moai-ai-nano-banana/examples.md +431 -0
  195. moai_adk/templates/.claude/skills/moai-ai-nano-banana/scripts/batch_generate.py +560 -0
  196. moai_adk/templates/.claude/skills/moai-ai-nano-banana/scripts/generate_image.py +362 -0
  197. moai_adk/templates/.claude/skills/moai-docs-generation/SKILL.md +249 -0
  198. moai_adk/templates/.claude/skills/moai-docs-generation/examples.md +406 -0
  199. moai_adk/templates/.claude/skills/moai-docs-generation/modules/README.md +44 -0
  200. moai_adk/templates/.claude/skills/moai-docs-generation/modules/api-documentation.md +130 -0
  201. moai_adk/templates/.claude/skills/moai-docs-generation/modules/code-documentation.md +152 -0
  202. moai_adk/templates/.claude/skills/moai-docs-generation/modules/multi-format-output.md +178 -0
  203. moai_adk/templates/.claude/skills/moai-docs-generation/modules/user-guides.md +147 -0
  204. moai_adk/templates/.claude/skills/moai-docs-generation/reference.md +328 -0
  205. moai_adk/templates/.claude/skills/moai-domain-backend/SKILL.md +320 -0
  206. moai_adk/templates/.claude/skills/moai-domain-backend/examples.md +718 -0
  207. moai_adk/templates/.claude/skills/moai-domain-backend/reference.md +464 -0
  208. moai_adk/templates/.claude/skills/moai-domain-database/SKILL.md +323 -0
  209. moai_adk/templates/.claude/skills/moai-domain-database/examples.md +830 -0
  210. moai_adk/templates/.claude/skills/moai-domain-database/modules/README.md +53 -0
  211. moai_adk/templates/.claude/skills/moai-domain-database/modules/mongodb.md +231 -0
  212. moai_adk/templates/.claude/skills/moai-domain-database/modules/postgresql.md +169 -0
  213. moai_adk/templates/.claude/skills/moai-domain-database/modules/redis.md +262 -0
  214. moai_adk/templates/.claude/skills/moai-domain-database/reference.md +545 -0
  215. moai_adk/templates/.claude/skills/moai-domain-frontend/SKILL.md +497 -0
  216. moai_adk/templates/.claude/skills/moai-domain-frontend/examples.md +968 -0
  217. moai_adk/templates/.claude/skills/moai-domain-frontend/reference.md +664 -0
  218. moai_adk/templates/.claude/skills/moai-domain-uiux/SKILL.md +455 -0
  219. moai_adk/templates/.claude/skills/moai-domain-uiux/examples.md +560 -0
  220. moai_adk/templates/.claude/skills/moai-domain-uiux/modules/accessibility-wcag.md +260 -0
  221. moai_adk/templates/.claude/skills/moai-domain-uiux/modules/component-architecture.md +228 -0
  222. moai_adk/templates/.claude/skills/moai-domain-uiux/modules/icon-libraries.md +401 -0
  223. moai_adk/templates/.claude/skills/moai-domain-uiux/modules/theming-system.md +373 -0
  224. moai_adk/templates/.claude/skills/moai-domain-uiux/reference.md +243 -0
  225. moai_adk/templates/.claude/skills/moai-formats-data/SKILL.md +492 -0
  226. moai_adk/templates/.claude/skills/moai-formats-data/examples.md +804 -0
  227. moai_adk/templates/.claude/skills/moai-formats-data/modules/README.md +98 -0
  228. moai_adk/templates/.claude/skills/moai-formats-data/modules/SKILL-MODULARIZATION-TEMPLATE.md +278 -0
  229. moai_adk/templates/.claude/skills/moai-formats-data/modules/caching-performance.md +459 -0
  230. moai_adk/templates/.claude/skills/moai-formats-data/modules/data-validation.md +485 -0
  231. moai_adk/templates/.claude/skills/moai-formats-data/modules/json-optimization.md +374 -0
  232. moai_adk/templates/.claude/skills/moai-formats-data/modules/toon-encoding.md +308 -0
  233. moai_adk/templates/.claude/skills/moai-formats-data/reference.md +585 -0
  234. moai_adk/templates/.claude/skills/moai-foundation-claude/SKILL.md +202 -0
  235. moai_adk/templates/.claude/skills/moai-foundation-claude/examples.md +732 -0
  236. moai_adk/templates/.claude/skills/moai-foundation-claude/reference/best-practices-checklist.md +616 -0
  237. moai_adk/templates/.claude/skills/moai-foundation-claude/reference/claude-code-custom-slash-commands-official.md +729 -0
  238. moai_adk/templates/.claude/skills/moai-foundation-claude/reference/claude-code-hooks-official.md +560 -0
  239. moai_adk/templates/.claude/skills/moai-foundation-claude/reference/claude-code-iam-official.md +635 -0
  240. moai_adk/templates/.claude/skills/moai-foundation-claude/reference/claude-code-memory-official.md +543 -0
  241. moai_adk/templates/.claude/skills/moai-foundation-claude/reference/claude-code-settings-official.md +663 -0
  242. moai_adk/templates/.claude/skills/moai-foundation-claude/reference/claude-code-skills-official.md +113 -0
  243. moai_adk/templates/.claude/skills/moai-foundation-claude/reference/claude-code-sub-agents-official.md +238 -0
  244. moai_adk/templates/.claude/skills/moai-foundation-claude/reference/complete-configuration-guide.md +175 -0
  245. moai_adk/templates/.claude/skills/moai-foundation-claude/reference/skill-examples.md +1674 -0
  246. moai_adk/templates/.claude/skills/moai-foundation-claude/reference/skill-formatting-guide.md +729 -0
  247. moai_adk/templates/.claude/skills/moai-foundation-claude/reference/sub-agents/sub-agent-examples.md +1513 -0
  248. moai_adk/templates/.claude/skills/moai-foundation-claude/reference/sub-agents/sub-agent-formatting-guide.md +1086 -0
  249. moai_adk/templates/.claude/skills/moai-foundation-claude/reference/sub-agents/sub-agent-integration-patterns.md +1100 -0
  250. moai_adk/templates/.claude/skills/moai-foundation-claude/reference.md +209 -0
  251. moai_adk/templates/.claude/skills/moai-foundation-context/SKILL.md +441 -0
  252. moai_adk/templates/.claude/skills/moai-foundation-context/examples.md +1048 -0
  253. moai_adk/templates/.claude/skills/moai-foundation-context/reference.md +246 -0
  254. moai_adk/templates/.claude/skills/moai-foundation-core/SKILL.md +420 -0
  255. moai_adk/templates/.claude/skills/moai-foundation-core/examples.md +358 -0
  256. moai_adk/templates/.claude/skills/moai-foundation-core/modules/README.md +296 -0
  257. moai_adk/templates/.claude/skills/moai-foundation-core/modules/agents-reference.md +359 -0
  258. moai_adk/templates/.claude/skills/moai-foundation-core/modules/commands-reference.md +432 -0
  259. moai_adk/templates/.claude/skills/moai-foundation-core/modules/delegation-patterns.md +757 -0
  260. moai_adk/templates/.claude/skills/moai-foundation-core/modules/execution-rules.md +687 -0
  261. moai_adk/templates/.claude/skills/moai-foundation-core/modules/modular-system.md +665 -0
  262. moai_adk/templates/.claude/skills/moai-foundation-core/modules/progressive-disclosure.md +649 -0
  263. moai_adk/templates/.claude/skills/moai-foundation-core/modules/spec-first-tdd.md +864 -0
  264. moai_adk/templates/.claude/skills/moai-foundation-core/modules/token-optimization.md +708 -0
  265. moai_adk/templates/.claude/skills/moai-foundation-core/modules/trust-5-framework.md +981 -0
  266. moai_adk/templates/.claude/skills/moai-foundation-core/reference.md +478 -0
  267. moai_adk/templates/.claude/skills/moai-foundation-philosopher/SKILL.md +315 -0
  268. moai_adk/templates/.claude/skills/moai-foundation-philosopher/examples.md +228 -0
  269. moai_adk/templates/.claude/skills/moai-foundation-philosopher/modules/assumption-matrix.md +80 -0
  270. moai_adk/templates/.claude/skills/moai-foundation-philosopher/modules/cognitive-bias.md +199 -0
  271. moai_adk/templates/.claude/skills/moai-foundation-philosopher/modules/first-principles.md +140 -0
  272. moai_adk/templates/.claude/skills/moai-foundation-philosopher/modules/trade-off-analysis.md +154 -0
  273. moai_adk/templates/.claude/skills/moai-foundation-philosopher/reference.md +157 -0
  274. moai_adk/templates/.claude/skills/moai-foundation-quality/SKILL.md +364 -0
  275. moai_adk/templates/.claude/skills/moai-foundation-quality/examples.md +1232 -0
  276. moai_adk/templates/.claude/skills/moai-foundation-quality/modules/best-practices.md +261 -0
  277. moai_adk/templates/.claude/skills/moai-foundation-quality/modules/integration-patterns.md +194 -0
  278. moai_adk/templates/.claude/skills/moai-foundation-quality/modules/proactive-analysis.md +229 -0
  279. moai_adk/templates/.claude/skills/moai-foundation-quality/modules/trust5-validation.md +169 -0
  280. moai_adk/templates/.claude/skills/moai-foundation-quality/reference.md +1266 -0
  281. moai_adk/templates/.claude/skills/moai-foundation-quality/scripts/quality-gate.sh +668 -0
  282. moai_adk/templates/.claude/skills/moai-foundation-quality/templates/github-actions-quality.yml +481 -0
  283. moai_adk/templates/.claude/skills/moai-foundation-quality/templates/quality-config.yaml +519 -0
  284. moai_adk/templates/.claude/skills/moai-lang-cpp/SKILL.md +649 -0
  285. moai_adk/templates/.claude/skills/moai-lang-csharp/SKILL.md +478 -0
  286. moai_adk/templates/.claude/skills/moai-lang-elixir/SKILL.md +612 -0
  287. moai_adk/templates/.claude/skills/moai-lang-flutter/SKILL.md +477 -0
  288. moai_adk/templates/.claude/skills/moai-lang-flutter/examples.md +1090 -0
  289. moai_adk/templates/.claude/skills/moai-lang-flutter/reference.md +686 -0
  290. moai_adk/templates/.claude/skills/moai-lang-go/SKILL.md +376 -0
  291. moai_adk/templates/.claude/skills/moai-lang-go/examples.md +919 -0
  292. moai_adk/templates/.claude/skills/moai-lang-go/reference.md +737 -0
  293. moai_adk/templates/.claude/skills/moai-lang-java/SKILL.md +385 -0
  294. moai_adk/templates/.claude/skills/moai-lang-java/examples.md +864 -0
  295. moai_adk/templates/.claude/skills/moai-lang-java/reference.md +291 -0
  296. moai_adk/templates/.claude/skills/moai-lang-kotlin/SKILL.md +382 -0
  297. moai_adk/templates/.claude/skills/moai-lang-kotlin/examples.md +1006 -0
  298. moai_adk/templates/.claude/skills/moai-lang-kotlin/reference.md +562 -0
  299. moai_adk/templates/.claude/skills/moai-lang-php/SKILL.md +644 -0
  300. moai_adk/templates/.claude/skills/moai-lang-python/SKILL.md +481 -0
  301. moai_adk/templates/.claude/skills/moai-lang-python/examples.md +977 -0
  302. moai_adk/templates/.claude/skills/moai-lang-python/reference.md +804 -0
  303. moai_adk/templates/.claude/skills/moai-lang-r/SKILL.md +579 -0
  304. moai_adk/templates/.claude/skills/moai-lang-ruby/SKILL.md +687 -0
  305. moai_adk/templates/.claude/skills/moai-lang-rust/SKILL.md +372 -0
  306. moai_adk/templates/.claude/skills/moai-lang-rust/examples.md +659 -0
  307. moai_adk/templates/.claude/skills/moai-lang-rust/reference.md +504 -0
  308. moai_adk/templates/.claude/skills/moai-lang-scala/SKILL.md +497 -0
  309. moai_adk/templates/.claude/skills/moai-lang-scala/examples.md +633 -0
  310. moai_adk/templates/.claude/skills/moai-lang-scala/reference.md +423 -0
  311. moai_adk/templates/.claude/skills/moai-lang-swift/SKILL.md +497 -0
  312. moai_adk/templates/.claude/skills/moai-lang-swift/examples.md +918 -0
  313. moai_adk/templates/.claude/skills/moai-lang-swift/reference.md +672 -0
  314. moai_adk/templates/.claude/skills/moai-lang-typescript/SKILL.md +368 -0
  315. moai_adk/templates/.claude/skills/moai-lang-typescript/examples.md +1089 -0
  316. moai_adk/templates/.claude/skills/moai-lang-typescript/reference.md +731 -0
  317. moai_adk/templates/.claude/skills/moai-library-mermaid/SKILL.md +300 -0
  318. moai_adk/templates/.claude/skills/moai-library-mermaid/advanced-patterns.md +465 -0
  319. moai_adk/templates/.claude/skills/moai-library-mermaid/examples.md +270 -0
  320. moai_adk/templates/.claude/skills/moai-library-mermaid/optimization.md +440 -0
  321. moai_adk/templates/.claude/skills/moai-library-mermaid/reference.md +228 -0
  322. moai_adk/templates/.claude/skills/moai-library-nextra/SKILL.md +319 -0
  323. moai_adk/templates/.claude/skills/moai-library-nextra/advanced-patterns.md +336 -0
  324. moai_adk/templates/.claude/skills/moai-library-nextra/examples.md +592 -0
  325. moai_adk/templates/.claude/skills/moai-library-nextra/modules/advanced-deployment-patterns.md +182 -0
  326. moai_adk/templates/.claude/skills/moai-library-nextra/modules/advanced-patterns.md +17 -0
  327. moai_adk/templates/.claude/skills/moai-library-nextra/modules/configuration.md +57 -0
  328. moai_adk/templates/.claude/skills/moai-library-nextra/modules/content-architecture-optimization.md +162 -0
  329. moai_adk/templates/.claude/skills/moai-library-nextra/modules/deployment.md +52 -0
  330. moai_adk/templates/.claude/skills/moai-library-nextra/modules/framework-core-configuration.md +186 -0
  331. moai_adk/templates/.claude/skills/moai-library-nextra/modules/i18n-setup.md +55 -0
  332. moai_adk/templates/.claude/skills/moai-library-nextra/modules/mdx-components.md +52 -0
  333. moai_adk/templates/.claude/skills/moai-library-nextra/optimization.md +303 -0
  334. moai_adk/templates/.claude/skills/moai-library-nextra/reference.md +379 -0
  335. moai_adk/templates/.claude/skills/moai-library-shadcn/SKILL.md +372 -0
  336. moai_adk/templates/.claude/skills/moai-library-shadcn/examples.md +575 -0
  337. moai_adk/templates/.claude/skills/moai-library-shadcn/modules/advanced-patterns.md +394 -0
  338. moai_adk/templates/.claude/skills/moai-library-shadcn/modules/optimization.md +278 -0
  339. moai_adk/templates/.claude/skills/moai-library-shadcn/modules/shadcn-components.md +457 -0
  340. moai_adk/templates/.claude/skills/moai-library-shadcn/modules/shadcn-theming.md +373 -0
  341. moai_adk/templates/.claude/skills/moai-library-shadcn/reference.md +74 -0
  342. moai_adk/templates/.claude/skills/moai-mcp-figma/SKILL.md +402 -0
  343. moai_adk/templates/.claude/skills/moai-mcp-figma/advanced-patterns.md +607 -0
  344. moai_adk/templates/.claude/skills/moai-mcp-notion/SKILL.md +300 -0
  345. moai_adk/templates/.claude/skills/moai-mcp-notion/advanced-patterns.md +537 -0
  346. moai_adk/templates/.claude/skills/moai-platform-auth0/SKILL.md +291 -0
  347. moai_adk/templates/.claude/skills/moai-platform-clerk/SKILL.md +390 -0
  348. moai_adk/templates/.claude/skills/moai-platform-convex/SKILL.md +398 -0
  349. moai_adk/templates/.claude/skills/moai-platform-firebase-auth/SKILL.md +379 -0
  350. moai_adk/templates/.claude/skills/moai-platform-firestore/SKILL.md +358 -0
  351. moai_adk/templates/.claude/skills/moai-platform-neon/SKILL.md +467 -0
  352. moai_adk/templates/.claude/skills/moai-platform-railway/SKILL.md +377 -0
  353. moai_adk/templates/.claude/skills/moai-platform-supabase/SKILL.md +466 -0
  354. moai_adk/templates/.claude/skills/moai-platform-vercel/SKILL.md +482 -0
  355. moai_adk/templates/.claude/skills/moai-plugin-builder/SKILL.md +474 -0
  356. moai_adk/templates/.claude/skills/moai-plugin-builder/examples.md +621 -0
  357. moai_adk/templates/.claude/skills/moai-plugin-builder/migration.md +341 -0
  358. moai_adk/templates/.claude/skills/moai-plugin-builder/reference.md +463 -0
  359. moai_adk/templates/.claude/skills/moai-plugin-builder/validation.md +373 -0
  360. moai_adk/templates/.claude/skills/moai-security-auth0/SKILL.md +275 -0
  361. moai_adk/templates/.claude/skills/moai-security-auth0/modules/adaptive-mfa.md +233 -0
  362. moai_adk/templates/.claude/skills/moai-security-auth0/modules/akamai-integration.md +215 -0
  363. moai_adk/templates/.claude/skills/moai-security-auth0/modules/application-credentials.md +280 -0
  364. moai_adk/templates/.claude/skills/moai-security-auth0/modules/attack-protection-log-events.md +225 -0
  365. moai_adk/templates/.claude/skills/moai-security-auth0/modules/attack-protection-overview.md +140 -0
  366. moai_adk/templates/.claude/skills/moai-security-auth0/modules/bot-detection.md +144 -0
  367. moai_adk/templates/.claude/skills/moai-security-auth0/modules/breached-password-detection.md +187 -0
  368. moai_adk/templates/.claude/skills/moai-security-auth0/modules/brute-force-protection.md +189 -0
  369. moai_adk/templates/.claude/skills/moai-security-auth0/modules/certifications.md +282 -0
  370. moai_adk/templates/.claude/skills/moai-security-auth0/modules/compliance-overview.md +263 -0
  371. moai_adk/templates/.claude/skills/moai-security-auth0/modules/continuous-session-protection.md +307 -0
  372. moai_adk/templates/.claude/skills/moai-security-auth0/modules/customize-mfa.md +178 -0
  373. moai_adk/templates/.claude/skills/moai-security-auth0/modules/dpop-implementation.md +283 -0
  374. moai_adk/templates/.claude/skills/moai-security-auth0/modules/fapi-implementation.md +259 -0
  375. moai_adk/templates/.claude/skills/moai-security-auth0/modules/gdpr-compliance.md +313 -0
  376. moai_adk/templates/.claude/skills/moai-security-auth0/modules/guardian-configuration.md +269 -0
  377. moai_adk/templates/.claude/skills/moai-security-auth0/modules/highly-regulated-identity.md +272 -0
  378. moai_adk/templates/.claude/skills/moai-security-auth0/modules/jwt-fundamentals.md +248 -0
  379. moai_adk/templates/.claude/skills/moai-security-auth0/modules/mdl-verification.md +211 -0
  380. moai_adk/templates/.claude/skills/moai-security-auth0/modules/mfa-api-management.md +278 -0
  381. moai_adk/templates/.claude/skills/moai-security-auth0/modules/mfa-factors.md +226 -0
  382. moai_adk/templates/.claude/skills/moai-security-auth0/modules/mfa-overview.md +174 -0
  383. moai_adk/templates/.claude/skills/moai-security-auth0/modules/mtls-sender-constraining.md +316 -0
  384. moai_adk/templates/.claude/skills/moai-security-auth0/modules/ropg-flow-mfa.md +217 -0
  385. moai_adk/templates/.claude/skills/moai-security-auth0/modules/security-center.md +325 -0
  386. moai_adk/templates/.claude/skills/moai-security-auth0/modules/security-guidance.md +277 -0
  387. moai_adk/templates/.claude/skills/moai-security-auth0/modules/state-parameters.md +178 -0
  388. moai_adk/templates/.claude/skills/moai-security-auth0/modules/step-up-authentication.md +251 -0
  389. moai_adk/templates/.claude/skills/moai-security-auth0/modules/suspicious-ip-throttling.md +240 -0
  390. moai_adk/templates/.claude/skills/moai-security-auth0/modules/tenant-access-control.md +180 -0
  391. moai_adk/templates/.claude/skills/moai-security-auth0/modules/webauthn-fido.md +235 -0
  392. moai_adk/templates/.claude/skills/moai-workflow-jit-docs/SKILL.md +449 -0
  393. moai_adk/templates/.claude/skills/moai-workflow-jit-docs/advanced-patterns.md +379 -0
  394. moai_adk/templates/.claude/skills/moai-workflow-jit-docs/examples.md +544 -0
  395. moai_adk/templates/.claude/skills/moai-workflow-jit-docs/optimization.md +286 -0
  396. moai_adk/templates/.claude/skills/moai-workflow-jit-docs/reference.md +307 -0
  397. moai_adk/templates/.claude/skills/moai-workflow-project/README.md +190 -0
  398. moai_adk/templates/.claude/skills/moai-workflow-project/SKILL.md +390 -0
  399. moai_adk/templates/.claude/skills/moai-workflow-project/__init__.py +520 -0
  400. moai_adk/templates/.claude/skills/moai-workflow-project/complete_workflow_demo_fixed.py +574 -0
  401. moai_adk/templates/.claude/skills/moai-workflow-project/examples/complete_project_setup.py +317 -0
  402. moai_adk/templates/.claude/skills/moai-workflow-project/examples/complete_workflow_demo.py +663 -0
  403. moai_adk/templates/.claude/skills/moai-workflow-project/examples/config-migration-example.json +190 -0
  404. moai_adk/templates/.claude/skills/moai-workflow-project/examples/question-examples.json +175 -0
  405. moai_adk/templates/.claude/skills/moai-workflow-project/examples/quick_start.py +196 -0
  406. moai_adk/templates/.claude/skills/moai-workflow-project/examples.md +547 -0
  407. moai_adk/templates/.claude/skills/moai-workflow-project/modules/__init__.py +17 -0
  408. moai_adk/templates/.claude/skills/moai-workflow-project/modules/advanced-patterns.md +158 -0
  409. moai_adk/templates/.claude/skills/moai-workflow-project/modules/ask_user_integration.py +340 -0
  410. moai_adk/templates/.claude/skills/moai-workflow-project/modules/batch_questions.py +713 -0
  411. moai_adk/templates/.claude/skills/moai-workflow-project/modules/config_manager.py +538 -0
  412. moai_adk/templates/.claude/skills/moai-workflow-project/modules/documentation_manager.py +1336 -0
  413. moai_adk/templates/.claude/skills/moai-workflow-project/modules/language_initializer.py +730 -0
  414. moai_adk/templates/.claude/skills/moai-workflow-project/modules/migration_manager.py +608 -0
  415. moai_adk/templates/.claude/skills/moai-workflow-project/modules/template_optimizer.py +1005 -0
  416. moai_adk/templates/.claude/skills/moai-workflow-project/reference.md +275 -0
  417. moai_adk/templates/.claude/skills/moai-workflow-project/schemas/config-schema.json +316 -0
  418. moai_adk/templates/.claude/skills/moai-workflow-project/schemas/tab_schema.json +1434 -0
  419. moai_adk/templates/.claude/skills/moai-workflow-project/templates/config-template.json +71 -0
  420. moai_adk/templates/.claude/skills/moai-workflow-project/templates/doc-templates/product-template.md +44 -0
  421. moai_adk/templates/.claude/skills/moai-workflow-project/templates/doc-templates/structure-template.md +48 -0
  422. moai_adk/templates/.claude/skills/moai-workflow-project/templates/doc-templates/tech-template.md +92 -0
  423. moai_adk/templates/.claude/skills/moai-workflow-project/templates/question-templates/config-manager-setup.json +109 -0
  424. moai_adk/templates/.claude/skills/moai-workflow-project/templates/question-templates/language-initializer.json +228 -0
  425. moai_adk/templates/.claude/skills/moai-workflow-project/templates/question-templates/menu-project-config.json +130 -0
  426. moai_adk/templates/.claude/skills/moai-workflow-project/templates/question-templates/project-batch-questions.json +97 -0
  427. moai_adk/templates/.claude/skills/moai-workflow-project/templates/question-templates/spec-workflow-setup.json +150 -0
  428. moai_adk/templates/.claude/skills/moai-workflow-project/test_integration_simple.py +436 -0
  429. moai_adk/templates/.claude/skills/moai-workflow-spec/SKILL.md +534 -0
  430. moai_adk/templates/.claude/skills/moai-workflow-spec/examples.md +900 -0
  431. moai_adk/templates/.claude/skills/moai-workflow-spec/reference.md +704 -0
  432. moai_adk/templates/.claude/skills/moai-workflow-templates/SKILL.md +377 -0
  433. moai_adk/templates/.claude/skills/moai-workflow-templates/examples.md +552 -0
  434. moai_adk/templates/.claude/skills/moai-workflow-templates/modules/code-templates.md +124 -0
  435. moai_adk/templates/.claude/skills/moai-workflow-templates/modules/feedback-templates.md +100 -0
  436. moai_adk/templates/.claude/skills/moai-workflow-templates/modules/template-optimizer.md +138 -0
  437. moai_adk/templates/.claude/skills/moai-workflow-templates/reference.md +346 -0
  438. moai_adk/templates/.claude/skills/moai-workflow-testing/LICENSE.txt +202 -0
  439. moai_adk/templates/.claude/skills/moai-workflow-testing/SKILL.md +456 -0
  440. moai_adk/templates/.claude/skills/moai-workflow-testing/advanced-patterns.md +576 -0
  441. moai_adk/templates/.claude/skills/moai-workflow-testing/examples/ai-powered-testing.py +294 -0
  442. moai_adk/templates/.claude/skills/moai-workflow-testing/examples/console_logging.py +35 -0
  443. moai_adk/templates/.claude/skills/moai-workflow-testing/examples/element_discovery.py +40 -0
  444. moai_adk/templates/.claude/skills/moai-workflow-testing/examples/static_html_automation.py +34 -0
  445. moai_adk/templates/.claude/skills/moai-workflow-testing/examples.md +672 -0
  446. moai_adk/templates/.claude/skills/moai-workflow-testing/modules/README.md +220 -0
  447. moai_adk/templates/.claude/skills/moai-workflow-testing/modules/ai-debugging.md +845 -0
  448. moai_adk/templates/.claude/skills/moai-workflow-testing/modules/automated-code-review.md +1416 -0
  449. moai_adk/templates/.claude/skills/moai-workflow-testing/modules/performance-optimization.md +1234 -0
  450. moai_adk/templates/.claude/skills/moai-workflow-testing/modules/smart-refactoring.md +1243 -0
  451. moai_adk/templates/.claude/skills/moai-workflow-testing/modules/tdd-context7.md +1260 -0
  452. moai_adk/templates/.claude/skills/moai-workflow-testing/optimization.md +505 -0
  453. moai_adk/templates/.claude/skills/moai-workflow-testing/reference/playwright-best-practices.md +57 -0
  454. moai_adk/templates/.claude/skills/moai-workflow-testing/reference.md +440 -0
  455. moai_adk/templates/.claude/skills/moai-workflow-testing/scripts/with_server.py +218 -0
  456. moai_adk/templates/.claude/skills/moai-workflow-testing/templates/alfred-integration.md +376 -0
  457. moai_adk/templates/.claude/skills/moai-workflow-testing/workflows/enterprise-testing-workflow.py +571 -0
  458. moai_adk/templates/.claude/skills/moai-worktree/SKILL.md +411 -0
  459. moai_adk/templates/.claude/skills/moai-worktree/examples.md +606 -0
  460. moai_adk/templates/.claude/skills/moai-worktree/modules/integration-patterns.md +982 -0
  461. moai_adk/templates/.claude/skills/moai-worktree/modules/parallel-development.md +778 -0
  462. moai_adk/templates/.claude/skills/moai-worktree/modules/worktree-commands.md +646 -0
  463. moai_adk/templates/.claude/skills/moai-worktree/modules/worktree-management.md +782 -0
  464. moai_adk/templates/.claude/skills/moai-worktree/reference.md +357 -0
  465. moai_adk/templates/.git-hooks/pre-commit +128 -0
  466. moai_adk/templates/.git-hooks/pre-push +365 -0
  467. moai_adk/templates/.github/workflows/ci-universal.yml +513 -0
  468. moai_adk/templates/.github/workflows/security-secrets-check.yml +179 -0
  469. moai_adk/templates/.github/workflows/spec-issue-sync.yml +337 -0
  470. moai_adk/templates/.gitignore +222 -0
  471. moai_adk/templates/.mcp.json +13 -0
  472. moai_adk/templates/.moai/config/config.yaml +58 -0
  473. moai_adk/templates/.moai/config/questions/_schema.yaml +174 -0
  474. moai_adk/templates/.moai/config/questions/tab0-init.yaml +251 -0
  475. moai_adk/templates/.moai/config/questions/tab1-user.yaml +107 -0
  476. moai_adk/templates/.moai/config/questions/tab2-project.yaml +79 -0
  477. moai_adk/templates/.moai/config/questions/tab3-git.yaml +632 -0
  478. moai_adk/templates/.moai/config/questions/tab4-quality.yaml +182 -0
  479. moai_adk/templates/.moai/config/questions/tab5-system.yaml +96 -0
  480. moai_adk/templates/.moai/config/sections/git-strategy.yaml +116 -0
  481. moai_adk/templates/.moai/config/sections/language.yaml +11 -0
  482. moai_adk/templates/.moai/config/sections/project.yaml +13 -0
  483. moai_adk/templates/.moai/config/sections/quality.yaml +17 -0
  484. moai_adk/templates/.moai/config/sections/system.yaml +24 -0
  485. moai_adk/templates/.moai/config/sections/user.yaml +5 -0
  486. moai_adk/templates/.moai/config/statusline-config.yaml +92 -0
  487. moai_adk/templates/.moai/scripts/setup-glm.py +136 -0
  488. moai_adk/templates/CLAUDE.md +642 -0
  489. moai_adk/utils/__init__.py +30 -0
  490. moai_adk/utils/banner.py +38 -0
  491. moai_adk/utils/common.py +294 -0
  492. moai_adk/utils/link_validator.py +241 -0
  493. moai_adk/utils/logger.py +147 -0
  494. moai_adk/utils/safe_file_reader.py +206 -0
  495. moai_adk/utils/timeout.py +160 -0
  496. moai_adk/utils/toon_utils.py +256 -0
  497. moai_adk/version.py +22 -0
  498. moai_adk-0.35.1.dist-info/METADATA +3018 -0
  499. moai_adk-0.35.1.dist-info/RECORD +502 -0
  500. moai_adk-0.35.1.dist-info/WHEEL +4 -0
  501. moai_adk-0.35.1.dist-info/entry_points.txt +3 -0
  502. moai_adk-0.35.1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,2686 @@
1
+ """Update command
2
+
3
+ Update MoAI-ADK to the latest version available on PyPI with 3-stage workflow:
4
+ - Stage 1: Package version check (PyPI vs current)
5
+ - Stage 2: Config version comparison (template_version in config.json)
6
+ - Stage 3: Template sync (only if versions differ)
7
+
8
+ Includes:
9
+ - Automatic installer detection (uv tool, pipx, pip)
10
+ - Package upgrade with intelligent re-run prompts
11
+ - Template and configuration updates with performance optimization
12
+ - Backward compatibility validation
13
+ - 70-80% performance improvement for up-to-date projects
14
+
15
+ ## Skill Invocation Guide (English-Only)
16
+ # mypy: disable-error-code=return-value
17
+
18
+ ### Related Skills
19
+ - **moai-foundation-trust**: For post-update validation
20
+ - Trigger: After updating MoAI-ADK version
21
+ - Invocation: `Skill("moai-foundation-trust")` to verify all toolchains still work
22
+
23
+ - **moai-foundation-langs**: For language detection after update
24
+ - Trigger: After updating, confirm language stack is intact
25
+ - Invocation: `Skill("moai-foundation-langs")` to re-detect and validate language configuration
26
+
27
+ ### When to Invoke Skills in Related Workflows
28
+ 1. **After successful update**:
29
+ - Run `Skill("moai-foundation-trust")` to validate all TRUST 4 gates
30
+ - Run `Skill("moai-foundation-langs")` to confirm language toolchain still works
31
+ - Run project doctor command for full system validation
32
+
33
+ 2. **Before updating**:
34
+ - Create backup with `python -m moai_adk backup`
35
+
36
+ 3. **If update fails**:
37
+ - Use backup to restore previous state
38
+ - Debug with `python -m moai_adk doctor --verbose`
39
+ """
40
+
41
+ # type: ignore
42
+
43
+ from __future__ import annotations
44
+
45
+ import json
46
+ import logging
47
+ import shutil
48
+ import subprocess
49
+ from datetime import datetime
50
+ from pathlib import Path
51
+ from typing import Any, Union, cast
52
+
53
+ import click
54
+ import yaml
55
+ from packaging import version
56
+ from rich.console import Console
57
+
58
+ from moai_adk import __version__
59
+ from moai_adk.core.merge import MergeAnalyzer
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
67
+ from moai_adk.core.template.processor import TemplateProcessor
68
+
69
+ console = Console()
70
+ logger = logging.getLogger(__name__)
71
+
72
+ # Constants for tool detection
73
+ TOOL_DETECTION_TIMEOUT = 5 # seconds
74
+ UV_TOOL_COMMAND = ["uv", "tool", "upgrade", "moai-adk"]
75
+ PIPX_COMMAND = ["pipx", "upgrade", "moai-adk"]
76
+ PIP_COMMAND = ["pip", "install", "--upgrade", "moai-adk"]
77
+
78
+
79
+ # Custom exceptions for better error handling
80
+ class UpdateError(Exception):
81
+ """Base exception for update operations."""
82
+
83
+ pass
84
+
85
+
86
+ class InstallerNotFoundError(UpdateError):
87
+ """Raised when no package installer detected."""
88
+
89
+ pass
90
+
91
+
92
+ class NetworkError(UpdateError):
93
+ """Raised when network operation fails."""
94
+
95
+ pass
96
+
97
+
98
+ class UpgradeError(UpdateError):
99
+ """Raised when package upgrade fails."""
100
+
101
+ pass
102
+
103
+
104
+ class TemplateSyncError(UpdateError):
105
+ """Raised when template sync fails."""
106
+
107
+ pass
108
+
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
+
149
+ def _is_installed_via_uv_tool() -> bool:
150
+ """Check if moai-adk installed via uv tool.
151
+
152
+ Returns:
153
+ True if uv tool list shows moai-adk, False otherwise
154
+ """
155
+ try:
156
+ result = subprocess.run(
157
+ ["uv", "tool", "list"],
158
+ capture_output=True,
159
+ text=True,
160
+ timeout=TOOL_DETECTION_TIMEOUT,
161
+ check=False,
162
+ )
163
+ return result.returncode == 0 and "moai-adk" in result.stdout
164
+ except (FileNotFoundError, subprocess.TimeoutExpired, OSError):
165
+ return False
166
+
167
+
168
+ def _is_installed_via_pipx() -> bool:
169
+ """Check if moai-adk installed via pipx.
170
+
171
+ Returns:
172
+ True if pipx list shows moai-adk, False otherwise
173
+ """
174
+ try:
175
+ result = subprocess.run(
176
+ ["pipx", "list"],
177
+ capture_output=True,
178
+ text=True,
179
+ timeout=TOOL_DETECTION_TIMEOUT,
180
+ check=False,
181
+ )
182
+ return result.returncode == 0 and "moai-adk" in result.stdout
183
+ except (FileNotFoundError, subprocess.TimeoutExpired, OSError):
184
+ return False
185
+
186
+
187
+ def _is_installed_via_pip() -> bool:
188
+ """Check if moai-adk installed via pip.
189
+
190
+ Returns:
191
+ True if pip show finds moai-adk, False otherwise
192
+ """
193
+ try:
194
+ result = subprocess.run(
195
+ ["pip", "show", "moai-adk"],
196
+ capture_output=True,
197
+ text=True,
198
+ timeout=TOOL_DETECTION_TIMEOUT,
199
+ check=False,
200
+ )
201
+ return result.returncode == 0
202
+ except (FileNotFoundError, subprocess.TimeoutExpired, OSError):
203
+ return False
204
+
205
+
206
+ def _detect_tool_installer() -> list[str] | None:
207
+ """Detect which tool installed moai-adk.
208
+
209
+ Checks in priority order:
210
+ 1. uv tool (most likely for MoAI-ADK users)
211
+ 2. pipx
212
+ 3. pip (fallback)
213
+
214
+ Returns:
215
+ Command list [tool, ...args] ready for subprocess.run()
216
+ or None if detection fails
217
+
218
+ Examples:
219
+ >>> # If uv tool is detected:
220
+ >>> _detect_tool_installer()
221
+ ['uv', 'tool', 'upgrade', 'moai-adk']
222
+
223
+ >>> # If pipx is detected:
224
+ >>> _detect_tool_installer()
225
+ ['pipx', 'upgrade', 'moai-adk']
226
+
227
+ >>> # If only pip is available:
228
+ >>> _detect_tool_installer()
229
+ ['pip', 'install', '--upgrade', 'moai-adk']
230
+
231
+ >>> # If none are detected:
232
+ >>> _detect_tool_installer()
233
+ None
234
+ """
235
+ if _is_installed_via_uv_tool():
236
+ return UV_TOOL_COMMAND
237
+ elif _is_installed_via_pipx():
238
+ return PIPX_COMMAND
239
+ elif _is_installed_via_pip():
240
+ return PIP_COMMAND
241
+ else:
242
+ return None
243
+
244
+
245
+ def _get_current_version() -> str:
246
+ """Get currently installed moai-adk version.
247
+
248
+ Returns:
249
+ Version string (e.g., "0.6.1")
250
+
251
+ Raises:
252
+ RuntimeError: If version cannot be determined
253
+ """
254
+ return __version__
255
+
256
+
257
+ def _get_latest_version() -> str:
258
+ """Fetch latest moai-adk version from PyPI.
259
+
260
+ Returns:
261
+ Version string (e.g., "0.6.2")
262
+
263
+ Raises:
264
+ RuntimeError: If PyPI API unavailable or parsing fails
265
+ """
266
+ try:
267
+ import urllib.error
268
+ import urllib.request
269
+
270
+ url = "https://pypi.org/pypi/moai-adk/json"
271
+ with urllib.request.urlopen(url, timeout=5) as response: # nosec B310 - URL is hardcoded HTTPS to PyPI API, no user input
272
+ data = json.loads(response.read().decode("utf-8"))
273
+ return cast(str, data["info"]["version"])
274
+ except (urllib.error.URLError, json.JSONDecodeError, KeyError, TimeoutError) as e:
275
+ raise RuntimeError(f"Failed to fetch latest version from PyPI: {e}") from e
276
+
277
+
278
+ def _compare_versions(current: str, latest: str) -> int:
279
+ """Compare semantic versions.
280
+
281
+ Args:
282
+ current: Current version string
283
+ latest: Latest version string
284
+
285
+ Returns:
286
+ -1 if current < latest (upgrade needed)
287
+ 0 if current == latest (up to date)
288
+ 1 if current > latest (unusual, already newer)
289
+ """
290
+ current_v = version.parse(current)
291
+ latest_v = version.parse(latest)
292
+
293
+ if current_v < latest_v:
294
+ return -1
295
+ elif current_v == latest_v:
296
+ return 0
297
+ else:
298
+ return 1
299
+
300
+
301
+ def _get_package_config_version() -> str:
302
+ """Get the current package template version.
303
+
304
+ This returns the version of the currently installed moai-adk package,
305
+ which is the version of templates that this package provides.
306
+
307
+ Returns:
308
+ Version string of the installed package (e.g., "0.6.1")
309
+ """
310
+ # Package template version = current installed package version
311
+ # This is simple and reliable since templates are versioned with the package
312
+ return __version__
313
+
314
+
315
+ def _get_project_config_version(project_path: Path) -> str:
316
+ """Get current project config.json template version.
317
+
318
+ This reads the project's .moai/config/config.json to determine the current
319
+ template version that the project is configured with.
320
+
321
+ Args:
322
+ project_path: Project directory path (absolute)
323
+
324
+ Returns:
325
+ Version string from project's config.json (e.g., "0.6.1")
326
+ Returns "0.0.0" if template_version field not found (indicates no prior sync)
327
+
328
+ Raises:
329
+ ValueError: If config file exists but cannot be parsed
330
+ """
331
+
332
+ def _is_placeholder_val(value: str) -> bool:
333
+ """Check if value contains unsubstituted template placeholders."""
334
+ return isinstance(value, str) and value.startswith("{{") and value.endswith("}}")
335
+
336
+ config_path, _ = _get_config_path(project_path)
337
+
338
+ if not config_path.exists():
339
+ # No config yet, treat as version 0.0.0 (needs initial sync)
340
+ return "0.0.0"
341
+
342
+ try:
343
+ config_data = _load_config(config_path)
344
+ # Check for template_version in project section
345
+ template_version = config_data.get("project", {}).get("template_version")
346
+ if template_version and not _is_placeholder_val(template_version):
347
+ return template_version
348
+
349
+ # Fallback to moai version if no template_version exists
350
+ moai_version = config_data.get("moai", {}).get("version")
351
+ if moai_version and not _is_placeholder_val(moai_version):
352
+ return moai_version
353
+
354
+ # If values are placeholders or don't exist, treat as uninitialized (0.0.0 triggers sync)
355
+ return "0.0.0"
356
+ except (json.JSONDecodeError, yaml.YAMLError) as e:
357
+ raise ValueError(f"Failed to parse project config: {e}") from e
358
+
359
+
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:
721
+ """
722
+ Detect if uv cache is stale by comparing versions.
723
+
724
+ A stale cache occurs when PyPI metadata is outdated, causing uv to incorrectly
725
+ report "Nothing to upgrade" even though a newer version exists. This function
726
+ detects this condition by:
727
+ 1. Checking if upgrade output contains "Nothing to upgrade"
728
+ 2. Verifying that latest version is actually newer than current version
729
+
730
+ Uses packaging.version.parse() for robust semantic version comparison that
731
+ handles pre-releases, dev versions, and other PEP 440 version formats correctly.
732
+
733
+ Args:
734
+ upgrade_output: Output from uv tool upgrade command
735
+ current_version: Currently installed version (string, e.g., "0.8.3")
736
+ latest_version: Latest version available on PyPI (string, e.g., "0.9.0")
737
+
738
+ Returns:
739
+ True if cache is stale (output shows "Nothing to upgrade" but current < latest),
740
+ False otherwise
741
+
742
+ Examples:
743
+ >>> _detect_stale_cache("Nothing to upgrade", "0.8.3", "0.9.0")
744
+ True
745
+ >>> _detect_stale_cache("Updated moai-adk", "0.8.3", "0.9.0")
746
+ False
747
+ >>> _detect_stale_cache("Nothing to upgrade", "0.9.0", "0.9.0")
748
+ False
749
+ """
750
+ # Check if output indicates no upgrade needed
751
+ if not upgrade_output or "Nothing to upgrade" not in upgrade_output:
752
+ return False
753
+
754
+ # Compare versions using packaging.version
755
+ try:
756
+ current_v = version.parse(current_version)
757
+ latest_v = version.parse(latest_version)
758
+ return current_v < latest_v
759
+ except (version.InvalidVersion, TypeError) as e:
760
+ # Graceful degradation: if version parsing fails, assume cache is not stale
761
+ logger.debug(f"Version parsing failed: {e}")
762
+ return False
763
+
764
+
765
+ def _clear_uv_package_cache(package_name: str = "moai-adk") -> bool:
766
+ """
767
+ Clear uv cache for specific package.
768
+
769
+ Executes `uv cache clean <package>` with 10-second timeout to prevent
770
+ hanging on network issues. Provides user-friendly error handling for
771
+ various failure scenarios (timeout, missing uv, etc.).
772
+
773
+ Args:
774
+ package_name: Package name to clear cache for (default: "moai-adk")
775
+
776
+ Returns:
777
+ True if cache cleared successfully, False otherwise
778
+
779
+ Exceptions:
780
+ - subprocess.TimeoutExpired: Logged as warning, returns False
781
+ - FileNotFoundError: Logged as warning, returns False
782
+ - Exception: Logged as warning, returns False
783
+
784
+ Examples:
785
+ >>> _clear_uv_package_cache("moai-adk")
786
+ True # If uv cache clean succeeds
787
+ """
788
+ try:
789
+ result = subprocess.run(
790
+ ["uv", "cache", "clean", package_name],
791
+ capture_output=True,
792
+ text=True,
793
+ timeout=10, # 10 second timeout
794
+ check=False,
795
+ )
796
+
797
+ if result.returncode == 0:
798
+ logger.debug(f"UV cache cleared for {package_name}")
799
+ return True
800
+ else:
801
+ logger.warning(f"Failed to clear UV cache: {result.stderr}")
802
+ return False
803
+
804
+ except subprocess.TimeoutExpired:
805
+ logger.warning(f"UV cache clean timed out for {package_name}")
806
+ return False
807
+ except FileNotFoundError:
808
+ logger.warning("UV command not found. Is uv installed?")
809
+ return False
810
+ except Exception as e:
811
+ logger.warning(f"Unexpected error clearing cache: {e}")
812
+ return False
813
+
814
+
815
+ def _execute_upgrade_with_retry(installer_cmd: list[str], package_name: str = "moai-adk") -> bool:
816
+ """
817
+ Execute upgrade with automatic cache retry on stale detection.
818
+
819
+ Implements a robust 7-stage upgrade flow that handles PyPI cache staleness:
820
+
821
+ Stage 1: First upgrade attempt (up to 60 seconds)
822
+ Stage 2: Check success condition (returncode=0 AND no "Nothing to upgrade")
823
+ Stage 3: Detect stale cache using _detect_stale_cache()
824
+ Stage 4: Show user feedback if stale cache detected
825
+ Stage 5: Clear cache using _clear_uv_package_cache()
826
+ Stage 6: Retry upgrade with same command
827
+ Stage 7: Return final result (success or failure)
828
+
829
+ Retry Logic:
830
+ - Only ONE retry is performed to prevent infinite loops
831
+ - Retry only happens if stale cache is detected AND cache clear succeeds
832
+ - Cache clear failures are reported to user with manual workaround
833
+
834
+ User Feedback:
835
+ - Shows emoji-based status messages for each stage
836
+ - Clear guidance on manual workaround if automatic retry fails
837
+ - All errors logged at WARNING level for debugging
838
+
839
+ Args:
840
+ installer_cmd: Command list from _detect_tool_installer()
841
+ e.g., ["uv", "tool", "upgrade", "moai-adk"]
842
+ package_name: Package name for cache clearing (default: "moai-adk")
843
+
844
+ Returns:
845
+ True if upgrade succeeded (either first attempt or after retry),
846
+ False otherwise
847
+
848
+ Examples:
849
+ >>> # First attempt succeeds
850
+ >>> _execute_upgrade_with_retry(["uv", "tool", "upgrade", "moai-adk"])
851
+ True
852
+
853
+ >>> # First attempt stale, retry succeeds
854
+ >>> _execute_upgrade_with_retry(["uv", "tool", "upgrade", "moai-adk"])
855
+ True # After cache clear and retry
856
+
857
+ Raises:
858
+ subprocess.TimeoutExpired: Re-raised if upgrade command times out
859
+ """
860
+ # Stage 1: First upgrade attempt
861
+ try:
862
+ result = subprocess.run(installer_cmd, capture_output=True, text=True, timeout=60, check=False)
863
+ except subprocess.TimeoutExpired:
864
+ raise # Re-raise timeout for caller to handle
865
+ except Exception:
866
+ return False
867
+
868
+ # Stage 2: Check if upgrade succeeded without stale cache
869
+ if result.returncode == 0 and "Nothing to upgrade" not in result.stdout:
870
+ return True
871
+
872
+ # Stage 3: Detect stale cache
873
+ try:
874
+ current_version = _get_current_version()
875
+ latest_version = _get_latest_version()
876
+ except RuntimeError:
877
+ # If version check fails, return original result
878
+ return result.returncode == 0
879
+
880
+ if _detect_stale_cache(result.stdout, current_version, latest_version):
881
+ # Stage 4: User feedback
882
+ console.print("[yellow]โš ๏ธ Cache outdated, refreshing...[/yellow]")
883
+
884
+ # Stage 5: Clear cache
885
+ if _clear_uv_package_cache(package_name):
886
+ console.print("[cyan]โ™ป๏ธ Cache cleared, retrying upgrade...[/cyan]")
887
+
888
+ # Stage 6: Retry upgrade
889
+ try:
890
+ result = subprocess.run(
891
+ installer_cmd,
892
+ capture_output=True,
893
+ text=True,
894
+ timeout=60,
895
+ check=False,
896
+ )
897
+
898
+ if result.returncode == 0:
899
+ return True
900
+ else:
901
+ console.print("[red]โœ— Upgrade failed after retry[/red]")
902
+ return False
903
+ except subprocess.TimeoutExpired:
904
+ raise # Re-raise timeout
905
+ except Exception:
906
+ return False
907
+ else:
908
+ # Cache clear failed
909
+ console.print("[red]โœ— Cache clear failed. Manual workaround:[/red]")
910
+ console.print(" [cyan]uv cache clean moai-adk && moai-adk update[/cyan]")
911
+ return False
912
+
913
+ # Stage 7: Cache is not stale, return original result
914
+ return result.returncode == 0
915
+
916
+
917
+ def _execute_upgrade(installer_cmd: list[str]) -> bool:
918
+ """Execute package upgrade using detected installer.
919
+
920
+ Args:
921
+ installer_cmd: Command list from _detect_tool_installer()
922
+ e.g., ["uv", "tool", "upgrade", "moai-adk"]
923
+
924
+ Returns:
925
+ True if upgrade succeeded, False otherwise
926
+
927
+ Raises:
928
+ subprocess.TimeoutExpired: If upgrade times out
929
+ """
930
+ try:
931
+ result = subprocess.run(installer_cmd, capture_output=True, text=True, timeout=60, check=False)
932
+ return result.returncode == 0
933
+ except subprocess.TimeoutExpired:
934
+ raise # Re-raise timeout for caller to handle
935
+ except Exception:
936
+ return False
937
+
938
+
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:
1466
+ """Sync templates to project with rollback mechanism.
1467
+
1468
+ Args:
1469
+ project_path: Project path (absolute)
1470
+ force: Force update without backup
1471
+ yes: Auto-confirm flag (skips interactive prompts)
1472
+
1473
+ Returns:
1474
+ True if sync succeeded, False otherwise
1475
+ """
1476
+ from moai_adk.core.template.backup import TemplateBackup
1477
+
1478
+ backup_path = None
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
+
1494
+ processor = TemplateProcessor(project_path)
1495
+
1496
+ # Create pre-sync backup for rollback
1497
+ if not force:
1498
+ backup = TemplateBackup(project_path)
1499
+ if backup.has_existing_files():
1500
+ backup_path = backup.create_backup()
1501
+ console.print(f"๐Ÿ’พ Created backup: {backup_path.name}")
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
+
1523
+ # Load existing config
1524
+ existing_config = _load_existing_config(project_path)
1525
+
1526
+ # Build context
1527
+ context = _build_template_context(project_path, existing_config, __version__)
1528
+ if context:
1529
+ processor.set_context(context)
1530
+
1531
+ # Copy templates (including moai folder)
1532
+ processor.copy_templates(backup=False, silent=True)
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
+
1554
+ # Validate template substitution
1555
+ validation_passed = _validate_template_substitution_with_rollback(project_path, backup_path)
1556
+ if not validation_passed:
1557
+ if backup_path:
1558
+ console.print(f"[yellow]๐Ÿ”„ Rolling back to backup: {backup_path.name}[/yellow]")
1559
+ backup.restore_backup(backup_path)
1560
+ return False
1561
+
1562
+ # Preserve metadata
1563
+ _preserve_project_metadata(project_path, context, existing_config, __version__)
1564
+ _apply_context_to_file(processor, project_path / "CLAUDE.md")
1565
+
1566
+ # Set optimized=false
1567
+ set_optimized_false(project_path)
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
+
1608
+ return True
1609
+ except Exception as e:
1610
+ console.print(f"[red]โœ— Template sync failed: {e}[/red]")
1611
+ if backup_path:
1612
+ console.print(f"[yellow]๐Ÿ”„ Rolling back to backup: {backup_path.name}[/yellow]")
1613
+ try:
1614
+ backup = TemplateBackup(project_path)
1615
+ backup.restore_backup(backup_path)
1616
+ console.print("[green]โœ… Rollback completed[/green]")
1617
+ except Exception as rollback_error:
1618
+ console.print(f"[red]โœ— Rollback failed: {rollback_error}[/red]")
1619
+ return False
1620
+
1621
+
1622
+ def get_latest_version() -> str | None:
1623
+ """Get the latest version from PyPI.
1624
+
1625
+ DEPRECATED: Use _get_latest_version() for new code.
1626
+ This function is kept for backward compatibility.
1627
+
1628
+ Returns:
1629
+ Latest version string, or None if fetch fails.
1630
+ """
1631
+ try:
1632
+ return _get_latest_version()
1633
+ except RuntimeError:
1634
+ # Return None if PyPI check fails (backward compatibility)
1635
+ return None
1636
+
1637
+
1638
+ def set_optimized_false(project_path: Path) -> None:
1639
+ """Set config's optimized field to false.
1640
+
1641
+ Args:
1642
+ project_path: Project path (absolute).
1643
+ """
1644
+ config_path, _ = _get_config_path(project_path)
1645
+ if not config_path.exists():
1646
+ return
1647
+
1648
+ try:
1649
+ config_data = _load_config(config_path)
1650
+ config_data.setdefault("project", {})["optimized"] = False
1651
+ _save_config(config_path, config_data)
1652
+ except (json.JSONDecodeError, yaml.YAMLError, KeyError):
1653
+ # Ignore errors if config is invalid
1654
+ pass
1655
+
1656
+
1657
+ def _load_existing_config(project_path: Path) -> dict[str, Any]:
1658
+ """Load existing config (YAML or JSON) if available."""
1659
+ config_path, _ = _get_config_path(project_path)
1660
+ if config_path.exists():
1661
+ try:
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]")
1665
+ return {}
1666
+
1667
+
1668
+ def _is_placeholder(value: Any) -> bool:
1669
+ """Check if a string value is an unsubstituted template placeholder."""
1670
+ return isinstance(value, str) and value.strip().startswith("{{") and value.strip().endswith("}}")
1671
+
1672
+
1673
+ def _coalesce(*values: Any, default: str = "") -> str:
1674
+ """Return the first non-empty, non-placeholder string value."""
1675
+ for value in values:
1676
+ if isinstance(value, str):
1677
+ if not value.strip():
1678
+ continue
1679
+ if _is_placeholder(value):
1680
+ continue
1681
+ return value
1682
+ for value in values:
1683
+ if value is not None and not isinstance(value, str):
1684
+ return str(value)
1685
+ return default
1686
+
1687
+
1688
+ def _extract_project_section(config: dict[str, Any]) -> dict[str, Any]:
1689
+ """Return the nested project section if present."""
1690
+ project_section = config.get("project")
1691
+ if isinstance(project_section, dict):
1692
+ return project_section
1693
+ return {}
1694
+
1695
+
1696
+ def _build_template_context(
1697
+ project_path: Path,
1698
+ existing_config: dict[str, Any],
1699
+ version_for_config: str,
1700
+ ) -> dict[str, str]:
1701
+ """Build substitution context for template files."""
1702
+ import platform
1703
+
1704
+ project_section = _extract_project_section(existing_config)
1705
+
1706
+ project_name = _coalesce(
1707
+ project_section.get("name"),
1708
+ existing_config.get("projectName"), # Legacy fallback
1709
+ project_path.name,
1710
+ )
1711
+ project_mode = _coalesce(
1712
+ project_section.get("mode"),
1713
+ existing_config.get("mode"), # Legacy fallback
1714
+ default="personal",
1715
+ )
1716
+ project_description = _coalesce(
1717
+ project_section.get("description"),
1718
+ existing_config.get("projectDescription"), # Legacy fallback
1719
+ existing_config.get("description"), # Legacy fallback
1720
+ )
1721
+ project_version = _coalesce(
1722
+ project_section.get("version"),
1723
+ existing_config.get("projectVersion"),
1724
+ existing_config.get("version"),
1725
+ default="0.1.0",
1726
+ )
1727
+ created_at = _coalesce(
1728
+ project_section.get("created_at"),
1729
+ existing_config.get("created_at"),
1730
+ default=datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
1731
+ )
1732
+
1733
+ # Detect OS for cross-platform Hook path configuration
1734
+ hook_project_dir = "%CLAUDE_PROJECT_DIR%" if platform.system() == "Windows" else "$CLAUDE_PROJECT_DIR"
1735
+
1736
+ # Detect OS for cross-platform statusline command
1737
+ # Windows: Use python -m for better PATH compatibility
1738
+ # Unix: Use moai-adk directly (assumes installed via uv tool)
1739
+ if platform.system() == "Windows":
1740
+ statusline_command = "python -m moai_adk statusline"
1741
+ else:
1742
+ statusline_command = "moai-adk statusline"
1743
+
1744
+ # Extract and resolve language configuration using centralized resolver
1745
+ try:
1746
+ from moai_adk.core.language_config_resolver import get_resolver
1747
+
1748
+ # Use language resolver to get complete configuration
1749
+ resolver = get_resolver(str(project_path))
1750
+ resolved_config = resolver.resolve_config()
1751
+
1752
+ # Extract language configuration with environment variable priority
1753
+ language_config = {
1754
+ "conversation_language": resolved_config.get("conversation_language", "en"),
1755
+ "conversation_language_name": resolved_config.get("conversation_language_name", "English"),
1756
+ "agent_prompt_language": resolved_config.get("agent_prompt_language", "en"),
1757
+ }
1758
+
1759
+ # Extract user personalization
1760
+ user_name = resolved_config.get("user_name", "")
1761
+ personalized_greeting = resolver.get_personalized_greeting(resolved_config)
1762
+ config_source = resolved_config.get("config_source", "config_file")
1763
+
1764
+ except ImportError:
1765
+ # Fallback to basic language config extraction if resolver not available
1766
+ language_config = existing_config.get("language", {})
1767
+ if not isinstance(language_config, dict):
1768
+ language_config = {}
1769
+
1770
+ user_name = existing_config.get("user", {}).get("name", "")
1771
+ conv_lang = language_config.get("conversation_language")
1772
+ personalized_greeting = f"{user_name}๋‹˜" if user_name and conv_lang == "ko" else user_name
1773
+ config_source = "config_file"
1774
+
1775
+ # Enhanced version formatting (matches TemplateProcessor.get_enhanced_version_context)
1776
+ def format_short_version(v: str) -> str:
1777
+ """Remove 'v' prefix if present."""
1778
+ return v[1:] if v.startswith("v") else v
1779
+
1780
+ def format_display_version(v: str) -> str:
1781
+ """Format display version with proper formatting."""
1782
+ if v == "unknown":
1783
+ return "MoAI-ADK unknown version"
1784
+ elif v.startswith("v"):
1785
+ return f"MoAI-ADK {v}"
1786
+ else:
1787
+ return f"MoAI-ADK v{v}"
1788
+
1789
+ def format_trimmed_version(v: str, max_length: int = 10) -> str:
1790
+ """Format version with maximum length for UI displays."""
1791
+ if v == "unknown":
1792
+ return "unknown"
1793
+ clean_version = v[1:] if v.startswith("v") else v
1794
+ if len(clean_version) > max_length:
1795
+ return clean_version[:max_length]
1796
+ return clean_version
1797
+
1798
+ def format_semver_version(v: str) -> str:
1799
+ """Format version as semantic version."""
1800
+ if v == "unknown":
1801
+ return "0.0.0"
1802
+ clean_version = v[1:] if v.startswith("v") else v
1803
+ import re
1804
+
1805
+ semver_match = re.match(r"^(\d+\.\d+\.\d+)", clean_version)
1806
+ if semver_match:
1807
+ return semver_match.group(1)
1808
+ return "0.0.0"
1809
+
1810
+ return {
1811
+ "MOAI_VERSION": version_for_config,
1812
+ "MOAI_VERSION_SHORT": format_short_version(version_for_config),
1813
+ "MOAI_VERSION_DISPLAY": format_display_version(version_for_config),
1814
+ "MOAI_VERSION_TRIMMED": format_trimmed_version(version_for_config),
1815
+ "MOAI_VERSION_SEMVER": format_semver_version(version_for_config),
1816
+ "MOAI_VERSION_VALID": "true" if version_for_config != "unknown" else "false",
1817
+ "MOAI_VERSION_SOURCE": "config_cached",
1818
+ "PROJECT_NAME": project_name,
1819
+ "PROJECT_MODE": project_mode,
1820
+ "PROJECT_DESCRIPTION": project_description,
1821
+ "PROJECT_VERSION": project_version,
1822
+ "CREATION_TIMESTAMP": created_at,
1823
+ "PROJECT_DIR": hook_project_dir,
1824
+ "CONVERSATION_LANGUAGE": language_config.get("conversation_language", "en"),
1825
+ "CONVERSATION_LANGUAGE_NAME": language_config.get("conversation_language_name", "English"),
1826
+ "AGENT_PROMPT_LANGUAGE": language_config.get("agent_prompt_language", "en"),
1827
+ "GIT_COMMIT_MESSAGES_LANGUAGE": language_config.get("git_commit_messages", "en"),
1828
+ "CODE_COMMENTS_LANGUAGE": language_config.get("code_comments", "en"),
1829
+ "DOCUMENTATION_LANGUAGE": language_config.get(
1830
+ "documentation", language_config.get("conversation_language", "en")
1831
+ ),
1832
+ "ERROR_MESSAGES_LANGUAGE": language_config.get(
1833
+ "error_messages", language_config.get("conversation_language", "en")
1834
+ ),
1835
+ "USER_NAME": user_name,
1836
+ "PERSONALIZED_GREETING": personalized_greeting,
1837
+ "LANGUAGE_CONFIG_SOURCE": config_source,
1838
+ "CODEBASE_LANGUAGE": project_section.get("language", "generic"),
1839
+ "PROJECT_OWNER": project_section.get("author", "@user"),
1840
+ "AUTHOR": project_section.get("author", "@user"),
1841
+ "STATUSLINE_COMMAND": statusline_command,
1842
+ }
1843
+
1844
+
1845
+ def _preserve_project_metadata(
1846
+ project_path: Path,
1847
+ context: dict[str, str],
1848
+ existing_config: dict[str, Any],
1849
+ version_for_config: str,
1850
+ ) -> None:
1851
+ """Restore project-specific metadata in the new config (YAML or JSON).
1852
+
1853
+ Also updates template_version to track which template version is synchronized.
1854
+ """
1855
+ config_path, _ = _get_config_path(project_path)
1856
+ if not config_path.exists():
1857
+ return
1858
+
1859
+ try:
1860
+ config_data = _load_config(config_path)
1861
+ except (json.JSONDecodeError, yaml.YAMLError):
1862
+ console.print("[red]โœ— Failed to parse config after template copy[/red]")
1863
+ return
1864
+
1865
+ project_data = config_data.setdefault("project", {})
1866
+ project_data["name"] = context["PROJECT_NAME"]
1867
+ project_data["mode"] = context["PROJECT_MODE"]
1868
+ project_data["description"] = context["PROJECT_DESCRIPTION"]
1869
+ project_data["created_at"] = context["CREATION_TIMESTAMP"]
1870
+
1871
+ if "optimized" not in project_data and isinstance(existing_config, dict):
1872
+ existing_project = _extract_project_section(existing_config)
1873
+ if isinstance(existing_project, dict) and "optimized" in existing_project:
1874
+ project_data["optimized"] = bool(existing_project["optimized"])
1875
+
1876
+ # Preserve locale and language preferences when possible
1877
+ existing_project = _extract_project_section(existing_config)
1878
+ locale = _coalesce(existing_project.get("locale"), existing_config.get("locale"))
1879
+ if locale:
1880
+ project_data["locale"] = locale
1881
+
1882
+ language = _coalesce(existing_project.get("language"), existing_config.get("language"))
1883
+ if language:
1884
+ project_data["language"] = language
1885
+
1886
+ config_data.setdefault("moai", {})
1887
+ config_data["moai"]["version"] = version_for_config
1888
+
1889
+ # This allows Stage 2 to compare package vs project template versions
1890
+ project_data["template_version"] = version_for_config
1891
+
1892
+ _save_config(config_path, config_data)
1893
+
1894
+
1895
+ def _apply_context_to_file(processor: TemplateProcessor, target_path: Path) -> None:
1896
+ """Apply the processor context to an existing file (post-merge pass)."""
1897
+ if not processor.context or not target_path.exists():
1898
+ return
1899
+
1900
+ try:
1901
+ content = target_path.read_text(encoding="utf-8")
1902
+ except UnicodeDecodeError:
1903
+ return
1904
+
1905
+ substituted, warnings = processor._substitute_variables(content) # pylint: disable=protected-access
1906
+ if warnings:
1907
+ console.print("[yellow]โš  Template warnings:[/yellow]")
1908
+ for warning in warnings:
1909
+ console.print(f" {warning}")
1910
+
1911
+ target_path.write_text(substituted, encoding="utf-8")
1912
+
1913
+
1914
+ def _validate_template_substitution(project_path: Path) -> None:
1915
+ """Validate that all template variables have been properly substituted."""
1916
+ import re
1917
+
1918
+ # Files to check for unsubstituted variables
1919
+ files_to_check = [
1920
+ project_path / ".claude" / "settings.json",
1921
+ project_path / "CLAUDE.md",
1922
+ ]
1923
+
1924
+ issues_found = []
1925
+
1926
+ for file_path in files_to_check:
1927
+ if not file_path.exists():
1928
+ continue
1929
+
1930
+ try:
1931
+ content = file_path.read_text(encoding="utf-8")
1932
+ # Look for unsubstituted template variables
1933
+ unsubstituted = re.findall(r"\{\{([A-Z_]+)\}\}", content)
1934
+ if unsubstituted:
1935
+ unique_vars = sorted(set(unsubstituted))
1936
+ issues_found.append(f"{file_path.relative_to(project_path)}: {', '.join(unique_vars)}")
1937
+ except Exception as e:
1938
+ console.print(f"[yellow]โš ๏ธ Could not validate {file_path.relative_to(project_path)}: {e}[/yellow]")
1939
+
1940
+ if issues_found:
1941
+ console.print("[red]โœ— Template substitution validation failed:[/red]")
1942
+ for issue in issues_found:
1943
+ console.print(f" {issue}")
1944
+ console.print("[yellow]๐Ÿ’ก Run '/moai:0-project' to fix template variables[/yellow]")
1945
+ else:
1946
+ console.print("[green]โœ… Template substitution validation passed[/green]")
1947
+
1948
+
1949
+ def _validate_template_substitution_with_rollback(project_path: Path, backup_path: Path | None) -> bool:
1950
+ """Validate template substitution with rollback capability.
1951
+
1952
+ Returns:
1953
+ True if validation passed, False if failed (rollback handled by caller)
1954
+ """
1955
+ import re
1956
+
1957
+ # Files to check for unsubstituted variables
1958
+ files_to_check = [
1959
+ project_path / ".claude" / "settings.json",
1960
+ project_path / "CLAUDE.md",
1961
+ ]
1962
+
1963
+ issues_found = []
1964
+
1965
+ for file_path in files_to_check:
1966
+ if not file_path.exists():
1967
+ continue
1968
+
1969
+ try:
1970
+ content = file_path.read_text(encoding="utf-8")
1971
+ # Look for unsubstituted template variables
1972
+ unsubstituted = re.findall(r"\{\{([A-Z_]+)\}\}", content)
1973
+ if unsubstituted:
1974
+ unique_vars = sorted(set(unsubstituted))
1975
+ issues_found.append(f"{file_path.relative_to(project_path)}: {', '.join(unique_vars)}")
1976
+ except Exception as e:
1977
+ console.print(f"[yellow]โš ๏ธ Could not validate {file_path.relative_to(project_path)}: {e}[/yellow]")
1978
+
1979
+ if issues_found:
1980
+ console.print("[red]โœ— Template substitution validation failed:[/red]")
1981
+ for issue in issues_found:
1982
+ console.print(f" {issue}")
1983
+
1984
+ if backup_path:
1985
+ console.print("[yellow]๐Ÿ”„ Rolling back due to validation failure...[/yellow]")
1986
+ else:
1987
+ console.print("[yellow]๐Ÿ’ก Run '/moai:0-project' to fix template variables[/yellow]")
1988
+ console.print("[red]โš ๏ธ No backup available - manual fix required[/red]")
1989
+
1990
+ return False
1991
+ else:
1992
+ console.print("[green]โœ… Template substitution validation passed[/green]")
1993
+ return True
1994
+
1995
+
1996
+ def _show_version_info(current: str, latest: str) -> None:
1997
+ """Display version information.
1998
+
1999
+ Args:
2000
+ current: Current installed version
2001
+ latest: Latest available version
2002
+ """
2003
+ console.print("[cyan]๐Ÿ” Checking versions...[/cyan]")
2004
+ console.print(f" Current version: {current}")
2005
+ console.print(f" Latest version: {latest}")
2006
+
2007
+
2008
+ def _show_installer_not_found_help() -> None:
2009
+ """Show help when installer not found."""
2010
+ console.print("[red]โŒ Cannot detect package installer[/red]\n")
2011
+ console.print("Installation method not detected. To update manually:\n")
2012
+ console.print(" โ€ข If installed via uv tool:")
2013
+ console.print(" [cyan]uv tool upgrade moai-adk[/cyan]\n")
2014
+ console.print(" โ€ข If installed via pipx:")
2015
+ console.print(" [cyan]pipx upgrade moai-adk[/cyan]\n")
2016
+ console.print(" โ€ข If installed via pip:")
2017
+ console.print(" [cyan]pip install --upgrade moai-adk[/cyan]\n")
2018
+ console.print("Then run:")
2019
+ console.print(" [cyan]moai-adk update --templates-only[/cyan]")
2020
+
2021
+
2022
+ def _show_upgrade_failure_help(installer_cmd: list[str]) -> None:
2023
+ """Show help when upgrade fails.
2024
+
2025
+ Args:
2026
+ installer_cmd: The installer command that failed
2027
+ """
2028
+ console.print("[red]โŒ Upgrade failed[/red]\n")
2029
+ console.print("Troubleshooting:")
2030
+ console.print(" 1. Check network connection")
2031
+ console.print(f" 2. Clear cache: {installer_cmd[0]} cache clean")
2032
+ console.print(f" 3. Try manually: {' '.join(installer_cmd)}")
2033
+ console.print(" 4. Report issue: https://github.com/modu-ai/moai-adk/issues")
2034
+
2035
+
2036
+ def _show_network_error_help() -> None:
2037
+ """Show help for network errors."""
2038
+ console.print("[yellow]โš ๏ธ Cannot reach PyPI to check latest version[/yellow]\n")
2039
+ console.print("Options:")
2040
+ console.print(" 1. Check network connection")
2041
+ console.print(" 2. Try again with: [cyan]moai-adk update --force[/cyan]")
2042
+ console.print(" 3. Skip version check: [cyan]moai-adk update --templates-only[/cyan]")
2043
+
2044
+
2045
+ def _show_template_sync_failure_help() -> None:
2046
+ """Show help when template sync fails."""
2047
+ console.print("[yellow]โš ๏ธ Template sync failed[/yellow]\n")
2048
+ console.print("Rollback options:")
2049
+ console.print(" 1. Restore from backup: [cyan]cp -r .moai-backups/TIMESTAMP .moai/[/cyan]")
2050
+ console.print(" 2. Skip backup and retry: [cyan]moai-adk update --force[/cyan]")
2051
+ console.print(" 3. Report issue: https://github.com/modu-ai/moai-adk/issues")
2052
+
2053
+
2054
+ def _show_timeout_error_help() -> None:
2055
+ """Show help for timeout errors."""
2056
+ console.print("[red]โŒ Error: Operation timed out[/red]\n")
2057
+ console.print("Try again with:")
2058
+ console.print(" [cyan]moai-adk update --yes --force[/cyan]")
2059
+
2060
+
2061
+ def _execute_migration_if_needed(project_path: Path, yes: bool = False) -> bool:
2062
+ """Check and execute migration if needed.
2063
+
2064
+ Args:
2065
+ project_path: Project directory path
2066
+ yes: Auto-confirm without prompting
2067
+
2068
+ Returns:
2069
+ True if no migration needed or migration succeeded, False if migration failed
2070
+ """
2071
+ try:
2072
+ migrator = VersionMigrator(project_path)
2073
+
2074
+ # Check if migration is needed
2075
+ if not migrator.needs_migration():
2076
+ return True
2077
+
2078
+ # Get migration info
2079
+ info = migrator.get_migration_info()
2080
+ console.print("\n[cyan]๐Ÿ”„ Migration Required[/cyan]")
2081
+ console.print(f" Current version: {info['current_version']}")
2082
+ console.print(f" Target version: {info['target_version']}")
2083
+ console.print(f" Files to migrate: {info['file_count']}")
2084
+ console.print()
2085
+ console.print(" This will migrate configuration files to new locations:")
2086
+ console.print(" โ€ข .moai/config.json โ†’ .moai/config/config.json")
2087
+ console.print(" โ€ข .claude/statusline-config.yaml โ†’ .moai/config/statusline-config.yaml")
2088
+ console.print()
2089
+ console.print(" A backup will be created automatically.")
2090
+ console.print()
2091
+
2092
+ # Confirm with user (unless --yes)
2093
+ if not yes:
2094
+ if not click.confirm("Do you want to proceed with migration?", default=True):
2095
+ console.print("[yellow]โš ๏ธ Migration skipped. Some features may not work correctly.[/yellow]")
2096
+ console.print("[cyan]๐Ÿ’ก Run 'moai-adk migrate' manually when ready[/cyan]")
2097
+ return False
2098
+
2099
+ # Execute migration
2100
+ console.print("[cyan]๐Ÿš€ Starting migration...[/cyan]")
2101
+ success = migrator.migrate_to_v024(dry_run=False, cleanup=True)
2102
+
2103
+ if success:
2104
+ console.print("[green]โœ… Migration completed successfully![/green]")
2105
+ return True
2106
+ else:
2107
+ console.print("[red]โŒ Migration failed[/red]")
2108
+ console.print("[cyan]๐Ÿ’ก Use 'moai-adk migrate --rollback' to restore from backup[/cyan]")
2109
+ return False
2110
+
2111
+ except Exception as e:
2112
+ console.print(f"[red]โŒ Migration error: {e}[/red]")
2113
+ logger.error(f"Migration failed: {e}", exc_info=True)
2114
+ return False
2115
+
2116
+
2117
+ @click.command()
2118
+ @click.option(
2119
+ "--path",
2120
+ type=click.Path(exists=True),
2121
+ default=".",
2122
+ help="Project path (default: current directory)",
2123
+ )
2124
+ @click.option("--force", is_flag=True, help="Skip backup and force the update")
2125
+ @click.option("--check", is_flag=True, help="Only check version (do not update)")
2126
+ @click.option("--templates-only", is_flag=True, help="Skip package upgrade, sync templates only")
2127
+ @click.option("--yes", is_flag=True, help="Auto-confirm all prompts (CI/CD mode)")
2128
+ @click.option(
2129
+ "--merge",
2130
+ "merge_strategy",
2131
+ flag_value="auto",
2132
+ help="Auto-merge: Apply template + preserve user changes",
2133
+ )
2134
+ @click.option(
2135
+ "--manual",
2136
+ "merge_strategy",
2137
+ flag_value="manual",
2138
+ help="Manual merge: Preserve backup, generate merge guide",
2139
+ )
2140
+ def update(
2141
+ path: str,
2142
+ force: bool,
2143
+ check: bool,
2144
+ templates_only: bool,
2145
+ yes: bool,
2146
+ merge_strategy: str | None,
2147
+ ) -> None:
2148
+ """Update command with 3-stage workflow + merge strategy selection (v0.26.0+).
2149
+
2150
+ Stage 1 (Package Version Check):
2151
+ - Fetches current and latest versions from PyPI
2152
+ - If current < latest: detects installer (uv tool, pipx, pip) and upgrades package
2153
+ - Prompts user to re-run after upgrade completes
2154
+
2155
+ Stage 2 (Config Version Comparison - NEW in v0.6.3):
2156
+ - Compares package template_version with project config.json template_version
2157
+ - If versions match: skips Stage 3 (already up-to-date)
2158
+ - Performance improvement: 70-80% faster for unchanged projects (3-4s vs 12-18s)
2159
+
2160
+ Stage 3 (Template Sync with Merge Strategy - NEW in v0.26.0):
2161
+ - Syncs templates only if versions differ
2162
+ - User chooses merge strategy:
2163
+ * Auto-merge (default): Template + preserved user changes
2164
+ * Manual merge: Backup + comprehensive merge guide (full control)
2165
+ - Updates .claude/, .moai/, CLAUDE.md, config.json
2166
+ - Preserves specs and reports
2167
+ - Saves new template_version to config.json
2168
+
2169
+ Examples:
2170
+ python -m moai_adk update # interactive merge strategy selection
2171
+ python -m moai_adk update --merge # auto-merge (template + user changes)
2172
+ python -m moai_adk update --manual # manual merge (backup + guide)
2173
+ python -m moai_adk update --force # force template sync (no backup)
2174
+ python -m moai_adk update --check # check version only
2175
+ python -m moai_adk update --templates-only # skip package upgrade
2176
+ python -m moai_adk update --yes # CI/CD mode (auto-confirm + auto-merge)
2177
+
2178
+ Merge Strategies:
2179
+ --merge: Auto-merge applies template + preserves your changes (default)
2180
+ Generated files: backup, merge report
2181
+ --manual: Manual merge preserves backup + generates comprehensive guide
2182
+ Generated files: backup, merge guide
2183
+
2184
+ Generated Files:
2185
+ - Backup: .moai-backups/pre-update-backup_{timestamp}/
2186
+ - Report: .moai/reports/merge-report.md (auto-merge only)
2187
+ - Guide: .moai/guides/merge-guide.md (manual merge only)
2188
+ """
2189
+ try:
2190
+ project_path = Path(path).resolve()
2191
+
2192
+ # Verify the project is initialized
2193
+ if not (project_path / ".moai").exists():
2194
+ console.print("[yellow]โš  Project not initialized[/yellow]")
2195
+ raise click.Abort()
2196
+
2197
+ # Get versions (needed for --check and normal workflow, but not for --templates-only alone)
2198
+ # Note: If --check is used, always fetch versions even if --templates-only is also present
2199
+ if check or not templates_only:
2200
+ try:
2201
+ # Try to use new spinner UI
2202
+ try:
2203
+ from moai_adk.cli.ui.progress import SpinnerContext
2204
+
2205
+ with SpinnerContext("Checking for updates...") as spinner:
2206
+ current = _get_current_version()
2207
+ spinner.update("Fetching latest version from PyPI...")
2208
+ latest = _get_latest_version()
2209
+ spinner.success("Version check complete")
2210
+ except ImportError:
2211
+ # Fallback to simple console output
2212
+ console.print("[dim]Checking for updates...[/dim]")
2213
+ current = _get_current_version()
2214
+ latest = _get_latest_version()
2215
+ except RuntimeError as e:
2216
+ console.print(f"[red]Error: {e}[/red]")
2217
+ if not force:
2218
+ console.print("[yellow]โš  Cannot check for updates. Use --force to update anyway.[/yellow]")
2219
+ raise click.Abort()
2220
+ # With --force, proceed to Stage 2 even if version check fails
2221
+ current = __version__
2222
+ latest = __version__
2223
+
2224
+ _show_version_info(current, latest)
2225
+
2226
+ # Step 1: Handle --check (preview mode, no changes) - takes priority
2227
+ if check:
2228
+ comparison = _compare_versions(current, latest)
2229
+ if comparison < 0:
2230
+ console.print(f"\n[yellow]๐Ÿ“ฆ Update available: {current} โ†’ {latest}[/yellow]")
2231
+ console.print(" Run 'moai-adk update' to upgrade")
2232
+ elif comparison == 0:
2233
+ console.print(f"[green]โœ“ Already up to date ({current})[/green]")
2234
+ else:
2235
+ console.print(f"[cyan]โ„น๏ธ Dev version: {current} (latest: {latest})[/cyan]")
2236
+ return
2237
+
2238
+ # Step 2: Handle --templates-only (skip upgrade, go straight to sync)
2239
+ if templates_only:
2240
+ console.print("[cyan]๐Ÿ“„ Syncing templates only...[/cyan]")
2241
+
2242
+ # Preserve user-specific settings before sync
2243
+ console.print(" [cyan]๐Ÿ’พ Preserving user settings...[/cyan]")
2244
+ preserved_settings = _preserve_user_settings(project_path)
2245
+
2246
+ try:
2247
+ if not _sync_templates(project_path, force, yes):
2248
+ raise TemplateSyncError("Template sync returned False")
2249
+ except TemplateSyncError:
2250
+ console.print("[red]Error: Template sync failed[/red]")
2251
+ _show_template_sync_failure_help()
2252
+ raise click.Abort()
2253
+ except Exception as e:
2254
+ console.print(f"[red]Error: Template sync failed - {e}[/red]")
2255
+ _show_template_sync_failure_help()
2256
+ raise click.Abort()
2257
+
2258
+ # Restore user-specific settings after sync
2259
+ _restore_user_settings(project_path, preserved_settings)
2260
+
2261
+ console.print(" [green]โœ… .claude/ update complete[/green]")
2262
+ console.print(" [green]โœ… .moai/ update complete (specs/reports preserved)[/green]")
2263
+ console.print(" [green]๐Ÿ”„ CLAUDE.md merge complete[/green]")
2264
+ console.print(" [green]๐Ÿ”„ config.json merge complete[/green]")
2265
+ console.print("\n[green]โœ“ Template sync complete![/green]")
2266
+ return
2267
+
2268
+ # Compare versions
2269
+ comparison = _compare_versions(current, latest)
2270
+
2271
+ # Stage 1: Package Upgrade (if current < latest)
2272
+ if comparison < 0:
2273
+ console.print(f"\n[cyan]๐Ÿ“ฆ Upgrading: {current} โ†’ {latest}[/cyan]")
2274
+
2275
+ # Confirm upgrade (unless --yes)
2276
+ if not yes:
2277
+ if not click.confirm(f"Upgrade {current} โ†’ {latest}?", default=True):
2278
+ console.print("Cancelled")
2279
+ return
2280
+
2281
+ # Detect installer
2282
+ try:
2283
+ installer_cmd = _detect_tool_installer()
2284
+ if not installer_cmd:
2285
+ raise InstallerNotFoundError("No package installer detected")
2286
+ except InstallerNotFoundError:
2287
+ _show_installer_not_found_help()
2288
+ raise click.Abort()
2289
+
2290
+ # Display upgrade command
2291
+ console.print(f"Running: {' '.join(installer_cmd)}")
2292
+
2293
+ # Execute upgrade with timeout handling
2294
+ try:
2295
+ upgrade_result = _execute_upgrade(installer_cmd)
2296
+ if not upgrade_result:
2297
+ raise UpgradeError(f"Upgrade command failed: {' '.join(installer_cmd)}")
2298
+ except subprocess.TimeoutExpired:
2299
+ _show_timeout_error_help()
2300
+ raise click.Abort()
2301
+ except UpgradeError:
2302
+ _show_upgrade_failure_help(installer_cmd)
2303
+ raise click.Abort()
2304
+
2305
+ # Prompt re-run
2306
+ console.print("\n[green]โœ“ Upgrade complete![/green]")
2307
+ console.print("[cyan]๐Ÿ“ข Run 'moai-adk update' again to sync templates[/cyan]")
2308
+ return
2309
+
2310
+ # Stage 1.5: Migration Check (NEW in v0.24.0)
2311
+ console.print(f"โœ“ Package already up to date ({current})")
2312
+
2313
+ # Execute migration if needed
2314
+ if not _execute_migration_if_needed(project_path, yes):
2315
+ console.print("[yellow]โš ๏ธ Update continuing without migration[/yellow]")
2316
+ console.print("[cyan]๐Ÿ’ก Some features may require migration to work correctly[/cyan]")
2317
+
2318
+ # Migrate config.json โ†’ config.yaml (v0.32.0+)
2319
+ console.print("\n[cyan]๐Ÿ” Checking for config format migration...[/cyan]")
2320
+ if not _migrate_config_json_to_yaml(project_path):
2321
+ console.print("[yellow]โš ๏ธ Config migration failed, continuing with existing format[/yellow]")
2322
+
2323
+ # Stage 2: Config Version Comparison
2324
+ try:
2325
+ package_config_version = _get_package_config_version()
2326
+ project_config_version = _get_project_config_version(project_path)
2327
+ except ValueError as e:
2328
+ console.print(f"[yellow]โš  Warning: {e}[/yellow]")
2329
+ # On version detection error, proceed with template sync (safer choice)
2330
+ package_config_version = __version__
2331
+ project_config_version = "0.0.0"
2332
+
2333
+ console.print("\n[cyan]๐Ÿ” Comparing config versions...[/cyan]")
2334
+ console.print(f" Package template: {package_config_version}")
2335
+ console.print(f" Project config: {project_config_version}")
2336
+
2337
+ try:
2338
+ config_comparison = _compare_versions(package_config_version, project_config_version)
2339
+ except version.InvalidVersion as e:
2340
+ # Handle invalid version strings (e.g., unsubstituted template placeholders, corrupted configs)
2341
+ console.print(f"[yellow]โš  Invalid version format in config: {e}[/yellow]")
2342
+ console.print("[cyan]โ„น๏ธ Forcing template sync to repair configuration...[/cyan]")
2343
+ # Force template sync by treating project version as outdated
2344
+ config_comparison = 1 # package_config_version > project_config_version
2345
+
2346
+ # If versions are equal, no sync needed
2347
+ if config_comparison <= 0:
2348
+ console.print(f"\n[green]โœ“ Project already has latest template version ({project_config_version})[/green]")
2349
+ console.print("[cyan]โ„น๏ธ Templates are up to date! No changes needed.[/cyan]")
2350
+ return
2351
+
2352
+ # Stage 3: Template Sync (Only if package_config_version > project_config_version)
2353
+ console.print(f"\n[cyan]๐Ÿ“„ Syncing templates ({project_config_version} โ†’ {package_config_version})...[/cyan]")
2354
+
2355
+ # Determine merge strategy (default: auto-merge)
2356
+ final_merge_strategy = merge_strategy or "auto"
2357
+
2358
+ # Handle merge strategy
2359
+ if final_merge_strategy == "manual":
2360
+ # Manual merge mode: Create full backup + generate guide, no template sync
2361
+ console.print("\n[cyan]๐Ÿ”€ Manual merge mode selected[/cyan]")
2362
+
2363
+ # Create full project backup
2364
+ console.print(" [cyan]๐Ÿ’พ Creating full project backup...[/cyan]")
2365
+ try:
2366
+ from moai_adk.core.migration.backup_manager import BackupManager
2367
+
2368
+ backup_manager = BackupManager(project_path)
2369
+ full_backup_path = backup_manager.create_full_project_backup(description="pre-update-backup")
2370
+ console.print(f" [green]โœ“ Backup: {full_backup_path.relative_to(project_path)}/[/green]")
2371
+
2372
+ # Generate merge guide
2373
+ console.print(" [cyan]๐Ÿ“‹ Generating merge guide...[/cyan]")
2374
+ template_path = Path(__file__).parent.parent.parent / "templates"
2375
+ guide_path = _generate_manual_merge_guide(full_backup_path, template_path, project_path)
2376
+ console.print(f" [green]โœ“ Guide: {guide_path.relative_to(project_path)}[/green]")
2377
+
2378
+ # Summary
2379
+ console.print("\n[green]โœ“ Manual merge setup complete![/green]")
2380
+ console.print(f"[cyan]๐Ÿ“ Backup location: {full_backup_path.relative_to(project_path)}/[/cyan]")
2381
+ console.print(f"[cyan]๐Ÿ“‹ Merge guide: {guide_path.relative_to(project_path)}[/cyan]")
2382
+ console.print("\n[yellow]โš ๏ธ Next steps:[/yellow]")
2383
+ console.print("[yellow] 1. Review the merge guide[/yellow]")
2384
+ console.print("[yellow] 2. Compare files using diff or visual tools[/yellow]")
2385
+ console.print("[yellow] 3. Manually merge your customizations[/yellow]")
2386
+ console.print("[yellow] 4. Test and commit changes[/yellow]")
2387
+
2388
+ except Exception as e:
2389
+ console.print(f"[red]Error: Manual merge setup failed - {e}[/red]")
2390
+ raise click.Abort()
2391
+
2392
+ return
2393
+
2394
+ # Auto merge mode: Preserve user-specific settings before sync
2395
+ console.print("\n[cyan]๐Ÿ”€ Auto-merge mode selected[/cyan]")
2396
+ console.print(" [cyan]๐Ÿ’พ Preserving user settings...[/cyan]")
2397
+ preserved_settings = _preserve_user_settings(project_path)
2398
+
2399
+ # Create backup unless --force
2400
+ if not force:
2401
+ console.print(" [cyan]๐Ÿ’พ Creating backup...[/cyan]")
2402
+ try:
2403
+ processor = TemplateProcessor(project_path)
2404
+ backup_path = processor.create_backup()
2405
+ console.print(f" [green]โœ“ Backup: {backup_path.relative_to(project_path)}/[/green]")
2406
+ except Exception as e:
2407
+ console.print(f" [yellow]โš  Backup failed: {e}[/yellow]")
2408
+ console.print(" [yellow]โš  Continuing without backup...[/yellow]")
2409
+ else:
2410
+ console.print(" [yellow]โš  Skipping backup (--force)[/yellow]")
2411
+
2412
+ # Sync templates (NO spinner - user interaction may be required)
2413
+ # SpinnerContext blocks stdin, causing hang when click.confirm() is called
2414
+ try:
2415
+ console.print(" [cyan]Syncing templates...[/cyan]")
2416
+ if not _sync_templates(project_path, force, yes):
2417
+ raise TemplateSyncError("Template sync returned False")
2418
+ _restore_user_settings(project_path, preserved_settings)
2419
+ console.print(" [green]โœ“ Template sync complete[/green]")
2420
+ except TemplateSyncError:
2421
+ console.print("[red]Error: Template sync failed[/red]")
2422
+ _show_template_sync_failure_help()
2423
+ raise click.Abort()
2424
+ except Exception as e:
2425
+ console.print(f"[red]Error: Template sync failed - {e}[/red]")
2426
+ _show_template_sync_failure_help()
2427
+ raise click.Abort()
2428
+
2429
+ console.print(" [green]โœ… .claude/ update complete[/green]")
2430
+ console.print(" [green]โœ… .moai/ update complete (specs/reports preserved)[/green]")
2431
+ console.print(" [green]๐Ÿ”„ CLAUDE.md merge complete[/green]")
2432
+ console.print(" [green]๐Ÿ”„ config.json merge complete[/green]")
2433
+ console.print(" [yellow]โš™๏ธ Set optimized=false (optimization needed)[/yellow]")
2434
+
2435
+ console.print("\n[green]โœ“ Update complete![/green]")
2436
+ console.print("[cyan]โ„น๏ธ Next step: Run /moai:0-project update to optimize template changes[/cyan]")
2437
+
2438
+ except Exception as e:
2439
+ console.print(f"[red]โœ— Update failed: {e}[/red]")
2440
+ raise click.ClickException(str(e)) from e
2441
+
2442
+
2443
+ def _handle_custom_element_restoration(project_path: Path, backup_path: Path | None, yes: bool = False) -> None:
2444
+ """Handle custom element restoration using the enhanced system.
2445
+
2446
+ This function provides an improved interface for restoring user-created custom elements
2447
+ (agents, commands, skills, hooks) from backup during MoAI-ADK updates.
2448
+
2449
+ Key improvements:
2450
+ - Preserves unselected elements (fixes disappearing issue)
2451
+ - Only overwrites/creates selected elements from backup
2452
+ - Interactive checkbox selection with arrow key navigation
2453
+ - Includes all categories (Agents, Commands, Skills, Hooks)
2454
+
2455
+ Args:
2456
+ project_path: Path to the MoAI-ADK project directory
2457
+ backup_path: Path to the backup directory (None if no backup)
2458
+ yes: Whether to automatically accept defaults (non-interactive mode)
2459
+ """
2460
+ if not backup_path:
2461
+ # No backup available, cannot restore
2462
+ return
2463
+
2464
+ try:
2465
+ # Create scanner to find custom elements in backup (not current project)
2466
+ backup_scanner = create_custom_element_scanner(backup_path)
2467
+
2468
+ # Get count of custom elements in backup
2469
+ backup_element_count = backup_scanner.get_element_count()
2470
+
2471
+ if backup_element_count == 0:
2472
+ # No custom elements found in backup
2473
+ console.print("[green]โœ“ No custom elements found in backup to restore[/green]")
2474
+ return
2475
+
2476
+ # Create enhanced user selection UI
2477
+ ui = create_user_selection_ui(project_path)
2478
+
2479
+ console.print(f"\n[cyan]๐Ÿ” Found {backup_element_count} custom elements in backup[/cyan]")
2480
+
2481
+ # If yes mode is enabled, restore all elements automatically
2482
+ if yes:
2483
+ console.print(f"[cyan]๐Ÿ”„ Auto-restoring {backup_element_count} custom elements...[/cyan]")
2484
+ backup_custom_elements = backup_scanner.scan_custom_elements()
2485
+ selected_elements = []
2486
+
2487
+ # Collect all element paths from backup
2488
+ for element_type, elements in backup_custom_elements.items():
2489
+ if element_type == "skills":
2490
+ for skill in elements:
2491
+ selected_elements.append(str(skill.path))
2492
+ else:
2493
+ for element_path in elements:
2494
+ selected_elements.append(str(element_path))
2495
+ else:
2496
+ # Interactive mode - prompt user for selection using enhanced UI
2497
+ selected_elements = ui.prompt_user_selection(backup_available=True)
2498
+
2499
+ if not selected_elements:
2500
+ console.print("[yellow]โš  No elements selected for restoration[/yellow]")
2501
+ console.print("[green]โœ“ All existing custom elements will be preserved[/green]")
2502
+ return
2503
+
2504
+ # Confirm selection
2505
+ if not ui.confirm_selection(selected_elements):
2506
+ console.print("[yellow]โš  Restoration cancelled by user[/yellow]")
2507
+ console.print("[green]โœ“ All existing custom elements will be preserved[/green]")
2508
+ return
2509
+
2510
+ # Perform selective restoration - ONLY restore selected elements
2511
+ if selected_elements:
2512
+ console.print(f"[cyan]๐Ÿ”„ Restoring {len(selected_elements)} selected elements from backup...[/cyan]")
2513
+ restorer = create_selective_restorer(project_path, backup_path)
2514
+ success, stats = restorer.restore_elements(selected_elements)
2515
+
2516
+ if success:
2517
+ console.print(f"[green]โœ… Successfully restored {stats['success']} custom elements[/green]")
2518
+ console.print("[green]โœ“ All unselected elements remain preserved[/green]")
2519
+ else:
2520
+ console.print(f"[yellow]โš ๏ธ Partial restoration: {stats['success']}/{stats['total']} elements[/yellow]")
2521
+ if stats["failed"] > 0:
2522
+ console.print(f"[red]โŒ Failed to restore {stats['failed']} elements[/red]")
2523
+ console.print("[yellow]โš ๏ธ All other elements remain preserved[/yellow]")
2524
+ else:
2525
+ console.print("[green]โœ“ No elements selected, all custom elements preserved[/green]")
2526
+
2527
+ except Exception as e:
2528
+ console.print(f"[yellow]โš ๏ธ Custom element restoration failed: {e}[/yellow]")
2529
+ logger.warning(f"Custom element restoration error: {e}")
2530
+ console.print("[yellow]โš ๏ธ All existing custom elements remain as-is[/yellow]")
2531
+ # Don't fail the entire update process, just log the error
2532
+ pass
2533
+
2534
+
2535
+ def _cleanup_legacy_presets(project_path: Path) -> None:
2536
+ """Remove legacy presets directory entirely.
2537
+
2538
+ This function removes the entire .moai/config/presets/ directory as it is
2539
+ no longer used. All preset settings are now consolidated in sections/git-strategy.yaml.
2540
+
2541
+ Args:
2542
+ project_path: Project directory path (absolute)
2543
+ """
2544
+ import shutil
2545
+
2546
+ presets_dir = project_path / ".moai" / "config" / "presets"
2547
+
2548
+ if not presets_dir.exists() or not presets_dir.is_dir():
2549
+ return
2550
+
2551
+ try:
2552
+ # Remove entire presets directory (no longer needed)
2553
+ shutil.rmtree(presets_dir)
2554
+ console.print(" [cyan]๐Ÿงน Removed legacy presets directory (now in sections/git-strategy.yaml)[/cyan]")
2555
+ logger.info(f"Removed legacy presets directory: {presets_dir}")
2556
+ except Exception as e:
2557
+ logger.warning(f"Failed to remove legacy presets directory {presets_dir}: {e}")
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")