stackai-cli 0.1.3__tar.gz → 0.2.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.
- {stackai_cli-0.1.3 → stackai_cli-0.2.0}/PKG-INFO +1 -1
- stackai_cli-0.2.0/devai/__init__.py +1 -0
- stackai_cli-0.2.0/devai/cli.py +58 -0
- stackai_cli-0.2.0/devai/detector.py +524 -0
- {stackai_cli-0.1.3 → stackai_cli-0.2.0}/devai/generator.py +135 -0
- {stackai_cli-0.1.3 → stackai_cli-0.2.0}/pyproject.toml +1 -1
- {stackai_cli-0.1.3 → stackai_cli-0.2.0}/stackai_cli.egg-info/PKG-INFO +1 -1
- stackai_cli-0.1.3/devai/__init__.py +0 -1
- stackai_cli-0.1.3/devai/cli.py +0 -56
- stackai_cli-0.1.3/devai/detector.py +0 -151
- {stackai_cli-0.1.3 → stackai_cli-0.2.0}/README.md +0 -0
- {stackai_cli-0.1.3 → stackai_cli-0.2.0}/devai/ai.py +0 -0
- {stackai_cli-0.1.3 → stackai_cli-0.2.0}/devai/debugger.py +0 -0
- {stackai_cli-0.1.3 → stackai_cli-0.2.0}/setup.cfg +0 -0
- {stackai_cli-0.1.3 → stackai_cli-0.2.0}/stackai_cli.egg-info/SOURCES.txt +0 -0
- {stackai_cli-0.1.3 → stackai_cli-0.2.0}/stackai_cli.egg-info/dependency_links.txt +0 -0
- {stackai_cli-0.1.3 → stackai_cli-0.2.0}/stackai_cli.egg-info/entry_points.txt +0 -0
- {stackai_cli-0.1.3 → stackai_cli-0.2.0}/stackai_cli.egg-info/requires.txt +0 -0
- {stackai_cli-0.1.3 → stackai_cli-0.2.0}/stackai_cli.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.2.0"
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import click
|
|
2
|
+
from devai.detector import detect_stack
|
|
3
|
+
from devai.generator import generate_files
|
|
4
|
+
from devai.debugger import debug_container
|
|
5
|
+
from devai import __version__
|
|
6
|
+
|
|
7
|
+
@click.group()
|
|
8
|
+
@click.version_option(version=__version__, prog_name="stackai")
|
|
9
|
+
def main():
|
|
10
|
+
"""stackai — AI-powered Docker assistant. No configuration needed."""
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@main.command()
|
|
15
|
+
@click.argument("path", default=".", type=click.Path(exists=True))
|
|
16
|
+
def init(path):
|
|
17
|
+
"""Analyse ton projet et génère Dockerfile + docker-compose."""
|
|
18
|
+
click.echo(click.style("🔍 Analyzing project...", fg="cyan"))
|
|
19
|
+
|
|
20
|
+
stack = detect_stack(path)
|
|
21
|
+
if not stack:
|
|
22
|
+
click.echo(click.style("❌ Could not detect stack. Make sure you are in a project directory.", fg="red"))
|
|
23
|
+
raise SystemExit(1)
|
|
24
|
+
|
|
25
|
+
services = ', '.join(stack['services']) if stack['services'] else 'none'
|
|
26
|
+
click.echo(click.style(f"✅ Stack detected: {stack['language']} / {stack.get('framework','generic')} + {services}", fg="green"))
|
|
27
|
+
click.echo(click.style("⚙️ Generating Docker files...", fg="cyan"))
|
|
28
|
+
|
|
29
|
+
generated = generate_files(path, stack)
|
|
30
|
+
for f in generated:
|
|
31
|
+
click.echo(click.style(f" ✅ {f} created", fg="green"))
|
|
32
|
+
|
|
33
|
+
click.echo(click.style("\n🚀 Ready! Run: docker compose up -d", fg="bright_green", bold=True))
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@main.command()
|
|
37
|
+
@click.argument("container_name")
|
|
38
|
+
def debug(container_name):
|
|
39
|
+
"""Analyse les logs d'un container et explique l'erreur."""
|
|
40
|
+
click.echo(click.style(f"🔍 Reading logs from '{container_name}'...", fg="cyan"))
|
|
41
|
+
debug_container(container_name)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@main.command()
|
|
45
|
+
@click.argument("path", default=".", type=click.Path(exists=True))
|
|
46
|
+
def scan(path):
|
|
47
|
+
"""Affiche un résumé de la stack détectée sans rien générer."""
|
|
48
|
+
stack = detect_stack(path)
|
|
49
|
+
if not stack:
|
|
50
|
+
click.echo(click.style("❌ Aucune stack détectée.", fg="red"))
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
click.echo(click.style("📦 Stack detected:", fg="cyan", bold=True))
|
|
54
|
+
click.echo(f" Language : {stack['language']}")
|
|
55
|
+
click.echo(f" Framework : {stack.get('framework', 'unknown')}")
|
|
56
|
+
click.echo(f" Port : {stack.get('port', 'unknown')}")
|
|
57
|
+
click.echo(f" Services : {', '.join(stack['services']) if stack['services'] else 'none'}")
|
|
58
|
+
click.echo(f" Python ver. : {stack.get('python_version', 'N/A')}")
|
|
@@ -0,0 +1,524 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import re
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
# ---------------------------------------------------------------------------
|
|
8
|
+
# SERVICE MAP dep_name → [service_ids]
|
|
9
|
+
# A single dependency can imply multiple services.
|
|
10
|
+
# ---------------------------------------------------------------------------
|
|
11
|
+
SERVICE_MAP: dict[str, list[str]] = {
|
|
12
|
+
# ── Relational databases ──────────────────────────────────────────────
|
|
13
|
+
"psycopg2": ["postgres"], "psycopg2-binary": ["postgres"],
|
|
14
|
+
"asyncpg": ["postgres"], "sqlalchemy": ["postgres"],
|
|
15
|
+
"alembic": ["postgres"], "databases": ["postgres"],
|
|
16
|
+
"tortoise-orm": ["postgres"], "peewee": ["postgres"],
|
|
17
|
+
"dbt-core": ["postgres"], "dbt-postgres": ["postgres"],
|
|
18
|
+
"dbt-spark": ["spark"], "dbt-bigquery": [],
|
|
19
|
+
"pymysql": ["mysql"], "mysql-connector-python": ["mysql"],
|
|
20
|
+
"aiomysql": ["mysql"],
|
|
21
|
+
"cx-oracle": ["oracle"], "oracledb": ["oracle"],
|
|
22
|
+
"pyodbc": ["mssql"], "pymssql": ["mssql"],
|
|
23
|
+
"psycopg": ["postgres"],
|
|
24
|
+
|
|
25
|
+
# ── NoSQL ─────────────────────────────────────────────────────────────
|
|
26
|
+
"pymongo": ["mongodb"], "motor": ["mongodb"],
|
|
27
|
+
"mongoengine": ["mongodb"], "beanie": ["mongodb"],
|
|
28
|
+
"redis": ["redis"], "aioredis": ["redis"],
|
|
29
|
+
"redis-py": ["redis"], "coredis": ["redis"],
|
|
30
|
+
"celery": ["redis"], "dramatiq": ["redis"],
|
|
31
|
+
"rq": ["redis"],
|
|
32
|
+
"cassandra-driver": ["cassandra"],
|
|
33
|
+
"aiofiles": [],
|
|
34
|
+
|
|
35
|
+
# ── Search & Analytics ────────────────────────────────────────────────
|
|
36
|
+
"elasticsearch": ["elasticsearch"],
|
|
37
|
+
"elasticsearch-dsl": ["elasticsearch"],
|
|
38
|
+
"opensearch-py": ["opensearch"],
|
|
39
|
+
"opensearchpy": ["opensearch"],
|
|
40
|
+
"solr": ["solr"],
|
|
41
|
+
"clickhouse-driver": ["clickhouse"],
|
|
42
|
+
"clickhouse-connect": ["clickhouse"],
|
|
43
|
+
|
|
44
|
+
# ── Streaming / Messaging ─────────────────────────────────────────────
|
|
45
|
+
"kafka-python": ["kafka"], "confluent-kafka": ["kafka"],
|
|
46
|
+
"aiokafka": ["kafka"], "faust": ["kafka"],
|
|
47
|
+
"pika": ["rabbitmq"], "aio-pika": ["rabbitmq"],
|
|
48
|
+
"kombu": ["rabbitmq"],
|
|
49
|
+
"nats-py": ["nats"],
|
|
50
|
+
|
|
51
|
+
# ── Object Storage / Cloud ────────────────────────────────────────────
|
|
52
|
+
"boto3": ["minio"], "botocore": ["minio"],
|
|
53
|
+
"s3fs": ["minio"], "s3transfer": ["minio"],
|
|
54
|
+
"minio": ["minio"],
|
|
55
|
+
"google-cloud-storage": [], "azure-storage-blob": [],
|
|
56
|
+
|
|
57
|
+
# ── Big Data / Spark ──────────────────────────────────────────────────
|
|
58
|
+
"pyspark": ["spark"], "delta-spark": ["spark"],
|
|
59
|
+
"spark": ["spark"],
|
|
60
|
+
|
|
61
|
+
# ── Streaming / Real-time ─────────────────────────────────────────────
|
|
62
|
+
"pyflink": ["flink"], "apache-flink": ["flink"],
|
|
63
|
+
|
|
64
|
+
# ── Orchestration ─────────────────────────────────────────────────────
|
|
65
|
+
"apache-airflow": ["airflow", "postgres"],
|
|
66
|
+
"airflow": ["airflow", "postgres"],
|
|
67
|
+
"prefect": ["prefect"],
|
|
68
|
+
"dagster": ["dagster", "postgres"],
|
|
69
|
+
"luigi": [],
|
|
70
|
+
|
|
71
|
+
# ── ML / AI ───────────────────────────────────────────────────────────
|
|
72
|
+
"mlflow": ["mlflow"],
|
|
73
|
+
"torch": [], "tensorflow": [],
|
|
74
|
+
"onnxruntime": [], "triton": [],
|
|
75
|
+
|
|
76
|
+
# ── Tracing / Monitoring ──────────────────────────────────────────────
|
|
77
|
+
"opentelemetry-sdk": ["jaeger"],
|
|
78
|
+
"prometheus-client": ["prometheus"],
|
|
79
|
+
"grafana": ["grafana"],
|
|
80
|
+
|
|
81
|
+
# ── Web frameworks (no extra service needed) ──────────────────────────
|
|
82
|
+
"fastapi": [], "flask": [], "django": [], "tornado": [],
|
|
83
|
+
"aiohttp": [], "starlette": [], "litestar": [],
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
# ---------------------------------------------------------------------------
|
|
87
|
+
# IMPORT → dep alias (for scanning .py source files)
|
|
88
|
+
# ---------------------------------------------------------------------------
|
|
89
|
+
IMPORT_TO_DEP: dict[str, str] = {
|
|
90
|
+
"psycopg2": "psycopg2", "asyncpg": "asyncpg",
|
|
91
|
+
"sqlalchemy": "sqlalchemy", "alembic": "alembic",
|
|
92
|
+
"pymysql": "pymysql", "pymongo": "pymongo",
|
|
93
|
+
"motor": "motor", "redis": "redis",
|
|
94
|
+
"celery": "celery", "kafka": "kafka-python",
|
|
95
|
+
"confluent_kafka": "confluent-kafka", "aiokafka": "aiokafka",
|
|
96
|
+
"pika": "pika", "aio_pika": "aio-pika",
|
|
97
|
+
"boto3": "boto3", "botocore": "botocore",
|
|
98
|
+
"s3fs": "s3fs", "minio": "minio",
|
|
99
|
+
"pyspark": "pyspark", "delta": "delta-spark",
|
|
100
|
+
"airflow": "apache-airflow", "prefect": "prefect",
|
|
101
|
+
"dagster": "dagster", "mlflow": "mlflow",
|
|
102
|
+
"elasticsearch": "elasticsearch",
|
|
103
|
+
"opensearchpy": "opensearch-py",
|
|
104
|
+
"clickhouse_driver": "clickhouse-driver",
|
|
105
|
+
"cassandra": "cassandra-driver",
|
|
106
|
+
"faust": "faust", "pyflink": "pyflink",
|
|
107
|
+
"torch": "torch", "tensorflow": "tensorflow",
|
|
108
|
+
"prometheus_client": "prometheus-client",
|
|
109
|
+
"opentelemetry": "opentelemetry-sdk",
|
|
110
|
+
"dbt": "dbt-core",
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
# Framework detection: import name → (framework_label, default_port)
|
|
114
|
+
FRAMEWORK_MAP: dict[str, tuple[str, int]] = {
|
|
115
|
+
"fastapi": ("fastapi", 8000), "uvicorn": ("fastapi", 8000),
|
|
116
|
+
"flask": ("flask", 5000), "django": ("django", 8000),
|
|
117
|
+
"tornado": ("tornado", 8888), "aiohttp": ("aiohttp", 8080),
|
|
118
|
+
"starlette": ("starlette", 8000), "litestar": ("litestar", 8000),
|
|
119
|
+
"express": ("express", 3000), "next": ("nextjs", 3000),
|
|
120
|
+
"nuxt": ("nuxtjs", 3000),
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
# ---------------------------------------------------------------------------
|
|
125
|
+
# Public entry point
|
|
126
|
+
# ---------------------------------------------------------------------------
|
|
127
|
+
|
|
128
|
+
def detect_stack(project_path: str) -> dict | None:
|
|
129
|
+
path = Path(project_path)
|
|
130
|
+
|
|
131
|
+
if (path / "requirements.txt").exists() or (path / "pyproject.toml").exists():
|
|
132
|
+
return _detect_python(path)
|
|
133
|
+
if (path / "package.json").exists():
|
|
134
|
+
return _detect_node(path)
|
|
135
|
+
if (path / "pom.xml").exists() or (path / "build.gradle").exists():
|
|
136
|
+
return _detect_java(path)
|
|
137
|
+
if (path / "go.mod").exists():
|
|
138
|
+
return _detect_go(path)
|
|
139
|
+
return None
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
# ---------------------------------------------------------------------------
|
|
143
|
+
# Python
|
|
144
|
+
# ---------------------------------------------------------------------------
|
|
145
|
+
|
|
146
|
+
def _detect_python(path: Path) -> dict:
|
|
147
|
+
deps = _read_python_deps(path)
|
|
148
|
+
deps |= _scan_python_imports(path) # enrich with source scan
|
|
149
|
+
deps |= _read_env_hints(path) # enrich with .env hints
|
|
150
|
+
|
|
151
|
+
framework, port = _detect_framework(deps)
|
|
152
|
+
services = _deps_to_services(deps)
|
|
153
|
+
python_version = _detect_python_version(path)
|
|
154
|
+
dep_file = "requirements.txt" if (path / "requirements.txt").exists() else "pyproject.toml"
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
"language": "python",
|
|
158
|
+
"framework": framework,
|
|
159
|
+
"port": port,
|
|
160
|
+
"services": services,
|
|
161
|
+
"python_version": python_version,
|
|
162
|
+
"dep_file": dep_file,
|
|
163
|
+
"raw_deps": sorted(deps),
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _read_python_deps(path: Path) -> set[str]:
|
|
168
|
+
"""Read requirements.txt and/or pyproject.toml."""
|
|
169
|
+
deps: set[str] = set()
|
|
170
|
+
|
|
171
|
+
req = path / "requirements.txt"
|
|
172
|
+
if req.exists():
|
|
173
|
+
for line in req.read_text(encoding="utf-8", errors="ignore").splitlines():
|
|
174
|
+
line = line.strip()
|
|
175
|
+
if not line or line.startswith("#") or line.startswith("-"):
|
|
176
|
+
continue
|
|
177
|
+
# strip version specifiers
|
|
178
|
+
name = re.split(r"[>=<!;\[]", line)[0].strip().lower()
|
|
179
|
+
if name:
|
|
180
|
+
deps.add(name)
|
|
181
|
+
|
|
182
|
+
pyproject = path / "pyproject.toml"
|
|
183
|
+
if pyproject.exists():
|
|
184
|
+
content = pyproject.read_text(encoding="utf-8", errors="ignore")
|
|
185
|
+
for m in re.finditer(r'"([a-zA-Z0-9_\-]+)\s*[>=<!]', content):
|
|
186
|
+
deps.add(m.group(1).lower())
|
|
187
|
+
|
|
188
|
+
return deps
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _scan_python_imports(path: Path) -> set[str]:
|
|
192
|
+
"""Scan .py files for import statements and map to dep names."""
|
|
193
|
+
found: set[str] = set()
|
|
194
|
+
py_files = list(path.rglob("*.py"))[:200] # cap at 200 files
|
|
195
|
+
|
|
196
|
+
import_re = re.compile(
|
|
197
|
+
r"^(?:import|from)\s+([a-zA-Z0-9_]+)", re.MULTILINE
|
|
198
|
+
)
|
|
199
|
+
for f in py_files:
|
|
200
|
+
try:
|
|
201
|
+
content = f.read_text(encoding="utf-8", errors="ignore")
|
|
202
|
+
for m in import_re.finditer(content):
|
|
203
|
+
root = m.group(1).lower()
|
|
204
|
+
if root in IMPORT_TO_DEP:
|
|
205
|
+
found.add(IMPORT_TO_DEP[root])
|
|
206
|
+
except OSError:
|
|
207
|
+
pass
|
|
208
|
+
return found
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _read_env_hints(path: Path) -> set[str]:
|
|
212
|
+
"""Infer services from .env or .env.example variable names."""
|
|
213
|
+
hints: set[str] = set()
|
|
214
|
+
for fname in [".env", ".env.example", ".env.sample"]:
|
|
215
|
+
env_file = path / fname
|
|
216
|
+
if not env_file.exists():
|
|
217
|
+
continue
|
|
218
|
+
content = env_file.read_text(encoding="utf-8", errors="ignore").upper()
|
|
219
|
+
if "POSTGRES" in content or "DATABASE_URL" in content:
|
|
220
|
+
hints.add("psycopg2")
|
|
221
|
+
if "REDIS" in content:
|
|
222
|
+
hints.add("redis")
|
|
223
|
+
if "MONGO" in content:
|
|
224
|
+
hints.add("pymongo")
|
|
225
|
+
if "KAFKA" in content:
|
|
226
|
+
hints.add("kafka-python")
|
|
227
|
+
if "MINIO" in content or "S3_ENDPOINT" in content or "AWS_" in content:
|
|
228
|
+
hints.add("boto3")
|
|
229
|
+
if "SPARK" in content:
|
|
230
|
+
hints.add("pyspark")
|
|
231
|
+
if "AIRFLOW" in content:
|
|
232
|
+
hints.add("apache-airflow")
|
|
233
|
+
if "MLFLOW" in content:
|
|
234
|
+
hints.add("mlflow")
|
|
235
|
+
if "ELASTICSEARCH" in content:
|
|
236
|
+
hints.add("elasticsearch")
|
|
237
|
+
return hints
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
# ---------------------------------------------------------------------------
|
|
241
|
+
# Node.js
|
|
242
|
+
# ---------------------------------------------------------------------------
|
|
243
|
+
|
|
244
|
+
def _detect_node(path: Path) -> dict:
|
|
245
|
+
pkg = json.loads((path / "package.json").read_text(encoding="utf-8"))
|
|
246
|
+
all_deps = {
|
|
247
|
+
**pkg.get("dependencies", {}),
|
|
248
|
+
**pkg.get("devDependencies", {}),
|
|
249
|
+
}
|
|
250
|
+
deps = {k.lower() for k in all_deps}
|
|
251
|
+
|
|
252
|
+
framework, port = _detect_framework(deps)
|
|
253
|
+
services = _deps_to_services(deps)
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
"language": "nodejs",
|
|
257
|
+
"framework": framework,
|
|
258
|
+
"port": port,
|
|
259
|
+
"services": services,
|
|
260
|
+
"python_version": None,
|
|
261
|
+
"dep_file": "package.json",
|
|
262
|
+
"raw_deps": sorted(deps),
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
# ---------------------------------------------------------------------------
|
|
267
|
+
# Java / Go
|
|
268
|
+
# ---------------------------------------------------------------------------
|
|
269
|
+
|
|
270
|
+
def _detect_java(path: Path) -> dict:
|
|
271
|
+
content = ""
|
|
272
|
+
for f in ["pom.xml", "build.gradle"]:
|
|
273
|
+
p = path / f
|
|
274
|
+
if p.exists():
|
|
275
|
+
content = p.read_text(encoding="utf-8", errors="ignore").lower()
|
|
276
|
+
break
|
|
277
|
+
|
|
278
|
+
services = []
|
|
279
|
+
if "postgresql" in content or "postgres" in content:
|
|
280
|
+
services.append("postgres")
|
|
281
|
+
if "mysql" in content:
|
|
282
|
+
services.append("mysql")
|
|
283
|
+
if "mongodb" in content or "mongo" in content:
|
|
284
|
+
services.append("mongodb")
|
|
285
|
+
if "redis" in content:
|
|
286
|
+
services.append("redis")
|
|
287
|
+
if "kafka" in content:
|
|
288
|
+
services.append("kafka")
|
|
289
|
+
if "elasticsearch" in content:
|
|
290
|
+
services.append("elasticsearch")
|
|
291
|
+
|
|
292
|
+
return {
|
|
293
|
+
"language": "java",
|
|
294
|
+
"framework": "spring-boot",
|
|
295
|
+
"port": 8080,
|
|
296
|
+
"services": services,
|
|
297
|
+
"python_version": None,
|
|
298
|
+
"dep_file": "pom.xml" if (path / "pom.xml").exists() else "build.gradle",
|
|
299
|
+
"raw_deps": [],
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def _detect_go(path: Path) -> dict:
|
|
304
|
+
content = (path / "go.mod").read_text(encoding="utf-8", errors="ignore").lower()
|
|
305
|
+
services = []
|
|
306
|
+
if "postgres" in content or "pgx" in content:
|
|
307
|
+
services.append("postgres")
|
|
308
|
+
if "mongo" in content:
|
|
309
|
+
services.append("mongodb")
|
|
310
|
+
if "redis" in content:
|
|
311
|
+
services.append("redis")
|
|
312
|
+
if "kafka" in content:
|
|
313
|
+
services.append("kafka")
|
|
314
|
+
|
|
315
|
+
return {
|
|
316
|
+
"language": "go",
|
|
317
|
+
"framework": "generic",
|
|
318
|
+
"port": 8080,
|
|
319
|
+
"services": services,
|
|
320
|
+
"python_version": None,
|
|
321
|
+
"dep_file": "go.mod",
|
|
322
|
+
"raw_deps": [],
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
# ---------------------------------------------------------------------------
|
|
327
|
+
# Helpers
|
|
328
|
+
# ---------------------------------------------------------------------------
|
|
329
|
+
|
|
330
|
+
def _deps_to_services(deps: set[str]) -> list[str]:
|
|
331
|
+
seen: set[str] = set()
|
|
332
|
+
result: list[str] = []
|
|
333
|
+
for dep in deps:
|
|
334
|
+
for svc in SERVICE_MAP.get(dep, []):
|
|
335
|
+
if svc and svc not in seen:
|
|
336
|
+
seen.add(svc)
|
|
337
|
+
result.append(svc)
|
|
338
|
+
return result
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def _detect_framework(deps: set[str]) -> tuple[str, int]:
|
|
342
|
+
for dep, (fw, port) in FRAMEWORK_MAP.items():
|
|
343
|
+
if dep in deps:
|
|
344
|
+
return fw, port
|
|
345
|
+
return "generic", 8000
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def _detect_python_version(path: Path) -> str:
|
|
349
|
+
for f in [".python-version", "runtime.txt"]:
|
|
350
|
+
p = path / f
|
|
351
|
+
if p.exists():
|
|
352
|
+
m = re.search(r"3\.\d+", p.read_text())
|
|
353
|
+
if m:
|
|
354
|
+
return m.group()
|
|
355
|
+
pyproject = path / "pyproject.toml"
|
|
356
|
+
if pyproject.exists():
|
|
357
|
+
m = re.search(r'python_requires\s*=\s*">=\s*(3\.\d+)"', pyproject.read_text())
|
|
358
|
+
if m:
|
|
359
|
+
return m.group(1)
|
|
360
|
+
return "3.11"
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
# Mapping dépendance → service(s) Docker requis
|
|
365
|
+
# Un service peut en impliquer plusieurs (ex: airflow → airflow + postgres)
|
|
366
|
+
SERVICE_HINTS = {
|
|
367
|
+
# Databases
|
|
368
|
+
"psycopg2": ["postgres"],
|
|
369
|
+
"asyncpg": ["postgres"],
|
|
370
|
+
"sqlalchemy": ["postgres"],
|
|
371
|
+
"pymongo": ["mongodb"],
|
|
372
|
+
"motor": ["mongodb"],
|
|
373
|
+
"mysql-connector": ["mysql"],
|
|
374
|
+
"pymysql": ["mysql"],
|
|
375
|
+
# Cache / Queue
|
|
376
|
+
"redis": ["redis"],
|
|
377
|
+
"celery": ["redis"],
|
|
378
|
+
# Search
|
|
379
|
+
"elasticsearch": ["elasticsearch"],
|
|
380
|
+
# Streaming
|
|
381
|
+
"kafka": ["kafka"],
|
|
382
|
+
# Object storage
|
|
383
|
+
"boto3": ["minio"],
|
|
384
|
+
"s3fs": ["minio"],
|
|
385
|
+
# Big Data
|
|
386
|
+
"pyspark": ["spark"],
|
|
387
|
+
"delta-spark": ["spark"],
|
|
388
|
+
"pyflink": ["flink"],
|
|
389
|
+
# Orchestration (Airflow implique postgres)
|
|
390
|
+
"apache-airflow": ["airflow", "postgres"],
|
|
391
|
+
"airflow": ["airflow", "postgres"],
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
# Mapping dépendance → framework
|
|
395
|
+
FRAMEWORK_HINTS = {
|
|
396
|
+
"fastapi": ("fastapi", 8000),
|
|
397
|
+
"uvicorn": ("fastapi", 8000),
|
|
398
|
+
"flask": ("flask", 5000),
|
|
399
|
+
"django": ("django", 8000),
|
|
400
|
+
"tornado": ("tornado", 8888),
|
|
401
|
+
"aiohttp": ("aiohttp", 8080),
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def detect_stack(project_path: str) -> dict | None:
|
|
406
|
+
path = Path(project_path)
|
|
407
|
+
|
|
408
|
+
# --- Python ---
|
|
409
|
+
req_file = path / "requirements.txt"
|
|
410
|
+
pyproject = path / "pyproject.toml"
|
|
411
|
+
if req_file.exists() or pyproject.exists():
|
|
412
|
+
return _detect_python(path, req_file if req_file.exists() else pyproject)
|
|
413
|
+
|
|
414
|
+
# --- Node.js ---
|
|
415
|
+
if (path / "package.json").exists():
|
|
416
|
+
return _detect_node(path)
|
|
417
|
+
|
|
418
|
+
# --- Java ---
|
|
419
|
+
if (path / "pom.xml").exists() or (path / "build.gradle").exists():
|
|
420
|
+
return _detect_java(path)
|
|
421
|
+
|
|
422
|
+
# --- Go ---
|
|
423
|
+
if (path / "go.mod").exists():
|
|
424
|
+
return _detect_go(path)
|
|
425
|
+
|
|
426
|
+
return None
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def _detect_python(path: Path, dep_file: Path) -> dict:
|
|
430
|
+
content = dep_file.read_text(encoding="utf-8", errors="ignore").lower()
|
|
431
|
+
deps = [line.split("==")[0].split(">=")[0].strip() for line in content.splitlines() if line.strip() and not line.startswith("#")]
|
|
432
|
+
|
|
433
|
+
framework = "generic"
|
|
434
|
+
port = 8000
|
|
435
|
+
for dep, (fw, p) in FRAMEWORK_HINTS.items():
|
|
436
|
+
if dep in deps:
|
|
437
|
+
framework = fw
|
|
438
|
+
port = p
|
|
439
|
+
break
|
|
440
|
+
|
|
441
|
+
services = list({s for dep in deps if dep in SERVICE_HINTS for s in SERVICE_HINTS[dep]})
|
|
442
|
+
|
|
443
|
+
python_version = _detect_python_version(path)
|
|
444
|
+
|
|
445
|
+
return {
|
|
446
|
+
"language": "python",
|
|
447
|
+
"framework": framework,
|
|
448
|
+
"port": port,
|
|
449
|
+
"services": services,
|
|
450
|
+
"python_version": python_version,
|
|
451
|
+
"dep_file": dep_file.name,
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def _detect_node(path: Path) -> dict:
|
|
456
|
+
import json
|
|
457
|
+
pkg = json.loads((path / "package.json").read_text(encoding="utf-8"))
|
|
458
|
+
deps = list(pkg.get("dependencies", {}).keys()) + list(pkg.get("devDependencies", {}).keys())
|
|
459
|
+
deps_lower = [d.lower() for d in deps]
|
|
460
|
+
|
|
461
|
+
framework = "express"
|
|
462
|
+
port = 3000
|
|
463
|
+
if "next" in deps_lower:
|
|
464
|
+
framework = "nextjs"
|
|
465
|
+
port = 3000
|
|
466
|
+
elif "nuxt" in deps_lower:
|
|
467
|
+
framework = "nuxtjs"
|
|
468
|
+
port = 3000
|
|
469
|
+
elif "react" in deps_lower:
|
|
470
|
+
framework = "react"
|
|
471
|
+
port = 3000
|
|
472
|
+
|
|
473
|
+
services = list({s for dep in deps_lower if dep in SERVICE_HINTS for s in SERVICE_HINTS[dep]})
|
|
474
|
+
|
|
475
|
+
return {
|
|
476
|
+
"language": "nodejs",
|
|
477
|
+
"framework": framework,
|
|
478
|
+
"port": port,
|
|
479
|
+
"services": services,
|
|
480
|
+
"python_version": None,
|
|
481
|
+
"dep_file": "package.json",
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
def _detect_java(path: Path) -> dict:
|
|
486
|
+
return {
|
|
487
|
+
"language": "java",
|
|
488
|
+
"framework": "spring-boot",
|
|
489
|
+
"port": 8080,
|
|
490
|
+
"services": [],
|
|
491
|
+
"python_version": None,
|
|
492
|
+
"dep_file": "pom.xml" if (path / "pom.xml").exists() else "build.gradle",
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
def _detect_go(path: Path) -> dict:
|
|
497
|
+
return {
|
|
498
|
+
"language": "go",
|
|
499
|
+
"framework": "generic",
|
|
500
|
+
"port": 8080,
|
|
501
|
+
"services": [],
|
|
502
|
+
"python_version": None,
|
|
503
|
+
"dep_file": "go.mod",
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
def _detect_python_version(path: Path) -> str:
|
|
508
|
+
"""Cherche la version Python dans .python-version, pyproject.toml ou runtime.txt."""
|
|
509
|
+
for f in [".python-version", "runtime.txt"]:
|
|
510
|
+
p = path / f
|
|
511
|
+
if p.exists():
|
|
512
|
+
content = p.read_text().strip()
|
|
513
|
+
match = re.search(r"3\.\d+", content)
|
|
514
|
+
if match:
|
|
515
|
+
return match.group()
|
|
516
|
+
|
|
517
|
+
pyproject = path / "pyproject.toml"
|
|
518
|
+
if pyproject.exists():
|
|
519
|
+
content = pyproject.read_text()
|
|
520
|
+
match = re.search(r'python_requires\s*=\s*">=\s*(3\.\d+)"', content)
|
|
521
|
+
if match:
|
|
522
|
+
return match.group(1)
|
|
523
|
+
|
|
524
|
+
return "3.11" # défaut
|
|
@@ -143,6 +143,140 @@ SERVICE_TEMPLATES = {
|
|
|
143
143
|
- xpack.security.enabled=false
|
|
144
144
|
ports:
|
|
145
145
|
- "9200:9200"
|
|
146
|
+
""",
|
|
147
|
+
"minio": """\
|
|
148
|
+
minio:
|
|
149
|
+
image: minio/minio:latest
|
|
150
|
+
environment:
|
|
151
|
+
MINIO_ROOT_USER: minioadmin
|
|
152
|
+
MINIO_ROOT_PASSWORD: minioadmin
|
|
153
|
+
command: server /data --console-address ":9001"
|
|
154
|
+
ports:
|
|
155
|
+
- "9000:9000"
|
|
156
|
+
- "9001:9001"
|
|
157
|
+
volumes:
|
|
158
|
+
- minio_data:/data
|
|
159
|
+
""",
|
|
160
|
+
"airflow": """\
|
|
161
|
+
airflow-webserver:
|
|
162
|
+
image: apache/airflow:2.8.1-python3.11
|
|
163
|
+
environment:
|
|
164
|
+
- AIRFLOW__CORE__EXECUTOR=LocalExecutor
|
|
165
|
+
- AIRFLOW__DATABASE__SQL_ALCHEMY_CONN=postgresql+psycopg2://airflow:airflow@postgres:5432/airflow
|
|
166
|
+
- AIRFLOW__CORE__LOAD_EXAMPLES=False
|
|
167
|
+
volumes:
|
|
168
|
+
- ./dags:/opt/airflow/dags
|
|
169
|
+
ports:
|
|
170
|
+
- "8080:8080"
|
|
171
|
+
command: >
|
|
172
|
+
bash -c "airflow db migrate && airflow users create --username admin --password admin --firstname Admin --lastname User --role Admin --email admin@example.com; airflow webserver"
|
|
173
|
+
depends_on:
|
|
174
|
+
- postgres
|
|
175
|
+
|
|
176
|
+
airflow-scheduler:
|
|
177
|
+
image: apache/airflow:2.8.1-python3.11
|
|
178
|
+
environment:
|
|
179
|
+
- AIRFLOW__CORE__EXECUTOR=LocalExecutor
|
|
180
|
+
- AIRFLOW__DATABASE__SQL_ALCHEMY_CONN=postgresql+psycopg2://airflow:airflow@postgres:5432/airflow
|
|
181
|
+
- AIRFLOW__CORE__LOAD_EXAMPLES=False
|
|
182
|
+
volumes:
|
|
183
|
+
- ./dags:/opt/airflow/dags
|
|
184
|
+
command: airflow scheduler
|
|
185
|
+
depends_on:
|
|
186
|
+
- postgres
|
|
187
|
+
""",
|
|
188
|
+
|
|
189
|
+
"spark": """\
|
|
190
|
+
spark-master:
|
|
191
|
+
image: bitnami/spark:3.5
|
|
192
|
+
environment:
|
|
193
|
+
- SPARK_MODE=master
|
|
194
|
+
- SPARK_MASTER_HOST=spark-master
|
|
195
|
+
ports:
|
|
196
|
+
- "7077:7077"
|
|
197
|
+
- "8081:8080"
|
|
198
|
+
|
|
199
|
+
spark-worker:
|
|
200
|
+
image: bitnami/spark:3.5
|
|
201
|
+
environment:
|
|
202
|
+
- SPARK_MODE=worker
|
|
203
|
+
- SPARK_MASTER_URL=spark://spark-master:7077
|
|
204
|
+
- SPARK_WORKER_MEMORY=2g
|
|
205
|
+
- SPARK_WORKER_CORES=2
|
|
206
|
+
depends_on:
|
|
207
|
+
- spark-master
|
|
208
|
+
""",
|
|
209
|
+
"kafka": """\
|
|
210
|
+
zookeeper:
|
|
211
|
+
image: confluentinc/cp-zookeeper:7.6.0
|
|
212
|
+
environment:
|
|
213
|
+
ZOOKEEPER_CLIENT_PORT: 2181
|
|
214
|
+
|
|
215
|
+
kafka:
|
|
216
|
+
image: confluentinc/cp-kafka:7.6.0
|
|
217
|
+
ports:
|
|
218
|
+
- "9092:9092"
|
|
219
|
+
environment:
|
|
220
|
+
KAFKA_BROKER_ID: 1
|
|
221
|
+
KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
|
|
222
|
+
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092
|
|
223
|
+
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
|
|
224
|
+
depends_on:
|
|
225
|
+
- zookeeper
|
|
226
|
+
""",
|
|
227
|
+
"rabbitmq": """\
|
|
228
|
+
rabbitmq:
|
|
229
|
+
image: rabbitmq:3-management-alpine
|
|
230
|
+
ports:
|
|
231
|
+
- "5672:5672"
|
|
232
|
+
- "15672:15672"
|
|
233
|
+
""",
|
|
234
|
+
"mlflow": """\
|
|
235
|
+
mlflow:
|
|
236
|
+
image: ghcr.io/mlflow/mlflow:v2.12.1
|
|
237
|
+
ports:
|
|
238
|
+
- "5000:5000"
|
|
239
|
+
command: mlflow server --host 0.0.0.0 --port 5000
|
|
240
|
+
""",
|
|
241
|
+
"prometheus": """\
|
|
242
|
+
prometheus:
|
|
243
|
+
image: prom/prometheus:latest
|
|
244
|
+
ports:
|
|
245
|
+
- "9090:9090"
|
|
246
|
+
volumes:
|
|
247
|
+
- ./prometheus.yml:/etc/prometheus/prometheus.yml
|
|
248
|
+
""",
|
|
249
|
+
"prefect": """\
|
|
250
|
+
prefect:
|
|
251
|
+
image: prefecthq/prefect:2-latest
|
|
252
|
+
command: prefect server start --host 0.0.0.0
|
|
253
|
+
ports:
|
|
254
|
+
- "4200:4200"
|
|
255
|
+
""",
|
|
256
|
+
"dagster": """\
|
|
257
|
+
dagster:
|
|
258
|
+
image: dagster/dagster-k8s:latest
|
|
259
|
+
ports:
|
|
260
|
+
- "3000:3000"
|
|
261
|
+
depends_on:
|
|
262
|
+
- postgres
|
|
263
|
+
""",
|
|
264
|
+
"flink": """\
|
|
265
|
+
flink-jobmanager:
|
|
266
|
+
image: flink:1.18-scala_2.12
|
|
267
|
+
command: jobmanager
|
|
268
|
+
ports:
|
|
269
|
+
- "8082:8081"
|
|
270
|
+
environment:
|
|
271
|
+
- JOB_MANAGER_RPC_ADDRESS=flink-jobmanager
|
|
272
|
+
|
|
273
|
+
flink-taskmanager:
|
|
274
|
+
image: flink:1.18-scala_2.12
|
|
275
|
+
command: taskmanager
|
|
276
|
+
depends_on:
|
|
277
|
+
- flink-jobmanager
|
|
278
|
+
environment:
|
|
279
|
+
- JOB_MANAGER_RPC_ADDRESS=flink-jobmanager
|
|
146
280
|
""",
|
|
147
281
|
}
|
|
148
282
|
|
|
@@ -150,6 +284,7 @@ VOLUME_NAMES = {
|
|
|
150
284
|
"postgres": "postgres_data",
|
|
151
285
|
"mongodb": "mongo_data",
|
|
152
286
|
"mysql": "mysql_data",
|
|
287
|
+
"minio": "minio_data",
|
|
153
288
|
}
|
|
154
289
|
|
|
155
290
|
FRAMEWORK_CMDS = {
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "stackai-cli"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.2.0"
|
|
8
8
|
description = "AI-powered Docker assistant — detects your stack, generates Dockerfile & compose, debugs containers."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.1.3"
|
stackai_cli-0.1.3/devai/cli.py
DELETED
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import click
|
|
2
|
-
from devai.detector import detect_stack
|
|
3
|
-
from devai.generator import generate_files
|
|
4
|
-
from devai.debugger import debug_container
|
|
5
|
-
|
|
6
|
-
@click.group()
|
|
7
|
-
@click.version_option()
|
|
8
|
-
def main():
|
|
9
|
-
"""devai — AI-powered Docker assistant. No config needed."""
|
|
10
|
-
pass
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
@main.command()
|
|
14
|
-
@click.argument("path", default=".", type=click.Path(exists=True))
|
|
15
|
-
def init(path):
|
|
16
|
-
"""Analyse ton projet et génère Dockerfile + docker-compose."""
|
|
17
|
-
click.echo(click.style("🔍 Analyse du projet...", fg="cyan"))
|
|
18
|
-
|
|
19
|
-
stack = detect_stack(path)
|
|
20
|
-
if not stack:
|
|
21
|
-
click.echo(click.style("❌ Impossible de détecter la stack. Vérifie que tu es dans le bon dossier.", fg="red"))
|
|
22
|
-
raise SystemExit(1)
|
|
23
|
-
|
|
24
|
-
click.echo(click.style(f"✅ Stack détectée : {stack['language']} / {', '.join(stack['services'])}", fg="green"))
|
|
25
|
-
click.echo(click.style("⚙️ Génération des fichiers Docker...", fg="cyan"))
|
|
26
|
-
|
|
27
|
-
generated = generate_files(path, stack)
|
|
28
|
-
for f in generated:
|
|
29
|
-
click.echo(click.style(f" ✅ {f} créé", fg="green"))
|
|
30
|
-
|
|
31
|
-
click.echo(click.style("\n🚀 Prêt ! Lance avec : docker compose up -d", fg="bright_green", bold=True))
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
@main.command()
|
|
35
|
-
@click.argument("container_name")
|
|
36
|
-
def debug(container_name):
|
|
37
|
-
"""Analyse les logs d'un container et explique l'erreur."""
|
|
38
|
-
click.echo(click.style(f"🔍 Lecture des logs de '{container_name}'...", fg="cyan"))
|
|
39
|
-
debug_container(container_name)
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
@main.command()
|
|
43
|
-
@click.argument("path", default=".", type=click.Path(exists=True))
|
|
44
|
-
def scan(path):
|
|
45
|
-
"""Affiche un résumé de la stack détectée sans rien générer."""
|
|
46
|
-
stack = detect_stack(path)
|
|
47
|
-
if not stack:
|
|
48
|
-
click.echo(click.style("❌ Aucune stack détectée.", fg="red"))
|
|
49
|
-
return
|
|
50
|
-
|
|
51
|
-
click.echo(click.style("📦 Stack détectée :", fg="cyan", bold=True))
|
|
52
|
-
click.echo(f" Langage : {stack['language']}")
|
|
53
|
-
click.echo(f" Framework : {stack.get('framework', 'inconnu')}")
|
|
54
|
-
click.echo(f" Port : {stack.get('port', 'inconnu')}")
|
|
55
|
-
click.echo(f" Services : {', '.join(stack['services']) if stack['services'] else 'aucun'}")
|
|
56
|
-
click.echo(f" Python ver. : {stack.get('python_version', 'N/A')}")
|
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import re
|
|
3
|
-
from pathlib import Path
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
# Mapping dépendance → service Docker requis
|
|
7
|
-
SERVICE_HINTS = {
|
|
8
|
-
"psycopg2": "postgres",
|
|
9
|
-
"asyncpg": "postgres",
|
|
10
|
-
"sqlalchemy": "postgres",
|
|
11
|
-
"pymongo": "mongodb",
|
|
12
|
-
"motor": "mongodb",
|
|
13
|
-
"redis": "redis",
|
|
14
|
-
"celery": "redis",
|
|
15
|
-
"elasticsearch": "elasticsearch",
|
|
16
|
-
"kafka": "kafka",
|
|
17
|
-
"mysql-connector": "mysql",
|
|
18
|
-
"pymysql": "mysql",
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
# Mapping dépendance → framework
|
|
22
|
-
FRAMEWORK_HINTS = {
|
|
23
|
-
"fastapi": ("fastapi", 8000),
|
|
24
|
-
"uvicorn": ("fastapi", 8000),
|
|
25
|
-
"flask": ("flask", 5000),
|
|
26
|
-
"django": ("django", 8000),
|
|
27
|
-
"tornado": ("tornado", 8888),
|
|
28
|
-
"aiohttp": ("aiohttp", 8080),
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def detect_stack(project_path: str) -> dict | None:
|
|
33
|
-
path = Path(project_path)
|
|
34
|
-
|
|
35
|
-
# --- Python ---
|
|
36
|
-
req_file = path / "requirements.txt"
|
|
37
|
-
pyproject = path / "pyproject.toml"
|
|
38
|
-
if req_file.exists() or pyproject.exists():
|
|
39
|
-
return _detect_python(path, req_file if req_file.exists() else pyproject)
|
|
40
|
-
|
|
41
|
-
# --- Node.js ---
|
|
42
|
-
if (path / "package.json").exists():
|
|
43
|
-
return _detect_node(path)
|
|
44
|
-
|
|
45
|
-
# --- Java ---
|
|
46
|
-
if (path / "pom.xml").exists() or (path / "build.gradle").exists():
|
|
47
|
-
return _detect_java(path)
|
|
48
|
-
|
|
49
|
-
# --- Go ---
|
|
50
|
-
if (path / "go.mod").exists():
|
|
51
|
-
return _detect_go(path)
|
|
52
|
-
|
|
53
|
-
return None
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
def _detect_python(path: Path, dep_file: Path) -> dict:
|
|
57
|
-
content = dep_file.read_text(encoding="utf-8", errors="ignore").lower()
|
|
58
|
-
deps = [line.split("==")[0].split(">=")[0].strip() for line in content.splitlines() if line.strip() and not line.startswith("#")]
|
|
59
|
-
|
|
60
|
-
framework = "generic"
|
|
61
|
-
port = 8000
|
|
62
|
-
for dep, (fw, p) in FRAMEWORK_HINTS.items():
|
|
63
|
-
if dep in deps:
|
|
64
|
-
framework = fw
|
|
65
|
-
port = p
|
|
66
|
-
break
|
|
67
|
-
|
|
68
|
-
services = list({SERVICE_HINTS[d] for d in deps if d in SERVICE_HINTS})
|
|
69
|
-
|
|
70
|
-
python_version = _detect_python_version(path)
|
|
71
|
-
|
|
72
|
-
return {
|
|
73
|
-
"language": "python",
|
|
74
|
-
"framework": framework,
|
|
75
|
-
"port": port,
|
|
76
|
-
"services": services,
|
|
77
|
-
"python_version": python_version,
|
|
78
|
-
"dep_file": dep_file.name,
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
def _detect_node(path: Path) -> dict:
|
|
83
|
-
import json
|
|
84
|
-
pkg = json.loads((path / "package.json").read_text(encoding="utf-8"))
|
|
85
|
-
deps = list(pkg.get("dependencies", {}).keys()) + list(pkg.get("devDependencies", {}).keys())
|
|
86
|
-
deps_lower = [d.lower() for d in deps]
|
|
87
|
-
|
|
88
|
-
framework = "express"
|
|
89
|
-
port = 3000
|
|
90
|
-
if "next" in deps_lower:
|
|
91
|
-
framework = "nextjs"
|
|
92
|
-
port = 3000
|
|
93
|
-
elif "nuxt" in deps_lower:
|
|
94
|
-
framework = "nuxtjs"
|
|
95
|
-
port = 3000
|
|
96
|
-
elif "react" in deps_lower:
|
|
97
|
-
framework = "react"
|
|
98
|
-
port = 3000
|
|
99
|
-
|
|
100
|
-
services = list({SERVICE_HINTS[d] for d in deps_lower if d in SERVICE_HINTS})
|
|
101
|
-
|
|
102
|
-
return {
|
|
103
|
-
"language": "nodejs",
|
|
104
|
-
"framework": framework,
|
|
105
|
-
"port": port,
|
|
106
|
-
"services": services,
|
|
107
|
-
"python_version": None,
|
|
108
|
-
"dep_file": "package.json",
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
def _detect_java(path: Path) -> dict:
|
|
113
|
-
return {
|
|
114
|
-
"language": "java",
|
|
115
|
-
"framework": "spring-boot",
|
|
116
|
-
"port": 8080,
|
|
117
|
-
"services": [],
|
|
118
|
-
"python_version": None,
|
|
119
|
-
"dep_file": "pom.xml" if (path / "pom.xml").exists() else "build.gradle",
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
def _detect_go(path: Path) -> dict:
|
|
124
|
-
return {
|
|
125
|
-
"language": "go",
|
|
126
|
-
"framework": "generic",
|
|
127
|
-
"port": 8080,
|
|
128
|
-
"services": [],
|
|
129
|
-
"python_version": None,
|
|
130
|
-
"dep_file": "go.mod",
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
def _detect_python_version(path: Path) -> str:
|
|
135
|
-
"""Cherche la version Python dans .python-version, pyproject.toml ou runtime.txt."""
|
|
136
|
-
for f in [".python-version", "runtime.txt"]:
|
|
137
|
-
p = path / f
|
|
138
|
-
if p.exists():
|
|
139
|
-
content = p.read_text().strip()
|
|
140
|
-
match = re.search(r"3\.\d+", content)
|
|
141
|
-
if match:
|
|
142
|
-
return match.group()
|
|
143
|
-
|
|
144
|
-
pyproject = path / "pyproject.toml"
|
|
145
|
-
if pyproject.exists():
|
|
146
|
-
content = pyproject.read_text()
|
|
147
|
-
match = re.search(r'python_requires\s*=\s*">=\s*(3\.\d+)"', content)
|
|
148
|
-
if match:
|
|
149
|
-
return match.group(1)
|
|
150
|
-
|
|
151
|
-
return "3.11" # défaut
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|