moai-adk 0.3.3__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 CHANGED
@@ -4,5 +4,5 @@
4
4
  SPEC-First TDD Framework with Alfred SuperAgent
5
5
  """
6
6
 
7
- __version__ = "0.3.3"
7
+ __version__ = "0.3.6"
8
8
  __all__ = ["__version__"]
@@ -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 current version if fetch fails.
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
- # Fallback to current version if PyPI check fails
30
- return __version__
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
- console.print(f" Current version: {current_version}")
77
- console.print(f" Latest version: {latest_version}")
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 current_version == latest_version:
82
- console.print("[green] Already up to date[/green]")
83
- else:
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 current_version == latest_version:
89
- # Check optimized status in config.json
90
- config_path = project_path / ".moai" / "config.json"
91
- if config_path.exists():
92
- try:
93
- config_data = json.loads(config_path.read_text())
94
- is_optimized = config_data.get("project", {}).get("optimized", False)
95
-
96
- if is_optimized:
97
- # Already up to date and optimized - exit silently
98
- return
99
- else:
100
- console.print("[yellow]⚠ Optimization needed[/yellow]")
101
- console.print("[dim]Use /alfred:0-project update for template optimization[/dim]")
102
- return
103
- except (json.JSONDecodeError, KeyError):
104
- # If config.json is invalid, proceed with update
105
- pass
106
- else:
107
- console.print("[green]✓ Already up to date[/green]")
108
- return
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
- # Phase 3: Resource (copy templates)
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
- config = {
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, config, progress_callback
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
- shutil.copy2(item, dst_item)
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
- shutil.copytree(item, dst_item)
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 (alfred folders overwritten, others preserved)")
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
- shutil.copy2(item, dst_item)
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 (user content preserved)")
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
- shutil.copy2(src, dst)
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