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.
Files changed (48) hide show
  1. aes/__init__.py +5 -0
  2. aes/__main__.py +37 -0
  3. aes/analyzer.py +487 -0
  4. aes/commands/__init__.py +0 -0
  5. aes/commands/init.py +727 -0
  6. aes/commands/inspect.py +204 -0
  7. aes/commands/install.py +379 -0
  8. aes/commands/publish.py +432 -0
  9. aes/commands/search.py +65 -0
  10. aes/commands/status.py +153 -0
  11. aes/commands/sync.py +413 -0
  12. aes/commands/validate.py +77 -0
  13. aes/config.py +43 -0
  14. aes/domains.py +1382 -0
  15. aes/frameworks.py +522 -0
  16. aes/mcp_server.py +213 -0
  17. aes/registry.py +294 -0
  18. aes/scaffold/agent.yaml.jinja +135 -0
  19. aes/scaffold/agentignore.jinja +61 -0
  20. aes/scaffold/instructions.md.jinja +311 -0
  21. aes/scaffold/local.example.yaml.jinja +35 -0
  22. aes/scaffold/local.yaml.jinja +29 -0
  23. aes/scaffold/operations.md.jinja +33 -0
  24. aes/scaffold/orchestrator.md.jinja +95 -0
  25. aes/scaffold/permissions.yaml.jinja +151 -0
  26. aes/scaffold/setup.md.jinja +244 -0
  27. aes/scaffold/skill.md.jinja +27 -0
  28. aes/scaffold/skill.yaml.jinja +175 -0
  29. aes/scaffold/workflow.yaml.jinja +44 -0
  30. aes/scaffold/workflow_command.md.jinja +48 -0
  31. aes/schemas/agent.schema.json +188 -0
  32. aes/schemas/permissions.schema.json +100 -0
  33. aes/schemas/registry.schema.json +72 -0
  34. aes/schemas/skill.schema.json +209 -0
  35. aes/schemas/workflow.schema.json +92 -0
  36. aes/targets/__init__.py +29 -0
  37. aes/targets/_base.py +77 -0
  38. aes/targets/_composer.py +338 -0
  39. aes/targets/claude.py +153 -0
  40. aes/targets/copilot.py +48 -0
  41. aes/targets/cursor.py +46 -0
  42. aes/targets/windsurf.py +46 -0
  43. aes/validator.py +394 -0
  44. aes_cli-0.2.0.dist-info/METADATA +110 -0
  45. aes_cli-0.2.0.dist-info/RECORD +48 -0
  46. aes_cli-0.2.0.dist-info/WHEEL +5 -0
  47. aes_cli-0.2.0.dist-info/entry_points.txt +3 -0
  48. aes_cli-0.2.0.dist-info/top_level.txt +1 -0
