quickscale-cli 0.62.0__tar.gz → 0.65.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.
- {quickscale_cli-0.62.0 → quickscale_cli-0.65.0}/PKG-INFO +3 -4
- {quickscale_cli-0.62.0 → quickscale_cli-0.65.0}/pyproject.toml +12 -57
- {quickscale_cli-0.62.0 → quickscale_cli-0.65.0}/src/quickscale_cli/_version.py +1 -1
- {quickscale_cli-0.62.0 → quickscale_cli-0.65.0}/src/quickscale_cli/commands/deployment_commands.py +38 -13
- {quickscale_cli-0.62.0 → quickscale_cli-0.65.0}/src/quickscale_cli/commands/development_commands.py +132 -22
- quickscale_cli-0.65.0/src/quickscale_cli/commands/module_commands.py +689 -0
- {quickscale_cli-0.62.0 → quickscale_cli-0.65.0}/src/quickscale_cli/main.py +104 -23
- quickscale_cli-0.65.0/src/quickscale_cli/utils/dependency_utils.py +211 -0
- {quickscale_cli-0.62.0 → quickscale_cli-0.65.0}/src/quickscale_cli/utils/docker_utils.py +80 -3
- {quickscale_cli-0.62.0 → quickscale_cli-0.65.0}/src/quickscale_cli/utils/project_manager.py +6 -1
- {quickscale_cli-0.62.0 → quickscale_cli-0.65.0}/src/quickscale_cli/utils/railway_utils.py +66 -14
- quickscale_cli-0.62.0/src/quickscale_cli/commands/module_commands.py +0 -307
- {quickscale_cli-0.62.0 → quickscale_cli-0.65.0}/src/quickscale_cli/__init__.py +0 -0
- {quickscale_cli-0.62.0 → quickscale_cli-0.65.0}/src/quickscale_cli/commands/__init__.py +0 -0
- {quickscale_cli-0.62.0 → quickscale_cli-0.65.0}/src/quickscale_cli/utils/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: quickscale-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.65.0
|
|
4
4
|
Summary: Command-line interface for QuickScale Django project generator
|
|
5
5
|
Home-page: https://github.com/Experto-AI/quickscale
|
|
6
6
|
License: Apache-2.0
|
|
@@ -10,7 +10,6 @@ Author-email: victor@experto.ai
|
|
|
10
10
|
Requires-Python: >=3.10,<4.0
|
|
11
11
|
Classifier: Development Status :: 3 - Alpha
|
|
12
12
|
Classifier: Framework :: Django
|
|
13
|
-
Classifier: Framework :: Django :: 4.2
|
|
14
13
|
Classifier: Framework :: Django :: 5.0
|
|
15
14
|
Classifier: Intended Audience :: Developers
|
|
16
15
|
Classifier: License :: OSI Approved :: Apache Software License
|
|
@@ -18,7 +17,7 @@ Classifier: Programming Language :: Python :: 3
|
|
|
18
17
|
Classifier: Programming Language :: Python :: 3.10
|
|
19
18
|
Classifier: Programming Language :: Python :: 3.11
|
|
20
19
|
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
-
Requires-Dist: click (>=8.
|
|
22
|
-
Requires-Dist: quickscale-core (>=0.
|
|
20
|
+
Requires-Dist: click (>=8.3.0,<9.0.0)
|
|
21
|
+
Requires-Dist: quickscale-core (>=0.65.0,<0.66.0)
|
|
23
22
|
Project-URL: Documentation, https://github.com/Experto-AI/quickscale/tree/main/docs
|
|
24
23
|
Project-URL: Repository, https://github.com/Experto-AI/quickscale
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "quickscale-cli"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.65.0"
|
|
4
4
|
description = "Command-line interface for QuickScale Django project generator"
|
|
5
5
|
authors = ["Experto AI <victor@experto.ai>"]
|
|
6
6
|
license = "Apache-2.0"
|
|
@@ -17,21 +17,18 @@ classifiers = [
|
|
|
17
17
|
"Programming Language :: Python :: 3.11",
|
|
18
18
|
"Programming Language :: Python :: 3.12",
|
|
19
19
|
"Framework :: Django",
|
|
20
|
-
"Framework :: Django :: 4.2",
|
|
21
20
|
"Framework :: Django :: 5.0",
|
|
22
21
|
]
|
|
23
22
|
packages = [{include = "quickscale_cli", from = "src"}]
|
|
24
23
|
|
|
25
24
|
[tool.poetry.dependencies]
|
|
26
25
|
python = "^3.10"
|
|
27
|
-
click = "^8.
|
|
28
|
-
quickscale-core = "^0.
|
|
26
|
+
click = "^8.3.0"
|
|
27
|
+
quickscale-core = "^0.65.0"
|
|
29
28
|
|
|
30
29
|
[tool.poetry.group.dev.dependencies]
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
ruff = "^0.6.0"
|
|
34
|
-
mypy = "^1.8.0"
|
|
30
|
+
# Dev dependencies centralized in root pyproject.toml
|
|
31
|
+
# Use: poetry install from repository root
|
|
35
32
|
|
|
36
33
|
[tool.poetry.scripts]
|
|
37
34
|
quickscale = "quickscale_cli.main:cli"
|
|
@@ -53,60 +50,18 @@ addopts = [
|
|
|
53
50
|
"--cov-report=html",
|
|
54
51
|
"-m", "not e2e", # Skip E2E tests by default (run with: pytest -m e2e)
|
|
55
52
|
]
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
[tool.ruff.lint]
|
|
62
|
-
select = [
|
|
63
|
-
"E", # pycodestyle errors
|
|
64
|
-
"W", # pycodestyle warnings
|
|
65
|
-
"F", # pyflakes
|
|
66
|
-
"I", # isort
|
|
67
|
-
"N", # pep8-naming
|
|
68
|
-
"UP", # pyupgrade
|
|
69
|
-
"D", # docstring rules
|
|
70
|
-
]
|
|
71
|
-
ignore = [
|
|
72
|
-
"D100", # Missing docstring in public module
|
|
73
|
-
"D101", # Missing docstring in public class
|
|
74
|
-
"D102", # Missing docstring in public method
|
|
75
|
-
"D103", # Missing docstring in public function
|
|
76
|
-
"D105", # Missing docstring in magic method
|
|
77
|
-
"D107", # Missing docstring in __init__
|
|
78
|
-
"D203", # 1 blank line required before class docstring (incompatible with D211)
|
|
79
|
-
"D212", # Multi-line docstring summary should start at the first line (incompatible with D213)
|
|
80
|
-
"D400", # First line should end with a period
|
|
81
|
-
"D415", # First line should end with a period, question mark, or exclamation point
|
|
53
|
+
markers = [
|
|
54
|
+
"integration: integration tests (slower, require external dependencies)",
|
|
55
|
+
"slow: slow tests that take significant time to run",
|
|
56
|
+
"e2e: end-to-end tests",
|
|
82
57
|
]
|
|
83
58
|
|
|
59
|
+
# Ruff and mypy configurations centralized in root ruff.toml and mypy.ini
|
|
60
|
+
# Package-specific isort override
|
|
84
61
|
[tool.ruff.lint.isort]
|
|
85
62
|
known-first-party = ["quickscale_cli", "quickscale_core"]
|
|
86
63
|
|
|
87
|
-
|
|
88
|
-
python_version = "3.10"
|
|
89
|
-
warn_return_any = true
|
|
90
|
-
warn_unused_configs = true
|
|
91
|
-
disallow_untyped_defs = true
|
|
92
|
-
disallow_incomplete_defs = true
|
|
93
|
-
check_untyped_defs = true
|
|
94
|
-
disallow_untyped_decorators = true
|
|
95
|
-
no_implicit_optional = true
|
|
96
|
-
warn_redundant_casts = true
|
|
97
|
-
warn_unused_ignores = true
|
|
98
|
-
warn_no_return = true
|
|
99
|
-
warn_unreachable = true
|
|
100
|
-
strict_equality = true
|
|
101
|
-
show_error_codes = true
|
|
102
|
-
|
|
103
|
-
[[tool.mypy.overrides]]
|
|
104
|
-
module = "tests.*"
|
|
105
|
-
disallow_untyped_defs = false
|
|
106
|
-
check_untyped_defs = false
|
|
107
|
-
|
|
64
|
+
# Package-specific mypy overrides
|
|
108
65
|
[[tool.mypy.overrides]]
|
|
109
66
|
module = "quickscale_core.*"
|
|
110
67
|
ignore_missing_imports = true
|
|
111
|
-
|
|
112
|
-
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
# Auto-generated by scripts/version_tool.sh
|
|
2
|
-
__version__ = "0.
|
|
2
|
+
__version__ = "0.65.0"
|
{quickscale_cli-0.62.0 → quickscale_cli-0.65.0}/src/quickscale_cli/commands/deployment_commands.py
RENAMED
|
@@ -37,7 +37,9 @@ def deploy() -> None:
|
|
|
37
37
|
|
|
38
38
|
|
|
39
39
|
@deploy.command()
|
|
40
|
-
@click.option(
|
|
40
|
+
@click.option(
|
|
41
|
+
"--project-name", help="Railway project name (auto-detected if not provided)"
|
|
42
|
+
)
|
|
41
43
|
def railway(project_name: str | None) -> None:
|
|
42
44
|
"""
|
|
43
45
|
Deploy project to Railway with automated setup.
|
|
@@ -106,7 +108,9 @@ def railway(project_name: str | None) -> None:
|
|
|
106
108
|
click.secho(f"⚠️ Warning: {lock_message}", fg="yellow")
|
|
107
109
|
if "not found" not in lock_message.lower():
|
|
108
110
|
# Lock file exists but is inconsistent - offer to fix it
|
|
109
|
-
click.echo(
|
|
111
|
+
click.echo(
|
|
112
|
+
"💡 The lock file needs to be regenerated to match pyproject.toml"
|
|
113
|
+
)
|
|
110
114
|
if click.confirm("Run 'poetry lock --no-update' to fix?", default=True):
|
|
111
115
|
click.echo("🔄 Updating poetry.lock...")
|
|
112
116
|
success, fix_message = fix_poetry_lock()
|
|
@@ -114,7 +118,9 @@ def railway(project_name: str | None) -> None:
|
|
|
114
118
|
click.secho(f"✅ {fix_message}", fg="green")
|
|
115
119
|
else:
|
|
116
120
|
click.secho(f"❌ Error: {fix_message}", fg="red", err=True)
|
|
117
|
-
click.echo(
|
|
121
|
+
click.echo(
|
|
122
|
+
"💡 Try running manually: poetry lock --no-update", err=True
|
|
123
|
+
)
|
|
118
124
|
if not click.confirm("Continue anyway?", default=False):
|
|
119
125
|
click.echo("Deployment cancelled")
|
|
120
126
|
sys.exit(0)
|
|
@@ -143,7 +149,9 @@ def railway(project_name: str | None) -> None:
|
|
|
143
149
|
click.secho("❌ Error: npm is not installed", fg="red", err=True)
|
|
144
150
|
click.echo("\n💡 Install Node.js and npm first:", err=True)
|
|
145
151
|
click.echo(" https://nodejs.org/", err=True)
|
|
146
|
-
click.echo(
|
|
152
|
+
click.echo(
|
|
153
|
+
"\nThen run this command again to auto-install Railway CLI", err=True
|
|
154
|
+
)
|
|
147
155
|
sys.exit(1)
|
|
148
156
|
|
|
149
157
|
# Auto-install Railway CLI
|
|
@@ -178,10 +186,14 @@ def railway(project_name: str | None) -> None:
|
|
|
178
186
|
new_version = get_railway_cli_version()
|
|
179
187
|
click.secho(f"✅ Railway CLI upgraded to {new_version}", fg="green")
|
|
180
188
|
else:
|
|
181
|
-
click.secho(
|
|
189
|
+
click.secho(
|
|
190
|
+
"⚠️ Warning: Failed to upgrade Railway CLI", fg="yellow"
|
|
191
|
+
)
|
|
182
192
|
click.echo("💡 Try upgrading manually:", err=True)
|
|
183
193
|
click.echo(" npm update -g @railway/cli", err=True)
|
|
184
|
-
if not click.confirm(
|
|
194
|
+
if not click.confirm(
|
|
195
|
+
"Continue with current version?", default=False
|
|
196
|
+
):
|
|
185
197
|
sys.exit(1)
|
|
186
198
|
else:
|
|
187
199
|
click.secho("✅ Railway CLI version is up to date", fg="green")
|
|
@@ -203,7 +215,8 @@ def railway(project_name: str | None) -> None:
|
|
|
203
215
|
click.echo("\n💡 Try authenticating manually:", err=True)
|
|
204
216
|
click.echo(" railway login (opens browser)", err=True)
|
|
205
217
|
click.echo(
|
|
206
|
-
" railway login --browserless (for headless/remote systems)",
|
|
218
|
+
" railway login --browserless (for headless/remote systems)",
|
|
219
|
+
err=True,
|
|
207
220
|
)
|
|
208
221
|
sys.exit(1)
|
|
209
222
|
else:
|
|
@@ -219,7 +232,9 @@ def railway(project_name: str | None) -> None:
|
|
|
219
232
|
result = run_railway_command(["init"], timeout=60, interactive=True)
|
|
220
233
|
|
|
221
234
|
if result.returncode != 0:
|
|
222
|
-
click.secho(
|
|
235
|
+
click.secho(
|
|
236
|
+
"❌ Error: Failed to initialize Railway project", fg="red", err=True
|
|
237
|
+
)
|
|
223
238
|
sys.exit(1)
|
|
224
239
|
|
|
225
240
|
click.secho("✅ Railway project initialized", fg="green")
|
|
@@ -268,7 +283,9 @@ def railway(project_name: str | None) -> None:
|
|
|
268
283
|
)
|
|
269
284
|
|
|
270
285
|
if result.returncode != 0:
|
|
271
|
-
click.secho(
|
|
286
|
+
click.secho(
|
|
287
|
+
f"⚠️ Warning: Could not create service '{app_service}'", fg="yellow"
|
|
288
|
+
)
|
|
272
289
|
click.echo("💡 Create manually: railway add --service", err=True)
|
|
273
290
|
else:
|
|
274
291
|
click.secho(f"✅ Service '{app_service}' created", fg="green")
|
|
@@ -288,7 +305,9 @@ def railway(project_name: str | None) -> None:
|
|
|
288
305
|
f' railway variables --set "DATABASE_URL=${{{{Postgres.DATABASE_URL}}}}" '
|
|
289
306
|
f"--service {app_service}"
|
|
290
307
|
)
|
|
291
|
-
click.echo(
|
|
308
|
+
click.echo(
|
|
309
|
+
" Or link via Railway dashboard > Variables > New Variable > Reference"
|
|
310
|
+
)
|
|
292
311
|
|
|
293
312
|
# Step 7: Generate public domain first (before setting env vars)
|
|
294
313
|
click.echo("\n🌐 Generating public domain...")
|
|
@@ -332,7 +351,9 @@ def railway(project_name: str | None) -> None:
|
|
|
332
351
|
click.echo(f" • {key}={env_vars[key]}")
|
|
333
352
|
click.echo("💡 This triggers ONE deployment with all variables set")
|
|
334
353
|
else:
|
|
335
|
-
click.secho(
|
|
354
|
+
click.secho(
|
|
355
|
+
"⚠️ Warning: Some environment variables could not be set", fg="yellow"
|
|
356
|
+
)
|
|
336
357
|
if failed_keys:
|
|
337
358
|
click.echo("Failed variables:")
|
|
338
359
|
for key in failed_keys:
|
|
@@ -348,7 +369,9 @@ def railway(project_name: str | None) -> None:
|
|
|
348
369
|
try:
|
|
349
370
|
# Deploy to the specific app service
|
|
350
371
|
# railway.json in project root configures the build and startCommand
|
|
351
|
-
result = run_railway_command(
|
|
372
|
+
result = run_railway_command(
|
|
373
|
+
["up", "--service", app_service, "--detach"], timeout=60
|
|
374
|
+
)
|
|
352
375
|
|
|
353
376
|
if result.returncode != 0:
|
|
354
377
|
click.secho("❌ Error: Deployment failed", fg="red", err=True)
|
|
@@ -358,7 +381,9 @@ def railway(project_name: str | None) -> None:
|
|
|
358
381
|
click.echo(" - Verify railway.json exists in project root", err=True)
|
|
359
382
|
click.echo(" - Verify Dockerfile is present", err=True)
|
|
360
383
|
click.echo(" - Check pyproject.toml dependencies", err=True)
|
|
361
|
-
click.echo(
|
|
384
|
+
click.echo(
|
|
385
|
+
" - Ensure DATABASE_URL is linked to PostgreSQL service", err=True
|
|
386
|
+
)
|
|
362
387
|
sys.exit(1)
|
|
363
388
|
|
|
364
389
|
click.secho("✅ Deployment started", fg="green")
|
{quickscale_cli-0.62.0 → quickscale_cli-0.65.0}/src/quickscale_cli/commands/development_commands.py
RENAMED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Development lifecycle commands for QuickScale projects."""
|
|
2
2
|
|
|
3
|
+
import re
|
|
3
4
|
import subprocess
|
|
4
5
|
import sys
|
|
5
6
|
|
|
@@ -7,10 +8,16 @@ import click
|
|
|
7
8
|
|
|
8
9
|
from quickscale_cli.utils.docker_utils import (
|
|
9
10
|
get_docker_compose_command,
|
|
11
|
+
get_port_from_env,
|
|
10
12
|
is_docker_running,
|
|
11
13
|
is_interactive,
|
|
14
|
+
is_port_available,
|
|
15
|
+
wait_for_port_release,
|
|
16
|
+
)
|
|
17
|
+
from quickscale_cli.utils.project_manager import (
|
|
18
|
+
get_web_container_name,
|
|
19
|
+
is_in_quickscale_project,
|
|
12
20
|
)
|
|
13
|
-
from quickscale_cli.utils.project_manager import get_web_container_name, is_in_quickscale_project
|
|
14
21
|
|
|
15
22
|
|
|
16
23
|
@click.command()
|
|
@@ -20,7 +27,9 @@ def up(build: bool, no_cache: bool) -> None:
|
|
|
20
27
|
"""Start Docker services for development."""
|
|
21
28
|
# Check if in QuickScale project
|
|
22
29
|
if not is_in_quickscale_project():
|
|
23
|
-
click.secho(
|
|
30
|
+
click.secho(
|
|
31
|
+
"❌ Error: Not in a QuickScale project directory", fg="red", err=True
|
|
32
|
+
)
|
|
24
33
|
click.echo(
|
|
25
34
|
"💡 Tip: Navigate to a project directory or run 'quickscale init <name>' to create one",
|
|
26
35
|
err=True,
|
|
@@ -33,6 +42,24 @@ def up(build: bool, no_cache: bool) -> None:
|
|
|
33
42
|
click.echo("💡 Tip: Start Docker Desktop or the Docker daemon", err=True)
|
|
34
43
|
sys.exit(1)
|
|
35
44
|
|
|
45
|
+
# Check if required port is available BEFORE calling docker-compose
|
|
46
|
+
port = get_port_from_env()
|
|
47
|
+
if not is_port_available(port):
|
|
48
|
+
click.secho(f"❌ Error: Port {port} is already in use", fg="red", err=True)
|
|
49
|
+
click.echo(
|
|
50
|
+
f"\n💡 To resolve this issue, try one of the following:\n"
|
|
51
|
+
f" 1. If you just ran 'quickscale down': Wait 5-10 seconds for docker-proxy to release the port\n"
|
|
52
|
+
f" 2. Stop and cleanup: quickscale down && sleep 5 && quickscale up\n"
|
|
53
|
+
f" 3. Check for other containers: docker ps -a | grep {port}\n"
|
|
54
|
+
f" 4. Find and kill the process using the port:\n"
|
|
55
|
+
f" • Check what's using it: sudo netstat -tulpn | grep :{port}\n"
|
|
56
|
+
f" • If it's docker-proxy: wait a few seconds and try again\n"
|
|
57
|
+
f" • If it's another process: sudo lsof -ti:{port} | xargs kill -9\n"
|
|
58
|
+
f" 5. Change the port in .env file: PORT=8001",
|
|
59
|
+
err=True,
|
|
60
|
+
)
|
|
61
|
+
sys.exit(1)
|
|
62
|
+
|
|
36
63
|
try:
|
|
37
64
|
compose_cmd = get_docker_compose_command()
|
|
38
65
|
cmd = compose_cmd + ["up", "-d"]
|
|
@@ -44,15 +71,52 @@ def up(build: bool, no_cache: bool) -> None:
|
|
|
44
71
|
cmd.append("--no-cache")
|
|
45
72
|
|
|
46
73
|
click.echo("🚀 Starting Docker services...")
|
|
47
|
-
subprocess.run(cmd, check=True)
|
|
74
|
+
subprocess.run(cmd, check=True, capture_output=True, text=True)
|
|
48
75
|
click.secho("✅ Services started successfully!", fg="green", bold=True)
|
|
49
76
|
click.echo("💡 Tip: Use 'quickscale logs' to view service logs")
|
|
50
77
|
|
|
51
78
|
except subprocess.CalledProcessError as e:
|
|
52
|
-
|
|
53
|
-
|
|
79
|
+
# Check for port conflict in error output
|
|
80
|
+
# Docker error format: "Bind for 0.0.0.0:8000 failed: port is already allocated"
|
|
81
|
+
# Check both stdout and stderr since docker-compose may output to either
|
|
82
|
+
error_output = e.stderr if e.stderr else ""
|
|
83
|
+
stdout_output = e.stdout if e.stdout else ""
|
|
84
|
+
full_output = error_output + stdout_output
|
|
85
|
+
|
|
86
|
+
port_conflict_match = re.search(
|
|
87
|
+
r"Bind for [\d.]+:(\d+) failed: port is already allocated",
|
|
88
|
+
full_output,
|
|
89
|
+
re.IGNORECASE,
|
|
54
90
|
)
|
|
55
|
-
|
|
91
|
+
|
|
92
|
+
if port_conflict_match:
|
|
93
|
+
conflict_port = port_conflict_match.group(1)
|
|
94
|
+
click.secho(
|
|
95
|
+
f"❌ Error: Port {conflict_port} is already in use",
|
|
96
|
+
fg="red",
|
|
97
|
+
err=True,
|
|
98
|
+
)
|
|
99
|
+
click.echo(
|
|
100
|
+
f"\n💡 To resolve this issue, try one of the following:\n"
|
|
101
|
+
f" 1. Stop existing containers: quickscale down\n"
|
|
102
|
+
f" 2. Remove orphaned containers: docker-compose down --remove-orphans\n"
|
|
103
|
+
f" 3. Find and kill the process: lsof -ti:{conflict_port} | xargs kill -9\n"
|
|
104
|
+
f" 4. Find process details: sudo lsof -i:{conflict_port}\n"
|
|
105
|
+
f" 5. Or use: sudo fuser -k {conflict_port}/tcp",
|
|
106
|
+
err=True,
|
|
107
|
+
)
|
|
108
|
+
else:
|
|
109
|
+
click.secho(
|
|
110
|
+
f"❌ Error: Failed to start services (exit code: {e.returncode})",
|
|
111
|
+
fg="red",
|
|
112
|
+
err=True,
|
|
113
|
+
)
|
|
114
|
+
if full_output:
|
|
115
|
+
click.echo(f"\nError output:\n{full_output}", err=True)
|
|
116
|
+
click.echo(
|
|
117
|
+
"💡 Tip: Check Docker logs with 'quickscale logs' for details",
|
|
118
|
+
err=True,
|
|
119
|
+
)
|
|
56
120
|
sys.exit(1)
|
|
57
121
|
except KeyboardInterrupt:
|
|
58
122
|
click.echo("\n⚠️ Interrupted by user")
|
|
@@ -64,7 +128,9 @@ def up(build: bool, no_cache: bool) -> None:
|
|
|
64
128
|
def down(volumes: bool) -> None:
|
|
65
129
|
"""Stop Docker services."""
|
|
66
130
|
if not is_in_quickscale_project():
|
|
67
|
-
click.secho(
|
|
131
|
+
click.secho(
|
|
132
|
+
"❌ Error: Not in a QuickScale project directory", fg="red", err=True
|
|
133
|
+
)
|
|
68
134
|
click.echo("💡 Tip: Navigate to a project directory", err=True)
|
|
69
135
|
sys.exit(1)
|
|
70
136
|
|
|
@@ -75,18 +141,34 @@ def down(volumes: bool) -> None:
|
|
|
75
141
|
|
|
76
142
|
try:
|
|
77
143
|
compose_cmd = get_docker_compose_command()
|
|
78
|
-
cmd = compose_cmd + ["down"]
|
|
144
|
+
cmd = compose_cmd + ["down", "--remove-orphans"]
|
|
79
145
|
|
|
80
146
|
if volumes:
|
|
81
147
|
cmd.append("--volumes")
|
|
82
148
|
|
|
83
149
|
click.echo("🛑 Stopping Docker services...")
|
|
84
150
|
subprocess.run(cmd, check=True)
|
|
151
|
+
|
|
152
|
+
# Wait for Docker's proxy process to fully release ports
|
|
153
|
+
# docker-proxy can take a few seconds to release ports after containers stop
|
|
154
|
+
port = get_port_from_env()
|
|
155
|
+
click.echo(f"⏳ Waiting for port {port} to be released...")
|
|
156
|
+
if not wait_for_port_release(port, timeout=5.0):
|
|
157
|
+
click.echo(
|
|
158
|
+
f"⚠️ Warning: Port {port} still in use after 5 seconds. "
|
|
159
|
+
f"Wait a moment before running 'quickscale up'.",
|
|
160
|
+
err=True,
|
|
161
|
+
)
|
|
162
|
+
else:
|
|
163
|
+
click.echo(f"✅ Port {port} released")
|
|
164
|
+
|
|
85
165
|
click.secho("✅ Services stopped successfully!", fg="green")
|
|
86
166
|
|
|
87
167
|
except subprocess.CalledProcessError as e:
|
|
88
168
|
click.secho(
|
|
89
|
-
f"❌ Error: Failed to stop services (exit code: {e.returncode})",
|
|
169
|
+
f"❌ Error: Failed to stop services (exit code: {e.returncode})",
|
|
170
|
+
fg="red",
|
|
171
|
+
err=True,
|
|
90
172
|
)
|
|
91
173
|
sys.exit(1)
|
|
92
174
|
except KeyboardInterrupt:
|
|
@@ -95,11 +177,15 @@ def down(volumes: bool) -> None:
|
|
|
95
177
|
|
|
96
178
|
|
|
97
179
|
@click.command()
|
|
98
|
-
@click.option(
|
|
180
|
+
@click.option(
|
|
181
|
+
"-c", "--command", "cmd", help="Run a single command instead of interactive shell"
|
|
182
|
+
)
|
|
99
183
|
def shell(cmd: str | None) -> None:
|
|
100
184
|
"""Open an interactive bash shell in the web container."""
|
|
101
185
|
if not is_in_quickscale_project():
|
|
102
|
-
click.secho(
|
|
186
|
+
click.secho(
|
|
187
|
+
"❌ Error: Not in a QuickScale project directory", fg="red", err=True
|
|
188
|
+
)
|
|
103
189
|
click.echo("💡 Tip: Navigate to a project directory", err=True)
|
|
104
190
|
sys.exit(1)
|
|
105
191
|
|
|
@@ -117,7 +203,9 @@ def shell(cmd: str | None) -> None:
|
|
|
117
203
|
if is_interactive():
|
|
118
204
|
subprocess.run(docker_cmd, check=True)
|
|
119
205
|
else:
|
|
120
|
-
result = subprocess.run(
|
|
206
|
+
result = subprocess.run(
|
|
207
|
+
docker_cmd, capture_output=True, text=True, check=True
|
|
208
|
+
)
|
|
121
209
|
if result.stdout:
|
|
122
210
|
click.echo(result.stdout, nl=False)
|
|
123
211
|
if result.stderr:
|
|
@@ -135,7 +223,11 @@ def shell(cmd: str | None) -> None:
|
|
|
135
223
|
click.secho("❌ Error: Container not running", fg="red", err=True)
|
|
136
224
|
click.echo("💡 Tip: Start services with 'quickscale up' first", err=True)
|
|
137
225
|
else:
|
|
138
|
-
click.secho(
|
|
226
|
+
click.secho(
|
|
227
|
+
f"❌ Error: Command failed (exit code: {e.returncode})",
|
|
228
|
+
fg="red",
|
|
229
|
+
err=True,
|
|
230
|
+
)
|
|
139
231
|
sys.exit(e.returncode)
|
|
140
232
|
except KeyboardInterrupt:
|
|
141
233
|
click.echo("\n⚠️ Exited shell")
|
|
@@ -147,7 +239,9 @@ def shell(cmd: str | None) -> None:
|
|
|
147
239
|
def manage(args: tuple) -> None:
|
|
148
240
|
"""Run Django management commands in the web container."""
|
|
149
241
|
if not is_in_quickscale_project():
|
|
150
|
-
click.secho(
|
|
242
|
+
click.secho(
|
|
243
|
+
"❌ Error: Not in a QuickScale project directory", fg="red", err=True
|
|
244
|
+
)
|
|
151
245
|
click.echo("💡 Tip: Navigate to a project directory", err=True)
|
|
152
246
|
sys.exit(1)
|
|
153
247
|
|
|
@@ -157,8 +251,12 @@ def manage(args: tuple) -> None:
|
|
|
157
251
|
sys.exit(1)
|
|
158
252
|
|
|
159
253
|
if not args:
|
|
160
|
-
click.secho(
|
|
161
|
-
|
|
254
|
+
click.secho(
|
|
255
|
+
"❌ Error: No Django management command specified", fg="red", err=True
|
|
256
|
+
)
|
|
257
|
+
click.echo(
|
|
258
|
+
"💡 Tip: Run 'quickscale manage help' to see available commands", err=True
|
|
259
|
+
)
|
|
162
260
|
sys.exit(1)
|
|
163
261
|
|
|
164
262
|
try:
|
|
@@ -176,7 +274,9 @@ def manage(args: tuple) -> None:
|
|
|
176
274
|
if is_interactive():
|
|
177
275
|
subprocess.run(docker_cmd, check=True)
|
|
178
276
|
else:
|
|
179
|
-
result = subprocess.run(
|
|
277
|
+
result = subprocess.run(
|
|
278
|
+
docker_cmd, capture_output=True, text=True, check=True
|
|
279
|
+
)
|
|
180
280
|
if result.stdout:
|
|
181
281
|
click.echo(result.stdout, nl=False)
|
|
182
282
|
if result.stderr:
|
|
@@ -184,7 +284,9 @@ def manage(args: tuple) -> None:
|
|
|
184
284
|
|
|
185
285
|
except subprocess.CalledProcessError as e:
|
|
186
286
|
if e.returncode == 1:
|
|
187
|
-
click.secho(
|
|
287
|
+
click.secho(
|
|
288
|
+
"❌ Error: Container not running or command failed", fg="red", err=True
|
|
289
|
+
)
|
|
188
290
|
click.echo("💡 Tip: Start services with 'quickscale up' first", err=True)
|
|
189
291
|
sys.exit(e.returncode)
|
|
190
292
|
except KeyboardInterrupt:
|
|
@@ -195,12 +297,16 @@ def manage(args: tuple) -> None:
|
|
|
195
297
|
@click.command()
|
|
196
298
|
@click.argument("service", required=False)
|
|
197
299
|
@click.option("-f", "--follow", is_flag=True, help="Follow log output")
|
|
198
|
-
@click.option(
|
|
300
|
+
@click.option(
|
|
301
|
+
"--tail", default=None, help="Number of lines to show from the end of the logs"
|
|
302
|
+
)
|
|
199
303
|
@click.option("--timestamps", is_flag=True, help="Show timestamps")
|
|
200
304
|
def logs(service: str | None, follow: bool, tail: str | None, timestamps: bool) -> None:
|
|
201
305
|
"""View Docker service logs."""
|
|
202
306
|
if not is_in_quickscale_project():
|
|
203
|
-
click.secho(
|
|
307
|
+
click.secho(
|
|
308
|
+
"❌ Error: Not in a QuickScale project directory", fg="red", err=True
|
|
309
|
+
)
|
|
204
310
|
click.echo("💡 Tip: Navigate to a project directory", err=True)
|
|
205
311
|
sys.exit(1)
|
|
206
312
|
|
|
@@ -229,7 +335,9 @@ def logs(service: str | None, follow: bool, tail: str | None, timestamps: bool)
|
|
|
229
335
|
|
|
230
336
|
except subprocess.CalledProcessError as e:
|
|
231
337
|
click.secho(
|
|
232
|
-
f"❌ Error: Failed to retrieve logs (exit code: {e.returncode})",
|
|
338
|
+
f"❌ Error: Failed to retrieve logs (exit code: {e.returncode})",
|
|
339
|
+
fg="red",
|
|
340
|
+
err=True,
|
|
233
341
|
)
|
|
234
342
|
sys.exit(1)
|
|
235
343
|
except KeyboardInterrupt:
|
|
@@ -241,7 +349,9 @@ def logs(service: str | None, follow: bool, tail: str | None, timestamps: bool)
|
|
|
241
349
|
def ps() -> None:
|
|
242
350
|
"""Show service status."""
|
|
243
351
|
if not is_in_quickscale_project():
|
|
244
|
-
click.secho(
|
|
352
|
+
click.secho(
|
|
353
|
+
"❌ Error: Not in a QuickScale project directory", fg="red", err=True
|
|
354
|
+
)
|
|
245
355
|
click.echo("💡 Tip: Navigate to a project directory", err=True)
|
|
246
356
|
sys.exit(1)
|
|
247
357
|
|