quickscale-cli 0.74.2__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.74.2 → quickscale_cli-0.76.0}/pyproject.toml +18 -19
- {quickscale_cli-0.74.2 → quickscale_cli-0.76.0}/src/quickscale_cli/_version.py +1 -1
- {quickscale_cli-0.74.2 → quickscale_cli-0.76.0}/src/quickscale_cli/commands/apply_command.py +270 -60
- {quickscale_cli-0.74.2 → quickscale_cli-0.76.0}/src/quickscale_cli/commands/deployment_commands.py +59 -16
- {quickscale_cli-0.74.2 → quickscale_cli-0.76.0}/src/quickscale_cli/commands/development_commands.py +139 -8
- {quickscale_cli-0.74.2 → quickscale_cli-0.76.0}/src/quickscale_cli/commands/module_commands.py +124 -22
- quickscale_cli-0.76.0/src/quickscale_cli/commands/module_config.py +1334 -0
- quickscale_cli-0.76.0/src/quickscale_cli/commands/module_wiring_specs.py +291 -0
- {quickscale_cli-0.74.2 → quickscale_cli-0.76.0}/src/quickscale_cli/commands/plan_command.py +396 -129
- {quickscale_cli-0.74.2 → quickscale_cli-0.76.0}/src/quickscale_cli/commands/remove_command.py +27 -82
- {quickscale_cli-0.74.2 → quickscale_cli-0.76.0}/src/quickscale_cli/commands/status_command.py +7 -4
- quickscale_cli-0.76.0/src/quickscale_cli/module_catalog.py +82 -0
- {quickscale_cli-0.74.2 → quickscale_cli-0.76.0}/src/quickscale_cli/schema/config_schema.py +112 -27
- {quickscale_cli-0.74.2 → quickscale_cli-0.76.0}/src/quickscale_cli/schema/state_schema.py +32 -4
- {quickscale_cli-0.74.2 → quickscale_cli-0.76.0}/src/quickscale_cli/utils/dependency_utils.py +3 -3
- {quickscale_cli-0.74.2 → quickscale_cli-0.76.0}/src/quickscale_cli/utils/docker_utils.py +4 -4
- quickscale_cli-0.76.0/src/quickscale_cli/utils/module_wiring_manager.py +115 -0
- quickscale_cli-0.76.0/src/quickscale_cli/utils/project_identity.py +99 -0
- {quickscale_cli-0.74.2 → quickscale_cli-0.76.0}/src/quickscale_cli/utils/project_manager.py +5 -5
- {quickscale_cli-0.74.2 → quickscale_cli-0.76.0}/src/quickscale_cli/utils/railway_utils.py +144 -44
- quickscale_cli-0.74.2/PKG-INFO +0 -376
- quickscale_cli-0.74.2/README.md +0 -350
- quickscale_cli-0.74.2/src/quickscale_cli/commands/module_config.py +0 -906
- {quickscale_cli-0.74.2 → quickscale_cli-0.76.0}/src/quickscale_cli/__init__.py +0 -0
- {quickscale_cli-0.74.2 → quickscale_cli-0.76.0}/src/quickscale_cli/commands/__init__.py +0 -0
- {quickscale_cli-0.74.2 → quickscale_cli-0.76.0}/src/quickscale_cli/main.py +0 -0
- {quickscale_cli-0.74.2 → quickscale_cli-0.76.0}/src/quickscale_cli/schema/__init__.py +0 -0
- {quickscale_cli-0.74.2 → quickscale_cli-0.76.0}/src/quickscale_cli/schema/delta.py +0 -0
- {quickscale_cli-0.74.2 → quickscale_cli-0.76.0}/src/quickscale_cli/utils/__init__.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.74.2 → quickscale_cli-0.76.0}/src/quickscale_cli/commands/apply_command.py
RENAMED
|
@@ -7,10 +7,12 @@ import subprocess
|
|
|
7
7
|
from dataclasses import dataclass
|
|
8
8
|
from datetime import datetime
|
|
9
9
|
from pathlib import Path
|
|
10
|
+
from typing import Any, cast
|
|
10
11
|
|
|
11
12
|
import click
|
|
12
13
|
|
|
13
14
|
from quickscale_cli.commands.module_commands import embed_module
|
|
15
|
+
from quickscale_cli.utils.module_wiring_manager import regenerate_managed_wiring
|
|
14
16
|
from quickscale_cli.schema.config_schema import (
|
|
15
17
|
ConfigValidationError,
|
|
16
18
|
QuickScaleConfig,
|
|
@@ -43,6 +45,15 @@ class ApplyContext:
|
|
|
43
45
|
delta: ConfigDelta
|
|
44
46
|
|
|
45
47
|
|
|
48
|
+
@dataclass
|
|
49
|
+
class EmbedModulesResult:
|
|
50
|
+
"""Result for module embedding step."""
|
|
51
|
+
|
|
52
|
+
success: bool
|
|
53
|
+
embedded_modules: list[str]
|
|
54
|
+
failed_module: str | None = None
|
|
55
|
+
|
|
56
|
+
|
|
46
57
|
def _run_command(
|
|
47
58
|
cmd: list[str],
|
|
48
59
|
cwd: Path,
|
|
@@ -89,28 +100,19 @@ def _run_command(
|
|
|
89
100
|
def _generate_project(config: QuickScaleConfig, output_path: Path) -> bool:
|
|
90
101
|
"""Generate project using ProjectGenerator"""
|
|
91
102
|
try:
|
|
92
|
-
click.echo(
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
click.secho(
|
|
97
|
-
f"❌ Error: Theme '{config.project.theme}' is not yet implemented",
|
|
98
|
-
fg="red",
|
|
99
|
-
err=True,
|
|
100
|
-
)
|
|
101
|
-
click.echo(
|
|
102
|
-
"\n💡 The 'showcase_htmx' theme is planned for a future release:",
|
|
103
|
-
err=True,
|
|
104
|
-
)
|
|
105
|
-
click.echo(" - showcase_htmx: Coming in v0.78.0", err=True)
|
|
106
|
-
click.echo(
|
|
107
|
-
" - Use 'showcase_react' (default) or 'showcase_html' instead",
|
|
108
|
-
err=True,
|
|
109
|
-
)
|
|
110
|
-
return False
|
|
103
|
+
click.echo(
|
|
104
|
+
f"⏳ Generating project: {config.project.slug} "
|
|
105
|
+
f"(package: {config.project.package})..."
|
|
106
|
+
)
|
|
111
107
|
|
|
112
108
|
generator = ProjectGenerator(theme=config.project.theme)
|
|
113
|
-
|
|
109
|
+
# mypy can resolve an older installed quickscale-core signature here.
|
|
110
|
+
generate_project = cast(Any, generator.generate)
|
|
111
|
+
generate_project(
|
|
112
|
+
config.project.slug,
|
|
113
|
+
output_path,
|
|
114
|
+
package_name=config.project.package,
|
|
115
|
+
)
|
|
114
116
|
|
|
115
117
|
click.secho(f"✅ Project generated: {output_path}", fg="green")
|
|
116
118
|
return True
|
|
@@ -160,7 +162,11 @@ def _git_commit(project_path: Path, message: str) -> bool:
|
|
|
160
162
|
return success
|
|
161
163
|
|
|
162
164
|
|
|
163
|
-
def _embed_module(
|
|
165
|
+
def _embed_module(
|
|
166
|
+
project_path: Path,
|
|
167
|
+
module_name: str,
|
|
168
|
+
skip_auth_migration_check: bool = False,
|
|
169
|
+
) -> bool:
|
|
164
170
|
"""Embed a module using the embed_module function with non-interactive mode"""
|
|
165
171
|
click.echo(f"⏳ Embedding module: {module_name}...")
|
|
166
172
|
|
|
@@ -169,6 +175,8 @@ def _embed_module(project_path: Path, module_name: str) -> bool:
|
|
|
169
175
|
module=module_name,
|
|
170
176
|
project_path=project_path,
|
|
171
177
|
non_interactive=True,
|
|
178
|
+
allow_unverifiable_auth_state=True,
|
|
179
|
+
skip_auth_migration_check=skip_auth_migration_check,
|
|
172
180
|
)
|
|
173
181
|
|
|
174
182
|
if success:
|
|
@@ -200,6 +208,15 @@ def _run_migrations(project_path: Path) -> bool:
|
|
|
200
208
|
)[0]
|
|
201
209
|
|
|
202
210
|
|
|
211
|
+
def _run_migrations_in_docker(project_path: Path) -> bool:
|
|
212
|
+
"""Run Django migrations inside the backend Docker container."""
|
|
213
|
+
return _run_command(
|
|
214
|
+
["quickscale", "manage", "migrate"],
|
|
215
|
+
project_path,
|
|
216
|
+
"Running migrations (Docker)",
|
|
217
|
+
)[0]
|
|
218
|
+
|
|
219
|
+
|
|
203
220
|
def _start_docker(
|
|
204
221
|
project_path: Path, build: bool = True, verbose: bool = False
|
|
205
222
|
) -> bool:
|
|
@@ -246,6 +263,7 @@ def _start_docker(
|
|
|
246
263
|
cmd,
|
|
247
264
|
project_path,
|
|
248
265
|
"Starting Docker services",
|
|
266
|
+
capture=False,
|
|
249
267
|
)
|
|
250
268
|
return success
|
|
251
269
|
|
|
@@ -352,7 +370,10 @@ def _load_and_validate_config(config_path: Path) -> QuickScaleConfig:
|
|
|
352
370
|
click.secho(
|
|
353
371
|
f"❌ Configuration file not found: {config_path}", fg="red", err=True
|
|
354
372
|
)
|
|
355
|
-
click.echo(
|
|
373
|
+
click.echo(
|
|
374
|
+
"\n💡 Create a configuration with: quickscale plan <project-slug>",
|
|
375
|
+
err=True,
|
|
376
|
+
)
|
|
356
377
|
raise click.Abort()
|
|
357
378
|
|
|
358
379
|
click.echo(f"\n📋 Reading configuration: {config_path}")
|
|
@@ -367,18 +388,19 @@ def _load_and_validate_config(config_path: Path) -> QuickScaleConfig:
|
|
|
367
388
|
raise click.Abort()
|
|
368
389
|
|
|
369
390
|
|
|
370
|
-
def _determine_output_path(config_path: Path,
|
|
391
|
+
def _determine_output_path(config_path: Path, project_slug: str) -> Path:
|
|
371
392
|
"""Determine output directory for project."""
|
|
372
393
|
config_path = config_path.resolve()
|
|
373
|
-
if config_path.parent.name ==
|
|
394
|
+
if config_path.parent.name == project_slug:
|
|
374
395
|
return config_path.parent
|
|
375
|
-
return Path.cwd() /
|
|
396
|
+
return Path.cwd() / project_slug
|
|
376
397
|
|
|
377
398
|
|
|
378
399
|
def _display_config_summary(qs_config: QuickScaleConfig) -> None:
|
|
379
400
|
"""Display configuration summary."""
|
|
380
401
|
click.echo("\n🚀 Applying configuration:")
|
|
381
|
-
click.echo(f" Project: {qs_config.project.
|
|
402
|
+
click.echo(f" Project slug: {qs_config.project.slug}")
|
|
403
|
+
click.echo(f" Python package: {qs_config.project.package}")
|
|
382
404
|
click.echo(f" Theme: {qs_config.project.theme}")
|
|
383
405
|
if qs_config.modules:
|
|
384
406
|
click.echo(f" Modules: {', '.join(qs_config.modules.keys())}")
|
|
@@ -495,7 +517,7 @@ def _generate_with_existing_config(
|
|
|
495
517
|
item.unlink()
|
|
496
518
|
|
|
497
519
|
temp_dir = Path(tempfile.mkdtemp())
|
|
498
|
-
temp_project = temp_dir / qs_config.project.
|
|
520
|
+
temp_project = temp_dir / qs_config.project.slug
|
|
499
521
|
|
|
500
522
|
if not _generate_project(qs_config, temp_project):
|
|
501
523
|
shutil.rmtree(temp_dir)
|
|
@@ -536,42 +558,92 @@ def _init_git_with_config(output_path: Path) -> None:
|
|
|
536
558
|
click.secho("⚠️ Initial commit failed, continuing...", fg="yellow")
|
|
537
559
|
|
|
538
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
|
+
|
|
539
600
|
def _embed_modules_step(
|
|
540
601
|
output_path: Path,
|
|
541
602
|
modules_to_embed: list[str],
|
|
542
603
|
no_modules: bool,
|
|
543
604
|
existing_state: QuickScaleState | None,
|
|
544
|
-
) ->
|
|
545
|
-
"""Embed modules
|
|
605
|
+
) -> EmbedModulesResult:
|
|
606
|
+
"""Embed modules with fail-fast semantics."""
|
|
546
607
|
embedded_modules: list[str] = []
|
|
547
608
|
|
|
548
609
|
if no_modules or not modules_to_embed:
|
|
549
610
|
if existing_state and not modules_to_embed:
|
|
550
611
|
click.echo("⏭️ No new modules to embed")
|
|
551
|
-
return embedded_modules
|
|
612
|
+
return EmbedModulesResult(success=True, embedded_modules=embedded_modules)
|
|
613
|
+
|
|
614
|
+
skip_auth_migration_check = existing_state is None
|
|
552
615
|
|
|
553
616
|
for module_name in modules_to_embed:
|
|
554
|
-
if not _embed_module(
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
# Commit any partial changes to allow subsequent modules to be embedded
|
|
617
|
+
if not _embed_module(
|
|
618
|
+
output_path,
|
|
619
|
+
module_name,
|
|
620
|
+
skip_auth_migration_check=skip_auth_migration_check,
|
|
621
|
+
):
|
|
560
622
|
if not is_working_directory_clean(output_path):
|
|
561
623
|
_git_commit(output_path, f"Partial module: {module_name} (incomplete)")
|
|
624
|
+
click.secho(
|
|
625
|
+
f"❌ Module embedding failed for required module: {module_name}",
|
|
626
|
+
fg="red",
|
|
627
|
+
err=True,
|
|
628
|
+
)
|
|
629
|
+
return EmbedModulesResult(
|
|
630
|
+
success=False,
|
|
631
|
+
embedded_modules=embedded_modules,
|
|
632
|
+
failed_module=module_name,
|
|
633
|
+
)
|
|
562
634
|
else:
|
|
563
635
|
embedded_modules.append(module_name)
|
|
564
636
|
_git_commit(output_path, f"Add module: {module_name}")
|
|
565
637
|
|
|
566
|
-
return embedded_modules
|
|
638
|
+
return EmbedModulesResult(success=True, embedded_modules=embedded_modules)
|
|
567
639
|
|
|
568
640
|
|
|
569
|
-
def _run_post_generation_steps(output_path: Path) -> None:
|
|
570
|
-
"""Run poetry install and migrations."""
|
|
641
|
+
def _run_post_generation_steps(output_path: Path, run_migrations: bool = True) -> None:
|
|
642
|
+
"""Run poetry install and optionally migrations."""
|
|
571
643
|
if not _run_poetry_install(output_path):
|
|
572
644
|
click.secho("⚠️ Poetry install failed, continuing...", fg="yellow")
|
|
573
645
|
|
|
574
|
-
if not _run_migrations(output_path):
|
|
646
|
+
if run_migrations and not _run_migrations(output_path):
|
|
575
647
|
click.secho("⚠️ Migrations failed, continuing...", fg="yellow")
|
|
576
648
|
|
|
577
649
|
|
|
@@ -590,7 +662,8 @@ def _save_project_state(
|
|
|
590
662
|
new_state = QuickScaleState(
|
|
591
663
|
version="1",
|
|
592
664
|
project=ProjectState(
|
|
593
|
-
|
|
665
|
+
slug=qs_config.project.slug,
|
|
666
|
+
package=qs_config.project.package,
|
|
594
667
|
theme=qs_config.project.theme,
|
|
595
668
|
created_at=datetime.now().isoformat(),
|
|
596
669
|
last_applied=datetime.now().isoformat(),
|
|
@@ -624,7 +697,10 @@ def _save_project_state(
|
|
|
624
697
|
|
|
625
698
|
|
|
626
699
|
def _display_next_steps(
|
|
627
|
-
output_path: Path,
|
|
700
|
+
output_path: Path,
|
|
701
|
+
qs_config: QuickScaleConfig,
|
|
702
|
+
no_docker: bool,
|
|
703
|
+
docker_started: bool | None = None,
|
|
628
704
|
) -> None:
|
|
629
705
|
"""Display success message and next steps."""
|
|
630
706
|
click.echo("\n" + "=" * 50)
|
|
@@ -633,12 +709,17 @@ def _display_next_steps(
|
|
|
633
709
|
|
|
634
710
|
click.echo("\n📋 Next steps:")
|
|
635
711
|
if output_path != Path.cwd():
|
|
636
|
-
click.echo(f" cd {qs_config.project.
|
|
712
|
+
click.echo(f" cd {qs_config.project.slug}")
|
|
637
713
|
|
|
638
714
|
if qs_config.docker.start and not no_docker:
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
715
|
+
if docker_started is False:
|
|
716
|
+
click.echo(" # Docker auto-start failed during apply")
|
|
717
|
+
click.echo(" quickscale up --build # Retry Docker startup")
|
|
718
|
+
click.echo(" quickscale logs # View failure details")
|
|
719
|
+
else:
|
|
720
|
+
click.echo(" # Docker services should be running")
|
|
721
|
+
click.echo(" quickscale logs backend # View logs")
|
|
722
|
+
click.echo(" quickscale ps # Check status")
|
|
642
723
|
else:
|
|
643
724
|
click.echo(" quickscale up # Start Docker services")
|
|
644
725
|
click.echo(" # Or run without Docker:")
|
|
@@ -657,7 +738,7 @@ def _prepare_apply_context(config_path: Path) -> ApplyContext:
|
|
|
657
738
|
qs_config = _load_and_validate_config(config_path)
|
|
658
739
|
|
|
659
740
|
# Determine output path
|
|
660
|
-
output_path = _determine_output_path(config_path, qs_config.project.
|
|
741
|
+
output_path = _determine_output_path(config_path, qs_config.project.slug)
|
|
661
742
|
|
|
662
743
|
# Load existing state if project exists
|
|
663
744
|
state_manager = StateManager(output_path)
|
|
@@ -684,6 +765,57 @@ def _prepare_apply_context(config_path: Path) -> ApplyContext:
|
|
|
684
765
|
)
|
|
685
766
|
|
|
686
767
|
|
|
768
|
+
def _regenerate_managed_wiring_for_apply(
|
|
769
|
+
ctx: ApplyContext,
|
|
770
|
+
embedded_modules: list[str],
|
|
771
|
+
) -> bool:
|
|
772
|
+
"""Regenerate managed module wiring files after embed/config changes."""
|
|
773
|
+
desired_module_names = sorted(ctx.qs_config.modules.keys())
|
|
774
|
+
if ctx.existing_state is None:
|
|
775
|
+
selected_modules = embedded_modules
|
|
776
|
+
else:
|
|
777
|
+
# Existing state may include unchanged modules that should remain wired.
|
|
778
|
+
selected_modules = sorted(
|
|
779
|
+
set(ctx.delta.modules_unchanged) | set(embedded_modules)
|
|
780
|
+
)
|
|
781
|
+
|
|
782
|
+
# If no desired modules are configured, explicitly render empty managed files.
|
|
783
|
+
if not desired_module_names:
|
|
784
|
+
selected_modules = []
|
|
785
|
+
|
|
786
|
+
options = {
|
|
787
|
+
module_name: module_config.options
|
|
788
|
+
for module_name, module_config in ctx.qs_config.modules.items()
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
success, message = regenerate_managed_wiring(
|
|
792
|
+
ctx.output_path,
|
|
793
|
+
module_names=selected_modules,
|
|
794
|
+
option_overrides=options,
|
|
795
|
+
project_package=ctx.qs_config.project.package,
|
|
796
|
+
)
|
|
797
|
+
if success:
|
|
798
|
+
click.secho("✅ Managed module wiring regenerated", fg="green")
|
|
799
|
+
return True
|
|
800
|
+
|
|
801
|
+
click.secho(f"❌ Managed wiring regeneration failed: {message}", fg="red", err=True)
|
|
802
|
+
return False
|
|
803
|
+
|
|
804
|
+
|
|
805
|
+
def _print_apply_failure_summary(failed_step: str, reason: str) -> None:
|
|
806
|
+
"""Print explicit failure summary and skipped steps."""
|
|
807
|
+
click.echo("\n" + "=" * 50)
|
|
808
|
+
click.secho("❌ Apply failed", fg="red", bold=True)
|
|
809
|
+
click.echo("=" * 50)
|
|
810
|
+
click.echo(f"\nFailed step: {failed_step}")
|
|
811
|
+
click.echo(f"Reason: {reason}")
|
|
812
|
+
click.echo("\nSkipped downstream steps:")
|
|
813
|
+
click.echo(" • poetry install")
|
|
814
|
+
click.echo(" • migrations")
|
|
815
|
+
click.echo(" • docker start")
|
|
816
|
+
click.echo(" • success completion output")
|
|
817
|
+
|
|
818
|
+
|
|
687
819
|
def _execute_apply_steps(
|
|
688
820
|
ctx: ApplyContext,
|
|
689
821
|
force: bool,
|
|
@@ -714,24 +846,101 @@ def _execute_apply_steps(
|
|
|
714
846
|
if ctx.existing_state
|
|
715
847
|
else list(ctx.qs_config.modules.keys())
|
|
716
848
|
)
|
|
717
|
-
|
|
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
|
+
|
|
856
|
+
embed_result = _embed_modules_step(
|
|
718
857
|
ctx.output_path, modules_to_embed, no_modules, ctx.existing_state
|
|
719
858
|
)
|
|
859
|
+
embedded_modules = embed_result.embedded_modules
|
|
860
|
+
|
|
861
|
+
if not embed_result.success:
|
|
862
|
+
# Persist successful partial embeds (explicit no-rollback contract).
|
|
863
|
+
_save_project_state(
|
|
864
|
+
ctx.output_path,
|
|
865
|
+
ctx.qs_config,
|
|
866
|
+
ctx.existing_state,
|
|
867
|
+
embedded_modules,
|
|
868
|
+
ctx.delta,
|
|
869
|
+
)
|
|
870
|
+
_print_apply_failure_summary(
|
|
871
|
+
failed_step="module embedding",
|
|
872
|
+
reason=f"required module '{embed_result.failed_module}' failed to embed",
|
|
873
|
+
)
|
|
874
|
+
raise click.Abort()
|
|
875
|
+
|
|
876
|
+
# Deterministic managed wiring generation for selected modules.
|
|
877
|
+
if not _regenerate_managed_wiring_for_apply(ctx, embedded_modules):
|
|
878
|
+
_save_project_state(
|
|
879
|
+
ctx.output_path,
|
|
880
|
+
ctx.qs_config,
|
|
881
|
+
ctx.existing_state,
|
|
882
|
+
embedded_modules,
|
|
883
|
+
ctx.delta,
|
|
884
|
+
)
|
|
885
|
+
_print_apply_failure_summary(
|
|
886
|
+
failed_step="managed module wiring generation",
|
|
887
|
+
reason="unable to render settings/modules.py and urls_modules.py",
|
|
888
|
+
)
|
|
889
|
+
raise click.Abort()
|
|
720
890
|
|
|
721
|
-
|
|
722
|
-
|
|
891
|
+
should_auto_start_docker = not no_docker and ctx.qs_config.docker.start
|
|
892
|
+
|
|
893
|
+
# Run post-generation steps. Defer migrations until after Docker startup
|
|
894
|
+
# when auto-start is enabled, so PostgreSQL is reachable.
|
|
895
|
+
_run_post_generation_steps(
|
|
896
|
+
ctx.output_path,
|
|
897
|
+
run_migrations=not should_auto_start_docker,
|
|
898
|
+
)
|
|
723
899
|
|
|
724
900
|
# Apply mutable configuration changes
|
|
725
901
|
if ctx.existing_state and ctx.delta.has_mutable_config_changes:
|
|
726
|
-
|
|
727
|
-
|
|
902
|
+
click.secho(
|
|
903
|
+
"✅ Mutable configuration changes applied via managed wiring",
|
|
904
|
+
fg="green",
|
|
905
|
+
)
|
|
728
906
|
|
|
729
907
|
# Start Docker
|
|
730
|
-
|
|
731
|
-
|
|
908
|
+
docker_started: bool | None = None
|
|
909
|
+
if should_auto_start_docker:
|
|
910
|
+
docker_started = _start_docker(
|
|
732
911
|
ctx.output_path, ctx.qs_config.docker.build, verbose_docker
|
|
733
|
-
)
|
|
734
|
-
|
|
912
|
+
)
|
|
913
|
+
if not docker_started:
|
|
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()
|
|
926
|
+
|
|
927
|
+
# For Docker auto-start projects, run migrations in the backend container
|
|
928
|
+
# so database connectivity uses the internal Docker network.
|
|
929
|
+
if should_auto_start_docker:
|
|
930
|
+
if docker_started:
|
|
931
|
+
if not _run_migrations_in_docker(ctx.output_path):
|
|
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()
|
|
735
944
|
|
|
736
945
|
# Save state
|
|
737
946
|
_save_project_state(
|
|
@@ -743,7 +952,7 @@ def _execute_apply_steps(
|
|
|
743
952
|
)
|
|
744
953
|
|
|
745
954
|
# Display next steps
|
|
746
|
-
_display_next_steps(ctx.output_path, ctx.qs_config, no_docker)
|
|
955
|
+
_display_next_steps(ctx.output_path, ctx.qs_config, no_docker, docker_started)
|
|
747
956
|
|
|
748
957
|
|
|
749
958
|
@click.command()
|
|
@@ -796,10 +1005,11 @@ def apply(
|
|
|
796
1005
|
1. Validate configuration
|
|
797
1006
|
2. Generate project
|
|
798
1007
|
3. Initialize git + initial commit
|
|
799
|
-
4. Embed modules (if configured)
|
|
800
|
-
5.
|
|
801
|
-
6. Run
|
|
1008
|
+
4. Embed modules (if configured, fail-fast on required module failure)
|
|
1009
|
+
5. Regenerate managed module wiring files
|
|
1010
|
+
6. Run poetry install
|
|
802
1011
|
7. Start Docker (if configured)
|
|
1012
|
+
8. Run migrations (after Docker auto-start when enabled)
|
|
803
1013
|
"""
|
|
804
1014
|
# Prepare context
|
|
805
1015
|
ctx = _prepare_apply_context(Path(config))
|