quickscale-cli 0.74.2__tar.gz → 0.75.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.74.2 → quickscale_cli-0.75.0}/PKG-INFO +10 -6
- {quickscale_cli-0.74.2 → quickscale_cli-0.75.0}/README.md +8 -4
- {quickscale_cli-0.74.2 → quickscale_cli-0.75.0}/pyproject.toml +2 -2
- {quickscale_cli-0.74.2 → quickscale_cli-0.75.0}/src/quickscale_cli/_version.py +1 -1
- {quickscale_cli-0.74.2 → quickscale_cli-0.75.0}/src/quickscale_cli/commands/apply_command.py +202 -41
- {quickscale_cli-0.74.2 → quickscale_cli-0.75.0}/src/quickscale_cli/commands/deployment_commands.py +53 -15
- {quickscale_cli-0.74.2 → quickscale_cli-0.75.0}/src/quickscale_cli/commands/development_commands.py +17 -6
- {quickscale_cli-0.74.2 → quickscale_cli-0.75.0}/src/quickscale_cli/commands/module_commands.py +55 -20
- {quickscale_cli-0.74.2 → quickscale_cli-0.75.0}/src/quickscale_cli/commands/module_config.py +326 -263
- quickscale_cli-0.75.0/src/quickscale_cli/commands/module_wiring_specs.py +199 -0
- {quickscale_cli-0.74.2 → quickscale_cli-0.75.0}/src/quickscale_cli/commands/plan_command.py +187 -71
- {quickscale_cli-0.74.2 → quickscale_cli-0.75.0}/src/quickscale_cli/commands/remove_command.py +27 -82
- {quickscale_cli-0.74.2 → quickscale_cli-0.75.0}/src/quickscale_cli/commands/status_command.py +6 -3
- quickscale_cli-0.75.0/src/quickscale_cli/module_catalog.py +77 -0
- {quickscale_cli-0.74.2 → quickscale_cli-0.75.0}/src/quickscale_cli/schema/config_schema.py +95 -24
- {quickscale_cli-0.74.2 → quickscale_cli-0.75.0}/src/quickscale_cli/schema/state_schema.py +32 -4
- quickscale_cli-0.75.0/src/quickscale_cli/utils/module_wiring_manager.py +115 -0
- quickscale_cli-0.75.0/src/quickscale_cli/utils/project_identity.py +99 -0
- {quickscale_cli-0.74.2 → quickscale_cli-0.75.0}/src/quickscale_cli/utils/project_manager.py +5 -5
- {quickscale_cli-0.74.2 → quickscale_cli-0.75.0}/src/quickscale_cli/utils/railway_utils.py +133 -33
- {quickscale_cli-0.74.2 → quickscale_cli-0.75.0}/src/quickscale_cli/__init__.py +0 -0
- {quickscale_cli-0.74.2 → quickscale_cli-0.75.0}/src/quickscale_cli/commands/__init__.py +0 -0
- {quickscale_cli-0.74.2 → quickscale_cli-0.75.0}/src/quickscale_cli/main.py +0 -0
- {quickscale_cli-0.74.2 → quickscale_cli-0.75.0}/src/quickscale_cli/schema/__init__.py +0 -0
- {quickscale_cli-0.74.2 → quickscale_cli-0.75.0}/src/quickscale_cli/schema/delta.py +0 -0
- {quickscale_cli-0.74.2 → quickscale_cli-0.75.0}/src/quickscale_cli/utils/__init__.py +0 -0
- {quickscale_cli-0.74.2 → quickscale_cli-0.75.0}/src/quickscale_cli/utils/dependency_utils.py +0 -0
- {quickscale_cli-0.74.2 → quickscale_cli-0.75.0}/src/quickscale_cli/utils/docker_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: quickscale-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.75.0
|
|
4
4
|
Summary: Command-line interface for QuickScale Django project generator
|
|
5
5
|
License: Apache-2.0
|
|
6
6
|
Keywords: django,cli,project-generator,saas
|
|
@@ -17,7 +17,7 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
17
17
|
Classifier: Programming Language :: Python :: 3.13
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.14
|
|
19
19
|
Requires-Dist: click (>=8.3.1,<9.0.0)
|
|
20
|
-
Requires-Dist: quickscale-core (>=0.
|
|
20
|
+
Requires-Dist: quickscale-core (>=0.75.0,<0.76.0)
|
|
21
21
|
Project-URL: Documentation, https://github.com/Experto-AI/quickscale/tree/main/docs
|
|
22
22
|
Project-URL: Homepage, https://github.com/Experto-AI/quickscale
|
|
23
23
|
Project-URL: Repository, https://github.com/Experto-AI/quickscale
|
|
@@ -30,7 +30,7 @@ Description-Content-Type: text/markdown
|
|
|
30
30
|
[](https://github.com/Experto-AI/quickscale/actions/workflows/ci.yml)
|
|
31
31
|
[](https://www.python.org/downloads/)
|
|
32
32
|
[](https://opensource.org/licenses/Apache-2.0)
|
|
33
|
-
[](https://github.com/Experto-AI/quickscale/releases)
|
|
34
34
|
[](https://github.com/astral-sh/ruff)
|
|
35
35
|
|
|
36
36
|
> **You are here**: **QuickScale README** (Project Overview)
|
|
@@ -107,6 +107,10 @@ See [decisions.md - Glossary section](./docs/technical/decisions.md#document-res
|
|
|
107
107
|
5. Build your unique client application
|
|
108
108
|
6. Deploy to Railway with `quickscale deploy railway` (or use standard Django deployment)
|
|
109
109
|
|
|
110
|
+
`quickscale.yml` now uses explicit identity fields:
|
|
111
|
+
- `project.slug`: filesystem/service slug (for directory names)
|
|
112
|
+
- `project.package`: Python package/import name (for Django module paths)
|
|
113
|
+
|
|
110
114
|
ℹ️ The [MVP Feature Matrix](./docs/technical/decisions.md#mvp-feature-matrix-authoritative) is the single source of truth for what's in or out.
|
|
111
115
|
|
|
112
116
|
### What You Get
|
|
@@ -203,7 +207,7 @@ If you want to contribute to QuickScale development, see the [Development Guide]
|
|
|
203
207
|
|
|
204
208
|
# 2. Create your project configuration
|
|
205
209
|
quickscale plan myapp
|
|
206
|
-
# → Accept defaults: theme (
|
|
210
|
+
# → Accept defaults: theme (showcase_react), no modules, Docker enabled
|
|
207
211
|
|
|
208
212
|
# 3. Generate and enter the project
|
|
209
213
|
cd myapp
|
|
@@ -306,7 +310,7 @@ quickscale apply
|
|
|
306
310
|
### Option 1: Docker (Recommended for production parity)
|
|
307
311
|
|
|
308
312
|
```bash
|
|
309
|
-
# Start all services (
|
|
313
|
+
# Start all services (backend + database)
|
|
310
314
|
quickscale up
|
|
311
315
|
|
|
312
316
|
# Run migrations
|
|
@@ -316,7 +320,7 @@ quickscale manage migrate
|
|
|
316
320
|
quickscale manage createsuperuser
|
|
317
321
|
|
|
318
322
|
# View logs
|
|
319
|
-
quickscale logs -f
|
|
323
|
+
quickscale logs -f backend
|
|
320
324
|
|
|
321
325
|
# Open a shell in the container
|
|
322
326
|
quickscale shell
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
[](https://github.com/Experto-AI/quickscale/actions/workflows/ci.yml)
|
|
6
6
|
[](https://www.python.org/downloads/)
|
|
7
7
|
[](https://opensource.org/licenses/Apache-2.0)
|
|
8
|
-
[](https://github.com/Experto-AI/quickscale/releases)
|
|
9
9
|
[](https://github.com/astral-sh/ruff)
|
|
10
10
|
|
|
11
11
|
> **You are here**: **QuickScale README** (Project Overview)
|
|
@@ -82,6 +82,10 @@ See [decisions.md - Glossary section](./docs/technical/decisions.md#document-res
|
|
|
82
82
|
5. Build your unique client application
|
|
83
83
|
6. Deploy to Railway with `quickscale deploy railway` (or use standard Django deployment)
|
|
84
84
|
|
|
85
|
+
`quickscale.yml` now uses explicit identity fields:
|
|
86
|
+
- `project.slug`: filesystem/service slug (for directory names)
|
|
87
|
+
- `project.package`: Python package/import name (for Django module paths)
|
|
88
|
+
|
|
85
89
|
ℹ️ The [MVP Feature Matrix](./docs/technical/decisions.md#mvp-feature-matrix-authoritative) is the single source of truth for what's in or out.
|
|
86
90
|
|
|
87
91
|
### What You Get
|
|
@@ -178,7 +182,7 @@ If you want to contribute to QuickScale development, see the [Development Guide]
|
|
|
178
182
|
|
|
179
183
|
# 2. Create your project configuration
|
|
180
184
|
quickscale plan myapp
|
|
181
|
-
# → Accept defaults: theme (
|
|
185
|
+
# → Accept defaults: theme (showcase_react), no modules, Docker enabled
|
|
182
186
|
|
|
183
187
|
# 3. Generate and enter the project
|
|
184
188
|
cd myapp
|
|
@@ -281,7 +285,7 @@ quickscale apply
|
|
|
281
285
|
### Option 1: Docker (Recommended for production parity)
|
|
282
286
|
|
|
283
287
|
```bash
|
|
284
|
-
# Start all services (
|
|
288
|
+
# Start all services (backend + database)
|
|
285
289
|
quickscale up
|
|
286
290
|
|
|
287
291
|
# Run migrations
|
|
@@ -291,7 +295,7 @@ quickscale manage migrate
|
|
|
291
295
|
quickscale manage createsuperuser
|
|
292
296
|
|
|
293
297
|
# View logs
|
|
294
|
-
quickscale logs -f
|
|
298
|
+
quickscale logs -f backend
|
|
295
299
|
|
|
296
300
|
# Open a shell in the container
|
|
297
301
|
quickscale shell
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "quickscale-cli"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.75.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"
|
|
@@ -25,7 +25,7 @@ packages = [{include = "quickscale_cli", from = "src"}]
|
|
|
25
25
|
[tool.poetry.dependencies]
|
|
26
26
|
python = "^3.12"
|
|
27
27
|
click = "^8.3.1"
|
|
28
|
-
quickscale-core = "^0.
|
|
28
|
+
quickscale-core = "^0.75.0"
|
|
29
29
|
|
|
30
30
|
[tool.poetry.group.dev.dependencies]
|
|
31
31
|
# 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.75.0"
|
{quickscale_cli-0.74.2 → quickscale_cli-0.75.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,7 +100,10 @@ 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(
|
|
103
|
+
click.echo(
|
|
104
|
+
f"⏳ Generating project: {config.project.slug} "
|
|
105
|
+
f"(package: {config.project.package})..."
|
|
106
|
+
)
|
|
93
107
|
|
|
94
108
|
# Validate theme availability
|
|
95
109
|
if config.project.theme == "showcase_htmx":
|
|
@@ -110,7 +124,13 @@ def _generate_project(config: QuickScaleConfig, output_path: Path) -> bool:
|
|
|
110
124
|
return False
|
|
111
125
|
|
|
112
126
|
generator = ProjectGenerator(theme=config.project.theme)
|
|
113
|
-
|
|
127
|
+
# mypy can resolve an older installed quickscale-core signature here.
|
|
128
|
+
generate_project = cast(Any, generator.generate)
|
|
129
|
+
generate_project(
|
|
130
|
+
config.project.slug,
|
|
131
|
+
output_path,
|
|
132
|
+
package_name=config.project.package,
|
|
133
|
+
)
|
|
114
134
|
|
|
115
135
|
click.secho(f"✅ Project generated: {output_path}", fg="green")
|
|
116
136
|
return True
|
|
@@ -160,7 +180,11 @@ def _git_commit(project_path: Path, message: str) -> bool:
|
|
|
160
180
|
return success
|
|
161
181
|
|
|
162
182
|
|
|
163
|
-
def _embed_module(
|
|
183
|
+
def _embed_module(
|
|
184
|
+
project_path: Path,
|
|
185
|
+
module_name: str,
|
|
186
|
+
skip_auth_migration_check: bool = False,
|
|
187
|
+
) -> bool:
|
|
164
188
|
"""Embed a module using the embed_module function with non-interactive mode"""
|
|
165
189
|
click.echo(f"⏳ Embedding module: {module_name}...")
|
|
166
190
|
|
|
@@ -169,6 +193,8 @@ def _embed_module(project_path: Path, module_name: str) -> bool:
|
|
|
169
193
|
module=module_name,
|
|
170
194
|
project_path=project_path,
|
|
171
195
|
non_interactive=True,
|
|
196
|
+
allow_unverifiable_auth_state=True,
|
|
197
|
+
skip_auth_migration_check=skip_auth_migration_check,
|
|
172
198
|
)
|
|
173
199
|
|
|
174
200
|
if success:
|
|
@@ -200,6 +226,15 @@ def _run_migrations(project_path: Path) -> bool:
|
|
|
200
226
|
)[0]
|
|
201
227
|
|
|
202
228
|
|
|
229
|
+
def _run_migrations_in_docker(project_path: Path) -> bool:
|
|
230
|
+
"""Run Django migrations inside the backend Docker container."""
|
|
231
|
+
return _run_command(
|
|
232
|
+
["quickscale", "manage", "migrate"],
|
|
233
|
+
project_path,
|
|
234
|
+
"Running migrations (Docker)",
|
|
235
|
+
)[0]
|
|
236
|
+
|
|
237
|
+
|
|
203
238
|
def _start_docker(
|
|
204
239
|
project_path: Path, build: bool = True, verbose: bool = False
|
|
205
240
|
) -> bool:
|
|
@@ -352,7 +387,10 @@ def _load_and_validate_config(config_path: Path) -> QuickScaleConfig:
|
|
|
352
387
|
click.secho(
|
|
353
388
|
f"❌ Configuration file not found: {config_path}", fg="red", err=True
|
|
354
389
|
)
|
|
355
|
-
click.echo(
|
|
390
|
+
click.echo(
|
|
391
|
+
"\n💡 Create a configuration with: quickscale plan <project-slug>",
|
|
392
|
+
err=True,
|
|
393
|
+
)
|
|
356
394
|
raise click.Abort()
|
|
357
395
|
|
|
358
396
|
click.echo(f"\n📋 Reading configuration: {config_path}")
|
|
@@ -367,18 +405,19 @@ def _load_and_validate_config(config_path: Path) -> QuickScaleConfig:
|
|
|
367
405
|
raise click.Abort()
|
|
368
406
|
|
|
369
407
|
|
|
370
|
-
def _determine_output_path(config_path: Path,
|
|
408
|
+
def _determine_output_path(config_path: Path, project_slug: str) -> Path:
|
|
371
409
|
"""Determine output directory for project."""
|
|
372
410
|
config_path = config_path.resolve()
|
|
373
|
-
if config_path.parent.name ==
|
|
411
|
+
if config_path.parent.name == project_slug:
|
|
374
412
|
return config_path.parent
|
|
375
|
-
return Path.cwd() /
|
|
413
|
+
return Path.cwd() / project_slug
|
|
376
414
|
|
|
377
415
|
|
|
378
416
|
def _display_config_summary(qs_config: QuickScaleConfig) -> None:
|
|
379
417
|
"""Display configuration summary."""
|
|
380
418
|
click.echo("\n🚀 Applying configuration:")
|
|
381
|
-
click.echo(f" Project: {qs_config.project.
|
|
419
|
+
click.echo(f" Project slug: {qs_config.project.slug}")
|
|
420
|
+
click.echo(f" Python package: {qs_config.project.package}")
|
|
382
421
|
click.echo(f" Theme: {qs_config.project.theme}")
|
|
383
422
|
if qs_config.modules:
|
|
384
423
|
click.echo(f" Modules: {', '.join(qs_config.modules.keys())}")
|
|
@@ -495,7 +534,7 @@ def _generate_with_existing_config(
|
|
|
495
534
|
item.unlink()
|
|
496
535
|
|
|
497
536
|
temp_dir = Path(tempfile.mkdtemp())
|
|
498
|
-
temp_project = temp_dir / qs_config.project.
|
|
537
|
+
temp_project = temp_dir / qs_config.project.slug
|
|
499
538
|
|
|
500
539
|
if not _generate_project(qs_config, temp_project):
|
|
501
540
|
shutil.rmtree(temp_dir)
|
|
@@ -541,37 +580,48 @@ def _embed_modules_step(
|
|
|
541
580
|
modules_to_embed: list[str],
|
|
542
581
|
no_modules: bool,
|
|
543
582
|
existing_state: QuickScaleState | None,
|
|
544
|
-
) ->
|
|
545
|
-
"""Embed modules
|
|
583
|
+
) -> EmbedModulesResult:
|
|
584
|
+
"""Embed modules with fail-fast semantics."""
|
|
546
585
|
embedded_modules: list[str] = []
|
|
547
586
|
|
|
548
587
|
if no_modules or not modules_to_embed:
|
|
549
588
|
if existing_state and not modules_to_embed:
|
|
550
589
|
click.echo("⏭️ No new modules to embed")
|
|
551
|
-
return embedded_modules
|
|
590
|
+
return EmbedModulesResult(success=True, embedded_modules=embedded_modules)
|
|
591
|
+
|
|
592
|
+
skip_auth_migration_check = existing_state is None
|
|
552
593
|
|
|
553
594
|
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
|
|
595
|
+
if not _embed_module(
|
|
596
|
+
output_path,
|
|
597
|
+
module_name,
|
|
598
|
+
skip_auth_migration_check=skip_auth_migration_check,
|
|
599
|
+
):
|
|
560
600
|
if not is_working_directory_clean(output_path):
|
|
561
601
|
_git_commit(output_path, f"Partial module: {module_name} (incomplete)")
|
|
602
|
+
click.secho(
|
|
603
|
+
f"❌ Module embedding failed for required module: {module_name}",
|
|
604
|
+
fg="red",
|
|
605
|
+
err=True,
|
|
606
|
+
)
|
|
607
|
+
return EmbedModulesResult(
|
|
608
|
+
success=False,
|
|
609
|
+
embedded_modules=embedded_modules,
|
|
610
|
+
failed_module=module_name,
|
|
611
|
+
)
|
|
562
612
|
else:
|
|
563
613
|
embedded_modules.append(module_name)
|
|
564
614
|
_git_commit(output_path, f"Add module: {module_name}")
|
|
565
615
|
|
|
566
|
-
return embedded_modules
|
|
616
|
+
return EmbedModulesResult(success=True, embedded_modules=embedded_modules)
|
|
567
617
|
|
|
568
618
|
|
|
569
|
-
def _run_post_generation_steps(output_path: Path) -> None:
|
|
570
|
-
"""Run poetry install and migrations."""
|
|
619
|
+
def _run_post_generation_steps(output_path: Path, run_migrations: bool = True) -> None:
|
|
620
|
+
"""Run poetry install and optionally migrations."""
|
|
571
621
|
if not _run_poetry_install(output_path):
|
|
572
622
|
click.secho("⚠️ Poetry install failed, continuing...", fg="yellow")
|
|
573
623
|
|
|
574
|
-
if not _run_migrations(output_path):
|
|
624
|
+
if run_migrations and not _run_migrations(output_path):
|
|
575
625
|
click.secho("⚠️ Migrations failed, continuing...", fg="yellow")
|
|
576
626
|
|
|
577
627
|
|
|
@@ -590,7 +640,8 @@ def _save_project_state(
|
|
|
590
640
|
new_state = QuickScaleState(
|
|
591
641
|
version="1",
|
|
592
642
|
project=ProjectState(
|
|
593
|
-
|
|
643
|
+
slug=qs_config.project.slug,
|
|
644
|
+
package=qs_config.project.package,
|
|
594
645
|
theme=qs_config.project.theme,
|
|
595
646
|
created_at=datetime.now().isoformat(),
|
|
596
647
|
last_applied=datetime.now().isoformat(),
|
|
@@ -624,7 +675,10 @@ def _save_project_state(
|
|
|
624
675
|
|
|
625
676
|
|
|
626
677
|
def _display_next_steps(
|
|
627
|
-
output_path: Path,
|
|
678
|
+
output_path: Path,
|
|
679
|
+
qs_config: QuickScaleConfig,
|
|
680
|
+
no_docker: bool,
|
|
681
|
+
docker_started: bool | None = None,
|
|
628
682
|
) -> None:
|
|
629
683
|
"""Display success message and next steps."""
|
|
630
684
|
click.echo("\n" + "=" * 50)
|
|
@@ -633,12 +687,17 @@ def _display_next_steps(
|
|
|
633
687
|
|
|
634
688
|
click.echo("\n📋 Next steps:")
|
|
635
689
|
if output_path != Path.cwd():
|
|
636
|
-
click.echo(f" cd {qs_config.project.
|
|
690
|
+
click.echo(f" cd {qs_config.project.slug}")
|
|
637
691
|
|
|
638
692
|
if qs_config.docker.start and not no_docker:
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
693
|
+
if docker_started is False:
|
|
694
|
+
click.echo(" # Docker auto-start failed during apply")
|
|
695
|
+
click.echo(" quickscale up --build # Retry Docker startup")
|
|
696
|
+
click.echo(" quickscale logs # View failure details")
|
|
697
|
+
else:
|
|
698
|
+
click.echo(" # Docker services should be running")
|
|
699
|
+
click.echo(" quickscale logs backend # View logs")
|
|
700
|
+
click.echo(" quickscale ps # Check status")
|
|
642
701
|
else:
|
|
643
702
|
click.echo(" quickscale up # Start Docker services")
|
|
644
703
|
click.echo(" # Or run without Docker:")
|
|
@@ -657,7 +716,7 @@ def _prepare_apply_context(config_path: Path) -> ApplyContext:
|
|
|
657
716
|
qs_config = _load_and_validate_config(config_path)
|
|
658
717
|
|
|
659
718
|
# Determine output path
|
|
660
|
-
output_path = _determine_output_path(config_path, qs_config.project.
|
|
719
|
+
output_path = _determine_output_path(config_path, qs_config.project.slug)
|
|
661
720
|
|
|
662
721
|
# Load existing state if project exists
|
|
663
722
|
state_manager = StateManager(output_path)
|
|
@@ -684,6 +743,57 @@ def _prepare_apply_context(config_path: Path) -> ApplyContext:
|
|
|
684
743
|
)
|
|
685
744
|
|
|
686
745
|
|
|
746
|
+
def _regenerate_managed_wiring_for_apply(
|
|
747
|
+
ctx: ApplyContext,
|
|
748
|
+
embedded_modules: list[str],
|
|
749
|
+
) -> bool:
|
|
750
|
+
"""Regenerate managed module wiring files after embed/config changes."""
|
|
751
|
+
desired_module_names = sorted(ctx.qs_config.modules.keys())
|
|
752
|
+
if ctx.existing_state is None:
|
|
753
|
+
selected_modules = embedded_modules
|
|
754
|
+
else:
|
|
755
|
+
# Existing state may include unchanged modules that should remain wired.
|
|
756
|
+
selected_modules = sorted(
|
|
757
|
+
set(ctx.delta.modules_unchanged) | set(embedded_modules)
|
|
758
|
+
)
|
|
759
|
+
|
|
760
|
+
# If no desired modules are configured, explicitly render empty managed files.
|
|
761
|
+
if not desired_module_names:
|
|
762
|
+
selected_modules = []
|
|
763
|
+
|
|
764
|
+
options = {
|
|
765
|
+
module_name: module_config.options
|
|
766
|
+
for module_name, module_config in ctx.qs_config.modules.items()
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
success, message = regenerate_managed_wiring(
|
|
770
|
+
ctx.output_path,
|
|
771
|
+
module_names=selected_modules,
|
|
772
|
+
option_overrides=options,
|
|
773
|
+
project_package=ctx.qs_config.project.package,
|
|
774
|
+
)
|
|
775
|
+
if success:
|
|
776
|
+
click.secho("✅ Managed module wiring regenerated", fg="green")
|
|
777
|
+
return True
|
|
778
|
+
|
|
779
|
+
click.secho(f"❌ Managed wiring regeneration failed: {message}", fg="red", err=True)
|
|
780
|
+
return False
|
|
781
|
+
|
|
782
|
+
|
|
783
|
+
def _print_apply_failure_summary(failed_step: str, reason: str) -> None:
|
|
784
|
+
"""Print explicit failure summary and skipped steps."""
|
|
785
|
+
click.echo("\n" + "=" * 50)
|
|
786
|
+
click.secho("❌ Apply failed", fg="red", bold=True)
|
|
787
|
+
click.echo("=" * 50)
|
|
788
|
+
click.echo(f"\nFailed step: {failed_step}")
|
|
789
|
+
click.echo(f"Reason: {reason}")
|
|
790
|
+
click.echo("\nSkipped downstream steps:")
|
|
791
|
+
click.echo(" • poetry install")
|
|
792
|
+
click.echo(" • migrations")
|
|
793
|
+
click.echo(" • docker start")
|
|
794
|
+
click.echo(" • success completion output")
|
|
795
|
+
|
|
796
|
+
|
|
687
797
|
def _execute_apply_steps(
|
|
688
798
|
ctx: ApplyContext,
|
|
689
799
|
force: bool,
|
|
@@ -714,25 +824,75 @@ def _execute_apply_steps(
|
|
|
714
824
|
if ctx.existing_state
|
|
715
825
|
else list(ctx.qs_config.modules.keys())
|
|
716
826
|
)
|
|
717
|
-
|
|
827
|
+
embed_result = _embed_modules_step(
|
|
718
828
|
ctx.output_path, modules_to_embed, no_modules, ctx.existing_state
|
|
719
829
|
)
|
|
830
|
+
embedded_modules = embed_result.embedded_modules
|
|
831
|
+
|
|
832
|
+
if not embed_result.success:
|
|
833
|
+
# Persist successful partial embeds (explicit no-rollback contract).
|
|
834
|
+
_save_project_state(
|
|
835
|
+
ctx.output_path,
|
|
836
|
+
ctx.qs_config,
|
|
837
|
+
ctx.existing_state,
|
|
838
|
+
embedded_modules,
|
|
839
|
+
ctx.delta,
|
|
840
|
+
)
|
|
841
|
+
_print_apply_failure_summary(
|
|
842
|
+
failed_step="module embedding",
|
|
843
|
+
reason=f"required module '{embed_result.failed_module}' failed to embed",
|
|
844
|
+
)
|
|
845
|
+
raise click.Abort()
|
|
846
|
+
|
|
847
|
+
# Deterministic managed wiring generation for selected modules.
|
|
848
|
+
if not _regenerate_managed_wiring_for_apply(ctx, embedded_modules):
|
|
849
|
+
_save_project_state(
|
|
850
|
+
ctx.output_path,
|
|
851
|
+
ctx.qs_config,
|
|
852
|
+
ctx.existing_state,
|
|
853
|
+
embedded_modules,
|
|
854
|
+
ctx.delta,
|
|
855
|
+
)
|
|
856
|
+
_print_apply_failure_summary(
|
|
857
|
+
failed_step="managed module wiring generation",
|
|
858
|
+
reason="unable to render settings/modules.py and urls_modules.py",
|
|
859
|
+
)
|
|
860
|
+
raise click.Abort()
|
|
720
861
|
|
|
721
|
-
|
|
722
|
-
|
|
862
|
+
should_auto_start_docker = not no_docker and ctx.qs_config.docker.start
|
|
863
|
+
|
|
864
|
+
# Run post-generation steps. Defer migrations until after Docker startup
|
|
865
|
+
# when auto-start is enabled, so PostgreSQL is reachable.
|
|
866
|
+
_run_post_generation_steps(
|
|
867
|
+
ctx.output_path,
|
|
868
|
+
run_migrations=not should_auto_start_docker,
|
|
869
|
+
)
|
|
723
870
|
|
|
724
871
|
# Apply mutable configuration changes
|
|
725
872
|
if ctx.existing_state and ctx.delta.has_mutable_config_changes:
|
|
726
|
-
|
|
727
|
-
|
|
873
|
+
click.secho(
|
|
874
|
+
"✅ Mutable configuration changes applied via managed wiring",
|
|
875
|
+
fg="green",
|
|
876
|
+
)
|
|
728
877
|
|
|
729
878
|
# Start Docker
|
|
730
|
-
|
|
731
|
-
|
|
879
|
+
docker_started: bool | None = None
|
|
880
|
+
if should_auto_start_docker:
|
|
881
|
+
docker_started = _start_docker(
|
|
732
882
|
ctx.output_path, ctx.qs_config.docker.build, verbose_docker
|
|
733
|
-
)
|
|
883
|
+
)
|
|
884
|
+
if not docker_started:
|
|
734
885
|
click.secho("⚠️ Docker start failed, continuing...", fg="yellow")
|
|
735
886
|
|
|
887
|
+
# For Docker auto-start projects, run migrations in the backend container
|
|
888
|
+
# so database connectivity uses the internal Docker network.
|
|
889
|
+
if should_auto_start_docker:
|
|
890
|
+
if docker_started:
|
|
891
|
+
if not _run_migrations_in_docker(ctx.output_path):
|
|
892
|
+
click.secho("⚠️ Migrations failed, continuing...", fg="yellow")
|
|
893
|
+
elif not _run_migrations(ctx.output_path):
|
|
894
|
+
click.secho("⚠️ Migrations failed, continuing...", fg="yellow")
|
|
895
|
+
|
|
736
896
|
# Save state
|
|
737
897
|
_save_project_state(
|
|
738
898
|
ctx.output_path,
|
|
@@ -743,7 +903,7 @@ def _execute_apply_steps(
|
|
|
743
903
|
)
|
|
744
904
|
|
|
745
905
|
# Display next steps
|
|
746
|
-
_display_next_steps(ctx.output_path, ctx.qs_config, no_docker)
|
|
906
|
+
_display_next_steps(ctx.output_path, ctx.qs_config, no_docker, docker_started)
|
|
747
907
|
|
|
748
908
|
|
|
749
909
|
@click.command()
|
|
@@ -796,10 +956,11 @@ def apply(
|
|
|
796
956
|
1. Validate configuration
|
|
797
957
|
2. Generate project
|
|
798
958
|
3. Initialize git + initial commit
|
|
799
|
-
4. Embed modules (if configured)
|
|
800
|
-
5.
|
|
801
|
-
6. Run
|
|
959
|
+
4. Embed modules (if configured, fail-fast on required module failure)
|
|
960
|
+
5. Regenerate managed module wiring files
|
|
961
|
+
6. Run poetry install
|
|
802
962
|
7. Start Docker (if configured)
|
|
963
|
+
8. Run migrations (after Docker auto-start when enabled)
|
|
803
964
|
"""
|
|
804
965
|
# Prepare context
|
|
805
966
|
ctx = _prepare_apply_context(Path(config))
|