monoco-toolkit 0.1.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.
File without changes
monoco/core/config.py ADDED
@@ -0,0 +1,113 @@
1
+ import os
2
+ import yaml
3
+ from pathlib import Path
4
+ from typing import Optional, Dict, Any
5
+ from pydantic import BaseModel, Field
6
+
7
+ class PathsConfig(BaseModel):
8
+ """Configuration for directory paths."""
9
+ root: str = Field(default=".", description="Project root directory")
10
+ issues: str = Field(default="Issues", description="Directory for issues")
11
+ spikes: str = Field(default=".references", description="Directory for spikes/research")
12
+ specs: str = Field(default="SPECS", description="Directory for specifications")
13
+
14
+ class CoreConfig(BaseModel):
15
+ """Core system configuration."""
16
+ editor: str = Field(default_factory=lambda: os.getenv("EDITOR", "vim"), description="Preferred text editor")
17
+ log_level: str = Field(default="INFO", description="Logging verbosity")
18
+ author: Optional[str] = Field(default=None, description="Default author for new artifacts")
19
+
20
+ class ProjectConfig(BaseModel):
21
+ """Project identity configuration."""
22
+ name: str = Field(default="Monoco Project", description="Project name")
23
+ key: str = Field(default="MON", description="Project key/prefix for IDs")
24
+ spike_repos: Dict[str, str] = Field(default_factory=dict, description="Managed external research repositories (name -> url)")
25
+ members: Dict[str, str] = Field(default_factory=dict, description="Workspace member projects (name -> relative_path)")
26
+
27
+ class I18nConfig(BaseModel):
28
+ """Configuration for internationalization."""
29
+ source_lang: str = Field(default="en", description="Source language code")
30
+ target_langs: list[str] = Field(default_factory=lambda: ["zh"], description="Target language codes")
31
+
32
+ class UIConfig(BaseModel):
33
+ """Configuration for UI customizations."""
34
+ dictionary: Dict[str, str] = Field(default_factory=dict, description="Custom domain terminology mapping")
35
+
36
+ class TelemetryConfig(BaseModel):
37
+ """Configuration for Telemetry."""
38
+ enabled: Optional[bool] = Field(default=None, description="Whether telemetry is enabled")
39
+
40
+ class MonocoConfig(BaseModel):
41
+ """
42
+ Main Configuration Schema.
43
+ Hierarchy: Defaults < User Config (~/.monoco/config.yaml) < Project Config (./.monoco/config.yaml)
44
+ """
45
+ core: CoreConfig = Field(default_factory=CoreConfig)
46
+ paths: PathsConfig = Field(default_factory=PathsConfig)
47
+ project: ProjectConfig = Field(default_factory=ProjectConfig)
48
+ i18n: I18nConfig = Field(default_factory=I18nConfig)
49
+ ui: UIConfig = Field(default_factory=UIConfig)
50
+ telemetry: TelemetryConfig = Field(default_factory=TelemetryConfig)
51
+
52
+ @staticmethod
53
+ def _deep_merge(base: Dict[str, Any], update: Dict[str, Any]) -> Dict[str, Any]:
54
+ """Recursive dict merge."""
55
+ for k, v in update.items():
56
+ if isinstance(v, dict) and k in base and isinstance(base[k], dict):
57
+ MonocoConfig._deep_merge(base[k], v)
58
+ else:
59
+ base[k] = v
60
+ return base
61
+
62
+ @classmethod
63
+ def load(cls, project_root: Optional[str] = None) -> "MonocoConfig":
64
+ """
65
+ Load configuration from multiple sources.
66
+ """
67
+ # 1. Start with empty dict (will use defaults via Pydantic)
68
+ config_data = {}
69
+
70
+ # 2. Define config paths
71
+ home_path = Path.home() / ".monoco" / "config.yaml"
72
+
73
+ # Determine project path
74
+ cwd = Path(project_root) if project_root else Path.cwd()
75
+ proj_path_hidden = cwd / ".monoco" / "config.yaml"
76
+ proj_path_root = cwd / "monoco.yaml"
77
+
78
+ # 3. Load User Config
79
+ if home_path.exists():
80
+ try:
81
+ with open(home_path, "r") as f:
82
+ user_config = yaml.safe_load(f)
83
+ if user_config:
84
+ cls._deep_merge(config_data, user_config)
85
+ except Exception as e:
86
+ # We don't want to crash on config load fail, implementing simple warning equivalent
87
+ pass
88
+
89
+ # 4. Load Project Config (prefer .monoco/config.yaml, fallback to monoco.yaml)
90
+ target_proj_conf = proj_path_hidden if proj_path_hidden.exists() else (
91
+ proj_path_root if proj_path_root.exists() else None
92
+ )
93
+
94
+ if target_proj_conf:
95
+ try:
96
+ with open(target_proj_conf, "r") as f:
97
+ proj_config = yaml.safe_load(f)
98
+ if proj_config:
99
+ cls._deep_merge(config_data, proj_config)
100
+ except Exception:
101
+ pass
102
+
103
+ # 5. Instantiate Model
104
+ return cls(**config_data)
105
+
106
+ # Global singleton
107
+ _settings = None
108
+
109
+ def get_config(project_root: Optional[str] = None) -> MonocoConfig:
110
+ global _settings
111
+ if _settings is None or project_root is not None:
112
+ _settings = MonocoConfig.load(project_root)
113
+ return _settings
monoco/core/git.py ADDED
@@ -0,0 +1,184 @@
1
+ import subprocess
2
+ import shutil
3
+ from pathlib import Path
4
+ from typing import List, Tuple, Optional, Dict
5
+
6
+ def _run_git(args: List[str], cwd: Path) -> Tuple[int, str, str]:
7
+ """Run a raw git command."""
8
+ try:
9
+ result = subprocess.run(
10
+ ["git"] + args,
11
+ cwd=cwd,
12
+ capture_output=True,
13
+ text=True,
14
+ check=False
15
+ )
16
+ return result.returncode, result.stdout, result.stderr
17
+ except FileNotFoundError:
18
+ return 1, "", "Git executable not found"
19
+
20
+ def is_git_repo(path: Path) -> bool:
21
+ code, _, _ = _run_git(["rev-parse", "--is-inside-work-tree"], path)
22
+ return code == 0
23
+
24
+ def get_git_status(path: Path, subpath: Optional[str] = None) -> List[str]:
25
+ """
26
+ Get list of modified files.
27
+ If subpath is provided, only check that path.
28
+ """
29
+ cmd = ["status", "--porcelain"]
30
+ if subpath:
31
+ cmd.append(subpath)
32
+
33
+ code, stdout, _ = _run_git(cmd, path)
34
+ if code != 0:
35
+ raise RuntimeError("Failed to check git status")
36
+
37
+ lines = []
38
+ for line in stdout.splitlines():
39
+ line = line.strip()
40
+ if not line:
41
+ continue
42
+ # Porcelain format: XY PATH
43
+ if len(line) > 3:
44
+ path_str = line[3:]
45
+ if path_str.startswith('"') and path_str.endswith('"'):
46
+ path_str = path_str[1:-1]
47
+ lines.append(path_str)
48
+ return lines
49
+
50
+ def git_add(path: Path, files: List[str]) -> None:
51
+ if not files:
52
+ return
53
+ code, _, stderr = _run_git(["add"] + files, path)
54
+ if code != 0:
55
+ raise RuntimeError(f"Git add failed: {stderr}")
56
+
57
+ def git_commit(path: Path, message: str) -> str:
58
+ code, stdout, stderr = _run_git(["commit", "-m", message], path)
59
+ if code != 0:
60
+ raise RuntimeError(f"Git commit failed: {stderr}")
61
+
62
+ code, hash_out, _ = _run_git(["rev-parse", "HEAD"], path)
63
+ return hash_out.strip()
64
+
65
+ def search_commits_by_message(path: Path, grep_pattern: str) -> List[Dict[str, str]]:
66
+ cmd = ["log", f"--grep={grep_pattern}", "--name-only", "--format=COMMIT:%H|%s"]
67
+ code, stdout, stderr = _run_git(cmd, path)
68
+ if code != 0:
69
+ raise RuntimeError(f"Git log failed: {stderr}")
70
+
71
+ commits = []
72
+ current_commit = None
73
+
74
+ for line in stdout.splitlines():
75
+ if line.startswith("COMMIT:"):
76
+ if current_commit:
77
+ commits.append(current_commit)
78
+
79
+ parts = line[7:].split("|", 1)
80
+ current_commit = {
81
+ "hash": parts[0],
82
+ "subject": parts[1] if len(parts) > 1 else "",
83
+ "files": []
84
+ }
85
+ elif line.strip():
86
+ if current_commit:
87
+ current_commit["files"].append(line.strip())
88
+
89
+ if current_commit:
90
+ commits.append(current_commit)
91
+
92
+ return commits
93
+
94
+ def get_commit_stats(path: Path, commit_hash: str) -> Dict[str, int]:
95
+ cmd = ["show", "--shortstat", "--format=", commit_hash]
96
+ code, stdout, _ = _run_git(cmd, path)
97
+ stats = {"files": 0, "insertions": 0, "deletions": 0}
98
+ if code == 0 and stdout.strip():
99
+ parts = stdout.strip().split(",")
100
+ for p in parts:
101
+ p = p.strip()
102
+ if "file" in p:
103
+ stats["files"] = int(p.split()[0])
104
+ elif "insertion" in p:
105
+ stats["insertions"] = int(p.split()[0])
106
+ elif "deletion" in p:
107
+ stats["deletions"] = int(p.split()[0])
108
+ return stats
109
+
110
+ # --- Branch & Worktree Extensions ---
111
+
112
+ def get_current_branch(path: Path) -> str:
113
+ code, stdout, _ = _run_git(["rev-parse", "--abbrev-ref", "HEAD"], path)
114
+ if code != 0:
115
+ return ""
116
+ return stdout.strip()
117
+
118
+ def branch_exists(path: Path, branch_name: str) -> bool:
119
+ code, _, _ = _run_git(["rev-parse", "--verify", branch_name], path)
120
+ return code == 0
121
+
122
+ def create_branch(path: Path, branch_name: str, checkout: bool = False):
123
+ cmd = ["checkout", "-b", branch_name] if checkout else ["branch", branch_name]
124
+ code, _, stderr = _run_git(cmd, path)
125
+ if code != 0:
126
+ raise RuntimeError(f"Failed to create branch {branch_name}: {stderr}")
127
+
128
+ def checkout_branch(path: Path, branch_name: str):
129
+ code, _, stderr = _run_git(["checkout", branch_name], path)
130
+ if code != 0:
131
+ raise RuntimeError(f"Failed to checkout {branch_name}: {stderr}")
132
+
133
+ def delete_branch(path: Path, branch_name: str, force: bool = False):
134
+ flag = "-D" if force else "-d"
135
+ code, _, stderr = _run_git(["branch", flag, branch_name], path)
136
+ if code != 0:
137
+ raise RuntimeError(f"Failed to delete branch {branch_name}: {stderr}")
138
+
139
+ def get_worktrees(path: Path) -> List[Tuple[str, str, str]]:
140
+ """Returns list of (path, head, branch)"""
141
+ code, stdout, stderr = _run_git(["worktree", "list", "--porcelain"], path)
142
+ if code != 0:
143
+ raise RuntimeError(f"Failed to list worktrees: {stderr}")
144
+
145
+ trees = []
146
+ current = {}
147
+ for line in stdout.splitlines():
148
+ if line.startswith("worktree "):
149
+ if current:
150
+ trees.append((current.get("worktree"), current.get("HEAD"), current.get("branch")))
151
+ current = {"worktree": line[9:].strip()}
152
+ elif line.startswith("HEAD "):
153
+ current["HEAD"] = line[5:].strip()
154
+ elif line.startswith("branch "):
155
+ current["branch"] = line[7:].strip()
156
+
157
+ if current:
158
+ trees.append((current.get("worktree"), current.get("HEAD"), current.get("branch")))
159
+ return trees
160
+
161
+ def worktree_add(path: Path, branch_name: str, worktree_path: Path):
162
+ # If branch doesn't exist, -b will create it.
163
+ # Logic: git worktree add [-b <new_branch>] <path> <commit-ish>
164
+
165
+ # We assume if branch_exists, use it. If not, create it.
166
+ cmd = ["worktree", "add"]
167
+ if not branch_exists(path, branch_name):
168
+ cmd.extend(["-b", branch_name])
169
+
170
+ cmd.extend([str(worktree_path), branch_name])
171
+
172
+ code, _, stderr = _run_git(cmd, path)
173
+ if code != 0:
174
+ raise RuntimeError(f"Failed to create worktree: {stderr}")
175
+
176
+ def worktree_remove(path: Path, worktree_path: Path, force: bool = False):
177
+ cmd = ["worktree", "remove"]
178
+ if force:
179
+ cmd.append("--force")
180
+ cmd.append(str(worktree_path))
181
+
182
+ code, _, stderr = _run_git(cmd, path)
183
+ if code != 0:
184
+ raise RuntimeError(f"Failed to remove worktree: {stderr}")
monoco/core/output.py ADDED
@@ -0,0 +1,97 @@
1
+ import os
2
+ import json
3
+ import typer
4
+ from typing import Any, List, Union, Annotated
5
+ from pydantic import BaseModel
6
+ from rich.console import Console
7
+ from rich.table import Table
8
+ from rich import print as rprint
9
+
10
+ def _set_agent_mode(value: bool):
11
+ if value:
12
+ os.environ["AGENT_FLAG"] = "true"
13
+
14
+ # Reusable dependency for commands
15
+ AgentOutput = Annotated[bool, typer.Option("--json", help="Output in compact JSON for Agents", callback=_set_agent_mode)]
16
+
17
+ class OutputManager:
18
+ """
19
+ Manages output rendering based on the environment (Human vs Agent).
20
+ """
21
+
22
+ @staticmethod
23
+ def is_agent_mode() -> bool:
24
+ """
25
+ Check if running in Agent Mode.
26
+ Triggers:
27
+ 1. Environment variable AGENT_FLAG=true (or 1)
28
+ 2. Environment variable MONOCO_AGENT=true (or 1)
29
+ """
30
+ return os.getenv("AGENT_FLAG", "").lower() in ("true", "1") or \
31
+ os.getenv("MONOCO_AGENT", "").lower() in ("true", "1")
32
+
33
+ @staticmethod
34
+ def print(data: Union[BaseModel, List[BaseModel], dict, list, str], title: str = ""):
35
+ """
36
+ Dual frontend dispatcher.
37
+ """
38
+ if OutputManager.is_agent_mode():
39
+ OutputManager._render_agent(data)
40
+ else:
41
+ OutputManager._render_human(data, title)
42
+
43
+ @staticmethod
44
+ def _render_agent(data: Any):
45
+ """
46
+ Agent channel: Zero decoration, Pure Data, Max Token Density.
47
+ Uses compact JSON.
48
+ """
49
+ if isinstance(data, BaseModel):
50
+ print(data.model_dump_json(exclude_none=True))
51
+ elif isinstance(data, list) and all(isinstance(item, BaseModel) for item in data):
52
+ # Pydantic v2 adapter for list of models
53
+ print(json.dumps([item.model_dump(mode='json', exclude_none=True) for item in data], separators=(',', ':')))
54
+ else:
55
+ # Fallback for dicts/lists/primitives
56
+ try:
57
+ print(json.dumps(data, separators=(',', ':'), default=str))
58
+ except TypeError:
59
+ print(str(data))
60
+
61
+ @staticmethod
62
+ def _render_human(data: Any, title: str):
63
+ """
64
+ Human channel: Visual priority.
65
+ """
66
+ console = Console()
67
+
68
+ if title:
69
+ console.rule(f"[bold blue]{title}[/bold blue]")
70
+
71
+ if isinstance(data, str):
72
+ console.print(data)
73
+ return
74
+
75
+ # Special handling for Lists of Pydantic Models -> Table
76
+ if isinstance(data, list) and data and isinstance(data[0], BaseModel):
77
+ table = Table(show_header=True, header_style="bold magenta")
78
+
79
+ # Introspect fields from the first item
80
+ model_type = type(data[0])
81
+ fields = model_type.model_fields.keys()
82
+
83
+ for field in fields:
84
+ table.add_column(field.replace("_", " ").title())
85
+
86
+ for item in data:
87
+ row = [str(getattr(item, field)) for field in fields]
88
+ table.add_row(*row)
89
+
90
+ console.print(table)
91
+ return
92
+
93
+ # Fallback to rich pretty print
94
+ rprint(data)
95
+
96
+ # Global helper
97
+ print_output = OutputManager.print
monoco/core/setup.py ADDED
@@ -0,0 +1,285 @@
1
+ import os
2
+ import subprocess
3
+ import yaml
4
+ from pathlib import Path
5
+ import typer
6
+ from rich.console import Console
7
+ from monoco.core.output import print_output
8
+
9
+ console = Console()
10
+
11
+ def get_git_user() -> str:
12
+ try:
13
+ result = subprocess.run(
14
+ ["git", "config", "user.name"],
15
+ capture_output=True,
16
+ text=True,
17
+ timeout=1
18
+ )
19
+ return result.stdout.strip()
20
+ except Exception:
21
+ return ""
22
+
23
+ def generate_key(name: str) -> str:
24
+ """Generate a 3-4 letter uppercase key from name."""
25
+ # Strategy 1: Upper case of first letters of words
26
+ parts = name.split()
27
+ if len(parts) >= 2:
28
+ candidate = "".join(p[0] for p in parts[:4]).upper()
29
+ if len(candidate) >= 2:
30
+ return candidate
31
+
32
+ # Strategy 2: First 3 letters
33
+ return name[:3].upper()
34
+
35
+ from prompt_toolkit.application import Application
36
+ from prompt_toolkit.layout.containers import Window, HSplit
37
+ from prompt_toolkit.layout.controls import FormattedTextControl
38
+ from prompt_toolkit.layout.layout import Layout
39
+ from prompt_toolkit.key_binding import KeyBindings
40
+ from prompt_toolkit.styles import Style
41
+ import sys
42
+
43
+ def ask_with_selection(message: str, default: str) -> str:
44
+ """Provides a selection-based prompt for stable rendering."""
45
+ options = [f"{default} (Default)", "Custom Input..."]
46
+ selected_index = 0
47
+
48
+ kb = KeyBindings()
49
+
50
+ @kb.add('up')
51
+ @kb.add('k')
52
+ def _(event):
53
+ nonlocal selected_index
54
+ selected_index = (selected_index - 1) % len(options)
55
+
56
+ @kb.add('down')
57
+ @kb.add('j')
58
+ def _(event):
59
+ nonlocal selected_index
60
+ selected_index = (selected_index + 1) % len(options)
61
+
62
+ @kb.add('enter')
63
+ def _(event):
64
+ event.app.exit(result=selected_index)
65
+
66
+ @kb.add('c-c')
67
+ def _(event):
68
+ console.print("\n[red]Aborted by user.[/red]")
69
+ sys.exit(0)
70
+
71
+ def get_text():
72
+ # Render the menu with explicit highlighting
73
+ res = [('class:message', f"{message}:\n")]
74
+ for i, opt in enumerate(options):
75
+ if i == selected_index:
76
+ res.append(('class:selected', f" ➔ {opt}\n"))
77
+ else:
78
+ res.append(('class:unselected', f" {opt}\n"))
79
+ return res
80
+
81
+ style = Style.from_dict({
82
+ 'message': 'bold #ffffff',
83
+ 'selected': 'bold #00ff00', # High contrast green
84
+ 'unselected': '#888888',
85
+ })
86
+
87
+ # Run a mini application to handle the selection
88
+ app = Application(
89
+ layout=Layout(HSplit([Window(content=FormattedTextControl(get_text), height=len(options)+1)])),
90
+ key_bindings=kb,
91
+ style=style,
92
+ full_screen=False,
93
+ )
94
+
95
+ # Flush stdout to ensure previous output is visible
96
+ sys.stdout.flush()
97
+
98
+ choice = app.run()
99
+
100
+ if choice == 0:
101
+ return default
102
+ else:
103
+ # Prompt for custom input
104
+ from prompt_toolkit import prompt
105
+ return prompt(f"Enter custom {message.lower()}: ").strip() or default
106
+
107
+ def init_cli(
108
+ ctx: typer.Context,
109
+ global_only: bool = typer.Option(False, "--global", help="Only configure global user settings"),
110
+ project_only: bool = typer.Option(False, "--project", help="Only configure current project")
111
+ ):
112
+ """
113
+ Initialize Monoco configuration (Global and/or Project).
114
+ """
115
+ from rich.prompt import Confirm
116
+
117
+ home_dir = Path.home() / ".monoco"
118
+ global_config_path = home_dir / "config.yaml"
119
+
120
+ # --- 1. Global Configuration ---
121
+ if not project_only:
122
+ if not global_config_path.exists() or global_only:
123
+ console.rule("[bold blue]Global Setup[/bold blue]")
124
+
125
+ # Ensure ~/.monoco exists
126
+ home_dir.mkdir(parents=True, exist_ok=True)
127
+
128
+ default_author = get_git_user() or os.getenv("USER", "developer")
129
+ author = ask_with_selection("Your Name (for issue tracking)", default_author)
130
+
131
+ telemetry_enabled = Confirm.ask("Enable anonymous telemetry to help improve Monoco?", default=True)
132
+
133
+ user_config = {
134
+ "core": {
135
+ "author": author,
136
+ # Editor is handled by env/config defaults, no need to prompt
137
+ },
138
+ "telemetry": {
139
+ "enabled": telemetry_enabled
140
+ }
141
+ }
142
+
143
+ with open(global_config_path, "w") as f:
144
+ yaml.dump(user_config, f, default_flow_style=False)
145
+
146
+ console.print(f"[green]✓ Global config saved to {global_config_path}[/green]\n")
147
+
148
+ if global_only:
149
+ return
150
+
151
+ # --- 2. Project Configuration ---
152
+ cwd = Path.cwd()
153
+ project_config_dir = cwd / ".monoco"
154
+ project_config_path = project_config_dir / "config.yaml"
155
+
156
+ # Check if we should init project
157
+ if project_config_path.exists():
158
+ if not Confirm.ask(f"Project config already exists at [dim]{project_config_path}[/dim]. Overwrite?"):
159
+ console.print("[yellow]Skipping project initialization.[/yellow]")
160
+ return
161
+
162
+ console.rule("[bold blue]Project Setup[/bold blue]")
163
+
164
+ default_name = cwd.name
165
+ project_name = ask_with_selection("Project Name", default_name)
166
+
167
+ default_key = generate_key(project_name)
168
+ project_key = ask_with_selection("Project Key (prefix for issues)", default_key)
169
+
170
+
171
+ project_config_dir.mkdir(exist_ok=True)
172
+
173
+ project_config = {
174
+ "project": {
175
+ "name": project_name,
176
+ "key": project_key
177
+ },
178
+ "paths": {
179
+ "issues": "Issues",
180
+ "spikes": ".references",
181
+ "specs": "SPECS"
182
+ }
183
+ }
184
+
185
+ with open(project_config_path, "w") as f:
186
+ yaml.dump(project_config, f, default_flow_style=False)
187
+
188
+ # 2b. Generate Config Template
189
+ template_path = project_config_dir / "config_template.yaml"
190
+ template_content = """# Monoco Configuration Template
191
+ # This file serves as a reference for all available configuration options.
192
+ # Rename this file to config.yaml to use it.
193
+
194
+ core:
195
+ # Default author for new artifacts (e.g. issues)
196
+ # author: "Developer Name"
197
+
198
+ # Logging verbosity (DEBUG, INFO, WARNING, ERROR)
199
+ # log_level: "INFO"
200
+
201
+ # Preferred text editor
202
+ # editor: "vim"
203
+
204
+ project:
205
+ # The display name of the project
206
+ name: "My Project"
207
+
208
+ # The prefix used for issue IDs (e.g. MON-001)
209
+ key: "MON"
210
+
211
+ # Managed external research repositories (name -> url)
212
+ # spike_repos:
213
+ # react: "https://github.com/facebook/react"
214
+
215
+ paths:
216
+ # Directory for tracking issues
217
+ issues: "Issues"
218
+
219
+ # Directory for specifications/documents
220
+ specs: "SPECS"
221
+
222
+ # Directory for research references (spikes)
223
+ spikes: ".references"
224
+
225
+ i18n:
226
+ # Source language code
227
+ source_lang: "en"
228
+
229
+ # Target language codes for translation
230
+ target_langs:
231
+ - "zh"
232
+
233
+ ui:
234
+ # Custom Domain Terminology Mapping
235
+ # Use this to rename core concepts in the UI without changing internal logic.
236
+ dictionary:
237
+ # Entities
238
+ epic: "Saga"
239
+ feature: "Story"
240
+ chore: "Task"
241
+ fix: "Bug"
242
+
243
+ # Statuses
244
+ todo: "Pending"
245
+ doing: "In Progress"
246
+ review: "QA"
247
+ done: "Released"
248
+ """
249
+ with open(template_path, "w") as f:
250
+ f.write(template_content)
251
+
252
+ # 3. Scaffold Directories & Modules
253
+
254
+ # Import Feature Cores locally to avoid circular deps if any (though setup is core)
255
+ from monoco.features.issue import core as issue_core
256
+ from monoco.features.spike import core as spike_core
257
+ from monoco.features.i18n import core as i18n_core
258
+ from monoco.features import skills
259
+
260
+ # Initialize Issues
261
+ issues_path = cwd / project_config["paths"]["issues"]
262
+ issue_core.init(issues_path)
263
+
264
+ # Initialize Spikes
265
+ spikes_name = project_config["paths"]["spikes"]
266
+ spike_core.init(cwd, spikes_name)
267
+
268
+ # Initialize I18n
269
+ i18n_core.init(cwd)
270
+
271
+ # Initialize Skills & Agent Docs
272
+ resources = [
273
+ issue_core.get_resources(),
274
+ spike_core.get_resources(),
275
+ i18n_core.get_resources()
276
+ ]
277
+ skills.init(cwd, resources)
278
+
279
+ console.print(f"[green]✓ Project config initialized at {project_config_path}[/green]")
280
+ console.print(f"[green]✓ Config template generated at {template_path}[/green]")
281
+
282
+
283
+
284
+ console.print(f"[green]Access configured! issues will be created as {project_key}-XXX[/green]")
285
+