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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: quickscale-cli
3
- Version: 0.57.2
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.57.2,<0.58.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.57.2"
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.57.2"
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
- """QuickScale CLI package.
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__ # type: ignore
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: Tuple[int, int, int] = (
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.57.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"