quickscale-cli 0.57.2__tar.gz → 0.59.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.57.2 → quickscale_cli-0.59.0}/PKG-INFO +25 -2
- {quickscale_cli-0.57.2 → quickscale_cli-0.59.0}/README.md +23 -0
- {quickscale_cli-0.57.2 → quickscale_cli-0.59.0}/pyproject.toml +3 -2
- {quickscale_cli-0.57.2 → quickscale_cli-0.59.0}/src/quickscale_cli/__init__.py +4 -4
- {quickscale_cli-0.57.2 → quickscale_cli-0.59.0}/src/quickscale_cli/_version.py +1 -1
- quickscale_cli-0.59.0/src/quickscale_cli/commands/__init__.py +1 -0
- quickscale_cli-0.59.0/src/quickscale_cli/commands/development_commands.py +237 -0
- {quickscale_cli-0.57.2 → quickscale_cli-0.59.0}/src/quickscale_cli/main.py +10 -0
- quickscale_cli-0.59.0/src/quickscale_cli/utils/__init__.py +1 -0
- quickscale_cli-0.59.0/src/quickscale_cli/utils/docker_utils.py +76 -0
- quickscale_cli-0.59.0/src/quickscale_cli/utils/project_manager.py +61 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: quickscale-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.59.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
|
|
@@ -19,7 +19,7 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
19
19
|
Classifier: Programming Language :: Python :: 3.11
|
|
20
20
|
Classifier: Programming Language :: Python :: 3.12
|
|
21
21
|
Requires-Dist: click (>=8.1.0,<9.0.0)
|
|
22
|
-
Requires-Dist: quickscale-core (>=0.
|
|
22
|
+
Requires-Dist: quickscale-core (>=0.59.0,<0.60.0)
|
|
23
23
|
Project-URL: Documentation, https://github.com/Experto-AI/quickscale/tree/main/docs
|
|
24
24
|
Project-URL: Repository, https://github.com/Experto-AI/quickscale
|
|
25
25
|
Description-Content-Type: text/markdown
|
|
@@ -67,6 +67,7 @@ QuickScale is a **composable Django framework** designed for **solo developers a
|
|
|
67
67
|
**Start here for your needs:**
|
|
68
68
|
- 📖 **New user?** You're in the right place. This README shows you what QuickScale is and how to get started.
|
|
69
69
|
- 🔧 **Need commands?** See [user_manual.md](./docs/technical/user_manual.md) for all commands and workflows
|
|
70
|
+
- 🚀 **Deploying to Railway?** See [railway.md](./docs/deployment/railway.md) for Railway deployment guide
|
|
70
71
|
- 📋 **Planning a feature?** Check [decisions.md](./docs/technical/decisions.md) for the authoritative MVP scope and technical rules
|
|
71
72
|
- 🗓️ **Timeline & tasks?** See [roadmap.md](./docs/technical/roadmap.md)
|
|
72
73
|
- 🏗️ **Project structure?** See [scaffolding.md](./docs/technical/scaffolding.md) for complete directory layouts
|
|
@@ -149,6 +150,28 @@ poetry run python manage.py runserver
|
|
|
149
150
|
|
|
150
151
|
**That's it!** Visit http://localhost:8000 to see your new Django project.
|
|
151
152
|
|
|
153
|
+
### Development Workflow with Docker
|
|
154
|
+
|
|
155
|
+
QuickScale includes convenient CLI commands for Docker-based development:
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
# Start all services (web + database)
|
|
159
|
+
quickscale up
|
|
160
|
+
|
|
161
|
+
# View logs
|
|
162
|
+
quickscale logs -f web
|
|
163
|
+
|
|
164
|
+
# Run Django commands
|
|
165
|
+
quickscale manage migrate
|
|
166
|
+
quickscale manage createsuperuser
|
|
167
|
+
|
|
168
|
+
# Open a shell in the container
|
|
169
|
+
quickscale shell
|
|
170
|
+
|
|
171
|
+
# Stop services
|
|
172
|
+
quickscale down
|
|
173
|
+
```
|
|
174
|
+
|
|
152
175
|
**For complete command reference and workflows**, see the [user_manual.md](./docs/technical/user_manual.md).
|
|
153
176
|
|
|
154
177
|
## Learn More
|
|
@@ -41,6 +41,7 @@ QuickScale is a **composable Django framework** designed for **solo developers a
|
|
|
41
41
|
**Start here for your needs:**
|
|
42
42
|
- 📖 **New user?** You're in the right place. This README shows you what QuickScale is and how to get started.
|
|
43
43
|
- 🔧 **Need commands?** See [user_manual.md](./docs/technical/user_manual.md) for all commands and workflows
|
|
44
|
+
- 🚀 **Deploying to Railway?** See [railway.md](./docs/deployment/railway.md) for Railway deployment guide
|
|
44
45
|
- 📋 **Planning a feature?** Check [decisions.md](./docs/technical/decisions.md) for the authoritative MVP scope and technical rules
|
|
45
46
|
- 🗓️ **Timeline & tasks?** See [roadmap.md](./docs/technical/roadmap.md)
|
|
46
47
|
- 🏗️ **Project structure?** See [scaffolding.md](./docs/technical/scaffolding.md) for complete directory layouts
|
|
@@ -123,6 +124,28 @@ poetry run python manage.py runserver
|
|
|
123
124
|
|
|
124
125
|
**That's it!** Visit http://localhost:8000 to see your new Django project.
|
|
125
126
|
|
|
127
|
+
### Development Workflow with Docker
|
|
128
|
+
|
|
129
|
+
QuickScale includes convenient CLI commands for Docker-based development:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
# Start all services (web + database)
|
|
133
|
+
quickscale up
|
|
134
|
+
|
|
135
|
+
# View logs
|
|
136
|
+
quickscale logs -f web
|
|
137
|
+
|
|
138
|
+
# Run Django commands
|
|
139
|
+
quickscale manage migrate
|
|
140
|
+
quickscale manage createsuperuser
|
|
141
|
+
|
|
142
|
+
# Open a shell in the container
|
|
143
|
+
quickscale shell
|
|
144
|
+
|
|
145
|
+
# Stop services
|
|
146
|
+
quickscale down
|
|
147
|
+
```
|
|
148
|
+
|
|
126
149
|
**For complete command reference and workflows**, see the [user_manual.md](./docs/technical/user_manual.md).
|
|
127
150
|
|
|
128
151
|
## Learn More
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "quickscale-cli"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.59.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"
|
|
@@ -26,7 +26,7 @@ packages = [{include = "quickscale_cli", from = "src"}]
|
|
|
26
26
|
[tool.poetry.dependencies]
|
|
27
27
|
python = "^3.10"
|
|
28
28
|
click = "^8.1.0"
|
|
29
|
-
quickscale-core = "^0.
|
|
29
|
+
quickscale-core = "^0.59.0"
|
|
30
30
|
|
|
31
31
|
[tool.poetry.group.dev.dependencies]
|
|
32
32
|
pytest = "^7.4.0"
|
|
@@ -52,6 +52,7 @@ addopts = [
|
|
|
52
52
|
"--cov=quickscale_cli",
|
|
53
53
|
"--cov-report=term-missing",
|
|
54
54
|
"--cov-report=html",
|
|
55
|
+
"-m", "not e2e", # Skip E2E tests by default (run with: pytest -m e2e)
|
|
55
56
|
]
|
|
56
57
|
|
|
57
58
|
[tool.ruff]
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""
|
|
2
|
+
QuickScale CLI package.
|
|
2
3
|
|
|
3
4
|
Reads the canonical version from the repository-level `VERSION` file so the CLI
|
|
4
5
|
stays in sync with other packages when the release is bumped in one place.
|
|
@@ -7,14 +8,13 @@ stays in sync with other packages when the release is bumped in one place.
|
|
|
7
8
|
from __future__ import annotations
|
|
8
9
|
|
|
9
10
|
from pathlib import Path
|
|
10
|
-
from typing import Tuple
|
|
11
11
|
|
|
12
12
|
__author__ = "Experto AI"
|
|
13
13
|
__email__ = "victor@experto.ai"
|
|
14
14
|
|
|
15
15
|
# Prefer an embedded package-level `_version.py` if it exists (build step).
|
|
16
16
|
try:
|
|
17
|
-
from ._version import __version__
|
|
17
|
+
from ._version import __version__
|
|
18
18
|
except Exception:
|
|
19
19
|
_root = Path(__file__).resolve().parents[3]
|
|
20
20
|
_version_file = _root / "VERSION"
|
|
@@ -25,7 +25,7 @@ except Exception:
|
|
|
25
25
|
|
|
26
26
|
# Version tuple for programmatic access
|
|
27
27
|
parts = __version__.split("-")[0].split(".")
|
|
28
|
-
VERSION:
|
|
28
|
+
VERSION: tuple[int, int, int] = (
|
|
29
29
|
int(parts[0]) if len(parts) > 0 else 0,
|
|
30
30
|
int(parts[1]) if len(parts) > 1 else 0,
|
|
31
31
|
int(parts[2]) if len(parts) > 2 else 0,
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
# Auto-generated by scripts/version_tool.sh
|
|
2
|
-
__version__ = "0.
|
|
2
|
+
__version__ = "0.59.0"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""CLI commands package for QuickScale."""
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
"""Development lifecycle commands for QuickScale projects."""
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
|
|
8
|
+
from quickscale_cli.utils.docker_utils import (
|
|
9
|
+
get_docker_compose_command,
|
|
10
|
+
is_docker_running,
|
|
11
|
+
)
|
|
12
|
+
from quickscale_cli.utils.project_manager import get_web_container_name, is_in_quickscale_project
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@click.command()
|
|
16
|
+
@click.option("--build", is_flag=True, help="Rebuild containers before starting")
|
|
17
|
+
@click.option("--no-cache", is_flag=True, help="Build without using cache")
|
|
18
|
+
def up(build: bool, no_cache: bool) -> None:
|
|
19
|
+
"""Start Docker services for development."""
|
|
20
|
+
# Check if in QuickScale project
|
|
21
|
+
if not is_in_quickscale_project():
|
|
22
|
+
click.secho("❌ Error: Not in a QuickScale project directory", fg="red", err=True)
|
|
23
|
+
click.echo(
|
|
24
|
+
"💡 Tip: Navigate to a project directory or run 'quickscale init <name>' to create one",
|
|
25
|
+
err=True,
|
|
26
|
+
)
|
|
27
|
+
sys.exit(1)
|
|
28
|
+
|
|
29
|
+
# Check if Docker is running
|
|
30
|
+
if not is_docker_running():
|
|
31
|
+
click.secho("❌ Error: Docker is not running", fg="red", err=True)
|
|
32
|
+
click.echo("💡 Tip: Start Docker Desktop or the Docker daemon", err=True)
|
|
33
|
+
sys.exit(1)
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
compose_cmd = get_docker_compose_command()
|
|
37
|
+
cmd = compose_cmd + ["up", "-d"]
|
|
38
|
+
|
|
39
|
+
if build or no_cache:
|
|
40
|
+
cmd.append("--build")
|
|
41
|
+
|
|
42
|
+
if no_cache:
|
|
43
|
+
cmd.append("--no-cache")
|
|
44
|
+
|
|
45
|
+
click.echo("🚀 Starting Docker services...")
|
|
46
|
+
subprocess.run(cmd, check=True)
|
|
47
|
+
click.secho("✅ Services started successfully!", fg="green", bold=True)
|
|
48
|
+
click.echo("💡 Tip: Use 'quickscale logs' to view service logs")
|
|
49
|
+
|
|
50
|
+
except subprocess.CalledProcessError as e:
|
|
51
|
+
click.secho(
|
|
52
|
+
f"❌ Error: Failed to start services (exit code: {e.returncode})", fg="red", err=True
|
|
53
|
+
)
|
|
54
|
+
click.echo("💡 Tip: Check Docker logs with 'quickscale logs' for details", err=True)
|
|
55
|
+
sys.exit(1)
|
|
56
|
+
except KeyboardInterrupt:
|
|
57
|
+
click.echo("\n⚠️ Interrupted by user")
|
|
58
|
+
sys.exit(130)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@click.command()
|
|
62
|
+
@click.option("--volumes", is_flag=True, help="Remove volumes as well")
|
|
63
|
+
def down(volumes: bool) -> None:
|
|
64
|
+
"""Stop Docker services."""
|
|
65
|
+
if not is_in_quickscale_project():
|
|
66
|
+
click.secho("❌ Error: Not in a QuickScale project directory", fg="red", err=True)
|
|
67
|
+
click.echo("💡 Tip: Navigate to a project directory", err=True)
|
|
68
|
+
sys.exit(1)
|
|
69
|
+
|
|
70
|
+
if not is_docker_running():
|
|
71
|
+
click.secho("❌ Error: Docker is not running", fg="red", err=True)
|
|
72
|
+
click.echo("💡 Tip: Start Docker Desktop or the Docker daemon", err=True)
|
|
73
|
+
sys.exit(1)
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
compose_cmd = get_docker_compose_command()
|
|
77
|
+
cmd = compose_cmd + ["down"]
|
|
78
|
+
|
|
79
|
+
if volumes:
|
|
80
|
+
cmd.append("--volumes")
|
|
81
|
+
|
|
82
|
+
click.echo("🛑 Stopping Docker services...")
|
|
83
|
+
subprocess.run(cmd, check=True)
|
|
84
|
+
click.secho("✅ Services stopped successfully!", fg="green")
|
|
85
|
+
|
|
86
|
+
except subprocess.CalledProcessError as e:
|
|
87
|
+
click.secho(
|
|
88
|
+
f"❌ Error: Failed to stop services (exit code: {e.returncode})", fg="red", err=True
|
|
89
|
+
)
|
|
90
|
+
sys.exit(1)
|
|
91
|
+
except KeyboardInterrupt:
|
|
92
|
+
click.echo("\n⚠️ Interrupted by user")
|
|
93
|
+
sys.exit(130)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@click.command()
|
|
97
|
+
@click.option("-c", "--command", "cmd", help="Run a single command instead of interactive shell")
|
|
98
|
+
def shell(cmd: str | None) -> None:
|
|
99
|
+
"""Open an interactive bash shell in the web container."""
|
|
100
|
+
if not is_in_quickscale_project():
|
|
101
|
+
click.secho("❌ Error: Not in a QuickScale project directory", fg="red", err=True)
|
|
102
|
+
click.echo("💡 Tip: Navigate to a project directory", err=True)
|
|
103
|
+
sys.exit(1)
|
|
104
|
+
|
|
105
|
+
if not is_docker_running():
|
|
106
|
+
click.secho("❌ Error: Docker is not running", fg="red", err=True)
|
|
107
|
+
click.echo("�� Tip: Start Docker Desktop or the Docker daemon", err=True)
|
|
108
|
+
sys.exit(1)
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
container_name = get_web_container_name()
|
|
112
|
+
|
|
113
|
+
if cmd:
|
|
114
|
+
# Run single command
|
|
115
|
+
docker_cmd = ["docker", "exec", container_name, "bash", "-c", cmd]
|
|
116
|
+
subprocess.run(docker_cmd, check=True)
|
|
117
|
+
else:
|
|
118
|
+
# Interactive shell
|
|
119
|
+
docker_cmd = ["docker", "exec", "-it", container_name, "bash"]
|
|
120
|
+
subprocess.run(docker_cmd, check=True)
|
|
121
|
+
|
|
122
|
+
except subprocess.CalledProcessError as e:
|
|
123
|
+
if e.returncode == 1:
|
|
124
|
+
click.secho("❌ Error: Container not running", fg="red", err=True)
|
|
125
|
+
click.echo("💡 Tip: Start services with 'quickscale up' first", err=True)
|
|
126
|
+
else:
|
|
127
|
+
click.secho(f"❌ Error: Command failed (exit code: {e.returncode})", fg="red", err=True)
|
|
128
|
+
sys.exit(e.returncode)
|
|
129
|
+
except KeyboardInterrupt:
|
|
130
|
+
click.echo("\n⚠️ Exited shell")
|
|
131
|
+
sys.exit(0)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@click.command(context_settings=dict(ignore_unknown_options=True))
|
|
135
|
+
@click.argument("args", nargs=-1, type=click.UNPROCESSED)
|
|
136
|
+
def manage(args: tuple) -> None:
|
|
137
|
+
"""Run Django management commands in the web container."""
|
|
138
|
+
if not is_in_quickscale_project():
|
|
139
|
+
click.secho("❌ Error: Not in a QuickScale project directory", fg="red", err=True)
|
|
140
|
+
click.echo("💡 Tip: Navigate to a project directory", err=True)
|
|
141
|
+
sys.exit(1)
|
|
142
|
+
|
|
143
|
+
if not is_docker_running():
|
|
144
|
+
click.secho("❌ Error: Docker is not running", fg="red", err=True)
|
|
145
|
+
click.echo("💡 Tip: Start Docker Desktop or the Docker daemon", err=True)
|
|
146
|
+
sys.exit(1)
|
|
147
|
+
|
|
148
|
+
if not args:
|
|
149
|
+
click.secho("❌ Error: No Django management command specified", fg="red", err=True)
|
|
150
|
+
click.echo("💡 Tip: Run 'quickscale manage help' to see available commands", err=True)
|
|
151
|
+
sys.exit(1)
|
|
152
|
+
|
|
153
|
+
try:
|
|
154
|
+
container_name = get_web_container_name()
|
|
155
|
+
docker_cmd = ["docker", "exec", "-it", container_name, "python", "manage.py"] + list(args)
|
|
156
|
+
subprocess.run(docker_cmd, check=True)
|
|
157
|
+
|
|
158
|
+
except subprocess.CalledProcessError as e:
|
|
159
|
+
if e.returncode == 1:
|
|
160
|
+
click.secho("❌ Error: Container not running or command failed", fg="red", err=True)
|
|
161
|
+
click.echo("💡 Tip: Start services with 'quickscale up' first", err=True)
|
|
162
|
+
sys.exit(e.returncode)
|
|
163
|
+
except KeyboardInterrupt:
|
|
164
|
+
click.echo("\n⚠️ Interrupted by user")
|
|
165
|
+
sys.exit(130)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@click.command()
|
|
169
|
+
@click.argument("service", required=False)
|
|
170
|
+
@click.option("-f", "--follow", is_flag=True, help="Follow log output")
|
|
171
|
+
@click.option("--tail", default=None, help="Number of lines to show from the end of the logs")
|
|
172
|
+
@click.option("--timestamps", is_flag=True, help="Show timestamps")
|
|
173
|
+
def logs(service: str | None, follow: bool, tail: str | None, timestamps: bool) -> None:
|
|
174
|
+
"""View Docker service logs."""
|
|
175
|
+
if not is_in_quickscale_project():
|
|
176
|
+
click.secho("❌ Error: Not in a QuickScale project directory", fg="red", err=True)
|
|
177
|
+
click.echo("💡 Tip: Navigate to a project directory", err=True)
|
|
178
|
+
sys.exit(1)
|
|
179
|
+
|
|
180
|
+
if not is_docker_running():
|
|
181
|
+
click.secho("❌ Error: Docker is not running", fg="red", err=True)
|
|
182
|
+
click.echo("�� Tip: Start Docker Desktop or the Docker daemon", err=True)
|
|
183
|
+
sys.exit(1)
|
|
184
|
+
|
|
185
|
+
try:
|
|
186
|
+
compose_cmd = get_docker_compose_command()
|
|
187
|
+
cmd = compose_cmd + ["logs"]
|
|
188
|
+
|
|
189
|
+
if follow:
|
|
190
|
+
cmd.append("--follow")
|
|
191
|
+
|
|
192
|
+
if tail:
|
|
193
|
+
cmd.extend(["--tail", tail])
|
|
194
|
+
|
|
195
|
+
if timestamps:
|
|
196
|
+
cmd.append("--timestamps")
|
|
197
|
+
|
|
198
|
+
if service:
|
|
199
|
+
cmd.append(service)
|
|
200
|
+
|
|
201
|
+
subprocess.run(cmd, check=True)
|
|
202
|
+
|
|
203
|
+
except subprocess.CalledProcessError as e:
|
|
204
|
+
click.secho(
|
|
205
|
+
f"❌ Error: Failed to retrieve logs (exit code: {e.returncode})", fg="red", err=True
|
|
206
|
+
)
|
|
207
|
+
sys.exit(1)
|
|
208
|
+
except KeyboardInterrupt:
|
|
209
|
+
click.echo("\n⚠️ Stopped following logs")
|
|
210
|
+
sys.exit(0)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
@click.command()
|
|
214
|
+
def ps() -> None:
|
|
215
|
+
"""Show service status."""
|
|
216
|
+
if not is_in_quickscale_project():
|
|
217
|
+
click.secho("❌ Error: Not in a QuickScale project directory", fg="red", err=True)
|
|
218
|
+
click.echo("💡 Tip: Navigate to a project directory", err=True)
|
|
219
|
+
sys.exit(1)
|
|
220
|
+
|
|
221
|
+
if not is_docker_running():
|
|
222
|
+
click.secho("❌ Error: Docker is not running", fg="red", err=True)
|
|
223
|
+
click.echo("💡 Tip: Start Docker Desktop or the Docker daemon", err=True)
|
|
224
|
+
sys.exit(1)
|
|
225
|
+
|
|
226
|
+
try:
|
|
227
|
+
compose_cmd = get_docker_compose_command()
|
|
228
|
+
cmd = compose_cmd + ["ps"]
|
|
229
|
+
subprocess.run(cmd, check=True)
|
|
230
|
+
|
|
231
|
+
except subprocess.CalledProcessError as e:
|
|
232
|
+
click.secho(
|
|
233
|
+
f"❌ Error: Failed to get service status (exit code: {e.returncode})",
|
|
234
|
+
fg="red",
|
|
235
|
+
err=True,
|
|
236
|
+
)
|
|
237
|
+
sys.exit(1)
|
|
@@ -6,6 +6,7 @@ import click
|
|
|
6
6
|
|
|
7
7
|
import quickscale_cli
|
|
8
8
|
import quickscale_core
|
|
9
|
+
from quickscale_cli.commands.development_commands import down, logs, manage, ps, shell, up
|
|
9
10
|
from quickscale_core.generator import ProjectGenerator
|
|
10
11
|
|
|
11
12
|
|
|
@@ -23,6 +24,15 @@ def version() -> None:
|
|
|
23
24
|
click.echo(f"QuickScale Core v{quickscale_core.__version__}")
|
|
24
25
|
|
|
25
26
|
|
|
27
|
+
# Register development commands
|
|
28
|
+
cli.add_command(up)
|
|
29
|
+
cli.add_command(down)
|
|
30
|
+
cli.add_command(shell)
|
|
31
|
+
cli.add_command(manage)
|
|
32
|
+
cli.add_command(logs)
|
|
33
|
+
cli.add_command(ps)
|
|
34
|
+
|
|
35
|
+
|
|
26
36
|
@cli.command()
|
|
27
37
|
@click.argument("project_name")
|
|
28
38
|
def init(project_name: str) -> None:
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Utility functions package for QuickScale CLI."""
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Docker interaction utilities for QuickScale CLI."""
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def is_docker_running() -> bool:
|
|
8
|
+
"""Check if Docker daemon is running."""
|
|
9
|
+
try:
|
|
10
|
+
subprocess.run(["docker", "info"], capture_output=True, check=True, timeout=5)
|
|
11
|
+
return True
|
|
12
|
+
except (subprocess.SubprocessError, FileNotFoundError, subprocess.TimeoutExpired):
|
|
13
|
+
return False
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def find_docker_compose() -> Path | None:
|
|
17
|
+
"""Locate docker-compose.yml in current directory."""
|
|
18
|
+
compose_file = Path("docker-compose.yml")
|
|
19
|
+
return compose_file if compose_file.exists() else None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def get_docker_compose_command() -> list[str]:
|
|
23
|
+
"""Get the appropriate docker compose command."""
|
|
24
|
+
# Try docker-compose first, fall back to docker compose
|
|
25
|
+
try:
|
|
26
|
+
subprocess.run(["docker-compose", "--version"], capture_output=True, check=True, timeout=2)
|
|
27
|
+
return ["docker-compose"]
|
|
28
|
+
except (subprocess.SubprocessError, FileNotFoundError, subprocess.TimeoutExpired):
|
|
29
|
+
# Fall back to docker compose (newer Docker versions)
|
|
30
|
+
return ["docker", "compose"]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def get_container_status(container_name: str) -> str | None:
|
|
34
|
+
"""Get status of a specific container."""
|
|
35
|
+
try:
|
|
36
|
+
result = subprocess.run(
|
|
37
|
+
["docker", "ps", "-a", "--filter", f"name={container_name}", "--format", "{{.Status}}"],
|
|
38
|
+
capture_output=True,
|
|
39
|
+
text=True,
|
|
40
|
+
check=True,
|
|
41
|
+
timeout=5,
|
|
42
|
+
)
|
|
43
|
+
return result.stdout.strip() or None
|
|
44
|
+
except (subprocess.SubprocessError, subprocess.TimeoutExpired):
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def exec_in_container(container_name: str, command: list[str], interactive: bool = False) -> int:
|
|
49
|
+
"""Execute command in a container."""
|
|
50
|
+
cmd = ["docker", "exec"]
|
|
51
|
+
if interactive:
|
|
52
|
+
cmd.append("-it")
|
|
53
|
+
cmd.append(container_name)
|
|
54
|
+
cmd.extend(command)
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
result = subprocess.run(cmd)
|
|
58
|
+
return result.returncode
|
|
59
|
+
except subprocess.SubprocessError:
|
|
60
|
+
return 1
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def get_running_containers() -> list[str]:
|
|
64
|
+
"""Get list of running QuickScale containers."""
|
|
65
|
+
try:
|
|
66
|
+
result = subprocess.run(
|
|
67
|
+
["docker", "ps", "--format", "{{.Names}}"],
|
|
68
|
+
capture_output=True,
|
|
69
|
+
text=True,
|
|
70
|
+
check=True,
|
|
71
|
+
timeout=5,
|
|
72
|
+
)
|
|
73
|
+
containers = [c for c in result.stdout.strip().split("\n") if c]
|
|
74
|
+
return containers
|
|
75
|
+
except (subprocess.SubprocessError, subprocess.TimeoutExpired):
|
|
76
|
+
return []
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Project state detection and management utilities."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from .docker_utils import find_docker_compose, get_running_containers
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_project_state() -> dict[str, Any]:
|
|
10
|
+
"""Get comprehensive project state including directory and containers."""
|
|
11
|
+
try:
|
|
12
|
+
current_dir = Path.cwd()
|
|
13
|
+
except OSError:
|
|
14
|
+
return {"has_project": False, "project_dir": None, "project_name": None, "containers": []}
|
|
15
|
+
|
|
16
|
+
compose_file = find_docker_compose()
|
|
17
|
+
has_project = compose_file is not None
|
|
18
|
+
containers = get_running_containers() if has_project else []
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
"has_project": has_project,
|
|
22
|
+
"project_dir": current_dir if has_project else None,
|
|
23
|
+
"project_name": current_dir.name if has_project else None,
|
|
24
|
+
"containers": containers,
|
|
25
|
+
"compose_file": compose_file,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def is_in_quickscale_project() -> bool:
|
|
30
|
+
"""Check if current directory is a QuickScale project."""
|
|
31
|
+
return find_docker_compose() is not None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def get_web_container_name() -> str:
|
|
35
|
+
"""Get the name of the web container (dynamically detected)."""
|
|
36
|
+
project_name = Path.cwd().name
|
|
37
|
+
containers = get_running_containers()
|
|
38
|
+
|
|
39
|
+
# Try different naming patterns used by Docker Compose
|
|
40
|
+
# Examples: test59_web, test59-web-1, test59_web_1
|
|
41
|
+
for container in containers:
|
|
42
|
+
if project_name in container and "web" in container:
|
|
43
|
+
return container
|
|
44
|
+
|
|
45
|
+
# Fallback to common patterns if no running container found
|
|
46
|
+
return f"{project_name}-web-1"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def get_db_container_name() -> str:
|
|
50
|
+
"""Get the name of the database container (dynamically detected)."""
|
|
51
|
+
project_name = Path.cwd().name
|
|
52
|
+
containers = get_running_containers()
|
|
53
|
+
|
|
54
|
+
# Try different naming patterns used by Docker Compose
|
|
55
|
+
# Examples: test59_db, test59-db-1, test59_db_1
|
|
56
|
+
for container in containers:
|
|
57
|
+
if project_name in container and "db" in container:
|
|
58
|
+
return container
|
|
59
|
+
|
|
60
|
+
# Fallback to common patterns if no running container found
|
|
61
|
+
return f"{project_name}-db-1"
|