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/__init__.py
ADDED
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
|
+
)
|
aes/commands/__init__.py
ADDED
|
File without changes
|