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,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,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,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)
|