quickscale-cli 0.75.0__tar.gz → 0.76.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.76.0/PKG-INFO +60 -0
- quickscale_cli-0.76.0/README.md +37 -0
- {quickscale_cli-0.75.0 → quickscale_cli-0.76.0}/pyproject.toml +18 -19
- {quickscale_cli-0.75.0 → quickscale_cli-0.76.0}/src/quickscale_cli/_version.py +1 -1
- {quickscale_cli-0.75.0 → quickscale_cli-0.76.0}/src/quickscale_cli/commands/apply_command.py +71 -22
- {quickscale_cli-0.75.0 → quickscale_cli-0.76.0}/src/quickscale_cli/commands/deployment_commands.py +6 -1
- {quickscale_cli-0.75.0 → quickscale_cli-0.76.0}/src/quickscale_cli/commands/development_commands.py +122 -2
- {quickscale_cli-0.75.0 → quickscale_cli-0.76.0}/src/quickscale_cli/commands/module_commands.py +70 -3
- {quickscale_cli-0.75.0 → quickscale_cli-0.76.0}/src/quickscale_cli/commands/module_config.py +411 -46
- {quickscale_cli-0.75.0 → quickscale_cli-0.76.0}/src/quickscale_cli/commands/module_wiring_specs.py +95 -3
- {quickscale_cli-0.75.0 → quickscale_cli-0.76.0}/src/quickscale_cli/commands/plan_command.py +214 -63
- {quickscale_cli-0.75.0 → quickscale_cli-0.76.0}/src/quickscale_cli/commands/status_command.py +1 -1
- {quickscale_cli-0.75.0 → quickscale_cli-0.76.0}/src/quickscale_cli/module_catalog.py +5 -0
- {quickscale_cli-0.75.0 → quickscale_cli-0.76.0}/src/quickscale_cli/schema/config_schema.py +17 -3
- {quickscale_cli-0.75.0 → quickscale_cli-0.76.0}/src/quickscale_cli/utils/dependency_utils.py +3 -3
- {quickscale_cli-0.75.0 → quickscale_cli-0.76.0}/src/quickscale_cli/utils/docker_utils.py +4 -4
- {quickscale_cli-0.75.0 → quickscale_cli-0.76.0}/src/quickscale_cli/utils/railway_utils.py +12 -12
- quickscale_cli-0.75.0/PKG-INFO +0 -380
- quickscale_cli-0.75.0/README.md +0 -354
- {quickscale_cli-0.75.0 → quickscale_cli-0.76.0}/src/quickscale_cli/__init__.py +0 -0
- {quickscale_cli-0.75.0 → quickscale_cli-0.76.0}/src/quickscale_cli/commands/__init__.py +0 -0
- {quickscale_cli-0.75.0 → quickscale_cli-0.76.0}/src/quickscale_cli/commands/remove_command.py +0 -0
- {quickscale_cli-0.75.0 → quickscale_cli-0.76.0}/src/quickscale_cli/main.py +0 -0
- {quickscale_cli-0.75.0 → quickscale_cli-0.76.0}/src/quickscale_cli/schema/__init__.py +0 -0
- {quickscale_cli-0.75.0 → quickscale_cli-0.76.0}/src/quickscale_cli/schema/delta.py +0 -0
- {quickscale_cli-0.75.0 → quickscale_cli-0.76.0}/src/quickscale_cli/schema/state_schema.py +0 -0
- {quickscale_cli-0.75.0 → quickscale_cli-0.76.0}/src/quickscale_cli/utils/__init__.py +0 -0
- {quickscale_cli-0.75.0 → quickscale_cli-0.76.0}/src/quickscale_cli/utils/module_wiring_manager.py +0 -0
- {quickscale_cli-0.75.0 → quickscale_cli-0.76.0}/src/quickscale_cli/utils/project_identity.py +0 -0
- {quickscale_cli-0.75.0 → quickscale_cli-0.76.0}/src/quickscale_cli/utils/project_manager.py +0 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: quickscale-cli
|
|
3
|
+
Version: 0.76.0
|
|
4
|
+
Summary: Command-line interface for QuickScale Django project generator
|
|
5
|
+
License-Expression: Apache-2.0
|
|
6
|
+
Keywords: django,cli,project-generator,saas
|
|
7
|
+
Author: Experto AI
|
|
8
|
+
Author-email: victor@experto.ai
|
|
9
|
+
Requires-Python: >=3.14,<4.0
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
14
|
+
Classifier: Framework :: Django
|
|
15
|
+
Classifier: Framework :: Django :: 6.0
|
|
16
|
+
Requires-Dist: click (>=8.3.1,<9.0.0)
|
|
17
|
+
Requires-Dist: quickscale-core (>=0.76.0,<0.77.0)
|
|
18
|
+
Project-URL: Documentation, https://github.com/Experto-AI/quickscale/tree/main/docs
|
|
19
|
+
Project-URL: Homepage, https://github.com/Experto-AI/quickscale
|
|
20
|
+
Project-URL: Repository, https://github.com/Experto-AI/quickscale
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# quickscale-cli
|
|
24
|
+
|
|
25
|
+
Command-line interface package for QuickScale.
|
|
26
|
+
|
|
27
|
+
This package exposes the `quickscale` command used to plan, apply, inspect, and operate generated projects.
|
|
28
|
+
|
|
29
|
+
## Command groups
|
|
30
|
+
|
|
31
|
+
- Project lifecycle: `plan`, `apply`, `status`, `remove`
|
|
32
|
+
- Local development: `up`, `down`, `ps`, `logs`, `shell`, `manage`
|
|
33
|
+
- Deployment: `deploy`
|
|
34
|
+
- Module workflows: `update`, `push`
|
|
35
|
+
|
|
36
|
+
## Relationship to other packages
|
|
37
|
+
|
|
38
|
+
- `quickscale-cli` is the command surface
|
|
39
|
+
- `quickscale-core` contains the underlying scaffolding and template logic
|
|
40
|
+
- `quickscale` installs both together for convenience
|
|
41
|
+
|
|
42
|
+
## Install
|
|
43
|
+
|
|
44
|
+
Usually installed via the meta-package:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pip install quickscale
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Standalone package installs can use this package's published metadata and dependency constraints.
|
|
51
|
+
|
|
52
|
+
## Documentation
|
|
53
|
+
|
|
54
|
+
- Repository overview: [QuickScale README](https://github.com/Experto-AI/quickscale/blob/main/README.md)
|
|
55
|
+
- Start guide: [START_HERE.md](https://github.com/Experto-AI/quickscale/blob/main/START_HERE.md)
|
|
56
|
+
- Technical decisions: [docs/technical/decisions.md](https://github.com/Experto-AI/quickscale/blob/main/docs/technical/decisions.md)
|
|
57
|
+
- CLI source entrypoint: [src/quickscale_cli/main.py](https://github.com/Experto-AI/quickscale/blob/main/quickscale_cli/src/quickscale_cli/main.py)
|
|
58
|
+
|
|
59
|
+
This README is package-local context only. Root documentation remains authoritative for repo-wide policy and scope.
|
|
60
|
+
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# quickscale-cli
|
|
2
|
+
|
|
3
|
+
Command-line interface package for QuickScale.
|
|
4
|
+
|
|
5
|
+
This package exposes the `quickscale` command used to plan, apply, inspect, and operate generated projects.
|
|
6
|
+
|
|
7
|
+
## Command groups
|
|
8
|
+
|
|
9
|
+
- Project lifecycle: `plan`, `apply`, `status`, `remove`
|
|
10
|
+
- Local development: `up`, `down`, `ps`, `logs`, `shell`, `manage`
|
|
11
|
+
- Deployment: `deploy`
|
|
12
|
+
- Module workflows: `update`, `push`
|
|
13
|
+
|
|
14
|
+
## Relationship to other packages
|
|
15
|
+
|
|
16
|
+
- `quickscale-cli` is the command surface
|
|
17
|
+
- `quickscale-core` contains the underlying scaffolding and template logic
|
|
18
|
+
- `quickscale` installs both together for convenience
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
Usually installed via the meta-package:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install quickscale
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Standalone package installs can use this package's published metadata and dependency constraints.
|
|
29
|
+
|
|
30
|
+
## Documentation
|
|
31
|
+
|
|
32
|
+
- Repository overview: [QuickScale README](https://github.com/Experto-AI/quickscale/blob/main/README.md)
|
|
33
|
+
- Start guide: [START_HERE.md](https://github.com/Experto-AI/quickscale/blob/main/START_HERE.md)
|
|
34
|
+
- Technical decisions: [docs/technical/decisions.md](https://github.com/Experto-AI/quickscale/blob/main/docs/technical/decisions.md)
|
|
35
|
+
- CLI source entrypoint: [src/quickscale_cli/main.py](https://github.com/Experto-AI/quickscale/blob/main/quickscale_cli/src/quickscale_cli/main.py)
|
|
36
|
+
|
|
37
|
+
This README is package-local context only. Root documentation remains authoritative for repo-wide policy and scope.
|
|
@@ -1,39 +1,42 @@
|
|
|
1
|
-
[
|
|
1
|
+
[project]
|
|
2
2
|
name = "quickscale-cli"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.76.0"
|
|
4
4
|
description = "Command-line interface for QuickScale Django project generator"
|
|
5
|
-
|
|
5
|
+
requires-python = ">=3.14,<4.0"
|
|
6
|
+
readme = "README.md"
|
|
6
7
|
license = "Apache-2.0"
|
|
8
|
+
authors = [{name = "Experto AI", email = "victor@experto.ai"}]
|
|
7
9
|
keywords = ["django", "cli", "project-generator", "saas"]
|
|
8
|
-
homepage = "https://github.com/Experto-AI/quickscale"
|
|
9
|
-
repository = "https://github.com/Experto-AI/quickscale"
|
|
10
|
-
documentation = "https://github.com/Experto-AI/quickscale/tree/main/docs"
|
|
11
|
-
readme = "README.md"
|
|
12
10
|
classifiers = [
|
|
13
11
|
"Development Status :: 3 - Alpha",
|
|
14
12
|
"Intended Audience :: Developers",
|
|
15
|
-
"License :: OSI Approved :: Apache Software License",
|
|
16
13
|
"Programming Language :: Python :: 3",
|
|
17
|
-
"Programming Language :: Python :: 3.12",
|
|
18
|
-
"Programming Language :: Python :: 3.13",
|
|
19
14
|
"Programming Language :: Python :: 3.14",
|
|
20
15
|
"Framework :: Django",
|
|
21
16
|
"Framework :: Django :: 6.0",
|
|
22
17
|
]
|
|
18
|
+
dynamic = ["dependencies"]
|
|
19
|
+
|
|
20
|
+
[project.urls]
|
|
21
|
+
Homepage = "https://github.com/Experto-AI/quickscale"
|
|
22
|
+
Repository = "https://github.com/Experto-AI/quickscale"
|
|
23
|
+
Documentation = "https://github.com/Experto-AI/quickscale/tree/main/docs"
|
|
24
|
+
|
|
25
|
+
[project.scripts]
|
|
26
|
+
quickscale = "quickscale_cli.main:cli"
|
|
27
|
+
|
|
28
|
+
[tool.poetry]
|
|
23
29
|
packages = [{include = "quickscale_cli", from = "src"}]
|
|
24
30
|
|
|
25
31
|
[tool.poetry.dependencies]
|
|
26
|
-
python = "^3.
|
|
32
|
+
python = "^3.14"
|
|
27
33
|
click = "^8.3.1"
|
|
28
|
-
quickscale-core = "^0.
|
|
34
|
+
quickscale-core = "^0.76.0"
|
|
29
35
|
|
|
30
36
|
[tool.poetry.group.dev.dependencies]
|
|
31
37
|
# Dev dependencies centralized in root pyproject.toml
|
|
32
38
|
# Use: poetry install from repository root
|
|
33
39
|
|
|
34
|
-
[tool.poetry.scripts]
|
|
35
|
-
quickscale = "quickscale_cli.main:cli"
|
|
36
|
-
|
|
37
40
|
[build-system]
|
|
38
41
|
requires = ["poetry-core"]
|
|
39
42
|
build-backend = "poetry.core.masonry.api"
|
|
@@ -46,10 +49,6 @@ python_functions = ["test_*"]
|
|
|
46
49
|
addopts = [
|
|
47
50
|
"--strict-markers",
|
|
48
51
|
"--strict-config",
|
|
49
|
-
"--cov=quickscale_cli",
|
|
50
|
-
"--cov-report=term-missing",
|
|
51
|
-
"--cov-report=html",
|
|
52
|
-
"--cov-fail-under=90",
|
|
53
52
|
"-m", "not e2e", # Skip E2E tests by default (run with: pytest -m e2e)
|
|
54
53
|
]
|
|
55
54
|
markers = [
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
# Auto-generated by scripts/version_tool.sh
|
|
2
|
-
__version__ = "0.
|
|
2
|
+
__version__ = "0.76.0"
|
{quickscale_cli-0.75.0 → quickscale_cli-0.76.0}/src/quickscale_cli/commands/apply_command.py
RENAMED
|
@@ -105,24 +105,6 @@ def _generate_project(config: QuickScaleConfig, output_path: Path) -> bool:
|
|
|
105
105
|
f"(package: {config.project.package})..."
|
|
106
106
|
)
|
|
107
107
|
|
|
108
|
-
# Validate theme availability
|
|
109
|
-
if config.project.theme == "showcase_htmx":
|
|
110
|
-
click.secho(
|
|
111
|
-
f"❌ Error: Theme '{config.project.theme}' is not yet implemented",
|
|
112
|
-
fg="red",
|
|
113
|
-
err=True,
|
|
114
|
-
)
|
|
115
|
-
click.echo(
|
|
116
|
-
"\n💡 The 'showcase_htmx' theme is planned for a future release:",
|
|
117
|
-
err=True,
|
|
118
|
-
)
|
|
119
|
-
click.echo(" - showcase_htmx: Coming in v0.78.0", err=True)
|
|
120
|
-
click.echo(
|
|
121
|
-
" - Use 'showcase_react' (default) or 'showcase_html' instead",
|
|
122
|
-
err=True,
|
|
123
|
-
)
|
|
124
|
-
return False
|
|
125
|
-
|
|
126
108
|
generator = ProjectGenerator(theme=config.project.theme)
|
|
127
109
|
# mypy can resolve an older installed quickscale-core signature here.
|
|
128
110
|
generate_project = cast(Any, generator.generate)
|
|
@@ -281,6 +263,7 @@ def _start_docker(
|
|
|
281
263
|
cmd,
|
|
282
264
|
project_path,
|
|
283
265
|
"Starting Docker services",
|
|
266
|
+
capture=False,
|
|
284
267
|
)
|
|
285
268
|
return success
|
|
286
269
|
|
|
@@ -575,6 +558,45 @@ def _init_git_with_config(output_path: Path) -> None:
|
|
|
575
558
|
click.secho("⚠️ Initial commit failed, continuing...", fg="yellow")
|
|
576
559
|
|
|
577
560
|
|
|
561
|
+
def _commit_pending_config_changes(output_path: Path) -> None:
|
|
562
|
+
"""Commit pending QuickScale config changes before module embedding
|
|
563
|
+
|
|
564
|
+
Stages and commits quickscale.yml and .quickscale/ directory changes so
|
|
565
|
+
git subtree operations have a clean working directory. Called before
|
|
566
|
+
embedding modules in existing projects.
|
|
567
|
+
|
|
568
|
+
Args:
|
|
569
|
+
output_path: Path to the project directory
|
|
570
|
+
|
|
571
|
+
"""
|
|
572
|
+
if is_working_directory_clean(output_path):
|
|
573
|
+
return
|
|
574
|
+
|
|
575
|
+
# Stage only QuickScale-managed config files
|
|
576
|
+
subprocess.run(
|
|
577
|
+
["git", "add", "quickscale.yml", ".quickscale/"],
|
|
578
|
+
cwd=output_path,
|
|
579
|
+
capture_output=True,
|
|
580
|
+
check=False,
|
|
581
|
+
)
|
|
582
|
+
|
|
583
|
+
# Check if anything was staged
|
|
584
|
+
result = subprocess.run(
|
|
585
|
+
["git", "diff", "--cached", "--name-only"],
|
|
586
|
+
cwd=output_path,
|
|
587
|
+
capture_output=True,
|
|
588
|
+
text=True,
|
|
589
|
+
check=False,
|
|
590
|
+
)
|
|
591
|
+
|
|
592
|
+
if result.stdout.strip():
|
|
593
|
+
_run_command(
|
|
594
|
+
["git", "commit", "-m", "Update QuickScale configuration"],
|
|
595
|
+
output_path,
|
|
596
|
+
"Committing pending QuickScale configuration changes",
|
|
597
|
+
)
|
|
598
|
+
|
|
599
|
+
|
|
578
600
|
def _embed_modules_step(
|
|
579
601
|
output_path: Path,
|
|
580
602
|
modules_to_embed: list[str],
|
|
@@ -824,6 +846,13 @@ def _execute_apply_steps(
|
|
|
824
846
|
if ctx.existing_state
|
|
825
847
|
else list(ctx.qs_config.modules.keys())
|
|
826
848
|
)
|
|
849
|
+
|
|
850
|
+
# For existing projects, commit any pending QuickScale config changes
|
|
851
|
+
# (e.g. quickscale.yml updated by `quickscale plan`) before embedding
|
|
852
|
+
# modules, so git subtree has a clean working directory to operate on.
|
|
853
|
+
if ctx.existing_state is not None and modules_to_embed:
|
|
854
|
+
_commit_pending_config_changes(ctx.output_path)
|
|
855
|
+
|
|
827
856
|
embed_result = _embed_modules_step(
|
|
828
857
|
ctx.output_path, modules_to_embed, no_modules, ctx.existing_state
|
|
829
858
|
)
|
|
@@ -882,16 +911,36 @@ def _execute_apply_steps(
|
|
|
882
911
|
ctx.output_path, ctx.qs_config.docker.build, verbose_docker
|
|
883
912
|
)
|
|
884
913
|
if not docker_started:
|
|
885
|
-
|
|
914
|
+
_save_project_state(
|
|
915
|
+
ctx.output_path,
|
|
916
|
+
ctx.qs_config,
|
|
917
|
+
ctx.existing_state,
|
|
918
|
+
embedded_modules,
|
|
919
|
+
ctx.delta,
|
|
920
|
+
)
|
|
921
|
+
_print_apply_failure_summary(
|
|
922
|
+
failed_step="docker startup",
|
|
923
|
+
reason="Docker auto-start failed. Run 'quickscale logs' to inspect the failing service.",
|
|
924
|
+
)
|
|
925
|
+
raise click.Abort()
|
|
886
926
|
|
|
887
927
|
# For Docker auto-start projects, run migrations in the backend container
|
|
888
928
|
# so database connectivity uses the internal Docker network.
|
|
889
929
|
if should_auto_start_docker:
|
|
890
930
|
if docker_started:
|
|
891
931
|
if not _run_migrations_in_docker(ctx.output_path):
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
932
|
+
_save_project_state(
|
|
933
|
+
ctx.output_path,
|
|
934
|
+
ctx.qs_config,
|
|
935
|
+
ctx.existing_state,
|
|
936
|
+
embedded_modules,
|
|
937
|
+
ctx.delta,
|
|
938
|
+
)
|
|
939
|
+
_print_apply_failure_summary(
|
|
940
|
+
failed_step="database migrations",
|
|
941
|
+
reason="Migrations failed inside Docker backend container. Run 'quickscale logs backend' for details.",
|
|
942
|
+
)
|
|
943
|
+
raise click.Abort()
|
|
895
944
|
|
|
896
945
|
# Save state
|
|
897
946
|
_save_project_state(
|
{quickscale_cli-0.75.0 → quickscale_cli-0.76.0}/src/quickscale_cli/commands/deployment_commands.py
RENAMED
|
@@ -457,7 +457,12 @@ def _display_summary(
|
|
|
457
457
|
else:
|
|
458
458
|
click.echo(" 3. Get URL: railway status")
|
|
459
459
|
click.echo(
|
|
460
|
-
|
|
460
|
+
" 4. Create superuser automatically: set DJANGO_SUPERUSER_USERNAME, "
|
|
461
|
+
"DJANGO_SUPERUSER_EMAIL and DJANGO_SUPERUSER_PASSWORD variables in Railway "
|
|
462
|
+
"dashboard — start.sh will create it on next deploy"
|
|
463
|
+
)
|
|
464
|
+
click.echo(
|
|
465
|
+
f" Or manually: railway run --service {app_service} "
|
|
461
466
|
f"python manage.py createsuperuser"
|
|
462
467
|
)
|
|
463
468
|
click.echo(" 5. Configure custom domain (optional): railway domain")
|
{quickscale_cli-0.75.0 → quickscale_cli-0.76.0}/src/quickscale_cli/commands/development_commands.py
RENAMED
|
@@ -8,6 +8,7 @@ from pathlib import Path
|
|
|
8
8
|
|
|
9
9
|
import click
|
|
10
10
|
|
|
11
|
+
from quickscale_cli.schema.config_schema import QuickScaleConfig
|
|
11
12
|
from quickscale_cli.utils.docker_utils import (
|
|
12
13
|
get_docker_compose_command,
|
|
13
14
|
get_port_from_env,
|
|
@@ -140,6 +141,100 @@ def _handle_up_error(error: subprocess.CalledProcessError) -> None:
|
|
|
140
141
|
)
|
|
141
142
|
|
|
142
143
|
|
|
144
|
+
def _run_migrations_after_up() -> None:
|
|
145
|
+
"""Run Django migrations in backend container after services start"""
|
|
146
|
+
click.echo("⏳ Applying database migrations...")
|
|
147
|
+
container_name = get_backend_container_name()
|
|
148
|
+
_run_docker_exec_command(
|
|
149
|
+
container_name,
|
|
150
|
+
["python", "manage.py", "migrate"],
|
|
151
|
+
capture=True,
|
|
152
|
+
)
|
|
153
|
+
click.secho("✅ Database migrations applied", fg="green")
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _superuser_exists_in_backend(container_name: str) -> bool | None:
|
|
157
|
+
"""Check whether a Django superuser already exists
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
True when at least one superuser exists
|
|
161
|
+
False when no superuser exists
|
|
162
|
+
None when status cannot be verified reliably
|
|
163
|
+
"""
|
|
164
|
+
check_cmd = [
|
|
165
|
+
"docker",
|
|
166
|
+
"exec",
|
|
167
|
+
container_name,
|
|
168
|
+
"python",
|
|
169
|
+
"manage.py",
|
|
170
|
+
"shell",
|
|
171
|
+
"-c",
|
|
172
|
+
(
|
|
173
|
+
"from django.contrib.auth import get_user_model; import sys; "
|
|
174
|
+
"sys.exit(0 if get_user_model().objects.filter(is_superuser=True).exists() else 1)"
|
|
175
|
+
),
|
|
176
|
+
]
|
|
177
|
+
result = subprocess.run(check_cmd, capture_output=True, text=True)
|
|
178
|
+
if result.returncode == 0:
|
|
179
|
+
return True
|
|
180
|
+
|
|
181
|
+
stdout_output = (result.stdout or "").strip()
|
|
182
|
+
stderr_output = (result.stderr or "").strip()
|
|
183
|
+
|
|
184
|
+
if result.returncode == 1 and not stdout_output and not stderr_output:
|
|
185
|
+
return False
|
|
186
|
+
|
|
187
|
+
return None
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _handle_superuser_after_up(config: QuickScaleConfig | None) -> None:
|
|
191
|
+
"""Create superuser if configured and one does not already exist"""
|
|
192
|
+
if config is None or config.docker is None:
|
|
193
|
+
return
|
|
194
|
+
|
|
195
|
+
if not getattr(config.docker, "create_superuser", False):
|
|
196
|
+
return
|
|
197
|
+
|
|
198
|
+
container_name = get_backend_container_name()
|
|
199
|
+
superuser_exists = _superuser_exists_in_backend(container_name)
|
|
200
|
+
interactive_session = is_interactive()
|
|
201
|
+
|
|
202
|
+
if superuser_exists is True:
|
|
203
|
+
click.echo("ℹ️ Superuser already exists. Skipping createsuperuser step.")
|
|
204
|
+
return
|
|
205
|
+
|
|
206
|
+
if superuser_exists is None:
|
|
207
|
+
click.secho("⚠️ Could not verify superuser status.", fg="yellow")
|
|
208
|
+
if not interactive_session:
|
|
209
|
+
click.echo(" Run: quickscale manage createsuperuser")
|
|
210
|
+
return
|
|
211
|
+
click.echo(" Proceeding with interactive superuser creation.")
|
|
212
|
+
elif not interactive_session:
|
|
213
|
+
click.secho(
|
|
214
|
+
"⚠️ Superuser creation is enabled but requires interactive input.",
|
|
215
|
+
fg="yellow",
|
|
216
|
+
)
|
|
217
|
+
click.echo(" Run: quickscale manage createsuperuser")
|
|
218
|
+
return
|
|
219
|
+
|
|
220
|
+
click.echo("👤 Creating Django superuser...")
|
|
221
|
+
_run_docker_exec_command(
|
|
222
|
+
container_name,
|
|
223
|
+
["python", "manage.py", "createsuperuser"],
|
|
224
|
+
capture=False,
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _command_contains(error: subprocess.CalledProcessError, *tokens: str) -> bool:
|
|
229
|
+
"""Check whether error command contains all tokens"""
|
|
230
|
+
cmd = error.cmd
|
|
231
|
+
if isinstance(cmd, list):
|
|
232
|
+
cmd_text = " ".join(str(part) for part in cmd)
|
|
233
|
+
else:
|
|
234
|
+
cmd_text = str(cmd)
|
|
235
|
+
return all(token in cmd_text for token in tokens)
|
|
236
|
+
|
|
237
|
+
|
|
143
238
|
@click.command()
|
|
144
239
|
@click.option("--build", is_flag=True, help="Rebuild containers before starting")
|
|
145
240
|
@click.option("--no-cache", is_flag=True, help="Build without using cache")
|
|
@@ -180,8 +275,33 @@ def up(build: bool, no_cache: bool) -> None:
|
|
|
180
275
|
if should_build:
|
|
181
276
|
_update_last_build_timestamp()
|
|
182
277
|
|
|
278
|
+
_run_migrations_after_up()
|
|
279
|
+
_handle_superuser_after_up(config)
|
|
280
|
+
|
|
183
281
|
except subprocess.CalledProcessError as e:
|
|
184
|
-
|
|
282
|
+
if _command_contains(e, "manage.py", "migrate"):
|
|
283
|
+
click.secho(
|
|
284
|
+
"❌ Error: Services started but database migration failed",
|
|
285
|
+
fg="red",
|
|
286
|
+
err=True,
|
|
287
|
+
)
|
|
288
|
+
if e.stderr:
|
|
289
|
+
click.echo(f"\nError output:\n{e.stderr}", err=True)
|
|
290
|
+
click.echo(
|
|
291
|
+
"💡 Tip: Run 'quickscale manage migrate' and inspect logs with 'quickscale logs backend'",
|
|
292
|
+
err=True,
|
|
293
|
+
)
|
|
294
|
+
elif _command_contains(e, "manage.py", "createsuperuser"):
|
|
295
|
+
click.secho(
|
|
296
|
+
"❌ Error: Services started but superuser creation failed",
|
|
297
|
+
fg="red",
|
|
298
|
+
err=True,
|
|
299
|
+
)
|
|
300
|
+
click.echo(
|
|
301
|
+
"💡 Tip: Run 'quickscale manage createsuperuser' manually", err=True
|
|
302
|
+
)
|
|
303
|
+
else:
|
|
304
|
+
_handle_up_error(e)
|
|
185
305
|
sys.exit(1)
|
|
186
306
|
except KeyboardInterrupt:
|
|
187
307
|
click.echo("\n⚠️ Interrupted by user")
|
|
@@ -428,7 +548,7 @@ def _dependencies_changed_since_last_build() -> bool:
|
|
|
428
548
|
)
|
|
429
549
|
return changed
|
|
430
550
|
|
|
431
|
-
except
|
|
551
|
+
except json.JSONDecodeError, KeyError, OSError:
|
|
432
552
|
# If we can't read state, don't warn (fail safe)
|
|
433
553
|
return False
|
|
434
554
|
|
{quickscale_cli-0.75.0 → quickscale_cli-0.76.0}/src/quickscale_cli/commands/module_commands.py
RENAMED
|
@@ -195,7 +195,7 @@ def _perform_module_embed(
|
|
|
195
195
|
applier(project_path, config)
|
|
196
196
|
|
|
197
197
|
# Install dependencies for modules that need it
|
|
198
|
-
if module in ["auth", "blog", "listings", "crm", "forms"]:
|
|
198
|
+
if module in ["auth", "blog", "listings", "crm", "forms", "storage"]:
|
|
199
199
|
if not _install_module_dependencies(project_path, module):
|
|
200
200
|
return False
|
|
201
201
|
|
|
@@ -440,7 +440,7 @@ def _validate_update_environment() -> None:
|
|
|
440
440
|
|
|
441
441
|
def _update_single_module(
|
|
442
442
|
name: str, info: Any, default_remote: str, no_preview: bool
|
|
443
|
-
) ->
|
|
443
|
+
) -> bool:
|
|
444
444
|
"""Update a single module via git subtree pull."""
|
|
445
445
|
click.echo(f"\n📥 Updating {name} module...")
|
|
446
446
|
|
|
@@ -455,15 +455,62 @@ def _update_single_module(
|
|
|
455
455
|
# Update version in config
|
|
456
456
|
update_module_version(name, "v0.62.0") # Placeholder version
|
|
457
457
|
|
|
458
|
+
_commit_module_update(name, info.prefix)
|
|
459
|
+
|
|
458
460
|
click.secho(f"✅ Updated {name} successfully", fg="green")
|
|
459
461
|
|
|
460
462
|
if output and not no_preview:
|
|
461
463
|
click.echo("\n📋 Changes summary:")
|
|
462
464
|
click.echo(output[:500]) # Show first 500 chars
|
|
463
465
|
|
|
466
|
+
return True
|
|
467
|
+
|
|
464
468
|
except GitError as e:
|
|
465
469
|
click.secho(f"❌ Failed to update {name}: {e}", fg="red", err=True)
|
|
466
470
|
click.echo(f"💡 Tip: Check for conflicts in modules/{name}/", err=True)
|
|
471
|
+
return False
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def _commit_module_update(module_name: str, module_prefix: str) -> None:
|
|
475
|
+
"""Create a commit for a successfully updated module."""
|
|
476
|
+
tracked_paths = [module_prefix]
|
|
477
|
+
config_path = Path(".quickscale") / "config.yml"
|
|
478
|
+
if config_path.exists():
|
|
479
|
+
tracked_paths.append(str(config_path))
|
|
480
|
+
|
|
481
|
+
try:
|
|
482
|
+
subprocess.run(
|
|
483
|
+
["git", "add", *tracked_paths],
|
|
484
|
+
check=True,
|
|
485
|
+
capture_output=True,
|
|
486
|
+
text=True,
|
|
487
|
+
)
|
|
488
|
+
except subprocess.CalledProcessError as e:
|
|
489
|
+
raise GitError(f"Failed to stage {module_name} update commit: {e.stderr}")
|
|
490
|
+
|
|
491
|
+
cached_diff = subprocess.run(
|
|
492
|
+
["git", "diff", "--cached", "--quiet"],
|
|
493
|
+
capture_output=True,
|
|
494
|
+
text=True,
|
|
495
|
+
)
|
|
496
|
+
|
|
497
|
+
if cached_diff.returncode == 0:
|
|
498
|
+
click.echo(f"ℹ️ No staged changes detected for {module_name}; skipping commit")
|
|
499
|
+
return
|
|
500
|
+
|
|
501
|
+
if cached_diff.returncode != 1:
|
|
502
|
+
raise GitError("Failed to inspect staged changes before module update commit")
|
|
503
|
+
|
|
504
|
+
commit_message = f"chore(modules): update {module_name} module"
|
|
505
|
+
try:
|
|
506
|
+
subprocess.run(
|
|
507
|
+
["git", "commit", "-m", commit_message],
|
|
508
|
+
check=True,
|
|
509
|
+
capture_output=True,
|
|
510
|
+
text=True,
|
|
511
|
+
)
|
|
512
|
+
except subprocess.CalledProcessError as e:
|
|
513
|
+
raise GitError(f"Failed to commit {module_name} update: {e.stderr}")
|
|
467
514
|
|
|
468
515
|
|
|
469
516
|
@click.command()
|
|
@@ -487,6 +534,7 @@ def update(no_preview: bool) -> None:
|
|
|
487
534
|
- Updates ONLY modules you've explicitly installed
|
|
488
535
|
- Shows a diff preview before updating (unless --no-preview)
|
|
489
536
|
- Updates the installed version in config after successful update
|
|
537
|
+
- Commits each successful module update before continuing
|
|
490
538
|
"""
|
|
491
539
|
try:
|
|
492
540
|
_validate_update_environment()
|
|
@@ -514,15 +562,34 @@ def update(no_preview: bool) -> None:
|
|
|
514
562
|
click.echo("❌ Update cancelled")
|
|
515
563
|
return
|
|
516
564
|
|
|
565
|
+
failed_modules: list[str] = []
|
|
566
|
+
|
|
517
567
|
# Update each module
|
|
518
568
|
for name, info in config.modules.items():
|
|
519
|
-
_update_single_module(name, info, config.default_remote, no_preview)
|
|
569
|
+
if not _update_single_module(name, info, config.default_remote, no_preview):
|
|
570
|
+
failed_modules.append(name)
|
|
571
|
+
break
|
|
572
|
+
|
|
573
|
+
if failed_modules:
|
|
574
|
+
click.secho(
|
|
575
|
+
"\n❌ Module update stopped due to failure",
|
|
576
|
+
fg="red",
|
|
577
|
+
bold=True,
|
|
578
|
+
err=True,
|
|
579
|
+
)
|
|
580
|
+
click.echo(
|
|
581
|
+
f"Failed module(s): {', '.join(failed_modules)}",
|
|
582
|
+
err=True,
|
|
583
|
+
)
|
|
584
|
+
raise click.Abort()
|
|
520
585
|
|
|
521
586
|
click.secho("\n🎉 Module update complete!", fg="green", bold=True)
|
|
522
587
|
|
|
523
588
|
except GitError as e:
|
|
524
589
|
click.secho(f"❌ Git error: {e}", fg="red", err=True)
|
|
525
590
|
raise click.Abort()
|
|
591
|
+
except click.Abort:
|
|
592
|
+
raise
|
|
526
593
|
except Exception as e:
|
|
527
594
|
click.secho(f"❌ Unexpected error: {e}", fg="red", err=True)
|
|
528
595
|
raise click.Abort()
|