moai-adk 0.8.0__py3-none-any.whl → 0.8.1__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/core/issue_creator.py +309 -0
- moai_adk/core/template_engine.py +253 -0
- moai_adk/templates/.claude/agents/alfred/git-manager.md +25 -2
- moai_adk/templates/.claude/commands/alfred/9-feedback.md +149 -0
- moai_adk/templates/.claude/hooks/alfred/core/project.py +2 -2
- moai_adk/templates/.github/ISSUE_TEMPLATE/spec.yml +5 -3
- moai_adk/templates/.github/PULL_REQUEST_TEMPLATE.md +20 -8
- moai_adk/templates/.github/workflows/moai-gitflow.yml +22 -16
- moai_adk/templates/.github/workflows/spec-issue-sync.yml +10 -6
- moai_adk/templates/.moai/config.json +12 -0
- moai_adk/templates/.moai/docs/quick-issue-creation-guide.md +219 -0
- moai_adk/templates/.moai/memory/issue-label-mapping.md +150 -0
- {moai_adk-0.8.0.dist-info → moai_adk-0.8.1.dist-info}/METADATA +123 -1
- {moai_adk-0.8.0.dist-info → moai_adk-0.8.1.dist-info}/RECORD +17 -13
- moai_adk/templates/.claude/hooks/alfred/test_hook_output.py +0 -175
- {moai_adk-0.8.0.dist-info → moai_adk-0.8.1.dist-info}/WHEEL +0 -0
- {moai_adk-0.8.0.dist-info → moai_adk-0.8.1.dist-info}/entry_points.txt +0 -0
- {moai_adk-0.8.0.dist-info → moai_adk-0.8.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
"""
|
|
2
|
+
GitHub Issue Creator for MoAI-ADK quick issue reporting.
|
|
3
|
+
|
|
4
|
+
Enables users to quickly create GitHub Issues with standardized templates
|
|
5
|
+
using `/alfred:9-feedback` interactive dialog.
|
|
6
|
+
|
|
7
|
+
@TAG:ISSUE-CREATOR-001 - GitHub issue creation system
|
|
8
|
+
@TAG:QUICK-REPORTING-001 - Quick issue reporting functionality
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import subprocess
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from enum import Enum
|
|
14
|
+
from typing import Any, Dict, List, Optional
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class IssueType(Enum):
|
|
18
|
+
"""Supported GitHub issue types."""
|
|
19
|
+
BUG = "bug"
|
|
20
|
+
FEATURE = "feature"
|
|
21
|
+
IMPROVEMENT = "improvement"
|
|
22
|
+
QUESTION = "question"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class IssuePriority(Enum):
|
|
26
|
+
"""Issue priority levels."""
|
|
27
|
+
CRITICAL = "critical"
|
|
28
|
+
HIGH = "high"
|
|
29
|
+
MEDIUM = "medium"
|
|
30
|
+
LOW = "low"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class IssueConfig:
|
|
35
|
+
"""Configuration for issue creation."""
|
|
36
|
+
issue_type: IssueType
|
|
37
|
+
title: str
|
|
38
|
+
description: str
|
|
39
|
+
priority: IssuePriority = IssuePriority.MEDIUM
|
|
40
|
+
category: Optional[str] = None
|
|
41
|
+
assignees: Optional[List[str]] = None
|
|
42
|
+
custom_labels: Optional[List[str]] = None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class GitHubIssueCreator:
|
|
46
|
+
"""
|
|
47
|
+
Creates GitHub Issues using the `gh` CLI.
|
|
48
|
+
|
|
49
|
+
Supports:
|
|
50
|
+
- Multiple issue types (bug, feature, improvement, question)
|
|
51
|
+
- Priority levels and categories
|
|
52
|
+
- Standard templates for each type
|
|
53
|
+
- Label automation
|
|
54
|
+
- Priority emoji indicators
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
# Label mapping for issue types
|
|
58
|
+
LABEL_MAP = {
|
|
59
|
+
IssueType.BUG: ["bug", "reported"],
|
|
60
|
+
IssueType.FEATURE: ["feature-request", "enhancement"],
|
|
61
|
+
IssueType.IMPROVEMENT: ["improvement", "enhancement"],
|
|
62
|
+
IssueType.QUESTION: ["question", "help-wanted"],
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
# Priority emoji
|
|
66
|
+
PRIORITY_EMOJI = {
|
|
67
|
+
IssuePriority.CRITICAL: "🔴",
|
|
68
|
+
IssuePriority.HIGH: "🟠",
|
|
69
|
+
IssuePriority.MEDIUM: "🟡",
|
|
70
|
+
IssuePriority.LOW: "🟢",
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
# Issue type emoji
|
|
74
|
+
TYPE_EMOJI = {
|
|
75
|
+
IssueType.BUG: "🐛",
|
|
76
|
+
IssueType.FEATURE: "✨",
|
|
77
|
+
IssueType.IMPROVEMENT: "⚡",
|
|
78
|
+
IssueType.QUESTION: "❓",
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
def __init__(self, github_token: Optional[str] = None):
|
|
82
|
+
"""
|
|
83
|
+
Initialize the GitHub Issue Creator.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
github_token: GitHub API token. If not provided, uses GITHUB_TOKEN env var.
|
|
87
|
+
"""
|
|
88
|
+
self.github_token = github_token
|
|
89
|
+
self._check_gh_cli()
|
|
90
|
+
|
|
91
|
+
def _check_gh_cli(self) -> None:
|
|
92
|
+
"""
|
|
93
|
+
Check if `gh` CLI is installed and accessible.
|
|
94
|
+
|
|
95
|
+
Raises:
|
|
96
|
+
RuntimeError: If `gh` CLI is not found or not authenticated.
|
|
97
|
+
"""
|
|
98
|
+
try:
|
|
99
|
+
result = subprocess.run(
|
|
100
|
+
["gh", "auth", "status"],
|
|
101
|
+
capture_output=True,
|
|
102
|
+
text=True,
|
|
103
|
+
timeout=5
|
|
104
|
+
)
|
|
105
|
+
if result.returncode != 0:
|
|
106
|
+
raise RuntimeError(
|
|
107
|
+
"GitHub CLI (gh) is not authenticated. "
|
|
108
|
+
"Run `gh auth login` to authenticate."
|
|
109
|
+
)
|
|
110
|
+
except FileNotFoundError:
|
|
111
|
+
raise RuntimeError(
|
|
112
|
+
"GitHub CLI (gh) is not installed. "
|
|
113
|
+
"Please install it: https://cli.github.com"
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
def create_issue(self, config: IssueConfig) -> Dict[str, Any]:
|
|
117
|
+
"""
|
|
118
|
+
Create a GitHub issue with the given configuration.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
config: Issue configuration
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
Dictionary containing issue creation result:
|
|
125
|
+
{
|
|
126
|
+
"success": bool,
|
|
127
|
+
"issue_number": int,
|
|
128
|
+
"issue_url": str,
|
|
129
|
+
"message": str
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
Raises:
|
|
133
|
+
RuntimeError: If issue creation fails
|
|
134
|
+
"""
|
|
135
|
+
# Build title with emoji and priority
|
|
136
|
+
emoji = self.TYPE_EMOJI.get(config.issue_type, "📋")
|
|
137
|
+
priority_emoji = self.PRIORITY_EMOJI.get(config.priority, "")
|
|
138
|
+
full_title = f"{emoji} [{config.issue_type.value.upper()}] {config.title}"
|
|
139
|
+
if priority_emoji:
|
|
140
|
+
full_title = f"{priority_emoji} {full_title}"
|
|
141
|
+
|
|
142
|
+
# Build body with template
|
|
143
|
+
body = self._build_body(config)
|
|
144
|
+
|
|
145
|
+
# Collect labels
|
|
146
|
+
labels = self.LABEL_MAP.get(config.issue_type, []).copy()
|
|
147
|
+
if config.priority:
|
|
148
|
+
labels.append(f"priority-{config.priority.value}")
|
|
149
|
+
if config.category:
|
|
150
|
+
labels.append(f"category-{config.category.lower().replace(' ', '-')}")
|
|
151
|
+
if config.custom_labels:
|
|
152
|
+
labels.extend(config.custom_labels)
|
|
153
|
+
|
|
154
|
+
# Build gh command
|
|
155
|
+
gh_command = [
|
|
156
|
+
"gh", "issue", "create",
|
|
157
|
+
"--title", full_title,
|
|
158
|
+
"--body", body,
|
|
159
|
+
]
|
|
160
|
+
|
|
161
|
+
# Add labels
|
|
162
|
+
if labels:
|
|
163
|
+
gh_command.extend(["--label", ",".join(set(labels))])
|
|
164
|
+
|
|
165
|
+
# Add assignees if provided
|
|
166
|
+
if config.assignees:
|
|
167
|
+
gh_command.extend(["--assignee", ",".join(config.assignees)])
|
|
168
|
+
|
|
169
|
+
try:
|
|
170
|
+
result = subprocess.run(
|
|
171
|
+
gh_command,
|
|
172
|
+
capture_output=True,
|
|
173
|
+
text=True,
|
|
174
|
+
timeout=30
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
if result.returncode != 0:
|
|
178
|
+
error_msg = result.stderr or result.stdout
|
|
179
|
+
raise RuntimeError(f"Failed to create GitHub issue: {error_msg}")
|
|
180
|
+
|
|
181
|
+
# Parse issue URL from output
|
|
182
|
+
issue_url = result.stdout.strip()
|
|
183
|
+
issue_number = self._extract_issue_number(issue_url)
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
"success": True,
|
|
187
|
+
"issue_number": issue_number,
|
|
188
|
+
"issue_url": issue_url,
|
|
189
|
+
"message": f"✅ GitHub Issue #{issue_number} created successfully",
|
|
190
|
+
"title": full_title,
|
|
191
|
+
"labels": labels,
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
except subprocess.TimeoutExpired:
|
|
195
|
+
raise RuntimeError("GitHub issue creation timed out")
|
|
196
|
+
except Exception as e:
|
|
197
|
+
raise RuntimeError(f"Error creating GitHub issue: {e}")
|
|
198
|
+
|
|
199
|
+
def _build_body(self, config: IssueConfig) -> str:
|
|
200
|
+
"""
|
|
201
|
+
Build the issue body based on issue type.
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
config: Issue configuration
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
Formatted issue body
|
|
208
|
+
"""
|
|
209
|
+
body = config.description
|
|
210
|
+
|
|
211
|
+
# Add metadata footer
|
|
212
|
+
footer = "\n\n---\n\n"
|
|
213
|
+
footer += f"**Type**: {config.issue_type.value} \n"
|
|
214
|
+
footer += f"**Priority**: {config.priority.value} \n"
|
|
215
|
+
if config.category:
|
|
216
|
+
footer += f"**Category**: {config.category} \n"
|
|
217
|
+
footer += f"**Created via**: `/alfred:9-feedback`"
|
|
218
|
+
|
|
219
|
+
return body + footer
|
|
220
|
+
|
|
221
|
+
@staticmethod
|
|
222
|
+
def _extract_issue_number(url: str) -> int:
|
|
223
|
+
"""
|
|
224
|
+
Extract issue number from GitHub URL.
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
url: GitHub issue URL
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
Issue number
|
|
231
|
+
|
|
232
|
+
Raises:
|
|
233
|
+
ValueError: If unable to extract issue number
|
|
234
|
+
"""
|
|
235
|
+
try:
|
|
236
|
+
# URL format: https://github.com/owner/repo/issues/123
|
|
237
|
+
return int(url.strip().split("/")[-1])
|
|
238
|
+
except (ValueError, IndexError):
|
|
239
|
+
raise ValueError(f"Unable to extract issue number from URL: {url}")
|
|
240
|
+
|
|
241
|
+
def format_result(self, result: Dict[str, Any]) -> str:
|
|
242
|
+
"""
|
|
243
|
+
Format the issue creation result for display.
|
|
244
|
+
|
|
245
|
+
Args:
|
|
246
|
+
result: Issue creation result
|
|
247
|
+
|
|
248
|
+
Returns:
|
|
249
|
+
Formatted result string
|
|
250
|
+
"""
|
|
251
|
+
if result["success"]:
|
|
252
|
+
output = f"{result['message']}\n"
|
|
253
|
+
output += f"📋 Title: {result['title']}\n"
|
|
254
|
+
output += f"🔗 URL: {result['issue_url']}\n"
|
|
255
|
+
if result.get("labels"):
|
|
256
|
+
output += f"🏷️ Labels: {', '.join(result['labels'])}\n"
|
|
257
|
+
return output
|
|
258
|
+
else:
|
|
259
|
+
return f"❌ Failed to create issue: {result.get('message', 'Unknown error')}"
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
class IssueCreatorFactory:
|
|
263
|
+
"""
|
|
264
|
+
Factory for creating issue creators with predefined configurations.
|
|
265
|
+
"""
|
|
266
|
+
|
|
267
|
+
@staticmethod
|
|
268
|
+
def create_bug_issue(title: str, description: str, priority: IssuePriority = IssuePriority.HIGH) -> IssueConfig:
|
|
269
|
+
"""Create a bug report issue configuration."""
|
|
270
|
+
return IssueConfig(
|
|
271
|
+
issue_type=IssueType.BUG,
|
|
272
|
+
title=title,
|
|
273
|
+
description=description,
|
|
274
|
+
priority=priority,
|
|
275
|
+
category="Bug Report",
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
@staticmethod
|
|
279
|
+
def create_feature_issue(title: str, description: str, priority: IssuePriority = IssuePriority.MEDIUM) -> IssueConfig:
|
|
280
|
+
"""Create a feature request issue configuration."""
|
|
281
|
+
return IssueConfig(
|
|
282
|
+
issue_type=IssueType.FEATURE,
|
|
283
|
+
title=title,
|
|
284
|
+
description=description,
|
|
285
|
+
priority=priority,
|
|
286
|
+
category="Feature Request",
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
@staticmethod
|
|
290
|
+
def create_improvement_issue(title: str, description: str, priority: IssuePriority = IssuePriority.MEDIUM) -> IssueConfig:
|
|
291
|
+
"""Create an improvement issue configuration."""
|
|
292
|
+
return IssueConfig(
|
|
293
|
+
issue_type=IssueType.IMPROVEMENT,
|
|
294
|
+
title=title,
|
|
295
|
+
description=description,
|
|
296
|
+
priority=priority,
|
|
297
|
+
category="Improvement",
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
@staticmethod
|
|
301
|
+
def create_question_issue(title: str, description: str, priority: IssuePriority = IssuePriority.LOW) -> IssueConfig:
|
|
302
|
+
"""Create a question/discussion issue configuration."""
|
|
303
|
+
return IssueConfig(
|
|
304
|
+
issue_type=IssueType.QUESTION,
|
|
305
|
+
title=title,
|
|
306
|
+
description=description,
|
|
307
|
+
priority=priority,
|
|
308
|
+
category="Question",
|
|
309
|
+
)
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Template engine for parameterizing GitHub templates and other configuration files.
|
|
3
|
+
|
|
4
|
+
Supports Jinja2-style templating with variable substitution and conditional sections.
|
|
5
|
+
Enables users to customize MoAI-ADK templates for their own projects.
|
|
6
|
+
|
|
7
|
+
@TAG:TEMPLATE-ENGINE-001 - Template variable substitution system
|
|
8
|
+
@TAG:GITHUB-CUSTOMIZATION-001 - GitHub template parameterization
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any, Dict, Optional
|
|
13
|
+
|
|
14
|
+
from jinja2 import (
|
|
15
|
+
Environment,
|
|
16
|
+
FileSystemLoader,
|
|
17
|
+
StrictUndefined,
|
|
18
|
+
TemplateNotFound,
|
|
19
|
+
TemplateRuntimeError,
|
|
20
|
+
TemplateSyntaxError,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class TemplateEngine:
|
|
25
|
+
"""
|
|
26
|
+
Jinja2-based template engine for MoAI-ADK configuration and GitHub templates.
|
|
27
|
+
|
|
28
|
+
Supports:
|
|
29
|
+
- Variable substitution: {{PROJECT_NAME}}, {{SPEC_DIR}}, etc.
|
|
30
|
+
- Conditional sections: {{#ENABLE_TRUST_5}}...{{/ENABLE_TRUST_5}}
|
|
31
|
+
- File-based and string-based template rendering
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self, strict_undefined: bool = False):
|
|
35
|
+
"""
|
|
36
|
+
Initialize the template engine.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
strict_undefined: If True, raise error on undefined variables.
|
|
40
|
+
If False, render undefined variables as empty strings.
|
|
41
|
+
"""
|
|
42
|
+
self.strict_undefined = strict_undefined
|
|
43
|
+
self.undefined_behavior = StrictUndefined if strict_undefined else None
|
|
44
|
+
|
|
45
|
+
def render_string(
|
|
46
|
+
self,
|
|
47
|
+
template_string: str,
|
|
48
|
+
variables: Dict[str, Any]
|
|
49
|
+
) -> str:
|
|
50
|
+
"""
|
|
51
|
+
Render a Jinja2 template string with provided variables.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
template_string: The template content as a string
|
|
55
|
+
variables: Dictionary of variables to substitute
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
Rendered template string
|
|
59
|
+
|
|
60
|
+
Raises:
|
|
61
|
+
TemplateSyntaxError: If template syntax is invalid
|
|
62
|
+
TemplateRuntimeError: If variable substitution fails in strict mode
|
|
63
|
+
"""
|
|
64
|
+
try:
|
|
65
|
+
env = Environment(
|
|
66
|
+
undefined=self.undefined_behavior,
|
|
67
|
+
trim_blocks=False,
|
|
68
|
+
lstrip_blocks=False
|
|
69
|
+
)
|
|
70
|
+
template = env.from_string(template_string)
|
|
71
|
+
return template.render(**variables)
|
|
72
|
+
except (TemplateSyntaxError, TemplateRuntimeError) as e:
|
|
73
|
+
raise RuntimeError(f"Template rendering error: {e}")
|
|
74
|
+
|
|
75
|
+
def render_file(
|
|
76
|
+
self,
|
|
77
|
+
template_path: Path,
|
|
78
|
+
variables: Dict[str, Any],
|
|
79
|
+
output_path: Optional[Path] = None
|
|
80
|
+
) -> str:
|
|
81
|
+
"""
|
|
82
|
+
Render a Jinja2 template file with provided variables.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
template_path: Path to the template file
|
|
86
|
+
variables: Dictionary of variables to substitute
|
|
87
|
+
output_path: If provided, write rendered content to this path
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
Rendered template content
|
|
91
|
+
|
|
92
|
+
Raises:
|
|
93
|
+
FileNotFoundError: If template file doesn't exist
|
|
94
|
+
TemplateSyntaxError: If template syntax is invalid
|
|
95
|
+
TemplateRuntimeError: If variable substitution fails in strict mode
|
|
96
|
+
"""
|
|
97
|
+
if not template_path.exists():
|
|
98
|
+
raise FileNotFoundError(f"Template file not found: {template_path}")
|
|
99
|
+
|
|
100
|
+
template_dir = template_path.parent
|
|
101
|
+
template_name = template_path.name
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
env = Environment(
|
|
105
|
+
loader=FileSystemLoader(str(template_dir)),
|
|
106
|
+
undefined=self.undefined_behavior,
|
|
107
|
+
trim_blocks=False,
|
|
108
|
+
lstrip_blocks=False
|
|
109
|
+
)
|
|
110
|
+
template = env.get_template(template_name)
|
|
111
|
+
rendered = template.render(**variables)
|
|
112
|
+
|
|
113
|
+
if output_path:
|
|
114
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
115
|
+
output_path.write_text(rendered, encoding='utf-8')
|
|
116
|
+
|
|
117
|
+
return rendered
|
|
118
|
+
except TemplateNotFound:
|
|
119
|
+
raise FileNotFoundError(f"Template not found in {template_dir}: {template_name}")
|
|
120
|
+
except (TemplateSyntaxError, TemplateRuntimeError) as e:
|
|
121
|
+
raise RuntimeError(f"Template rendering error in {template_path}: {e}")
|
|
122
|
+
|
|
123
|
+
def render_directory(
|
|
124
|
+
self,
|
|
125
|
+
template_dir: Path,
|
|
126
|
+
output_dir: Path,
|
|
127
|
+
variables: Dict[str, Any],
|
|
128
|
+
pattern: str = "**/*.{md,yml,yaml,json}"
|
|
129
|
+
) -> Dict[str, str]:
|
|
130
|
+
"""
|
|
131
|
+
Render all template files in a directory.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
template_dir: Source directory containing templates
|
|
135
|
+
output_dir: Destination directory for rendered files
|
|
136
|
+
variables: Dictionary of variables to substitute
|
|
137
|
+
pattern: Glob pattern for files to process (default: template files)
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
Dictionary mapping input paths to rendered content
|
|
141
|
+
|
|
142
|
+
Raises:
|
|
143
|
+
FileNotFoundError: If template directory doesn't exist
|
|
144
|
+
"""
|
|
145
|
+
if not template_dir.exists():
|
|
146
|
+
raise FileNotFoundError(f"Template directory not found: {template_dir}")
|
|
147
|
+
|
|
148
|
+
results = {}
|
|
149
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
150
|
+
|
|
151
|
+
for template_file in template_dir.glob(pattern):
|
|
152
|
+
if template_file.is_file():
|
|
153
|
+
relative_path = template_file.relative_to(template_dir)
|
|
154
|
+
output_file = output_dir / relative_path
|
|
155
|
+
|
|
156
|
+
try:
|
|
157
|
+
rendered = self.render_file(template_file, variables, output_file)
|
|
158
|
+
results[str(relative_path)] = rendered
|
|
159
|
+
except Exception as e:
|
|
160
|
+
raise RuntimeError(f"Error rendering {relative_path}: {e}")
|
|
161
|
+
|
|
162
|
+
return results
|
|
163
|
+
|
|
164
|
+
@staticmethod
|
|
165
|
+
def get_default_variables(config: Dict[str, Any]) -> Dict[str, Any]:
|
|
166
|
+
"""
|
|
167
|
+
Extract template variables from project configuration.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
config: Project configuration dictionary (from .moai/config.json)
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
Dictionary of template variables
|
|
174
|
+
"""
|
|
175
|
+
github_config = config.get("github", {}).get("templates", {})
|
|
176
|
+
project_config = config.get("project", {})
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
# Project information
|
|
180
|
+
"PROJECT_NAME": project_config.get("name", "MyProject"),
|
|
181
|
+
"PROJECT_DESCRIPTION": project_config.get("description", ""),
|
|
182
|
+
"PROJECT_MODE": project_config.get("mode", "team"), # team or personal
|
|
183
|
+
|
|
184
|
+
# Directory structure
|
|
185
|
+
"SPEC_DIR": github_config.get("spec_directory", ".moai/specs"),
|
|
186
|
+
"DOCS_DIR": github_config.get("docs_directory", ".moai/docs"),
|
|
187
|
+
"TEST_DIR": github_config.get("test_directory", "tests"),
|
|
188
|
+
|
|
189
|
+
# Feature flags
|
|
190
|
+
"ENABLE_TRUST_5": github_config.get("enable_trust_5", True),
|
|
191
|
+
"ENABLE_TAG_SYSTEM": github_config.get("enable_tag_system", True),
|
|
192
|
+
"ENABLE_ALFRED_COMMANDS": github_config.get("enable_alfred_commands", True),
|
|
193
|
+
|
|
194
|
+
# Language configuration
|
|
195
|
+
"CONVERSATION_LANGUAGE": project_config.get("conversation_language", "en"),
|
|
196
|
+
"CONVERSATION_LANGUAGE_NAME": project_config.get("conversation_language_name", "English"),
|
|
197
|
+
|
|
198
|
+
# Additional metadata
|
|
199
|
+
"MOAI_VERSION": config.get("moai", {}).get("version", "0.7.0"),
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
class TemplateVariableValidator:
|
|
204
|
+
"""
|
|
205
|
+
Validates template variables for completeness and correctness.
|
|
206
|
+
Ensures all required variables are present before rendering.
|
|
207
|
+
"""
|
|
208
|
+
|
|
209
|
+
REQUIRED_VARIABLES = {
|
|
210
|
+
"PROJECT_NAME": str,
|
|
211
|
+
"SPEC_DIR": str,
|
|
212
|
+
"DOCS_DIR": str,
|
|
213
|
+
"TEST_DIR": str,
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
OPTIONAL_VARIABLES = {
|
|
217
|
+
"PROJECT_DESCRIPTION": (str, type(None)),
|
|
218
|
+
"PROJECT_MODE": str,
|
|
219
|
+
"ENABLE_TRUST_5": bool,
|
|
220
|
+
"ENABLE_TAG_SYSTEM": bool,
|
|
221
|
+
"ENABLE_ALFRED_COMMANDS": bool,
|
|
222
|
+
"CONVERSATION_LANGUAGE": str,
|
|
223
|
+
"CONVERSATION_LANGUAGE_NAME": str,
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
@classmethod
|
|
227
|
+
def validate(cls, variables: Dict[str, Any]) -> tuple[bool, list[str]]:
|
|
228
|
+
"""
|
|
229
|
+
Validate template variables.
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
variables: Dictionary of variables to validate
|
|
233
|
+
|
|
234
|
+
Returns:
|
|
235
|
+
Tuple of (is_valid, list_of_errors)
|
|
236
|
+
"""
|
|
237
|
+
errors = []
|
|
238
|
+
|
|
239
|
+
# Check required variables
|
|
240
|
+
for var_name, var_type in cls.REQUIRED_VARIABLES.items():
|
|
241
|
+
if var_name not in variables:
|
|
242
|
+
errors.append(f"Missing required variable: {var_name}")
|
|
243
|
+
elif not isinstance(variables[var_name], var_type):
|
|
244
|
+
errors.append(f"Invalid type for {var_name}: expected {var_type.__name__}, got {type(variables[var_name]).__name__}")
|
|
245
|
+
|
|
246
|
+
# Check optional variables (if present)
|
|
247
|
+
for var_name, var_type in cls.OPTIONAL_VARIABLES.items():
|
|
248
|
+
if var_name in variables:
|
|
249
|
+
if not isinstance(variables[var_name], var_type):
|
|
250
|
+
type_names = " or ".join(t.__name__ for t in var_type) if isinstance(var_type, tuple) else var_type.__name__
|
|
251
|
+
errors.append(f"Invalid type for {var_name}: expected {type_names}, got {type(variables[var_name]).__name__}")
|
|
252
|
+
|
|
253
|
+
return len(errors) == 0, errors
|
|
@@ -358,9 +358,10 @@ Git-manager automatically handles the following exception situations:
|
|
|
358
358
|
**All commits created by git-manager follow this signature format**:
|
|
359
359
|
|
|
360
360
|
```
|
|
361
|
-
|
|
361
|
+
🎩 Alfred@MoAI
|
|
362
|
+
🔗 https://adk.mo.ai.kr
|
|
362
363
|
|
|
363
|
-
Co-Authored-By:
|
|
364
|
+
Co-Authored-By: Claude <noreply@anthropic.com>
|
|
364
365
|
```
|
|
365
366
|
|
|
366
367
|
This signature applies to all Git operations:
|
|
@@ -370,6 +371,28 @@ This signature applies to all Git operations:
|
|
|
370
371
|
- Merge commits
|
|
371
372
|
- Tag creation
|
|
372
373
|
|
|
374
|
+
**Signature breakdown**:
|
|
375
|
+
- `🎩 Alfred@MoAI` - Alfred 에이전트의 공식 식별자
|
|
376
|
+
- `🔗 https://adk.mo.ai.kr` - MoAI-ADK 공식 홈페이지 링크
|
|
377
|
+
- `Co-Authored-By: Claude <noreply@anthropic.com>` - Claude AI 협력자 표시
|
|
378
|
+
|
|
379
|
+
**Implementation Example (HEREDOC)**:
|
|
380
|
+
```bash
|
|
381
|
+
git commit -m "$(cat <<'EOF'
|
|
382
|
+
feat(update): Implement 3-stage workflow with config version comparison
|
|
383
|
+
|
|
384
|
+
- Stage 2: Config version comparison (NEW)
|
|
385
|
+
- 70-80% performance improvement
|
|
386
|
+
- All tests passing
|
|
387
|
+
|
|
388
|
+
🎩 Alfred@MoAI
|
|
389
|
+
🔗 https://adk.mo.ai.kr
|
|
390
|
+
|
|
391
|
+
Co-Authored-By: Claude <noreply@anthropic.com>
|
|
392
|
+
EOF
|
|
393
|
+
)"
|
|
394
|
+
```
|
|
395
|
+
|
|
373
396
|
---
|
|
374
397
|
|
|
375
398
|
**git-manager provides a simple and stable work environment with direct Git commands instead of complex scripts.**
|