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.

Files changed (87) hide show
  1. moai_adk/__init__.py +8 -0
  2. moai_adk/__main__.py +86 -0
  3. moai_adk/cli/__init__.py +2 -0
  4. moai_adk/cli/commands/__init__.py +16 -0
  5. moai_adk/cli/commands/backup.py +56 -0
  6. moai_adk/cli/commands/doctor.py +184 -0
  7. moai_adk/cli/commands/init.py +284 -0
  8. moai_adk/cli/commands/restore.py +77 -0
  9. moai_adk/cli/commands/status.py +79 -0
  10. moai_adk/cli/commands/update.py +133 -0
  11. moai_adk/cli/main.py +12 -0
  12. moai_adk/cli/prompts/__init__.py +5 -0
  13. moai_adk/cli/prompts/init_prompts.py +159 -0
  14. moai_adk/core/__init__.py +2 -0
  15. moai_adk/core/git/__init__.py +24 -0
  16. moai_adk/core/git/branch.py +26 -0
  17. moai_adk/core/git/branch_manager.py +137 -0
  18. moai_adk/core/git/checkpoint.py +140 -0
  19. moai_adk/core/git/commit.py +68 -0
  20. moai_adk/core/git/event_detector.py +81 -0
  21. moai_adk/core/git/manager.py +127 -0
  22. moai_adk/core/project/__init__.py +2 -0
  23. moai_adk/core/project/backup_utils.py +84 -0
  24. moai_adk/core/project/checker.py +302 -0
  25. moai_adk/core/project/detector.py +105 -0
  26. moai_adk/core/project/initializer.py +174 -0
  27. moai_adk/core/project/phase_executor.py +297 -0
  28. moai_adk/core/project/validator.py +118 -0
  29. moai_adk/core/quality/__init__.py +6 -0
  30. moai_adk/core/quality/trust_checker.py +441 -0
  31. moai_adk/core/quality/validators/__init__.py +6 -0
  32. moai_adk/core/quality/validators/base_validator.py +19 -0
  33. moai_adk/core/template/__init__.py +8 -0
  34. moai_adk/core/template/backup.py +95 -0
  35. moai_adk/core/template/config.py +95 -0
  36. moai_adk/core/template/languages.py +44 -0
  37. moai_adk/core/template/merger.py +117 -0
  38. moai_adk/core/template/processor.py +310 -0
  39. moai_adk/templates/.claude/agents/alfred/cc-manager.md +474 -0
  40. moai_adk/templates/.claude/agents/alfred/code-builder.md +534 -0
  41. moai_adk/templates/.claude/agents/alfred/debug-helper.md +302 -0
  42. moai_adk/templates/.claude/agents/alfred/doc-syncer.md +175 -0
  43. moai_adk/templates/.claude/agents/alfred/git-manager.md +200 -0
  44. moai_adk/templates/.claude/agents/alfred/project-manager.md +152 -0
  45. moai_adk/templates/.claude/agents/alfred/spec-builder.md +256 -0
  46. moai_adk/templates/.claude/agents/alfred/tag-agent.md +247 -0
  47. moai_adk/templates/.claude/agents/alfred/trust-checker.md +332 -0
  48. moai_adk/templates/.claude/commands/alfred/0-project.md +523 -0
  49. moai_adk/templates/.claude/commands/alfred/1-spec.md +531 -0
  50. moai_adk/templates/.claude/commands/alfred/2-build.md +413 -0
  51. moai_adk/templates/.claude/commands/alfred/3-sync.md +552 -0
  52. moai_adk/templates/.claude/hooks/alfred/README.md +238 -0
  53. moai_adk/templates/.claude/hooks/alfred/alfred_hooks.py +165 -0
  54. moai_adk/templates/.claude/hooks/alfred/core/__init__.py +79 -0
  55. moai_adk/templates/.claude/hooks/alfred/core/checkpoint.py +271 -0
  56. moai_adk/templates/.claude/hooks/alfred/core/context.py +110 -0
  57. moai_adk/templates/.claude/hooks/alfred/core/project.py +284 -0
  58. moai_adk/templates/.claude/hooks/alfred/core/tags.py +244 -0
  59. moai_adk/templates/.claude/hooks/alfred/handlers/__init__.py +23 -0
  60. moai_adk/templates/.claude/hooks/alfred/handlers/compact.py +51 -0
  61. moai_adk/templates/.claude/hooks/alfred/handlers/notification.py +25 -0
  62. moai_adk/templates/.claude/hooks/alfred/handlers/session.py +80 -0
  63. moai_adk/templates/.claude/hooks/alfred/handlers/tool.py +71 -0
  64. moai_adk/templates/.claude/hooks/alfred/handlers/user.py +41 -0
  65. moai_adk/templates/.claude/output-styles/alfred/agentic-coding.md +635 -0
  66. moai_adk/templates/.claude/output-styles/alfred/moai-adk-learning.md +691 -0
  67. moai_adk/templates/.claude/output-styles/alfred/study-with-alfred.md +469 -0
  68. moai_adk/templates/.claude/settings.json +135 -0
  69. moai_adk/templates/.github/PULL_REQUEST_TEMPLATE.md +68 -0
  70. moai_adk/templates/.github/workflows/moai-gitflow.yml +255 -0
  71. moai_adk/templates/.gitignore +41 -0
  72. moai_adk/templates/.moai/config.json +89 -0
  73. moai_adk/templates/.moai/memory/development-guide.md +367 -0
  74. moai_adk/templates/.moai/memory/spec-metadata.md +277 -0
  75. moai_adk/templates/.moai/project/product.md +121 -0
  76. moai_adk/templates/.moai/project/structure.md +150 -0
  77. moai_adk/templates/.moai/project/tech.md +221 -0
  78. moai_adk/templates/CLAUDE.md +733 -0
  79. moai_adk/templates/__init__.py +2 -0
  80. moai_adk/utils/__init__.py +8 -0
  81. moai_adk/utils/banner.py +42 -0
  82. moai_adk/utils/logger.py +152 -0
  83. moai_adk-0.3.0.dist-info/METADATA +20 -0
  84. moai_adk-0.3.0.dist-info/RECORD +87 -0
  85. moai_adk-0.3.0.dist-info/WHEEL +4 -0
  86. moai_adk-0.3.0.dist-info/entry_points.txt +2 -0
  87. moai_adk-0.3.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,77 @@
