aes-cli 0.2.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.
- aes/__init__.py +5 -0
- aes/__main__.py +37 -0
- aes/analyzer.py +487 -0
- aes/commands/__init__.py +0 -0
- aes/commands/init.py +727 -0
- aes/commands/inspect.py +204 -0
- aes/commands/install.py +379 -0
- aes/commands/publish.py +432 -0
- aes/commands/search.py +65 -0
- aes/commands/status.py +153 -0
- aes/commands/sync.py +413 -0
- aes/commands/validate.py +77 -0
- aes/config.py +43 -0
- aes/domains.py +1382 -0
- aes/frameworks.py +522 -0
- aes/mcp_server.py +213 -0
- aes/registry.py +294 -0
- aes/scaffold/agent.yaml.jinja +135 -0
- aes/scaffold/agentignore.jinja +61 -0
- aes/scaffold/instructions.md.jinja +311 -0
- aes/scaffold/local.example.yaml.jinja +35 -0
- aes/scaffold/local.yaml.jinja +29 -0
- aes/scaffold/operations.md.jinja +33 -0
- aes/scaffold/orchestrator.md.jinja +95 -0
- aes/scaffold/permissions.yaml.jinja +151 -0
- aes/scaffold/setup.md.jinja +244 -0
- aes/scaffold/skill.md.jinja +27 -0
- aes/scaffold/skill.yaml.jinja +175 -0
- aes/scaffold/workflow.yaml.jinja +44 -0
- aes/scaffold/workflow_command.md.jinja +48 -0
- aes/schemas/agent.schema.json +188 -0
- aes/schemas/permissions.schema.json +100 -0
- aes/schemas/registry.schema.json +72 -0
- aes/schemas/skill.schema.json +209 -0
- aes/schemas/workflow.schema.json +92 -0
- aes/targets/__init__.py +29 -0
- aes/targets/_base.py +77 -0
- aes/targets/_composer.py +338 -0
- aes/targets/claude.py +153 -0
- aes/targets/copilot.py +48 -0
- aes/targets/cursor.py +46 -0
- aes/targets/windsurf.py +46 -0
- aes/validator.py +394 -0
- aes_cli-0.2.0.dist-info/METADATA +110 -0
- aes_cli-0.2.0.dist-info/RECORD +48 -0
- aes_cli-0.2.0.dist-info/WHEEL +5 -0
- aes_cli-0.2.0.dist-info/entry_points.txt +3 -0
- aes_cli-0.2.0.dist-info/top_level.txt +1 -0
aes/commands/init.py
ADDED
|
@@ -0,0 +1,727 @@
|
|
|
1
|
+
"""aes init — Scaffold a .agent/ directory."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
import shutil
|
|
8
|
+
import sys
|
|
9
|
+
import tarfile
|
|
10
|
+
import tempfile
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Dict, List, Optional
|
|
13
|
+
|
|
14
|
+
import click
|
|
15
|
+
from jinja2 import Environment, FileSystemLoader
|
|
16
|
+
from rich.console import Console
|
|
17
|
+
from rich.panel import Panel
|
|
18
|
+
from rich.tree import Tree
|
|
19
|
+
|
|
20
|
+
from aes.config import (
|
|
21
|
+
AGENT_DIR,
|
|
22
|
+
AGENTIGNORE_FILE,
|
|
23
|
+
LOCAL_EXAMPLE_FILE,
|
|
24
|
+
LOCAL_FILE,
|
|
25
|
+
SCAFFOLD_DIR,
|
|
26
|
+
SKILLS_DIR,
|
|
27
|
+
REGISTRY_DIR,
|
|
28
|
+
WORKFLOWS_DIR,
|
|
29
|
+
COMMANDS_DIR,
|
|
30
|
+
MEMORY_DIR,
|
|
31
|
+
OVERRIDES_DIR,
|
|
32
|
+
)
|
|
33
|
+
from aes.commands.install import _safe_extract
|
|
34
|
+
from aes.commands.sync import run_sync
|
|
35
|
+
from aes.domains import AGENT_INTEGRATED_BASE_CONFIG, DEV_ASSIST_BASE_CONFIG, DOMAIN_CONFIGS
|
|
36
|
+
from aes.analyzer import analyze_project, ProjectAnalysis
|
|
37
|
+
from aes.frameworks import resolve_config
|
|
38
|
+
|
|
39
|
+
console = Console()
|
|
40
|
+
|
|
41
|
+
MCP_CONFIG_FILE = ".mcp.json"
|
|
42
|
+
|
|
43
|
+
_MCP_CONFIG = {
|
|
44
|
+
"mcpServers": {
|
|
45
|
+
"aes-registry": {
|
|
46
|
+
"command": "aes-mcp",
|
|
47
|
+
"args": [],
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _write_mcp_config(project_root: Path) -> bool:
|
|
54
|
+
"""Write .mcp.json if it doesn't already exist. Returns True if written."""
|
|
55
|
+
mcp_path = project_root / MCP_CONFIG_FILE
|
|
56
|
+
if mcp_path.exists():
|
|
57
|
+
return False
|
|
58
|
+
mcp_path.write_text(json.dumps(_MCP_CONFIG, indent=2) + "\n")
|
|
59
|
+
return True
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# ---------------------------------------------------------------------------
|
|
63
|
+
# Auto-detection helpers (kept for backward compat with explicit --language)
|
|
64
|
+
# ---------------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
_LANGUAGE_MARKERS = [
|
|
67
|
+
("python", ["pyproject.toml", "setup.py", "setup.cfg", "requirements.txt", "Pipfile"]),
|
|
68
|
+
("typescript", ["tsconfig.json"]),
|
|
69
|
+
("javascript", ["package.json"]),
|
|
70
|
+
("go", ["go.mod"]),
|
|
71
|
+
("rust", ["Cargo.toml"]),
|
|
72
|
+
("java", ["pom.xml", "build.gradle", "build.gradle.kts"]),
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _detect_language(project_root: Path) -> str:
|
|
77
|
+
"""Auto-detect the primary language from marker files in *project_root*."""
|
|
78
|
+
for language, markers in _LANGUAGE_MARKERS:
|
|
79
|
+
for marker in markers:
|
|
80
|
+
if (project_root / marker).exists():
|
|
81
|
+
return language
|
|
82
|
+
return "other"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _detect_name(project_root: Path) -> str:
|
|
86
|
+
"""Derive a kebab-case project name from the directory name."""
|
|
87
|
+
raw = project_root.name
|
|
88
|
+
kebab = re.sub(r"[^a-z0-9]+", "-", raw.lower()).strip("-")
|
|
89
|
+
return kebab or "my-project"
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _render_template(env: Environment, template_name: str, context: dict) -> str:
|
|
93
|
+
"""Render a Jinja2 template with context."""
|
|
94
|
+
tmpl = env.get_template(template_name)
|
|
95
|
+
return tmpl.render(**context)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _init_from_registry(source: str, project_root: Path) -> None:
|
|
99
|
+
"""Initialize a project from a registry template.
|
|
100
|
+
|
|
101
|
+
*source* is a registry reference like ``aes-hub/ml-pipeline@^2.0``.
|
|
102
|
+
Downloads the template tarball and extracts its ``.agent/`` directory.
|
|
103
|
+
"""
|
|
104
|
+
from aes.registry import (
|
|
105
|
+
parse_registry_source,
|
|
106
|
+
fetch_index,
|
|
107
|
+
resolve_version,
|
|
108
|
+
download_package,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
name, version_spec = parse_registry_source(source)
|
|
112
|
+
|
|
113
|
+
# Fetch index and resolve version
|
|
114
|
+
try:
|
|
115
|
+
index = fetch_index()
|
|
116
|
+
except Exception as exc:
|
|
117
|
+
raise click.ClickException(f"Failed to fetch registry: {exc}")
|
|
118
|
+
|
|
119
|
+
pkg = index.get("packages", {}).get(name)
|
|
120
|
+
if not pkg:
|
|
121
|
+
raise click.ClickException(f"Package '{name}' not found in registry")
|
|
122
|
+
|
|
123
|
+
available = list(pkg.get("versions", {}).keys())
|
|
124
|
+
version = resolve_version(version_spec, available)
|
|
125
|
+
if not version:
|
|
126
|
+
raise click.ClickException(
|
|
127
|
+
f"No version of '{name}' matching '{version_spec}'. "
|
|
128
|
+
f"Available: {', '.join(available)}"
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
ver_info = pkg["versions"][version]
|
|
132
|
+
|
|
133
|
+
# Check for existing .agent/ directory
|
|
134
|
+
agent_dir = project_root / AGENT_DIR
|
|
135
|
+
if agent_dir.exists():
|
|
136
|
+
console.print(f"[yellow]Warning:[/] {AGENT_DIR}/ already exists at {project_root}")
|
|
137
|
+
if not click.confirm("Overwrite existing files?", default=False):
|
|
138
|
+
raise SystemExit(1)
|
|
139
|
+
|
|
140
|
+
# Download and extract
|
|
141
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
142
|
+
tmp_path = Path(tmp)
|
|
143
|
+
tarball = download_package(
|
|
144
|
+
name, version, ver_info["sha256"], tmp_path,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
with tarfile.open(tarball, "r:gz") as tar:
|
|
148
|
+
_safe_extract(tar, tmp_path)
|
|
149
|
+
|
|
150
|
+
# Find .agent/ directory inside extracted content
|
|
151
|
+
extracted_agent = None
|
|
152
|
+
for candidate in tmp_path.rglob(AGENT_DIR):
|
|
153
|
+
if candidate.is_dir() and (candidate / "agent.yaml").exists():
|
|
154
|
+
extracted_agent = candidate
|
|
155
|
+
break
|
|
156
|
+
|
|
157
|
+
if extracted_agent is None:
|
|
158
|
+
raise click.ClickException(
|
|
159
|
+
f"Downloaded package '{name}@{version}' does not contain "
|
|
160
|
+
f"a {AGENT_DIR}/ directory with agent.yaml"
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
# Copy to project root
|
|
164
|
+
if agent_dir.exists():
|
|
165
|
+
shutil.rmtree(agent_dir)
|
|
166
|
+
shutil.copytree(extracted_agent, agent_dir, symlinks=False)
|
|
167
|
+
|
|
168
|
+
# Auto-sync
|
|
169
|
+
synced_files = run_sync(project_root, force=True, quiet=True)
|
|
170
|
+
mcp_written = _write_mcp_config(project_root)
|
|
171
|
+
|
|
172
|
+
console.print()
|
|
173
|
+
console.print(f"[green]Initialized from template:[/] {name}@{version}")
|
|
174
|
+
console.print(f" Source: {source}")
|
|
175
|
+
console.print(f" Installed to: {agent_dir}")
|
|
176
|
+
if synced_files > 0:
|
|
177
|
+
console.print(f" Synced to {synced_files} tool-specific config file(s).")
|
|
178
|
+
if mcp_written:
|
|
179
|
+
console.print(f" Created {MCP_CONFIG_FILE} (AES registry MCP server)")
|
|
180
|
+
console.print()
|
|
181
|
+
console.print("[dim]Done! Start a new agent session to use the template.[/]")
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _init_from_tarball(tarball_path: Path, project_root: Path) -> None:
|
|
185
|
+
"""Initialize a project from a local template tarball."""
|
|
186
|
+
agent_dir = project_root / AGENT_DIR
|
|
187
|
+
|
|
188
|
+
if agent_dir.exists():
|
|
189
|
+
console.print(f"[yellow]Warning:[/] {AGENT_DIR}/ already exists at {project_root}")
|
|
190
|
+
if not click.confirm("Overwrite existing files?", default=False):
|
|
191
|
+
raise SystemExit(1)
|
|
192
|
+
|
|
193
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
194
|
+
tmp_path = Path(tmp)
|
|
195
|
+
with tarfile.open(tarball_path, "r:gz") as tar:
|
|
196
|
+
_safe_extract(tar, tmp_path)
|
|
197
|
+
|
|
198
|
+
# Find .agent/ directory inside extracted content
|
|
199
|
+
extracted_agent = None
|
|
200
|
+
for candidate in tmp_path.rglob(AGENT_DIR):
|
|
201
|
+
if candidate.is_dir() and (candidate / "agent.yaml").exists():
|
|
202
|
+
extracted_agent = candidate
|
|
203
|
+
break
|
|
204
|
+
|
|
205
|
+
if extracted_agent is None:
|
|
206
|
+
raise click.ClickException(
|
|
207
|
+
f"Tarball does not contain a {AGENT_DIR}/ directory with agent.yaml"
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
if agent_dir.exists():
|
|
211
|
+
shutil.rmtree(agent_dir)
|
|
212
|
+
shutil.copytree(extracted_agent, agent_dir, symlinks=False)
|
|
213
|
+
|
|
214
|
+
synced_files = run_sync(project_root, force=True, quiet=True)
|
|
215
|
+
mcp_written = _write_mcp_config(project_root)
|
|
216
|
+
|
|
217
|
+
console.print()
|
|
218
|
+
console.print(f"[green]Initialized from template:[/] {tarball_path.name}")
|
|
219
|
+
console.print(f" Installed to: {agent_dir}")
|
|
220
|
+
if synced_files > 0:
|
|
221
|
+
console.print(f" Synced to {synced_files} tool-specific config file(s).")
|
|
222
|
+
if mcp_written:
|
|
223
|
+
console.print(f" Created {MCP_CONFIG_FILE} (AES registry MCP server)")
|
|
224
|
+
console.print()
|
|
225
|
+
console.print("[dim]Done! Start a new agent session to use the template.[/]")
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
# ---------------------------------------------------------------------------
|
|
229
|
+
# Interactive picker (when nothing detected)
|
|
230
|
+
# ---------------------------------------------------------------------------
|
|
231
|
+
|
|
232
|
+
_MODE_CHOICES = [
|
|
233
|
+
("Dev-Assist — Agent builds the project, then steps back", "dev-assist"),
|
|
234
|
+
("Agent-Integrated — Agent is embedded in the running product", "agent-integrated"),
|
|
235
|
+
]
|
|
236
|
+
|
|
237
|
+
_DEV_ASSIST_TYPES = [
|
|
238
|
+
("API service", "api"),
|
|
239
|
+
("Web app", "fullstack"),
|
|
240
|
+
("CLI tool", "cli-tool"),
|
|
241
|
+
("Library / Package", "library"),
|
|
242
|
+
("DevOps / Infra", "devops"),
|
|
243
|
+
("Skip", "other"),
|
|
244
|
+
]
|
|
245
|
+
|
|
246
|
+
_AGENT_INTEGRATED_TYPES = [
|
|
247
|
+
("ML pipeline", "ml"),
|
|
248
|
+
("Research / Content pipeline", "research"),
|
|
249
|
+
("Custom", "other"),
|
|
250
|
+
]
|
|
251
|
+
|
|
252
|
+
_LANGUAGE_CHOICES = ["python", "typescript", "javascript", "go", "rust", "java"]
|
|
253
|
+
|
|
254
|
+
# project_type + language -> list of framework labels (from FRAMEWORK_OVERLAYS keys)
|
|
255
|
+
_FRAMEWORK_PICKER: Dict[tuple, List[str]] = {
|
|
256
|
+
("api", "python"): ["fastapi", "django", "flask"],
|
|
257
|
+
("api", "typescript"): ["express"],
|
|
258
|
+
("api", "javascript"): ["express"],
|
|
259
|
+
("api", "go"): [], # gin/fiber/echo not in overlays yet
|
|
260
|
+
("api", "rust"): [], # actix/rocket/axum not in overlays yet
|
|
261
|
+
("fullstack", "typescript"): ["nextjs", "react"],
|
|
262
|
+
("fullstack", "javascript"): ["nextjs", "react"],
|
|
263
|
+
("web-frontend", "typescript"): ["nextjs", "react"],
|
|
264
|
+
("web-frontend", "javascript"): ["nextjs", "react"],
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _interactive_pick(analysis: ProjectAnalysis) -> tuple:
|
|
269
|
+
"""Show a two-step interactive picker when nothing was auto-detected.
|
|
270
|
+
|
|
271
|
+
Step 1: Choose mode (dev-assist vs agent-integrated).
|
|
272
|
+
Step 2: Choose project type within the selected mode.
|
|
273
|
+
|
|
274
|
+
Returns ``(project_type, language, frameworks, mode)`` chosen by the user.
|
|
275
|
+
"""
|
|
276
|
+
console.print()
|
|
277
|
+
console.print("[bold]How will the agent work with this project?[/]\n")
|
|
278
|
+
|
|
279
|
+
# --- Step 1: Mode ---
|
|
280
|
+
for i, (label, _) in enumerate(_MODE_CHOICES, 1):
|
|
281
|
+
console.print(f" [bold cyan][{i}][/] {label}")
|
|
282
|
+
console.print()
|
|
283
|
+
|
|
284
|
+
mode_idx = click.prompt(
|
|
285
|
+
"Choice",
|
|
286
|
+
type=click.IntRange(1, len(_MODE_CHOICES)),
|
|
287
|
+
default=1,
|
|
288
|
+
)
|
|
289
|
+
_, chosen_mode = _MODE_CHOICES[mode_idx - 1]
|
|
290
|
+
|
|
291
|
+
# --- Step 2: Project type based on mode ---
|
|
292
|
+
type_choices = _DEV_ASSIST_TYPES if chosen_mode == "dev-assist" else _AGENT_INTEGRATED_TYPES
|
|
293
|
+
|
|
294
|
+
console.print()
|
|
295
|
+
console.print("[bold]What type of project?[/]\n")
|
|
296
|
+
for i, (label, _) in enumerate(type_choices, 1):
|
|
297
|
+
console.print(f" [bold cyan][{i}][/] {label}")
|
|
298
|
+
console.print()
|
|
299
|
+
|
|
300
|
+
type_idx = click.prompt(
|
|
301
|
+
"Choice",
|
|
302
|
+
type=click.IntRange(1, len(type_choices)),
|
|
303
|
+
default=len(type_choices),
|
|
304
|
+
)
|
|
305
|
+
chosen_label, chosen_type = type_choices[type_idx - 1]
|
|
306
|
+
|
|
307
|
+
if chosen_type == "other":
|
|
308
|
+
return ("other", analysis.language, [], chosen_mode)
|
|
309
|
+
|
|
310
|
+
# Domain configs (ml, web, devops, research) skip language/framework
|
|
311
|
+
if chosen_type in DOMAIN_CONFIGS:
|
|
312
|
+
lang = analysis.language if analysis.language != "other" else "python"
|
|
313
|
+
return (chosen_type, lang, [], chosen_mode)
|
|
314
|
+
|
|
315
|
+
# --- Language ---
|
|
316
|
+
console.print()
|
|
317
|
+
console.print("[bold]Language?[/]\n")
|
|
318
|
+
for i, lang in enumerate(_LANGUAGE_CHOICES, 1):
|
|
319
|
+
console.print(f" [bold cyan][{i}][/] {lang.title()}")
|
|
320
|
+
console.print()
|
|
321
|
+
|
|
322
|
+
lang_idx = click.prompt(
|
|
323
|
+
"Choice",
|
|
324
|
+
type=click.IntRange(1, len(_LANGUAGE_CHOICES)),
|
|
325
|
+
default=1,
|
|
326
|
+
)
|
|
327
|
+
chosen_lang = _LANGUAGE_CHOICES[lang_idx - 1]
|
|
328
|
+
|
|
329
|
+
# --- Framework (optional) ---
|
|
330
|
+
fw_options = _FRAMEWORK_PICKER.get((chosen_type, chosen_lang), [])
|
|
331
|
+
chosen_frameworks: List[str] = []
|
|
332
|
+
|
|
333
|
+
if fw_options:
|
|
334
|
+
console.print()
|
|
335
|
+
console.print("[bold]Framework?[/] (optional, Enter to skip)\n")
|
|
336
|
+
for i, fw in enumerate(fw_options, 1):
|
|
337
|
+
console.print(f" [bold cyan][{i}][/] {fw.title()}")
|
|
338
|
+
console.print(f" [bold cyan][{len(fw_options) + 1}][/] None")
|
|
339
|
+
console.print()
|
|
340
|
+
|
|
341
|
+
fw_idx = click.prompt(
|
|
342
|
+
"Choice",
|
|
343
|
+
type=click.IntRange(1, len(fw_options) + 1),
|
|
344
|
+
default=len(fw_options) + 1,
|
|
345
|
+
)
|
|
346
|
+
if fw_idx <= len(fw_options):
|
|
347
|
+
chosen_frameworks = [fw_options[fw_idx - 1]]
|
|
348
|
+
|
|
349
|
+
return (chosen_type, chosen_lang, chosen_frameworks, chosen_mode)
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def _format_detection_summary(analysis: ProjectAnalysis) -> str:
|
|
353
|
+
"""Build a detection summary string for display."""
|
|
354
|
+
lines = []
|
|
355
|
+
lines.append(f" Language: [cyan]{analysis.language}[/]")
|
|
356
|
+
if analysis.frameworks:
|
|
357
|
+
fw_str = " + ".join(analysis.frameworks)
|
|
358
|
+
lines.append(f" Framework: [cyan]{fw_str}[/]")
|
|
359
|
+
lines.append(f" Type: [cyan]{analysis.project_type}[/]")
|
|
360
|
+
if analysis.has_tests:
|
|
361
|
+
cmd_hint = f" ({analysis.test_command})" if analysis.test_command else ""
|
|
362
|
+
lines.append(f" Tests: [green]found[/]{cmd_hint}")
|
|
363
|
+
if analysis.has_ci:
|
|
364
|
+
lines.append(f" CI/CD: [green]found[/]")
|
|
365
|
+
if analysis.has_docker:
|
|
366
|
+
lines.append(f" Docker: [green]found[/]")
|
|
367
|
+
if analysis.has_database:
|
|
368
|
+
lines.append(f" Database: [green]migrations found[/]")
|
|
369
|
+
if analysis.existing_agent_configs:
|
|
370
|
+
tools = ", ".join(analysis.existing_agent_configs.keys())
|
|
371
|
+
lines.append(f" Existing: [yellow]{tools}[/]")
|
|
372
|
+
return "\n".join(lines)
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def _print_post_init_summary(
|
|
376
|
+
project_root: Path,
|
|
377
|
+
name: str,
|
|
378
|
+
project_type: str,
|
|
379
|
+
language: str,
|
|
380
|
+
domain_config: object,
|
|
381
|
+
skills: bool,
|
|
382
|
+
workflows: bool,
|
|
383
|
+
registry: bool,
|
|
384
|
+
synced_files: int,
|
|
385
|
+
) -> None:
|
|
386
|
+
"""Print a rich post-init summary."""
|
|
387
|
+
from aes.domains import DomainConfig
|
|
388
|
+
|
|
389
|
+
# Header
|
|
390
|
+
type_label = project_type.replace("-", " ").title()
|
|
391
|
+
console.print()
|
|
392
|
+
console.print(Panel(
|
|
393
|
+
f"[bold green]AES Initialized:[/] {name}\n"
|
|
394
|
+
f"[dim]{type_label} ({language})[/]",
|
|
395
|
+
expand=False,
|
|
396
|
+
))
|
|
397
|
+
|
|
398
|
+
# File tree
|
|
399
|
+
tree = Tree(f"[bold].agent/[/]")
|
|
400
|
+
tree.add("agent.yaml")
|
|
401
|
+
tree.add(f"instructions.md [dim]({type_label}-specific)[/]")
|
|
402
|
+
tree.add("permissions.yaml")
|
|
403
|
+
|
|
404
|
+
if skills:
|
|
405
|
+
skills_branch = tree.add("skills/")
|
|
406
|
+
skills_branch.add("ORCHESTRATOR.md")
|
|
407
|
+
if isinstance(domain_config, DomainConfig):
|
|
408
|
+
for skill_def in domain_config.skills:
|
|
409
|
+
skills_branch.add(f"{skill_def.id} [dim]{skill_def.description}[/]")
|
|
410
|
+
|
|
411
|
+
if workflows and isinstance(domain_config, DomainConfig) and domain_config.workflow:
|
|
412
|
+
wf_branch = tree.add("workflows/")
|
|
413
|
+
wf_branch.add(f"{domain_config.workflow.id}.yaml")
|
|
414
|
+
|
|
415
|
+
if registry:
|
|
416
|
+
tree.add("registry/")
|
|
417
|
+
|
|
418
|
+
cmd_branch = tree.add("commands/")
|
|
419
|
+
cmd_branch.add("setup.md")
|
|
420
|
+
if isinstance(domain_config, DomainConfig) and domain_config.workflow_commands:
|
|
421
|
+
for cmd_def in domain_config.workflow_commands:
|
|
422
|
+
cmd_branch.add(f"{cmd_def.id}.md [dim]{cmd_def.trigger}[/]")
|
|
423
|
+
|
|
424
|
+
mem_branch = tree.add("memory/")
|
|
425
|
+
mem_branch.add("project.md")
|
|
426
|
+
if isinstance(domain_config, DomainConfig) and domain_config.workflow_commands:
|
|
427
|
+
mem_branch.add("operations.md [dim](per-command activity log)[/]")
|
|
428
|
+
|
|
429
|
+
console.print(tree)
|
|
430
|
+
|
|
431
|
+
# Sync summary
|
|
432
|
+
if synced_files > 0:
|
|
433
|
+
console.print()
|
|
434
|
+
console.print(f"[green]Synced to {synced_files} tool config(s):[/]")
|
|
435
|
+
sync_targets = [
|
|
436
|
+
("Claude Code", "CLAUDE.md"),
|
|
437
|
+
("Cursor", ".cursorrules"),
|
|
438
|
+
("Copilot", ".github/copilot-instructions.md"),
|
|
439
|
+
("Windsurf", ".windsurfrules"),
|
|
440
|
+
]
|
|
441
|
+
for tool_name, file_name in sync_targets:
|
|
442
|
+
if (project_root / file_name).exists():
|
|
443
|
+
console.print(f" {tool_name:12s} -> {file_name}")
|
|
444
|
+
|
|
445
|
+
# MCP info
|
|
446
|
+
mcp_path = project_root / MCP_CONFIG_FILE
|
|
447
|
+
if mcp_path.exists():
|
|
448
|
+
console.print()
|
|
449
|
+
console.print(f"[green]MCP:[/] {MCP_CONFIG_FILE} configured (AES registry tools)")
|
|
450
|
+
console.print(" [dim]Install MCP server: pip install aes-cli[mcp][/]")
|
|
451
|
+
|
|
452
|
+
# Next steps
|
|
453
|
+
console.print()
|
|
454
|
+
workflow_hint = ""
|
|
455
|
+
if isinstance(domain_config, DomainConfig) and domain_config.workflow_commands:
|
|
456
|
+
trigger = domain_config.workflow_commands[0].trigger
|
|
457
|
+
workflow_hint = f", or {trigger} to begin"
|
|
458
|
+
console.print(f"[dim]Next: Start a new agent session, then type /setup to fine-tune{workflow_hint}.[/]")
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
@click.command("init")
|
|
462
|
+
@click.option("--name", default=None, help="Project name (kebab-case). Default: directory name.")
|
|
463
|
+
@click.option("--domain", default=None, help="Project domain (ml, web, devops, etc.). Default: auto-detected.")
|
|
464
|
+
@click.option("--language", default=None, help="Primary language. Default: auto-detected from files.")
|
|
465
|
+
@click.option("--skills/--no-skills", default=True)
|
|
466
|
+
@click.option("--workflows/--no-workflows", default=True)
|
|
467
|
+
@click.option("--registry/--no-registry", default=False)
|
|
468
|
+
@click.option("--path", default=".", type=click.Path(exists=True), help="Project root directory")
|
|
469
|
+
@click.option("--from", "from_registry", default=None, help="Initialize from a registry template (e.g. aes-hub/ml-pipeline@^2.0) or local tarball")
|
|
470
|
+
def init_cmd(
|
|
471
|
+
name: Optional[str],
|
|
472
|
+
domain: Optional[str],
|
|
473
|
+
language: Optional[str],
|
|
474
|
+
skills: bool,
|
|
475
|
+
workflows: bool,
|
|
476
|
+
registry: bool,
|
|
477
|
+
path: str,
|
|
478
|
+
from_registry: Optional[str],
|
|
479
|
+
) -> None:
|
|
480
|
+
"""Scaffold a .agent/ directory for your project.
|
|
481
|
+
|
|
482
|
+
All flags are optional. Without flags, the project is analyzed to detect
|
|
483
|
+
language, frameworks, and project type. The generated .agent/ is tailored
|
|
484
|
+
to the detected stack.
|
|
485
|
+
|
|
486
|
+
Use --domain ml/web/devops to use a pre-built domain config instead of
|
|
487
|
+
auto-detection. Use --from for registry templates or local tarballs.
|
|
488
|
+
|
|
489
|
+
\b
|
|
490
|
+
Examples:
|
|
491
|
+
aes init # zero-arg, auto-detect everything
|
|
492
|
+
aes init --name my-app --domain ml # explicit name and domain
|
|
493
|
+
aes init --language python --no-workflows # override auto-detect
|
|
494
|
+
aes init --from aes-hub/ml-pipeline@^2.0 # from registry template
|
|
495
|
+
aes init --from ./template.tar.gz # from local tarball
|
|
496
|
+
"""
|
|
497
|
+
project_root = Path(path).resolve()
|
|
498
|
+
|
|
499
|
+
# --from: initialize from registry template or local tarball
|
|
500
|
+
if from_registry:
|
|
501
|
+
source_path = Path(from_registry)
|
|
502
|
+
if source_path.exists() and source_path.suffix == ".gz":
|
|
503
|
+
_init_from_tarball(source_path, project_root)
|
|
504
|
+
else:
|
|
505
|
+
_init_from_registry(from_registry, project_root)
|
|
506
|
+
return
|
|
507
|
+
|
|
508
|
+
agent_dir = project_root / AGENT_DIR
|
|
509
|
+
|
|
510
|
+
# --- Smart detection mode ---
|
|
511
|
+
# When no --domain is given, analyze the project
|
|
512
|
+
analysis: Optional[ProjectAnalysis] = None
|
|
513
|
+
detected_domain_config = None
|
|
514
|
+
|
|
515
|
+
if domain is None:
|
|
516
|
+
analysis = analyze_project(project_root)
|
|
517
|
+
|
|
518
|
+
# Use analysis for defaults
|
|
519
|
+
if name is None:
|
|
520
|
+
name = analysis.name
|
|
521
|
+
if language is None:
|
|
522
|
+
language = analysis.language
|
|
523
|
+
|
|
524
|
+
# Try to resolve a framework-aware config
|
|
525
|
+
detected_domain_config = resolve_config(
|
|
526
|
+
project_type=analysis.project_type,
|
|
527
|
+
frameworks=analysis.frameworks,
|
|
528
|
+
language=language,
|
|
529
|
+
test_command=analysis.test_command,
|
|
530
|
+
build_command=analysis.build_command,
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
# Interactive mode: show what we found and confirm
|
|
534
|
+
is_interactive = sys.stdin.isatty() and not any(
|
|
535
|
+
x in sys.argv for x in ("--name", "--language")
|
|
536
|
+
)
|
|
537
|
+
if is_interactive and (analysis.frameworks or analysis.project_type != "other"):
|
|
538
|
+
console.print()
|
|
539
|
+
console.print(Panel(
|
|
540
|
+
f"[bold]Detected:[/]\n{_format_detection_summary(analysis)}",
|
|
541
|
+
title="Project Analysis",
|
|
542
|
+
expand=False,
|
|
543
|
+
))
|
|
544
|
+
|
|
545
|
+
type_label = analysis.project_type.replace("-", " ").title()
|
|
546
|
+
fw_str = ""
|
|
547
|
+
if analysis.frameworks:
|
|
548
|
+
fw_str = " + ".join(f.title() for f in analysis.frameworks) + " "
|
|
549
|
+
if not click.confirm(
|
|
550
|
+
f"\nGenerate .agent/ for {fw_str}{type_label}?",
|
|
551
|
+
default=True,
|
|
552
|
+
):
|
|
553
|
+
raise SystemExit(0)
|
|
554
|
+
|
|
555
|
+
# Offer to import existing agent configs
|
|
556
|
+
if analysis.existing_agent_configs:
|
|
557
|
+
console.print()
|
|
558
|
+
for tool, cfg_path in analysis.existing_agent_configs.items():
|
|
559
|
+
size = cfg_path.stat().st_size
|
|
560
|
+
console.print(f" Found existing: [yellow]{cfg_path.name}[/] ({size / 1024:.1f} KB)")
|
|
561
|
+
|
|
562
|
+
elif is_interactive:
|
|
563
|
+
# Nothing detected — show interactive picker
|
|
564
|
+
picked_type, picked_lang, picked_frameworks, picked_mode = _interactive_pick(analysis)
|
|
565
|
+
|
|
566
|
+
language = picked_lang
|
|
567
|
+
if picked_type in DOMAIN_CONFIGS:
|
|
568
|
+
detected_domain_config = DOMAIN_CONFIGS.get(picked_type)
|
|
569
|
+
elif picked_type != "other":
|
|
570
|
+
detected_domain_config = resolve_config(
|
|
571
|
+
project_type=picked_type,
|
|
572
|
+
frameworks=picked_frameworks,
|
|
573
|
+
language=picked_lang,
|
|
574
|
+
)
|
|
575
|
+
elif picked_mode == "agent-integrated":
|
|
576
|
+
detected_domain_config = AGENT_INTEGRATED_BASE_CONFIG
|
|
577
|
+
else:
|
|
578
|
+
detected_domain_config = DEV_ASSIST_BASE_CONFIG
|
|
579
|
+
# Update analysis so post-init summary is correct
|
|
580
|
+
analysis = ProjectAnalysis(
|
|
581
|
+
name=analysis.name,
|
|
582
|
+
language=picked_lang,
|
|
583
|
+
frameworks=picked_frameworks,
|
|
584
|
+
project_type=picked_type,
|
|
585
|
+
)
|
|
586
|
+
|
|
587
|
+
# Fall into "other" domain handling for the template context
|
|
588
|
+
domain = analysis.project_type if detected_domain_config else "other"
|
|
589
|
+
else:
|
|
590
|
+
# Explicit --domain: use legacy behavior
|
|
591
|
+
if name is None:
|
|
592
|
+
name = _detect_name(project_root)
|
|
593
|
+
if language is None:
|
|
594
|
+
language = _detect_language(project_root)
|
|
595
|
+
|
|
596
|
+
if agent_dir.exists():
|
|
597
|
+
console.print(f"[yellow]Warning:[/] {AGENT_DIR}/ already exists at {project_root}")
|
|
598
|
+
if not click.confirm("Overwrite existing files?", default=False):
|
|
599
|
+
raise SystemExit(1)
|
|
600
|
+
|
|
601
|
+
# Look up domain config: framework-resolved > explicit domain > None
|
|
602
|
+
domain_config = detected_domain_config or DOMAIN_CONFIGS.get(domain) or DEV_ASSIST_BASE_CONFIG
|
|
603
|
+
|
|
604
|
+
# Create directory structure
|
|
605
|
+
agent_dir.mkdir(exist_ok=True)
|
|
606
|
+
(agent_dir / MEMORY_DIR).mkdir(exist_ok=True)
|
|
607
|
+
(agent_dir / MEMORY_DIR / "sessions").mkdir(exist_ok=True)
|
|
608
|
+
(agent_dir / OVERRIDES_DIR).mkdir(exist_ok=True)
|
|
609
|
+
|
|
610
|
+
if skills:
|
|
611
|
+
(agent_dir / SKILLS_DIR).mkdir(exist_ok=True)
|
|
612
|
+
if workflows:
|
|
613
|
+
(agent_dir / WORKFLOWS_DIR).mkdir(exist_ok=True)
|
|
614
|
+
if registry:
|
|
615
|
+
(agent_dir / REGISTRY_DIR).mkdir(exist_ok=True)
|
|
616
|
+
|
|
617
|
+
context = {
|
|
618
|
+
"name": name,
|
|
619
|
+
"domain": domain,
|
|
620
|
+
"language": language,
|
|
621
|
+
"has_skills": skills,
|
|
622
|
+
"has_workflows": workflows,
|
|
623
|
+
"has_registry": registry,
|
|
624
|
+
"domain_config": domain_config,
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
# Render templates
|
|
628
|
+
env = Environment(
|
|
629
|
+
loader=FileSystemLoader(str(SCAFFOLD_DIR)),
|
|
630
|
+
keep_trailing_newline=True,
|
|
631
|
+
)
|
|
632
|
+
|
|
633
|
+
# agent.yaml
|
|
634
|
+
content = _render_template(env, "agent.yaml.jinja", context)
|
|
635
|
+
(agent_dir / "agent.yaml").write_text(content)
|
|
636
|
+
|
|
637
|
+
# instructions.md
|
|
638
|
+
content = _render_template(env, "instructions.md.jinja", context)
|
|
639
|
+
(agent_dir / "instructions.md").write_text(content)
|
|
640
|
+
|
|
641
|
+
# permissions.yaml
|
|
642
|
+
content = _render_template(env, "permissions.yaml.jinja", context)
|
|
643
|
+
(agent_dir / "permissions.yaml").write_text(content)
|
|
644
|
+
|
|
645
|
+
# .agentignore
|
|
646
|
+
agentignore_path = project_root / AGENTIGNORE_FILE
|
|
647
|
+
if not agentignore_path.exists():
|
|
648
|
+
content = _render_template(env, "agentignore.jinja", context)
|
|
649
|
+
agentignore_path.write_text(content)
|
|
650
|
+
|
|
651
|
+
# ORCHESTRATOR.md (if skills enabled)
|
|
652
|
+
if skills:
|
|
653
|
+
content = _render_template(env, "orchestrator.md.jinja", context)
|
|
654
|
+
(agent_dir / SKILLS_DIR / "ORCHESTRATOR.md").write_text(content)
|
|
655
|
+
|
|
656
|
+
# Domain-specific skill files (manifest + runbook)
|
|
657
|
+
if skills and domain_config:
|
|
658
|
+
for skill_def in domain_config.skills:
|
|
659
|
+
skill_context = {"skill": skill_def}
|
|
660
|
+
# Skill manifest
|
|
661
|
+
content = _render_template(env, "skill.yaml.jinja", skill_context)
|
|
662
|
+
(agent_dir / SKILLS_DIR / f"{skill_def.id}.skill.yaml").write_text(content)
|
|
663
|
+
# Skill runbook
|
|
664
|
+
content = _render_template(env, "skill.md.jinja", skill_context)
|
|
665
|
+
(agent_dir / SKILLS_DIR / f"{skill_def.id}.md").write_text(content)
|
|
666
|
+
|
|
667
|
+
# Domain-specific workflow file
|
|
668
|
+
if workflows and domain_config and domain_config.workflow:
|
|
669
|
+
workflow_context = {"workflow": domain_config.workflow}
|
|
670
|
+
content = _render_template(env, "workflow.yaml.jinja", workflow_context)
|
|
671
|
+
(agent_dir / WORKFLOWS_DIR / f"{domain_config.workflow.id}.yaml").write_text(content)
|
|
672
|
+
|
|
673
|
+
# Local config files
|
|
674
|
+
content = _render_template(env, "local.yaml.jinja", context)
|
|
675
|
+
(agent_dir / LOCAL_FILE).write_text(content)
|
|
676
|
+
|
|
677
|
+
content = _render_template(env, "local.example.yaml.jinja", context)
|
|
678
|
+
(agent_dir / LOCAL_EXAMPLE_FILE).write_text(content)
|
|
679
|
+
|
|
680
|
+
# Memory project.md
|
|
681
|
+
memory_content = f"# {name} — Agent Memory\n\n## Project Overview\n\n## Architecture\n\n## Status\n\n## Key Patterns\n"
|
|
682
|
+
(agent_dir / MEMORY_DIR / "project.md").write_text(memory_content)
|
|
683
|
+
|
|
684
|
+
# Commands directory + /setup runbook
|
|
685
|
+
(agent_dir / COMMANDS_DIR).mkdir(exist_ok=True)
|
|
686
|
+
content = _render_template(env, "setup.md.jinja", context)
|
|
687
|
+
(agent_dir / COMMANDS_DIR / "setup.md").write_text(content)
|
|
688
|
+
|
|
689
|
+
# Workflow command runbooks
|
|
690
|
+
if domain_config and domain_config.workflow_commands:
|
|
691
|
+
for cmd_def in domain_config.workflow_commands:
|
|
692
|
+
cmd_context = {"cmd": cmd_def}
|
|
693
|
+
content = _render_template(env, "workflow_command.md.jinja", cmd_context)
|
|
694
|
+
(agent_dir / COMMANDS_DIR / f"{cmd_def.id}.md").write_text(content)
|
|
695
|
+
|
|
696
|
+
# Operations memory file (when domain has workflow commands)
|
|
697
|
+
if domain_config and domain_config.workflow_commands:
|
|
698
|
+
ops_context = {
|
|
699
|
+
"name": name,
|
|
700
|
+
"domain_config": domain_config,
|
|
701
|
+
"workflow_commands": domain_config.workflow_commands,
|
|
702
|
+
}
|
|
703
|
+
content = _render_template(env, "operations.md.jinja", ops_context)
|
|
704
|
+
(agent_dir / MEMORY_DIR / "operations.md").write_text(content)
|
|
705
|
+
|
|
706
|
+
# Auto-sync: generate tool-specific config files
|
|
707
|
+
synced_files = run_sync(project_root, force=True, quiet=True)
|
|
708
|
+
_write_mcp_config(project_root)
|
|
709
|
+
|
|
710
|
+
# Determine project type label for output
|
|
711
|
+
project_type = "other"
|
|
712
|
+
if analysis is not None:
|
|
713
|
+
project_type = analysis.project_type
|
|
714
|
+
elif domain in ("ml", "web", "devops", "research"):
|
|
715
|
+
project_type = domain
|
|
716
|
+
|
|
717
|
+
_print_post_init_summary(
|
|
718
|
+
project_root=project_root,
|
|
719
|
+
name=name,
|
|
720
|
+
project_type=project_type,
|
|
721
|
+
language=language,
|
|
722
|
+
domain_config=domain_config,
|
|
723
|
+
skills=skills,
|
|
724
|
+
workflows=workflows,
|
|
725
|
+
registry=registry,
|
|
726
|
+
synced_files=synced_files,
|
|
727
|
+
)
|