moai-adk 0.3.1__py3-none-any.whl → 0.3.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of moai-adk might be problematic. Click here for more details.
- moai_adk/__init__.py +1 -1
- {moai_adk-0.3.1.dist-info → moai_adk-0.3.3.dist-info}/METADATA +152 -147
- {moai_adk-0.3.1.dist-info → moai_adk-0.3.3.dist-info}/RECORD +6 -37
- moai_adk/templates/.claude/agents/alfred/cc-manager.md +0 -474
- moai_adk/templates/.claude/agents/alfred/code-builder.md +0 -534
- moai_adk/templates/.claude/agents/alfred/debug-helper.md +0 -302
- moai_adk/templates/.claude/agents/alfred/doc-syncer.md +0 -175
- moai_adk/templates/.claude/agents/alfred/git-manager.md +0 -200
- moai_adk/templates/.claude/agents/alfred/project-manager.md +0 -152
- moai_adk/templates/.claude/agents/alfred/spec-builder.md +0 -256
- moai_adk/templates/.claude/agents/alfred/tag-agent.md +0 -247
- moai_adk/templates/.claude/agents/alfred/trust-checker.md +0 -332
- moai_adk/templates/.claude/commands/alfred/0-project.md +0 -523
- moai_adk/templates/.claude/commands/alfred/1-spec.md +0 -531
- moai_adk/templates/.claude/commands/alfred/2-build.md +0 -413
- moai_adk/templates/.claude/commands/alfred/3-sync.md +0 -552
- moai_adk/templates/.claude/hooks/alfred/README.md +0 -238
- moai_adk/templates/.claude/hooks/alfred/alfred_hooks.py +0 -165
- moai_adk/templates/.claude/hooks/alfred/core/__init__.py +0 -79
- moai_adk/templates/.claude/hooks/alfred/core/checkpoint.py +0 -271
- moai_adk/templates/.claude/hooks/alfred/core/context.py +0 -110
- moai_adk/templates/.claude/hooks/alfred/core/project.py +0 -284
- moai_adk/templates/.claude/hooks/alfred/core/tags.py +0 -244
- moai_adk/templates/.claude/hooks/alfred/handlers/__init__.py +0 -23
- moai_adk/templates/.claude/hooks/alfred/handlers/compact.py +0 -51
- moai_adk/templates/.claude/hooks/alfred/handlers/notification.py +0 -25
- moai_adk/templates/.claude/hooks/alfred/handlers/session.py +0 -80
- moai_adk/templates/.claude/hooks/alfred/handlers/tool.py +0 -71
- moai_adk/templates/.claude/hooks/alfred/handlers/user.py +0 -41
- moai_adk/templates/.claude/output-styles/alfred/agentic-coding.md +0 -635
- moai_adk/templates/.claude/output-styles/alfred/moai-adk-learning.md +0 -691
- moai_adk/templates/.claude/output-styles/alfred/study-with-alfred.md +0 -469
- moai_adk/templates/.claude/settings.json +0 -135
- moai_adk/templates/CLAUDE.md +0 -733
- {moai_adk-0.3.1.dist-info → moai_adk-0.3.3.dist-info}/WHEEL +0 -0
- {moai_adk-0.3.1.dist-info → moai_adk-0.3.3.dist-info}/entry_points.txt +0 -0
- {moai_adk-0.3.1.dist-info → moai_adk-0.3.3.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,271 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Event-Driven Checkpoint system
|
|
3
|
-
|
|
4
|
-
위험한 작업 감지 및 자동 Checkpoint 생성
|
|
5
|
-
@TAG:CHECKPOINT-EVENT-001
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
import json
|
|
9
|
-
import re
|
|
10
|
-
import subprocess
|
|
11
|
-
from datetime import datetime
|
|
12
|
-
from pathlib import Path
|
|
13
|
-
from typing import Any
|
|
14
|
-
|
|
15
|
-
# MoAI-ADK 지원 언어별 스크립트 실행 패턴
|
|
16
|
-
# Python, TypeScript, Java, Go, Rust, Dart, Swift, Kotlin + Shell
|
|
17
|
-
SCRIPT_EXECUTION_PATTERN = re.compile(
|
|
18
|
-
r"\b("
|
|
19
|
-
# Python ecosystem
|
|
20
|
-
r"python3?|pytest|pip|uv|"
|
|
21
|
-
# JavaScript/TypeScript ecosystem
|
|
22
|
-
r"node|npm|npx|yarn|bun|tsx|ts-node|vitest|jest|"
|
|
23
|
-
# Java ecosystem
|
|
24
|
-
r"java|javac|mvn|gradle|"
|
|
25
|
-
# Go
|
|
26
|
-
r"go|"
|
|
27
|
-
# Rust
|
|
28
|
-
r"cargo|"
|
|
29
|
-
# Dart/Flutter
|
|
30
|
-
r"dart|flutter|"
|
|
31
|
-
# Swift
|
|
32
|
-
r"swift|xcodebuild|"
|
|
33
|
-
# Kotlin
|
|
34
|
-
r"kotlinc?|"
|
|
35
|
-
# Shell scripts and build tools
|
|
36
|
-
r"bash|sh|zsh|fish|make"
|
|
37
|
-
r")\b"
|
|
38
|
-
)
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
def detect_risky_operation(tool_name: str, tool_args: dict[str, Any], cwd: str) -> tuple[bool, str]:
|
|
42
|
-
"""위험한 작업 감지 (Event-Driven Checkpoint용)
|
|
43
|
-
|
|
44
|
-
Claude Code tool 사용 전 위험한 작업을 자동으로 감지합니다.
|
|
45
|
-
위험 감지 시 자동으로 checkpoint를 생성하여 롤백 가능하게 합니다.
|
|
46
|
-
|
|
47
|
-
Args:
|
|
48
|
-
tool_name: Claude Code tool 이름 (Bash, Edit, Write, MultiEdit)
|
|
49
|
-
tool_args: Tool 인자 딕셔너리
|
|
50
|
-
cwd: 프로젝트 루트 디렉토리 경로
|
|
51
|
-
|
|
52
|
-
Returns:
|
|
53
|
-
(is_risky, operation_type) 튜플
|
|
54
|
-
- is_risky: 위험한 작업 여부 (bool)
|
|
55
|
-
- operation_type: 작업 유형 (str: delete, merge, script, critical-file, refactor)
|
|
56
|
-
|
|
57
|
-
Risky Operations:
|
|
58
|
-
- Bash tool: rm -rf, git merge, git reset --hard, git rebase, script execution
|
|
59
|
-
- Edit/Write tool: CLAUDE.md, config.json, .moai/memory/*.md
|
|
60
|
-
- MultiEdit tool: ≥10개 파일 동시 수정
|
|
61
|
-
- Script execution: Python, Node, Java, Go, Rust, Dart, Swift, Kotlin, Shell scripts
|
|
62
|
-
|
|
63
|
-
Examples:
|
|
64
|
-
>>> detect_risky_operation("Bash", {"command": "rm -rf src/"}, ".")
|
|
65
|
-
(True, 'delete')
|
|
66
|
-
>>> detect_risky_operation("Edit", {"file_path": "CLAUDE.md"}, ".")
|
|
67
|
-
(True, 'critical-file')
|
|
68
|
-
>>> detect_risky_operation("Read", {"file_path": "test.py"}, ".")
|
|
69
|
-
(False, '')
|
|
70
|
-
|
|
71
|
-
Notes:
|
|
72
|
-
- False Positive 최소화: 안전한 작업은 무시
|
|
73
|
-
- 성능: 가벼운 문자열 매칭 (< 1ms)
|
|
74
|
-
- 확장성: patterns 딕셔너리로 쉽게 추가 가능
|
|
75
|
-
|
|
76
|
-
@TAG:CHECKPOINT-EVENT-001
|
|
77
|
-
"""
|
|
78
|
-
# Bash tool: 위험한 명령어 감지
|
|
79
|
-
if tool_name == "Bash":
|
|
80
|
-
command = tool_args.get("command", "")
|
|
81
|
-
|
|
82
|
-
# 대규모 삭제
|
|
83
|
-
if any(pattern in command for pattern in ["rm -rf", "git rm"]):
|
|
84
|
-
return (True, "delete")
|
|
85
|
-
|
|
86
|
-
# Git 병합/리셋/리베이스
|
|
87
|
-
if any(pattern in command for pattern in ["git merge", "git reset --hard", "git rebase"]):
|
|
88
|
-
return (True, "merge")
|
|
89
|
-
|
|
90
|
-
# 외부 스크립트 실행 (파괴적 가능성)
|
|
91
|
-
if any(command.startswith(prefix) for prefix in ["python ", "node ", "bash ", "sh "]):
|
|
92
|
-
return (True, "script")
|
|
93
|
-
|
|
94
|
-
# Edit/Write tool: 중요 파일 감지
|
|
95
|
-
if tool_name in ("Edit", "Write"):
|
|
96
|
-
file_path = tool_args.get("file_path", "")
|
|
97
|
-
|
|
98
|
-
critical_files = [
|
|
99
|
-
"CLAUDE.md",
|
|
100
|
-
"config.json",
|
|
101
|
-
".moai/memory/development-guide.md",
|
|
102
|
-
".moai/memory/spec-metadata.md",
|
|
103
|
-
".moai/config.json",
|
|
104
|
-
]
|
|
105
|
-
|
|
106
|
-
if any(cf in file_path for cf in critical_files):
|
|
107
|
-
return (True, "critical-file")
|
|
108
|
-
|
|
109
|
-
# MultiEdit tool: 대규모 수정 감지
|
|
110
|
-
if tool_name == "MultiEdit":
|
|
111
|
-
edits = tool_args.get("edits", [])
|
|
112
|
-
if len(edits) >= 10:
|
|
113
|
-
return (True, "refactor")
|
|
114
|
-
|
|
115
|
-
return (False, "")
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
def create_checkpoint(cwd: str, operation_type: str) -> str:
|
|
119
|
-
"""Checkpoint 생성 (Git local branch)
|
|
120
|
-
|
|
121
|
-
위험한 작업 전 자동으로 checkpoint를 생성합니다.
|
|
122
|
-
Git local branch로 생성하여 원격 저장소 오염을 방지합니다.
|
|
123
|
-
|
|
124
|
-
Args:
|
|
125
|
-
cwd: 프로젝트 루트 디렉토리 경로
|
|
126
|
-
operation_type: 작업 유형 (delete, merge, script 등)
|
|
127
|
-
|
|
128
|
-
Returns:
|
|
129
|
-
checkpoint_branch: 생성된 브랜치명
|
|
130
|
-
실패 시 "checkpoint-failed" 반환
|
|
131
|
-
|
|
132
|
-
Branch Naming:
|
|
133
|
-
before-{operation}-{YYYYMMDD-HHMMSS}
|
|
134
|
-
예: before-delete-20251015-143000
|
|
135
|
-
|
|
136
|
-
Examples:
|
|
137
|
-
>>> create_checkpoint(".", "delete")
|
|
138
|
-
'before-delete-20251015-143000'
|
|
139
|
-
|
|
140
|
-
Notes:
|
|
141
|
-
- Local branch만 생성 (원격 push 안 함)
|
|
142
|
-
- Git 오류 시 fallback (무시하고 계속 진행)
|
|
143
|
-
- Dirty working directory 체크 안 함 (커밋 안 된 변경사항 허용)
|
|
144
|
-
- Checkpoint 로그 자동 기록 (.moai/checkpoints.log)
|
|
145
|
-
|
|
146
|
-
@TAG:CHECKPOINT-EVENT-001
|
|
147
|
-
"""
|
|
148
|
-
timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
|
|
149
|
-
branch_name = f"before-{operation_type}-{timestamp}"
|
|
150
|
-
|
|
151
|
-
try:
|
|
152
|
-
# 현재 브랜치에서 새 local branch 생성 (체크아웃 안 함)
|
|
153
|
-
result = subprocess.run(
|
|
154
|
-
["git", "branch", branch_name],
|
|
155
|
-
cwd=cwd,
|
|
156
|
-
check=True,
|
|
157
|
-
capture_output=True,
|
|
158
|
-
text=True,
|
|
159
|
-
timeout=2,
|
|
160
|
-
)
|
|
161
|
-
|
|
162
|
-
# Checkpoint 로그 기록
|
|
163
|
-
log_checkpoint(cwd, branch_name, operation_type)
|
|
164
|
-
|
|
165
|
-
return branch_name
|
|
166
|
-
|
|
167
|
-
except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError):
|
|
168
|
-
# Git 오류 시 fallback (무시)
|
|
169
|
-
return "checkpoint-failed"
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
def log_checkpoint(cwd: str, branch_name: str, operation_type: str) -> None:
|
|
173
|
-
"""Checkpoint 로그 기록 (.moai/checkpoints.log)
|
|
174
|
-
|
|
175
|
-
Checkpoint 생성 이력을 JSON Lines 형식으로 기록합니다.
|
|
176
|
-
SessionStart에서 이 로그를 읽어 checkpoint 목록을 표시합니다.
|
|
177
|
-
|
|
178
|
-
Args:
|
|
179
|
-
cwd: 프로젝트 루트 디렉토리 경로
|
|
180
|
-
branch_name: 생성된 checkpoint 브랜치명
|
|
181
|
-
operation_type: 작업 유형
|
|
182
|
-
|
|
183
|
-
Log Format (JSON Lines):
|
|
184
|
-
{"timestamp": "2025-10-15T14:30:00", "branch": "before-delete-...", "operation": "delete"}
|
|
185
|
-
|
|
186
|
-
Examples:
|
|
187
|
-
>>> log_checkpoint(".", "before-delete-20251015-143000", "delete")
|
|
188
|
-
# .moai/checkpoints.log에 1줄 추가
|
|
189
|
-
|
|
190
|
-
Notes:
|
|
191
|
-
- 파일 없으면 자동 생성
|
|
192
|
-
- append 모드로 기록 (기존 로그 보존)
|
|
193
|
-
- 실패 시 무시 (critical하지 않음)
|
|
194
|
-
|
|
195
|
-
@TAG:CHECKPOINT-EVENT-001
|
|
196
|
-
"""
|
|
197
|
-
log_file = Path(cwd) / ".moai" / "checkpoints.log"
|
|
198
|
-
|
|
199
|
-
try:
|
|
200
|
-
log_file.parent.mkdir(parents=True, exist_ok=True)
|
|
201
|
-
|
|
202
|
-
log_entry = {
|
|
203
|
-
"timestamp": datetime.now().isoformat(),
|
|
204
|
-
"branch": branch_name,
|
|
205
|
-
"operation": operation_type,
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
with log_file.open("a") as f:
|
|
209
|
-
f.write(json.dumps(log_entry) + "\n")
|
|
210
|
-
|
|
211
|
-
except (OSError, PermissionError):
|
|
212
|
-
# 로그 실패는 무시 (critical하지 않음)
|
|
213
|
-
pass
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
def list_checkpoints(cwd: str, max_count: int = 10) -> list[dict[str, str]]:
|
|
217
|
-
"""Checkpoint 목록 조회 (.moai/checkpoints.log 파싱)
|
|
218
|
-
|
|
219
|
-
최근 생성된 checkpoint 목록을 반환합니다.
|
|
220
|
-
SessionStart, /alfred:0-project restore 커맨드에서 사용합니다.
|
|
221
|
-
|
|
222
|
-
Args:
|
|
223
|
-
cwd: 프로젝트 루트 디렉토리 경로
|
|
224
|
-
max_count: 반환할 최대 개수 (기본 10개)
|
|
225
|
-
|
|
226
|
-
Returns:
|
|
227
|
-
Checkpoint 목록 (최신순)
|
|
228
|
-
[{"timestamp": "...", "branch": "...", "operation": "..."}, ...]
|
|
229
|
-
|
|
230
|
-
Examples:
|
|
231
|
-
>>> list_checkpoints(".")
|
|
232
|
-
[
|
|
233
|
-
{"timestamp": "2025-10-15T14:30:00", "branch": "before-delete-...", "operation": "delete"},
|
|
234
|
-
{"timestamp": "2025-10-15T14:25:00", "branch": "before-merge-...", "operation": "merge"},
|
|
235
|
-
]
|
|
236
|
-
|
|
237
|
-
Notes:
|
|
238
|
-
- 로그 파일 없으면 빈 리스트 반환
|
|
239
|
-
- JSON 파싱 실패한 줄은 무시
|
|
240
|
-
- 최신 max_count개만 반환
|
|
241
|
-
|
|
242
|
-
@TAG:CHECKPOINT-EVENT-001
|
|
243
|
-
"""
|
|
244
|
-
log_file = Path(cwd) / ".moai" / "checkpoints.log"
|
|
245
|
-
|
|
246
|
-
if not log_file.exists():
|
|
247
|
-
return []
|
|
248
|
-
|
|
249
|
-
checkpoints = []
|
|
250
|
-
|
|
251
|
-
try:
|
|
252
|
-
with log_file.open("r") as f:
|
|
253
|
-
for line in f:
|
|
254
|
-
try:
|
|
255
|
-
checkpoints.append(json.loads(line.strip()))
|
|
256
|
-
except json.JSONDecodeError:
|
|
257
|
-
# 파싱 실패한 줄 무시
|
|
258
|
-
pass
|
|
259
|
-
except (OSError, PermissionError):
|
|
260
|
-
return []
|
|
261
|
-
|
|
262
|
-
# 최근 max_count개만 반환 (최신순)
|
|
263
|
-
return checkpoints[-max_count:]
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
__all__ = [
|
|
267
|
-
"detect_risky_operation",
|
|
268
|
-
"create_checkpoint",
|
|
269
|
-
"log_checkpoint",
|
|
270
|
-
"list_checkpoints",
|
|
271
|
-
]
|
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Context Engineering utilities
|
|
3
|
-
|
|
4
|
-
JIT (Just-in-Time) Retrieval 및 워크플로우 컨텍스트 관리
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
import time
|
|
8
|
-
from pathlib import Path
|
|
9
|
-
from typing import Any
|
|
10
|
-
|
|
11
|
-
# Workflow context shared across phases
|
|
12
|
-
_workflow_context: dict[str, Any] = {}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def get_jit_context(prompt: str, cwd: str) -> list[str]:
|
|
16
|
-
"""프롬프트 기반 JIT Context Retrieval
|
|
17
|
-
|
|
18
|
-
사용자 프롬프트를 분석하여 관련 문서를 자동으로 추천합니다.
|
|
19
|
-
Alfred 커맨드, 키워드 기반 패턴 매칭으로 필요한 문서만 로드합니다.
|
|
20
|
-
|
|
21
|
-
Args:
|
|
22
|
-
prompt: 사용자 입력 프롬프트 (대소문자 무관)
|
|
23
|
-
cwd: 프로젝트 루트 디렉토리 경로
|
|
24
|
-
|
|
25
|
-
Returns:
|
|
26
|
-
추천 문서 경로 리스트 (상대 경로).
|
|
27
|
-
매칭되는 패턴이 없거나 파일이 없으면 빈 리스트 []
|
|
28
|
-
|
|
29
|
-
Patterns:
|
|
30
|
-
- "/alfred:1-spec" → .moai/memory/spec-metadata.md
|
|
31
|
-
- "/alfred:2-build" → .moai/memory/development-guide.md
|
|
32
|
-
- "test" → tests/ (디렉토리가 존재하는 경우)
|
|
33
|
-
|
|
34
|
-
Examples:
|
|
35
|
-
>>> get_jit_context("/alfred:1-spec", "/project")
|
|
36
|
-
['.moai/memory/spec-metadata.md']
|
|
37
|
-
>>> get_jit_context("implement test", "/project")
|
|
38
|
-
['tests/']
|
|
39
|
-
>>> get_jit_context("unknown", "/project")
|
|
40
|
-
[]
|
|
41
|
-
|
|
42
|
-
Notes:
|
|
43
|
-
- Context Engineering: JIT Retrieval 원칙 준수
|
|
44
|
-
- 필요한 문서만 로드하여 초기 컨텍스트 부담 최소화
|
|
45
|
-
- 파일 존재 여부 확인 후 반환
|
|
46
|
-
|
|
47
|
-
TDD History:
|
|
48
|
-
- RED: 18개 시나리오 테스트 (커맨드 매칭, 키워드, 빈 결과)
|
|
49
|
-
- GREEN: 패턴 매칭 딕셔너리 기반 구현
|
|
50
|
-
- REFACTOR: 확장 가능한 패턴 구조, 파일 존재 검증 추가
|
|
51
|
-
"""
|
|
52
|
-
context_files = []
|
|
53
|
-
cwd_path = Path(cwd)
|
|
54
|
-
|
|
55
|
-
# Pattern matching
|
|
56
|
-
patterns = {
|
|
57
|
-
"/alfred:1-spec": [".moai/memory/spec-metadata.md"],
|
|
58
|
-
"/alfred:2-build": [".moai/memory/development-guide.md"],
|
|
59
|
-
"test": ["tests/"],
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
for pattern, files in patterns.items():
|
|
63
|
-
if pattern in prompt.lower():
|
|
64
|
-
for file in files:
|
|
65
|
-
file_path = cwd_path / file
|
|
66
|
-
if file_path.exists():
|
|
67
|
-
context_files.append(file)
|
|
68
|
-
|
|
69
|
-
return context_files
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
def save_phase_context(phase: str, data: dict):
|
|
73
|
-
"""Store per-phase workflow context data.
|
|
74
|
-
|
|
75
|
-
Args:
|
|
76
|
-
phase: ``"analysis"``, ``"implementation"`` or ``"verification"``.
|
|
77
|
-
data: Payload to share with later phases.
|
|
78
|
-
|
|
79
|
-
Notes:
|
|
80
|
-
- Enables reuse of expensive analysis across phases.
|
|
81
|
-
- Entries expire after 10 minutes to avoid stale data.
|
|
82
|
-
"""
|
|
83
|
-
_workflow_context[phase] = {"data": data, "timestamp": time.time()}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
def load_phase_context(phase: str) -> dict | None:
|
|
87
|
-
"""Load previously stored workflow context for a phase.
|
|
88
|
-
|
|
89
|
-
Returns:
|
|
90
|
-
Stored context data or ``None`` when expired or missing.
|
|
91
|
-
"""
|
|
92
|
-
if phase in _workflow_context:
|
|
93
|
-
ctx = _workflow_context[phase]
|
|
94
|
-
# Only accept entries that are younger than 10 minutes
|
|
95
|
-
if time.time() - ctx["timestamp"] < 600:
|
|
96
|
-
return ctx["data"]
|
|
97
|
-
return None
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
def clear_workflow_context():
|
|
101
|
-
"""Clear all cached workflow context (call at workflow end)."""
|
|
102
|
-
_workflow_context.clear()
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
__all__ = [
|
|
106
|
-
"get_jit_context",
|
|
107
|
-
"save_phase_context",
|
|
108
|
-
"load_phase_context",
|
|
109
|
-
"clear_workflow_context",
|
|
110
|
-
]
|
|
@@ -1,284 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Project metadata utilities
|
|
3
|
-
|
|
4
|
-
프로젝트 정보 조회 (언어, Git, SPEC 진행도 등)
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
import json
|
|
8
|
-
import subprocess
|
|
9
|
-
from pathlib import Path
|
|
10
|
-
from typing import Any
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
def detect_language(cwd: str) -> str:
|
|
14
|
-
"""프로젝트 언어 감지 (20개 언어 지원)
|
|
15
|
-
|
|
16
|
-
파일 시스템을 탐색하여 프로젝트의 주 개발 언어를 감지합니다.
|
|
17
|
-
pyproject.toml, tsconfig.json 등의 설정 파일을 우선 검사하며,
|
|
18
|
-
TypeScript 우선 원칙을 적용합니다 (tsconfig.json 존재 시).
|
|
19
|
-
|
|
20
|
-
Args:
|
|
21
|
-
cwd: 프로젝트 루트 디렉토리 경로 (절대/상대 경로 모두 가능)
|
|
22
|
-
|
|
23
|
-
Returns:
|
|
24
|
-
감지된 언어명 (소문자). 감지 실패 시 "Unknown Language" 반환.
|
|
25
|
-
지원 언어: python, typescript, javascript, java, go, rust,
|
|
26
|
-
dart, swift, kotlin, php, ruby, elixir, scala,
|
|
27
|
-
clojure, cpp, c, csharp, haskell, shell, lua
|
|
28
|
-
|
|
29
|
-
Examples:
|
|
30
|
-
>>> detect_language("/path/to/python/project")
|
|
31
|
-
'python'
|
|
32
|
-
>>> detect_language("/path/to/typescript/project")
|
|
33
|
-
'typescript'
|
|
34
|
-
>>> detect_language("/path/to/unknown/project")
|
|
35
|
-
'Unknown Language'
|
|
36
|
-
|
|
37
|
-
TDD History:
|
|
38
|
-
- RED: 21개 언어 감지 테스트 작성 (20개 언어 + 1개 unknown)
|
|
39
|
-
- GREEN: 20개 언어 + unknown 구현, 모든 테스트 통과
|
|
40
|
-
- REFACTOR: 파일 검사 순서 최적화, TypeScript 우선 원칙 적용
|
|
41
|
-
"""
|
|
42
|
-
cwd_path = Path(cwd)
|
|
43
|
-
|
|
44
|
-
# Language detection mapping
|
|
45
|
-
language_files = {
|
|
46
|
-
"pyproject.toml": "python",
|
|
47
|
-
"tsconfig.json": "typescript",
|
|
48
|
-
"package.json": "javascript",
|
|
49
|
-
"pom.xml": "java",
|
|
50
|
-
"go.mod": "go",
|
|
51
|
-
"Cargo.toml": "rust",
|
|
52
|
-
"pubspec.yaml": "dart",
|
|
53
|
-
"Package.swift": "swift",
|
|
54
|
-
"build.gradle.kts": "kotlin",
|
|
55
|
-
"composer.json": "php",
|
|
56
|
-
"Gemfile": "ruby",
|
|
57
|
-
"mix.exs": "elixir",
|
|
58
|
-
"build.sbt": "scala",
|
|
59
|
-
"project.clj": "clojure",
|
|
60
|
-
"CMakeLists.txt": "cpp",
|
|
61
|
-
"Makefile": "c",
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
# Check standard language files
|
|
65
|
-
for file_name, language in language_files.items():
|
|
66
|
-
if (cwd_path / file_name).exists():
|
|
67
|
-
# Special handling for package.json - prefer typescript if tsconfig exists
|
|
68
|
-
if file_name == "package.json" and (cwd_path / "tsconfig.json").exists():
|
|
69
|
-
return "typescript"
|
|
70
|
-
return language
|
|
71
|
-
|
|
72
|
-
# Check for C# project files (*.csproj)
|
|
73
|
-
if any(cwd_path.glob("*.csproj")):
|
|
74
|
-
return "csharp"
|
|
75
|
-
|
|
76
|
-
# Check for Haskell project files (*.cabal)
|
|
77
|
-
if any(cwd_path.glob("*.cabal")):
|
|
78
|
-
return "haskell"
|
|
79
|
-
|
|
80
|
-
# Check for Shell scripts (*.sh)
|
|
81
|
-
if any(cwd_path.glob("*.sh")):
|
|
82
|
-
return "shell"
|
|
83
|
-
|
|
84
|
-
# Check for Lua files (*.lua)
|
|
85
|
-
if any(cwd_path.glob("*.lua")):
|
|
86
|
-
return "lua"
|
|
87
|
-
|
|
88
|
-
return "Unknown Language"
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
def _run_git_command(args: list[str], cwd: str, timeout: int = 2) -> str:
|
|
92
|
-
"""Git 명령어 실행 헬퍼 함수
|
|
93
|
-
|
|
94
|
-
Git 명령어를 안전하게 실행하고 출력을 반환합니다.
|
|
95
|
-
코드 중복을 제거하고 일관된 에러 처리를 제공합니다.
|
|
96
|
-
|
|
97
|
-
Args:
|
|
98
|
-
args: Git 명령어 인자 리스트 (git은 자동 추가)
|
|
99
|
-
cwd: 실행 디렉토리 경로
|
|
100
|
-
timeout: 타임아웃 (초, 기본 2초)
|
|
101
|
-
|
|
102
|
-
Returns:
|
|
103
|
-
Git 명령어 출력 (stdout, 앞뒤 공백 제거)
|
|
104
|
-
|
|
105
|
-
Raises:
|
|
106
|
-
subprocess.TimeoutExpired: 타임아웃 초과
|
|
107
|
-
subprocess.CalledProcessError: Git 명령어 실패
|
|
108
|
-
|
|
109
|
-
Examples:
|
|
110
|
-
>>> _run_git_command(["branch", "--show-current"], ".")
|
|
111
|
-
'main'
|
|
112
|
-
"""
|
|
113
|
-
result = subprocess.run(
|
|
114
|
-
["git"] + args,
|
|
115
|
-
cwd=cwd,
|
|
116
|
-
capture_output=True,
|
|
117
|
-
text=True,
|
|
118
|
-
timeout=timeout,
|
|
119
|
-
check=True,
|
|
120
|
-
)
|
|
121
|
-
return result.stdout.strip()
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
def get_git_info(cwd: str) -> dict[str, Any]:
|
|
125
|
-
"""Git 리포지토리 정보 수집
|
|
126
|
-
|
|
127
|
-
Git 리포지토리의 현재 상태를 조회합니다.
|
|
128
|
-
브랜치명, 커밋 해시, 변경사항 개수를 반환하며,
|
|
129
|
-
Git 리포지토리가 아닌 경우 빈 딕셔너리를 반환합니다.
|
|
130
|
-
|
|
131
|
-
Args:
|
|
132
|
-
cwd: 프로젝트 루트 디렉토리 경로
|
|
133
|
-
|
|
134
|
-
Returns:
|
|
135
|
-
Git 정보 딕셔너리. 다음 키를 포함:
|
|
136
|
-
- branch: 현재 브랜치명 (str)
|
|
137
|
-
- commit: 현재 커밋 해시 (str, full hash)
|
|
138
|
-
- changes: 변경된 파일 개수 (int, staged + unstaged)
|
|
139
|
-
|
|
140
|
-
Git 리포지토리가 아니거나 조회 실패 시 빈 딕셔너리 {}
|
|
141
|
-
|
|
142
|
-
Examples:
|
|
143
|
-
>>> get_git_info("/path/to/git/repo")
|
|
144
|
-
{'branch': 'main', 'commit': 'abc123...', 'changes': 3}
|
|
145
|
-
>>> get_git_info("/path/to/non-git")
|
|
146
|
-
{}
|
|
147
|
-
|
|
148
|
-
Notes:
|
|
149
|
-
- 타임아웃: 각 Git 명령어 2초
|
|
150
|
-
- 보안: subprocess.run(shell=False)로 안전한 실행
|
|
151
|
-
- 에러 처리: 모든 예외 시 빈 딕셔너리 반환
|
|
152
|
-
|
|
153
|
-
TDD History:
|
|
154
|
-
- RED: 3개 시나리오 테스트 (Git 리포, 비 Git, 에러)
|
|
155
|
-
- GREEN: subprocess 기반 Git 명령어 실행 구현
|
|
156
|
-
- REFACTOR: 타임아웃 추가 (2초), 예외 처리 강화, 헬퍼 함수로 중복 제거
|
|
157
|
-
"""
|
|
158
|
-
try:
|
|
159
|
-
# Check if it's a git repository
|
|
160
|
-
_run_git_command(["rev-parse", "--git-dir"], cwd)
|
|
161
|
-
|
|
162
|
-
# Get branch name, commit hash, and changes
|
|
163
|
-
branch = _run_git_command(["branch", "--show-current"], cwd)
|
|
164
|
-
commit = _run_git_command(["rev-parse", "HEAD"], cwd)
|
|
165
|
-
status_output = _run_git_command(["status", "--short"], cwd)
|
|
166
|
-
changes = len([line for line in status_output.splitlines() if line])
|
|
167
|
-
|
|
168
|
-
return {
|
|
169
|
-
"branch": branch,
|
|
170
|
-
"commit": commit,
|
|
171
|
-
"changes": changes,
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
except (subprocess.TimeoutExpired, subprocess.CalledProcessError, FileNotFoundError):
|
|
175
|
-
return {}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
def count_specs(cwd: str) -> dict[str, int]:
|
|
179
|
-
"""SPEC 파일 카운트 및 진행도 계산
|
|
180
|
-
|
|
181
|
-
.moai/specs/ 디렉토리를 탐색하여 SPEC 파일 개수와
|
|
182
|
-
완료 상태(status: completed)인 SPEC 개수를 집계합니다.
|
|
183
|
-
|
|
184
|
-
Args:
|
|
185
|
-
cwd: 프로젝트 루트 디렉토리 경로
|
|
186
|
-
|
|
187
|
-
Returns:
|
|
188
|
-
SPEC 진행도 딕셔너리. 다음 키를 포함:
|
|
189
|
-
- completed: 완료된 SPEC 개수 (int)
|
|
190
|
-
- total: 전체 SPEC 개수 (int)
|
|
191
|
-
- percentage: 완료율 (int, 0~100)
|
|
192
|
-
|
|
193
|
-
.moai/specs/ 디렉토리가 없으면 모두 0
|
|
194
|
-
|
|
195
|
-
Examples:
|
|
196
|
-
>>> count_specs("/path/to/project")
|
|
197
|
-
{'completed': 2, 'total': 5, 'percentage': 40}
|
|
198
|
-
>>> count_specs("/path/to/no-specs")
|
|
199
|
-
{'completed': 0, 'total': 0, 'percentage': 0}
|
|
200
|
-
|
|
201
|
-
Notes:
|
|
202
|
-
- SPEC 파일 위치: .moai/specs/SPEC-{ID}/spec.md
|
|
203
|
-
- 완료 조건: YAML front matter에 "status: completed" 포함
|
|
204
|
-
- 파싱 실패 시 해당 SPEC은 미완료로 간주
|
|
205
|
-
|
|
206
|
-
TDD History:
|
|
207
|
-
- RED: 5개 시나리오 테스트 (0/0, 2/5, 5/5, 디렉토리 없음, 파싱 에러)
|
|
208
|
-
- GREEN: Path.iterdir()로 SPEC 탐색, YAML 파싱 구현
|
|
209
|
-
- REFACTOR: 예외 처리 강화, 퍼센트 계산 안전성 개선
|
|
210
|
-
"""
|
|
211
|
-
specs_dir = Path(cwd) / ".moai" / "specs"
|
|
212
|
-
|
|
213
|
-
if not specs_dir.exists():
|
|
214
|
-
return {"completed": 0, "total": 0, "percentage": 0}
|
|
215
|
-
|
|
216
|
-
completed = 0
|
|
217
|
-
total = 0
|
|
218
|
-
|
|
219
|
-
for spec_dir in specs_dir.iterdir():
|
|
220
|
-
if not spec_dir.is_dir() or not spec_dir.name.startswith("SPEC-"):
|
|
221
|
-
continue
|
|
222
|
-
|
|
223
|
-
spec_file = spec_dir / "spec.md"
|
|
224
|
-
if not spec_file.exists():
|
|
225
|
-
continue
|
|
226
|
-
|
|
227
|
-
total += 1
|
|
228
|
-
|
|
229
|
-
# Parse YAML front matter
|
|
230
|
-
try:
|
|
231
|
-
content = spec_file.read_text()
|
|
232
|
-
if content.startswith("---"):
|
|
233
|
-
yaml_end = content.find("---", 3)
|
|
234
|
-
if yaml_end > 0:
|
|
235
|
-
yaml_content = content[3:yaml_end]
|
|
236
|
-
if "status: completed" in yaml_content:
|
|
237
|
-
completed += 1
|
|
238
|
-
except (OSError, UnicodeDecodeError):
|
|
239
|
-
# 파일 읽기 실패 또는 인코딩 오류 - 미완료로 간주
|
|
240
|
-
pass
|
|
241
|
-
|
|
242
|
-
percentage = int(completed / total * 100) if total > 0 else 0
|
|
243
|
-
|
|
244
|
-
return {
|
|
245
|
-
"completed": completed,
|
|
246
|
-
"total": total,
|
|
247
|
-
"percentage": percentage,
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
def get_project_language(cwd: str) -> str:
|
|
252
|
-
"""Determine the primary project language (prefers config.json).
|
|
253
|
-
|
|
254
|
-
Args:
|
|
255
|
-
cwd: Project root directory.
|
|
256
|
-
|
|
257
|
-
Returns:
|
|
258
|
-
Language string in lower-case.
|
|
259
|
-
|
|
260
|
-
Notes:
|
|
261
|
-
- Reads ``.moai/config.json`` first for a quick answer.
|
|
262
|
-
- Falls back to ``detect_language`` if configuration is missing.
|
|
263
|
-
"""
|
|
264
|
-
config_path = Path(cwd) / ".moai" / "config.json"
|
|
265
|
-
if config_path.exists():
|
|
266
|
-
try:
|
|
267
|
-
config = json.loads(config_path.read_text())
|
|
268
|
-
lang = config.get("language", "")
|
|
269
|
-
if lang:
|
|
270
|
-
return lang
|
|
271
|
-
except (OSError, json.JSONDecodeError):
|
|
272
|
-
# Fall back to detection on parse errors
|
|
273
|
-
pass
|
|
274
|
-
|
|
275
|
-
# Fall back to the original language detection routine
|
|
276
|
-
return detect_language(cwd)
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
__all__ = [
|
|
280
|
-
"detect_language",
|
|
281
|
-
"get_git_info",
|
|
282
|
-
"count_specs",
|
|
283
|
-
"get_project_language",
|
|
284
|
-
]
|