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,140 @@
|
|
|
1
|
+
# @CODE:CHECKPOINT-EVENT-001 | SPEC: SPEC-CHECKPOINT-EVENT-001.md | TEST: tests/unit/test_checkpoint.py
|
|
2
|
+
"""
|
|
3
|
+
Checkpoint Manager - Event-driven checkpoint system.
|
|
4
|
+
|
|
5
|
+
SPEC: .moai/specs/SPEC-CHECKPOINT-EVENT-001/spec.md
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
import git
|
|
13
|
+
|
|
14
|
+
from moai_adk.core.git.branch_manager import BranchManager
|
|
15
|
+
from moai_adk.core.git.event_detector import EventDetector
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class CheckpointManager:
|
|
19
|
+
"""Manage creation and restoration of event-driven checkpoints."""
|
|
20
|
+
|
|
21
|
+
def __init__(self, repo: git.Repo, project_root: Path):
|
|
22
|
+
"""
|
|
23
|
+
Initialize the CheckpointManager.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
repo: GitPython Repo instance.
|
|
27
|
+
project_root: Project root directory.
|
|
28
|
+
"""
|
|
29
|
+
self.repo = repo
|
|
30
|
+
self.project_root = project_root
|
|
31
|
+
self.event_detector = EventDetector()
|
|
32
|
+
self.branch_manager = BranchManager(repo)
|
|
33
|
+
self.log_file = project_root / ".moai" / "checkpoints.log"
|
|
34
|
+
|
|
35
|
+
# Ensure the log directory exists
|
|
36
|
+
self.log_file.parent.mkdir(parents=True, exist_ok=True)
|
|
37
|
+
|
|
38
|
+
def create_checkpoint_if_risky(
|
|
39
|
+
self,
|
|
40
|
+
operation: str,
|
|
41
|
+
deleted_files: Optional[list[str]] = None,
|
|
42
|
+
renamed_files: Optional[list[tuple[str, str]]] = None,
|
|
43
|
+
modified_files: Optional[list[Path]] = None,
|
|
44
|
+
) -> Optional[str]:
|
|
45
|
+
"""
|
|
46
|
+
Create a checkpoint when a risky operation is detected.
|
|
47
|
+
|
|
48
|
+
SPEC requirement: automatically create a checkpoint for risky actions.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
operation: Operation type.
|
|
52
|
+
deleted_files: Files scheduled for deletion.
|
|
53
|
+
renamed_files: Files that will be renamed.
|
|
54
|
+
modified_files: Files that will be modified.
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
Created checkpoint ID (branch name) or None when the operation is safe.
|
|
58
|
+
"""
|
|
59
|
+
is_risky = False
|
|
60
|
+
|
|
61
|
+
# Identify large deletion operations
|
|
62
|
+
if deleted_files and self.event_detector.is_risky_deletion(deleted_files):
|
|
63
|
+
is_risky = True
|
|
64
|
+
|
|
65
|
+
# Identify large-scale refactoring
|
|
66
|
+
if renamed_files and self.event_detector.is_risky_refactoring(renamed_files):
|
|
67
|
+
is_risky = True
|
|
68
|
+
|
|
69
|
+
# Check for critical file modifications
|
|
70
|
+
if modified_files:
|
|
71
|
+
for file_path in modified_files:
|
|
72
|
+
if self.event_detector.is_critical_file(file_path):
|
|
73
|
+
is_risky = True
|
|
74
|
+
break
|
|
75
|
+
|
|
76
|
+
if not is_risky:
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
# Create a checkpoint
|
|
80
|
+
checkpoint_id = self.branch_manager.create_checkpoint_branch(operation)
|
|
81
|
+
|
|
82
|
+
# Record checkpoint metadata
|
|
83
|
+
self._log_checkpoint(checkpoint_id, operation)
|
|
84
|
+
|
|
85
|
+
return checkpoint_id
|
|
86
|
+
|
|
87
|
+
def restore_checkpoint(self, checkpoint_id: str) -> None:
|
|
88
|
+
"""
|
|
89
|
+
Restore the repository to the specified checkpoint.
|
|
90
|
+
|
|
91
|
+
SPEC requirement: capture the current state as a new checkpoint before restoring.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
checkpoint_id: Target checkpoint ID (branch name).
|
|
95
|
+
"""
|
|
96
|
+
# Save current state as a safety checkpoint before restoring
|
|
97
|
+
safety_checkpoint = self.branch_manager.create_checkpoint_branch("restore")
|
|
98
|
+
self._log_checkpoint(safety_checkpoint, "restore", is_safety=True)
|
|
99
|
+
|
|
100
|
+
# Check out the checkpoint branch
|
|
101
|
+
self.repo.git.checkout(checkpoint_id)
|
|
102
|
+
|
|
103
|
+
def list_checkpoints(self) -> list[str]:
|
|
104
|
+
"""
|
|
105
|
+
List all checkpoints.
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
List of checkpoint IDs.
|
|
109
|
+
"""
|
|
110
|
+
return self.branch_manager.list_checkpoint_branches()
|
|
111
|
+
|
|
112
|
+
def _log_checkpoint(
|
|
113
|
+
self,
|
|
114
|
+
checkpoint_id: str,
|
|
115
|
+
operation: str,
|
|
116
|
+
is_safety: bool = False
|
|
117
|
+
) -> None:
|
|
118
|
+
"""
|
|
119
|
+
Append checkpoint metadata to the log file.
|
|
120
|
+
|
|
121
|
+
SPEC requirement: write metadata to .moai/checkpoints.log.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
checkpoint_id: Checkpoint identifier.
|
|
125
|
+
operation: Operation type.
|
|
126
|
+
is_safety: Whether the checkpoint was created for safety.
|
|
127
|
+
"""
|
|
128
|
+
timestamp = datetime.now().isoformat()
|
|
129
|
+
|
|
130
|
+
log_entry = f"""---
|
|
131
|
+
checkpoint_id: {checkpoint_id}
|
|
132
|
+
operation: {operation}
|
|
133
|
+
timestamp: {timestamp}
|
|
134
|
+
is_safety: {is_safety}
|
|
135
|
+
---
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
# Append the entry to the log
|
|
139
|
+
with open(self.log_file, "a", encoding="utf-8") as f:
|
|
140
|
+
f.write(log_entry)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# @CODE:CORE-GIT-001 | SPEC: SPEC-CORE-GIT-001.md | TEST: tests/unit/test_git.py
|
|
2
|
+
"""
|
|
3
|
+
Commit message formatting utilities.
|
|
4
|
+
|
|
5
|
+
SPEC: .moai/specs/SPEC-CORE-GIT-001/spec.md
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Literal
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def format_commit_message(
|
|
12
|
+
stage: Literal["red", "green", "refactor", "docs"],
|
|
13
|
+
description: str,
|
|
14
|
+
locale: str = "ko",
|
|
15
|
+
) -> str:
|
|
16
|
+
"""
|
|
17
|
+
Generate a commit message for each TDD stage.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
stage: TDD stage (red, green, refactor, docs).
|
|
21
|
+
description: Commit description text.
|
|
22
|
+
locale: Language code (ko, en, ja, zh).
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
Formatted commit message.
|
|
26
|
+
|
|
27
|
+
Examples:
|
|
28
|
+
>>> format_commit_message("red", "Add failing authentication test", "ko")
|
|
29
|
+
'🔴 RED: Add failing authentication test'
|
|
30
|
+
|
|
31
|
+
>>> format_commit_message("green", "Implement authentication", "en")
|
|
32
|
+
'🟢 GREEN: Implement authentication'
|
|
33
|
+
|
|
34
|
+
>>> format_commit_message("refactor", "Improve code structure", "ko")
|
|
35
|
+
'♻️ REFACTOR: Improve code structure'
|
|
36
|
+
"""
|
|
37
|
+
templates = {
|
|
38
|
+
"ko": {
|
|
39
|
+
"red": "🔴 RED: {desc}",
|
|
40
|
+
"green": "🟢 GREEN: {desc}",
|
|
41
|
+
"refactor": "♻️ REFACTOR: {desc}",
|
|
42
|
+
"docs": "📝 DOCS: {desc}",
|
|
43
|
+
},
|
|
44
|
+
"en": {
|
|
45
|
+
"red": "🔴 RED: {desc}",
|
|
46
|
+
"green": "🟢 GREEN: {desc}",
|
|
47
|
+
"refactor": "♻️ REFACTOR: {desc}",
|
|
48
|
+
"docs": "📝 DOCS: {desc}",
|
|
49
|
+
},
|
|
50
|
+
"ja": {
|
|
51
|
+
"red": "🔴 RED: {desc}",
|
|
52
|
+
"green": "🟢 GREEN: {desc}",
|
|
53
|
+
"refactor": "♻️ REFACTOR: {desc}",
|
|
54
|
+
"docs": "📝 DOCS: {desc}",
|
|
55
|
+
},
|
|
56
|
+
"zh": {
|
|
57
|
+
"red": "🔴 RED: {desc}",
|
|
58
|
+
"green": "🟢 GREEN: {desc}",
|
|
59
|
+
"refactor": "♻️ REFACTOR: {desc}",
|
|
60
|
+
"docs": "📝 DOCS: {desc}",
|
|
61
|
+
},
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
template = templates.get(locale, templates["en"]).get(stage.lower())
|
|
65
|
+
if not template:
|
|
66
|
+
raise ValueError(f"Invalid stage: {stage}")
|
|
67
|
+
|
|
68
|
+
return template.format(desc=description)
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# @CODE:CHECKPOINT-EVENT-001 | SPEC: SPEC-CHECKPOINT-EVENT-001.md | TEST: tests/unit/test_event_detector.py
|
|
2
|
+
"""
|
|
3
|
+
Event Detector - Identify risky operations.
|
|
4
|
+
|
|
5
|
+
SPEC: .moai/specs/SPEC-CHECKPOINT-EVENT-001/spec.md
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class EventDetector:
|
|
12
|
+
"""Detect potentially risky operations."""
|
|
13
|
+
|
|
14
|
+
# @CODE:CHECKPOINT-EVENT-001:DOMAIN - Critical file list
|
|
15
|
+
CRITICAL_FILES = {
|
|
16
|
+
"CLAUDE.md",
|
|
17
|
+
"config.json",
|
|
18
|
+
".moai/config.json",
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
CRITICAL_DIRS = {
|
|
22
|
+
".moai/memory",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
def is_risky_deletion(self, deleted_files: list[str]) -> bool:
|
|
26
|
+
"""
|
|
27
|
+
Detect large-scale file deletions.
|
|
28
|
+
|
|
29
|
+
SPEC requirement: deleting 10 or more files counts as risky.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
deleted_files: Files slated for deletion.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
True when 10 or more files are deleted, otherwise False.
|
|
36
|
+
"""
|
|
37
|
+
return len(deleted_files) >= 10
|
|
38
|
+
|
|
39
|
+
def is_risky_refactoring(self, renamed_files: list[tuple[str, str]]) -> bool:
|
|
40
|
+
"""
|
|
41
|
+
Detect large-scale refactoring.
|
|
42
|
+
|
|
43
|
+
SPEC requirement: renaming 10 or more files counts as risky.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
renamed_files: List of (old_name, new_name) pairs.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
True when 10 or more files are renamed, otherwise False.
|
|
50
|
+
"""
|
|
51
|
+
return len(renamed_files) >= 10
|
|
52
|
+
|
|
53
|
+
def is_critical_file(self, file_path: Path) -> bool:
|
|
54
|
+
"""
|
|
55
|
+
Determine whether the file is critical.
|
|
56
|
+
|
|
57
|
+
SPEC requirement: modifying CLAUDE.md, config.json, or .moai/memory/*.md is risky.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
file_path: File path to inspect.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
True when the file is critical, otherwise False.
|
|
64
|
+
"""
|
|
65
|
+
# Check whether the file name is in the critical list
|
|
66
|
+
if file_path.name in self.CRITICAL_FILES:
|
|
67
|
+
return True
|
|
68
|
+
|
|
69
|
+
# Convert to string for further checks
|
|
70
|
+
path_str = str(file_path)
|
|
71
|
+
|
|
72
|
+
# Detect .moai/config.json paths
|
|
73
|
+
if ".moai/config.json" in path_str or ".moai\\config.json" in path_str:
|
|
74
|
+
return True
|
|
75
|
+
|
|
76
|
+
# Detect files inside the .moai/memory/ directory
|
|
77
|
+
for critical_dir in self.CRITICAL_DIRS:
|
|
78
|
+
if critical_dir in path_str or critical_dir.replace("/", "\\") in path_str:
|
|
79
|
+
return True
|
|
80
|
+
|
|
81
|
+
return False
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# @CODE:CORE-GIT-001 | SPEC: SPEC-CORE-GIT-001.md | TEST: tests/unit/test_git.py
|
|
2
|
+
"""
|
|
3
|
+
Git repository management built on GitPython.
|
|
4
|
+
|
|
5
|
+
SPEC: .moai/specs/SPEC-CORE-GIT-001/spec.md
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from git import InvalidGitRepositoryError, Repo
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class GitManager:
|
|
12
|
+
"""Manage interactions with a Git repository."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, repo_path: str = "."):
|
|
15
|
+
"""
|
|
16
|
+
Initialize the GitManager.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
repo_path: Path to the Git repository (default: current directory)
|
|
20
|
+
|
|
21
|
+
Raises:
|
|
22
|
+
InvalidGitRepositoryError: Raised when the path is not a Git repository.
|
|
23
|
+
"""
|
|
24
|
+
self.repo = Repo(repo_path)
|
|
25
|
+
self.git = self.repo.git
|
|
26
|
+
|
|
27
|
+
def is_repo(self) -> bool:
|
|
28
|
+
"""
|
|
29
|
+
Check whether the path points to a Git repository.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
True when the location is a Git repository, otherwise False.
|
|
33
|
+
|
|
34
|
+
Examples:
|
|
35
|
+
>>> manager = GitManager("/path/to/repo")
|
|
36
|
+
>>> manager.is_repo()
|
|
37
|
+
True
|
|
38
|
+
"""
|
|
39
|
+
try:
|
|
40
|
+
_ = self.repo.git_dir
|
|
41
|
+
return True
|
|
42
|
+
except (InvalidGitRepositoryError, Exception):
|
|
43
|
+
return False
|
|
44
|
+
|
|
45
|
+
def current_branch(self) -> str:
|
|
46
|
+
"""
|
|
47
|
+
Return the active branch name.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
Name of the currently checked-out branch.
|
|
51
|
+
|
|
52
|
+
Examples:
|
|
53
|
+
>>> manager = GitManager()
|
|
54
|
+
>>> manager.current_branch()
|
|
55
|
+
'main'
|
|
56
|
+
"""
|
|
57
|
+
return self.repo.active_branch.name
|
|
58
|
+
|
|
59
|
+
def is_dirty(self) -> bool:
|
|
60
|
+
"""
|
|
61
|
+
Check whether the working tree has uncommitted changes.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
True when the worktree is dirty, otherwise False.
|
|
65
|
+
|
|
66
|
+
Examples:
|
|
67
|
+
>>> manager = GitManager()
|
|
68
|
+
>>> manager.is_dirty()
|
|
69
|
+
False
|
|
70
|
+
"""
|
|
71
|
+
return self.repo.is_dirty()
|
|
72
|
+
|
|
73
|
+
def create_branch(self, branch_name: str, from_branch: str | None = None) -> None:
|
|
74
|
+
"""
|
|
75
|
+
Create and switch to a new branch.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
branch_name: Name of the branch to create.
|
|
79
|
+
from_branch: Base branch (default: current branch).
|
|
80
|
+
|
|
81
|
+
Examples:
|
|
82
|
+
>>> manager = GitManager()
|
|
83
|
+
>>> manager.create_branch("feature/SPEC-AUTH-001")
|
|
84
|
+
>>> manager.current_branch()
|
|
85
|
+
'feature/SPEC-AUTH-001'
|
|
86
|
+
"""
|
|
87
|
+
if from_branch:
|
|
88
|
+
self.git.checkout("-b", branch_name, from_branch)
|
|
89
|
+
else:
|
|
90
|
+
self.git.checkout("-b", branch_name)
|
|
91
|
+
|
|
92
|
+
def commit(self, message: str, files: list[str] | None = None) -> None:
|
|
93
|
+
"""
|
|
94
|
+
Stage files and create a commit.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
message: Commit message.
|
|
98
|
+
files: Optional list of files to commit (default: all changes).
|
|
99
|
+
|
|
100
|
+
Examples:
|
|
101
|
+
>>> manager = GitManager()
|
|
102
|
+
>>> manager.commit("feat: add authentication", files=["auth.py"])
|
|
103
|
+
"""
|
|
104
|
+
if files:
|
|
105
|
+
self.repo.index.add(files)
|
|
106
|
+
else:
|
|
107
|
+
self.git.add(A=True)
|
|
108
|
+
|
|
109
|
+
self.repo.index.commit(message)
|
|
110
|
+
|
|
111
|
+
def push(self, branch: str | None = None, set_upstream: bool = False) -> None:
|
|
112
|
+
"""
|
|
113
|
+
Push commits to the remote repository.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
branch: Branch to push (default: current branch).
|
|
117
|
+
set_upstream: Whether to set the upstream tracking branch.
|
|
118
|
+
|
|
119
|
+
Examples:
|
|
120
|
+
>>> manager = GitManager()
|
|
121
|
+
>>> manager.push(set_upstream=True)
|
|
122
|
+
"""
|
|
123
|
+
if set_upstream:
|
|
124
|
+
target_branch = branch or self.current_branch()
|
|
125
|
+
self.git.push("--set-upstream", "origin", target_branch)
|
|
126
|
+
else:
|
|
127
|
+
self.git.push()
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# @CODE:INIT-003:BACKUP | SPEC: .moai/specs/SPEC-INIT-003/spec.md | TEST: tests/unit/test_backup_utils.py
|
|
2
|
+
"""Backup utility module (SPEC-INIT-003 v0.3.0)
|
|
3
|
+
|
|
4
|
+
Selective backup strategy:
|
|
5
|
+
- Back up only the required files (OR condition)
|
|
6
|
+
- Backup path: .moai-backups/{timestamp}/ (v0.3.0)
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
# Backup targets (OR condition - back up when any exist)
|
|
13
|
+
BACKUP_TARGETS = [
|
|
14
|
+
".moai/config.json",
|
|
15
|
+
".moai/project/",
|
|
16
|
+
".moai/memory/",
|
|
17
|
+
".claude/",
|
|
18
|
+
"CLAUDE.md",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
# User data protection paths (excluded from backups)
|
|
22
|
+
PROTECTED_PATHS = [
|
|
23
|
+
".moai/specs/",
|
|
24
|
+
".moai/reports/",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def has_any_moai_files(project_path: Path) -> bool:
|
|
29
|
+
"""Check whether any MoAI-ADK files exist (OR condition).
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
project_path: Project path.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
True when any backup target exists.
|
|
36
|
+
"""
|
|
37
|
+
for target in BACKUP_TARGETS:
|
|
38
|
+
target_path = project_path / target
|
|
39
|
+
if target_path.exists():
|
|
40
|
+
return True
|
|
41
|
+
return False
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def get_backup_targets(project_path: Path) -> list[str]:
|
|
45
|
+
"""Return existing backup targets.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
project_path: Project path.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
List of backup targets that exist.
|
|
52
|
+
"""
|
|
53
|
+
targets: list[str] = []
|
|
54
|
+
for target in BACKUP_TARGETS:
|
|
55
|
+
target_path = project_path / target
|
|
56
|
+
if target_path.exists():
|
|
57
|
+
targets.append(target)
|
|
58
|
+
return targets
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def generate_backup_dir_name() -> str:
|
|
62
|
+
"""Generate a timestamp-based backup directory name (v0.3.0).
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
Timestamp formatted as YYYYMMDD-HHMMSS.
|
|
66
|
+
Note: callers use .moai-backups/{timestamp}/ format.
|
|
67
|
+
"""
|
|
68
|
+
timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
|
|
69
|
+
return timestamp
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def is_protected_path(rel_path: Path) -> bool:
|
|
73
|
+
"""Check whether the path is protected.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
rel_path: Relative path.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
True when the path should be excluded from backups.
|
|
80
|
+
"""
|
|
81
|
+
rel_str = str(rel_path).replace("\\", "/")
|
|
82
|
+
return any(
|
|
83
|
+
rel_str.startswith(p.lstrip("./").rstrip("/")) for p in PROTECTED_PATHS
|
|
84
|
+
)
|