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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: quickscale-cli
3
- Version: 0.62.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.1.0,<9.0.0)
22
- Requires-Dist: quickscale-core (>=0.62.0,<0.63.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.62.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.1.0"
28
- quickscale-core = "^0.62.0"
26
+ click = "^8.3.0"
27
+ quickscale-core = "^0.65.0"
29
28
 
30
29
  [tool.poetry.group.dev.dependencies]
31
- pytest = "^7.4.0"
32
- pytest-cov = "^4.1.0"
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
- [tool.ruff]
58
- line-length = 100
59
- target-version = "py310"
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
- [tool.mypy]
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.62.0"
2
+ __version__ = "0.65.0"
@@ -37,7 +37,9 @@ def deploy() -> None:
37
37
 
38
38
 
39
39
  @deploy.command()
40
- @click.option("--project-name", help="Railway project name (auto-detected if not provided)")
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("💡 The lock file needs to be regenerated to match pyproject.toml")
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("💡 Try running manually: poetry lock --no-update", err=True)
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("\nThen run this command again to auto-install Railway CLI", err=True)
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("⚠️ Warning: Failed to upgrade Railway CLI", fg="yellow")
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("Continue with current version?", default=False):
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)", err=True
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("❌ Error: Failed to initialize Railway project", fg="red", err=True)
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(f"⚠️ Warning: Could not create service '{app_service}'", fg="yellow")
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(" Or link via Railway dashboard > Variables > New Variable > Reference")
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("⚠️ Warning: Some environment variables could not be set", fg="yellow")
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(["up", "--service", app_service, "--detach"], timeout=60)
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(" - Ensure DATABASE_URL is linked to PostgreSQL service", err=True)
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")
@@ -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("❌ Error: Not in a QuickScale project directory", fg="red", err=True)
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
- click.secho(
53
- f"❌ Error: Failed to start services (exit code: {e.returncode})", fg="red", err=True
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
- click.echo("💡 Tip: Check Docker logs with 'quickscale logs' for details", err=True)
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("❌ Error: Not in a QuickScale project directory", fg="red", err=True)
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})", fg="red", err=True
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("-c", "--command", "cmd", help="Run a single command instead of interactive shell")
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("❌ Error: Not in a QuickScale project directory", fg="red", err=True)
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(docker_cmd, capture_output=True, text=True, check=True)
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(f"❌ Error: Command failed (exit code: {e.returncode})", fg="red", err=True)
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("❌ Error: Not in a QuickScale project directory", fg="red", err=True)
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("❌ Error: No Django management command specified", fg="red", err=True)
161
- click.echo("💡 Tip: Run 'quickscale manage help' to see available commands", err=True)
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(docker_cmd, capture_output=True, text=True, check=True)
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("❌ Error: Container not running or command failed", fg="red", err=True)
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("--tail", default=None, help="Number of lines to show from the end of the logs")
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("❌ Error: Not in a QuickScale project directory", fg="red", err=True)
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})", fg="red", err=True
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("❌ Error: Not in a QuickScale project directory", fg="red", err=True)
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