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.
Files changed (68) hide show
  1. {aes_cli-0.2.0 → aes_cli-0.3.0}/PKG-INFO +1 -1
  2. {aes_cli-0.2.0 → aes_cli-0.3.0}/aes/__init__.py +1 -1
  3. aes_cli-0.3.0/aes/commands/inspect.py +453 -0
  4. aes_cli-0.3.0/aes/commands/search.py +119 -0
  5. {aes_cli-0.2.0 → aes_cli-0.3.0}/aes/registry.py +7 -0
  6. {aes_cli-0.2.0 → aes_cli-0.3.0}/aes_cli.egg-info/PKG-INFO +1 -1
  7. {aes_cli-0.2.0 → aes_cli-0.3.0}/pyproject.toml +1 -1
  8. aes_cli-0.3.0/tests/test_inspect.py +309 -0
  9. {aes_cli-0.2.0 → aes_cli-0.3.0}/tests/test_search.py +75 -4
  10. aes_cli-0.2.0/aes/commands/inspect.py +0 -204
  11. aes_cli-0.2.0/aes/commands/search.py +0 -65
  12. aes_cli-0.2.0/tests/test_inspect.py +0 -54
  13. {aes_cli-0.2.0 → aes_cli-0.3.0}/README.md +0 -0
  14. {aes_cli-0.2.0 → aes_cli-0.3.0}/aes/__main__.py +0 -0
  15. {aes_cli-0.2.0 → aes_cli-0.3.0}/aes/analyzer.py +0 -0
  16. {aes_cli-0.2.0 → aes_cli-0.3.0}/aes/commands/__init__.py +0 -0
  17. {aes_cli-0.2.0 → aes_cli-0.3.0}/aes/commands/init.py +0 -0
  18. {aes_cli-0.2.0 → aes_cli-0.3.0}/aes/commands/install.py +0 -0
  19. {aes_cli-0.2.0 → aes_cli-0.3.0}/aes/commands/publish.py +0 -0
  20. {aes_cli-0.2.0 → aes_cli-0.3.0}/aes/commands/status.py +0 -0
  21. {aes_cli-0.2.0 → aes_cli-0.3.0}/aes/commands/sync.py +0 -0
  22. {aes_cli-0.2.0 → aes_cli-0.3.0}/aes/commands/validate.py +0 -0
  23. {aes_cli-0.2.0 → aes_cli-0.3.0}/aes/config.py +0 -0
  24. {aes_cli-0.2.0 → aes_cli-0.3.0}/aes/domains.py +0 -0
  25. {aes_cli-0.2.0 → aes_cli-0.3.0}/aes/frameworks.py +0 -0
  26. {aes_cli-0.2.0 → aes_cli-0.3.0}/aes/mcp_server.py +0 -0
  27. {aes_cli-0.2.0 → aes_cli-0.3.0}/aes/scaffold/agent.yaml.jinja +0 -0
  28. {aes_cli-0.2.0 → aes_cli-0.3.0}/aes/scaffold/agentignore.jinja +0 -0
  29. {aes_cli-0.2.0 → aes_cli-0.3.0}/aes/scaffold/instructions.md.jinja +0 -0
  30. {aes_cli-0.2.0 → aes_cli-0.3.0}/aes/scaffold/local.example.yaml.jinja +0 -0
  31. {aes_cli-0.2.0 → aes_cli-0.3.0}/aes/scaffold/local.yaml.jinja +0 -0
  32. {aes_cli-0.2.0 → aes_cli-0.3.0}/aes/scaffold/operations.md.jinja +0 -0
  33. {aes_cli-0.2.0 → aes_cli-0.3.0}/aes/scaffold/orchestrator.md.jinja +0 -0
  34. {aes_cli-0.2.0 → aes_cli-0.3.0}/aes/scaffold/permissions.yaml.jinja +0 -0
  35. {aes_cli-0.2.0 → aes_cli-0.3.0}/aes/scaffold/setup.md.jinja +0 -0
  36. {aes_cli-0.2.0 → aes_cli-0.3.0}/aes/scaffold/skill.md.jinja +0 -0
  37. {aes_cli-0.2.0 → aes_cli-0.3.0}/aes/scaffold/skill.yaml.jinja +0 -0
  38. {aes_cli-0.2.0 → aes_cli-0.3.0}/aes/scaffold/workflow.yaml.jinja +0 -0
  39. {aes_cli-0.2.0 → aes_cli-0.3.0}/aes/scaffold/workflow_command.md.jinja +0 -0
  40. {aes_cli-0.2.0 → aes_cli-0.3.0}/aes/schemas/agent.schema.json +0 -0
  41. {aes_cli-0.2.0 → aes_cli-0.3.0}/aes/schemas/permissions.schema.json +0 -0
  42. {aes_cli-0.2.0 → aes_cli-0.3.0}/aes/schemas/registry.schema.json +0 -0
  43. {aes_cli-0.2.0 → aes_cli-0.3.0}/aes/schemas/skill.schema.json +0 -0
  44. {aes_cli-0.2.0 → aes_cli-0.3.0}/aes/schemas/workflow.schema.json +0 -0
  45. {aes_cli-0.2.0 → aes_cli-0.3.0}/aes/targets/__init__.py +0 -0
  46. {aes_cli-0.2.0 → aes_cli-0.3.0}/aes/targets/_base.py +0 -0
  47. {aes_cli-0.2.0 → aes_cli-0.3.0}/aes/targets/_composer.py +0 -0
  48. {aes_cli-0.2.0 → aes_cli-0.3.0}/aes/targets/claude.py +0 -0
  49. {aes_cli-0.2.0 → aes_cli-0.3.0}/aes/targets/copilot.py +0 -0
  50. {aes_cli-0.2.0 → aes_cli-0.3.0}/aes/targets/cursor.py +0 -0
  51. {aes_cli-0.2.0 → aes_cli-0.3.0}/aes/targets/windsurf.py +0 -0
  52. {aes_cli-0.2.0 → aes_cli-0.3.0}/aes/validator.py +0 -0
  53. {aes_cli-0.2.0 → aes_cli-0.3.0}/aes_cli.egg-info/SOURCES.txt +0 -0
  54. {aes_cli-0.2.0 → aes_cli-0.3.0}/aes_cli.egg-info/dependency_links.txt +0 -0
  55. {aes_cli-0.2.0 → aes_cli-0.3.0}/aes_cli.egg-info/entry_points.txt +0 -0
  56. {aes_cli-0.2.0 → aes_cli-0.3.0}/aes_cli.egg-info/requires.txt +0 -0
  57. {aes_cli-0.2.0 → aes_cli-0.3.0}/aes_cli.egg-info/top_level.txt +0 -0
  58. {aes_cli-0.2.0 → aes_cli-0.3.0}/setup.cfg +0 -0
  59. {aes_cli-0.2.0 → aes_cli-0.3.0}/tests/test_analyzer.py +0 -0
  60. {aes_cli-0.2.0 → aes_cli-0.3.0}/tests/test_frameworks.py +0 -0
  61. {aes_cli-0.2.0 → aes_cli-0.3.0}/tests/test_init.py +0 -0
  62. {aes_cli-0.2.0 → aes_cli-0.3.0}/tests/test_install.py +0 -0
  63. {aes_cli-0.2.0 → aes_cli-0.3.0}/tests/test_mcp_server.py +0 -0
  64. {aes_cli-0.2.0 → aes_cli-0.3.0}/tests/test_publish.py +0 -0
  65. {aes_cli-0.2.0 → aes_cli-0.3.0}/tests/test_registry.py +0 -0
  66. {aes_cli-0.2.0 → aes_cli-0.3.0}/tests/test_status.py +0 -0
  67. {aes_cli-0.2.0 → aes_cli-0.3.0}/tests/test_sync.py +0 -0
  68. {aes_cli-0.2.0 → aes_cli-0.3.0}/tests/test_validate.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aes-cli
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: CLI tool for the Agentic Engineering Standard
5
5
  Author: Hiro
6
6
  License: Apache-2.0
@@ -2,4 +2,4 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- __version__ = "0.2.0"
5
+ __version__ = "0.3.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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aes-cli
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: CLI tool for the Agentic Engineering Standard
5
5
  Author: Hiro
6
6
  License: Apache-2.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "aes-cli"
7
- version = "0.2.0"
7
+ version = "0.3.0"
8
8
  description = "CLI tool for the Agentic Engineering Standard"
9
9
  readme = "README.md"
10
10
  license = {text = "Apache-2.0"}