monoco-toolkit 0.1.1__py3-none-any.whl → 0.2.5__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.
- monoco/cli/__init__.py +0 -0
- monoco/cli/project.py +87 -0
- monoco/cli/workspace.py +46 -0
- monoco/core/agent/__init__.py +5 -0
- monoco/core/agent/action.py +144 -0
- monoco/core/agent/adapters.py +106 -0
- monoco/core/agent/protocol.py +31 -0
- monoco/core/agent/state.py +106 -0
- monoco/core/config.py +152 -17
- monoco/core/execution.py +62 -0
- monoco/core/feature.py +58 -0
- monoco/core/git.py +51 -2
- monoco/core/injection.py +196 -0
- monoco/core/integrations.py +234 -0
- monoco/core/lsp.py +61 -0
- monoco/core/output.py +13 -2
- monoco/core/registry.py +36 -0
- monoco/core/resources/en/AGENTS.md +8 -0
- monoco/core/resources/en/SKILL.md +66 -0
- monoco/core/resources/zh/AGENTS.md +8 -0
- monoco/core/resources/zh/SKILL.md +66 -0
- monoco/core/setup.py +88 -110
- monoco/core/skills.py +444 -0
- monoco/core/state.py +53 -0
- monoco/core/sync.py +224 -0
- monoco/core/telemetry.py +4 -1
- monoco/core/workspace.py +85 -20
- monoco/daemon/app.py +127 -58
- monoco/daemon/models.py +4 -0
- monoco/daemon/services.py +56 -155
- monoco/features/agent/commands.py +166 -0
- monoco/features/agent/doctor.py +30 -0
- monoco/features/config/commands.py +125 -44
- monoco/features/i18n/adapter.py +29 -0
- monoco/features/i18n/commands.py +89 -10
- monoco/features/i18n/core.py +113 -27
- monoco/features/i18n/resources/en/AGENTS.md +8 -0
- monoco/features/i18n/resources/en/SKILL.md +94 -0
- monoco/features/i18n/resources/zh/AGENTS.md +8 -0
- monoco/features/i18n/resources/zh/SKILL.md +94 -0
- monoco/features/issue/adapter.py +34 -0
- monoco/features/issue/commands.py +183 -65
- monoco/features/issue/core.py +172 -77
- monoco/features/issue/linter.py +215 -116
- monoco/features/issue/migration.py +134 -0
- monoco/features/issue/models.py +23 -19
- monoco/features/issue/monitor.py +94 -0
- monoco/features/issue/resources/en/AGENTS.md +15 -0
- monoco/features/issue/resources/en/SKILL.md +87 -0
- monoco/features/issue/resources/zh/AGENTS.md +15 -0
- monoco/features/issue/resources/zh/SKILL.md +114 -0
- monoco/features/issue/validator.py +269 -0
- monoco/features/pty/core.py +185 -0
- monoco/features/pty/router.py +138 -0
- monoco/features/pty/server.py +56 -0
- monoco/features/spike/adapter.py +30 -0
- monoco/features/spike/commands.py +45 -24
- monoco/features/spike/core.py +4 -21
- monoco/features/spike/resources/en/AGENTS.md +7 -0
- monoco/features/spike/resources/en/SKILL.md +74 -0
- monoco/features/spike/resources/zh/AGENTS.md +7 -0
- monoco/features/spike/resources/zh/SKILL.md +74 -0
- monoco/main.py +115 -2
- {monoco_toolkit-0.1.1.dist-info → monoco_toolkit-0.2.5.dist-info}/METADATA +2 -2
- monoco_toolkit-0.2.5.dist-info/RECORD +77 -0
- monoco_toolkit-0.1.1.dist-info/RECORD +0 -33
- {monoco_toolkit-0.1.1.dist-info → monoco_toolkit-0.2.5.dist-info}/WHEEL +0 -0
- {monoco_toolkit-0.1.1.dist-info → monoco_toolkit-0.2.5.dist-info}/entry_points.txt +0 -0
- {monoco_toolkit-0.1.1.dist-info → monoco_toolkit-0.2.5.dist-info}/licenses/LICENSE +0 -0
monoco/core/skills.py
ADDED
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Skill Manager for Monoco Toolkit.
|
|
3
|
+
|
|
4
|
+
This module provides centralized management and distribution of Agent Skills
|
|
5
|
+
following the agentskills.io standard.
|
|
6
|
+
|
|
7
|
+
Key Responsibilities:
|
|
8
|
+
1. Discover skills from the source directory (Toolkit/skills/)
|
|
9
|
+
2. Validate skill structure and metadata (YAML frontmatter)
|
|
10
|
+
3. Distribute skills to target agent framework directories
|
|
11
|
+
4. Support i18n for skill content
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import shutil
|
|
15
|
+
import hashlib
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Dict, List, Optional, Set
|
|
18
|
+
from pydantic import BaseModel, Field, ValidationError
|
|
19
|
+
from rich.console import Console
|
|
20
|
+
import yaml
|
|
21
|
+
|
|
22
|
+
console = Console()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class SkillMetadata(BaseModel):
|
|
26
|
+
"""
|
|
27
|
+
Skill metadata from YAML frontmatter.
|
|
28
|
+
Based on agentskills.io standard.
|
|
29
|
+
"""
|
|
30
|
+
name: str = Field(..., description="Unique skill identifier (lowercase, hyphens)")
|
|
31
|
+
description: str = Field(..., description="Clear description of what the skill does and when to use it")
|
|
32
|
+
version: Optional[str] = Field(default=None, description="Skill version")
|
|
33
|
+
author: Optional[str] = Field(default=None, description="Skill author")
|
|
34
|
+
tags: Optional[List[str]] = Field(default=None, description="Skill tags for categorization")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class Skill:
|
|
38
|
+
"""
|
|
39
|
+
Represents a single skill with its metadata and file paths.
|
|
40
|
+
"""
|
|
41
|
+
def __init__(self, root_dir: Path, skill_dir: Path):
|
|
42
|
+
"""
|
|
43
|
+
Initialize a Skill instance.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
root_dir: Project root directory
|
|
47
|
+
skill_dir: Path to the skill directory (e.g., Toolkit/skills/issues-management)
|
|
48
|
+
"""
|
|
49
|
+
self.root_dir = root_dir
|
|
50
|
+
self.skill_dir = skill_dir
|
|
51
|
+
self.name = skill_dir.name
|
|
52
|
+
self.skill_file = skill_dir / "SKILL.md"
|
|
53
|
+
self.metadata: Optional[SkillMetadata] = None
|
|
54
|
+
self._load_metadata()
|
|
55
|
+
|
|
56
|
+
def _load_metadata(self) -> None:
|
|
57
|
+
"""Load and validate skill metadata from SKILL.md frontmatter."""
|
|
58
|
+
# Try to load from language subdirectories first (Feature resources pattern)
|
|
59
|
+
# Then fallback to root SKILL.md (legacy pattern)
|
|
60
|
+
skill_file_to_use = None
|
|
61
|
+
|
|
62
|
+
# Check language subdirectories
|
|
63
|
+
if self.skill_dir.exists():
|
|
64
|
+
for item in sorted(self.skill_dir.iterdir()):
|
|
65
|
+
if item.is_dir() and len(item.name) == 2: # 2-letter lang code
|
|
66
|
+
candidate = item / "SKILL.md"
|
|
67
|
+
if candidate.exists():
|
|
68
|
+
skill_file_to_use = candidate
|
|
69
|
+
break
|
|
70
|
+
|
|
71
|
+
# Fallback to root SKILL.md
|
|
72
|
+
if not skill_file_to_use and self.skill_file.exists():
|
|
73
|
+
skill_file_to_use = self.skill_file
|
|
74
|
+
|
|
75
|
+
if not skill_file_to_use:
|
|
76
|
+
return
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
content = skill_file_to_use.read_text(encoding="utf-8")
|
|
80
|
+
# Extract YAML frontmatter
|
|
81
|
+
if content.startswith("---"):
|
|
82
|
+
parts = content.split("---", 2)
|
|
83
|
+
if len(parts) >= 3:
|
|
84
|
+
frontmatter = parts[1].strip()
|
|
85
|
+
metadata_dict = yaml.safe_load(frontmatter)
|
|
86
|
+
|
|
87
|
+
# Validate against schema
|
|
88
|
+
self.metadata = SkillMetadata(**metadata_dict)
|
|
89
|
+
except ValidationError as e:
|
|
90
|
+
console.print(f"[red]Invalid metadata in {skill_file_to_use}: {e}[/red]")
|
|
91
|
+
except Exception as e:
|
|
92
|
+
console.print(f"[yellow]Warning: Failed to parse metadata from {skill_file_to_use}: {e}[/yellow]")
|
|
93
|
+
|
|
94
|
+
def is_valid(self) -> bool:
|
|
95
|
+
"""Check if the skill has valid metadata."""
|
|
96
|
+
return self.metadata is not None
|
|
97
|
+
|
|
98
|
+
def get_languages(self) -> List[str]:
|
|
99
|
+
"""
|
|
100
|
+
Detect available language versions of this skill.
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
List of language codes (e.g., ['en', 'zh'])
|
|
104
|
+
"""
|
|
105
|
+
languages = []
|
|
106
|
+
|
|
107
|
+
# Check for language subdirectories (Feature resources pattern)
|
|
108
|
+
# resources/en/SKILL.md, resources/zh/SKILL.md
|
|
109
|
+
for item in self.skill_dir.iterdir():
|
|
110
|
+
if item.is_dir() and len(item.name) == 2: # Assume 2-letter lang codes
|
|
111
|
+
lang_skill_file = item / "SKILL.md"
|
|
112
|
+
if lang_skill_file.exists():
|
|
113
|
+
languages.append(item.name)
|
|
114
|
+
|
|
115
|
+
# Fallback: check for root SKILL.md (legacy Toolkit/skills pattern)
|
|
116
|
+
# We don't assume a default language, just return what we found
|
|
117
|
+
if not languages and self.skill_file.exists():
|
|
118
|
+
# For legacy pattern, we can't determine the language from structure
|
|
119
|
+
# Return empty to indicate this skill uses legacy pattern
|
|
120
|
+
pass
|
|
121
|
+
|
|
122
|
+
return languages
|
|
123
|
+
|
|
124
|
+
def get_checksum(self, lang: str) -> str:
|
|
125
|
+
"""
|
|
126
|
+
Calculate checksum for the skill content.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
lang: Language code
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
SHA256 checksum of the skill file
|
|
133
|
+
"""
|
|
134
|
+
# Try language subdirectory first (Feature resources pattern)
|
|
135
|
+
target_file = self.skill_dir / lang / "SKILL.md"
|
|
136
|
+
|
|
137
|
+
# Fallback to root SKILL.md (legacy pattern)
|
|
138
|
+
if not target_file.exists():
|
|
139
|
+
target_file = self.skill_file
|
|
140
|
+
|
|
141
|
+
if not target_file.exists():
|
|
142
|
+
return ""
|
|
143
|
+
|
|
144
|
+
content = target_file.read_bytes()
|
|
145
|
+
return hashlib.sha256(content).hexdigest()
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class SkillManager:
|
|
149
|
+
"""
|
|
150
|
+
Central manager for Monoco skills.
|
|
151
|
+
|
|
152
|
+
Responsibilities:
|
|
153
|
+
- Collect skills from Feature resources
|
|
154
|
+
- Validate skill structure
|
|
155
|
+
- Distribute skills to agent framework directories
|
|
156
|
+
"""
|
|
157
|
+
|
|
158
|
+
def __init__(self, root: Path, features: Optional[List] = None):
|
|
159
|
+
"""
|
|
160
|
+
Initialize SkillManager.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
root: Project root directory
|
|
164
|
+
features: List of MonocoFeature instances (if None, will load from registry)
|
|
165
|
+
"""
|
|
166
|
+
self.root = root
|
|
167
|
+
self.features = features or []
|
|
168
|
+
self.skills: Dict[str, Skill] = {}
|
|
169
|
+
|
|
170
|
+
if self.features:
|
|
171
|
+
self._discover_skills_from_features()
|
|
172
|
+
|
|
173
|
+
# Also discover core skill (monoco/core/resources/)
|
|
174
|
+
self._discover_core_skill()
|
|
175
|
+
|
|
176
|
+
def _discover_core_skill(self) -> None:
|
|
177
|
+
"""
|
|
178
|
+
Discover skill from monoco/core/resources/.
|
|
179
|
+
|
|
180
|
+
Core is special - it's not a Feature but still has a skill.
|
|
181
|
+
"""
|
|
182
|
+
core_resources_dir = self.root / "monoco" / "core" / "resources"
|
|
183
|
+
|
|
184
|
+
if not core_resources_dir.exists():
|
|
185
|
+
return
|
|
186
|
+
|
|
187
|
+
# Check for SKILL.md in language directories
|
|
188
|
+
for lang_dir in core_resources_dir.iterdir():
|
|
189
|
+
if lang_dir.is_dir() and (lang_dir / "SKILL.md").exists():
|
|
190
|
+
skill = Skill(self.root, core_resources_dir)
|
|
191
|
+
|
|
192
|
+
# Use the skill's metadata name if available
|
|
193
|
+
if skill.metadata and skill.metadata.name:
|
|
194
|
+
skill.name = skill.metadata.name.replace('-', '_')
|
|
195
|
+
else:
|
|
196
|
+
skill.name = "monoco_core"
|
|
197
|
+
|
|
198
|
+
if skill.is_valid():
|
|
199
|
+
self.skills[skill.name] = skill
|
|
200
|
+
break # Only need to detect once
|
|
201
|
+
|
|
202
|
+
def _discover_skills_from_features(self) -> None:
|
|
203
|
+
"""
|
|
204
|
+
Discover skills from Feature resources.
|
|
205
|
+
|
|
206
|
+
Each feature should have:
|
|
207
|
+
- monoco/features/{feature}/resources/{lang}/SKILL.md
|
|
208
|
+
"""
|
|
209
|
+
from monoco.core.feature import MonocoFeature
|
|
210
|
+
|
|
211
|
+
for feature in self.features:
|
|
212
|
+
if not isinstance(feature, MonocoFeature):
|
|
213
|
+
continue
|
|
214
|
+
|
|
215
|
+
# Determine feature module path
|
|
216
|
+
# feature.__class__.__module__ is like 'monoco.features.issue.adapter'
|
|
217
|
+
module_parts = feature.__class__.__module__.split('.')
|
|
218
|
+
if len(module_parts) >= 3 and module_parts[0] == 'monoco' and module_parts[1] == 'features':
|
|
219
|
+
feature_name = module_parts[2]
|
|
220
|
+
|
|
221
|
+
# Construct path to feature resources
|
|
222
|
+
# monoco/features/{feature}/resources/
|
|
223
|
+
feature_dir = self.root / "monoco" / "features" / feature_name
|
|
224
|
+
resources_dir = feature_dir / "resources"
|
|
225
|
+
|
|
226
|
+
if not resources_dir.exists():
|
|
227
|
+
continue
|
|
228
|
+
|
|
229
|
+
# Check for SKILL.md in language directories
|
|
230
|
+
for lang_dir in resources_dir.iterdir():
|
|
231
|
+
if lang_dir.is_dir() and (lang_dir / "SKILL.md").exists():
|
|
232
|
+
# Create a Skill instance
|
|
233
|
+
# We need to adapt the Skill class to work with feature resources
|
|
234
|
+
skill = self._create_skill_from_feature(feature_name, resources_dir)
|
|
235
|
+
if skill and skill.is_valid():
|
|
236
|
+
# Use feature name as skill identifier
|
|
237
|
+
skill_key = f"{feature_name}"
|
|
238
|
+
if skill_key not in self.skills:
|
|
239
|
+
self.skills[skill_key] = skill
|
|
240
|
+
break # Only need to detect once per feature
|
|
241
|
+
|
|
242
|
+
def _create_skill_from_feature(self, feature_name: str, resources_dir: Path) -> Optional[Skill]:
|
|
243
|
+
"""
|
|
244
|
+
Create a Skill instance from a feature's resources directory.
|
|
245
|
+
|
|
246
|
+
Args:
|
|
247
|
+
feature_name: Name of the feature (e.g., 'issue', 'spike')
|
|
248
|
+
resources_dir: Path to the feature's resources directory
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
Skill instance or None if creation fails
|
|
252
|
+
"""
|
|
253
|
+
# Use the resources directory as the skill directory
|
|
254
|
+
# The Skill class expects a directory with SKILL.md or {lang}/SKILL.md
|
|
255
|
+
skill = Skill(self.root, resources_dir)
|
|
256
|
+
|
|
257
|
+
# Use the skill's metadata name if available (e.g., 'monoco-issue')
|
|
258
|
+
# Convert to snake_case for directory name (e.g., 'monoco_issue')
|
|
259
|
+
if skill.metadata and skill.metadata.name:
|
|
260
|
+
# Convert kebab-case to snake_case for directory name
|
|
261
|
+
skill.name = skill.metadata.name.replace('-', '_')
|
|
262
|
+
else:
|
|
263
|
+
# Fallback to feature name
|
|
264
|
+
skill.name = f"monoco_{feature_name}"
|
|
265
|
+
|
|
266
|
+
return skill
|
|
267
|
+
|
|
268
|
+
def list_skills(self) -> List[Skill]:
|
|
269
|
+
"""
|
|
270
|
+
Get all available skills.
|
|
271
|
+
|
|
272
|
+
Returns:
|
|
273
|
+
List of Skill instances
|
|
274
|
+
"""
|
|
275
|
+
return list(self.skills.values())
|
|
276
|
+
|
|
277
|
+
def get_skill(self, name: str) -> Optional[Skill]:
|
|
278
|
+
"""
|
|
279
|
+
Get a specific skill by name.
|
|
280
|
+
|
|
281
|
+
Args:
|
|
282
|
+
name: Skill name
|
|
283
|
+
|
|
284
|
+
Returns:
|
|
285
|
+
Skill instance or None if not found
|
|
286
|
+
"""
|
|
287
|
+
return self.skills.get(name)
|
|
288
|
+
|
|
289
|
+
def distribute(
|
|
290
|
+
self,
|
|
291
|
+
target_dir: Path,
|
|
292
|
+
lang: str,
|
|
293
|
+
force: bool = False
|
|
294
|
+
) -> Dict[str, bool]:
|
|
295
|
+
"""
|
|
296
|
+
Distribute skills to a target directory.
|
|
297
|
+
|
|
298
|
+
Args:
|
|
299
|
+
target_dir: Target directory for skill distribution (e.g., .cursor/skills/)
|
|
300
|
+
lang: Language code to distribute (e.g., 'en', 'zh')
|
|
301
|
+
force: Force overwrite even if checksum matches
|
|
302
|
+
|
|
303
|
+
Returns:
|
|
304
|
+
Dictionary mapping skill names to success status
|
|
305
|
+
"""
|
|
306
|
+
results = {}
|
|
307
|
+
|
|
308
|
+
# Ensure target directory exists
|
|
309
|
+
target_dir.mkdir(parents=True, exist_ok=True)
|
|
310
|
+
|
|
311
|
+
for skill_name, skill in self.skills.items():
|
|
312
|
+
try:
|
|
313
|
+
# Check if this skill has the requested language
|
|
314
|
+
available_languages = skill.get_languages()
|
|
315
|
+
|
|
316
|
+
if lang not in available_languages:
|
|
317
|
+
console.print(f"[yellow]Skill {skill_name} does not have {lang} version, skipping[/yellow]")
|
|
318
|
+
results[skill_name] = False
|
|
319
|
+
continue
|
|
320
|
+
|
|
321
|
+
# Distribute the specific language version
|
|
322
|
+
self._distribute_skill_language(skill, target_dir, lang, force)
|
|
323
|
+
results[skill_name] = True
|
|
324
|
+
|
|
325
|
+
except Exception as e:
|
|
326
|
+
console.print(f"[red]Failed to distribute skill {skill_name}: {e}[/red]")
|
|
327
|
+
results[skill_name] = False
|
|
328
|
+
|
|
329
|
+
return results
|
|
330
|
+
|
|
331
|
+
def _distribute_skill_language(
|
|
332
|
+
self,
|
|
333
|
+
skill: Skill,
|
|
334
|
+
target_dir: Path,
|
|
335
|
+
lang: str,
|
|
336
|
+
force: bool
|
|
337
|
+
) -> None:
|
|
338
|
+
"""
|
|
339
|
+
Distribute a specific language version of a skill.
|
|
340
|
+
|
|
341
|
+
Args:
|
|
342
|
+
skill: Skill instance
|
|
343
|
+
target_dir: Target directory (e.g., .cursor/skills/)
|
|
344
|
+
lang: Language code
|
|
345
|
+
force: Force overwrite
|
|
346
|
+
"""
|
|
347
|
+
# Determine source file (try language subdirectory first)
|
|
348
|
+
source_file = skill.skill_dir / lang / "SKILL.md"
|
|
349
|
+
|
|
350
|
+
# Fallback to root SKILL.md (legacy pattern)
|
|
351
|
+
if not source_file.exists():
|
|
352
|
+
source_file = skill.skill_file
|
|
353
|
+
|
|
354
|
+
if not source_file.exists():
|
|
355
|
+
console.print(f"[yellow]Source file not found for {skill.name}/{lang}[/yellow]")
|
|
356
|
+
return
|
|
357
|
+
|
|
358
|
+
# Target path: {target_dir}/{skill_name}/SKILL.md (no language subdirectory)
|
|
359
|
+
target_skill_dir = target_dir / skill.name
|
|
360
|
+
|
|
361
|
+
# Create target directory
|
|
362
|
+
target_skill_dir.mkdir(parents=True, exist_ok=True)
|
|
363
|
+
target_file = target_skill_dir / "SKILL.md"
|
|
364
|
+
|
|
365
|
+
# Check if update is needed
|
|
366
|
+
if target_file.exists() and not force:
|
|
367
|
+
source_checksum = skill.get_checksum(lang)
|
|
368
|
+
target_content = target_file.read_bytes()
|
|
369
|
+
target_checksum = hashlib.sha256(target_content).hexdigest()
|
|
370
|
+
|
|
371
|
+
if source_checksum == target_checksum:
|
|
372
|
+
console.print(f"[dim] = {skill.name}/SKILL.md is up to date[/dim]")
|
|
373
|
+
return
|
|
374
|
+
|
|
375
|
+
# Copy the file
|
|
376
|
+
shutil.copy2(source_file, target_file)
|
|
377
|
+
console.print(f"[green] ✓ Distributed {skill.name}/SKILL.md ({lang})[/green]")
|
|
378
|
+
|
|
379
|
+
# Copy additional resources if they exist
|
|
380
|
+
self._copy_skill_resources(skill.skill_dir, target_skill_dir, lang)
|
|
381
|
+
|
|
382
|
+
def _copy_skill_resources(
|
|
383
|
+
self,
|
|
384
|
+
source_dir: Path,
|
|
385
|
+
target_dir: Path,
|
|
386
|
+
lang: str
|
|
387
|
+
) -> None:
|
|
388
|
+
"""
|
|
389
|
+
Copy additional skill resources (scripts, examples, etc.).
|
|
390
|
+
|
|
391
|
+
Args:
|
|
392
|
+
source_dir: Source skill directory
|
|
393
|
+
target_dir: Target skill directory
|
|
394
|
+
lang: Language code
|
|
395
|
+
"""
|
|
396
|
+
# Define resource directories to copy
|
|
397
|
+
resource_dirs = ['scripts', 'examples', 'resources']
|
|
398
|
+
|
|
399
|
+
# Try language subdirectory first (Feature resources pattern)
|
|
400
|
+
source_base = source_dir / lang
|
|
401
|
+
|
|
402
|
+
# Fallback to root directory (legacy pattern)
|
|
403
|
+
if not source_base.exists():
|
|
404
|
+
source_base = source_dir
|
|
405
|
+
|
|
406
|
+
for resource_name in resource_dirs:
|
|
407
|
+
source_resource = source_base / resource_name
|
|
408
|
+
if source_resource.exists() and source_resource.is_dir():
|
|
409
|
+
target_resource = target_dir / resource_name
|
|
410
|
+
|
|
411
|
+
# Remove existing and copy fresh
|
|
412
|
+
if target_resource.exists():
|
|
413
|
+
shutil.rmtree(target_resource)
|
|
414
|
+
|
|
415
|
+
shutil.copytree(source_resource, target_resource)
|
|
416
|
+
console.print(f"[dim] Copied {resource_name}/ for {source_dir.name}/{lang}[/dim]")
|
|
417
|
+
|
|
418
|
+
def cleanup(self, target_dir: Path) -> None:
|
|
419
|
+
"""
|
|
420
|
+
Remove distributed skills from a target directory.
|
|
421
|
+
|
|
422
|
+
Args:
|
|
423
|
+
target_dir: Target directory to clean
|
|
424
|
+
"""
|
|
425
|
+
if not target_dir.exists():
|
|
426
|
+
console.print(f"[dim]Target directory does not exist: {target_dir}[/dim]")
|
|
427
|
+
return
|
|
428
|
+
|
|
429
|
+
removed_count = 0
|
|
430
|
+
|
|
431
|
+
for skill_name in self.skills.keys():
|
|
432
|
+
skill_target = target_dir / skill_name
|
|
433
|
+
if skill_target.exists():
|
|
434
|
+
shutil.rmtree(skill_target)
|
|
435
|
+
console.print(f"[green] ✓ Removed {skill_name}[/green]")
|
|
436
|
+
removed_count += 1
|
|
437
|
+
|
|
438
|
+
# Remove empty parent directory if no skills remain
|
|
439
|
+
if target_dir.exists() and not any(target_dir.iterdir()):
|
|
440
|
+
target_dir.rmdir()
|
|
441
|
+
console.print(f"[dim] Removed empty directory: {target_dir}[/dim]")
|
|
442
|
+
|
|
443
|
+
if removed_count == 0:
|
|
444
|
+
console.print(f"[dim]No skills to remove from {target_dir}[/dim]")
|
monoco/core/state.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Optional, Any, Dict
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger("monoco.core.state")
|
|
8
|
+
|
|
9
|
+
class WorkspaceState(BaseModel):
|
|
10
|
+
"""
|
|
11
|
+
Persisted state for a Monoco workspace (collection of projects).
|
|
12
|
+
Stored in <workspace_root>/.monoco/state.json
|
|
13
|
+
"""
|
|
14
|
+
last_active_project_id: Optional[str] = None
|
|
15
|
+
|
|
16
|
+
@classmethod
|
|
17
|
+
def load(cls, workspace_root: Path) -> "WorkspaceState":
|
|
18
|
+
state_file = workspace_root / ".monoco" / "state.json"
|
|
19
|
+
if not state_file.exists():
|
|
20
|
+
return cls()
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
content = state_file.read_text(encoding='utf-8')
|
|
24
|
+
if not content.strip():
|
|
25
|
+
return cls()
|
|
26
|
+
data = json.loads(content)
|
|
27
|
+
return cls(**data)
|
|
28
|
+
except Exception as e:
|
|
29
|
+
logger.error(f"Failed to load workspace state from {state_file}: {e}")
|
|
30
|
+
return cls()
|
|
31
|
+
|
|
32
|
+
def save(self, workspace_root: Path):
|
|
33
|
+
state_file = workspace_root / ".monoco" / "state.json"
|
|
34
|
+
state_file.parent.mkdir(parents=True, exist_ok=True)
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
# We merge with existing on disk if possible to preserve unknown keys
|
|
38
|
+
current_data = {}
|
|
39
|
+
if state_file.exists():
|
|
40
|
+
try:
|
|
41
|
+
content = state_file.read_text(encoding='utf-8')
|
|
42
|
+
if content.strip():
|
|
43
|
+
current_data = json.loads(content)
|
|
44
|
+
except:
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
new_data = self.model_dump(exclude_unset=True)
|
|
48
|
+
current_data.update(new_data)
|
|
49
|
+
|
|
50
|
+
state_file.write_text(json.dumps(current_data, indent=2), encoding='utf-8')
|
|
51
|
+
except Exception as e:
|
|
52
|
+
logger.error(f"Failed to save workspace state to {state_file}: {e}")
|
|
53
|
+
raise
|