monoco-toolkit 0.2.7__py3-none-any.whl → 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- monoco/cli/project.py +35 -31
- monoco/cli/workspace.py +26 -16
- monoco/core/agent/__init__.py +0 -2
- monoco/core/agent/action.py +44 -20
- monoco/core/agent/adapters.py +20 -16
- monoco/core/agent/protocol.py +5 -4
- monoco/core/agent/state.py +21 -21
- monoco/core/config.py +90 -33
- monoco/core/execution.py +21 -16
- monoco/core/feature.py +8 -5
- monoco/core/git.py +61 -30
- monoco/core/hooks.py +57 -0
- monoco/core/injection.py +47 -44
- monoco/core/integrations.py +50 -35
- monoco/core/lsp.py +12 -1
- monoco/core/output.py +35 -16
- monoco/core/registry.py +3 -2
- monoco/core/setup.py +190 -124
- monoco/core/skills.py +121 -107
- monoco/core/state.py +12 -10
- monoco/core/sync.py +85 -56
- monoco/core/telemetry.py +10 -6
- monoco/core/workspace.py +26 -19
- monoco/daemon/app.py +123 -79
- monoco/daemon/commands.py +14 -13
- monoco/daemon/models.py +11 -3
- monoco/daemon/reproduce_stats.py +8 -8
- monoco/daemon/services.py +32 -33
- monoco/daemon/stats.py +59 -40
- monoco/features/config/commands.py +38 -25
- monoco/features/i18n/adapter.py +4 -5
- monoco/features/i18n/commands.py +83 -49
- monoco/features/i18n/core.py +94 -54
- monoco/features/issue/adapter.py +6 -7
- monoco/features/issue/commands.py +500 -260
- monoco/features/issue/core.py +504 -293
- monoco/features/issue/domain/lifecycle.py +33 -23
- monoco/features/issue/domain/models.py +71 -38
- monoco/features/issue/domain/parser.py +92 -69
- monoco/features/issue/domain/workspace.py +19 -16
- monoco/features/issue/engine/__init__.py +3 -3
- monoco/features/issue/engine/config.py +18 -25
- monoco/features/issue/engine/machine.py +72 -39
- monoco/features/issue/engine/models.py +4 -2
- monoco/features/issue/linter.py +326 -111
- monoco/features/issue/lsp/definition.py +26 -19
- monoco/features/issue/migration.py +45 -34
- monoco/features/issue/models.py +30 -13
- monoco/features/issue/monitor.py +24 -8
- monoco/features/issue/resources/en/AGENTS.md +5 -0
- monoco/features/issue/resources/en/SKILL.md +30 -2
- monoco/features/issue/resources/zh/AGENTS.md +5 -0
- monoco/features/issue/resources/zh/SKILL.md +26 -1
- monoco/features/issue/validator.py +417 -172
- monoco/features/skills/__init__.py +0 -1
- monoco/features/skills/core.py +24 -18
- monoco/features/spike/adapter.py +4 -5
- monoco/features/spike/commands.py +51 -38
- monoco/features/spike/core.py +24 -16
- monoco/main.py +34 -21
- {monoco_toolkit-0.2.7.dist-info → monoco_toolkit-0.3.0.dist-info}/METADATA +10 -3
- monoco_toolkit-0.3.0.dist-info/RECORD +84 -0
- monoco_toolkit-0.2.7.dist-info/RECORD +0 -83
- {monoco_toolkit-0.2.7.dist-info → monoco_toolkit-0.3.0.dist-info}/WHEEL +0 -0
- {monoco_toolkit-0.2.7.dist-info → monoco_toolkit-0.3.0.dist-info}/entry_points.txt +0 -0
- {monoco_toolkit-0.2.7.dist-info → monoco_toolkit-0.3.0.dist-info}/licenses/LICENSE +0 -0
monoco/core/sync.py
CHANGED
|
@@ -10,10 +10,11 @@ from rich.console import Console
|
|
|
10
10
|
|
|
11
11
|
console = Console()
|
|
12
12
|
|
|
13
|
+
|
|
13
14
|
def _get_targets(root: Path, config, cli_target: Optional[Path]) -> List[Path]:
|
|
14
15
|
"""Helper to determine target files."""
|
|
15
16
|
targets = []
|
|
16
|
-
|
|
17
|
+
|
|
17
18
|
# 1. CLI Target
|
|
18
19
|
if cli_target:
|
|
19
20
|
targets.append(cli_target)
|
|
@@ -27,11 +28,9 @@ def _get_targets(root: Path, config, cli_target: Optional[Path]) -> List[Path]:
|
|
|
27
28
|
|
|
28
29
|
# 3. Registry Defaults (Dynamic Detection)
|
|
29
30
|
integrations = get_active_integrations(
|
|
30
|
-
root,
|
|
31
|
-
config_overrides=config.agent.integrations,
|
|
32
|
-
auto_detect=True
|
|
31
|
+
root, config_overrides=config.agent.integrations, auto_detect=True
|
|
33
32
|
)
|
|
34
|
-
|
|
33
|
+
|
|
35
34
|
if integrations:
|
|
36
35
|
for integration in integrations.values():
|
|
37
36
|
targets.append(root / integration.system_prompt_file)
|
|
@@ -40,34 +39,40 @@ def _get_targets(root: Path, config, cli_target: Optional[Path]) -> List[Path]:
|
|
|
40
39
|
# but we usually want at least one target for a generic sync.
|
|
41
40
|
defaults = ["GEMINI.md", "CLAUDE.md"]
|
|
42
41
|
targets.extend([root / fname for fname in defaults])
|
|
43
|
-
|
|
44
|
-
return list(set(targets))
|
|
42
|
+
|
|
43
|
+
return list(set(targets)) # Unique paths
|
|
44
|
+
|
|
45
45
|
|
|
46
46
|
def sync_command(
|
|
47
47
|
ctx: typer.Context,
|
|
48
|
-
target: Optional[Path] = typer.Option(
|
|
49
|
-
|
|
48
|
+
target: Optional[Path] = typer.Option(
|
|
49
|
+
None,
|
|
50
|
+
"--target",
|
|
51
|
+
"-t",
|
|
52
|
+
help="Specific file to update (default: auto-detect from config or standard files)",
|
|
53
|
+
),
|
|
54
|
+
check: bool = typer.Option(False, "--check", help="Dry run check mode"),
|
|
50
55
|
):
|
|
51
56
|
"""
|
|
52
57
|
Synchronize Agent Environment (System Prompts & Skills).
|
|
53
58
|
Aggregates prompts from all active features and injects them into the agent configuration files.
|
|
54
59
|
"""
|
|
55
|
-
root = Path.cwd()
|
|
56
|
-
|
|
60
|
+
root = Path.cwd() # TODO: Use workspace root detection properly if needed
|
|
61
|
+
|
|
57
62
|
# 0. Load Config
|
|
58
63
|
config = get_config(str(root))
|
|
59
|
-
|
|
64
|
+
|
|
60
65
|
# 1. Register Features
|
|
61
66
|
registry = FeatureRegistry()
|
|
62
67
|
registry.load_defaults()
|
|
63
|
-
|
|
68
|
+
|
|
64
69
|
# 2. Collect Data
|
|
65
70
|
collected_prompts = {}
|
|
66
|
-
|
|
71
|
+
|
|
67
72
|
# Filter features based on config if specified
|
|
68
73
|
all_features = registry.get_features()
|
|
69
74
|
active_features = []
|
|
70
|
-
|
|
75
|
+
|
|
71
76
|
if config.agent.includes:
|
|
72
77
|
for f in all_features:
|
|
73
78
|
if f.name in config.agent.includes:
|
|
@@ -84,52 +89,64 @@ def sync_command(
|
|
|
84
89
|
if data.system_prompts:
|
|
85
90
|
collected_prompts.update(data.system_prompts)
|
|
86
91
|
except Exception as e:
|
|
87
|
-
console.print(
|
|
88
|
-
|
|
89
|
-
|
|
92
|
+
console.print(
|
|
93
|
+
f"[red]Error integrating feature {feature.name}: {e}[/red]"
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
console.print(
|
|
97
|
+
f"[blue]Collected {len(collected_prompts)} prompts from {len(active_features)} features.[/blue]"
|
|
98
|
+
)
|
|
90
99
|
|
|
91
100
|
# 3. Distribute Skills
|
|
92
|
-
console.print(
|
|
93
|
-
|
|
101
|
+
console.print("[bold blue]Distributing skills to agent frameworks...[/bold blue]")
|
|
102
|
+
|
|
94
103
|
# Determine language from config
|
|
95
|
-
skill_lang = config.i18n.source_lang if config.i18n.source_lang else
|
|
104
|
+
skill_lang = config.i18n.source_lang if config.i18n.source_lang else "en"
|
|
96
105
|
console.print(f"[dim] Using language: {skill_lang}[/dim]")
|
|
97
|
-
|
|
106
|
+
|
|
98
107
|
# Initialize SkillManager with active features
|
|
99
108
|
skill_manager = SkillManager(root, active_features)
|
|
100
|
-
|
|
109
|
+
|
|
101
110
|
# Get active integrations
|
|
102
111
|
integrations = get_active_integrations(
|
|
103
|
-
root,
|
|
104
|
-
config_overrides=config.agent.integrations,
|
|
105
|
-
auto_detect=True
|
|
112
|
+
root, config_overrides=config.agent.integrations, auto_detect=True
|
|
106
113
|
)
|
|
107
|
-
|
|
114
|
+
|
|
108
115
|
if integrations:
|
|
109
116
|
for framework_key, integration in integrations.items():
|
|
110
117
|
skill_target_dir = root / integration.skill_root_dir
|
|
111
|
-
console.print(
|
|
112
|
-
|
|
118
|
+
console.print(
|
|
119
|
+
f"[dim] Distributing to {integration.name} ({skill_target_dir})...[/dim]"
|
|
120
|
+
)
|
|
121
|
+
|
|
113
122
|
try:
|
|
114
123
|
# Distribute only the configured language version
|
|
115
|
-
results = skill_manager.distribute(
|
|
124
|
+
results = skill_manager.distribute(
|
|
125
|
+
skill_target_dir, lang=skill_lang, force=False
|
|
126
|
+
)
|
|
116
127
|
success_count = sum(1 for v in results.values() if v)
|
|
117
|
-
console.print(
|
|
128
|
+
console.print(
|
|
129
|
+
f"[green] ✓ Distributed {success_count}/{len(results)} skills to {integration.name}[/green]"
|
|
130
|
+
)
|
|
118
131
|
except Exception as e:
|
|
119
|
-
console.print(
|
|
132
|
+
console.print(
|
|
133
|
+
f"[red] Failed to distribute skills to {integration.name}: {e}[/red]"
|
|
134
|
+
)
|
|
120
135
|
else:
|
|
121
|
-
console.print(
|
|
136
|
+
console.print(
|
|
137
|
+
"[yellow]No agent frameworks detected. Skipping skill distribution.[/yellow]"
|
|
138
|
+
)
|
|
122
139
|
|
|
123
140
|
# 4. Determine Targets
|
|
124
141
|
targets = _get_targets(root, config, target)
|
|
125
|
-
|
|
142
|
+
|
|
126
143
|
# Ensure targets exist for sync
|
|
127
144
|
final_targets = []
|
|
128
145
|
for t in targets:
|
|
129
146
|
if not t.exists():
|
|
130
147
|
# If explicit target, fail? Or create?
|
|
131
148
|
# If default, create.
|
|
132
|
-
if target:
|
|
149
|
+
if target:
|
|
133
150
|
# CLI target
|
|
134
151
|
console.print(f"[yellow]Creating {t.name}...[/yellow]")
|
|
135
152
|
try:
|
|
@@ -152,9 +169,9 @@ def sync_command(
|
|
|
152
169
|
# 5. Inject System Prompts
|
|
153
170
|
for t in final_targets:
|
|
154
171
|
injector = PromptInjector(t)
|
|
155
|
-
|
|
172
|
+
|
|
156
173
|
if check:
|
|
157
|
-
|
|
174
|
+
console.print(f"[dim][Dry Run] Would check/update {t.name}[/dim]")
|
|
158
175
|
else:
|
|
159
176
|
try:
|
|
160
177
|
changed = injector.inject(collected_prompts)
|
|
@@ -165,60 +182,72 @@ def sync_command(
|
|
|
165
182
|
except Exception as e:
|
|
166
183
|
console.print(f"[red]Failed to update {t.name}: {e}[/red]")
|
|
167
184
|
|
|
185
|
+
|
|
168
186
|
def uninstall_command(
|
|
169
187
|
ctx: typer.Context,
|
|
170
|
-
target: Optional[Path] = typer.Option(
|
|
188
|
+
target: Optional[Path] = typer.Option(
|
|
189
|
+
None,
|
|
190
|
+
"--target",
|
|
191
|
+
"-t",
|
|
192
|
+
help="Specific file to clean (default: auto-detect from config or standard files)",
|
|
193
|
+
),
|
|
171
194
|
):
|
|
172
195
|
"""
|
|
173
196
|
Remove Monoco Managed Block from Agent Environment files and clean up distributed skills.
|
|
174
197
|
"""
|
|
175
198
|
root = Path.cwd()
|
|
176
199
|
config = get_config(str(root))
|
|
177
|
-
|
|
200
|
+
|
|
178
201
|
# 1. Clean up System Prompts
|
|
179
202
|
targets = _get_targets(root, config, target)
|
|
180
|
-
|
|
203
|
+
|
|
181
204
|
for t in targets:
|
|
182
205
|
if not t.exists():
|
|
183
206
|
if target:
|
|
184
|
-
|
|
207
|
+
console.print(f"[yellow]Target {t} does not exist.[/yellow]")
|
|
185
208
|
continue
|
|
186
|
-
|
|
209
|
+
|
|
187
210
|
injector = PromptInjector(t)
|
|
188
211
|
try:
|
|
189
212
|
changed = injector.remove()
|
|
190
213
|
if changed:
|
|
191
|
-
console.print(
|
|
214
|
+
console.print(
|
|
215
|
+
f"[green]✓ Removed Monoco Managed Block from {t.name}[/green]"
|
|
216
|
+
)
|
|
192
217
|
else:
|
|
193
218
|
console.print(f"[dim]= No Monoco Block found in {t.name}[/dim]")
|
|
194
219
|
except Exception as e:
|
|
195
220
|
console.print(f"[red]Failed to uninstall from {t.name}: {e}[/red]")
|
|
196
|
-
|
|
221
|
+
|
|
197
222
|
# 2. Clean up Skills
|
|
198
|
-
console.print(
|
|
199
|
-
|
|
223
|
+
console.print("[bold blue]Cleaning up distributed skills...[/bold blue]")
|
|
224
|
+
|
|
200
225
|
# Load features to get skill list
|
|
201
226
|
registry = FeatureRegistry()
|
|
202
227
|
registry.load_defaults()
|
|
203
228
|
active_features = registry.get_features()
|
|
204
|
-
|
|
229
|
+
|
|
205
230
|
skill_manager = SkillManager(root, active_features)
|
|
206
|
-
|
|
231
|
+
|
|
207
232
|
# Get active integrations
|
|
208
233
|
integrations = get_active_integrations(
|
|
209
|
-
root,
|
|
210
|
-
config_overrides=config.agent.integrations,
|
|
211
|
-
auto_detect=True
|
|
234
|
+
root, config_overrides=config.agent.integrations, auto_detect=True
|
|
212
235
|
)
|
|
213
|
-
|
|
236
|
+
|
|
214
237
|
if integrations:
|
|
215
238
|
for framework_key, integration in integrations.items():
|
|
216
239
|
skill_target_dir = root / integration.skill_root_dir
|
|
217
|
-
console.print(
|
|
218
|
-
|
|
240
|
+
console.print(
|
|
241
|
+
f"[dim] Cleaning {integration.name} ({skill_target_dir})...[/dim]"
|
|
242
|
+
)
|
|
243
|
+
|
|
219
244
|
try:
|
|
220
245
|
skill_manager.cleanup(skill_target_dir)
|
|
221
246
|
except Exception as e:
|
|
222
|
-
console.print(
|
|
247
|
+
console.print(
|
|
248
|
+
f"[red] Failed to clean skills from {integration.name}: {e}[/red]"
|
|
249
|
+
)
|
|
223
250
|
else:
|
|
224
|
-
console.print(
|
|
251
|
+
console.print(
|
|
252
|
+
"[yellow]No agent frameworks detected. Skipping skill cleanup.[/yellow]"
|
|
253
|
+
)
|
monoco/core/telemetry.py
CHANGED
|
@@ -7,14 +7,15 @@ from pathlib import Path
|
|
|
7
7
|
from typing import Optional, Dict, Any
|
|
8
8
|
from monoco.core.config import get_config
|
|
9
9
|
|
|
10
|
-
POSTHOG_API_KEY = "phc_MndV8H8v0W3P7Yv1P7Z8X7X7X7X7X7X7X7X7"
|
|
10
|
+
POSTHOG_API_KEY = "phc_MndV8H8v0W3P7Yv1P7Z8X7X7X7X7X7X7X7X7"
|
|
11
11
|
POSTHOG_HOST = "https://app.posthog.com"
|
|
12
12
|
|
|
13
|
+
|
|
13
14
|
class Telemetry:
|
|
14
15
|
def __init__(self):
|
|
15
16
|
self.config = get_config()
|
|
16
17
|
self._device_id = self._get_or_create_device_id()
|
|
17
|
-
|
|
18
|
+
|
|
18
19
|
def _get_or_create_device_id(self) -> str:
|
|
19
20
|
state_path = Path.home() / ".monoco" / "state.json"
|
|
20
21
|
if state_path.exists():
|
|
@@ -25,7 +26,7 @@ class Telemetry:
|
|
|
25
26
|
return state["device_id"]
|
|
26
27
|
except Exception:
|
|
27
28
|
pass
|
|
28
|
-
|
|
29
|
+
|
|
29
30
|
device_id = str(uuid.uuid4())
|
|
30
31
|
state_path.parent.mkdir(parents=True, exist_ok=True)
|
|
31
32
|
try:
|
|
@@ -44,7 +45,7 @@ class Telemetry:
|
|
|
44
45
|
# Notify user on first use if not configured
|
|
45
46
|
if self.config.telemetry.enabled is None:
|
|
46
47
|
# We don't want to spam, but we must be transparent
|
|
47
|
-
# This is a one-time notice in a session via a class-level flag?
|
|
48
|
+
# This is a one-time notice in a session via a class-level flag?
|
|
48
49
|
# Or just rely on the fact that 'init' will fix it.
|
|
49
50
|
# To be safe and minimal, we'll just skip capture if not explicitly enabled
|
|
50
51
|
return
|
|
@@ -68,20 +69,23 @@ class Telemetry:
|
|
|
68
69
|
"api_key": POSTHOG_API_KEY,
|
|
69
70
|
"event": namespaced_event,
|
|
70
71
|
"properties": props,
|
|
71
|
-
"timestamp": time.strftime("%Y-%m-%dT%H:%M:%S%z")
|
|
72
|
+
"timestamp": time.strftime("%Y-%m-%dT%H:%M:%S%z"),
|
|
72
73
|
}
|
|
73
74
|
|
|
74
75
|
# Send asynchronously? For now, we'll do a simple non-blocking-ish call
|
|
75
76
|
try:
|
|
76
77
|
import httpx
|
|
78
|
+
|
|
77
79
|
httpx.post(f"{POSTHOG_HOST}/capture/", json=data, timeout=1.0)
|
|
78
80
|
except ImportError:
|
|
79
|
-
pass
|
|
81
|
+
pass # Telemetry is optional
|
|
80
82
|
except Exception:
|
|
81
83
|
pass
|
|
82
84
|
|
|
85
|
+
|
|
83
86
|
_instance = None
|
|
84
87
|
|
|
88
|
+
|
|
85
89
|
def capture_event(event: str, properties: Optional[Dict[str, Any]] = None):
|
|
86
90
|
global _instance
|
|
87
91
|
if _instance is None:
|
monoco/core/workspace.py
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import os
|
|
2
2
|
from pathlib import Path
|
|
3
|
-
from typing import List, Optional
|
|
4
|
-
from pydantic import BaseModel,
|
|
3
|
+
from typing import List, Optional
|
|
4
|
+
from pydantic import BaseModel, ConfigDict
|
|
5
5
|
|
|
6
6
|
from monoco.core.config import get_config, MonocoConfig
|
|
7
7
|
|
|
8
|
+
|
|
8
9
|
class MonocoProject(BaseModel):
|
|
9
10
|
"""
|
|
10
11
|
Representation of a single Monoco project.
|
|
11
12
|
"""
|
|
13
|
+
|
|
12
14
|
id: str # Unique ID within the workspace (usually the directory name)
|
|
13
15
|
name: str
|
|
14
16
|
path: Path
|
|
@@ -23,6 +25,7 @@ class MonocoProject(BaseModel):
|
|
|
23
25
|
|
|
24
26
|
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
25
27
|
|
|
28
|
+
|
|
26
29
|
def is_project_root(path: Path) -> bool:
|
|
27
30
|
"""
|
|
28
31
|
Check if a directory serves as a Monoco project root.
|
|
@@ -31,66 +34,70 @@ def is_project_root(path: Path) -> bool:
|
|
|
31
34
|
"""
|
|
32
35
|
if not path.is_dir():
|
|
33
36
|
return False
|
|
34
|
-
|
|
37
|
+
|
|
35
38
|
return (path / ".monoco").is_dir()
|
|
36
39
|
|
|
40
|
+
|
|
37
41
|
def load_project(path: Path) -> Optional[MonocoProject]:
|
|
38
42
|
"""Load a project from a path if it is a valid project root."""
|
|
39
43
|
if not is_project_root(path):
|
|
40
44
|
return None
|
|
41
|
-
|
|
45
|
+
|
|
42
46
|
try:
|
|
43
47
|
config = get_config(str(path))
|
|
44
48
|
# If name is default, use directory name
|
|
45
49
|
name = config.project.name
|
|
46
50
|
if name == "Monoco Project":
|
|
47
51
|
name = path.name
|
|
48
|
-
|
|
49
|
-
return MonocoProject(
|
|
50
|
-
id=path.name,
|
|
51
|
-
name=name,
|
|
52
|
-
path=path,
|
|
53
|
-
config=config
|
|
54
|
-
)
|
|
52
|
+
|
|
53
|
+
return MonocoProject(id=path.name, name=name, path=path, config=config)
|
|
55
54
|
except Exception:
|
|
56
55
|
return None
|
|
57
56
|
|
|
57
|
+
|
|
58
58
|
def find_projects(workspace_root: Path) -> List[MonocoProject]:
|
|
59
59
|
"""
|
|
60
60
|
Scan for projects in a workspace.
|
|
61
61
|
Returns list of MonocoProject instances.
|
|
62
62
|
"""
|
|
63
63
|
projects = []
|
|
64
|
-
|
|
64
|
+
|
|
65
65
|
# 1. Check workspace root itself
|
|
66
66
|
root_project = load_project(workspace_root)
|
|
67
67
|
if root_project:
|
|
68
68
|
projects.append(root_project)
|
|
69
|
-
|
|
69
|
+
|
|
70
70
|
# 2. Recursive Scan
|
|
71
71
|
for root, dirs, files in os.walk(workspace_root):
|
|
72
72
|
# Skip hidden directories and node_modules
|
|
73
|
-
dirs[:] = [
|
|
74
|
-
|
|
73
|
+
dirs[:] = [
|
|
74
|
+
d
|
|
75
|
+
for d in dirs
|
|
76
|
+
if not d.startswith(".") and d != "node_modules" and d != "venv"
|
|
77
|
+
]
|
|
78
|
+
|
|
75
79
|
for d in dirs:
|
|
76
80
|
project_path = Path(root) / d
|
|
77
81
|
# Avoid re-adding root if it was somehow added (unlikely here)
|
|
78
|
-
if project_path == workspace_root:
|
|
79
|
-
|
|
82
|
+
if project_path == workspace_root:
|
|
83
|
+
continue
|
|
84
|
+
|
|
80
85
|
if is_project_root(project_path):
|
|
81
86
|
p = load_project(project_path)
|
|
82
87
|
if p:
|
|
83
88
|
projects.append(p)
|
|
84
|
-
|
|
89
|
+
|
|
85
90
|
return projects
|
|
86
91
|
|
|
92
|
+
|
|
87
93
|
class Workspace(BaseModel):
|
|
88
94
|
"""
|
|
89
95
|
Standardized Workspace primitive.
|
|
90
96
|
"""
|
|
97
|
+
|
|
91
98
|
root: Path
|
|
92
99
|
projects: List[MonocoProject] = []
|
|
93
|
-
|
|
100
|
+
|
|
94
101
|
@classmethod
|
|
95
102
|
def discover(cls, root: Path) -> "Workspace":
|
|
96
103
|
projects = find_projects(root)
|