dockermind 0.1.2__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.
- dockermind/__init__.py +5 -0
- dockermind/analyzer.py +285 -0
- dockermind/cli.py +288 -0
- dockermind/compose_generator.py +43 -0
- dockermind/dockerfile_generator.py +389 -0
- dockermind/doctor.py +88 -0
- dockermind/runner.py +139 -0
- dockermind/utils.py +141 -0
- dockermind-0.1.2.dist-info/METADATA +119 -0
- dockermind-0.1.2.dist-info/RECORD +13 -0
- dockermind-0.1.2.dist-info/WHEEL +5 -0
- dockermind-0.1.2.dist-info/entry_points.txt +2 -0
- dockermind-0.1.2.dist-info/top_level.txt +1 -0
dockermind/__init__.py
ADDED
dockermind/analyzer.py
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Project analyzer -- detects project type, framework, version, port, and scripts.
|
|
3
|
+
|
|
4
|
+
Only Node.js and Python are supported in v1.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import re
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
|
|
14
|
+
from dockermind.utils import file_exists, read_file, extract_port_from_source, yellow
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# ---------------------------------------------------------------------------
|
|
18
|
+
# Analysis result dataclass
|
|
19
|
+
# ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class AnalysisResult:
|
|
24
|
+
"""Everything dockermind needs to know about the target project."""
|
|
25
|
+
|
|
26
|
+
# Core detection
|
|
27
|
+
language: str | None = None # "node" | "python" | None
|
|
28
|
+
framework: str | None = None # "express" | "fastapi" | "flask" | None
|
|
29
|
+
|
|
30
|
+
# Runtime version
|
|
31
|
+
node_version: str = "18" # Default Node version
|
|
32
|
+
python_version: str = "3.11" # Default Python version
|
|
33
|
+
|
|
34
|
+
# Scripts (Node)
|
|
35
|
+
has_build_script: bool = False
|
|
36
|
+
build_script: str | None = None
|
|
37
|
+
start_script: str | None = None
|
|
38
|
+
|
|
39
|
+
# Entry point (Python)
|
|
40
|
+
python_entry_module: str | None = None # e.g. "main:app"
|
|
41
|
+
uses_uvicorn: bool = False
|
|
42
|
+
|
|
43
|
+
# Port
|
|
44
|
+
port: int | None = None
|
|
45
|
+
port_is_default: bool = False # True if we fell back to a default
|
|
46
|
+
|
|
47
|
+
# Package manager
|
|
48
|
+
has_package_lock: bool = False
|
|
49
|
+
has_yarn_lock: bool = False
|
|
50
|
+
has_pnpm_lock: bool = False
|
|
51
|
+
|
|
52
|
+
# Raw Python dependency names (lowercase) for native-package inference
|
|
53
|
+
python_deps: set[str] = field(default_factory=set)
|
|
54
|
+
|
|
55
|
+
# Warnings generated during analysis
|
|
56
|
+
warnings: list[str] = field(default_factory=list)
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def runtime_version(self) -> str:
|
|
60
|
+
"""Return the resolved runtime version string."""
|
|
61
|
+
if self.language == "node":
|
|
62
|
+
return self.node_version
|
|
63
|
+
return self.python_version
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# ---------------------------------------------------------------------------
|
|
67
|
+
# Analyzer
|
|
68
|
+
# ---------------------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class Analyzer:
|
|
72
|
+
"""Analyse a project directory and return an AnalysisResult."""
|
|
73
|
+
|
|
74
|
+
def __init__(self, project_path: str) -> None:
|
|
75
|
+
if not os.path.isdir(project_path):
|
|
76
|
+
raise FileNotFoundError(f"Project path does not exist: {project_path}")
|
|
77
|
+
self.path = project_path
|
|
78
|
+
|
|
79
|
+
def run(self) -> AnalysisResult:
|
|
80
|
+
"""Run full analysis pipeline and return the result."""
|
|
81
|
+
result = AnalysisResult()
|
|
82
|
+
|
|
83
|
+
has_node = file_exists(self.path, "package.json")
|
|
84
|
+
has_python = (
|
|
85
|
+
file_exists(self.path, "requirements.txt")
|
|
86
|
+
or file_exists(self.path, "pyproject.toml")
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
# Language detection -- only one allowed
|
|
90
|
+
if has_node and has_python:
|
|
91
|
+
# Hard stop: ambiguous multi-runtime projects are not supported.
|
|
92
|
+
raise SystemExit(
|
|
93
|
+
"Error: Multiple backend runtimes detected (Node.js and Python).\n"
|
|
94
|
+
"Please run dockermind inside the specific service directory."
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
if has_node:
|
|
98
|
+
result.language = "node"
|
|
99
|
+
self._analyse_node(result)
|
|
100
|
+
elif has_python:
|
|
101
|
+
result.language = "python"
|
|
102
|
+
self._analyse_python(result)
|
|
103
|
+
else:
|
|
104
|
+
# Unsupported or empty project
|
|
105
|
+
result.language = None
|
|
106
|
+
|
|
107
|
+
return result
|
|
108
|
+
|
|
109
|
+
# ------------------------------------------------------------------
|
|
110
|
+
# Node.js analysis
|
|
111
|
+
# ------------------------------------------------------------------
|
|
112
|
+
|
|
113
|
+
def _analyse_node(self, result: AnalysisResult) -> None:
|
|
114
|
+
"""Populate *result* with Node.js-specific details."""
|
|
115
|
+
raw = read_file(self.path, "package.json")
|
|
116
|
+
if not raw:
|
|
117
|
+
result.warnings.append("package.json exists but could not be read.")
|
|
118
|
+
return
|
|
119
|
+
|
|
120
|
+
try:
|
|
121
|
+
pkg = json.loads(raw)
|
|
122
|
+
except json.JSONDecodeError:
|
|
123
|
+
result.warnings.append("package.json is not valid JSON.")
|
|
124
|
+
return
|
|
125
|
+
|
|
126
|
+
# -- Node version from engines field --
|
|
127
|
+
engines = pkg.get("engines", {})
|
|
128
|
+
requested_version = engines.get("node", "")
|
|
129
|
+
if requested_version:
|
|
130
|
+
result.node_version = self._parse_node_version(requested_version)
|
|
131
|
+
# else: stays at default "18"
|
|
132
|
+
|
|
133
|
+
# -- Scripts --
|
|
134
|
+
scripts = pkg.get("scripts", {})
|
|
135
|
+
if "build" in scripts:
|
|
136
|
+
result.has_build_script = True
|
|
137
|
+
result.build_script = scripts["build"]
|
|
138
|
+
result.start_script = scripts.get("start")
|
|
139
|
+
|
|
140
|
+
# -- Framework detection --
|
|
141
|
+
all_deps = {
|
|
142
|
+
**pkg.get("dependencies", {}),
|
|
143
|
+
**pkg.get("devDependencies", {}),
|
|
144
|
+
}
|
|
145
|
+
if "express" in all_deps:
|
|
146
|
+
result.framework = "express"
|
|
147
|
+
elif "fastify" in all_deps:
|
|
148
|
+
result.framework = "fastify"
|
|
149
|
+
elif "koa" in all_deps:
|
|
150
|
+
result.framework = "koa"
|
|
151
|
+
# For v1 we just note whatever framework; Dockerfile is the same.
|
|
152
|
+
|
|
153
|
+
# -- Lock file detection --
|
|
154
|
+
result.has_package_lock = file_exists(self.path, "package-lock.json")
|
|
155
|
+
result.has_yarn_lock = file_exists(self.path, "yarn.lock")
|
|
156
|
+
result.has_pnpm_lock = file_exists(self.path, "pnpm-lock.yaml")
|
|
157
|
+
|
|
158
|
+
# -- Port detection --
|
|
159
|
+
result.port = self._detect_node_port(pkg)
|
|
160
|
+
if result.port is None:
|
|
161
|
+
result.port = 3000
|
|
162
|
+
result.port_is_default = True
|
|
163
|
+
result.warnings.append(
|
|
164
|
+
"Could not detect port from source. Defaulting to 3000."
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
def _parse_node_version(self, version_spec: str) -> str:
|
|
168
|
+
"""Extract a major version number from an engines.node specifier.
|
|
169
|
+
|
|
170
|
+
Examples:
|
|
171
|
+
">=18" -> "18"
|
|
172
|
+
"^20.0.0" -> "20"
|
|
173
|
+
"18.x" -> "18"
|
|
174
|
+
">=18 <22" -> "18"
|
|
175
|
+
"""
|
|
176
|
+
match = re.search(r"(\d{1,3})", version_spec)
|
|
177
|
+
if match:
|
|
178
|
+
return match.group(1)
|
|
179
|
+
return "18" # safe fallback
|
|
180
|
+
|
|
181
|
+
def _detect_node_port(self, pkg: dict) -> int | None:
|
|
182
|
+
"""Try to detect port from source files or scripts."""
|
|
183
|
+
# Check common entry points for .listen(PORT)
|
|
184
|
+
candidates = ["server.js", "index.js", "app.js", "src/index.js", "src/server.js",
|
|
185
|
+
"src/app.js", "dist/index.js", "server.ts", "index.ts", "src/index.ts"]
|
|
186
|
+
|
|
187
|
+
for filename in candidates:
|
|
188
|
+
source = read_file(self.path, filename)
|
|
189
|
+
if source:
|
|
190
|
+
port = extract_port_from_source(source)
|
|
191
|
+
if port:
|
|
192
|
+
return port
|
|
193
|
+
|
|
194
|
+
return None
|
|
195
|
+
|
|
196
|
+
# ------------------------------------------------------------------
|
|
197
|
+
# Python analysis
|
|
198
|
+
# ------------------------------------------------------------------
|
|
199
|
+
|
|
200
|
+
def _analyse_python(self, result: AnalysisResult) -> None:
|
|
201
|
+
"""Populate *result* with Python-specific details."""
|
|
202
|
+
|
|
203
|
+
# Gather all dependency names
|
|
204
|
+
deps_lower: set[str] = set()
|
|
205
|
+
|
|
206
|
+
# From requirements.txt
|
|
207
|
+
reqs = read_file(self.path, "requirements.txt")
|
|
208
|
+
if reqs:
|
|
209
|
+
for line in reqs.splitlines():
|
|
210
|
+
line = line.strip()
|
|
211
|
+
if not line or line.startswith("#"):
|
|
212
|
+
continue
|
|
213
|
+
# "fastapi==0.100.0" -> "fastapi"
|
|
214
|
+
dep_name = re.split(r"[>=<!~\[]", line)[0].strip().lower()
|
|
215
|
+
if dep_name:
|
|
216
|
+
deps_lower.add(dep_name)
|
|
217
|
+
|
|
218
|
+
# From pyproject.toml (simple parse -- no toml library needed for v1)
|
|
219
|
+
pyproject = read_file(self.path, "pyproject.toml")
|
|
220
|
+
if pyproject:
|
|
221
|
+
# Quick regex extraction for dependencies list
|
|
222
|
+
for match in re.finditer(r'"([a-zA-Z0-9_-]+)', pyproject):
|
|
223
|
+
deps_lower.add(match.group(1).lower())
|
|
224
|
+
|
|
225
|
+
# Expose raw deps for native-package inference in dockerfile_generator
|
|
226
|
+
result.python_deps = deps_lower
|
|
227
|
+
|
|
228
|
+
# -- Framework detection --
|
|
229
|
+
if "fastapi" in deps_lower:
|
|
230
|
+
result.framework = "fastapi"
|
|
231
|
+
result.port = 8000
|
|
232
|
+
elif "flask" in deps_lower:
|
|
233
|
+
result.framework = "flask"
|
|
234
|
+
result.port = 5000
|
|
235
|
+
else:
|
|
236
|
+
# Unknown Python framework -- set a sensible default
|
|
237
|
+
result.framework = None
|
|
238
|
+
result.port = 8000
|
|
239
|
+
result.warnings.append(
|
|
240
|
+
"No recognised framework (FastAPI/Flask). Defaulting to port 8000."
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
# -- Uvicorn detection --
|
|
244
|
+
if "uvicorn" in deps_lower:
|
|
245
|
+
result.uses_uvicorn = True
|
|
246
|
+
|
|
247
|
+
# -- Detect entry module --
|
|
248
|
+
result.python_entry_module = self._detect_python_entry(result)
|
|
249
|
+
|
|
250
|
+
# -- Python version from pyproject.toml requires-python --
|
|
251
|
+
if pyproject:
|
|
252
|
+
match = re.search(r'requires-python\s*=\s*["\']([^"\']+)["\']', pyproject)
|
|
253
|
+
if match:
|
|
254
|
+
ver_match = re.search(r"(\d+\.\d+)", match.group(1))
|
|
255
|
+
if ver_match:
|
|
256
|
+
result.python_version = ver_match.group(1)
|
|
257
|
+
|
|
258
|
+
def _detect_python_entry(self, result: AnalysisResult) -> str | None:
|
|
259
|
+
"""Detect the Python entry module (e.g. 'main:app')."""
|
|
260
|
+
|
|
261
|
+
# Look for common entry files
|
|
262
|
+
candidates = ["main.py", "app.py", "application.py", "server.py",
|
|
263
|
+
"src/main.py", "src/app.py"]
|
|
264
|
+
|
|
265
|
+
for filename in candidates:
|
|
266
|
+
source = read_file(self.path, filename)
|
|
267
|
+
if source is None:
|
|
268
|
+
continue
|
|
269
|
+
|
|
270
|
+
# Look for FastAPI() or Flask() app instantiation
|
|
271
|
+
app_match = re.search(
|
|
272
|
+
r"(\w+)\s*=\s*(?:FastAPI|Flask)\s*\(", source
|
|
273
|
+
)
|
|
274
|
+
if app_match:
|
|
275
|
+
app_var = app_match.group(1)
|
|
276
|
+
module_name = filename.replace("/", ".").replace(".py", "")
|
|
277
|
+
return f"{module_name}:{app_var}"
|
|
278
|
+
|
|
279
|
+
# Fallback: if main.py exists, assume main:app
|
|
280
|
+
if file_exists(self.path, "main.py"):
|
|
281
|
+
return "main:app"
|
|
282
|
+
if file_exists(self.path, "app.py"):
|
|
283
|
+
return "app:app"
|
|
284
|
+
|
|
285
|
+
return None
|
dockermind/cli.py
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Dockermind CLI -- built with Typer.
|
|
3
|
+
|
|
4
|
+
Commands:
|
|
5
|
+
dockermind init Analyse project and generate Dockerfile
|
|
6
|
+
dockermind build Build Docker image from generated Dockerfile
|
|
7
|
+
dockermind run Run the built Docker image
|
|
8
|
+
dockermind doctor Check Docker environment health
|
|
9
|
+
dockermind version Show dockermind version
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import os
|
|
15
|
+
import sys
|
|
16
|
+
|
|
17
|
+
import typer
|
|
18
|
+
|
|
19
|
+
from dockermind import __version__
|
|
20
|
+
from dockermind.analyzer import Analyzer
|
|
21
|
+
from dockermind.compose_generator import generate_compose
|
|
22
|
+
from dockermind.doctor import run_doctor
|
|
23
|
+
from dockermind.dockerfile_generator import generate_dockerfile, generate_dockerignore
|
|
24
|
+
from dockermind.runner import docker_build, docker_run
|
|
25
|
+
from dockermind.utils import (
|
|
26
|
+
bold,
|
|
27
|
+
confirm,
|
|
28
|
+
cyan,
|
|
29
|
+
dim,
|
|
30
|
+
file_exists,
|
|
31
|
+
green,
|
|
32
|
+
red,
|
|
33
|
+
write_file,
|
|
34
|
+
yellow,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
# ---------------------------------------------------------------------------
|
|
38
|
+
# Typer app
|
|
39
|
+
# ---------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
app = typer.Typer(
|
|
42
|
+
name="dockermind",
|
|
43
|
+
help="Smart Dockerfile generation CLI for Node.js and Python projects.",
|
|
44
|
+
add_completion=False,
|
|
45
|
+
no_args_is_help=True,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _banner() -> None:
|
|
50
|
+
"""Print the dockermind banner."""
|
|
51
|
+
typer.echo()
|
|
52
|
+
typer.echo(f" {bold('dockermind')} {dim(f'v{__version__}')}")
|
|
53
|
+
typer.echo(f" {dim('Smart Dockerfile generation for Node.js & Python')}")
|
|
54
|
+
typer.echo()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# ---------------------------------------------------------------------------
|
|
58
|
+
# init
|
|
59
|
+
# ---------------------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@app.command()
|
|
63
|
+
def init(
|
|
64
|
+
path: str = typer.Argument(
|
|
65
|
+
".",
|
|
66
|
+
help="Path to the project directory to analyse.",
|
|
67
|
+
),
|
|
68
|
+
compose: bool = typer.Option(
|
|
69
|
+
False,
|
|
70
|
+
"--compose",
|
|
71
|
+
help="Also generate docker-compose.yml.",
|
|
72
|
+
),
|
|
73
|
+
dry_run: bool = typer.Option(
|
|
74
|
+
False,
|
|
75
|
+
"--dry-run",
|
|
76
|
+
help="Show generated files without writing to disk.",
|
|
77
|
+
),
|
|
78
|
+
force: bool = typer.Option(
|
|
79
|
+
False,
|
|
80
|
+
"--force",
|
|
81
|
+
help="Overwrite existing Dockerfile without asking.",
|
|
82
|
+
),
|
|
83
|
+
) -> None:
|
|
84
|
+
"""Analyse a project and generate an optimised Dockerfile."""
|
|
85
|
+
_banner()
|
|
86
|
+
|
|
87
|
+
project_path = os.path.abspath(path)
|
|
88
|
+
|
|
89
|
+
if not os.path.isdir(project_path):
|
|
90
|
+
typer.echo(red(f" Error: '{project_path}' is not a valid directory."))
|
|
91
|
+
raise typer.Exit(code=1)
|
|
92
|
+
|
|
93
|
+
typer.echo(dim(f" Analysing: {project_path}"))
|
|
94
|
+
typer.echo()
|
|
95
|
+
|
|
96
|
+
# -- Run analysis --
|
|
97
|
+
analyzer = Analyzer(project_path)
|
|
98
|
+
result = analyzer.run()
|
|
99
|
+
|
|
100
|
+
if result.language is None:
|
|
101
|
+
typer.echo(red(" Error: Could not detect a supported project type."))
|
|
102
|
+
typer.echo(dim(" Supported: Node.js (package.json) or Python (requirements.txt / pyproject.toml)"))
|
|
103
|
+
raise typer.Exit(code=1)
|
|
104
|
+
|
|
105
|
+
# -- Display analysis summary --
|
|
106
|
+
typer.echo(f" {bold('Project Analysis')}")
|
|
107
|
+
typer.echo(" " + "-" * 40)
|
|
108
|
+
typer.echo(f" Language: {cyan(result.language)}")
|
|
109
|
+
|
|
110
|
+
if result.framework:
|
|
111
|
+
typer.echo(f" Framework: {cyan(result.framework)}")
|
|
112
|
+
|
|
113
|
+
typer.echo(f" Version: {cyan(result.runtime_version)}")
|
|
114
|
+
typer.echo(f" Port: {cyan(str(result.port))}")
|
|
115
|
+
|
|
116
|
+
if result.language == "node":
|
|
117
|
+
typer.echo(f" Build step: {cyan('yes' if result.has_build_script else 'no')}")
|
|
118
|
+
if result.start_script:
|
|
119
|
+
typer.echo(f" Start: {dim(result.start_script)}")
|
|
120
|
+
|
|
121
|
+
if result.language == "python":
|
|
122
|
+
if result.python_entry_module:
|
|
123
|
+
typer.echo(f" Entry: {dim(result.python_entry_module)}")
|
|
124
|
+
typer.echo(f" Uvicorn: {cyan('yes' if result.uses_uvicorn else 'no')}")
|
|
125
|
+
|
|
126
|
+
# Show warnings
|
|
127
|
+
if result.warnings:
|
|
128
|
+
typer.echo()
|
|
129
|
+
for w in result.warnings:
|
|
130
|
+
typer.echo(f" {yellow(f'Warning: {w}')}")
|
|
131
|
+
|
|
132
|
+
typer.echo()
|
|
133
|
+
|
|
134
|
+
# -- Generate files --
|
|
135
|
+
dockerfile_content = generate_dockerfile(result)
|
|
136
|
+
dockerignore_content = generate_dockerignore(result)
|
|
137
|
+
compose_content = generate_compose(result) if compose else None
|
|
138
|
+
|
|
139
|
+
# -- Dry run: just print and exit --
|
|
140
|
+
if dry_run:
|
|
141
|
+
typer.echo(bold(" --- Dockerfile ---"))
|
|
142
|
+
typer.echo()
|
|
143
|
+
typer.echo(dockerfile_content)
|
|
144
|
+
|
|
145
|
+
typer.echo(bold(" --- .dockerignore ---"))
|
|
146
|
+
typer.echo()
|
|
147
|
+
typer.echo(dockerignore_content)
|
|
148
|
+
|
|
149
|
+
if compose_content:
|
|
150
|
+
typer.echo(bold(" --- docker-compose.yml ---"))
|
|
151
|
+
typer.echo()
|
|
152
|
+
typer.echo(compose_content)
|
|
153
|
+
|
|
154
|
+
raise typer.Exit(code=0)
|
|
155
|
+
|
|
156
|
+
# -- Safety: check for existing Dockerfile --
|
|
157
|
+
dockerfile_path = os.path.join(project_path, "Dockerfile")
|
|
158
|
+
if os.path.isfile(dockerfile_path) and not force:
|
|
159
|
+
typer.echo(yellow(" A Dockerfile already exists in this project."))
|
|
160
|
+
if not confirm(" Overwrite?"):
|
|
161
|
+
typer.echo(dim(" Aborted."))
|
|
162
|
+
raise typer.Exit(code=0)
|
|
163
|
+
|
|
164
|
+
# -- Write files --
|
|
165
|
+
files_written: list[str] = []
|
|
166
|
+
|
|
167
|
+
write_file(dockerfile_path, dockerfile_content)
|
|
168
|
+
files_written.append("Dockerfile")
|
|
169
|
+
|
|
170
|
+
dockerignore_path = os.path.join(project_path, ".dockerignore")
|
|
171
|
+
if not os.path.isfile(dockerignore_path):
|
|
172
|
+
write_file(dockerignore_path, dockerignore_content)
|
|
173
|
+
files_written.append(".dockerignore")
|
|
174
|
+
else:
|
|
175
|
+
typer.echo(dim(" .dockerignore already exists, skipping."))
|
|
176
|
+
|
|
177
|
+
if compose_content:
|
|
178
|
+
compose_path = os.path.join(project_path, "docker-compose.yml")
|
|
179
|
+
if os.path.isfile(compose_path) and not force:
|
|
180
|
+
typer.echo(yellow(" docker-compose.yml already exists."))
|
|
181
|
+
if confirm(" Overwrite?"):
|
|
182
|
+
write_file(compose_path, compose_content)
|
|
183
|
+
files_written.append("docker-compose.yml")
|
|
184
|
+
else:
|
|
185
|
+
typer.echo(dim(" Skipped docker-compose.yml."))
|
|
186
|
+
else:
|
|
187
|
+
write_file(compose_path, compose_content)
|
|
188
|
+
files_written.append("docker-compose.yml")
|
|
189
|
+
|
|
190
|
+
# -- Summary --
|
|
191
|
+
typer.echo()
|
|
192
|
+
typer.echo(green(" Generated files:"))
|
|
193
|
+
for f in files_written:
|
|
194
|
+
typer.echo(green(f" - {f}"))
|
|
195
|
+
typer.echo()
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
# ---------------------------------------------------------------------------
|
|
199
|
+
# build
|
|
200
|
+
# ---------------------------------------------------------------------------
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
@app.command()
|
|
204
|
+
def build(
|
|
205
|
+
path: str = typer.Argument(
|
|
206
|
+
".",
|
|
207
|
+
help="Path to the project directory containing the Dockerfile.",
|
|
208
|
+
),
|
|
209
|
+
tag: str = typer.Option(
|
|
210
|
+
"dockermind-app",
|
|
211
|
+
"--tag", "-t",
|
|
212
|
+
help="Docker image tag.",
|
|
213
|
+
),
|
|
214
|
+
) -> None:
|
|
215
|
+
"""Build a Docker image from the project's Dockerfile."""
|
|
216
|
+
_banner()
|
|
217
|
+
|
|
218
|
+
project_path = os.path.abspath(path)
|
|
219
|
+
dockerfile = os.path.join(project_path, "Dockerfile")
|
|
220
|
+
|
|
221
|
+
if not os.path.isfile(dockerfile):
|
|
222
|
+
typer.echo(red(" Dockerfile not found. Run 'dockermind init' first."))
|
|
223
|
+
raise typer.Exit(code=1)
|
|
224
|
+
|
|
225
|
+
success = docker_build(project_path, tag=tag)
|
|
226
|
+
raise typer.Exit(code=0 if success else 1)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
# ---------------------------------------------------------------------------
|
|
230
|
+
# run
|
|
231
|
+
# ---------------------------------------------------------------------------
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
@app.command()
|
|
235
|
+
def run(
|
|
236
|
+
tag: str = typer.Option(
|
|
237
|
+
"dockermind-app",
|
|
238
|
+
"--tag", "-t",
|
|
239
|
+
help="Docker image tag to run.",
|
|
240
|
+
),
|
|
241
|
+
port: int = typer.Option(
|
|
242
|
+
3000,
|
|
243
|
+
"--port", "-p",
|
|
244
|
+
help="Port to map (host:container).",
|
|
245
|
+
),
|
|
246
|
+
detach: bool = typer.Option(
|
|
247
|
+
False,
|
|
248
|
+
"--detach", "-d",
|
|
249
|
+
help="Run container in background.",
|
|
250
|
+
),
|
|
251
|
+
) -> None:
|
|
252
|
+
"""Run a Docker container from a built image."""
|
|
253
|
+
_banner()
|
|
254
|
+
success = docker_run(tag=tag, port=port, detach=detach)
|
|
255
|
+
raise typer.Exit(code=0 if success else 1)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
# ---------------------------------------------------------------------------
|
|
259
|
+
# doctor
|
|
260
|
+
# ---------------------------------------------------------------------------
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
@app.command()
|
|
264
|
+
def doctor() -> None:
|
|
265
|
+
"""Check that Docker is installed and running."""
|
|
266
|
+
_banner()
|
|
267
|
+
ok = run_doctor()
|
|
268
|
+
raise typer.Exit(code=0 if ok else 1)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
# ---------------------------------------------------------------------------
|
|
272
|
+
# version
|
|
273
|
+
# ---------------------------------------------------------------------------
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
@app.command()
|
|
277
|
+
def version() -> None:
|
|
278
|
+
"""Show dockermind version."""
|
|
279
|
+
typer.echo(f"dockermind v{__version__}")
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
# ---------------------------------------------------------------------------
|
|
283
|
+
# Entry point
|
|
284
|
+
# ---------------------------------------------------------------------------
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
if __name__ == "__main__":
|
|
288
|
+
app()
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Docker Compose generator -- produces docker-compose.yml content from AnalysisResult.
|
|
3
|
+
|
|
4
|
+
Only generated when the user explicitly requests it via ``dockermind init --compose``.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dockermind.analyzer import AnalysisResult
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def generate_compose(analysis: AnalysisResult, service_name: str = "app") -> str:
|
|
13
|
+
"""Return docker-compose.yml content as a string.
|
|
14
|
+
|
|
15
|
+
Includes:
|
|
16
|
+
- restart: unless-stopped
|
|
17
|
+
- correct port mapping
|
|
18
|
+
- env_file support
|
|
19
|
+
"""
|
|
20
|
+
port = analysis.port or (3000 if analysis.language == "node" else 8000)
|
|
21
|
+
|
|
22
|
+
lines = [
|
|
23
|
+
"services:",
|
|
24
|
+
f" {service_name}:",
|
|
25
|
+
" build:",
|
|
26
|
+
" context: .",
|
|
27
|
+
" dockerfile: Dockerfile",
|
|
28
|
+
f" ports:",
|
|
29
|
+
f' - "{port}:{port}"',
|
|
30
|
+
" restart: unless-stopped",
|
|
31
|
+
" env_file:",
|
|
32
|
+
" - .env",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
# Add environment section with common variables
|
|
36
|
+
lines += [
|
|
37
|
+
" environment:",
|
|
38
|
+
" - NODE_ENV=production" if analysis.language == "node" else " - PYTHONUNBUFFERED=1",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
lines.append("")
|
|
42
|
+
|
|
43
|
+
return "\n".join(lines)
|