moai-adk 0.3.0__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.
- moai_adk/__init__.py +8 -0
- moai_adk/__main__.py +86 -0
- moai_adk/cli/__init__.py +2 -0
- moai_adk/cli/commands/__init__.py +16 -0
- moai_adk/cli/commands/backup.py +56 -0
- moai_adk/cli/commands/doctor.py +184 -0
- moai_adk/cli/commands/init.py +284 -0
- moai_adk/cli/commands/restore.py +77 -0
- moai_adk/cli/commands/status.py +79 -0
- moai_adk/cli/commands/update.py +133 -0
- moai_adk/cli/main.py +12 -0
- moai_adk/cli/prompts/__init__.py +5 -0
- moai_adk/cli/prompts/init_prompts.py +159 -0
- moai_adk/core/__init__.py +2 -0
- moai_adk/core/git/__init__.py +24 -0
- moai_adk/core/git/branch.py +26 -0
- moai_adk/core/git/branch_manager.py +137 -0
- moai_adk/core/git/checkpoint.py +140 -0
- moai_adk/core/git/commit.py +68 -0
- moai_adk/core/git/event_detector.py +81 -0
- moai_adk/core/git/manager.py +127 -0
- moai_adk/core/project/__init__.py +2 -0
- moai_adk/core/project/backup_utils.py +84 -0
- moai_adk/core/project/checker.py +302 -0
- moai_adk/core/project/detector.py +105 -0
- moai_adk/core/project/initializer.py +174 -0
- moai_adk/core/project/phase_executor.py +297 -0
- moai_adk/core/project/validator.py +118 -0
- moai_adk/core/quality/__init__.py +6 -0
- moai_adk/core/quality/trust_checker.py +441 -0
- moai_adk/core/quality/validators/__init__.py +6 -0
- moai_adk/core/quality/validators/base_validator.py +19 -0
- moai_adk/core/template/__init__.py +8 -0
- moai_adk/core/template/backup.py +95 -0
- moai_adk/core/template/config.py +95 -0
- moai_adk/core/template/languages.py +44 -0
- moai_adk/core/template/merger.py +117 -0
- moai_adk/core/template/processor.py +310 -0
- moai_adk/templates/.claude/agents/alfred/cc-manager.md +474 -0
- moai_adk/templates/.claude/agents/alfred/code-builder.md +534 -0
- moai_adk/templates/.claude/agents/alfred/debug-helper.md +302 -0
- moai_adk/templates/.claude/agents/alfred/doc-syncer.md +175 -0
- moai_adk/templates/.claude/agents/alfred/git-manager.md +200 -0
- moai_adk/templates/.claude/agents/alfred/project-manager.md +152 -0
- moai_adk/templates/.claude/agents/alfred/spec-builder.md +256 -0
- moai_adk/templates/.claude/agents/alfred/tag-agent.md +247 -0
- moai_adk/templates/.claude/agents/alfred/trust-checker.md +332 -0
- moai_adk/templates/.claude/commands/alfred/0-project.md +523 -0
- moai_adk/templates/.claude/commands/alfred/1-spec.md +531 -0
- moai_adk/templates/.claude/commands/alfred/2-build.md +413 -0
- moai_adk/templates/.claude/commands/alfred/3-sync.md +552 -0
- moai_adk/templates/.claude/hooks/alfred/README.md +238 -0
- moai_adk/templates/.claude/hooks/alfred/alfred_hooks.py +165 -0
- moai_adk/templates/.claude/hooks/alfred/core/__init__.py +79 -0
- moai_adk/templates/.claude/hooks/alfred/core/checkpoint.py +271 -0
- moai_adk/templates/.claude/hooks/alfred/core/context.py +110 -0
- moai_adk/templates/.claude/hooks/alfred/core/project.py +284 -0
- moai_adk/templates/.claude/hooks/alfred/core/tags.py +244 -0
- moai_adk/templates/.claude/hooks/alfred/handlers/__init__.py +23 -0
- moai_adk/templates/.claude/hooks/alfred/handlers/compact.py +51 -0
- moai_adk/templates/.claude/hooks/alfred/handlers/notification.py +25 -0
- moai_adk/templates/.claude/hooks/alfred/handlers/session.py +80 -0
- moai_adk/templates/.claude/hooks/alfred/handlers/tool.py +71 -0
- moai_adk/templates/.claude/hooks/alfred/handlers/user.py +41 -0
- moai_adk/templates/.claude/output-styles/alfred/agentic-coding.md +635 -0
- moai_adk/templates/.claude/output-styles/alfred/moai-adk-learning.md +691 -0
- moai_adk/templates/.claude/output-styles/alfred/study-with-alfred.md +469 -0
- moai_adk/templates/.claude/settings.json +135 -0
- moai_adk/templates/.github/PULL_REQUEST_TEMPLATE.md +68 -0
- moai_adk/templates/.github/workflows/moai-gitflow.yml +255 -0
- moai_adk/templates/.gitignore +41 -0
- moai_adk/templates/.moai/config.json +89 -0
- moai_adk/templates/.moai/memory/development-guide.md +367 -0
- moai_adk/templates/.moai/memory/spec-metadata.md +277 -0
- moai_adk/templates/.moai/project/product.md +121 -0
- moai_adk/templates/.moai/project/structure.md +150 -0
- moai_adk/templates/.moai/project/tech.md +221 -0
- moai_adk/templates/CLAUDE.md +733 -0
- moai_adk/templates/__init__.py +2 -0
- moai_adk/utils/__init__.py +8 -0
- moai_adk/utils/banner.py +42 -0
- moai_adk/utils/logger.py +152 -0
- moai_adk-0.3.0.dist-info/METADATA +20 -0
- moai_adk-0.3.0.dist-info/RECORD +87 -0
- moai_adk-0.3.0.dist-info/WHEEL +4 -0
- moai_adk-0.3.0.dist-info/entry_points.txt +2 -0
- moai_adk-0.3.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# @CODE:CORE-PROJECT-001 | SPEC: SPEC-CORE-PROJECT-001.md | TEST: tests/unit/test_language_mapping.py
|
|
2
|
+
"""Template mapping by language.
|
|
3
|
+
|
|
4
|
+
Defines template paths for 20 programming languages.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
LANGUAGE_TEMPLATES: dict[str, str] = {
|
|
8
|
+
"python": ".moai/project/tech/python.md.j2",
|
|
9
|
+
"typescript": ".moai/project/tech/typescript.md.j2",
|
|
10
|
+
"javascript": ".moai/project/tech/javascript.md.j2",
|
|
11
|
+
"java": ".moai/project/tech/java.md.j2",
|
|
12
|
+
"go": ".moai/project/tech/go.md.j2",
|
|
13
|
+
"rust": ".moai/project/tech/rust.md.j2",
|
|
14
|
+
"dart": ".moai/project/tech/dart.md.j2",
|
|
15
|
+
"swift": ".moai/project/tech/swift.md.j2",
|
|
16
|
+
"kotlin": ".moai/project/tech/kotlin.md.j2",
|
|
17
|
+
"csharp": ".moai/project/tech/csharp.md.j2",
|
|
18
|
+
"php": ".moai/project/tech/php.md.j2",
|
|
19
|
+
"ruby": ".moai/project/tech/ruby.md.j2",
|
|
20
|
+
"elixir": ".moai/project/tech/elixir.md.j2",
|
|
21
|
+
"scala": ".moai/project/tech/scala.md.j2",
|
|
22
|
+
"clojure": ".moai/project/tech/clojure.md.j2",
|
|
23
|
+
"haskell": ".moai/project/tech/haskell.md.j2",
|
|
24
|
+
"c": ".moai/project/tech/c.md.j2",
|
|
25
|
+
"cpp": ".moai/project/tech/cpp.md.j2",
|
|
26
|
+
"lua": ".moai/project/tech/lua.md.j2",
|
|
27
|
+
"ocaml": ".moai/project/tech/ocaml.md.j2",
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def get_language_template(language: str) -> str:
|
|
32
|
+
"""Return the template path for a language (case-insensitive).
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
language: Language name (case-insensitive).
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
Template path; defaults to default.md.j2 for unknown languages.
|
|
39
|
+
"""
|
|
40
|
+
if not language:
|
|
41
|
+
return ".moai/project/tech/default.md.j2"
|
|
42
|
+
|
|
43
|
+
language_lower = language.lower()
|
|
44
|
+
return LANGUAGE_TEMPLATES.get(language_lower, ".moai/project/tech/default.md.j2")
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# @CODE:TEMPLATE-001 | SPEC: SPEC-INIT-003.md | Chain: TEMPLATE-001
|
|
2
|
+
"""Template file merger (SPEC-INIT-003 v0.3.0).
|
|
3
|
+
|
|
4
|
+
Intelligently merges existing user files with new templates.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import shutil
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TemplateMerger:
|
|
16
|
+
"""Encapsulate template merging logic."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, target_path: Path) -> None:
|
|
19
|
+
"""Initialize the merger.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
target_path: Project path (absolute).
|
|
23
|
+
"""
|
|
24
|
+
self.target_path = target_path.resolve()
|
|
25
|
+
|
|
26
|
+
def merge_claude_md(self, template_path: Path, existing_path: Path) -> None:
|
|
27
|
+
"""Smart merge for CLAUDE.md.
|
|
28
|
+
|
|
29
|
+
Rules:
|
|
30
|
+
- Use the latest template structure/content.
|
|
31
|
+
- Preserve the existing "## 프로젝트 정보" section.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
template_path: Template CLAUDE.md.
|
|
35
|
+
existing_path: Existing CLAUDE.md.
|
|
36
|
+
"""
|
|
37
|
+
# Extract the existing "## 프로젝트 정보" section
|
|
38
|
+
existing_content = existing_path.read_text(encoding="utf-8")
|
|
39
|
+
project_info_start = existing_content.find("## 프로젝트 정보")
|
|
40
|
+
project_info = ""
|
|
41
|
+
if project_info_start != -1:
|
|
42
|
+
# Extract until EOF
|
|
43
|
+
project_info = existing_content[project_info_start:]
|
|
44
|
+
|
|
45
|
+
# Load template content
|
|
46
|
+
template_content = template_path.read_text(encoding="utf-8")
|
|
47
|
+
|
|
48
|
+
# Merge when project info exists
|
|
49
|
+
if project_info:
|
|
50
|
+
# Remove the project info section from the template
|
|
51
|
+
template_project_start = template_content.find("## 프로젝트 정보")
|
|
52
|
+
if template_project_start != -1:
|
|
53
|
+
template_content = template_content[:template_project_start].rstrip()
|
|
54
|
+
|
|
55
|
+
# Merge template content with the preserved section
|
|
56
|
+
merged_content = f"{template_content}\n\n{project_info}"
|
|
57
|
+
existing_path.write_text(merged_content, encoding="utf-8")
|
|
58
|
+
else:
|
|
59
|
+
# No project info; copy the template as-is
|
|
60
|
+
shutil.copy2(template_path, existing_path)
|
|
61
|
+
|
|
62
|
+
def merge_gitignore(self, template_path: Path, existing_path: Path) -> None:
|
|
63
|
+
""".gitignore merge.
|
|
64
|
+
|
|
65
|
+
Rules:
|
|
66
|
+
- Keep existing entries.
|
|
67
|
+
- Add new entries from the template.
|
|
68
|
+
- Remove duplicates.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
template_path: Template .gitignore file.
|
|
72
|
+
existing_path: Existing .gitignore file.
|
|
73
|
+
"""
|
|
74
|
+
template_lines = set(template_path.read_text(encoding="utf-8").splitlines())
|
|
75
|
+
existing_lines = existing_path.read_text(encoding="utf-8").splitlines()
|
|
76
|
+
|
|
77
|
+
# Merge while removing duplicates
|
|
78
|
+
merged_lines = existing_lines + [
|
|
79
|
+
line for line in template_lines if line not in existing_lines
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
existing_path.write_text("\n".join(merged_lines) + "\n", encoding="utf-8")
|
|
83
|
+
|
|
84
|
+
def merge_config(self, detected_language: str | None = None) -> dict[str, str]:
|
|
85
|
+
"""Smart merge for config.json.
|
|
86
|
+
|
|
87
|
+
Rules:
|
|
88
|
+
- Prefer existing settings.
|
|
89
|
+
- Use detected language plus defaults for new projects.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
detected_language: Detected language.
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
Merged configuration dictionary.
|
|
96
|
+
"""
|
|
97
|
+
config_path = self.target_path / ".moai" / "config.json"
|
|
98
|
+
|
|
99
|
+
# Load existing config if present
|
|
100
|
+
existing_config: dict[str, Any] = {}
|
|
101
|
+
if config_path.exists():
|
|
102
|
+
with open(config_path, encoding="utf-8") as f:
|
|
103
|
+
existing_config = json.load(f)
|
|
104
|
+
|
|
105
|
+
# Build new config while preferring existing values
|
|
106
|
+
new_config: dict[str, str] = {
|
|
107
|
+
"projectName": existing_config.get(
|
|
108
|
+
"projectName", self.target_path.name
|
|
109
|
+
),
|
|
110
|
+
"mode": existing_config.get("mode", "personal"),
|
|
111
|
+
"locale": existing_config.get("locale", "ko"),
|
|
112
|
+
"language": existing_config.get(
|
|
113
|
+
"language", detected_language or "generic"
|
|
114
|
+
),
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return new_config
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
# @CODE:TEMPLATE-001 | SPEC: SPEC-INIT-003.md | Chain: TEMPLATE-001
|
|
2
|
+
"""Template copy and backup processor (SPEC-INIT-003 v0.3.0: preserve user content)."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import shutil
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
|
|
11
|
+
from moai_adk.core.template.backup import TemplateBackup
|
|
12
|
+
from moai_adk.core.template.merger import TemplateMerger
|
|
13
|
+
|
|
14
|
+
console = Console()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TemplateProcessor:
|
|
18
|
+
"""Orchestrate template copying and backups."""
|
|
19
|
+
|
|
20
|
+
# User data protection paths (never touch) - SPEC-INIT-003 v0.3.0
|
|
21
|
+
PROTECTED_PATHS = [
|
|
22
|
+
".moai/specs/", # User SPEC documents
|
|
23
|
+
".moai/reports/", # User reports
|
|
24
|
+
".moai/project/", # User project documents (product/structure/tech.md)
|
|
25
|
+
".moai/config.json", # User configuration (merged via /alfred:9-update flow)
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
# Paths excluded from backups
|
|
29
|
+
BACKUP_EXCLUDE = PROTECTED_PATHS
|
|
30
|
+
|
|
31
|
+
def __init__(self, target_path: Path) -> None:
|
|
32
|
+
"""Initialize the processor.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
target_path: Project path.
|
|
36
|
+
"""
|
|
37
|
+
self.target_path = target_path.resolve()
|
|
38
|
+
self.template_root = self._get_template_root()
|
|
39
|
+
self.backup = TemplateBackup(self.target_path)
|
|
40
|
+
self.merger = TemplateMerger(self.target_path)
|
|
41
|
+
|
|
42
|
+
def _get_template_root(self) -> Path:
|
|
43
|
+
"""Return the template root path."""
|
|
44
|
+
# src/moai_adk/core/template/processor.py → src/moai_adk/templates/
|
|
45
|
+
current_file = Path(__file__).resolve()
|
|
46
|
+
package_root = current_file.parent.parent.parent
|
|
47
|
+
return package_root / "templates"
|
|
48
|
+
|
|
49
|
+
def copy_templates(self, backup: bool = True, silent: bool = False) -> None:
|
|
50
|
+
"""Copy template files into the project.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
backup: Whether to create a backup.
|
|
54
|
+
silent: Reduce log output when True.
|
|
55
|
+
"""
|
|
56
|
+
# 1. Create a backup when existing files are present
|
|
57
|
+
if backup and self._has_existing_files():
|
|
58
|
+
backup_path = self.create_backup()
|
|
59
|
+
if not silent:
|
|
60
|
+
console.print(f"💾 Backup created: {backup_path.name}")
|
|
61
|
+
|
|
62
|
+
# 2. Copy templates
|
|
63
|
+
if not silent:
|
|
64
|
+
console.print("📄 Copying templates...")
|
|
65
|
+
|
|
66
|
+
self._copy_claude(silent)
|
|
67
|
+
self._copy_moai(silent)
|
|
68
|
+
self._copy_claude_md(silent)
|
|
69
|
+
self._copy_gitignore(silent)
|
|
70
|
+
|
|
71
|
+
if not silent:
|
|
72
|
+
console.print("✅ Templates copied successfully")
|
|
73
|
+
|
|
74
|
+
def _has_existing_files(self) -> bool:
|
|
75
|
+
"""Determine whether project files exist (backup decision helper)."""
|
|
76
|
+
return self.backup.has_existing_files()
|
|
77
|
+
|
|
78
|
+
def create_backup(self) -> Path:
|
|
79
|
+
"""Create a timestamped backup (delegated)."""
|
|
80
|
+
return self.backup.create_backup()
|
|
81
|
+
|
|
82
|
+
def _copy_exclude_protected(self, src: Path, dst: Path) -> None:
|
|
83
|
+
"""Copy content while excluding protected paths.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
src: Source directory.
|
|
87
|
+
dst: Destination directory.
|
|
88
|
+
"""
|
|
89
|
+
dst.mkdir(parents=True, exist_ok=True)
|
|
90
|
+
|
|
91
|
+
# PROTECTED_PATHS: only specs/ and reports/ are excluded during copying
|
|
92
|
+
# project/ and config.json are preserved only when they already exist
|
|
93
|
+
template_protected_paths = [
|
|
94
|
+
"specs",
|
|
95
|
+
"reports",
|
|
96
|
+
]
|
|
97
|
+
|
|
98
|
+
for item in src.rglob("*"):
|
|
99
|
+
rel_path = item.relative_to(src)
|
|
100
|
+
rel_path_str = str(rel_path)
|
|
101
|
+
|
|
102
|
+
# Skip template copy for specs/ and reports/
|
|
103
|
+
if any(rel_path_str.startswith(p) for p in template_protected_paths):
|
|
104
|
+
continue
|
|
105
|
+
|
|
106
|
+
dst_item = dst / rel_path
|
|
107
|
+
if item.is_file():
|
|
108
|
+
# Preserve user content by skipping existing files (v0.3.0)
|
|
109
|
+
# This automatically protects project/ and config.json
|
|
110
|
+
if dst_item.exists():
|
|
111
|
+
continue
|
|
112
|
+
dst_item.parent.mkdir(parents=True, exist_ok=True)
|
|
113
|
+
shutil.copy2(item, dst_item)
|
|
114
|
+
elif item.is_dir():
|
|
115
|
+
dst_item.mkdir(parents=True, exist_ok=True)
|
|
116
|
+
|
|
117
|
+
def _copy_claude(self, silent: bool = False) -> None:
|
|
118
|
+
""".claude/ directory copy (selective with alfred folder overwrite).
|
|
119
|
+
|
|
120
|
+
Strategy:
|
|
121
|
+
- Alfred folders (commands/agents/hooks/output-styles/alfred) → copy wholesale (delete & overwrite)
|
|
122
|
+
* Creates individual backup before deletion for safety
|
|
123
|
+
- Other files/folders → copy individually (preserve existing)
|
|
124
|
+
"""
|
|
125
|
+
src = self.template_root / ".claude"
|
|
126
|
+
dst = self.target_path / ".claude"
|
|
127
|
+
|
|
128
|
+
if not src.exists():
|
|
129
|
+
if not silent:
|
|
130
|
+
console.print("⚠️ .claude/ template not found")
|
|
131
|
+
return
|
|
132
|
+
|
|
133
|
+
# Create .claude directory if not exists
|
|
134
|
+
dst.mkdir(parents=True, exist_ok=True)
|
|
135
|
+
|
|
136
|
+
# Alfred folders to copy wholesale (overwrite)
|
|
137
|
+
alfred_folders = [
|
|
138
|
+
"hooks/alfred",
|
|
139
|
+
"commands/alfred",
|
|
140
|
+
"output-styles/alfred",
|
|
141
|
+
"agents/alfred",
|
|
142
|
+
]
|
|
143
|
+
|
|
144
|
+
# 1. Copy Alfred folders wholesale (backup before delete & overwrite)
|
|
145
|
+
for folder in alfred_folders:
|
|
146
|
+
src_folder = src / folder
|
|
147
|
+
dst_folder = dst / folder
|
|
148
|
+
|
|
149
|
+
if src_folder.exists():
|
|
150
|
+
# Backup this folder before deletion (safety measure)
|
|
151
|
+
if dst_folder.exists():
|
|
152
|
+
self._backup_alfred_folder(dst_folder, folder)
|
|
153
|
+
shutil.rmtree(dst_folder)
|
|
154
|
+
|
|
155
|
+
# Create parent directory if needed
|
|
156
|
+
dst_folder.parent.mkdir(parents=True, exist_ok=True)
|
|
157
|
+
shutil.copytree(src_folder, dst_folder)
|
|
158
|
+
if not silent:
|
|
159
|
+
console.print(f" ✅ .claude/{folder}/ overwritten")
|
|
160
|
+
|
|
161
|
+
# 2. Copy other files/folders individually (preserve existing)
|
|
162
|
+
for item in src.iterdir():
|
|
163
|
+
rel_path = item.relative_to(src)
|
|
164
|
+
dst_item = dst / rel_path
|
|
165
|
+
|
|
166
|
+
# Skip Alfred parent folders (already handled above)
|
|
167
|
+
if item.is_dir() and item.name in ["hooks", "commands", "output-styles", "agents"]:
|
|
168
|
+
continue
|
|
169
|
+
|
|
170
|
+
if item.is_file():
|
|
171
|
+
# Copy file, skip if exists (preserve user modifications)
|
|
172
|
+
if not dst_item.exists():
|
|
173
|
+
shutil.copy2(item, dst_item)
|
|
174
|
+
elif item.is_dir():
|
|
175
|
+
# Copy directory recursively (preserve existing files)
|
|
176
|
+
if not dst_item.exists():
|
|
177
|
+
shutil.copytree(item, dst_item)
|
|
178
|
+
|
|
179
|
+
if not silent:
|
|
180
|
+
console.print(" ✅ .claude/ copy complete (alfred folders overwritten, others preserved)")
|
|
181
|
+
|
|
182
|
+
def _backup_alfred_folder(self, folder_path: Path, folder_name: str) -> None:
|
|
183
|
+
"""Backup an Alfred folder before overwriting (safety measure).
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
folder_path: Path to the folder to backup.
|
|
187
|
+
folder_name: Name of the folder (e.g., "hooks/alfred").
|
|
188
|
+
"""
|
|
189
|
+
if not folder_path.exists():
|
|
190
|
+
return
|
|
191
|
+
|
|
192
|
+
# Create backup directory in .moai-backups/.claude-backups/{timestamp}/
|
|
193
|
+
backup_base = self.target_path / ".moai-backups" / ".claude-backups"
|
|
194
|
+
backup_base.mkdir(parents=True, exist_ok=True)
|
|
195
|
+
|
|
196
|
+
# Generate timestamp-based backup directory
|
|
197
|
+
from moai_adk.core.project.backup_utils import generate_backup_dir_name
|
|
198
|
+
|
|
199
|
+
timestamp = generate_backup_dir_name()
|
|
200
|
+
backup_dir = backup_base / timestamp
|
|
201
|
+
|
|
202
|
+
# Backup this specific folder
|
|
203
|
+
backup_folder = backup_dir / folder_name
|
|
204
|
+
backup_folder.parent.mkdir(parents=True, exist_ok=True)
|
|
205
|
+
shutil.copytree(folder_path, backup_folder)
|
|
206
|
+
|
|
207
|
+
def _copy_moai(self, silent: bool = False) -> None:
|
|
208
|
+
""".moai/ directory copy (excludes protected paths)."""
|
|
209
|
+
src = self.template_root / ".moai"
|
|
210
|
+
dst = self.target_path / ".moai"
|
|
211
|
+
|
|
212
|
+
if not src.exists():
|
|
213
|
+
if not silent:
|
|
214
|
+
console.print("⚠️ .moai/ template not found")
|
|
215
|
+
return
|
|
216
|
+
|
|
217
|
+
# Paths excluded from template copying (specs/, reports/)
|
|
218
|
+
template_protected_paths = [
|
|
219
|
+
"specs",
|
|
220
|
+
"reports",
|
|
221
|
+
]
|
|
222
|
+
|
|
223
|
+
# Copy while skipping protected paths
|
|
224
|
+
for item in src.rglob("*"):
|
|
225
|
+
rel_path = item.relative_to(src)
|
|
226
|
+
rel_path_str = str(rel_path)
|
|
227
|
+
|
|
228
|
+
# Skip specs/ and reports/
|
|
229
|
+
if any(rel_path_str.startswith(p) for p in template_protected_paths):
|
|
230
|
+
continue
|
|
231
|
+
|
|
232
|
+
dst_item = dst / rel_path
|
|
233
|
+
if item.is_file():
|
|
234
|
+
# Skip existing files to preserve user content (v0.3.0)
|
|
235
|
+
if dst_item.exists():
|
|
236
|
+
continue
|
|
237
|
+
dst_item.parent.mkdir(parents=True, exist_ok=True)
|
|
238
|
+
shutil.copy2(item, dst_item)
|
|
239
|
+
elif item.is_dir():
|
|
240
|
+
dst_item.mkdir(parents=True, exist_ok=True)
|
|
241
|
+
|
|
242
|
+
if not silent:
|
|
243
|
+
console.print(" ✅ .moai/ copy complete (user content preserved)")
|
|
244
|
+
|
|
245
|
+
def _copy_claude_md(self, silent: bool = False) -> None:
|
|
246
|
+
"""Copy CLAUDE.md with smart merging."""
|
|
247
|
+
src = self.template_root / "CLAUDE.md"
|
|
248
|
+
dst = self.target_path / "CLAUDE.md"
|
|
249
|
+
|
|
250
|
+
if not src.exists():
|
|
251
|
+
if not silent:
|
|
252
|
+
console.print("⚠️ CLAUDE.md template not found")
|
|
253
|
+
return
|
|
254
|
+
|
|
255
|
+
# Preserve project information when the file exists
|
|
256
|
+
if dst.exists():
|
|
257
|
+
self._merge_claude_md(src, dst)
|
|
258
|
+
if not silent:
|
|
259
|
+
console.print(" 🔄 CLAUDE.md merged (project information preserved)")
|
|
260
|
+
else:
|
|
261
|
+
shutil.copy2(src, dst)
|
|
262
|
+
if not silent:
|
|
263
|
+
console.print(" ✅ CLAUDE.md copy complete")
|
|
264
|
+
|
|
265
|
+
def _merge_claude_md(self, src: Path, dst: Path) -> None:
|
|
266
|
+
"""Delegate the smart merge for CLAUDE.md.
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
src: Template CLAUDE.md.
|
|
270
|
+
dst: Project CLAUDE.md.
|
|
271
|
+
"""
|
|
272
|
+
self.merger.merge_claude_md(src, dst)
|
|
273
|
+
|
|
274
|
+
def _copy_gitignore(self, silent: bool = False) -> None:
|
|
275
|
+
""".gitignore copy (optional)."""
|
|
276
|
+
src = self.template_root / ".gitignore"
|
|
277
|
+
dst = self.target_path / ".gitignore"
|
|
278
|
+
|
|
279
|
+
if not src.exists():
|
|
280
|
+
return
|
|
281
|
+
|
|
282
|
+
# Merge with the existing .gitignore when present
|
|
283
|
+
if dst.exists():
|
|
284
|
+
self._merge_gitignore(src, dst)
|
|
285
|
+
if not silent:
|
|
286
|
+
console.print(" 🔄 .gitignore merged")
|
|
287
|
+
else:
|
|
288
|
+
shutil.copy2(src, dst)
|
|
289
|
+
if not silent:
|
|
290
|
+
console.print(" ✅ .gitignore copy complete")
|
|
291
|
+
|
|
292
|
+
def _merge_gitignore(self, src: Path, dst: Path) -> None:
|
|
293
|
+
"""Delegate the .gitignore merge.
|
|
294
|
+
|
|
295
|
+
Args:
|
|
296
|
+
src: Template .gitignore.
|
|
297
|
+
dst: Project .gitignore.
|
|
298
|
+
"""
|
|
299
|
+
self.merger.merge_gitignore(src, dst)
|
|
300
|
+
|
|
301
|
+
def merge_config(self, detected_language: str | None = None) -> dict[str, str]:
|
|
302
|
+
"""Delegate the smart merge for config.json.
|
|
303
|
+
|
|
304
|
+
Args:
|
|
305
|
+
detected_language: Detected language.
|
|
306
|
+
|
|
307
|
+
Returns:
|
|
308
|
+
Merged configuration dictionary.
|
|
309
|
+
"""
|
|
310
|
+
return self.merger.merge_config(detected_language)
|