quickscale-cli 0.65.0__tar.gz → 0.67.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.65.0 → quickscale_cli-0.67.0}/PKG-INFO +2 -2
- {quickscale_cli-0.65.0 → quickscale_cli-0.67.0}/pyproject.toml +2 -2
- {quickscale_cli-0.65.0 → quickscale_cli-0.67.0}/src/quickscale_cli/_version.py +1 -1
- {quickscale_cli-0.65.0 → quickscale_cli-0.67.0}/src/quickscale_cli/commands/development_commands.py +108 -2
- {quickscale_cli-0.65.0 → quickscale_cli-0.67.0}/src/quickscale_cli/commands/module_commands.py +459 -20
- {quickscale_cli-0.65.0 → quickscale_cli-0.67.0}/src/quickscale_cli/main.py +4 -4
- {quickscale_cli-0.65.0 → quickscale_cli-0.67.0}/src/quickscale_cli/utils/dependency_utils.py +100 -1
- {quickscale_cli-0.65.0 → quickscale_cli-0.67.0}/src/quickscale_cli/utils/docker_utils.py +10 -6
- {quickscale_cli-0.65.0 → quickscale_cli-0.67.0}/src/quickscale_cli/__init__.py +0 -0
- {quickscale_cli-0.65.0 → quickscale_cli-0.67.0}/src/quickscale_cli/commands/__init__.py +0 -0
- {quickscale_cli-0.65.0 → quickscale_cli-0.67.0}/src/quickscale_cli/commands/deployment_commands.py +0 -0
- {quickscale_cli-0.65.0 → quickscale_cli-0.67.0}/src/quickscale_cli/utils/__init__.py +0 -0
- {quickscale_cli-0.65.0 → quickscale_cli-0.67.0}/src/quickscale_cli/utils/project_manager.py +0 -0
- {quickscale_cli-0.65.0 → quickscale_cli-0.67.0}/src/quickscale_cli/utils/railway_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: quickscale-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.67.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
|
|
@@ -18,6 +18,6 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.11
|
|
19
19
|
Classifier: Programming Language :: Python :: 3.12
|
|
20
20
|
Requires-Dist: click (>=8.3.0,<9.0.0)
|
|
21
|
-
Requires-Dist: quickscale-core (>=0.
|
|
21
|
+
Requires-Dist: quickscale-core (>=0.67.0,<0.68.0)
|
|
22
22
|
Project-URL: Documentation, https://github.com/Experto-AI/quickscale/tree/main/docs
|
|
23
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.67.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"
|
|
@@ -24,7 +24,7 @@ packages = [{include = "quickscale_cli", from = "src"}]
|
|
|
24
24
|
[tool.poetry.dependencies]
|
|
25
25
|
python = "^3.10"
|
|
26
26
|
click = "^8.3.0"
|
|
27
|
-
quickscale-core = "^0.
|
|
27
|
+
quickscale-core = "^0.67.0"
|
|
28
28
|
|
|
29
29
|
[tool.poetry.group.dev.dependencies]
|
|
30
30
|
# Dev dependencies centralized in root pyproject.toml
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
# Auto-generated by scripts/version_tool.sh
|
|
2
|
-
__version__ = "0.
|
|
2
|
+
__version__ = "0.67.0"
|
{quickscale_cli-0.65.0 → quickscale_cli-0.67.0}/src/quickscale_cli/commands/development_commands.py
RENAMED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
"""Development lifecycle commands for QuickScale projects."""
|
|
2
2
|
|
|
3
|
+
import json
|
|
3
4
|
import re
|
|
4
5
|
import subprocess
|
|
5
6
|
import sys
|
|
7
|
+
from pathlib import Path
|
|
6
8
|
|
|
7
9
|
import click
|
|
8
10
|
|
|
@@ -42,6 +44,19 @@ def up(build: bool, no_cache: bool) -> None:
|
|
|
42
44
|
click.echo("💡 Tip: Start Docker Desktop or the Docker daemon", err=True)
|
|
43
45
|
sys.exit(1)
|
|
44
46
|
|
|
47
|
+
# Check if dependencies changed and suggest rebuild
|
|
48
|
+
if not build and _dependencies_changed_since_last_build():
|
|
49
|
+
click.secho(
|
|
50
|
+
"⚠️ Warning: Dependencies may have changed since last Docker build",
|
|
51
|
+
fg="yellow",
|
|
52
|
+
bold=True,
|
|
53
|
+
)
|
|
54
|
+
click.echo(
|
|
55
|
+
" This can happen after embedding modules or updating dependencies.\n"
|
|
56
|
+
" If you encounter import errors, rebuild the image:\n"
|
|
57
|
+
)
|
|
58
|
+
click.secho(" quickscale down && quickscale up --build\n", fg="cyan")
|
|
59
|
+
|
|
45
60
|
# Check if required port is available BEFORE calling docker-compose
|
|
46
61
|
port = get_port_from_env()
|
|
47
62
|
if not is_port_available(port):
|
|
@@ -62,7 +77,12 @@ def up(build: bool, no_cache: bool) -> None:
|
|
|
62
77
|
|
|
63
78
|
try:
|
|
64
79
|
compose_cmd = get_docker_compose_command()
|
|
65
|
-
|
|
80
|
+
|
|
81
|
+
# Use --progress plain during build for cleaner output without repetitive updates
|
|
82
|
+
if build or no_cache:
|
|
83
|
+
cmd = compose_cmd + ["--progress", "plain", "up", "-d"]
|
|
84
|
+
else:
|
|
85
|
+
cmd = compose_cmd + ["up", "-d"]
|
|
66
86
|
|
|
67
87
|
if build or no_cache:
|
|
68
88
|
cmd.append("--build")
|
|
@@ -71,10 +91,23 @@ def up(build: bool, no_cache: bool) -> None:
|
|
|
71
91
|
cmd.append("--no-cache")
|
|
72
92
|
|
|
73
93
|
click.echo("🚀 Starting Docker services...")
|
|
74
|
-
|
|
94
|
+
|
|
95
|
+
# Show output during build operations for transparency
|
|
96
|
+
# Capture output for simple startups to keep it clean
|
|
97
|
+
if build or no_cache:
|
|
98
|
+
click.echo("📦 Building Docker images...")
|
|
99
|
+
click.echo("") # Blank line for readability
|
|
100
|
+
subprocess.run(cmd, check=True, text=True)
|
|
101
|
+
else:
|
|
102
|
+
subprocess.run(cmd, check=True, capture_output=True, text=True)
|
|
103
|
+
|
|
75
104
|
click.secho("✅ Services started successfully!", fg="green", bold=True)
|
|
76
105
|
click.echo("💡 Tip: Use 'quickscale logs' to view service logs")
|
|
77
106
|
|
|
107
|
+
# Update build timestamp if build was performed
|
|
108
|
+
if build:
|
|
109
|
+
_update_last_build_timestamp()
|
|
110
|
+
|
|
78
111
|
except subprocess.CalledProcessError as e:
|
|
79
112
|
# Check for port conflict in error output
|
|
80
113
|
# Docker error format: "Bind for 0.0.0.0:8000 failed: port is already allocated"
|
|
@@ -372,3 +405,76 @@ def ps() -> None:
|
|
|
372
405
|
err=True,
|
|
373
406
|
)
|
|
374
407
|
sys.exit(1)
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def _get_build_state_file() -> Path:
|
|
411
|
+
"""Get path to build state tracking file."""
|
|
412
|
+
return Path.cwd() / ".quickscale" / "build_state.json"
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def _dependencies_changed_since_last_build() -> bool:
|
|
416
|
+
"""
|
|
417
|
+
Check if pyproject.toml or poetry.lock changed since last Docker build.
|
|
418
|
+
|
|
419
|
+
Returns
|
|
420
|
+
-------
|
|
421
|
+
True if dependencies may have changed, False otherwise
|
|
422
|
+
"""
|
|
423
|
+
build_state_file = _get_build_state_file()
|
|
424
|
+
pyproject_file = Path.cwd() / "pyproject.toml"
|
|
425
|
+
poetry_lock_file = Path.cwd() / "poetry.lock"
|
|
426
|
+
|
|
427
|
+
# If build state file doesn't exist, we can't determine if changed
|
|
428
|
+
# (likely first time running, or old project)
|
|
429
|
+
if not build_state_file.exists():
|
|
430
|
+
return False
|
|
431
|
+
|
|
432
|
+
# If dependency files don't exist, something is wrong but don't warn
|
|
433
|
+
if not pyproject_file.exists() or not poetry_lock_file.exists():
|
|
434
|
+
return False
|
|
435
|
+
|
|
436
|
+
try:
|
|
437
|
+
with open(build_state_file) as f:
|
|
438
|
+
build_state = json.load(f)
|
|
439
|
+
|
|
440
|
+
last_pyproject_mtime: float = build_state.get("pyproject_mtime", 0)
|
|
441
|
+
last_poetry_lock_mtime: float = build_state.get("poetry_lock_mtime", 0)
|
|
442
|
+
|
|
443
|
+
current_pyproject_mtime = pyproject_file.stat().st_mtime
|
|
444
|
+
current_poetry_lock_mtime = poetry_lock_file.stat().st_mtime
|
|
445
|
+
|
|
446
|
+
# Return True if either file changed since last build
|
|
447
|
+
changed: bool = (
|
|
448
|
+
current_pyproject_mtime > last_pyproject_mtime
|
|
449
|
+
or current_poetry_lock_mtime > last_poetry_lock_mtime
|
|
450
|
+
)
|
|
451
|
+
return changed
|
|
452
|
+
|
|
453
|
+
except (json.JSONDecodeError, KeyError, OSError):
|
|
454
|
+
# If we can't read state, don't warn (fail safe)
|
|
455
|
+
return False
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def _update_last_build_timestamp() -> None:
|
|
459
|
+
"""Update build state file with current dependency file timestamps."""
|
|
460
|
+
build_state_file = _get_build_state_file()
|
|
461
|
+
pyproject_file = Path.cwd() / "pyproject.toml"
|
|
462
|
+
poetry_lock_file = Path.cwd() / "poetry.lock"
|
|
463
|
+
|
|
464
|
+
# Ensure .quickscale directory exists
|
|
465
|
+
build_state_file.parent.mkdir(parents=True, exist_ok=True)
|
|
466
|
+
|
|
467
|
+
# Get current timestamps
|
|
468
|
+
pyproject_mtime = pyproject_file.stat().st_mtime if pyproject_file.exists() else 0
|
|
469
|
+
poetry_lock_mtime = (
|
|
470
|
+
poetry_lock_file.stat().st_mtime if poetry_lock_file.exists() else 0
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
# Write build state
|
|
474
|
+
build_state = {
|
|
475
|
+
"pyproject_mtime": pyproject_mtime,
|
|
476
|
+
"poetry_lock_mtime": poetry_lock_mtime,
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
with open(build_state_file, "w") as f:
|
|
480
|
+
json.dump(build_state, f, indent=2)
|
{quickscale_cli-0.65.0 → quickscale_cli-0.67.0}/src/quickscale_cli/commands/module_commands.py
RENAMED
|
@@ -18,7 +18,7 @@ from quickscale_core.utils.git_utils import (
|
|
|
18
18
|
)
|
|
19
19
|
|
|
20
20
|
# Available modules
|
|
21
|
-
AVAILABLE_MODULES = ["auth", "billing", "teams"]
|
|
21
|
+
AVAILABLE_MODULES = ["auth", "billing", "teams", "blog", "listings"]
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
def has_migrations_been_run() -> bool:
|
|
@@ -273,8 +273,277 @@ SITE_ID = 1
|
|
|
273
273
|
click.echo(f" • Authentication: {config['authentication_method']}")
|
|
274
274
|
|
|
275
275
|
|
|
276
|
+
def configure_blog_module() -> dict[str, Any]:
|
|
277
|
+
"""Interactive configuration for blog module"""
|
|
278
|
+
click.echo("\n⚙️ Configuring blog module...")
|
|
279
|
+
click.echo("The blog module will be configured with default settings.\n")
|
|
280
|
+
|
|
281
|
+
config = {
|
|
282
|
+
"posts_per_page": click.prompt(
|
|
283
|
+
"Posts per page",
|
|
284
|
+
type=int,
|
|
285
|
+
default=10,
|
|
286
|
+
),
|
|
287
|
+
"enable_rss": click.confirm("Enable RSS feed?", default=True),
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return config
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def apply_blog_configuration(project_path: Path, config: dict[str, Any]) -> None:
|
|
294
|
+
"""Apply blog module configuration to project settings"""
|
|
295
|
+
# QuickScale uses settings/base.py and project_name/urls.py structure
|
|
296
|
+
settings_path = project_path / f"{project_path.name}" / "settings" / "base.py"
|
|
297
|
+
urls_path = project_path / f"{project_path.name}" / "urls.py"
|
|
298
|
+
|
|
299
|
+
if not settings_path.exists():
|
|
300
|
+
click.secho(
|
|
301
|
+
"⚠️ Warning: settings.py not found, skipping auto-configuration",
|
|
302
|
+
fg="yellow",
|
|
303
|
+
)
|
|
304
|
+
return
|
|
305
|
+
|
|
306
|
+
# Read settings.py
|
|
307
|
+
with open(settings_path) as f:
|
|
308
|
+
settings_content = f.read()
|
|
309
|
+
|
|
310
|
+
# Check if already configured
|
|
311
|
+
if "quickscale_modules_blog" in settings_content:
|
|
312
|
+
click.echo("ℹ️ Blog module already configured in settings.py")
|
|
313
|
+
return
|
|
314
|
+
|
|
315
|
+
# Add required apps to INSTALLED_APPS
|
|
316
|
+
installed_apps_addition = """
|
|
317
|
+
# QuickScale Blog Module - Added by quickscale embed
|
|
318
|
+
INSTALLED_APPS += [
|
|
319
|
+
"markdownx", # Markdown editor with image upload
|
|
320
|
+
"quickscale_modules_blog", # Blog module
|
|
321
|
+
]
|
|
322
|
+
|
|
323
|
+
# Blog Module Settings
|
|
324
|
+
"""
|
|
325
|
+
|
|
326
|
+
installed_apps_addition += f"BLOG_POSTS_PER_PAGE = {config['posts_per_page']}\n"
|
|
327
|
+
installed_apps_addition += """MARKDOWNX_MARKDOWN_EXTENSIONS = [
|
|
328
|
+
"markdown.extensions.fenced_code",
|
|
329
|
+
"markdown.extensions.tables",
|
|
330
|
+
"markdown.extensions.toc",
|
|
331
|
+
]
|
|
332
|
+
MARKDOWNX_MEDIA_PATH = "blog/markdownx/"
|
|
333
|
+
"""
|
|
334
|
+
|
|
335
|
+
# Append to settings.py
|
|
336
|
+
with open(settings_path, "a") as f:
|
|
337
|
+
f.write("\n" + installed_apps_addition)
|
|
338
|
+
|
|
339
|
+
click.secho(" ✅ Updated settings.py with blog configuration", fg="green")
|
|
340
|
+
|
|
341
|
+
# Update urls.py
|
|
342
|
+
if urls_path.exists():
|
|
343
|
+
with open(urls_path) as f:
|
|
344
|
+
urls_content = f.read()
|
|
345
|
+
|
|
346
|
+
if "quickscale_modules_blog" not in urls_content:
|
|
347
|
+
# Find urlpatterns and add blog URLs
|
|
348
|
+
if "urlpatterns = [" in urls_content:
|
|
349
|
+
urls_addition = (
|
|
350
|
+
' path("blog/", include("quickscale_modules_blog.urls")),\n'
|
|
351
|
+
)
|
|
352
|
+
if config["enable_rss"]:
|
|
353
|
+
urls_addition += ' path("markdownx/", include("markdownx.urls")), # Markdown editor upload\n'
|
|
354
|
+
urls_content = urls_content.replace(
|
|
355
|
+
"urlpatterns = [", "urlpatterns = [\n" + urls_addition
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
with open(urls_path, "w") as f:
|
|
359
|
+
f.write(urls_content)
|
|
360
|
+
|
|
361
|
+
click.secho(" ✅ Updated urls.py with blog URLs", fg="green")
|
|
362
|
+
|
|
363
|
+
# Show configuration summary
|
|
364
|
+
click.echo("\n📋 Configuration applied:")
|
|
365
|
+
click.echo(f" • Posts per page: {config['posts_per_page']}")
|
|
366
|
+
click.echo(f" • RSS feed: {'Enabled' if config['enable_rss'] else 'Disabled'}")
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def configure_listings_module() -> dict[str, Any]:
|
|
370
|
+
"""Interactive configuration for listings module"""
|
|
371
|
+
click.echo("\n⚙️ Configuring listings module...")
|
|
372
|
+
click.echo(
|
|
373
|
+
"The listings module provides an abstract base model for marketplace listings.\n"
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
config = {
|
|
377
|
+
"listings_per_page": click.prompt(
|
|
378
|
+
"Listings per page",
|
|
379
|
+
type=int,
|
|
380
|
+
default=12,
|
|
381
|
+
),
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
return config
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def apply_listings_configuration(project_path: Path, config: dict[str, Any]) -> None:
|
|
388
|
+
"""Apply listings module configuration to project settings"""
|
|
389
|
+
import re
|
|
390
|
+
|
|
391
|
+
# QuickScale uses settings/base.py and project_name/urls.py structure
|
|
392
|
+
settings_path = project_path / f"{project_path.name}" / "settings" / "base.py"
|
|
393
|
+
urls_path = project_path / f"{project_path.name}" / "urls.py"
|
|
394
|
+
pyproject_path = project_path / "pyproject.toml"
|
|
395
|
+
|
|
396
|
+
if not settings_path.exists():
|
|
397
|
+
click.secho(
|
|
398
|
+
"⚠️ Warning: settings.py not found, skipping auto-configuration",
|
|
399
|
+
fg="yellow",
|
|
400
|
+
)
|
|
401
|
+
return
|
|
402
|
+
|
|
403
|
+
# Read settings.py
|
|
404
|
+
with open(settings_path) as f:
|
|
405
|
+
settings_content = f.read()
|
|
406
|
+
|
|
407
|
+
# Check if already configured
|
|
408
|
+
if "quickscale_modules_listings" in settings_content:
|
|
409
|
+
click.echo("ℹ️ Listings module already configured in settings.py")
|
|
410
|
+
return
|
|
411
|
+
|
|
412
|
+
# Add django-filter dependency to pyproject.toml
|
|
413
|
+
if pyproject_path.exists():
|
|
414
|
+
with open(pyproject_path) as f:
|
|
415
|
+
pyproject_content = f.read()
|
|
416
|
+
|
|
417
|
+
if "django-filter" not in pyproject_content:
|
|
418
|
+
# Read django-filter version from the embedded listings module
|
|
419
|
+
listings_pyproject_path = (
|
|
420
|
+
project_path / "modules" / "listings" / "pyproject.toml"
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
if not listings_pyproject_path.exists():
|
|
424
|
+
click.secho(
|
|
425
|
+
"❌ Error: Listings module pyproject.toml not found. "
|
|
426
|
+
"Cannot determine django-filter version requirement.",
|
|
427
|
+
fg="red",
|
|
428
|
+
err=True,
|
|
429
|
+
)
|
|
430
|
+
click.echo(f"Expected file: {listings_pyproject_path}", err=True)
|
|
431
|
+
click.echo(
|
|
432
|
+
"This indicates the listings module was not embedded correctly.",
|
|
433
|
+
err=True,
|
|
434
|
+
)
|
|
435
|
+
raise click.Abort()
|
|
436
|
+
|
|
437
|
+
# Extract django-filter version using regex
|
|
438
|
+
try:
|
|
439
|
+
with open(listings_pyproject_path) as f:
|
|
440
|
+
listings_pyproject_content = f.read()
|
|
441
|
+
|
|
442
|
+
version_match = re.search(
|
|
443
|
+
r'django-filter\s*=\s*["\']([^"\']+)["\']',
|
|
444
|
+
listings_pyproject_content,
|
|
445
|
+
)
|
|
446
|
+
if not version_match:
|
|
447
|
+
click.secho(
|
|
448
|
+
"❌ Error: Cannot find django-filter version in listings "
|
|
449
|
+
"module's pyproject.toml",
|
|
450
|
+
fg="red",
|
|
451
|
+
err=True,
|
|
452
|
+
)
|
|
453
|
+
click.echo(f"File: {listings_pyproject_path}", err=True)
|
|
454
|
+
click.echo('Expected format: django-filter = "^x.x.x"', err=True)
|
|
455
|
+
click.echo(
|
|
456
|
+
"Please check the listings module's dependencies.", err=True
|
|
457
|
+
)
|
|
458
|
+
raise click.Abort()
|
|
459
|
+
django_filter_version = version_match.group(1)
|
|
460
|
+
except (FileNotFoundError, AttributeError) as e:
|
|
461
|
+
click.secho(
|
|
462
|
+
f"❌ Error: Failed to parse django-filter version from listings "
|
|
463
|
+
f"module: {e}",
|
|
464
|
+
fg="red",
|
|
465
|
+
err=True,
|
|
466
|
+
)
|
|
467
|
+
click.echo(f"File: {listings_pyproject_path}", err=True)
|
|
468
|
+
click.echo(
|
|
469
|
+
"Please ensure the listings module is properly embedded and its "
|
|
470
|
+
"pyproject.toml is valid.",
|
|
471
|
+
err=True,
|
|
472
|
+
)
|
|
473
|
+
raise click.Abort()
|
|
474
|
+
|
|
475
|
+
# Try to add to [tool.poetry.dependencies] section
|
|
476
|
+
dependencies_pattern = r"(\[tool\.poetry\.dependencies\][^\[]*)"
|
|
477
|
+
match = re.search(dependencies_pattern, pyproject_content, re.DOTALL)
|
|
478
|
+
if match:
|
|
479
|
+
dependencies_section = match.group(1)
|
|
480
|
+
# Add django-filter after the python version line
|
|
481
|
+
updated_dependencies = re.sub(
|
|
482
|
+
r'(python = "[^"]*")',
|
|
483
|
+
rf'\1\ndjango-filter = "{django_filter_version}"',
|
|
484
|
+
dependencies_section,
|
|
485
|
+
)
|
|
486
|
+
pyproject_content = pyproject_content.replace(
|
|
487
|
+
dependencies_section, updated_dependencies
|
|
488
|
+
)
|
|
489
|
+
|
|
490
|
+
with open(pyproject_path, "w") as f:
|
|
491
|
+
f.write(pyproject_content)
|
|
492
|
+
|
|
493
|
+
click.secho(" ✅ Added django-filter to pyproject.toml", fg="green")
|
|
494
|
+
else:
|
|
495
|
+
click.secho(
|
|
496
|
+
"⚠️ Warning: Could not find [tool.poetry.dependencies] section in "
|
|
497
|
+
"pyproject.toml",
|
|
498
|
+
fg="yellow",
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
# Add required apps to INSTALLED_APPS
|
|
502
|
+
installed_apps_addition = """
|
|
503
|
+
# QuickScale Listings Module - Added by quickscale embed
|
|
504
|
+
INSTALLED_APPS += [
|
|
505
|
+
"django_filters", # Filtering support
|
|
506
|
+
"quickscale_modules_listings", # Listings module
|
|
507
|
+
]
|
|
508
|
+
|
|
509
|
+
# Listings Module Settings
|
|
510
|
+
"""
|
|
511
|
+
|
|
512
|
+
installed_apps_addition += f"LISTINGS_PER_PAGE = {config['listings_per_page']}\n"
|
|
513
|
+
|
|
514
|
+
# Append to settings.py
|
|
515
|
+
with open(settings_path, "a") as f:
|
|
516
|
+
f.write("\n" + installed_apps_addition)
|
|
517
|
+
|
|
518
|
+
click.secho(" ✅ Updated settings.py with listings configuration", fg="green")
|
|
519
|
+
|
|
520
|
+
# Update urls.py
|
|
521
|
+
if urls_path.exists():
|
|
522
|
+
with open(urls_path) as f:
|
|
523
|
+
urls_content = f.read()
|
|
524
|
+
|
|
525
|
+
if "quickscale_modules_listings" not in urls_content:
|
|
526
|
+
# Find urlpatterns and add listings URLs
|
|
527
|
+
if "urlpatterns = [" in urls_content:
|
|
528
|
+
urls_addition = ' path("listings/", include("quickscale_modules_listings.urls")),\n'
|
|
529
|
+
urls_content = urls_content.replace(
|
|
530
|
+
"urlpatterns = [", "urlpatterns = [\n" + urls_addition
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
with open(urls_path, "w") as f:
|
|
534
|
+
f.write(urls_content)
|
|
535
|
+
|
|
536
|
+
click.secho(" ✅ Updated urls.py with listings URLs", fg="green")
|
|
537
|
+
|
|
538
|
+
# Show configuration summary
|
|
539
|
+
click.echo("\n📋 Configuration applied:")
|
|
540
|
+
click.echo(f" • Listings per page: {config['listings_per_page']}")
|
|
541
|
+
|
|
542
|
+
|
|
276
543
|
MODULE_CONFIGURATORS = {
|
|
277
544
|
"auth": (configure_auth_module, apply_auth_configuration),
|
|
545
|
+
"blog": (configure_blog_module, apply_blog_configuration),
|
|
546
|
+
"listings": (configure_listings_module, apply_listings_configuration),
|
|
278
547
|
}
|
|
279
548
|
|
|
280
549
|
|
|
@@ -298,16 +567,20 @@ def embed(module: str, remote: str) -> None:
|
|
|
298
567
|
Examples:
|
|
299
568
|
quickscale embed --module auth
|
|
300
569
|
quickscale embed --module billing
|
|
570
|
+
quickscale embed --module blog
|
|
571
|
+
quickscale embed --module listings
|
|
301
572
|
|
|
302
573
|
\b
|
|
303
|
-
Available modules
|
|
304
|
-
- auth: Authentication with django-allauth
|
|
574
|
+
Available modules:
|
|
575
|
+
- auth: Authentication with django-allauth
|
|
305
576
|
- billing: Stripe integration with dj-stripe (placeholder)
|
|
306
577
|
- teams: Multi-tenancy and team management (placeholder)
|
|
578
|
+
- blog: Markdown-powered blog with categories, tags, and RSS
|
|
579
|
+
- listings: Generic listings with filtering for marketplace verticals
|
|
307
580
|
|
|
308
581
|
\b
|
|
309
|
-
Note:
|
|
310
|
-
|
|
582
|
+
Note: Auth, blog, and listings modules are fully implemented.
|
|
583
|
+
Billing and teams modules contain placeholder READMEs.
|
|
311
584
|
"""
|
|
312
585
|
try:
|
|
313
586
|
# Validate git repository
|
|
@@ -420,37 +693,117 @@ def embed(module: str, remote: str) -> None:
|
|
|
420
693
|
project_root = Path.cwd()
|
|
421
694
|
applier(project_root, config)
|
|
422
695
|
|
|
423
|
-
# Install dependencies
|
|
424
|
-
if module
|
|
425
|
-
click.echo("📦 Installing dependencies...")
|
|
696
|
+
# Install dependencies for modules that need it
|
|
697
|
+
if module in ["auth", "blog", "listings"]:
|
|
698
|
+
click.echo("\n📦 Installing dependencies...")
|
|
426
699
|
try:
|
|
427
700
|
import subprocess
|
|
428
701
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
702
|
+
project_root = Path.cwd()
|
|
703
|
+
module_path = project_root / "modules" / module
|
|
704
|
+
|
|
705
|
+
# Verify module was actually embedded
|
|
706
|
+
if not module_path.exists():
|
|
707
|
+
click.secho(
|
|
708
|
+
f"❌ Error: Module directory not found at {module_path}",
|
|
709
|
+
fg="red",
|
|
710
|
+
err=True,
|
|
711
|
+
)
|
|
712
|
+
click.echo(
|
|
713
|
+
" The git subtree add may have failed. Check the output above.",
|
|
714
|
+
err=True,
|
|
715
|
+
)
|
|
716
|
+
raise click.Abort()
|
|
717
|
+
|
|
718
|
+
# Determine the correct path to add
|
|
719
|
+
# Sometimes the split branch might contain the full repo (e.g. during dev/testing)
|
|
720
|
+
# We need to find the actual module's pyproject.toml
|
|
721
|
+
target_path = module_path
|
|
722
|
+
nested_path = module_path / "quickscale_modules" / module
|
|
723
|
+
|
|
724
|
+
if nested_path.exists() and (nested_path / "pyproject.toml").exists():
|
|
725
|
+
click.secho(
|
|
726
|
+
f"⚠️ Warning: Detected full repository in {module} module path.",
|
|
727
|
+
fg="yellow",
|
|
728
|
+
)
|
|
729
|
+
click.echo(
|
|
730
|
+
f" Using nested path: {nested_path.relative_to(project_root)}"
|
|
731
|
+
)
|
|
732
|
+
target_path = nested_path
|
|
733
|
+
|
|
734
|
+
# Install the module
|
|
735
|
+
click.echo(f" • Installing {module} module...")
|
|
736
|
+
result = subprocess.run(
|
|
737
|
+
["poetry", "add", f"./{target_path.relative_to(project_root)}"],
|
|
432
738
|
cwd=project_root,
|
|
433
739
|
capture_output=True,
|
|
434
740
|
text=True,
|
|
435
|
-
check=True,
|
|
436
741
|
)
|
|
437
|
-
|
|
742
|
+
|
|
743
|
+
if result.returncode != 0:
|
|
744
|
+
click.secho(
|
|
745
|
+
f"\n❌ Failed to install {module} module",
|
|
746
|
+
fg="red",
|
|
747
|
+
err=True,
|
|
748
|
+
bold=True,
|
|
749
|
+
)
|
|
750
|
+
click.echo("\n📋 Error output (stderr):", err=True)
|
|
751
|
+
click.echo(result.stderr, err=True)
|
|
752
|
+
click.echo("\n📋 Standard output (stdout):", err=True)
|
|
753
|
+
click.echo(result.stdout, err=True)
|
|
754
|
+
|
|
755
|
+
click.echo("\n💡 To fix this manually:", err=True)
|
|
756
|
+
click.echo(f" 1. cd {project_root}", err=True)
|
|
757
|
+
click.echo(f" 2. poetry add ./modules/{module}", err=True)
|
|
758
|
+
click.echo(" 3. poetry install", err=True)
|
|
759
|
+
click.echo(" 4. poetry run python manage.py migrate", err=True)
|
|
760
|
+
raise click.Abort()
|
|
761
|
+
|
|
762
|
+
click.secho(
|
|
763
|
+
f" ✅ {module.capitalize()} module installed successfully",
|
|
764
|
+
fg="green",
|
|
765
|
+
)
|
|
438
766
|
|
|
439
767
|
# Install dependencies
|
|
440
|
-
|
|
768
|
+
click.echo(" • Installing all dependencies...")
|
|
769
|
+
result = subprocess.run(
|
|
441
770
|
["poetry", "install"],
|
|
442
771
|
cwd=project_root,
|
|
443
772
|
capture_output=True,
|
|
444
773
|
text=True,
|
|
445
|
-
check=True,
|
|
446
774
|
)
|
|
775
|
+
|
|
776
|
+
if result.returncode != 0:
|
|
777
|
+
click.secho(
|
|
778
|
+
"\n❌ Failed to install dependencies",
|
|
779
|
+
fg="red",
|
|
780
|
+
err=True,
|
|
781
|
+
bold=True,
|
|
782
|
+
)
|
|
783
|
+
click.echo("\n📋 Error output (stderr):", err=True)
|
|
784
|
+
click.echo(result.stderr, err=True)
|
|
785
|
+
click.echo("\n📋 Standard output (stdout):", err=True)
|
|
786
|
+
click.echo(result.stdout, err=True)
|
|
787
|
+
|
|
788
|
+
click.echo("\n💡 To fix this manually:", err=True)
|
|
789
|
+
click.echo(f" 1. cd {project_root}", err=True)
|
|
790
|
+
click.echo(" 2. poetry install", err=True)
|
|
791
|
+
click.echo(" 3. poetry run python manage.py migrate", err=True)
|
|
792
|
+
raise click.Abort()
|
|
793
|
+
|
|
447
794
|
click.secho(" ✅ Dependencies installed successfully", fg="green")
|
|
795
|
+
|
|
448
796
|
except subprocess.CalledProcessError as e:
|
|
449
797
|
click.secho(
|
|
450
|
-
f"
|
|
451
|
-
fg="
|
|
798
|
+
f"\n❌ Unexpected error during dependency installation: {e}",
|
|
799
|
+
fg="red",
|
|
800
|
+
err=True,
|
|
801
|
+
)
|
|
802
|
+
click.echo(
|
|
803
|
+
f"\n💡 Try running 'poetry install' manually in {project_root}",
|
|
804
|
+
err=True,
|
|
452
805
|
)
|
|
453
|
-
click.
|
|
806
|
+
raise click.Abort()
|
|
454
807
|
|
|
455
808
|
# Success message
|
|
456
809
|
click.secho(
|
|
@@ -475,10 +828,96 @@ def embed(module: str, remote: str) -> None:
|
|
|
475
828
|
" 3. Create superuser (optional): poetry run python manage.py createsuperuser"
|
|
476
829
|
)
|
|
477
830
|
click.echo(" 4. Start development server:")
|
|
478
|
-
click.echo("
|
|
831
|
+
click.echo("")
|
|
832
|
+
click.secho(
|
|
833
|
+
" ⚠️ IMPORTANT: Use --build flag with Docker",
|
|
834
|
+
fg="yellow",
|
|
835
|
+
bold=True,
|
|
836
|
+
)
|
|
837
|
+
click.secho(
|
|
838
|
+
" • With Docker: quickscale down && quickscale up --build",
|
|
839
|
+
fg="cyan",
|
|
840
|
+
bold=True,
|
|
841
|
+
)
|
|
842
|
+
click.secho(
|
|
843
|
+
" ^^^ --build is REQUIRED to install new dependencies",
|
|
844
|
+
fg="yellow",
|
|
845
|
+
)
|
|
846
|
+
click.echo("")
|
|
479
847
|
click.echo(" • Without Docker: poetry run python manage.py runserver")
|
|
480
848
|
click.echo(" 5. Visit http://localhost:8000/accounts/login/")
|
|
481
|
-
click.echo("\n
|
|
849
|
+
click.echo("\n📖 Documentation: modules/auth/README.md")
|
|
850
|
+
elif module == "blog":
|
|
851
|
+
click.echo(f" 1. Review module code in modules/{module}/")
|
|
852
|
+
click.secho(
|
|
853
|
+
" 2. ⚠️ IMPORTANT: Run migrations (required before server start):",
|
|
854
|
+
fg="yellow",
|
|
855
|
+
bold=True,
|
|
856
|
+
)
|
|
857
|
+
click.secho(
|
|
858
|
+
" poetry run python manage.py migrate", fg="cyan", bold=True
|
|
859
|
+
)
|
|
860
|
+
click.echo(
|
|
861
|
+
" 3. Create superuser (optional): poetry run python manage.py createsuperuser"
|
|
862
|
+
)
|
|
863
|
+
click.echo(" 4. Start development server:")
|
|
864
|
+
click.echo("")
|
|
865
|
+
click.secho(
|
|
866
|
+
" ⚠️ IMPORTANT: Use --build flag with Docker",
|
|
867
|
+
fg="yellow",
|
|
868
|
+
bold=True,
|
|
869
|
+
)
|
|
870
|
+
click.secho(
|
|
871
|
+
" • With Docker: quickscale down && quickscale up --build",
|
|
872
|
+
fg="cyan",
|
|
873
|
+
bold=True,
|
|
874
|
+
)
|
|
875
|
+
click.secho(
|
|
876
|
+
" ^^^ --build is REQUIRED to install new dependencies",
|
|
877
|
+
fg="yellow",
|
|
878
|
+
)
|
|
879
|
+
click.echo("")
|
|
880
|
+
click.echo(" • Without Docker: poetry run python manage.py runserver")
|
|
881
|
+
click.echo(" 5. Visit http://localhost:8000/admin/ to create blog posts")
|
|
882
|
+
click.echo(" 6. View your blog at http://localhost:8000/blog/")
|
|
883
|
+
click.echo(" 7. RSS feed available at http://localhost:8000/blog/feed/")
|
|
884
|
+
click.echo("\n📖 Documentation: modules/blog/README.md")
|
|
885
|
+
elif module == "listings":
|
|
886
|
+
click.echo(f" 1. Review module code in modules/{module}/")
|
|
887
|
+
click.secho(
|
|
888
|
+
" 2. ⚠️ IMPORTANT: Run migrations (required before server start):",
|
|
889
|
+
fg="yellow",
|
|
890
|
+
bold=True,
|
|
891
|
+
)
|
|
892
|
+
click.secho(
|
|
893
|
+
" poetry run python manage.py migrate", fg="cyan", bold=True
|
|
894
|
+
)
|
|
895
|
+
click.echo(
|
|
896
|
+
" 3. Create superuser (optional): poetry run python manage.py createsuperuser"
|
|
897
|
+
)
|
|
898
|
+
click.echo(" 4. Start development server:")
|
|
899
|
+
click.echo("")
|
|
900
|
+
click.secho(
|
|
901
|
+
" ⚠️ IMPORTANT: Use --build flag with Docker",
|
|
902
|
+
fg="yellow",
|
|
903
|
+
bold=True,
|
|
904
|
+
)
|
|
905
|
+
click.secho(
|
|
906
|
+
" • With Docker: quickscale down && quickscale up --build",
|
|
907
|
+
fg="cyan",
|
|
908
|
+
bold=True,
|
|
909
|
+
)
|
|
910
|
+
click.secho(
|
|
911
|
+
" ^^^ --build is REQUIRED to install new dependencies",
|
|
912
|
+
fg="yellow",
|
|
913
|
+
)
|
|
914
|
+
click.echo("")
|
|
915
|
+
click.echo(" • Without Docker: poetry run python manage.py runserver")
|
|
916
|
+
click.echo(
|
|
917
|
+
" 5. Create a concrete model extending AbstractListing (see README)"
|
|
918
|
+
)
|
|
919
|
+
click.echo(" 6. View listings at http://localhost:8000/listings/")
|
|
920
|
+
click.echo("\n📖 Documentation: modules/listings/README.md")
|
|
482
921
|
else:
|
|
483
922
|
click.echo(f" 1. Review the module code in modules/{module}/")
|
|
484
923
|
click.echo(f" 2. Follow setup instructions in modules/{module}/README.md")
|
|
@@ -97,8 +97,8 @@ def init(project_name: str, theme: str) -> None:
|
|
|
97
97
|
\b
|
|
98
98
|
Available themes:
|
|
99
99
|
showcase_html Pure HTML + CSS (default, production-ready)
|
|
100
|
-
|
|
101
|
-
|
|
100
|
+
showcase_htmx HTMX + Alpine.js (coming in v0.70.0)
|
|
101
|
+
showcase_react React + TypeScript SPA (coming in v0.71.0)
|
|
102
102
|
"""
|
|
103
103
|
# Step 1: Check system dependencies BEFORE generation
|
|
104
104
|
click.echo("🔍 Checking system dependencies...")
|
|
@@ -145,8 +145,8 @@ def init(project_name: str, theme: str) -> None:
|
|
|
145
145
|
click.echo(
|
|
146
146
|
f"\n💡 The '{theme}' theme is planned for a future release:", err=True
|
|
147
147
|
)
|
|
148
|
-
click.echo(" - showcase_htmx: Coming in v0.
|
|
149
|
-
click.echo(" - showcase_react: Coming in v0.
|
|
148
|
+
click.echo(" - showcase_htmx: Coming in v0.70.0", err=True)
|
|
149
|
+
click.echo(" - showcase_react: Coming in v0.71.0", err=True)
|
|
150
150
|
click.echo("\n📖 For now, use the default 'showcase_html' theme", err=True)
|
|
151
151
|
raise click.Abort()
|
|
152
152
|
|
{quickscale_cli-0.65.0 → quickscale_cli-0.67.0}/src/quickscale_cli/utils/dependency_utils.py
RENAMED
|
@@ -1,10 +1,36 @@
|
|
|
1
1
|
"""System dependency checking utilities."""
|
|
2
2
|
|
|
3
|
+
import os
|
|
3
4
|
import shutil
|
|
4
5
|
import subprocess
|
|
5
6
|
from typing import NamedTuple
|
|
6
7
|
|
|
7
8
|
|
|
9
|
+
SKIP_ENV_VAR = "QUICKSCALE_SKIP_DEPENDENCY_CHECKS"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _should_skip_dependency_checks() -> bool:
|
|
13
|
+
"""Return True when dependency checks should be skipped."""
|
|
14
|
+
|
|
15
|
+
value = os.getenv(SKIP_ENV_VAR, "")
|
|
16
|
+
return value.strip().lower() in {"1", "true", "yes", "on"}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _skipped_dependency(
|
|
20
|
+
name: str, required: bool, purpose: str, version_env: str | None = None
|
|
21
|
+
) -> "DependencyStatus":
|
|
22
|
+
"""Create a dependency status representing a skipped check."""
|
|
23
|
+
|
|
24
|
+
version = os.getenv(version_env) if version_env else None
|
|
25
|
+
return DependencyStatus(
|
|
26
|
+
name=name,
|
|
27
|
+
installed=True,
|
|
28
|
+
version=version,
|
|
29
|
+
required=required,
|
|
30
|
+
purpose=purpose,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
8
34
|
class DependencyStatus(NamedTuple):
|
|
9
35
|
"""Status of a system dependency check."""
|
|
10
36
|
|
|
@@ -35,6 +61,14 @@ def check_python_version() -> DependencyStatus:
|
|
|
35
61
|
|
|
36
62
|
def check_poetry_installed() -> DependencyStatus:
|
|
37
63
|
"""Check if Poetry is installed."""
|
|
64
|
+
|
|
65
|
+
if _should_skip_dependency_checks():
|
|
66
|
+
return _skipped_dependency(
|
|
67
|
+
name="Poetry",
|
|
68
|
+
required=True,
|
|
69
|
+
purpose="Dependency management for generated projects",
|
|
70
|
+
version_env="QUICKSCALE_MOCK_POETRY_VERSION",
|
|
71
|
+
)
|
|
38
72
|
try:
|
|
39
73
|
result = subprocess.run(
|
|
40
74
|
["poetry", "--version"],
|
|
@@ -66,6 +100,14 @@ def check_poetry_installed() -> DependencyStatus:
|
|
|
66
100
|
|
|
67
101
|
def check_git_installed() -> DependencyStatus:
|
|
68
102
|
"""Check if Git is installed."""
|
|
103
|
+
|
|
104
|
+
if _should_skip_dependency_checks():
|
|
105
|
+
return _skipped_dependency(
|
|
106
|
+
name="Git",
|
|
107
|
+
required=False,
|
|
108
|
+
purpose="Version control and module management (quickscale embed/update)",
|
|
109
|
+
version_env="QUICKSCALE_MOCK_GIT_VERSION",
|
|
110
|
+
)
|
|
69
111
|
git_path = shutil.which("git")
|
|
70
112
|
|
|
71
113
|
if git_path:
|
|
@@ -105,7 +147,15 @@ def check_git_installed() -> DependencyStatus:
|
|
|
105
147
|
|
|
106
148
|
|
|
107
149
|
def check_docker_installed() -> DependencyStatus:
|
|
108
|
-
"""Check if Docker
|
|
150
|
+
"""Check if Docker and Docker Compose are installed."""
|
|
151
|
+
|
|
152
|
+
if _should_skip_dependency_checks():
|
|
153
|
+
return _skipped_dependency(
|
|
154
|
+
name="Docker",
|
|
155
|
+
required=False,
|
|
156
|
+
purpose="Containerized development (quickscale up/down)",
|
|
157
|
+
version_env="QUICKSCALE_MOCK_DOCKER_VERSION",
|
|
158
|
+
)
|
|
109
159
|
docker_path = shutil.which("docker")
|
|
110
160
|
|
|
111
161
|
if docker_path:
|
|
@@ -122,6 +172,47 @@ def check_docker_installed() -> DependencyStatus:
|
|
|
122
172
|
parts = version_str.split()
|
|
123
173
|
version = parts[2].rstrip(",") if len(parts) >= 3 else None
|
|
124
174
|
|
|
175
|
+
# Also check for Docker Compose (either v2 plugin or v1 standalone)
|
|
176
|
+
compose_available = False
|
|
177
|
+
try:
|
|
178
|
+
# Try docker compose v2 (plugin)
|
|
179
|
+
subprocess.run(
|
|
180
|
+
["docker", "compose", "version"],
|
|
181
|
+
capture_output=True,
|
|
182
|
+
check=True,
|
|
183
|
+
timeout=2,
|
|
184
|
+
)
|
|
185
|
+
compose_available = True
|
|
186
|
+
except (
|
|
187
|
+
subprocess.SubprocessError,
|
|
188
|
+
FileNotFoundError,
|
|
189
|
+
subprocess.TimeoutExpired,
|
|
190
|
+
):
|
|
191
|
+
# Try docker-compose v1 (standalone)
|
|
192
|
+
try:
|
|
193
|
+
subprocess.run(
|
|
194
|
+
["docker-compose", "--version"],
|
|
195
|
+
capture_output=True,
|
|
196
|
+
check=True,
|
|
197
|
+
timeout=2,
|
|
198
|
+
)
|
|
199
|
+
compose_available = True
|
|
200
|
+
except (
|
|
201
|
+
subprocess.SubprocessError,
|
|
202
|
+
FileNotFoundError,
|
|
203
|
+
subprocess.TimeoutExpired,
|
|
204
|
+
):
|
|
205
|
+
pass
|
|
206
|
+
|
|
207
|
+
if not compose_available:
|
|
208
|
+
return DependencyStatus(
|
|
209
|
+
name="Docker",
|
|
210
|
+
installed=False,
|
|
211
|
+
version=version,
|
|
212
|
+
required=False,
|
|
213
|
+
purpose="Containerized development (quickscale up/down) - Docker Compose not found",
|
|
214
|
+
)
|
|
215
|
+
|
|
125
216
|
return DependencyStatus(
|
|
126
217
|
name="Docker",
|
|
127
218
|
installed=True,
|
|
@@ -147,6 +238,14 @@ def check_docker_installed() -> DependencyStatus:
|
|
|
147
238
|
|
|
148
239
|
def check_postgresql_installed() -> DependencyStatus:
|
|
149
240
|
"""Check if PostgreSQL client is installed."""
|
|
241
|
+
|
|
242
|
+
if _should_skip_dependency_checks():
|
|
243
|
+
return _skipped_dependency(
|
|
244
|
+
name="PostgreSQL",
|
|
245
|
+
required=False,
|
|
246
|
+
purpose="Database server (required for production, Docker provides for dev)",
|
|
247
|
+
version_env="QUICKSCALE_MOCK_POSTGRES_VERSION",
|
|
248
|
+
)
|
|
150
249
|
psql_path = shutil.which("psql")
|
|
151
250
|
|
|
152
251
|
if psql_path:
|
|
@@ -28,16 +28,20 @@ def find_docker_compose() -> Path | None:
|
|
|
28
28
|
|
|
29
29
|
|
|
30
30
|
def get_docker_compose_command() -> list[str]:
|
|
31
|
-
"""Get the appropriate docker compose command.
|
|
32
|
-
|
|
31
|
+
"""Get the appropriate docker compose command.
|
|
32
|
+
|
|
33
|
+
Tries 'docker compose' (v2 plugin) first, falls back to 'docker-compose' (v1 standalone).
|
|
34
|
+
Both are fully supported.
|
|
35
|
+
"""
|
|
36
|
+
# Try docker compose first (v2 plugin)
|
|
33
37
|
try:
|
|
34
38
|
subprocess.run(
|
|
35
|
-
["docker
|
|
39
|
+
["docker", "compose", "version"], capture_output=True, check=True, timeout=2
|
|
36
40
|
)
|
|
37
|
-
return ["docker-compose"]
|
|
38
|
-
except (subprocess.SubprocessError, FileNotFoundError, subprocess.TimeoutExpired):
|
|
39
|
-
# Fall back to docker compose (newer Docker versions)
|
|
40
41
|
return ["docker", "compose"]
|
|
42
|
+
except (subprocess.SubprocessError, FileNotFoundError, subprocess.TimeoutExpired):
|
|
43
|
+
# Fall back to docker-compose (v1 standalone)
|
|
44
|
+
return ["docker-compose"]
|
|
41
45
|
|
|
42
46
|
|
|
43
47
|
def get_container_status(container_name: str) -> str | None:
|
|
File without changes
|
|
File without changes
|
{quickscale_cli-0.65.0 → quickscale_cli-0.67.0}/src/quickscale_cli/commands/deployment_commands.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|