aes-cli 0.2.0__tar.gz → 0.3.0__tar.gz
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.
- {aes_cli-0.2.0 → aes_cli-0.3.0}/PKG-INFO +1 -1
- {aes_cli-0.2.0 → aes_cli-0.3.0}/aes/__init__.py +1 -1
- aes_cli-0.3.0/aes/commands/inspect.py +453 -0
- aes_cli-0.3.0/aes/commands/search.py +119 -0
- {aes_cli-0.2.0 → aes_cli-0.3.0}/aes/registry.py +7 -0
- {aes_cli-0.2.0 → aes_cli-0.3.0}/aes_cli.egg-info/PKG-INFO +1 -1
- {aes_cli-0.2.0 → aes_cli-0.3.0}/pyproject.toml +1 -1
- aes_cli-0.3.0/tests/test_inspect.py +309 -0
- {aes_cli-0.2.0 → aes_cli-0.3.0}/tests/test_search.py +75 -4
- aes_cli-0.2.0/aes/commands/inspect.py +0 -204
- aes_cli-0.2.0/aes/commands/search.py +0 -65
- aes_cli-0.2.0/tests/test_inspect.py +0 -54
- {aes_cli-0.2.0 → aes_cli-0.3.0}/README.md +0 -0
- {aes_cli-0.2.0 → aes_cli-0.3.0}/aes/__main__.py +0 -0
- {aes_cli-0.2.0 → aes_cli-0.3.0}/aes/analyzer.py +0 -0
- {aes_cli-0.2.0 → aes_cli-0.3.0}/aes/commands/__init__.py +0 -0
- {aes_cli-0.2.0 → aes_cli-0.3.0}/aes/commands/init.py +0 -0
- {aes_cli-0.2.0 → aes_cli-0.3.0}/aes/commands/install.py +0 -0
- {aes_cli-0.2.0 → aes_cli-0.3.0}/aes/commands/publish.py +0 -0
- {aes_cli-0.2.0 → aes_cli-0.3.0}/aes/commands/status.py +0 -0
- {aes_cli-0.2.0 → aes_cli-0.3.0}/aes/commands/sync.py +0 -0
- {aes_cli-0.2.0 → aes_cli-0.3.0}/aes/commands/validate.py +0 -0
- {aes_cli-0.2.0 → aes_cli-0.3.0}/aes/config.py +0 -0
- {aes_cli-0.2.0 → aes_cli-0.3.0}/aes/domains.py +0 -0
- {aes_cli-0.2.0 → aes_cli-0.3.0}/aes/frameworks.py +0 -0
- {aes_cli-0.2.0 → aes_cli-0.3.0}/aes/mcp_server.py +0 -0
- {aes_cli-0.2.0 → aes_cli-0.3.0}/aes/scaffold/agent.yaml.jinja +0 -0
- {aes_cli-0.2.0 → aes_cli-0.3.0}/aes/scaffold/agentignore.jinja +0 -0
- {aes_cli-0.2.0 → aes_cli-0.3.0}/aes/scaffold/instructions.md.jinja +0 -0
- {aes_cli-0.2.0 → aes_cli-0.3.0}/aes/scaffold/local.example.yaml.jinja +0 -0
- {aes_cli-0.2.0 → aes_cli-0.3.0}/aes/scaffold/local.yaml.jinja +0 -0
- {aes_cli-0.2.0 → aes_cli-0.3.0}/aes/scaffold/operations.md.jinja +0 -0
- {aes_cli-0.2.0 → aes_cli-0.3.0}/aes/scaffold/orchestrator.md.jinja +0 -0
- {aes_cli-0.2.0 → aes_cli-0.3.0}/aes/scaffold/permissions.yaml.jinja +0 -0
- {aes_cli-0.2.0 → aes_cli-0.3.0}/aes/scaffold/setup.md.jinja +0 -0
- {aes_cli-0.2.0 → aes_cli-0.3.0}/aes/scaffold/skill.md.jinja +0 -0
- {aes_cli-0.2.0 → aes_cli-0.3.0}/aes/scaffold/skill.yaml.jinja +0 -0
- {aes_cli-0.2.0 → aes_cli-0.3.0}/aes/scaffold/workflow.yaml.jinja +0 -0
- {aes_cli-0.2.0 → aes_cli-0.3.0}/aes/scaffold/workflow_command.md.jinja +0 -0
- {aes_cli-0.2.0 → aes_cli-0.3.0}/aes/schemas/agent.schema.json +0 -0
- {aes_cli-0.2.0 → aes_cli-0.3.0}/aes/schemas/permissions.schema.json +0 -0
- {aes_cli-0.2.0 → aes_cli-0.3.0}/aes/schemas/registry.schema.json +0 -0
- {aes_cli-0.2.0 → aes_cli-0.3.0}/aes/schemas/skill.schema.json +0 -0
- {aes_cli-0.2.0 → aes_cli-0.3.0}/aes/schemas/workflow.schema.json +0 -0
- {aes_cli-0.2.0 → aes_cli-0.3.0}/aes/targets/__init__.py +0 -0
- {aes_cli-0.2.0 → aes_cli-0.3.0}/aes/targets/_base.py +0 -0
- {aes_cli-0.2.0 → aes_cli-0.3.0}/aes/targets/_composer.py +0 -0
- {aes_cli-0.2.0 → aes_cli-0.3.0}/aes/targets/claude.py +0 -0
- {aes_cli-0.2.0 → aes_cli-0.3.0}/aes/targets/copilot.py +0 -0
- {aes_cli-0.2.0 → aes_cli-0.3.0}/aes/targets/cursor.py +0 -0
- {aes_cli-0.2.0 → aes_cli-0.3.0}/aes/targets/windsurf.py +0 -0
- {aes_cli-0.2.0 → aes_cli-0.3.0}/aes/validator.py +0 -0
- {aes_cli-0.2.0 → aes_cli-0.3.0}/aes_cli.egg-info/SOURCES.txt +0 -0
- {aes_cli-0.2.0 → aes_cli-0.3.0}/aes_cli.egg-info/dependency_links.txt +0 -0
- {aes_cli-0.2.0 → aes_cli-0.3.0}/aes_cli.egg-info/entry_points.txt +0 -0
- {aes_cli-0.2.0 → aes_cli-0.3.0}/aes_cli.egg-info/requires.txt +0 -0
- {aes_cli-0.2.0 → aes_cli-0.3.0}/aes_cli.egg-info/top_level.txt +0 -0
- {aes_cli-0.2.0 → aes_cli-0.3.0}/setup.cfg +0 -0
- {aes_cli-0.2.0 → aes_cli-0.3.0}/tests/test_analyzer.py +0 -0
- {aes_cli-0.2.0 → aes_cli-0.3.0}/tests/test_frameworks.py +0 -0
- {aes_cli-0.2.0 → aes_cli-0.3.0}/tests/test_init.py +0 -0
- {aes_cli-0.2.0 → aes_cli-0.3.0}/tests/test_install.py +0 -0
- {aes_cli-0.2.0 → aes_cli-0.3.0}/tests/test_mcp_server.py +0 -0
- {aes_cli-0.2.0 → aes_cli-0.3.0}/tests/test_publish.py +0 -0
- {aes_cli-0.2.0 → aes_cli-0.3.0}/tests/test_registry.py +0 -0
- {aes_cli-0.2.0 → aes_cli-0.3.0}/tests/test_status.py +0 -0
- {aes_cli-0.2.0 → aes_cli-0.3.0}/tests/test_sync.py +0 -0
- {aes_cli-0.2.0 → aes_cli-0.3.0}/tests/test_validate.py +0 -0
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
"""aes inspect — Show project structure and stats."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import tarfile
|
|
6
|
+
import tempfile
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
import click
|
|
11
|
+
import yaml
|
|
12
|
+
from rich.console import Console
|
|
13
|
+
from rich.table import Table
|
|
14
|
+
|
|
15
|
+
from aes.config import AGENT_DIR
|
|
16
|
+
from aes.registry import (
|
|
17
|
+
fetch_index,
|
|
18
|
+
download_package,
|
|
19
|
+
parse_registry_source,
|
|
20
|
+
resolve_version,
|
|
21
|
+
_parse_version,
|
|
22
|
+
)
|
|
23
|
+
from aes.commands.install import _safe_extract
|
|
24
|
+
|
|
25
|
+
console = Console()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _load_yaml(path: Path) -> dict:
|
|
29
|
+
"""Load YAML file, return empty dict on error."""
|
|
30
|
+
try:
|
|
31
|
+
with open(path) as f:
|
|
32
|
+
data = yaml.safe_load(f)
|
|
33
|
+
return data if isinstance(data, dict) else {}
|
|
34
|
+
except Exception:
|
|
35
|
+
return {}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _render_workflow_diagram(workflow: dict) -> str:
|
|
39
|
+
"""Render a simple ASCII state diagram from a workflow definition."""
|
|
40
|
+
states = workflow.get("states", {})
|
|
41
|
+
transitions = workflow.get("transitions", [])
|
|
42
|
+
|
|
43
|
+
if not states or not transitions:
|
|
44
|
+
return " (no states or transitions defined)"
|
|
45
|
+
|
|
46
|
+
lines = []
|
|
47
|
+
# Find initial and terminal states
|
|
48
|
+
initial = [s for s, v in states.items() if v.get("initial")]
|
|
49
|
+
terminal = [s for s, v in states.items() if v.get("terminal")]
|
|
50
|
+
intermediate = [s for s in states if s not in initial and s not in terminal]
|
|
51
|
+
|
|
52
|
+
# Render flow
|
|
53
|
+
all_ordered = initial + intermediate + terminal
|
|
54
|
+
if all_ordered:
|
|
55
|
+
# Build transition map
|
|
56
|
+
tx_map: dict[str, list[str]] = {}
|
|
57
|
+
for tx in transitions:
|
|
58
|
+
src = tx.get("from", "")
|
|
59
|
+
dst = tx.get("to", "")
|
|
60
|
+
tx_map.setdefault(src, []).append(dst)
|
|
61
|
+
|
|
62
|
+
# Show forward transitions
|
|
63
|
+
forward_chain = initial.copy()
|
|
64
|
+
visited = set(initial)
|
|
65
|
+
current = initial[0] if initial else ""
|
|
66
|
+
while current:
|
|
67
|
+
targets = tx_map.get(current, [])
|
|
68
|
+
next_state = None
|
|
69
|
+
for t in targets:
|
|
70
|
+
if t not in visited and t not in terminal:
|
|
71
|
+
next_state = t
|
|
72
|
+
break
|
|
73
|
+
if next_state:
|
|
74
|
+
forward_chain.append(next_state)
|
|
75
|
+
visited.add(next_state)
|
|
76
|
+
current = next_state
|
|
77
|
+
else:
|
|
78
|
+
break
|
|
79
|
+
|
|
80
|
+
lines.append(" " + " --> ".join(forward_chain))
|
|
81
|
+
if terminal:
|
|
82
|
+
lines.append(" Terminal: " + ", ".join(terminal))
|
|
83
|
+
|
|
84
|
+
# Show backward transitions
|
|
85
|
+
backward = [tx for tx in transitions if tx.get("to") in visited and
|
|
86
|
+
all_ordered.index(tx.get("from", "")) > all_ordered.index(tx.get("to", ""))
|
|
87
|
+
if tx.get("from", "") in all_ordered and tx.get("to", "") in all_ordered]
|
|
88
|
+
for tx in backward:
|
|
89
|
+
lines.append(f" (loop) {tx['from']} --> {tx['to']}: {tx.get('description', 'reframe')}")
|
|
90
|
+
|
|
91
|
+
return "\n".join(lines) if lines else " (could not render diagram)"
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _is_local_path(target: str) -> bool:
|
|
95
|
+
"""Return True if target looks like a local filesystem path."""
|
|
96
|
+
if target.startswith(("/", "./", "../")):
|
|
97
|
+
return True
|
|
98
|
+
if Path(target).is_dir():
|
|
99
|
+
return True
|
|
100
|
+
return False
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _inspect_local(path: str) -> None:
|
|
104
|
+
"""Inspect a local .agent/ directory."""
|
|
105
|
+
project_root = Path(path).resolve()
|
|
106
|
+
agent_dir = project_root / AGENT_DIR
|
|
107
|
+
|
|
108
|
+
if not agent_dir.exists():
|
|
109
|
+
console.print(f"[red]Error:[/] No {AGENT_DIR}/ directory found at {project_root}")
|
|
110
|
+
raise SystemExit(1)
|
|
111
|
+
|
|
112
|
+
manifest_path = agent_dir / "agent.yaml"
|
|
113
|
+
if not manifest_path.exists():
|
|
114
|
+
console.print(f"[red]Error:[/] No agent.yaml found in {agent_dir}")
|
|
115
|
+
raise SystemExit(1)
|
|
116
|
+
|
|
117
|
+
manifest = _load_yaml(manifest_path)
|
|
118
|
+
|
|
119
|
+
# Header
|
|
120
|
+
console.print()
|
|
121
|
+
console.print(f"[bold]{manifest.get('name', 'unknown')}[/] v{manifest.get('version', '?')}")
|
|
122
|
+
console.print(f" {manifest.get('description', '')}")
|
|
123
|
+
console.print(f" Domain: {manifest.get('domain', 'unspecified')} | "
|
|
124
|
+
f"Language: {manifest.get('runtime', {}).get('language', '?')} | "
|
|
125
|
+
f"AES: {manifest.get('aes', '?')}")
|
|
126
|
+
console.print()
|
|
127
|
+
|
|
128
|
+
# Skills table
|
|
129
|
+
skills = manifest.get("skills", [])
|
|
130
|
+
if skills:
|
|
131
|
+
table = Table(title="Skills", show_header=True, header_style="bold")
|
|
132
|
+
table.add_column("ID", style="cyan")
|
|
133
|
+
table.add_column("Manifest")
|
|
134
|
+
table.add_column("Runbook")
|
|
135
|
+
table.add_column("Status")
|
|
136
|
+
|
|
137
|
+
for skill in skills:
|
|
138
|
+
manifest_exists = (agent_dir / skill.get("manifest", "")).exists() if skill.get("manifest") else False
|
|
139
|
+
runbook_exists = (agent_dir / skill.get("runbook", "")).exists() if skill.get("runbook") else False
|
|
140
|
+
status = "[green]OK[/]" if manifest_exists and runbook_exists else "[red]MISSING[/]"
|
|
141
|
+
table.add_row(
|
|
142
|
+
skill.get("id", "?"),
|
|
143
|
+
skill.get("manifest", "-"),
|
|
144
|
+
skill.get("runbook", "-"),
|
|
145
|
+
status,
|
|
146
|
+
)
|
|
147
|
+
console.print(table)
|
|
148
|
+
console.print()
|
|
149
|
+
|
|
150
|
+
# Registries
|
|
151
|
+
registries = manifest.get("registries", [])
|
|
152
|
+
if registries:
|
|
153
|
+
table = Table(title="Registries", show_header=True, header_style="bold")
|
|
154
|
+
table.add_column("ID", style="cyan")
|
|
155
|
+
table.add_column("Path")
|
|
156
|
+
table.add_column("Description")
|
|
157
|
+
table.add_column("Entries")
|
|
158
|
+
|
|
159
|
+
for reg in registries:
|
|
160
|
+
reg_path = agent_dir / reg["path"]
|
|
161
|
+
entry_count = "?"
|
|
162
|
+
if reg_path.exists():
|
|
163
|
+
reg_data = _load_yaml(reg_path)
|
|
164
|
+
categories = reg_data.get("categories", {})
|
|
165
|
+
count = sum(
|
|
166
|
+
len(v) if isinstance(v, dict) else 0
|
|
167
|
+
for v in categories.values()
|
|
168
|
+
)
|
|
169
|
+
entry_count = str(count)
|
|
170
|
+
|
|
171
|
+
table.add_row(
|
|
172
|
+
reg.get("id", "?"),
|
|
173
|
+
reg["path"],
|
|
174
|
+
reg.get("description", "-"),
|
|
175
|
+
entry_count,
|
|
176
|
+
)
|
|
177
|
+
console.print(table)
|
|
178
|
+
console.print()
|
|
179
|
+
|
|
180
|
+
# Workflows
|
|
181
|
+
workflows = manifest.get("workflows", [])
|
|
182
|
+
if workflows:
|
|
183
|
+
for wf_ref in workflows:
|
|
184
|
+
wf_path = agent_dir / wf_ref["path"]
|
|
185
|
+
if wf_path.exists():
|
|
186
|
+
wf_data = _load_yaml(wf_path)
|
|
187
|
+
n_states = len(wf_data.get("states", {}))
|
|
188
|
+
n_transitions = len(wf_data.get("transitions", []))
|
|
189
|
+
console.print(f"[bold]Workflow:[/] {wf_ref['id']} ({n_states} states, {n_transitions} transitions)")
|
|
190
|
+
console.print(_render_workflow_diagram(wf_data))
|
|
191
|
+
console.print()
|
|
192
|
+
|
|
193
|
+
# Commands
|
|
194
|
+
commands = manifest.get("commands", [])
|
|
195
|
+
if commands:
|
|
196
|
+
table = Table(title="Commands", show_header=True, header_style="bold")
|
|
197
|
+
table.add_column("Trigger", style="cyan")
|
|
198
|
+
table.add_column("Description")
|
|
199
|
+
for cmd in commands:
|
|
200
|
+
table.add_row(
|
|
201
|
+
cmd.get("trigger", f"/{cmd.get('id', '?')}"),
|
|
202
|
+
cmd.get("description", "-"),
|
|
203
|
+
)
|
|
204
|
+
console.print(table)
|
|
205
|
+
console.print()
|
|
206
|
+
|
|
207
|
+
# Summary
|
|
208
|
+
console.print("[bold]Summary[/]")
|
|
209
|
+
console.print(f" Skills: {len(skills)}")
|
|
210
|
+
console.print(f" Registries: {len(registries)}")
|
|
211
|
+
console.print(f" Workflows: {len(workflows)}")
|
|
212
|
+
console.print(f" Commands: {len(commands)}")
|
|
213
|
+
|
|
214
|
+
# Resources
|
|
215
|
+
resources = manifest.get("resources", {})
|
|
216
|
+
if resources:
|
|
217
|
+
console.print(f" CPU limit: {resources.get('max_cpu_percent', '-')}%")
|
|
218
|
+
console.print(f" Mem limit: {resources.get('max_memory_percent', '-')}%")
|
|
219
|
+
console.print()
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
# ---------------------------------------------------------------------------
|
|
223
|
+
# Remote registry inspection
|
|
224
|
+
# ---------------------------------------------------------------------------
|
|
225
|
+
|
|
226
|
+
def _render_registry_metadata(name: str, pkg: dict, selected_version: str) -> None:
|
|
227
|
+
"""Render registry-level metadata for a package."""
|
|
228
|
+
console.print()
|
|
229
|
+
console.print(f"[bold]{name}[/] v{selected_version} [dim](registry)[/]")
|
|
230
|
+
console.print(f" {pkg.get('description', '')}")
|
|
231
|
+
console.print(f" Type: {pkg.get('type', 'skill')} | "
|
|
232
|
+
f"Visibility: {pkg.get('visibility', 'public')}")
|
|
233
|
+
|
|
234
|
+
tags = pkg.get("tags", [])
|
|
235
|
+
if tags:
|
|
236
|
+
console.print(f" Tags: {', '.join(tags)}")
|
|
237
|
+
console.print()
|
|
238
|
+
|
|
239
|
+
# Versions table
|
|
240
|
+
versions_dict = pkg.get("versions", {})
|
|
241
|
+
if versions_dict:
|
|
242
|
+
table = Table(title="Versions", show_header=True, header_style="bold")
|
|
243
|
+
table.add_column("Version", style="cyan")
|
|
244
|
+
table.add_column("Published")
|
|
245
|
+
table.add_column("SHA256", style="dim")
|
|
246
|
+
|
|
247
|
+
sorted_versions = sorted(
|
|
248
|
+
versions_dict.items(),
|
|
249
|
+
key=lambda kv: _parse_version(kv[0]),
|
|
250
|
+
reverse=True,
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
for ver, info in sorted_versions:
|
|
254
|
+
marker = " [bold green](latest)[/]" if ver == pkg.get("latest") else ""
|
|
255
|
+
published = info.get("published_at", "?")
|
|
256
|
+
if isinstance(published, str) and "T" in published:
|
|
257
|
+
published = published.split("T")[0]
|
|
258
|
+
sha_short = info.get("sha256", "?")[:12] + "..."
|
|
259
|
+
table.add_row(f"{ver}{marker}", published, sha_short)
|
|
260
|
+
|
|
261
|
+
console.print(table)
|
|
262
|
+
console.print()
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _inspect_remote_skill(extract_dir: Path) -> None:
|
|
266
|
+
"""Render skill manifest details from an extracted package."""
|
|
267
|
+
manifests = list(extract_dir.rglob("*.skill.yaml"))
|
|
268
|
+
if not manifests:
|
|
269
|
+
manifests = list(extract_dir.rglob("skill.yaml"))
|
|
270
|
+
if not manifests:
|
|
271
|
+
console.print("[dim]No skill manifest found in package.[/]")
|
|
272
|
+
return
|
|
273
|
+
|
|
274
|
+
manifest = _load_yaml(manifests[0])
|
|
275
|
+
|
|
276
|
+
console.print("[bold]Skill Details[/]")
|
|
277
|
+
console.print(f" ID: {manifest.get('id', '?')}")
|
|
278
|
+
console.print(f" Name: {manifest.get('name', '?')}")
|
|
279
|
+
console.print(f" Version: {manifest.get('version', '?')}")
|
|
280
|
+
console.print(f" Description: {manifest.get('description', '')}")
|
|
281
|
+
console.print()
|
|
282
|
+
|
|
283
|
+
# Inputs
|
|
284
|
+
inputs = manifest.get("inputs", {})
|
|
285
|
+
required = inputs.get("required", [])
|
|
286
|
+
optional = inputs.get("optional", [])
|
|
287
|
+
env_inputs = inputs.get("environment", [])
|
|
288
|
+
|
|
289
|
+
if required or optional:
|
|
290
|
+
table = Table(title="Inputs", show_header=True, header_style="bold")
|
|
291
|
+
table.add_column("Name", style="cyan")
|
|
292
|
+
table.add_column("Type")
|
|
293
|
+
table.add_column("Required")
|
|
294
|
+
table.add_column("Description")
|
|
295
|
+
|
|
296
|
+
for inp in required:
|
|
297
|
+
table.add_row(
|
|
298
|
+
inp.get("name", "?"),
|
|
299
|
+
inp.get("type", "?"),
|
|
300
|
+
"[green]Yes[/]",
|
|
301
|
+
inp.get("description", ""),
|
|
302
|
+
)
|
|
303
|
+
for inp in optional:
|
|
304
|
+
table.add_row(
|
|
305
|
+
inp.get("name", "?"),
|
|
306
|
+
inp.get("type", "?"),
|
|
307
|
+
"No",
|
|
308
|
+
inp.get("description", ""),
|
|
309
|
+
)
|
|
310
|
+
console.print(table)
|
|
311
|
+
console.print()
|
|
312
|
+
|
|
313
|
+
if env_inputs:
|
|
314
|
+
console.print(f" Environment: {', '.join(env_inputs)}")
|
|
315
|
+
console.print()
|
|
316
|
+
|
|
317
|
+
# Outputs
|
|
318
|
+
outputs = manifest.get("outputs", [])
|
|
319
|
+
if outputs:
|
|
320
|
+
table = Table(title="Outputs", show_header=True, header_style="bold")
|
|
321
|
+
table.add_column("Name", style="cyan")
|
|
322
|
+
table.add_column("Type")
|
|
323
|
+
table.add_column("Description")
|
|
324
|
+
|
|
325
|
+
for out in outputs:
|
|
326
|
+
table.add_row(
|
|
327
|
+
out.get("name", "?"),
|
|
328
|
+
out.get("type", "?"),
|
|
329
|
+
out.get("description", ""),
|
|
330
|
+
)
|
|
331
|
+
console.print(table)
|
|
332
|
+
console.print()
|
|
333
|
+
|
|
334
|
+
# Dependencies
|
|
335
|
+
depends_on = manifest.get("depends_on", [])
|
|
336
|
+
blocks = manifest.get("blocks", [])
|
|
337
|
+
if depends_on or blocks:
|
|
338
|
+
console.print("[bold]Dependencies[/]")
|
|
339
|
+
if depends_on:
|
|
340
|
+
console.print(f" Depends on: {', '.join(str(d) for d in depends_on)}")
|
|
341
|
+
if blocks:
|
|
342
|
+
console.print(f" Blocks: {', '.join(str(b) for b in blocks)}")
|
|
343
|
+
console.print()
|
|
344
|
+
|
|
345
|
+
# Triggers
|
|
346
|
+
triggers = manifest.get("triggers", [])
|
|
347
|
+
if triggers:
|
|
348
|
+
console.print("[bold]Triggers[/]")
|
|
349
|
+
for t in triggers:
|
|
350
|
+
label = t.get("command", t.get("cron", t.get("description", "")))
|
|
351
|
+
console.print(f" [{t.get('type', '?')}] {label}")
|
|
352
|
+
console.print()
|
|
353
|
+
|
|
354
|
+
# Negative triggers
|
|
355
|
+
neg_triggers = manifest.get("negative_triggers", [])
|
|
356
|
+
if neg_triggers:
|
|
357
|
+
console.print("[bold]Negative Triggers[/]")
|
|
358
|
+
for nt in neg_triggers:
|
|
359
|
+
console.print(f" [red]- {nt}[/]")
|
|
360
|
+
console.print()
|
|
361
|
+
|
|
362
|
+
# Tags
|
|
363
|
+
tags = manifest.get("tags", [])
|
|
364
|
+
if tags:
|
|
365
|
+
console.print(f" Tags: {', '.join(tags)}")
|
|
366
|
+
console.print()
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def _inspect_remote_template(extract_dir: Path) -> None:
|
|
370
|
+
"""Render template details by finding .agent/ and reusing local inspect."""
|
|
371
|
+
for candidate in extract_dir.rglob(AGENT_DIR):
|
|
372
|
+
if candidate.is_dir() and (candidate / "agent.yaml").exists():
|
|
373
|
+
project_root = candidate.parent
|
|
374
|
+
_inspect_local(str(project_root))
|
|
375
|
+
return
|
|
376
|
+
|
|
377
|
+
console.print("[dim]No .agent/ directory found in template package.[/]")
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def _inspect_remote(target: str) -> None:
|
|
381
|
+
"""Inspect a remote registry package."""
|
|
382
|
+
try:
|
|
383
|
+
name, version_spec = parse_registry_source(target)
|
|
384
|
+
except ValueError as exc:
|
|
385
|
+
console.print(f"[red]Error:[/] {exc}")
|
|
386
|
+
raise SystemExit(1)
|
|
387
|
+
|
|
388
|
+
try:
|
|
389
|
+
index = fetch_index()
|
|
390
|
+
except Exception as exc:
|
|
391
|
+
console.print(f"[red]Error:[/] Failed to fetch registry: {exc}")
|
|
392
|
+
console.print("[dim]Check your network or set AES_REGISTRY_URL.[/]")
|
|
393
|
+
raise SystemExit(1)
|
|
394
|
+
|
|
395
|
+
packages = index.get("packages", {})
|
|
396
|
+
if name not in packages:
|
|
397
|
+
console.print(f"[red]Error:[/] Package '{name}' not found in registry.")
|
|
398
|
+
console.print("[dim]Use 'aes search' to find available packages.[/]")
|
|
399
|
+
raise SystemExit(1)
|
|
400
|
+
|
|
401
|
+
pkg = packages[name]
|
|
402
|
+
pkg_type = pkg.get("type", "skill")
|
|
403
|
+
versions_dict = pkg.get("versions", {})
|
|
404
|
+
|
|
405
|
+
available = list(versions_dict.keys())
|
|
406
|
+
version = resolve_version(version_spec, available)
|
|
407
|
+
if version is None:
|
|
408
|
+
console.print(
|
|
409
|
+
f"[red]Error:[/] No version of '{name}' matches '{version_spec}'. "
|
|
410
|
+
f"Available: {', '.join(available)}"
|
|
411
|
+
)
|
|
412
|
+
raise SystemExit(1)
|
|
413
|
+
|
|
414
|
+
# Registry metadata
|
|
415
|
+
_render_registry_metadata(name, pkg, version)
|
|
416
|
+
|
|
417
|
+
# Download and inspect package contents
|
|
418
|
+
version_info = versions_dict[version]
|
|
419
|
+
sha256_expected = version_info.get("sha256", "")
|
|
420
|
+
|
|
421
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
422
|
+
tmp_dir = Path(tmp)
|
|
423
|
+
try:
|
|
424
|
+
tarball = download_package(name, version, sha256_expected, tmp_dir)
|
|
425
|
+
except Exception as exc:
|
|
426
|
+
console.print(f"[yellow]Warning:[/] Could not download package: {exc}")
|
|
427
|
+
console.print("[dim]Showing registry metadata only.[/]")
|
|
428
|
+
return
|
|
429
|
+
|
|
430
|
+
with tarfile.open(tarball, "r:gz") as tar:
|
|
431
|
+
_safe_extract(tar, tmp_dir)
|
|
432
|
+
|
|
433
|
+
if pkg_type == "template":
|
|
434
|
+
_inspect_remote_template(tmp_dir)
|
|
435
|
+
else:
|
|
436
|
+
_inspect_remote_skill(tmp_dir)
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
@click.command("inspect")
|
|
440
|
+
@click.argument("target", default=".")
|
|
441
|
+
def inspect_cmd(target: str) -> None:
|
|
442
|
+
"""Show AES project structure, or inspect a remote registry package.
|
|
443
|
+
|
|
444
|
+
\b
|
|
445
|
+
Local: aes inspect ./my-project
|
|
446
|
+
Remote: aes inspect deploy
|
|
447
|
+
aes inspect deploy@1.0.0
|
|
448
|
+
aes inspect aes-hub/deploy
|
|
449
|
+
"""
|
|
450
|
+
if _is_local_path(target):
|
|
451
|
+
_inspect_local(target)
|
|
452
|
+
else:
|
|
453
|
+
_inspect_remote(target)
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""aes search — Search the AES package registry."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.table import Table
|
|
10
|
+
|
|
11
|
+
from aes.registry import fetch_index, search_packages, _parse_version
|
|
12
|
+
|
|
13
|
+
console = Console()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _sort_results(results: list, sort_by: str) -> list:
|
|
17
|
+
"""Sort search results by the given key."""
|
|
18
|
+
if sort_by == "latest":
|
|
19
|
+
return sorted(
|
|
20
|
+
results,
|
|
21
|
+
key=lambda p: p.get("latest_published_at", ""),
|
|
22
|
+
reverse=True,
|
|
23
|
+
)
|
|
24
|
+
elif sort_by == "version":
|
|
25
|
+
def _ver_key(p: dict) -> tuple:
|
|
26
|
+
try:
|
|
27
|
+
return _parse_version(p.get("latest", "0.0.0"))
|
|
28
|
+
except ValueError:
|
|
29
|
+
return (0, 0, 0)
|
|
30
|
+
return sorted(results, key=_ver_key, reverse=True)
|
|
31
|
+
else:
|
|
32
|
+
return sorted(results, key=lambda p: p["name"])
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@click.command("search")
|
|
36
|
+
@click.argument("query", default="")
|
|
37
|
+
@click.option("--tag", default=None, help="Filter by tag")
|
|
38
|
+
@click.option("--domain", default=None, help="Filter by domain (convention: domain as tag)")
|
|
39
|
+
@click.option("--type", "pkg_type", default=None, type=click.Choice(["skill", "template"]), help="Filter by package type")
|
|
40
|
+
@click.option("--sort-by", "sort_by", default="name", type=click.Choice(["name", "latest", "version"]), help="Sort results")
|
|
41
|
+
@click.option("--limit", "limit", default=None, type=int, help="Show only first N results")
|
|
42
|
+
@click.option("--verbose", "-v", is_flag=True, default=False, help="Show version count and publish date")
|
|
43
|
+
def search_cmd(
|
|
44
|
+
query: str,
|
|
45
|
+
tag: Optional[str],
|
|
46
|
+
domain: Optional[str],
|
|
47
|
+
pkg_type: Optional[str],
|
|
48
|
+
sort_by: str,
|
|
49
|
+
limit: Optional[int],
|
|
50
|
+
verbose: bool,
|
|
51
|
+
) -> None:
|
|
52
|
+
"""Search the AES package registry.
|
|
53
|
+
|
|
54
|
+
\b
|
|
55
|
+
Examples:
|
|
56
|
+
aes search "deploy" # keyword search
|
|
57
|
+
aes search --tag ml # filter by tag
|
|
58
|
+
aes search --domain devops # filter by domain
|
|
59
|
+
aes search --type template # filter by type
|
|
60
|
+
aes search --sort-by version # sort by semver (highest first)
|
|
61
|
+
aes search --limit 5 # show top 5 results
|
|
62
|
+
aes search -v # verbose: show version count + date
|
|
63
|
+
aes search # list all packages
|
|
64
|
+
"""
|
|
65
|
+
try:
|
|
66
|
+
index = fetch_index()
|
|
67
|
+
except Exception as exc:
|
|
68
|
+
console.print(f"[red]Error:[/] Failed to fetch registry: {exc}")
|
|
69
|
+
console.print("[dim]Check your network or set AES_REGISTRY_URL.[/]")
|
|
70
|
+
raise SystemExit(1)
|
|
71
|
+
|
|
72
|
+
results = search_packages(query=query, tag=tag, domain=domain, index=index, pkg_type=pkg_type)
|
|
73
|
+
|
|
74
|
+
if not results:
|
|
75
|
+
if query:
|
|
76
|
+
console.print(f"[dim]No packages matching '{query}'.[/]")
|
|
77
|
+
else:
|
|
78
|
+
console.print("[dim]No packages found in registry.[/]")
|
|
79
|
+
return
|
|
80
|
+
|
|
81
|
+
total = len(results)
|
|
82
|
+
sorted_results = _sort_results(results, sort_by)
|
|
83
|
+
|
|
84
|
+
if limit is not None and limit > 0:
|
|
85
|
+
sorted_results = sorted_results[:limit]
|
|
86
|
+
|
|
87
|
+
table = Table(title="AES Registry")
|
|
88
|
+
table.add_column("Name", style="bold")
|
|
89
|
+
table.add_column("Type", style="cyan")
|
|
90
|
+
table.add_column("Latest")
|
|
91
|
+
table.add_column("Description")
|
|
92
|
+
table.add_column("Tags", style="dim")
|
|
93
|
+
if verbose:
|
|
94
|
+
table.add_column("Versions", style="dim")
|
|
95
|
+
table.add_column("Published", style="dim")
|
|
96
|
+
|
|
97
|
+
for pkg in sorted_results:
|
|
98
|
+
row = [
|
|
99
|
+
str(pkg["name"]),
|
|
100
|
+
str(pkg.get("type", "skill")),
|
|
101
|
+
str(pkg["latest"]),
|
|
102
|
+
str(pkg["description"]),
|
|
103
|
+
", ".join(str(t) for t in pkg.get("tags", [])),
|
|
104
|
+
]
|
|
105
|
+
if verbose:
|
|
106
|
+
row.append(str(pkg.get("version_count", len(pkg.get("versions", [])))))
|
|
107
|
+
published = pkg.get("latest_published_at", "?")
|
|
108
|
+
if isinstance(published, str) and "T" in published:
|
|
109
|
+
published = published.split("T")[0]
|
|
110
|
+
row.append(str(published))
|
|
111
|
+
table.add_row(*row)
|
|
112
|
+
|
|
113
|
+
console.print(table)
|
|
114
|
+
|
|
115
|
+
shown = len(sorted_results)
|
|
116
|
+
if limit is not None and shown < total:
|
|
117
|
+
console.print(f"\n[dim]{shown} of {total} package(s) shown (--limit {limit}).[/]")
|
|
118
|
+
else:
|
|
119
|
+
console.print(f"\n[dim]{total} package(s) found.[/]")
|
|
@@ -282,6 +282,11 @@ def search_packages(
|
|
|
282
282
|
if domain.lower() not in [t.lower() for t in pkg_tags]:
|
|
283
283
|
continue
|
|
284
284
|
|
|
285
|
+
# Compute latest_published_at from the latest version entry
|
|
286
|
+
latest_ver = pkg.get("latest", "")
|
|
287
|
+
latest_info = pkg.get("versions", {}).get(latest_ver, {})
|
|
288
|
+
latest_published_at = latest_info.get("published_at", "")
|
|
289
|
+
|
|
285
290
|
results.append({
|
|
286
291
|
"name": name,
|
|
287
292
|
"description": pkg.get("description", ""),
|
|
@@ -289,6 +294,8 @@ def search_packages(
|
|
|
289
294
|
"tags": pkg.get("tags", []),
|
|
290
295
|
"type": pkg.get("type", "skill"),
|
|
291
296
|
"versions": list(pkg.get("versions", {}).keys()),
|
|
297
|
+
"version_count": len(pkg.get("versions", {})),
|
|
298
|
+
"latest_published_at": latest_published_at,
|
|
292
299
|
})
|
|
293
300
|
|
|
294
301
|
return results
|