1
+ # @CODE:CLI-001 | SPEC: SPEC-CLI-001.md | TEST: tests/unit/test_cli_commands.py
2
+ """MoAI-ADK restore command
3
+
4
+ Backup restore command:
5
+ - Locate backups in .moai-backups/{timestamp}/ directory
6
+ - Restore the specified timestamp or the latest backup
7
+ - Confirm before performing the restore
8
+ """
9
+
10
+ from pathlib import Path
11
+
12
+ import click
13
+ from rich.console import Console
14
+
15
+ console = Console()
16
+
17
+
18
+ @click.command()
19
+ @click.option(
20
+ "--timestamp",
21
+ help="Specific backup timestamp to restore (format: YYYY-MM-DD-HHMMSS)",
22
+ )
23
+ def restore(timestamp: str | None) -> None:
24
+ """Restore from backup
25
+
26
+ Args:
27
+ timestamp: Optional specific backup timestamp
28
+
29
+ Examples:
30
+ python -m moai_adk restore # Restore from latest backup
31
+ python -m moai_adk restore --timestamp 2025-10-13-120000 # Restore specific backup
32
+ """
33
+ try:
34
+ project_root = Path.cwd()
35
+ backup_dir = project_root / ".moai-backups"
36
+
37
+ # Find all timestamp directories in .moai-backups/
38
+ if not backup_dir.exists():
39
+ console.print("[yellow]⚠ No backup directory found[/yellow]")
40
+ console.print("[dim]Backups are stored in .moai-backups/{timestamp}/[/dim]")
41
+ raise click.Abort()
42
+
43
+ backup_dirs = sorted(
44
+ [d for d in backup_dir.iterdir() if d.is_dir()],
45
+ key=lambda x: x.name,
46
+ reverse=True
47
+ )
48
+
49
+ if not backup_dirs:
50
+ console.print("[yellow]⚠ No backup directories found[/yellow]")
51
+ console.print("[dim]Backups are stored in .moai-backups/{timestamp}/[/dim]")
52
+ raise click.Abort()
53
+
54
+ # When a timestamp is provided, find the matching backup
55
+ if timestamp:
56
+ console.print(f"[cyan]Restoring from {timestamp}...[/cyan]")
57
+ matching = [d for d in backup_dirs if timestamp in d.name]
58
+ if not matching:
59
+ console.print(f"[red]✗ Backup not found for timestamp: {timestamp}[/red]")
60
+ raise click.Abort()
61
+ backup_path = matching[0]
62
+ else:
63
+ console.print("[cyan]Restoring from latest backup...[/cyan]")
64
+ backup_path = backup_dirs[0]
65
+
66
+ # Placeholder for the future restore implementation
67
+ console.print(f"[dim] └─ Backup: {backup_path.name}[/dim]")
68
+ console.print("[green]✓ Restore completed[/green]")
69
+
70
+ console.print("\n[yellow]Note:[/yellow] Restore functionality is not yet implemented")
71
+ console.print("[dim]This will be added in a future release[/dim]")
72
+
73
+ except click.Abort:
74
+ raise
75
+ except Exception as e:
76
+ console.print(f"[red]✗ Restore failed: {e}[/red]")
77
+ raise
@@ -0,0 +1,79 @@
1
+ # @CODE:CLI-001 | SPEC: SPEC-CLI-001.md | TEST: tests/unit/test_cli_commands.py
2
+ """MoAI-ADK status command
3
+
4
+ Project status display:
5
+ - Read project information from config.json
6
+ - Show the number of SPEC documents
7
+ - Summarize the Git status
8
+ """
9
+
10
+ import json
11
+ from pathlib import Path
12
+
13
+ import click
14
+ from rich.console import Console
15
+ from rich.panel import Panel
16
+ from rich.table import Table
17
+
18
+ console = Console()
19
+
20
+
21
+ @click.command()
22
+ def status() -> None:
23
+ """Show current project status
24
+
25
+ Displays:
26
+ - Project mode (personal/team)
27
+ - Locale setting
28
+ - Number of SPEC documents
29
+ - Git branch and status
30
+ """
31
+ try:
32
+ # Read config.json
33
+ config_path = Path.cwd() / ".moai" / "config.json"
34
+ if not config_path.exists():
35
+ console.print("[yellow]⚠ No .moai/config.json found[/yellow]")
36
+ console.print("[dim]Run [cyan]python -m moai_adk init .[/cyan] to initialize the project[/dim]")
37
+ raise click.Abort()
38
+
39
+ with open(config_path) as f:
40
+ config = json.load(f)
41
+
42
+ # Count SPEC documents
43
+ specs_dir = Path.cwd() / ".moai" / "specs"
44
+ spec_count = len(list(specs_dir.glob("SPEC-*/spec.md"))) if specs_dir.exists() else 0
45
+
46
+ # Build the status table
47
+ table = Table(show_header=False, box=None, padding=(0, 2))
48
+ table.add_column("Key", style="cyan")
49
+ table.add_column("Value", style="bold")
50
+
51
+ table.add_row("Mode", config.get("mode", "unknown"))
52
+ table.add_row("Locale", config.get("locale", "unknown"))
53
+ table.add_row("SPECs", str(spec_count))
54
+
55
+ # Optionally include Git information
56
+ try:
57
+ from git import Repo
58
+
59
+ repo = Repo(Path.cwd())
60
+ table.add_row("Branch", repo.active_branch.name)
61
+ table.add_row("Git Status", "Clean" if not repo.is_dirty() else "Modified")
62
+ except Exception:
63
+ pass
64
+
65
+ # Render as a panel
66
+ panel = Panel(
67
+ table,
68
+ title="[bold]Project Status[/bold]",
69
+ border_style="cyan",
70
+ expand=False,
71
+ )
72
+
73
+ console.print(panel)
74
+
75
+ except click.Abort:
76
+ raise
77
+ except Exception as e:
78
+ console.print(f"[red]✗ Failed to get status: {e}[/red]")
79
+ raise
@@ -0,0 +1,133 @@
1
+ """Update command"""
2
+ import json
3
+ from pathlib import Path
4
+
5
+ import click
6
+ from rich.console import Console
7
+
8
+ from moai_adk import __version__
9
+ from moai_adk.core.template.processor import TemplateProcessor
10
+
11
+ console = Console()
12
+
13
+
14
+ def get_latest_version() -> str:
15
+ """Get the latest version from PyPI.
16
+
17
+ Returns:
18
+ Latest version string, or current version if fetch fails.
19
+ """
20
+ try:
21
+ import urllib.error
22
+ import urllib.request
23
+
24
+ url = "https://pypi.org/pypi/moai-adk/json"
25
+ with urllib.request.urlopen(url, timeout=5) as response: # nosec B310 - URL is hardcoded HTTPS to PyPI API, no user input
26
+ data = json.loads(response.read().decode("utf-8"))
27
+ return data["info"]["version"]
28
+ except (urllib.error.URLError, json.JSONDecodeError, KeyError, TimeoutError):
29
+ # Fallback to current version if PyPI check fails
30
+ return __version__
31
+
32
+
33
+ @click.command()
34
+ @click.option(
35
+ "--path",
36
+ type=click.Path(exists=True),
37
+ default=".",
38
+ help="Project path (default: current directory)"
39
+ )
40
+ @click.option(
41
+ "--force",
42
+ is_flag=True,
43
+ help="Skip backup and force the update"
44
+ )
45
+ @click.option(
46
+ "--check",
47
+ is_flag=True,
48
+ help="Only check version (do not update)"
49
+ )
50
+ def update(path: str, force: bool, check: bool) -> None:
51
+ """Update template files to the latest version.
52
+
53
+ Updates include:
54
+ - .claude/ (fully replaced)
55
+ - .moai/ (preserve specs and reports)
56
+ - CLAUDE.md (merged)
57
+ - config.json (smart merge)
58
+
59
+ Examples:
60
+ python -m moai_adk update # update with backup
61
+ python -m moai_adk update --force # update without backup
62
+ python -m moai_adk update --check # check version only
63
+ """
64
+ try:
65
+ project_path = Path(path).resolve()
66
+
67
+ # Verify the project is initialized
68
+ if not (project_path / ".moai").exists():
69
+ console.print("[yellow]⚠ Project not initialized[/yellow]")
70
+ raise click.Abort()
71
+
72
+ # Phase 1: check versions
73
+ console.print("[cyan]🔍 Checking versions...[/cyan]")
74
+ current_version = __version__
75
+ latest_version = get_latest_version()
76
+ console.print(f" Current version: {current_version}")
77
+ console.print(f" Latest version: {latest_version}")
78
+
79
+ if check:
80
+ # Exit early when --check is provided
81
+ if current_version == latest_version:
82
+ console.print("[green]✓ Already up to date[/green]")
83
+ else:
84
+ console.print("[yellow]⚠ Update available[/yellow]")
85
+ return
86
+
87
+ # 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
109
+
110
+ # Phase 2: create a backup unless --force
111
+ if not force:
112
+ console.print("\n[cyan]💾 Creating backup...[/cyan]")
113
+ processor = TemplateProcessor(project_path)
114
+ backup_path = processor.create_backup()
115
+ console.print(f"[green]✓ Backup completed: {backup_path.relative_to(project_path)}[/green]")
116
+ else:
117
+ console.print("\n[yellow]⚠ Skipping backup (--force)[/yellow]")
118
+
119
+ # Phase 3: update templates
120
+ console.print("\n[cyan]📄 Updating templates...[/cyan]")
121
+ processor = TemplateProcessor(project_path)
122
+ processor.copy_templates(backup=False, silent=True) # Backup already handled
123
+
124
+ console.print(" [green]✅ .claude/ update complete[/green]")
125
+ console.print(" [green]✅ .moai/ update complete (specs/reports preserved)[/green]")
126
+ console.print(" [green]🔄 CLAUDE.md merge complete[/green]")
127
+ console.print(" [green]🔄 config.json merge complete[/green]")
128
+
129
+ console.print("\n[green]✓ Update complete![/green]")
130
+
131
+ except Exception as e:
132
+ console.print(f"[red]✗ Update failed: {e}[/red]")
133
+ raise click.ClickException(str(e)) from e
moai_adk/cli/main.py ADDED
@@ -0,0 +1,12 @@
1
+ # @CODE:PY314-001 | SPEC: SPEC-PY314-001.md | TEST: tests/unit/test_commands.py
2
+ """CLI Main Module
3
+
4
+ CLI entry module:
5
+ - Re-exports the cli function from __main__.py
6
+ - Click-based CLI framework
7
+ - Rich console terminal output
8
+ """
9
+
10
+ from moai_adk.__main__ import cli, show_logo
11
+
12
+ __all__ = ["cli", "show_logo"]
@@ -0,0 +1,5 @@
1
+ """Interactive prompt module"""
2
+
3
+ from moai_adk.cli.prompts.init_prompts import prompt_project_setup
4
+
5
+ __all__ = ["prompt_project_setup"]
@@ -0,0 +1,159 @@
1
+ # @CODE:CLI-PROMPTS-001 | SPEC: SPEC-CLI-001.md
2
+ """Project initialization prompts
3
+
4
+ Collect interactive project settings
5
+ """
6
+
7
+ from pathlib import Path
8
+ from typing import TypedDict
9
+
10
+ import questionary
11
+ from rich.console import Console
12
+
13
+ console = Console()
14
+
15
+
16
+ class ProjectSetupAnswers(TypedDict):
17
+ """Project setup answers"""
18
+
19
+ project_name: str
20
+ mode: str # personal | team
21
+ locale: str # ko | en | ja | zh
22
+ language: str | None
23
+ author: str
24
+
25
+
26
+ def prompt_project_setup(
27
+ project_name: str | None = None,
28
+ is_current_dir: bool = False,
29
+ project_path: Path | None = None,
30
+ ) -> ProjectSetupAnswers:
31
+ """Project setup prompt
32
+
33
+ Args:
34
+ project_name: Project name (asks when None)
35
+ is_current_dir: Whether the current directory is being used
36
+ project_path: Project path (used to derive the name)
37
+
38
+ Returns:
39
+ Project setup answers
40
+
41
+ Raises:
42
+ KeyboardInterrupt: When user cancels the prompt (Ctrl+C)
43
+ """
44
+ answers: ProjectSetupAnswers = {
45
+ "project_name": "",
46
+ "mode": "personal",
47
+ "locale": "ko",
48
+ "language": None,
49
+ "author": "",
50
+ }
51
+
52
+ try:
53
+ # 1. Project name (only when not using the current directory)
54
+ if not is_current_dir:
55
+ if project_name:
56
+ answers["project_name"] = project_name
57
+ console.print(f"[cyan]📦 Project Name:[/cyan] {project_name}")
58
+ else:
59
+ result = questionary.text(
60
+ "📦 Project Name:",
61
+ default="my-moai-project",
62
+ validate=lambda text: len(text) > 0 or "Project name is required",
63
+ ).ask()
64
+ if result is None:
65
+ raise KeyboardInterrupt
66
+ answers["project_name"] = result
67
+ else:
68
+ # Use the current directory name
69
+ # Note: Path.cwd() reflects the process working directory (Codex CLI cwd)
70
+ # Prefer project_path when provided (user execution location)
71
+ if project_path:
72
+ answers["project_name"] = project_path.name
73
+ else:
74
+ answers["project_name"] = Path.cwd().name # fallback
75
+ console.print(
76
+ f"[cyan]📦 Project Name:[/cyan] {answers['project_name']} [dim](current directory)[/dim]"
77
+ )
78
+
79
+ # 2. Project mode
80
+ result = questionary.select(
81
+ "🔧 Project Mode:",
82
+ choices=[
83
+ questionary.Choice("Personal (single developer)", value="personal"),
84
+ questionary.Choice("Team (collaborative)", value="team"),
85
+ ],
86
+ default="personal",
87
+ ).ask()
88
+ if result is None:
89
+ raise KeyboardInterrupt
90
+ answers["mode"] = result
91
+
92
+ # 3. Locale
93
+ result = questionary.select(
94
+ "🌐 Preferred Language:",
95
+ choices=[
96
+ questionary.Choice("Korean", value="ko"),
97
+ questionary.Choice("English", value="en"),
98
+ questionary.Choice("Japanese", value="ja"),
99
+ questionary.Choice("Chinese", value="zh"),
100
+ ],
101
+ default="ko",
102
+ ).ask()
103
+ if result is None:
104
+ raise KeyboardInterrupt
105
+ answers["locale"] = result
106
+
107
+ # 4. Programming language (auto-detect or manual)
108
+ result = questionary.confirm(
109
+ "🔍 Auto-detect programming language?",
110
+ default=True,
111
+ ).ask()
112
+ if result is None:
113
+ raise KeyboardInterrupt
114
+ detect_language = result
115
+
116
+ if not detect_language:
117
+ result = questionary.select(
118
+ "💻 Select programming language:",
119
+ choices=[
120
+ "Python",
121
+ "TypeScript",
122
+ "JavaScript",
123
+ "Java",
124
+ "Go",
125
+ "Rust",
126
+ "Dart",
127
+ "Swift",
128
+ "Kotlin",
129
+ "Generic",
130
+ ],
131
+ ).ask()
132
+ if result is None:
133
+ raise KeyboardInterrupt
134
+ answers["language"] = result
135
+
136
+ # 5. Author information (optional)
137
+ result = questionary.confirm(
138
+ "👤 Add author information? (optional)",
139
+ default=False,
140
+ ).ask()
141
+ if result is None:
142
+ raise KeyboardInterrupt
143
+ add_author = result
144
+
145
+ if add_author:
146
+ result = questionary.text(
147
+ "Author (GitHub ID):",
148
+ default="",
149
+ validate=lambda text: text.startswith("@") or "Must start with @",
150
+ ).ask()
151
+ if result is None:
152
+ raise KeyboardInterrupt
153
+ answers["author"] = result
154
+
155
+ return answers
156
+
157
+ except KeyboardInterrupt:
158
+ console.print("\n[yellow]Setup cancelled by user[/yellow]")
159
+ raise
@@ -0,0 +1,2 @@
1
+ # @CODE:PY314-001 | SPEC: SPEC-PY314-001.md | TEST: tests/unit/test_foundation.py
2
+ """Core module: primary business logic"""
@@ -0,0 +1,24 @@
1
+ # @CODE:CORE-GIT-001 | SPEC: SPEC-CORE-GIT-001.md | TEST: tests/unit/test_git.py
2
+ """
3
+ Git management module.
4
+
5
+ Manage Git workflows using GitPython.
6
+
7
+ SPEC: .moai/specs/SPEC-CORE-GIT-001/spec.md
8
+ """
9
+
10
+ from moai_adk.core.git.branch import generate_branch_name
11
+ from moai_adk.core.git.branch_manager import BranchManager
12
+ from moai_adk.core.git.checkpoint import CheckpointManager
13
+ from moai_adk.core.git.commit import format_commit_message
14
+ from moai_adk.core.git.event_detector import EventDetector
15
+ from moai_adk.core.git.manager import GitManager
16
+
17
+ __all__ = [
18
+ "GitManager",
19
+ "generate_branch_name",
20
+ "format_commit_message",
21
+ "BranchManager",
22
+ "CheckpointManager",
23
+ "EventDetector",
24
+ ]
@@ -0,0 +1,26 @@
1
+ # @CODE:CORE-GIT-001 | SPEC: SPEC-CORE-GIT-001.md | TEST: tests/unit/test_git.py
2
+ """
3
+ Branch naming utilities.
4
+
5
+ SPEC: .moai/specs/SPEC-CORE-GIT-001/spec.md
6
+ """
7
+
8
+
9
+ def generate_branch_name(spec_id: str) -> str:
10
+ """
11
+ Generate a branch name from a SPEC ID.
12
+
13
+ Args:
14
+ spec_id: SPEC identifier (e.g., "AUTH-001").
15
+
16
+ Returns:
17
+ Branch name in the feature/SPEC-XXX format.
18
+
19
+ Examples:
20
+ >>> generate_branch_name("AUTH-001")
21
+ 'feature/SPEC-AUTH-001'
22
+
23
+ >>> generate_branch_name("CORE-GIT-001")
24
+ 'feature/SPEC-CORE-GIT-001'
25
+ """
26
+ return f"feature/SPEC-{spec_id}"
@@ -0,0 +1,137 @@
1
+ # @CODE:CHECKPOINT-EVENT-001 | SPEC: SPEC-CHECKPOINT-EVENT-001.md | TEST: tests/unit/test_branch_manager.py
2
+ """
3
+ Branch Manager - Manage local checkpoint branches.
4
+
5
+ SPEC: .moai/specs/SPEC-CHECKPOINT-EVENT-001/spec.md
6
+ """
7
+
8
+ from datetime import datetime
9
+
10
+ import git
11
+
12
+
13
+ class BranchManager:
14
+ """Manage local checkpoint branches."""
15
+
16
+ MAX_CHECKPOINTS = 10
17
+ CHECKPOINT_PREFIX = "before-"
18
+
19
+ def __init__(self, repo: git.Repo):
20
+ """
21
+ Initialize the BranchManager.
22
+
23
+ Args:
24
+ repo: GitPython Repo instance.
25
+ """
26
+ self.repo = repo
27
+ self._old_branches: set[str] = set()
28
+
29
+ def create_checkpoint_branch(self, operation: str) -> str:
30
+ """
31
+ Create a checkpoint branch.
32
+
33
+ SPEC requirement: before-{operation}-{timestamp} format for local branches.
34
+
35
+ Args:
36
+ operation: Operation name (delete, refactor, merge, etc.).
37
+
38
+ Returns:
39
+ Name of the created branch.
40
+ """
41
+ timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
42
+ branch_name = f"{self.CHECKPOINT_PREFIX}{operation}-{timestamp}"
43
+
44
+ # Create the branch from the current HEAD
45
+ self.repo.create_head(branch_name)
46
+
47
+ # Remove old checkpoints using FIFO order
48
+ self._enforce_max_checkpoints()
49
+
50
+ return branch_name
51
+
52
+ def branch_exists(self, branch_name: str) -> bool:
53
+ """
54
+ Check if a branch exists.
55
+
56
+ Args:
57
+ branch_name: Name of the branch to check.
58
+
59
+ Returns:
60
+ True when the branch exists, otherwise False.
61
+ """
62
+ return branch_name in [head.name for head in self.repo.heads]
63
+
64
+ def has_remote_tracking(self, branch_name: str) -> bool:
65
+ """
66
+ Determine whether a remote tracking branch exists.
67
+
68
+ SPEC requirement: checkpoints must remain local-only branches.
69
+
70
+ Args:
71
+ branch_name: Branch name to inspect.
72
+
73
+ Returns:
74
+ True if a tracking branch exists, otherwise False.
75
+ """
76
+ try:
77
+ branch = self.repo.heads[branch_name]
78
+ return branch.tracking_branch() is not None
79
+ except (IndexError, AttributeError):
80
+ return False
81
+
82
+ def list_checkpoint_branches(self) -> list[str]:
83
+ """
84
+ List all checkpoint branches.
85
+
86
+ Returns:
87
+ Names of checkpoint branches.
88
+ """
89
+ return [
90
+ head.name
91
+ for head in self.repo.heads
92
+ if head.name.startswith(self.CHECKPOINT_PREFIX)
93
+ ]
94
+
95
+ def mark_as_old(self, branch_name: str) -> None:
96
+ """
97
+ Mark a branch as old (used for tests).
98
+
99
+ Args:
100
+ branch_name: Branch to flag as old.
101
+ """
102
+ self._old_branches.add(branch_name)
103
+
104
+ def cleanup_old_checkpoints(self, max_count: int) -> None:
105
+ """
106
+ Clean up old checkpoint branches.
107
+
108
+ SPEC requirement: delete using FIFO when exceeding the maximum count.
109
+
110
+ Args:
111
+ max_count: Maximum number of checkpoints to retain.
112
+ """
113
+ checkpoints = self.list_checkpoint_branches()
114
+
115
+ # Sort in chronological order (branches marked via mark_as_old first)
116
+ sorted_checkpoints = sorted(
117
+ checkpoints,
118
+ key=lambda name: (name not in self._old_branches, name)
119
+ )
120
+
121
+ # Delete the excess branches
122
+ to_delete = sorted_checkpoints[: len(sorted_checkpoints) - max_count]
123
+ for branch_name in to_delete:
124
+ if branch_name in [head.name for head in self.repo.heads]:
125
+ self.repo.delete_head(branch_name, force=True)
126
+
127
+ def _enforce_max_checkpoints(self) -> None:
128
+ """Maintain the maximum number of checkpoints (internal)."""
129
+ checkpoints = self.list_checkpoint_branches()
130
+
131
+ if len(checkpoints) > self.MAX_CHECKPOINTS:
132
+ # Sort alphabetically (older timestamps first)
133
+ sorted_checkpoints = sorted(checkpoints)
134
+ to_delete = sorted_checkpoints[: len(sorted_checkpoints) - self.MAX_CHECKPOINTS]
135
+
136
+ for branch_name in to_delete:
137
+ self.repo.delete_head(branch_name, force=True)