moai-adk 0.3.2__py3-none-any.whl → 0.3.6__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 +1 -1
- moai_adk/cli/commands/update.py +49 -30
- moai_adk/core/project/initializer.py +16 -5
- moai_adk/core/project/phase_executor.py +27 -3
- moai_adk/core/project/validator.py +46 -1
- moai_adk/core/template/processor.py +151 -11
- moai_adk/templates/.moai/hooks/pre-push.sample +88 -0
- moai_adk/templates/.moai/memory/development-guide.md +1 -26
- moai_adk/templates/.moai/memory/gitflow-protection-policy.md +220 -0
- {moai_adk-0.3.2.dist-info → moai_adk-0.3.6.dist-info}/METADATA +806 -208
- {moai_adk-0.3.2.dist-info → moai_adk-0.3.6.dist-info}/RECORD +14 -12
- {moai_adk-0.3.2.dist-info → moai_adk-0.3.6.dist-info}/WHEEL +0 -0
- {moai_adk-0.3.2.dist-info → moai_adk-0.3.6.dist-info}/entry_points.txt +0 -0
- {moai_adk-0.3.2.dist-info → moai_adk-0.3.6.dist-info}/licenses/LICENSE +0 -0
moai_adk/__init__.py
CHANGED
moai_adk/cli/commands/update.py
CHANGED
|
@@ -3,6 +3,7 @@ import json
|
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
|
|
5
5
|
import click
|
|
6
|
+
from packaging import version
|
|
6
7
|
from rich.console import Console
|
|
7
8
|
|
|
8
9
|
from moai_adk import __version__
|
|
@@ -11,11 +12,11 @@ from moai_adk.core.template.processor import TemplateProcessor
|
|
|
11
12
|
console = Console()
|
|
12
13
|
|
|
13
14
|
|
|
14
|
-
def get_latest_version() -> str:
|
|
15
|
+
def get_latest_version() -> str | None:
|
|
15
16
|
"""Get the latest version from PyPI.
|
|
16
17
|
|
|
17
18
|
Returns:
|
|
18
|
-
Latest version string, or
|
|
19
|
+
Latest version string, or None if fetch fails.
|
|
19
20
|
"""
|
|
20
21
|
try:
|
|
21
22
|
import urllib.error
|
|
@@ -26,8 +27,8 @@ def get_latest_version() -> str:
|
|
|
26
27
|
data = json.loads(response.read().decode("utf-8"))
|
|
27
28
|
return data["info"]["version"]
|
|
28
29
|
except (urllib.error.URLError, json.JSONDecodeError, KeyError, TimeoutError):
|
|
29
|
-
#
|
|
30
|
-
return
|
|
30
|
+
# Return None if PyPI check fails
|
|
31
|
+
return None
|
|
31
32
|
|
|
32
33
|
|
|
33
34
|
@click.command()
|
|
@@ -73,39 +74,57 @@ def update(path: str, force: bool, check: bool) -> None:
|
|
|
73
74
|
console.print("[cyan]🔍 Checking versions...[/cyan]")
|
|
74
75
|
current_version = __version__
|
|
75
76
|
latest_version = get_latest_version()
|
|
76
|
-
|
|
77
|
-
|
|
77
|
+
|
|
78
|
+
# Handle PyPI fetch failure
|
|
79
|
+
if latest_version is None:
|
|
80
|
+
console.print(f" Current version: {current_version}")
|
|
81
|
+
console.print(" Latest version: [yellow]Unable to fetch from PyPI[/yellow]")
|
|
82
|
+
if not force:
|
|
83
|
+
console.print("[yellow]⚠ Cannot check for updates. Use --force to update anyway.[/yellow]")
|
|
84
|
+
return
|
|
85
|
+
else:
|
|
86
|
+
console.print(f" Current version: {current_version}")
|
|
87
|
+
console.print(f" Latest version: {latest_version}")
|
|
78
88
|
|
|
79
89
|
if check:
|
|
80
90
|
# Exit early when --check is provided
|
|
81
|
-
if
|
|
82
|
-
console.print("[
|
|
83
|
-
|
|
91
|
+
if latest_version is None:
|
|
92
|
+
console.print("[yellow]⚠ Unable to check for updates[/yellow]")
|
|
93
|
+
elif version.parse(current_version) < version.parse(latest_version):
|
|
84
94
|
console.print("[yellow]⚠ Update available[/yellow]")
|
|
95
|
+
elif version.parse(current_version) > version.parse(latest_version):
|
|
96
|
+
console.print("[green]✓ Development version (newer than PyPI)[/green]")
|
|
97
|
+
else:
|
|
98
|
+
console.print("[green]✓ Already up to date[/green]")
|
|
85
99
|
return
|
|
86
100
|
|
|
87
101
|
# Check if update is needed (version + optimized status) - skip with --force
|
|
88
|
-
if not force and
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
102
|
+
if not force and latest_version is not None:
|
|
103
|
+
current_ver = version.parse(current_version)
|
|
104
|
+
latest_ver = version.parse(latest_version)
|
|
105
|
+
|
|
106
|
+
# Don't update if current version is newer or equal
|
|
107
|
+
if current_ver >= latest_ver:
|
|
108
|
+
# Check optimized status in config.json
|
|
109
|
+
config_path = project_path / ".moai" / "config.json"
|
|
110
|
+
if config_path.exists():
|
|
111
|
+
try:
|
|
112
|
+
config_data = json.loads(config_path.read_text())
|
|
113
|
+
is_optimized = config_data.get("project", {}).get("optimized", False)
|
|
114
|
+
|
|
115
|
+
if is_optimized:
|
|
116
|
+
# Already up to date and optimized - exit silently
|
|
117
|
+
return
|
|
118
|
+
else:
|
|
119
|
+
console.print("[yellow]⚠ Optimization needed[/yellow]")
|
|
120
|
+
console.print("[dim]Use /alfred:0-project update for template optimization[/dim]")
|
|
121
|
+
return
|
|
122
|
+
except (json.JSONDecodeError, KeyError):
|
|
123
|
+
# If config.json is invalid, proceed with update
|
|
124
|
+
pass
|
|
125
|
+
else:
|
|
126
|
+
console.print("[green]✓ Already up to date[/green]")
|
|
127
|
+
return
|
|
109
128
|
|
|
110
129
|
# Phase 2: create a backup unless --force
|
|
111
130
|
if not force:
|
|
@@ -103,20 +103,31 @@ class ProjectInitializer:
|
|
|
103
103
|
# Phase 2: Directory (create directories)
|
|
104
104
|
self.executor.execute_directory_phase(self.path, progress_callback)
|
|
105
105
|
|
|
106
|
-
#
|
|
106
|
+
# Prepare config for template variable substitution (Phase 3)
|
|
107
|
+
config = {
|
|
108
|
+
"name": self.path.name,
|
|
109
|
+
"mode": mode,
|
|
110
|
+
"locale": locale,
|
|
111
|
+
"language": detected_language,
|
|
112
|
+
"description": "",
|
|
113
|
+
"version": "0.1.0",
|
|
114
|
+
"author": "@user",
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
# Phase 3: Resource (copy templates with variable substitution)
|
|
107
118
|
resource_files = self.executor.execute_resource_phase(
|
|
108
|
-
self.path, progress_callback
|
|
119
|
+
self.path, config, progress_callback
|
|
109
120
|
)
|
|
110
121
|
|
|
111
|
-
# Phase 4: Configuration (generate config)
|
|
112
|
-
|
|
122
|
+
# Phase 4: Configuration (generate config.json)
|
|
123
|
+
config_data: dict[str, str | bool] = {
|
|
113
124
|
"projectName": self.path.name,
|
|
114
125
|
"mode": mode,
|
|
115
126
|
"locale": locale,
|
|
116
127
|
"language": detected_language,
|
|
117
128
|
}
|
|
118
129
|
config_files = self.executor.execute_configuration_phase(
|
|
119
|
-
self.path,
|
|
130
|
+
self.path, config_data, progress_callback
|
|
120
131
|
)
|
|
121
132
|
|
|
122
133
|
# Phase 5: Validation (verify and finalize)
|
|
@@ -13,6 +13,7 @@ import json
|
|
|
13
13
|
import shutil
|
|
14
14
|
import subprocess
|
|
15
15
|
from collections.abc import Callable
|
|
16
|
+
from datetime import datetime
|
|
16
17
|
from pathlib import Path
|
|
17
18
|
|
|
18
19
|
from rich.console import Console
|
|
@@ -115,12 +116,14 @@ class PhaseExecutor:
|
|
|
115
116
|
def execute_resource_phase(
|
|
116
117
|
self,
|
|
117
118
|
project_path: Path,
|
|
119
|
+
config: dict[str, str] | None = None,
|
|
118
120
|
progress_callback: ProgressCallback | None = None,
|
|
119
121
|
) -> list[str]:
|
|
120
|
-
"""Phase 3: install resources.
|
|
122
|
+
"""Phase 3: install resources with variable substitution.
|
|
121
123
|
|
|
122
124
|
Args:
|
|
123
125
|
project_path: Project path.
|
|
126
|
+
config: Configuration dictionary for template variable substitution.
|
|
124
127
|
progress_callback: Optional progress callback.
|
|
125
128
|
|
|
126
129
|
Returns:
|
|
@@ -135,6 +138,20 @@ class PhaseExecutor:
|
|
|
135
138
|
from moai_adk.core.template import TemplateProcessor
|
|
136
139
|
|
|
137
140
|
processor = TemplateProcessor(project_path)
|
|
141
|
+
|
|
142
|
+
# Set template variable context (if provided)
|
|
143
|
+
if config:
|
|
144
|
+
context = {
|
|
145
|
+
"MOAI_VERSION": __version__,
|
|
146
|
+
"CREATION_TIMESTAMP": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
|
147
|
+
"PROJECT_NAME": config.get("name", "unknown"),
|
|
148
|
+
"PROJECT_DESCRIPTION": config.get("description", ""),
|
|
149
|
+
"PROJECT_MODE": config.get("mode", "personal"),
|
|
150
|
+
"PROJECT_VERSION": config.get("version", "0.1.0"),
|
|
151
|
+
"AUTHOR": config.get("author", "@user"),
|
|
152
|
+
}
|
|
153
|
+
processor.set_context(context)
|
|
154
|
+
|
|
138
155
|
processor.copy_templates(backup=False, silent=True) # Avoid progress bar conflicts
|
|
139
156
|
|
|
140
157
|
# Return a simplified list of generated assets
|
|
@@ -148,7 +165,7 @@ class PhaseExecutor:
|
|
|
148
165
|
def execute_configuration_phase(
|
|
149
166
|
self,
|
|
150
167
|
project_path: Path,
|
|
151
|
-
config: dict[str, str],
|
|
168
|
+
config: dict[str, str | bool],
|
|
152
169
|
progress_callback: ProgressCallback | None = None,
|
|
153
170
|
) -> list[str]:
|
|
154
171
|
"""Phase 4: generate configuration.
|
|
@@ -185,6 +202,10 @@ class PhaseExecutor:
|
|
|
185
202
|
) -> None:
|
|
186
203
|
"""Phase 5: validation and wrap-up.
|
|
187
204
|
|
|
205
|
+
@CODE:INIT-004:PHASE5 | Phase 5 verification logic
|
|
206
|
+
@REQ:VALIDATION-001 | SPEC-INIT-004: Verify required files after initialization completion
|
|
207
|
+
@CODE:INIT-004:PHASE5-INTEGRATION | Integration of validation in Phase 5
|
|
208
|
+
|
|
188
209
|
Args:
|
|
189
210
|
project_path: Project path.
|
|
190
211
|
mode: Project mode (personal/team).
|
|
@@ -195,7 +216,10 @@ class PhaseExecutor:
|
|
|
195
216
|
"Phase 5: Validation and finalization...", progress_callback
|
|
196
217
|
)
|
|
197
218
|
|
|
198
|
-
# Validate installation results
|
|
219
|
+
# @CODE:INIT-004:VERIFY-001 | Validate installation results
|
|
220
|
+
# @CODE:INIT-004:VALIDATION-CHECK | Comprehensive installation validation
|
|
221
|
+
# Verifies all required files including 4 Alfred command files:
|
|
222
|
+
# - 0-project.md, 1-spec.md, 2-build.md, 3-sync.md
|
|
199
223
|
self.validator.validate_installation(project_path)
|
|
200
224
|
|
|
201
225
|
# Initialize Git for team mode
|
|
@@ -1,7 +1,20 @@
|
|
|
1
|
-
# @CODE:CORE-PROJECT-003 | SPEC: SPEC-CORE-PROJECT-001.md
|
|
1
|
+
# @CODE:CORE-PROJECT-003 | SPEC: SPEC-CORE-PROJECT-001.md, SPEC-INIT-004.md
|
|
2
|
+
# @CODE:INIT-004:VALIDATION | Chain: SPEC-INIT-004 -> CODE-INIT-004 -> TEST-INIT-004
|
|
3
|
+
# @REQ:VALIDATION-001 | SPEC-INIT-004: Initial verification after installation completion
|
|
4
|
+
# @SPEC:VERIFICATION-001 | SPEC-INIT-004: Verification logic implementation
|
|
2
5
|
"""Project initialization validation module.
|
|
3
6
|
|
|
4
7
|
Validates system requirements and installation results.
|
|
8
|
+
|
|
9
|
+
SPEC-INIT-004 Enhancement:
|
|
10
|
+
- Alfred command files validation (Phase 5)
|
|
11
|
+
- Explicit missing files reporting
|
|
12
|
+
- Required files verification checklist
|
|
13
|
+
|
|
14
|
+
TAG Chain:
|
|
15
|
+
SPEC-INIT-004 (spec.md)
|
|
16
|
+
└─> @CODE:INIT-004:VALIDATION (this file)
|
|
17
|
+
└─> @TEST:INIT-004:VALIDATION (test_validator.py)
|
|
5
18
|
"""
|
|
6
19
|
|
|
7
20
|
import shutil
|
|
@@ -32,6 +45,14 @@ class ProjectValidator:
|
|
|
32
45
|
"CLAUDE.md",
|
|
33
46
|
]
|
|
34
47
|
|
|
48
|
+
# Required Alfred command files (SPEC-INIT-004)
|
|
49
|
+
REQUIRED_ALFRED_COMMANDS = [
|
|
50
|
+
"0-project.md",
|
|
51
|
+
"1-spec.md",
|
|
52
|
+
"2-build.md",
|
|
53
|
+
"3-sync.md",
|
|
54
|
+
]
|
|
55
|
+
|
|
35
56
|
def validate_system_requirements(self) -> None:
|
|
36
57
|
"""Verify system requirements.
|
|
37
58
|
|
|
@@ -76,6 +97,10 @@ class ProjectValidator:
|
|
|
76
97
|
def validate_installation(self, project_path: Path) -> None:
|
|
77
98
|
"""Validate installation results.
|
|
78
99
|
|
|
100
|
+
@CODE:INIT-004:VERIFY-001 | Verification of all required files upon successful completion
|
|
101
|
+
@SPEC:VERIFICATION-001 | SPEC-INIT-004: Verification checklist implementation
|
|
102
|
+
@REQ:VALIDATION-001 | UR-003: All required files verified after init completes
|
|
103
|
+
|
|
79
104
|
Args:
|
|
80
105
|
project_path: Project path.
|
|
81
106
|
|
|
@@ -83,17 +108,37 @@ class ProjectValidator:
|
|
|
83
108
|
ValidationError: Raised when installation was incomplete.
|
|
84
109
|
"""
|
|
85
110
|
# Verify required directories
|
|
111
|
+
# @CODE:INIT-004:VALIDATION-001 | Core project structure validation
|
|
86
112
|
for directory in self.REQUIRED_DIRECTORIES:
|
|
87
113
|
dir_path = project_path / directory
|
|
88
114
|
if not dir_path.exists():
|
|
89
115
|
raise ValidationError(f"Required directory not found: {directory}")
|
|
90
116
|
|
|
91
117
|
# Verify required files
|
|
118
|
+
# @CODE:INIT-004:VALIDATION-002 | Required configuration files validation
|
|
92
119
|
for file in self.REQUIRED_FILES:
|
|
93
120
|
file_path = project_path / file
|
|
94
121
|
if not file_path.exists():
|
|
95
122
|
raise ValidationError(f"Required file not found: {file}")
|
|
96
123
|
|
|
124
|
+
# @CODE:INIT-004:VERIFY-002 | Verify required Alfred command files (SPEC-INIT-004)
|
|
125
|
+
# @REQ:COMMAND-GENERATION-001 | All 4 Alfred command files must be created
|
|
126
|
+
# @CODE:INIT-004:ALFRED-VALIDATION | Alfred command file integrity check
|
|
127
|
+
alfred_dir = project_path / ".claude" / "commands" / "alfred"
|
|
128
|
+
missing_commands = []
|
|
129
|
+
for cmd in self.REQUIRED_ALFRED_COMMANDS:
|
|
130
|
+
cmd_path = alfred_dir / cmd
|
|
131
|
+
if not cmd_path.exists():
|
|
132
|
+
missing_commands.append(cmd)
|
|
133
|
+
|
|
134
|
+
if missing_commands:
|
|
135
|
+
missing_list = ", ".join(missing_commands)
|
|
136
|
+
# @SPEC:ERROR-HANDLING-001 | Clear error messages upon missing files
|
|
137
|
+
# @CODE:INIT-004:ERROR-MESSAGE | Clear reporting of missing Alfred command files
|
|
138
|
+
raise ValidationError(
|
|
139
|
+
f"Required Alfred command files not found: {missing_list}"
|
|
140
|
+
)
|
|
141
|
+
|
|
97
142
|
def _is_inside_moai_package(self, project_path: Path) -> bool:
|
|
98
143
|
"""Determine whether the path is inside the MoAI-ADK package.
|
|
99
144
|
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
from __future__ import annotations
|
|
5
5
|
|
|
6
|
+
import re
|
|
6
7
|
import shutil
|
|
7
8
|
from pathlib import Path
|
|
8
9
|
|
|
@@ -38,6 +39,7 @@ class TemplateProcessor:
|
|
|
38
39
|
self.template_root = self._get_template_root()
|
|
39
40
|
self.backup = TemplateBackup(self.target_path)
|
|
40
41
|
self.merger = TemplateMerger(self.target_path)
|
|
42
|
+
self.context: dict[str, str] = {} # Template variable substitution context
|
|
41
43
|
|
|
42
44
|
def _get_template_root(self) -> Path:
|
|
43
45
|
"""Return the template root path."""
|
|
@@ -46,6 +48,116 @@ class TemplateProcessor:
|
|
|
46
48
|
package_root = current_file.parent.parent.parent
|
|
47
49
|
return package_root / "templates"
|
|
48
50
|
|
|
51
|
+
def set_context(self, context: dict[str, str]) -> None:
|
|
52
|
+
"""Set variable substitution context.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
context: Dictionary of template variables.
|
|
56
|
+
"""
|
|
57
|
+
self.context = context
|
|
58
|
+
|
|
59
|
+
def _substitute_variables(self, content: str) -> tuple[str, list[str]]:
|
|
60
|
+
"""Substitute template variables in content.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
Tuple of (substituted_content, warnings_list)
|
|
64
|
+
"""
|
|
65
|
+
warnings = []
|
|
66
|
+
|
|
67
|
+
# Perform variable substitution
|
|
68
|
+
for key, value in self.context.items():
|
|
69
|
+
placeholder = f"{{{{{key}}}}}" # {{KEY}}
|
|
70
|
+
if placeholder in content:
|
|
71
|
+
safe_value = self._sanitize_value(value)
|
|
72
|
+
content = content.replace(placeholder, safe_value)
|
|
73
|
+
|
|
74
|
+
# Detect unsubstituted variables
|
|
75
|
+
remaining = re.findall(r'\{\{([A-Z_]+)\}\}', content)
|
|
76
|
+
if remaining:
|
|
77
|
+
unique_remaining = sorted(set(remaining))
|
|
78
|
+
warnings.append(f"Unsubstituted variables: {', '.join(unique_remaining)}")
|
|
79
|
+
|
|
80
|
+
return content, warnings
|
|
81
|
+
|
|
82
|
+
def _sanitize_value(self, value: str) -> str:
|
|
83
|
+
"""Sanitize value to prevent recursive substitution and control characters.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
value: Value to sanitize.
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
Sanitized value.
|
|
90
|
+
"""
|
|
91
|
+
# Remove control characters (keep printable and whitespace)
|
|
92
|
+
value = ''.join(c for c in value if c.isprintable() or c in '\n\r\t')
|
|
93
|
+
# Prevent recursive substitution by removing placeholder patterns
|
|
94
|
+
value = value.replace('{{', '').replace('}}', '')
|
|
95
|
+
return value
|
|
96
|
+
|
|
97
|
+
def _is_text_file(self, file_path: Path) -> bool:
|
|
98
|
+
"""Check if file is text-based (not binary).
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
file_path: File path to check.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
True if file is text-based.
|
|
105
|
+
"""
|
|
106
|
+
text_extensions = {
|
|
107
|
+
'.md', '.json', '.txt', '.py', '.ts', '.js',
|
|
108
|
+
'.yaml', '.yml', '.toml', '.xml', '.sh', '.bash'
|
|
109
|
+
}
|
|
110
|
+
return file_path.suffix.lower() in text_extensions
|
|
111
|
+
|
|
112
|
+
def _copy_file_with_substitution(self, src: Path, dst: Path) -> list[str]:
|
|
113
|
+
"""Copy file with variable substitution for text files.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
src: Source file path.
|
|
117
|
+
dst: Destination file path.
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
List of warnings.
|
|
121
|
+
"""
|
|
122
|
+
warnings = []
|
|
123
|
+
|
|
124
|
+
# Text files: read, substitute, write
|
|
125
|
+
if self._is_text_file(src) and self.context:
|
|
126
|
+
try:
|
|
127
|
+
content = src.read_text(encoding='utf-8')
|
|
128
|
+
content, file_warnings = self._substitute_variables(content)
|
|
129
|
+
dst.write_text(content, encoding='utf-8')
|
|
130
|
+
warnings.extend(file_warnings)
|
|
131
|
+
except UnicodeDecodeError:
|
|
132
|
+
# Binary file fallback
|
|
133
|
+
shutil.copy2(src, dst)
|
|
134
|
+
else:
|
|
135
|
+
# Binary file or no context: simple copy
|
|
136
|
+
shutil.copy2(src, dst)
|
|
137
|
+
|
|
138
|
+
return warnings
|
|
139
|
+
|
|
140
|
+
def _copy_dir_with_substitution(self, src: Path, dst: Path) -> None:
|
|
141
|
+
"""Recursively copy directory with variable substitution for text files.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
src: Source directory path.
|
|
145
|
+
dst: Destination directory path.
|
|
146
|
+
"""
|
|
147
|
+
dst.mkdir(parents=True, exist_ok=True)
|
|
148
|
+
|
|
149
|
+
for item in src.rglob("*"):
|
|
150
|
+
rel_path = item.relative_to(src)
|
|
151
|
+
dst_item = dst / rel_path
|
|
152
|
+
|
|
153
|
+
if item.is_file():
|
|
154
|
+
# Create parent directory if needed
|
|
155
|
+
dst_item.parent.mkdir(parents=True, exist_ok=True)
|
|
156
|
+
# Copy with variable substitution
|
|
157
|
+
self._copy_file_with_substitution(item, dst_item)
|
|
158
|
+
elif item.is_dir():
|
|
159
|
+
dst_item.mkdir(parents=True, exist_ok=True)
|
|
160
|
+
|
|
49
161
|
def copy_templates(self, backup: bool = True, silent: bool = False) -> None:
|
|
50
162
|
"""Copy template files into the project.
|
|
51
163
|
|
|
@@ -115,11 +227,16 @@ class TemplateProcessor:
|
|
|
115
227
|
dst_item.mkdir(parents=True, exist_ok=True)
|
|
116
228
|
|
|
117
229
|
def _copy_claude(self, silent: bool = False) -> None:
|
|
118
|
-
""".claude/ directory copy (selective with alfred folder overwrite).
|
|
230
|
+
""".claude/ directory copy with variable substitution (selective with alfred folder overwrite).
|
|
231
|
+
|
|
232
|
+
@CODE:INIT-004:ALFRED-001 | Copy all 4 Alfred command files from templates
|
|
233
|
+
@REQ:COMMAND-GENERATION-001 | SPEC-INIT-004: Automatic generation of Alfred command files
|
|
234
|
+
@SPEC:TEMPLATE-PROCESSING-001 | Template processor integration for Alfred command files
|
|
119
235
|
|
|
120
236
|
Strategy:
|
|
121
237
|
- Alfred folders (commands/agents/hooks/output-styles/alfred) → copy wholesale (delete & overwrite)
|
|
122
238
|
* Creates individual backup before deletion for safety
|
|
239
|
+
* Commands: 0-project.md, 1-spec.md, 2-build.md, 3-sync.md
|
|
123
240
|
- Other files/folders → copy individually (preserve existing)
|
|
124
241
|
"""
|
|
125
242
|
src = self.template_root / ".claude"
|
|
@@ -133,10 +250,12 @@ class TemplateProcessor:
|
|
|
133
250
|
# Create .claude directory if not exists
|
|
134
251
|
dst.mkdir(parents=True, exist_ok=True)
|
|
135
252
|
|
|
253
|
+
# @CODE:INIT-004:ALFRED-002 | Alfred command files must always be overwritten
|
|
254
|
+
# @CODE:INIT-004:ALFRED-COPY | Copy all 4 Alfred command files from templates
|
|
136
255
|
# Alfred folders to copy wholesale (overwrite)
|
|
137
256
|
alfred_folders = [
|
|
138
257
|
"hooks/alfred",
|
|
139
|
-
"commands/alfred",
|
|
258
|
+
"commands/alfred", # Contains 0-project.md, 1-spec.md, 2-build.md, 3-sync.md
|
|
140
259
|
"output-styles/alfred",
|
|
141
260
|
"agents/alfred",
|
|
142
261
|
]
|
|
@@ -158,7 +277,8 @@ class TemplateProcessor:
|
|
|
158
277
|
if not silent:
|
|
159
278
|
console.print(f" ✅ .claude/{folder}/ overwritten")
|
|
160
279
|
|
|
161
|
-
# 2. Copy other files/folders individually (preserve existing)
|
|
280
|
+
# 2. Copy other files/folders individually (preserve existing, with substitution)
|
|
281
|
+
all_warnings = []
|
|
162
282
|
for item in src.iterdir():
|
|
163
283
|
rel_path = item.relative_to(src)
|
|
164
284
|
dst_item = dst / rel_path
|
|
@@ -170,14 +290,23 @@ class TemplateProcessor:
|
|
|
170
290
|
if item.is_file():
|
|
171
291
|
# Copy file, skip if exists (preserve user modifications)
|
|
172
292
|
if not dst_item.exists():
|
|
173
|
-
|
|
293
|
+
# Copy with variable substitution for text files
|
|
294
|
+
warnings = self._copy_file_with_substitution(item, dst_item)
|
|
295
|
+
all_warnings.extend(warnings)
|
|
174
296
|
elif item.is_dir():
|
|
175
297
|
# Copy directory recursively (preserve existing files)
|
|
176
298
|
if not dst_item.exists():
|
|
177
|
-
|
|
299
|
+
# Recursively copy directory with substitution
|
|
300
|
+
self._copy_dir_with_substitution(item, dst_item)
|
|
301
|
+
|
|
302
|
+
# Print warnings if any
|
|
303
|
+
if all_warnings and not silent:
|
|
304
|
+
console.print("[yellow]⚠️ Template warnings:[/yellow]")
|
|
305
|
+
for warning in set(all_warnings): # Deduplicate
|
|
306
|
+
console.print(f" {warning}")
|
|
178
307
|
|
|
179
308
|
if not silent:
|
|
180
|
-
console.print(" ✅ .claude/ copy complete (
|
|
309
|
+
console.print(" ✅ .claude/ copy complete (variables substituted)")
|
|
181
310
|
|
|
182
311
|
def _backup_alfred_folder(self, folder_path: Path, folder_name: str) -> None:
|
|
183
312
|
"""Backup an Alfred folder before overwriting (safety measure).
|
|
@@ -205,7 +334,7 @@ class TemplateProcessor:
|
|
|
205
334
|
shutil.copytree(folder_path, backup_folder)
|
|
206
335
|
|
|
207
336
|
def _copy_moai(self, silent: bool = False) -> None:
|
|
208
|
-
""".moai/ directory copy (excludes protected paths)."""
|
|
337
|
+
""".moai/ directory copy with variable substitution (excludes protected paths)."""
|
|
209
338
|
src = self.template_root / ".moai"
|
|
210
339
|
dst = self.target_path / ".moai"
|
|
211
340
|
|
|
@@ -220,6 +349,8 @@ class TemplateProcessor:
|
|
|
220
349
|
"reports",
|
|
221
350
|
]
|
|
222
351
|
|
|
352
|
+
all_warnings = []
|
|
353
|
+
|
|
223
354
|
# Copy while skipping protected paths
|
|
224
355
|
for item in src.rglob("*"):
|
|
225
356
|
rel_path = item.relative_to(src)
|
|
@@ -235,15 +366,23 @@ class TemplateProcessor:
|
|
|
235
366
|
if dst_item.exists():
|
|
236
367
|
continue
|
|
237
368
|
dst_item.parent.mkdir(parents=True, exist_ok=True)
|
|
238
|
-
|
|
369
|
+
# Copy with variable substitution
|
|
370
|
+
warnings = self._copy_file_with_substitution(item, dst_item)
|
|
371
|
+
all_warnings.extend(warnings)
|
|
239
372
|
elif item.is_dir():
|
|
240
373
|
dst_item.mkdir(parents=True, exist_ok=True)
|
|
241
374
|
|
|
375
|
+
# Print warnings if any
|
|
376
|
+
if all_warnings and not silent:
|
|
377
|
+
console.print("[yellow]⚠️ Template warnings:[/yellow]")
|
|
378
|
+
for warning in set(all_warnings): # Deduplicate
|
|
379
|
+
console.print(f" {warning}")
|
|
380
|
+
|
|
242
381
|
if not silent:
|
|
243
|
-
console.print(" ✅ .moai/ copy complete (
|
|
382
|
+
console.print(" ✅ .moai/ copy complete (variables substituted)")
|
|
244
383
|
|
|
245
384
|
def _copy_claude_md(self, silent: bool = False) -> None:
|
|
246
|
-
"""Copy CLAUDE.md with smart merging."""
|
|
385
|
+
"""Copy CLAUDE.md with smart merging and variable substitution."""
|
|
247
386
|
src = self.template_root / "CLAUDE.md"
|
|
248
387
|
dst = self.target_path / "CLAUDE.md"
|
|
249
388
|
|
|
@@ -258,7 +397,8 @@ class TemplateProcessor:
|
|
|
258
397
|
if not silent:
|
|
259
398
|
console.print(" 🔄 CLAUDE.md merged (project information preserved)")
|
|
260
399
|
else:
|
|
261
|
-
|
|
400
|
+
# Copy with variable substitution
|
|
401
|
+
self._copy_file_with_substitution(src, dst)
|
|
262
402
|
if not silent:
|
|
263
403
|
console.print(" ✅ CLAUDE.md copy complete")
|
|
264
404
|
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
# MoAI-ADK GitFlow Main Branch Advisory Hook
|
|
4
|
+
# Purpose: Advisory warnings for main branch operations (not blocking)
|
|
5
|
+
# Enforces: Best practices with flexibility
|
|
6
|
+
#
|
|
7
|
+
# This hook runs before any git push operation and provides advisories:
|
|
8
|
+
# 1. Warns about direct push to main branch (but allows it)
|
|
9
|
+
# 2. Warns about force-push to main branch (but allows it)
|
|
10
|
+
# 3. Recommends GitFlow best practices
|
|
11
|
+
#
|
|
12
|
+
# Exit codes:
|
|
13
|
+
# 0 - Push allowed (always)
|
|
14
|
+
|
|
15
|
+
set -e
|
|
16
|
+
|
|
17
|
+
# Colors for output
|
|
18
|
+
RED='\033[0;31m'
|
|
19
|
+
YELLOW='\033[1;33m'
|
|
20
|
+
GREEN='\033[0;32m'
|
|
21
|
+
BLUE='\033[0;34m'
|
|
22
|
+
NC='\033[0m' # No Color
|
|
23
|
+
|
|
24
|
+
# Read from stdin (git sends remote, local ref info)
|
|
25
|
+
# Format: <local ref> <local oid> <remote ref> <remote oid>
|
|
26
|
+
while read local_ref local_oid remote_ref remote_oid; do
|
|
27
|
+
# Extract the remote branch name from the reference
|
|
28
|
+
# remote_ref format: refs/heads/main
|
|
29
|
+
remote_branch=$(echo "$remote_ref" | sed 's|refs/heads/||')
|
|
30
|
+
local_branch=$(echo "$local_ref" | sed 's|refs/heads/||')
|
|
31
|
+
|
|
32
|
+
# Check if attempting to push to main branch
|
|
33
|
+
if [ "$remote_branch" = "main" ] || [ "$remote_branch" = "master" ]; then
|
|
34
|
+
# Get the current branch to determine if this is the develop branch
|
|
35
|
+
current_branch=$(git rev-parse --abbrev-ref HEAD)
|
|
36
|
+
|
|
37
|
+
# Advisory: recommend develop -> main workflow
|
|
38
|
+
if [ "$local_branch" != "develop" ] && [ "${local_branch#release/}" = "$local_branch" ]; then
|
|
39
|
+
echo ""
|
|
40
|
+
echo -e "${YELLOW}⚠️ ADVISORY: Non-standard GitFlow detected${NC}"
|
|
41
|
+
echo ""
|
|
42
|
+
echo -e "${BLUE}Current branch: ${local_branch}${NC}"
|
|
43
|
+
echo -e "${BLUE}Target branch: ${remote_branch}${NC}"
|
|
44
|
+
echo ""
|
|
45
|
+
echo "Recommended GitFlow workflow:"
|
|
46
|
+
echo " 1. Work on feature/SPEC-{ID} branch (created from develop)"
|
|
47
|
+
echo " 2. Push to feature/SPEC-{ID} and create PR to develop"
|
|
48
|
+
echo " 3. Merge into develop after code review"
|
|
49
|
+
echo " 4. When develop is stable, create PR from develop to main"
|
|
50
|
+
echo " 5. Release manager merges develop -> main with tag"
|
|
51
|
+
echo ""
|
|
52
|
+
echo -e "${GREEN}✓ Push will proceed (flexibility mode enabled)${NC}"
|
|
53
|
+
echo ""
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
# Check for delete operation
|
|
57
|
+
if [ "$local_oid" = "0000000000000000000000000000000000000000" ]; then
|
|
58
|
+
echo ""
|
|
59
|
+
echo -e "${RED}⚠️ WARNING: Attempting to delete main branch${NC}"
|
|
60
|
+
echo ""
|
|
61
|
+
echo -e "${YELLOW}This operation is highly discouraged.${NC}"
|
|
62
|
+
echo -e "${GREEN}✓ Push will proceed (flexibility mode enabled)${NC}"
|
|
63
|
+
echo ""
|
|
64
|
+
fi
|
|
65
|
+
|
|
66
|
+
# Check for force push attempts to main
|
|
67
|
+
if [ "$remote_branch" = "main" ] || [ "$remote_branch" = "master" ]; then
|
|
68
|
+
# Check if remote_oid exists (non-zero means we're trying to update existing ref)
|
|
69
|
+
if [ "$remote_oid" != "0000000000000000000000000000000000000000" ]; then
|
|
70
|
+
# Verify this is a fast-forward merge (no force push)
|
|
71
|
+
if ! git merge-base --is-ancestor "$remote_oid" "$local_oid" 2>/dev/null; then
|
|
72
|
+
echo ""
|
|
73
|
+
echo -e "${YELLOW}⚠️ ADVISORY: Force-push to main branch detected${NC}"
|
|
74
|
+
echo ""
|
|
75
|
+
echo "Recommended approach:"
|
|
76
|
+
echo " - Use GitHub PR with proper code review"
|
|
77
|
+
echo " - Ensure changes are merged via fast-forward"
|
|
78
|
+
echo ""
|
|
79
|
+
echo -e "${GREEN}✓ Push will proceed (flexibility mode enabled)${NC}"
|
|
80
|
+
echo ""
|
|
81
|
+
fi
|
|
82
|
+
fi
|
|
83
|
+
fi
|
|
84
|
+
fi
|
|
85
|
+
done
|
|
86
|
+
|
|
87
|
+
# All checks passed (or advisory warnings shown)
|
|
88
|
+
exit 0
|