aes/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ """AES CLI — Command-line tool for the Agentic Engineering Standard."""
2
+
3
+ from __future__ import annotations
4
+
5
+ __version__ = "0.2.0"
aes/__main__.py ADDED
@@ -0,0 +1,37 @@
1
+ """AES CLI entry point."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import click
6
+
7
+ from aes.commands.init import init_cmd
8
+ from aes.commands.validate import validate_cmd
9
+ from aes.commands.inspect import inspect_cmd
10
+ from aes.commands.publish import publish_cmd
11
+ from aes.commands.install import install_cmd
12
+ from aes.commands.search import search_cmd
13
+ from aes.commands.status import status_cmd
14
+ from aes.commands.sync import sync_cmd
15
+
16
+
17
+ @click.group()
18
+ @click.version_option(package_name="aes-cli")
19
+ def cli() -> None:
20
+ """AES — Agentic Engineering Standard CLI.
21
+
22
+ Scaffold, validate, inspect, and share agentic engineering projects.
23
+ """
24
+
25
+
26
+ cli.add_command(init_cmd, "init")
27
+ cli.add_command(validate_cmd, "validate")
28
+ cli.add_command(inspect_cmd, "inspect")
29
+ cli.add_command(publish_cmd, "publish")
30
+ cli.add_command(install_cmd, "install")
31
+ cli.add_command(sync_cmd, "sync")
32
+ cli.add_command(status_cmd, "status")
33
+ cli.add_command(search_cmd, "search")
34
+
35
+
36
+ if __name__ == "__main__":
37
+ cli()
aes/analyzer.py ADDED
@@ -0,0 +1,487 @@
1
+ """Smart project analyzer for aes init.
2
+
3
+ Scans a project directory to detect language, frameworks, project type,
4
+ and other signals that inform the scaffolding of .agent/ configuration.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import re
10
+ from dataclasses import dataclass, field
11
+ from pathlib import Path
12
+ from typing import Dict, List, Optional
13
+
14
+
15
+ # ---------------------------------------------------------------------------
16
+ # Known frameworks per language (dependency name -> framework label)
17
+ # ---------------------------------------------------------------------------
18
+
19
+ _PYTHON_FRAMEWORKS: Dict[str, str] = {
20
+ "fastapi": "fastapi",
21
+ "django": "django",
22
+ "flask": "flask",
23
+ "streamlit": "streamlit",
24
+ "celery": "celery",
25
+ "scrapy": "scrapy",
26
+ "pytorch": "pytorch",
27
+ "torch": "pytorch",
28
+ "tensorflow": "tensorflow",
29
+ "scikit-learn": "scikit-learn",
30
+ "sklearn": "scikit-learn",
31
+ "pandas": "pandas",
32
+ "airflow": "airflow",
33
+ "apache-airflow": "airflow",
34
+ "prefect": "prefect",
35
+ "luigi": "luigi",
36
+ "click": "click",
37
+ "typer": "typer",
38
+ "sqlalchemy": "sqlalchemy",
39
+ "alembic": "alembic",
40
+ }
41
+
42
+ _JS_FRAMEWORKS: Dict[str, str] = {
43
+ "next": "nextjs",
44
+ "react": "react",
45
+ "vue": "vue",
46
+ "nuxt": "nuxt",
47
+ "svelte": "svelte",
48
+ "@sveltejs/kit": "sveltekit",
49
+ "express": "express",
50
+ "@nestjs/core": "nestjs",
51
+ "fastify": "fastify",
52
+ "hono": "hono",
53
+ "angular": "angular",
54
+ "@angular/core": "angular",
55
+ "electron": "electron",
56
+ "prisma": "prisma",
57
+ "@prisma/client": "prisma",
58
+ "drizzle-orm": "drizzle",
59
+ }
60
+
61
+ _GO_FRAMEWORKS: Dict[str, str] = {
62
+ "github.com/gin-gonic/gin": "gin",
63
+ "github.com/gofiber/fiber": "fiber",
64
+ "github.com/labstack/echo": "echo",
65
+ "github.com/spf13/cobra": "cobra",
66
+ "google.golang.org/grpc": "grpc",
67
+ "github.com/gorilla/mux": "gorilla",
68
+ }
69
+
70
+ _RUST_FRAMEWORKS: Dict[str, str] = {
71
+ "actix-web": "actix-web",
72
+ "rocket": "rocket",
73
+ "axum": "axum",
74
+ "tokio": "tokio",
75
+ "clap": "clap",
76
+ "warp": "warp",
77
+ }
78
+
79
+ # ---------------------------------------------------------------------------
80
+ # Framework -> project type classification
81
+ # ---------------------------------------------------------------------------
82
+
83
+ _API_FRAMEWORKS = {
84
+ "fastapi", "django", "flask", "express", "nestjs", "fastify",
85
+ "hono", "gin", "fiber", "echo", "gorilla", "actix-web", "rocket",
86
+ "axum", "warp",
87
+ }
88
+ _FRONTEND_FRAMEWORKS = {"react", "vue", "svelte", "angular"}
89
+ _FULLSTACK_FRAMEWORKS = {"nextjs", "nuxt", "sveltekit"}
90
+ _CLI_FRAMEWORKS = {"click", "typer", "cobra", "clap"}
91
+ _ML_FRAMEWORKS = {"pytorch", "tensorflow", "scikit-learn"}
92
+ _DATA_FRAMEWORKS = {"airflow", "prefect", "luigi", "pandas"}
93
+
94
+
95
+ # ---------------------------------------------------------------------------
96
+ # Result data class
97
+ # ---------------------------------------------------------------------------
98
+
99
+ @dataclass
100
+ class ProjectAnalysis:
101
+ """Result of analyzing a project directory."""
102
+
103
+ name: str
104
+ language: str
105
+ frameworks: List[str] = field(default_factory=list)
106
+ project_type: str = "other"
107
+ has_tests: bool = False
108
+ has_ci: bool = False
109
+ has_docker: bool = False
110
+ has_database: bool = False
111
+ test_command: Optional[str] = None
112
+ build_command: Optional[str] = None
113
+ source_dirs: List[str] = field(default_factory=list)
114
+ existing_agent_configs: Dict[str, Path] = field(default_factory=dict)
115
+
116
+
117
+ # ---------------------------------------------------------------------------
118
+ # Language detection (improved from init.py)
119
+ # ---------------------------------------------------------------------------
120
+
121
+ _LANGUAGE_MARKERS = [
122
+ ("python", ["pyproject.toml", "setup.py", "setup.cfg", "requirements.txt", "Pipfile"]),
123
+ ("typescript", ["tsconfig.json"]),
124
+ ("javascript", ["package.json"]),
125
+ ("go", ["go.mod"]),
126
+ ("rust", ["Cargo.toml"]),
127
+ ("java", ["pom.xml", "build.gradle", "build.gradle.kts"]),
128
+ ]
129
+
130
+
131
+ def _detect_language(root: Path) -> str:
132
+ for language, markers in _LANGUAGE_MARKERS:
133
+ for marker in markers:
134
+ if (root / marker).exists():
135
+ return language
136
+ return "other"
137
+
138
+
139
+ # ---------------------------------------------------------------------------
140
+ # Framework detection per language
141
+ # ---------------------------------------------------------------------------
142
+
143
+ def _parse_python_deps(root: Path) -> List[str]:
144
+ """Extract dependency names from Python project files."""
145
+ deps: List[str] = []
146
+
147
+ # pyproject.toml
148
+ pyproject = root / "pyproject.toml"
149
+ if pyproject.exists():
150
+ try:
151
+ content = pyproject.read_text()
152
+ # Simple regex extraction from dependencies list
153
+ # Matches lines like: "fastapi>=0.100", 'django', etc.
154
+ in_deps = False
155
+ for line in content.splitlines():
156
+ stripped = line.strip()
157
+ if stripped in ("dependencies = [", "[project.dependencies]"):
158
+ in_deps = True
159
+ continue
160
+ if in_deps:
161
+ if stripped == "]" or (stripped.startswith("[") and not stripped.startswith("[")):
162
+ in_deps = False
163
+ continue
164
+ match = re.match(r'["\']([a-zA-Z0-9_-]+)', stripped)
165
+ if match:
166
+ deps.append(match.group(1).lower())
167
+ except (OSError, UnicodeDecodeError):
168
+ pass
169
+
170
+ # requirements.txt
171
+ reqtxt = root / "requirements.txt"
172
+ if reqtxt.exists():
173
+ try:
174
+ for line in reqtxt.read_text().splitlines():
175
+ line = line.strip()
176
+ if line and not line.startswith("#") and not line.startswith("-"):
177
+ match = re.match(r"([a-zA-Z0-9_-]+)", line)
178
+ if match:
179
+ deps.append(match.group(1).lower())
180
+ except (OSError, UnicodeDecodeError):
181
+ pass
182
+
183
+ return deps
184
+
185
+
186
+ def _parse_js_deps(root: Path) -> List[str]:
187
+ """Extract dependency names from package.json."""
188
+ import json
189
+
190
+ pkg = root / "package.json"
191
+ if not pkg.exists():
192
+ return []
193
+ try:
194
+ data = json.loads(pkg.read_text())
195
+ deps = list(data.get("dependencies", {}).keys())
196
+ deps += list(data.get("devDependencies", {}).keys())
197
+ return [d.lower() for d in deps]
198
+ except (OSError, json.JSONDecodeError, UnicodeDecodeError):
199
+ return []
200
+
201
+
202
+ def _parse_go_deps(root: Path) -> List[str]:
203
+ """Extract module paths from go.mod require block."""
204
+ gomod = root / "go.mod"
205
+ if not gomod.exists():
206
+ return []
207
+ try:
208
+ content = gomod.read_text()
209
+ deps: List[str] = []
210
+ in_require = False
211
+ for line in content.splitlines():
212
+ stripped = line.strip()
213
+ if stripped.startswith("require ("):
214
+ in_require = True
215
+ continue
216
+ if in_require:
217
+ if stripped == ")":
218
+ in_require = False
219
+ continue
220
+ parts = stripped.split()
221
+ if parts:
222
+ deps.append(parts[0])
223
+ return deps
224
+ except (OSError, UnicodeDecodeError):
225
+ return []
226
+
227
+
228
+ def _parse_rust_deps(root: Path) -> List[str]:
229
+ """Extract crate names from Cargo.toml [dependencies]."""
230
+ cargo = root / "Cargo.toml"
231
+ if not cargo.exists():
232
+ return []
233
+ try:
234
+ content = cargo.read_text()
235
+ deps: List[str] = []
236
+ in_deps = False
237
+ for line in content.splitlines():
238
+ stripped = line.strip()
239
+ if stripped == "[dependencies]":
240
+ in_deps = True
241
+ continue
242
+ if in_deps:
243
+ if stripped.startswith("["):
244
+ in_deps = False
245
+ continue
246
+ match = re.match(r"([a-zA-Z0-9_-]+)\s*=", stripped)
247
+ if match:
248
+ deps.append(match.group(1).lower())
249
+ return deps
250
+ except (OSError, UnicodeDecodeError):
251
+ return []
252
+
253
+
254
+ def _detect_frameworks(root: Path, language: str) -> List[str]:
255
+ """Detect frameworks from dependency files."""
256
+ framework_map: Dict[str, str]
257
+ raw_deps: List[str]
258
+
259
+ if language == "python":
260
+ raw_deps = _parse_python_deps(root)
261
+ framework_map = _PYTHON_FRAMEWORKS
262
+ elif language in ("javascript", "typescript"):
263
+ raw_deps = _parse_js_deps(root)
264
+ framework_map = _JS_FRAMEWORKS
265
+ elif language == "go":
266
+ raw_deps = _parse_go_deps(root)
267
+ framework_map = _GO_FRAMEWORKS
268
+ elif language == "rust":
269
+ raw_deps = _parse_rust_deps(root)
270
+ framework_map = _RUST_FRAMEWORKS
271
+ else:
272
+ return []
273
+
274
+ seen = set()
275
+ frameworks: List[str] = []
276
+ for dep in raw_deps:
277
+ label = framework_map.get(dep)
278
+ if label and label not in seen:
279
+ seen.add(label)
280
+ frameworks.append(label)
281
+
282
+ return frameworks
283
+
284
+
285
+ # ---------------------------------------------------------------------------
286
+ # Project type classification
287
+ # ---------------------------------------------------------------------------
288
+
289
+ def _classify_project_type(frameworks: List[str], root: Path) -> str:
290
+ """Classify project type from frameworks and directory structure."""
291
+ fw_set = set(frameworks)
292
+
293
+ # Check for fullstack first (most specific)
294
+ if fw_set & _FULLSTACK_FRAMEWORKS:
295
+ return "fullstack"
296
+
297
+ # Check for ML
298
+ if fw_set & _ML_FRAMEWORKS:
299
+ return "ml"
300
+
301
+ # Check for data pipeline
302
+ if fw_set & _DATA_FRAMEWORKS:
303
+ return "data-pipeline"
304
+
305
+ # Check for CLI tool
306
+ if fw_set & _CLI_FRAMEWORKS:
307
+ return "cli-tool"
308
+
309
+ # API with frontend = fullstack
310
+ has_api = bool(fw_set & _API_FRAMEWORKS)
311
+ has_frontend = bool(fw_set & _FRONTEND_FRAMEWORKS)
312
+ if has_api and has_frontend:
313
+ return "fullstack"
314
+ if has_api:
315
+ return "api"
316
+ if has_frontend:
317
+ return "web-frontend"
318
+
319
+ # Check for devops by directory structure
320
+ if (root / "terraform").is_dir() or list(root.glob("*.tf")):
321
+ return "devops"
322
+ if (root / "ansible").is_dir() or (root / "playbook.yml").exists():
323
+ return "devops"
324
+
325
+ # Check for library (no entry point, has package config)
326
+ has_pkg = (
327
+ (root / "pyproject.toml").exists()
328
+ or (root / "package.json").exists()
329
+ or (root / "Cargo.toml").exists()
330
+ )
331
+ has_entry = (
332
+ (root / "main.py").exists()
333
+ or (root / "app.py").exists()
334
+ or (root / "src" / "main.py").exists()
335
+ or (root / "src" / "index.ts").exists()
336
+ or (root / "src" / "index.js").exists()
337
+ or (root / "cmd").is_dir()
338
+ or (root / "src" / "main.rs").exists()
339
+ )
340
+ if has_pkg and not has_entry and not frameworks:
341
+ return "library"
342
+
343
+ return "other"
344
+
345
+
346
+ # ---------------------------------------------------------------------------
347
+ # Signal detection
348
+ # ---------------------------------------------------------------------------
349
+
350
+ def _detect_tests(root: Path, language: str) -> tuple:
351
+ """Detect test directory and infer test command. Returns (has_tests, command)."""
352
+ test_dirs = ["tests", "test", "__tests__", "spec"]
353
+ has_tests = any((root / d).is_dir() for d in test_dirs)
354
+
355
+ if not has_tests:
356
+ # Check for test files in src
357
+ has_tests = bool(
358
+ list(root.glob("**/test_*.py"))[:1]
359
+ or list(root.glob("**/*.test.ts"))[:1]
360
+ or list(root.glob("**/*.test.js"))[:1]
361
+ or list(root.glob("**/*_test.go"))[:1]
362
+ )
363
+
364
+ command: Optional[str] = None
365
+ if has_tests:
366
+ if language == "python":
367
+ command = "python -m pytest"
368
+ elif language in ("javascript", "typescript"):
369
+ # Check package.json for test script
370
+ import json
371
+ pkg = root / "package.json"
372
+ if pkg.exists():
373
+ try:
374
+ data = json.loads(pkg.read_text())
375
+ if "test" in data.get("scripts", {}):
376
+ command = "npm run test"
377
+ except (OSError, json.JSONDecodeError):
378
+ pass
379
+ if not command:
380
+ command = "npm test"
381
+ elif language == "go":
382
+ command = "go test ./..."
383
+ elif language == "rust":
384
+ command = "cargo test"
385
+
386
+ return has_tests, command
387
+
388
+
389
+ def _detect_build_command(root: Path, language: str) -> Optional[str]:
390
+ """Infer build command from project files."""
391
+ import json
392
+
393
+ if language in ("javascript", "typescript"):
394
+ pkg = root / "package.json"
395
+ if pkg.exists():
396
+ try:
397
+ data = json.loads(pkg.read_text())
398
+ if "build" in data.get("scripts", {}):
399
+ return "npm run build"
400
+ except (OSError, json.JSONDecodeError):
401
+ pass
402
+ elif language == "go":
403
+ return "go build ./..."
404
+ elif language == "rust":
405
+ return "cargo build"
406
+ return None
407
+
408
+
409
+ def _detect_existing_configs(root: Path) -> Dict[str, Path]:
410
+ """Find existing agent configuration files."""
411
+ configs: Dict[str, Path] = {}
412
+
413
+ candidates = {
414
+ "claude": root / "CLAUDE.md",
415
+ "cursor": root / ".cursorrules",
416
+ "copilot": root / ".github" / "copilot-instructions.md",
417
+ "windsurf": root / ".windsurfrules",
418
+ }
419
+ for tool, path in candidates.items():
420
+ if path.exists() and path.stat().st_size > 0:
421
+ configs[tool] = path
422
+
423
+ return configs
424
+
425
+
426
+ def _detect_source_dirs(root: Path) -> List[str]:
427
+ """Detect common source directories."""
428
+ candidates = ["src", "lib", "app", "pkg", "cmd", "internal", "api"]
429
+ return [d for d in candidates if (root / d).is_dir()]
430
+
431
+
432
+ # ---------------------------------------------------------------------------
433
+ # Main entry point
434
+ # ---------------------------------------------------------------------------
435
+
436
+ def analyze_project(root: Path) -> ProjectAnalysis:
437
+ """Analyze a project directory and return structured findings.
438
+
439
+ This is the main entry point. It detects language, frameworks,
440
+ project type, and various project signals without requiring any
441
+ user input.
442
+ """
443
+ name_raw = root.name
444
+ name = re.sub(r"[^a-z0-9]+", "-", name_raw.lower()).strip("-") or "my-project"
445
+
446
+ language = _detect_language(root)
447
+ frameworks = _detect_frameworks(root, language)
448
+ project_type = _classify_project_type(frameworks, root)
449
+
450
+ has_tests, test_command = _detect_tests(root, language)
451
+ build_command = _detect_build_command(root, language)
452
+
453
+ has_ci = (
454
+ (root / ".github" / "workflows").is_dir()
455
+ or (root / ".gitlab-ci.yml").exists()
456
+ or (root / "Jenkinsfile").exists()
457
+ or (root / ".circleci").is_dir()
458
+ )
459
+
460
+ has_docker = (
461
+ (root / "Dockerfile").exists()
462
+ or (root / "docker-compose.yml").exists()
463
+ or (root / "docker-compose.yaml").exists()
464
+ )
465
+
466
+ has_database = (
467
+ (root / "migrations").is_dir()
468
+ or (root / "alembic").is_dir()
469
+ or (root / "alembic.ini").exists()
470
+ or (root / "prisma").is_dir()
471
+ or (root / "drizzle").is_dir()
472
+ )
473
+
474
+ return ProjectAnalysis(
475
+ name=name,
476
+ language=language,
477
+ frameworks=frameworks,
478
+ project_type=project_type,
479
+ has_tests=has_tests,
480
+ has_ci=has_ci,
481
+ has_docker=has_docker,
482
+ has_database=has_database,
483
+ test_command=test_command,
484
+ build_command=build_command,
485
+ source_dirs=_detect_source_dirs(root),
486
+ existing_agent_configs=_detect_existing_configs(root),
487
+ )
File without changes