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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stackai-cli
3
- Version: 0.1.3
3
+ Version: 0.2.0
4
4
  Summary: AI-powered Docker assistant — detects your stack, generates Dockerfile & compose, debugs containers.
5
5
  Author-email: El Mehdi Boutahar <boutahar.elmehdi@gmail.com>
6
6
  License: MIT
@@ -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.1.3"
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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stackai-cli
3
- Version: 0.1.3
3
+ Version: 0.2.0
4
4
  Summary: AI-powered Docker assistant — detects your stack, generates Dockerfile & compose, debugs containers.
5
5
  Author-email: El Mehdi Boutahar <boutahar.elmehdi@gmail.com>
6
6
  License: MIT
@@ -1 +0,0 @@
1
- __version__ = "0.1.3"
@@ -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