stackai-cli 0.1.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.
- devai/__init__.py +1 -0
- devai/ai.py +32 -0
- devai/cli.py +56 -0
- devai/debugger.py +56 -0
- devai/detector.py +151 -0
- devai/generator.py +243 -0
- stackai_cli-0.1.0.dist-info/METADATA +111 -0
- stackai_cli-0.1.0.dist-info/RECORD +11 -0
- stackai_cli-0.1.0.dist-info/WHEEL +5 -0
- stackai_cli-0.1.0.dist-info/entry_points.txt +2 -0
- stackai_cli-0.1.0.dist-info/top_level.txt +1 -0
devai/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
devai/ai.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import urllib.request
|
|
2
|
+
import urllib.error
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
OLLAMA_URL = "http://localhost:11434/api/generate"
|
|
7
|
+
DEFAULT_MODEL = "llama3"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def ask_ollama(prompt: str, model: str = DEFAULT_MODEL) -> str | None:
|
|
11
|
+
"""Envoie un prompt à Ollama et retourne la réponse complète."""
|
|
12
|
+
payload = json.dumps({
|
|
13
|
+
"model": model,
|
|
14
|
+
"prompt": prompt,
|
|
15
|
+
"stream": False,
|
|
16
|
+
}).encode("utf-8")
|
|
17
|
+
|
|
18
|
+
req = urllib.request.Request(
|
|
19
|
+
OLLAMA_URL,
|
|
20
|
+
data=payload,
|
|
21
|
+
headers={"Content-Type": "application/json"},
|
|
22
|
+
method="POST",
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
with urllib.request.urlopen(req, timeout=60) as resp:
|
|
27
|
+
data = json.loads(resp.read().decode("utf-8"))
|
|
28
|
+
return data.get("response", "").strip()
|
|
29
|
+
except urllib.error.URLError:
|
|
30
|
+
return None
|
|
31
|
+
except json.JSONDecodeError:
|
|
32
|
+
return None
|
devai/cli.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
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')}")
|
devai/debugger.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
import click
|
|
3
|
+
from devai.ai import ask_ollama
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def debug_container(container_name: str):
|
|
7
|
+
"""Récupère les logs Docker et les envoie à l'IA pour analyse."""
|
|
8
|
+
|
|
9
|
+
# 1. Récupérer les logs
|
|
10
|
+
try:
|
|
11
|
+
result = subprocess.run(
|
|
12
|
+
["docker", "logs", "--tail", "100", container_name],
|
|
13
|
+
capture_output=True,
|
|
14
|
+
text=True,
|
|
15
|
+
timeout=15,
|
|
16
|
+
)
|
|
17
|
+
logs = result.stdout + result.stderr
|
|
18
|
+
except FileNotFoundError:
|
|
19
|
+
click.echo(click.style("❌ Docker n'est pas installé ou introuvable dans le PATH.", fg="red"))
|
|
20
|
+
return
|
|
21
|
+
except subprocess.TimeoutExpired:
|
|
22
|
+
click.echo(click.style("❌ Timeout lors de la récupération des logs.", fg="red"))
|
|
23
|
+
return
|
|
24
|
+
|
|
25
|
+
if not logs.strip():
|
|
26
|
+
click.echo(click.style("⚠️ Aucun log trouvé pour ce container.", fg="yellow"))
|
|
27
|
+
return
|
|
28
|
+
|
|
29
|
+
click.echo(click.style("📋 Derniers logs :", fg="cyan"))
|
|
30
|
+
click.echo(click.style("─" * 60, fg="bright_black"))
|
|
31
|
+
# Affiche les 20 dernières lignes pour ne pas surcharger
|
|
32
|
+
lines = logs.strip().splitlines()
|
|
33
|
+
for line in lines[-20:]:
|
|
34
|
+
click.echo(f" {line}")
|
|
35
|
+
click.echo(click.style("─" * 60, fg="bright_black"))
|
|
36
|
+
|
|
37
|
+
# 2. Envoyer à l'IA
|
|
38
|
+
click.echo(click.style("\n🤖 Analyse IA en cours...", fg="cyan"))
|
|
39
|
+
|
|
40
|
+
prompt = f"""Tu es un expert Docker et DevOps. Analyse ces logs de container Docker et réponds en français.
|
|
41
|
+
|
|
42
|
+
LOGS :
|
|
43
|
+
{logs[-3000:]}
|
|
44
|
+
|
|
45
|
+
Réponds avec ce format exact :
|
|
46
|
+
1. PROBLÈME : [explication courte du problème]
|
|
47
|
+
2. CAUSE : [pourquoi ça arrive]
|
|
48
|
+
3. FIX : [commande ou modification exacte à faire]
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
response = ask_ollama(prompt)
|
|
52
|
+
if response:
|
|
53
|
+
click.echo(click.style("\n💡 Analyse :", fg="green", bold=True))
|
|
54
|
+
click.echo(response)
|
|
55
|
+
else:
|
|
56
|
+
click.echo(click.style("❌ Impossible de contacter Ollama. Lance-le avec : ollama serve", fg="red"))
|
devai/detector.py
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
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
|
devai/generator.py
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
DOCKERIGNORE = """\
|
|
5
|
+
__pycache__/
|
|
6
|
+
*.pyc
|
|
7
|
+
*.pyo
|
|
8
|
+
.env
|
|
9
|
+
.venv
|
|
10
|
+
venv/
|
|
11
|
+
.git/
|
|
12
|
+
.gitignore
|
|
13
|
+
*.log
|
|
14
|
+
.DS_Store
|
|
15
|
+
node_modules/
|
|
16
|
+
dist/
|
|
17
|
+
build/
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
# ──────────────────────────────────────────
|
|
21
|
+
# DOCKERFILE TEMPLATES
|
|
22
|
+
# ──────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
PYTHON_DOCKERFILE = """\
|
|
25
|
+
FROM python:{python_version}-slim
|
|
26
|
+
|
|
27
|
+
WORKDIR /app
|
|
28
|
+
|
|
29
|
+
# Installer les dépendances système minimales
|
|
30
|
+
RUN apt-get update && apt-get install -y --no-install-recommends \\
|
|
31
|
+
curl \\
|
|
32
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
33
|
+
|
|
34
|
+
# Copier et installer les dépendances Python
|
|
35
|
+
COPY {dep_file} .
|
|
36
|
+
RUN pip install --no-cache-dir -r {dep_file}
|
|
37
|
+
|
|
38
|
+
# Copier le code source
|
|
39
|
+
COPY . .
|
|
40
|
+
|
|
41
|
+
EXPOSE {port}
|
|
42
|
+
|
|
43
|
+
CMD {cmd}
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
NODE_DOCKERFILE = """\
|
|
47
|
+
FROM node:20-slim
|
|
48
|
+
|
|
49
|
+
WORKDIR /app
|
|
50
|
+
|
|
51
|
+
COPY package*.json ./
|
|
52
|
+
RUN npm ci --only=production
|
|
53
|
+
|
|
54
|
+
COPY . .
|
|
55
|
+
|
|
56
|
+
EXPOSE {port}
|
|
57
|
+
|
|
58
|
+
CMD ["node", "index.js"]
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
JAVA_DOCKERFILE = """\
|
|
62
|
+
FROM maven:3.9-eclipse-temurin-17 AS build
|
|
63
|
+
WORKDIR /app
|
|
64
|
+
COPY pom.xml .
|
|
65
|
+
RUN mvn dependency:go-offline
|
|
66
|
+
COPY src ./src
|
|
67
|
+
RUN mvn package -DskipTests
|
|
68
|
+
|
|
69
|
+
FROM eclipse-temurin:17-jre-slim
|
|
70
|
+
WORKDIR /app
|
|
71
|
+
COPY --from=build /app/target/*.jar app.jar
|
|
72
|
+
EXPOSE 8080
|
|
73
|
+
CMD ["java", "-jar", "app.jar"]
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
GO_DOCKERFILE = """\
|
|
77
|
+
FROM golang:1.22-alpine AS build
|
|
78
|
+
WORKDIR /app
|
|
79
|
+
COPY go.mod go.sum ./
|
|
80
|
+
RUN go mod download
|
|
81
|
+
COPY . .
|
|
82
|
+
RUN go build -o main .
|
|
83
|
+
|
|
84
|
+
FROM alpine:latest
|
|
85
|
+
WORKDIR /app
|
|
86
|
+
COPY --from=build /app/main .
|
|
87
|
+
EXPOSE 8080
|
|
88
|
+
CMD ["./main"]
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
# ──────────────────────────────────────────
|
|
92
|
+
# SERVICE TEMPLATES pour docker-compose
|
|
93
|
+
# ──────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
SERVICE_TEMPLATES = {
|
|
96
|
+
"postgres": """\
|
|
97
|
+
postgres:
|
|
98
|
+
image: postgres:16-alpine
|
|
99
|
+
environment:
|
|
100
|
+
POSTGRES_USER: user
|
|
101
|
+
POSTGRES_PASSWORD: password
|
|
102
|
+
POSTGRES_DB: appdb
|
|
103
|
+
ports:
|
|
104
|
+
- "5432:5432"
|
|
105
|
+
volumes:
|
|
106
|
+
- postgres_data:/var/lib/postgresql/data
|
|
107
|
+
""",
|
|
108
|
+
"redis": """\
|
|
109
|
+
redis:
|
|
110
|
+
image: redis:7-alpine
|
|
111
|
+
ports:
|
|
112
|
+
- "6379:6379"
|
|
113
|
+
""",
|
|
114
|
+
"mongodb": """\
|
|
115
|
+
mongodb:
|
|
116
|
+
image: mongo:7
|
|
117
|
+
environment:
|
|
118
|
+
MONGO_INITDB_ROOT_USERNAME: user
|
|
119
|
+
MONGO_INITDB_ROOT_PASSWORD: password
|
|
120
|
+
ports:
|
|
121
|
+
- "27017:27017"
|
|
122
|
+
volumes:
|
|
123
|
+
- mongo_data:/data/db
|
|
124
|
+
""",
|
|
125
|
+
"mysql": """\
|
|
126
|
+
mysql:
|
|
127
|
+
image: mysql:8
|
|
128
|
+
environment:
|
|
129
|
+
MYSQL_ROOT_PASSWORD: rootpassword
|
|
130
|
+
MYSQL_DATABASE: appdb
|
|
131
|
+
MYSQL_USER: user
|
|
132
|
+
MYSQL_PASSWORD: password
|
|
133
|
+
ports:
|
|
134
|
+
- "3306:3306"
|
|
135
|
+
volumes:
|
|
136
|
+
- mysql_data:/var/lib/mysql
|
|
137
|
+
""",
|
|
138
|
+
"elasticsearch": """\
|
|
139
|
+
elasticsearch:
|
|
140
|
+
image: elasticsearch:8.13.0
|
|
141
|
+
environment:
|
|
142
|
+
- discovery.type=single-node
|
|
143
|
+
- xpack.security.enabled=false
|
|
144
|
+
ports:
|
|
145
|
+
- "9200:9200"
|
|
146
|
+
""",
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
VOLUME_NAMES = {
|
|
150
|
+
"postgres": "postgres_data",
|
|
151
|
+
"mongodb": "mongo_data",
|
|
152
|
+
"mysql": "mysql_data",
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
FRAMEWORK_CMDS = {
|
|
156
|
+
"fastapi": '["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "{port}"]',
|
|
157
|
+
"flask": '["flask", "run", "--host=0.0.0.0", "--port={port}"]',
|
|
158
|
+
"django": '["python", "manage.py", "runserver", "0.0.0.0:{port}"]',
|
|
159
|
+
"generic": '["python", "main.py"]',
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def generate_files(project_path: str, stack: dict) -> list[str]:
|
|
164
|
+
path = Path(project_path)
|
|
165
|
+
generated = []
|
|
166
|
+
|
|
167
|
+
# Dockerfile
|
|
168
|
+
dockerfile_content = _build_dockerfile(stack)
|
|
169
|
+
_write(path / "Dockerfile", dockerfile_content)
|
|
170
|
+
generated.append("Dockerfile")
|
|
171
|
+
|
|
172
|
+
# .dockerignore
|
|
173
|
+
_write(path / ".dockerignore", DOCKERIGNORE)
|
|
174
|
+
generated.append(".dockerignore")
|
|
175
|
+
|
|
176
|
+
# docker-compose.yaml (seulement si services détectés ou toujours utile)
|
|
177
|
+
compose_content = _build_compose(stack)
|
|
178
|
+
_write(path / "docker-compose.yaml", compose_content)
|
|
179
|
+
generated.append("docker-compose.yaml")
|
|
180
|
+
|
|
181
|
+
return generated
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _build_dockerfile(stack: dict) -> str:
|
|
185
|
+
lang = stack["language"]
|
|
186
|
+
port = stack["port"]
|
|
187
|
+
|
|
188
|
+
if lang == "python":
|
|
189
|
+
cmd = FRAMEWORK_CMDS.get(stack.get("framework", "generic"), FRAMEWORK_CMDS["generic"])
|
|
190
|
+
cmd = cmd.replace("{port}", str(port))
|
|
191
|
+
return PYTHON_DOCKERFILE.format(
|
|
192
|
+
python_version=stack.get("python_version", "3.11"),
|
|
193
|
+
dep_file=stack.get("dep_file", "requirements.txt"),
|
|
194
|
+
port=port,
|
|
195
|
+
cmd=cmd,
|
|
196
|
+
)
|
|
197
|
+
elif lang == "nodejs":
|
|
198
|
+
return NODE_DOCKERFILE.format(port=port)
|
|
199
|
+
elif lang == "java":
|
|
200
|
+
return JAVA_DOCKERFILE
|
|
201
|
+
elif lang == "go":
|
|
202
|
+
return GO_DOCKERFILE
|
|
203
|
+
else:
|
|
204
|
+
return f"FROM ubuntu:22.04\nWORKDIR /app\nCOPY . .\nEXPOSE {port}\n"
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _build_compose(stack: dict) -> str:
|
|
208
|
+
lang = stack["language"]
|
|
209
|
+
port = stack["port"]
|
|
210
|
+
services = stack.get("services", [])
|
|
211
|
+
framework = stack.get("framework", "generic")
|
|
212
|
+
|
|
213
|
+
# Depends_on
|
|
214
|
+
depends = ""
|
|
215
|
+
if services:
|
|
216
|
+
depends = "\n depends_on:\n" + "".join(f" - {s}\n" for s in services)
|
|
217
|
+
|
|
218
|
+
app_service = f"""\
|
|
219
|
+
app:
|
|
220
|
+
build: .
|
|
221
|
+
ports:
|
|
222
|
+
- "{port}:{port}"
|
|
223
|
+
env_file:
|
|
224
|
+
- .env{depends}
|
|
225
|
+
"""
|
|
226
|
+
|
|
227
|
+
extra_services = "".join(SERVICE_TEMPLATES.get(s, "") for s in services)
|
|
228
|
+
|
|
229
|
+
volumes_needed = [VOLUME_NAMES[s] for s in services if s in VOLUME_NAMES]
|
|
230
|
+
volumes_block = ""
|
|
231
|
+
if volumes_needed:
|
|
232
|
+
volumes_block = "\nvolumes:\n" + "".join(f" {v}:\n" for v in volumes_needed)
|
|
233
|
+
|
|
234
|
+
return f"""\
|
|
235
|
+
version: '3.9'
|
|
236
|
+
|
|
237
|
+
services:
|
|
238
|
+
{app_service}
|
|
239
|
+
{extra_services}{volumes_block}"""
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _write(filepath: Path, content: str):
|
|
243
|
+
filepath.write_text(content, encoding="utf-8")
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: stackai-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: AI-powered Docker assistant — detects your stack, generates Dockerfile & compose, debugs containers.
|
|
5
|
+
Author-email: El Mehdi Boutahar <boutahar.elmehdi@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/MehdiB7/devai
|
|
8
|
+
Project-URL: Repository, https://github.com/MehdiB7/devai
|
|
9
|
+
Project-URL: Bug-Tracker, https://github.com/MehdiB7/devai/issues
|
|
10
|
+
Keywords: docker,ai,devops,cli,ollama,automation
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
17
|
+
Requires-Python: >=3.10
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
Requires-Dist: click>=8.1
|
|
20
|
+
|
|
21
|
+
# devai
|
|
22
|
+
|
|
23
|
+
> AI-powered Docker assistant. No config needed.
|
|
24
|
+
|
|
25
|
+
`devai` analyse ton projet, génère le Dockerfile et le docker-compose, et debug tes containers avec l'IA — le tout en une commande.
|
|
26
|
+
|
|
27
|
+
## Installation
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install stackai-cli
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
> Requiert [Ollama](https://ollama.com) installé et `ollama pull llama3` pour le mode debug AI.
|
|
34
|
+
|
|
35
|
+
## Utilisation
|
|
36
|
+
|
|
37
|
+
### `devai init` — Générer les fichiers Docker
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
cd mon-projet
|
|
41
|
+
stackai init
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
🔍 Analyse du projet...
|
|
46
|
+
✅ Stack détectée : python / fastapi + postgres + redis
|
|
47
|
+
⚙️ Génération des fichiers Docker...
|
|
48
|
+
✅ Dockerfile créé
|
|
49
|
+
✅ .dockerignore créé
|
|
50
|
+
✅ docker-compose.yaml créé
|
|
51
|
+
|
|
52
|
+
🚀 Prêt ! Lance avec : docker compose up -d
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### `devai scan` — Voir la stack sans rien générer
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
stackai scan
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
📦 Stack détectée :
|
|
63
|
+
Langage : python
|
|
64
|
+
Framework : fastapi
|
|
65
|
+
Port : 8000
|
|
66
|
+
Services : postgres, redis
|
|
67
|
+
Python ver. : 3.11
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### `devai debug` — Analyser un container en erreur
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
stackai debug mon-container
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
📋 Derniers logs :
|
|
78
|
+
─────────────────────────────────────────
|
|
79
|
+
Error: could not connect to postgres...
|
|
80
|
+
─────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
🤖 Analyse IA en cours...
|
|
83
|
+
|
|
84
|
+
💡 Analyse :
|
|
85
|
+
1. PROBLÈME : L'application ne peut pas se connecter à PostgreSQL
|
|
86
|
+
2. CAUSE : Le container postgres n'est pas encore prêt au démarrage de l'app
|
|
87
|
+
3. FIX : Ajoute `depends_on: [postgres]` dans ton docker-compose.yaml
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Stacks supportées
|
|
91
|
+
|
|
92
|
+
| Langage | Frameworks détectés |
|
|
93
|
+
|---------|-------------------|
|
|
94
|
+
| Python | FastAPI, Flask, Django, générique |
|
|
95
|
+
| Node.js | Express, Next.js, Nuxt.js, React |
|
|
96
|
+
| Java | Spring Boot (Maven/Gradle) |
|
|
97
|
+
| Go | générique |
|
|
98
|
+
|
|
99
|
+
## Services auto-détectés
|
|
100
|
+
|
|
101
|
+
`postgres` · `redis` · `mongodb` · `mysql` · `elasticsearch`
|
|
102
|
+
|
|
103
|
+
## Prérequis
|
|
104
|
+
|
|
105
|
+
- Python 3.10+
|
|
106
|
+
- Docker installé
|
|
107
|
+
- [Ollama](https://ollama.com) + `ollama pull llama3` (pour `devai debug`)
|
|
108
|
+
|
|
109
|
+
## Licence
|
|
110
|
+
|
|
111
|
+
MIT
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
devai/__init__.py,sha256=kUR5RAFc7HCeiqdlX36dZOHkUI5wI6V_43RpEcD8b-0,22
|
|
2
|
+
devai/ai.py,sha256=K5T8_7BqkP5gLta0UsjoI-S-k5UGdALqy2DpPvWDfUg,852
|
|
3
|
+
devai/cli.py,sha256=4jK_UZmlGHZmMH0tZOuTEn3qZnMkSurBzWqQVJ88zw8,2183
|
|
4
|
+
devai/debugger.py,sha256=M1cRJD8c462EAxqbE7zMKf3ndM7PHFFpZDkwzmoFE7g,1909
|
|
5
|
+
devai/detector.py,sha256=pPqwp3vsKDInoqrWNOLwTACymeq6ZdA1Tg4gy43reHI,4094
|
|
6
|
+
devai/generator.py,sha256=OCMGfp5qAl-rSvHsjsN65TNkHTMMLKXJX47K3a_CgK8,5627
|
|
7
|
+
stackai_cli-0.1.0.dist-info/METADATA,sha256=WxCh-fTIo67nfR7avAeLFXzKO8qBQLC9pF1SxT1TGZw,2967
|
|
8
|
+
stackai_cli-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
9
|
+
stackai_cli-0.1.0.dist-info/entry_points.txt,sha256=NLmdAbALXQnZwvZM0Y8YwsqwH_pArHd6Da8f4pHcK8U,43
|
|
10
|
+
stackai_cli-0.1.0.dist-info/top_level.txt,sha256=Q-WXGK56TxXgAMLtaB1F77uzL4PBxPW22GkcKjJVdTc,6
|
|
11
|
+
stackai_cli-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
devai
|