emdash-cli 0.1.46__py3-none-any.whl → 0.1.70__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.
- emdash_cli/client.py +12 -28
- emdash_cli/commands/__init__.py +2 -2
- emdash_cli/commands/agent/constants.py +78 -0
- emdash_cli/commands/agent/handlers/__init__.py +10 -0
- emdash_cli/commands/agent/handlers/agents.py +67 -39
- emdash_cli/commands/agent/handlers/index.py +183 -0
- emdash_cli/commands/agent/handlers/misc.py +119 -0
- emdash_cli/commands/agent/handlers/registry.py +72 -0
- emdash_cli/commands/agent/handlers/rules.py +48 -31
- emdash_cli/commands/agent/handlers/sessions.py +1 -1
- emdash_cli/commands/agent/handlers/setup.py +187 -54
- emdash_cli/commands/agent/handlers/skills.py +42 -4
- emdash_cli/commands/agent/handlers/telegram.py +523 -0
- emdash_cli/commands/agent/handlers/todos.py +55 -34
- emdash_cli/commands/agent/handlers/verify.py +10 -5
- emdash_cli/commands/agent/help.py +236 -0
- emdash_cli/commands/agent/interactive.py +278 -47
- emdash_cli/commands/agent/menus.py +116 -84
- emdash_cli/commands/agent/onboarding.py +619 -0
- emdash_cli/commands/agent/session_restore.py +210 -0
- emdash_cli/commands/index.py +111 -13
- emdash_cli/commands/registry.py +635 -0
- emdash_cli/commands/skills.py +72 -6
- emdash_cli/design.py +328 -0
- emdash_cli/diff_renderer.py +438 -0
- emdash_cli/integrations/__init__.py +1 -0
- emdash_cli/integrations/telegram/__init__.py +15 -0
- emdash_cli/integrations/telegram/bot.py +402 -0
- emdash_cli/integrations/telegram/bridge.py +980 -0
- emdash_cli/integrations/telegram/config.py +155 -0
- emdash_cli/integrations/telegram/formatter.py +392 -0
- emdash_cli/main.py +52 -2
- emdash_cli/sse_renderer.py +632 -171
- {emdash_cli-0.1.46.dist-info → emdash_cli-0.1.70.dist-info}/METADATA +2 -2
- emdash_cli-0.1.70.dist-info/RECORD +63 -0
- emdash_cli/commands/swarm.py +0 -86
- emdash_cli-0.1.46.dist-info/RECORD +0 -49
- {emdash_cli-0.1.46.dist-info → emdash_cli-0.1.70.dist-info}/WHEEL +0 -0
- {emdash_cli-0.1.46.dist-info → emdash_cli-0.1.70.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,635 @@
|
|
|
1
|
+
"""Registry CLI commands for browsing and installing community components."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import shutil
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Literal
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
import httpx
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
|
|
12
|
+
from ..design import (
|
|
13
|
+
Colors,
|
|
14
|
+
header,
|
|
15
|
+
footer,
|
|
16
|
+
SEPARATOR_WIDTH,
|
|
17
|
+
STATUS_ACTIVE,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
console = Console()
|
|
21
|
+
|
|
22
|
+
# GitHub raw URL base for the registry
|
|
23
|
+
GITHUB_REPO = "mendyEdri/emdash-registry"
|
|
24
|
+
GITHUB_BRANCH = "main"
|
|
25
|
+
REGISTRY_BASE_URL = f"https://raw.githubusercontent.com/{GITHUB_REPO}/{GITHUB_BRANCH}"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
ComponentType = Literal["skill", "rule", "agent", "verifier"]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _fetch_registry() -> dict | None:
|
|
32
|
+
"""Fetch the registry.json from GitHub."""
|
|
33
|
+
url = f"{REGISTRY_BASE_URL}/registry.json"
|
|
34
|
+
try:
|
|
35
|
+
response = httpx.get(url, timeout=10)
|
|
36
|
+
response.raise_for_status()
|
|
37
|
+
return response.json()
|
|
38
|
+
except Exception as e:
|
|
39
|
+
console.print(f" [{Colors.ERROR}]error:[/{Colors.ERROR}] Failed to fetch registry: {e}")
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _fetch_component(path: str) -> str | None:
|
|
44
|
+
"""Fetch a component file from GitHub."""
|
|
45
|
+
url = f"{REGISTRY_BASE_URL}/{path}"
|
|
46
|
+
try:
|
|
47
|
+
response = httpx.get(url, timeout=10)
|
|
48
|
+
response.raise_for_status()
|
|
49
|
+
return response.text
|
|
50
|
+
except Exception as e:
|
|
51
|
+
console.print(f" [{Colors.ERROR}]error:[/{Colors.ERROR}] Failed to fetch component: {e}")
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _get_emdash_dir() -> Path:
|
|
56
|
+
"""Get the .emdash directory."""
|
|
57
|
+
return Path.cwd() / ".emdash"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _install_skill(name: str, content: str) -> bool:
|
|
61
|
+
"""Install a skill to .emdash/skills/."""
|
|
62
|
+
skill_dir = _get_emdash_dir() / "skills" / name
|
|
63
|
+
skill_dir.mkdir(parents=True, exist_ok=True)
|
|
64
|
+
skill_file = skill_dir / "SKILL.md"
|
|
65
|
+
skill_file.write_text(content)
|
|
66
|
+
return True
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _install_rule(name: str, content: str) -> bool:
|
|
70
|
+
"""Install a rule to .emdash/rules/."""
|
|
71
|
+
rules_dir = _get_emdash_dir() / "rules"
|
|
72
|
+
rules_dir.mkdir(parents=True, exist_ok=True)
|
|
73
|
+
rule_file = rules_dir / f"{name}.md"
|
|
74
|
+
rule_file.write_text(content)
|
|
75
|
+
return True
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _install_agent(name: str, content: str) -> bool:
|
|
79
|
+
"""Install an agent to .emdash/agents/."""
|
|
80
|
+
agents_dir = _get_emdash_dir() / "agents"
|
|
81
|
+
agents_dir.mkdir(parents=True, exist_ok=True)
|
|
82
|
+
agent_file = agents_dir / f"{name}.md"
|
|
83
|
+
agent_file.write_text(content)
|
|
84
|
+
return True
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _install_verifier(name: str, content: str) -> bool:
|
|
88
|
+
"""Install a verifier to .emdash/verifiers.json."""
|
|
89
|
+
verifiers_file = _get_emdash_dir() / "verifiers.json"
|
|
90
|
+
|
|
91
|
+
# Load or create verifiers config
|
|
92
|
+
if verifiers_file.exists():
|
|
93
|
+
existing = json.loads(verifiers_file.read_text())
|
|
94
|
+
else:
|
|
95
|
+
_get_emdash_dir().mkdir(parents=True, exist_ok=True)
|
|
96
|
+
existing = {"verifiers": [], "max_attempts": 3}
|
|
97
|
+
|
|
98
|
+
# Parse new verifier
|
|
99
|
+
new_verifier = json.loads(content)
|
|
100
|
+
|
|
101
|
+
# Check if already exists
|
|
102
|
+
existing_names = [v.get("name") for v in existing.get("verifiers", [])]
|
|
103
|
+
if name in existing_names:
|
|
104
|
+
# Update existing
|
|
105
|
+
existing["verifiers"] = [
|
|
106
|
+
new_verifier if v.get("name") == name else v
|
|
107
|
+
for v in existing["verifiers"]
|
|
108
|
+
]
|
|
109
|
+
else:
|
|
110
|
+
existing["verifiers"].append(new_verifier)
|
|
111
|
+
|
|
112
|
+
verifiers_file.write_text(json.dumps(existing, indent=2))
|
|
113
|
+
return True
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@click.group(invoke_without_command=True)
|
|
117
|
+
@click.pass_context
|
|
118
|
+
def registry(ctx):
|
|
119
|
+
"""Browse and install community skills, rules, agents, and verifiers.
|
|
120
|
+
|
|
121
|
+
Run without arguments to open the interactive wizard.
|
|
122
|
+
"""
|
|
123
|
+
if ctx.invoked_subcommand is None:
|
|
124
|
+
# Interactive wizard mode
|
|
125
|
+
_show_registry_wizard()
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@registry.command("list")
|
|
129
|
+
@click.argument("component_type", required=False,
|
|
130
|
+
type=click.Choice(["skills", "rules", "agents", "verifiers"]))
|
|
131
|
+
def registry_list(component_type: str | None):
|
|
132
|
+
"""List available components from the registry."""
|
|
133
|
+
reg = _fetch_registry()
|
|
134
|
+
if not reg:
|
|
135
|
+
return
|
|
136
|
+
|
|
137
|
+
types_to_show = [component_type] if component_type else ["skills", "rules", "agents", "verifiers"]
|
|
138
|
+
|
|
139
|
+
for ctype in types_to_show:
|
|
140
|
+
components = reg.get(ctype, {})
|
|
141
|
+
if not components:
|
|
142
|
+
continue
|
|
143
|
+
|
|
144
|
+
console.print()
|
|
145
|
+
console.print(f"[{Colors.MUTED}]{header(ctype.title(), SEPARATOR_WIDTH)}[/{Colors.MUTED}]")
|
|
146
|
+
console.print()
|
|
147
|
+
|
|
148
|
+
for name, info in components.items():
|
|
149
|
+
tags = ", ".join(info.get("tags", []))
|
|
150
|
+
desc = info.get("description", "")
|
|
151
|
+
console.print(f" [{Colors.PRIMARY}]{name}[/{Colors.PRIMARY}]")
|
|
152
|
+
if desc:
|
|
153
|
+
console.print(f" [{Colors.MUTED}]{desc}[/{Colors.MUTED}]")
|
|
154
|
+
if tags:
|
|
155
|
+
console.print(f" [{Colors.DIM}]{tags}[/{Colors.DIM}]")
|
|
156
|
+
|
|
157
|
+
console.print()
|
|
158
|
+
console.print(f"[{Colors.MUTED}]{footer(SEPARATOR_WIDTH)}[/{Colors.MUTED}]")
|
|
159
|
+
console.print()
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@registry.command("show")
|
|
163
|
+
@click.argument("component_id")
|
|
164
|
+
def registry_show(component_id: str):
|
|
165
|
+
"""Show details of a component.
|
|
166
|
+
|
|
167
|
+
COMPONENT_ID format: type:name (e.g., skill:frontend-design)
|
|
168
|
+
"""
|
|
169
|
+
if ":" not in component_id:
|
|
170
|
+
console.print(f" [{Colors.ERROR}]error:[/{Colors.ERROR}] Invalid format. Use type:name (e.g., skill:frontend-design)")
|
|
171
|
+
return
|
|
172
|
+
|
|
173
|
+
ctype, name = component_id.split(":", 1)
|
|
174
|
+
type_plural = ctype + "s" if not ctype.endswith("s") else ctype
|
|
175
|
+
|
|
176
|
+
reg = _fetch_registry()
|
|
177
|
+
if not reg:
|
|
178
|
+
return
|
|
179
|
+
|
|
180
|
+
components = reg.get(type_plural, {})
|
|
181
|
+
if name not in components:
|
|
182
|
+
console.print(f" [{Colors.WARNING}]{ctype.title()} '{name}' not found in registry.[/{Colors.WARNING}]")
|
|
183
|
+
return
|
|
184
|
+
|
|
185
|
+
info = components[name]
|
|
186
|
+
|
|
187
|
+
# Fetch the content
|
|
188
|
+
content = _fetch_component(info["path"])
|
|
189
|
+
if not content:
|
|
190
|
+
return
|
|
191
|
+
|
|
192
|
+
console.print()
|
|
193
|
+
console.print(f"[{Colors.MUTED}]{header(f'{ctype}:{name}', SEPARATOR_WIDTH)}[/{Colors.MUTED}]")
|
|
194
|
+
console.print()
|
|
195
|
+
|
|
196
|
+
if info.get('description'):
|
|
197
|
+
console.print(f" [{Colors.DIM}]desc[/{Colors.DIM}] {info.get('description', '')}")
|
|
198
|
+
if info.get('tags'):
|
|
199
|
+
console.print(f" [{Colors.DIM}]tags[/{Colors.DIM}] {', '.join(info.get('tags', []))}")
|
|
200
|
+
if info.get('path'):
|
|
201
|
+
console.print(f" [{Colors.DIM}]path[/{Colors.DIM}] {info.get('path', '')}")
|
|
202
|
+
|
|
203
|
+
console.print()
|
|
204
|
+
console.print(f" [{Colors.DIM}]content:[/{Colors.DIM}]")
|
|
205
|
+
console.print()
|
|
206
|
+
|
|
207
|
+
# Show content with indentation
|
|
208
|
+
for line in content.split('\n')[:30]: # Limit preview lines
|
|
209
|
+
if line.startswith('#'):
|
|
210
|
+
console.print(f" [{Colors.PRIMARY}]{line}[/{Colors.PRIMARY}]")
|
|
211
|
+
else:
|
|
212
|
+
console.print(f" [{Colors.MUTED}]{line}[/{Colors.MUTED}]")
|
|
213
|
+
|
|
214
|
+
if len(content.split('\n')) > 30:
|
|
215
|
+
console.print(f" [{Colors.DIM}]... ({len(content.split(chr(10))) - 30} more lines)[/{Colors.DIM}]")
|
|
216
|
+
|
|
217
|
+
console.print()
|
|
218
|
+
console.print(f"[{Colors.MUTED}]{footer(SEPARATOR_WIDTH)}[/{Colors.MUTED}]")
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
@registry.command("install")
|
|
222
|
+
@click.argument("component_ids", nargs=-1)
|
|
223
|
+
def registry_install(component_ids: tuple[str, ...]):
|
|
224
|
+
"""Install components from the registry.
|
|
225
|
+
|
|
226
|
+
COMPONENT_IDS format: type:name (e.g., skill:frontend-design rule:typescript)
|
|
227
|
+
"""
|
|
228
|
+
if not component_ids:
|
|
229
|
+
console.print()
|
|
230
|
+
console.print(f" [{Colors.WARNING}]usage:[/{Colors.WARNING}] emdash registry install type:name")
|
|
231
|
+
console.print(f" [{Colors.DIM}]example: emdash registry install skill:frontend-design rule:typescript[/{Colors.DIM}]")
|
|
232
|
+
console.print()
|
|
233
|
+
return
|
|
234
|
+
|
|
235
|
+
reg = _fetch_registry()
|
|
236
|
+
if not reg:
|
|
237
|
+
return
|
|
238
|
+
|
|
239
|
+
console.print()
|
|
240
|
+
for component_id in component_ids:
|
|
241
|
+
if ":" not in component_id:
|
|
242
|
+
console.print(f" [{Colors.ERROR}]error:[/{Colors.ERROR}] Invalid format: {component_id}. Use type:name")
|
|
243
|
+
continue
|
|
244
|
+
|
|
245
|
+
ctype, name = component_id.split(":", 1)
|
|
246
|
+
type_plural = ctype + "s" if not ctype.endswith("s") else ctype
|
|
247
|
+
|
|
248
|
+
components = reg.get(type_plural, {})
|
|
249
|
+
if name not in components:
|
|
250
|
+
console.print(f" [{Colors.WARNING}]not found:[/{Colors.WARNING}] {ctype}:{name}")
|
|
251
|
+
continue
|
|
252
|
+
|
|
253
|
+
info = components[name]
|
|
254
|
+
content = _fetch_component(info["path"])
|
|
255
|
+
if not content:
|
|
256
|
+
continue
|
|
257
|
+
|
|
258
|
+
# Install based on type
|
|
259
|
+
installers = {
|
|
260
|
+
"skill": _install_skill,
|
|
261
|
+
"rule": _install_rule,
|
|
262
|
+
"agent": _install_agent,
|
|
263
|
+
"verifier": _install_verifier,
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
installer = installers.get(ctype)
|
|
267
|
+
if not installer:
|
|
268
|
+
console.print(f" [{Colors.ERROR}]error:[/{Colors.ERROR}] Unknown component type: {ctype}")
|
|
269
|
+
continue
|
|
270
|
+
|
|
271
|
+
try:
|
|
272
|
+
installer(name, content)
|
|
273
|
+
console.print(f" [{Colors.SUCCESS}]{STATUS_ACTIVE}[/{Colors.SUCCESS}] [{Colors.MUTED}]installed:[/{Colors.MUTED}] {ctype}:{name}")
|
|
274
|
+
except Exception as e:
|
|
275
|
+
console.print(f" [{Colors.ERROR}]error:[/{Colors.ERROR}] {ctype}:{name} - {e}")
|
|
276
|
+
console.print()
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
@registry.command("search")
|
|
280
|
+
@click.argument("query")
|
|
281
|
+
@click.option("--tag", "-t", multiple=True, help="Filter by tag")
|
|
282
|
+
def registry_search(query: str, tag: tuple[str, ...]):
|
|
283
|
+
"""Search the registry by name or description."""
|
|
284
|
+
reg = _fetch_registry()
|
|
285
|
+
if not reg:
|
|
286
|
+
return
|
|
287
|
+
|
|
288
|
+
query_lower = query.lower()
|
|
289
|
+
tags_lower = [t.lower() for t in tag]
|
|
290
|
+
|
|
291
|
+
results = []
|
|
292
|
+
|
|
293
|
+
for ctype in ["skills", "rules", "agents", "verifiers"]:
|
|
294
|
+
components = reg.get(ctype, {})
|
|
295
|
+
for name, info in components.items():
|
|
296
|
+
# Match query
|
|
297
|
+
matches_query = (
|
|
298
|
+
query_lower in name.lower() or
|
|
299
|
+
query_lower in info.get("description", "").lower()
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
# Match tags
|
|
303
|
+
component_tags = [t.lower() for t in info.get("tags", [])]
|
|
304
|
+
matches_tags = not tags_lower or any(t in component_tags for t in tags_lower)
|
|
305
|
+
|
|
306
|
+
if matches_query and matches_tags:
|
|
307
|
+
results.append((ctype[:-1], name, info)) # Remove 's' from type
|
|
308
|
+
|
|
309
|
+
console.print()
|
|
310
|
+
if not results:
|
|
311
|
+
console.print(f" [{Colors.DIM}]no results for '{query}'[/{Colors.DIM}]")
|
|
312
|
+
console.print()
|
|
313
|
+
return
|
|
314
|
+
|
|
315
|
+
console.print(f"[{Colors.MUTED}]{header(f'Search: {query}', SEPARATOR_WIDTH)}[/{Colors.MUTED}]")
|
|
316
|
+
console.print()
|
|
317
|
+
|
|
318
|
+
for ctype, name, info in results:
|
|
319
|
+
tags = ", ".join(info.get("tags", []))
|
|
320
|
+
desc = info.get("description", "")
|
|
321
|
+
console.print(f" [{Colors.PRIMARY}]{ctype}:{name}[/{Colors.PRIMARY}]")
|
|
322
|
+
if desc:
|
|
323
|
+
console.print(f" [{Colors.MUTED}]{desc}[/{Colors.MUTED}]")
|
|
324
|
+
if tags:
|
|
325
|
+
console.print(f" [{Colors.DIM}]{tags}[/{Colors.DIM}]")
|
|
326
|
+
|
|
327
|
+
console.print()
|
|
328
|
+
console.print(f"[{Colors.MUTED}]{footer(SEPARATOR_WIDTH)}[/{Colors.MUTED}]")
|
|
329
|
+
console.print(f" [{Colors.DIM}]{len(results)} result{'s' if len(results) != 1 else ''}[/{Colors.DIM}]")
|
|
330
|
+
console.print()
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def _show_registry_wizard():
|
|
334
|
+
"""Show interactive registry wizard."""
|
|
335
|
+
from prompt_toolkit import Application
|
|
336
|
+
from prompt_toolkit.key_binding import KeyBindings
|
|
337
|
+
from prompt_toolkit.layout import Layout, HSplit, Window, FormattedTextControl
|
|
338
|
+
from prompt_toolkit.styles import Style
|
|
339
|
+
|
|
340
|
+
console.print()
|
|
341
|
+
console.print(f"[{Colors.MUTED}]{header('Registry', SEPARATOR_WIDTH)}[/{Colors.MUTED}]")
|
|
342
|
+
console.print()
|
|
343
|
+
console.print(f" [{Colors.DIM}]browse and install community components[/{Colors.DIM}]")
|
|
344
|
+
console.print()
|
|
345
|
+
|
|
346
|
+
# Fetch registry
|
|
347
|
+
console.print(f" [{Colors.DIM}]fetching...[/{Colors.DIM}]", end="\r")
|
|
348
|
+
reg = _fetch_registry()
|
|
349
|
+
console.print(" ", end="\r") # Clear fetching message
|
|
350
|
+
|
|
351
|
+
if not reg:
|
|
352
|
+
return
|
|
353
|
+
|
|
354
|
+
# Build menu items
|
|
355
|
+
categories = [
|
|
356
|
+
("skills", "Skills", "specialized capabilities"),
|
|
357
|
+
("rules", "Rules", "coding standards"),
|
|
358
|
+
("agents", "Agents", "custom configurations"),
|
|
359
|
+
("verifiers", "Verifiers", "verification configs"),
|
|
360
|
+
]
|
|
361
|
+
|
|
362
|
+
selected_category = [0]
|
|
363
|
+
result = [None]
|
|
364
|
+
|
|
365
|
+
kb = KeyBindings()
|
|
366
|
+
|
|
367
|
+
@kb.add("up")
|
|
368
|
+
@kb.add("k")
|
|
369
|
+
def move_up(event):
|
|
370
|
+
selected_category[0] = (selected_category[0] - 1) % len(categories)
|
|
371
|
+
|
|
372
|
+
@kb.add("down")
|
|
373
|
+
@kb.add("j")
|
|
374
|
+
def move_down(event):
|
|
375
|
+
selected_category[0] = (selected_category[0] + 1) % len(categories)
|
|
376
|
+
|
|
377
|
+
@kb.add("enter")
|
|
378
|
+
def select(event):
|
|
379
|
+
result[0] = categories[selected_category[0]][0]
|
|
380
|
+
event.app.exit()
|
|
381
|
+
|
|
382
|
+
@kb.add("1")
|
|
383
|
+
def select_1(event):
|
|
384
|
+
result[0] = "skills"
|
|
385
|
+
event.app.exit()
|
|
386
|
+
|
|
387
|
+
@kb.add("2")
|
|
388
|
+
def select_2(event):
|
|
389
|
+
result[0] = "rules"
|
|
390
|
+
event.app.exit()
|
|
391
|
+
|
|
392
|
+
@kb.add("3")
|
|
393
|
+
def select_3(event):
|
|
394
|
+
result[0] = "agents"
|
|
395
|
+
event.app.exit()
|
|
396
|
+
|
|
397
|
+
@kb.add("4")
|
|
398
|
+
def select_4(event):
|
|
399
|
+
result[0] = "verifiers"
|
|
400
|
+
event.app.exit()
|
|
401
|
+
|
|
402
|
+
@kb.add("c-c")
|
|
403
|
+
@kb.add("escape")
|
|
404
|
+
@kb.add("q")
|
|
405
|
+
def cancel(event):
|
|
406
|
+
result[0] = None
|
|
407
|
+
event.app.exit()
|
|
408
|
+
|
|
409
|
+
def get_formatted_menu():
|
|
410
|
+
lines = [("class:title", f"─── Categories {'─' * 30}\n\n")]
|
|
411
|
+
|
|
412
|
+
for i, (key, name, desc) in enumerate(categories):
|
|
413
|
+
count = len(reg.get(key, {}))
|
|
414
|
+
is_selected = i == selected_category[0]
|
|
415
|
+
prefix = "▸ " if is_selected else " "
|
|
416
|
+
|
|
417
|
+
if is_selected:
|
|
418
|
+
lines.append(("class:selected", f" {prefix}{name}"))
|
|
419
|
+
lines.append(("class:count-selected", f" {count}"))
|
|
420
|
+
lines.append(("class:desc-selected", f" {desc}\n"))
|
|
421
|
+
else:
|
|
422
|
+
lines.append(("class:option", f" {prefix}{name}"))
|
|
423
|
+
lines.append(("class:count", f" {count}"))
|
|
424
|
+
lines.append(("class:desc", f" {desc}\n"))
|
|
425
|
+
|
|
426
|
+
lines.append(("class:hint", f"\n{'─' * 45}\n ↑↓ navigate Enter select 1-4 quick q quit"))
|
|
427
|
+
return lines
|
|
428
|
+
|
|
429
|
+
style = Style.from_dict({
|
|
430
|
+
"title": f"{Colors.MUTED}",
|
|
431
|
+
"selected": f"{Colors.SUCCESS} bold",
|
|
432
|
+
"count-selected": f"{Colors.SUCCESS}",
|
|
433
|
+
"desc-selected": f"{Colors.SUCCESS}",
|
|
434
|
+
"option": f"{Colors.PRIMARY}",
|
|
435
|
+
"count": f"{Colors.MUTED}",
|
|
436
|
+
"desc": f"{Colors.DIM}",
|
|
437
|
+
"hint": f"{Colors.DIM}",
|
|
438
|
+
})
|
|
439
|
+
|
|
440
|
+
layout = Layout(
|
|
441
|
+
HSplit([
|
|
442
|
+
Window(
|
|
443
|
+
FormattedTextControl(get_formatted_menu),
|
|
444
|
+
height=len(categories) + 5,
|
|
445
|
+
),
|
|
446
|
+
])
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
app = Application(
|
|
450
|
+
layout=layout,
|
|
451
|
+
key_bindings=kb,
|
|
452
|
+
style=style,
|
|
453
|
+
full_screen=False,
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
try:
|
|
457
|
+
app.run()
|
|
458
|
+
except (KeyboardInterrupt, EOFError):
|
|
459
|
+
return
|
|
460
|
+
|
|
461
|
+
console.print()
|
|
462
|
+
|
|
463
|
+
if result[0] is None:
|
|
464
|
+
return
|
|
465
|
+
|
|
466
|
+
# Show components in selected category
|
|
467
|
+
_show_component_picker(reg, result[0])
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def _show_component_picker(reg: dict, category: str):
|
|
471
|
+
"""Show interactive component picker for a category."""
|
|
472
|
+
from prompt_toolkit import Application
|
|
473
|
+
from prompt_toolkit.key_binding import KeyBindings
|
|
474
|
+
from prompt_toolkit.layout import Layout, HSplit, Window, FormattedTextControl
|
|
475
|
+
from prompt_toolkit.styles import Style
|
|
476
|
+
|
|
477
|
+
components = reg.get(category, {})
|
|
478
|
+
if not components:
|
|
479
|
+
console.print(f" [{Colors.WARNING}]No {category} available.[/{Colors.WARNING}]")
|
|
480
|
+
return
|
|
481
|
+
|
|
482
|
+
# Build items list
|
|
483
|
+
items = [(name, info) for name, info in components.items()]
|
|
484
|
+
|
|
485
|
+
selected_index = [0]
|
|
486
|
+
selected_items = set() # For multi-select
|
|
487
|
+
result = [None] # "install", "back", or None
|
|
488
|
+
|
|
489
|
+
kb = KeyBindings()
|
|
490
|
+
|
|
491
|
+
@kb.add("up")
|
|
492
|
+
@kb.add("k")
|
|
493
|
+
def move_up(event):
|
|
494
|
+
selected_index[0] = (selected_index[0] - 1) % len(items)
|
|
495
|
+
|
|
496
|
+
@kb.add("down")
|
|
497
|
+
@kb.add("j")
|
|
498
|
+
def move_down(event):
|
|
499
|
+
selected_index[0] = (selected_index[0] + 1) % len(items)
|
|
500
|
+
|
|
501
|
+
@kb.add("space")
|
|
502
|
+
def toggle_select(event):
|
|
503
|
+
name = items[selected_index[0]][0]
|
|
504
|
+
if name in selected_items:
|
|
505
|
+
selected_items.remove(name)
|
|
506
|
+
else:
|
|
507
|
+
selected_items.add(name)
|
|
508
|
+
|
|
509
|
+
@kb.add("enter")
|
|
510
|
+
def install_selected(event):
|
|
511
|
+
if selected_items:
|
|
512
|
+
result[0] = "install"
|
|
513
|
+
else:
|
|
514
|
+
# Install current item
|
|
515
|
+
selected_items.add(items[selected_index[0]][0])
|
|
516
|
+
result[0] = "install"
|
|
517
|
+
event.app.exit()
|
|
518
|
+
|
|
519
|
+
@kb.add("a")
|
|
520
|
+
def select_all(event):
|
|
521
|
+
for name, _ in items:
|
|
522
|
+
selected_items.add(name)
|
|
523
|
+
|
|
524
|
+
@kb.add("b")
|
|
525
|
+
@kb.add("escape")
|
|
526
|
+
def go_back(event):
|
|
527
|
+
result[0] = "back"
|
|
528
|
+
event.app.exit()
|
|
529
|
+
|
|
530
|
+
@kb.add("c-c")
|
|
531
|
+
@kb.add("q")
|
|
532
|
+
def cancel(event):
|
|
533
|
+
result[0] = None
|
|
534
|
+
event.app.exit()
|
|
535
|
+
|
|
536
|
+
def get_formatted_menu():
|
|
537
|
+
lines = [("class:title", f"─── {category.title()} {'─' * (40 - len(category))}\n\n")]
|
|
538
|
+
|
|
539
|
+
for i, (name, info) in enumerate(items):
|
|
540
|
+
is_selected = i == selected_index[0]
|
|
541
|
+
is_checked = name in selected_items
|
|
542
|
+
prefix = "▸ " if is_selected else " "
|
|
543
|
+
checkbox = "●" if is_checked else "○"
|
|
544
|
+
|
|
545
|
+
desc = info.get("description", "")
|
|
546
|
+
if len(desc) > 45:
|
|
547
|
+
desc = desc[:42] + "..."
|
|
548
|
+
|
|
549
|
+
if is_selected:
|
|
550
|
+
lines.append(("class:selected", f" {prefix}{checkbox} {name}"))
|
|
551
|
+
lines.append(("class:desc-selected", f" {desc}\n"))
|
|
552
|
+
else:
|
|
553
|
+
style_class = "class:checked" if is_checked else "class:option"
|
|
554
|
+
lines.append((style_class, f" {prefix}{checkbox} {name}"))
|
|
555
|
+
lines.append(("class:desc", f" {desc}\n"))
|
|
556
|
+
|
|
557
|
+
selected_count = len(selected_items)
|
|
558
|
+
if selected_count > 0:
|
|
559
|
+
lines.append(("class:status", f"\n {selected_count} selected"))
|
|
560
|
+
|
|
561
|
+
lines.append(("class:hint", f"\n{'─' * 45}\n ↑↓ navigate Space toggle Enter install a all b back"))
|
|
562
|
+
return lines
|
|
563
|
+
|
|
564
|
+
style = Style.from_dict({
|
|
565
|
+
"title": f"{Colors.MUTED}",
|
|
566
|
+
"selected": f"{Colors.SUCCESS} bold",
|
|
567
|
+
"checked": f"{Colors.WARNING}",
|
|
568
|
+
"desc-selected": f"{Colors.SUCCESS}",
|
|
569
|
+
"option": f"{Colors.PRIMARY}",
|
|
570
|
+
"desc": f"{Colors.DIM}",
|
|
571
|
+
"status": f"{Colors.WARNING} bold",
|
|
572
|
+
"hint": f"{Colors.DIM}",
|
|
573
|
+
})
|
|
574
|
+
|
|
575
|
+
height = len(items) + 6
|
|
576
|
+
|
|
577
|
+
layout = Layout(
|
|
578
|
+
HSplit([
|
|
579
|
+
Window(
|
|
580
|
+
FormattedTextControl(get_formatted_menu),
|
|
581
|
+
height=height,
|
|
582
|
+
),
|
|
583
|
+
])
|
|
584
|
+
)
|
|
585
|
+
|
|
586
|
+
app = Application(
|
|
587
|
+
layout=layout,
|
|
588
|
+
key_bindings=kb,
|
|
589
|
+
style=style,
|
|
590
|
+
full_screen=False,
|
|
591
|
+
)
|
|
592
|
+
|
|
593
|
+
try:
|
|
594
|
+
app.run()
|
|
595
|
+
except (KeyboardInterrupt, EOFError):
|
|
596
|
+
return
|
|
597
|
+
|
|
598
|
+
console.print()
|
|
599
|
+
|
|
600
|
+
if result[0] == "back":
|
|
601
|
+
_show_registry_wizard()
|
|
602
|
+
return
|
|
603
|
+
|
|
604
|
+
if result[0] == "install" and selected_items:
|
|
605
|
+
singular = category[:-1]
|
|
606
|
+
component_ids = [f"{singular}:{name}" for name in selected_items]
|
|
607
|
+
|
|
608
|
+
console.print()
|
|
609
|
+
for cid in component_ids:
|
|
610
|
+
ctype, name = cid.split(":", 1)
|
|
611
|
+
info = components[name]
|
|
612
|
+
|
|
613
|
+
console.print(f" [{Colors.DIM}]installing {cid}...[/{Colors.DIM}]", end="\r")
|
|
614
|
+
content = _fetch_component(info["path"])
|
|
615
|
+
console.print(" ", end="\r") # Clear
|
|
616
|
+
|
|
617
|
+
if not content:
|
|
618
|
+
continue
|
|
619
|
+
|
|
620
|
+
installers = {
|
|
621
|
+
"skill": _install_skill,
|
|
622
|
+
"rule": _install_rule,
|
|
623
|
+
"agent": _install_agent,
|
|
624
|
+
"verifier": _install_verifier,
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
installer = installers.get(ctype)
|
|
628
|
+
if installer:
|
|
629
|
+
try:
|
|
630
|
+
installer(name, content)
|
|
631
|
+
console.print(f" [{Colors.SUCCESS}]{STATUS_ACTIVE}[/{Colors.SUCCESS}] [{Colors.MUTED}]installed:[/{Colors.MUTED}] {cid}")
|
|
632
|
+
except Exception as e:
|
|
633
|
+
console.print(f" [{Colors.ERROR}]error:[/{Colors.ERROR}] {cid} - {e}")
|
|
634
|
+
|
|
635
|
+
console.print()
|