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.
Files changed (31) hide show
  1. quickscale_cli-0.76.0/PKG-INFO +60 -0
  2. quickscale_cli-0.76.0/README.md +37 -0
  3. {quickscale_cli-0.74.2 → quickscale_cli-0.76.0}/pyproject.toml +18 -19
  4. {quickscale_cli-0.74.2 → quickscale_cli-0.76.0}/src/quickscale_cli/_version.py +1 -1
  5. {quickscale_cli-0.74.2 → quickscale_cli-0.76.0}/src/quickscale_cli/commands/apply_command.py +270 -60
  6. {quickscale_cli-0.74.2 → quickscale_cli-0.76.0}/src/quickscale_cli/commands/deployment_commands.py +59 -16
  7. {quickscale_cli-0.74.2 → quickscale_cli-0.76.0}/src/quickscale_cli/commands/development_commands.py +139 -8
  8. {quickscale_cli-0.74.2 → quickscale_cli-0.76.0}/src/quickscale_cli/commands/module_commands.py +124 -22
  9. quickscale_cli-0.76.0/src/quickscale_cli/commands/module_config.py +1334 -0
  10. quickscale_cli-0.76.0/src/quickscale_cli/commands/module_wiring_specs.py +291 -0
  11. {quickscale_cli-0.74.2 → quickscale_cli-0.76.0}/src/quickscale_cli/commands/plan_command.py +396 -129
  12. {quickscale_cli-0.74.2 → quickscale_cli-0.76.0}/src/quickscale_cli/commands/remove_command.py +27 -82
  13. {quickscale_cli-0.74.2 → quickscale_cli-0.76.0}/src/quickscale_cli/commands/status_command.py +7 -4
  14. quickscale_cli-0.76.0/src/quickscale_cli/module_catalog.py +82 -0
  15. {quickscale_cli-0.74.2 → quickscale_cli-0.76.0}/src/quickscale_cli/schema/config_schema.py +112 -27
  16. {quickscale_cli-0.74.2 → quickscale_cli-0.76.0}/src/quickscale_cli/schema/state_schema.py +32 -4
  17. {quickscale_cli-0.74.2 → quickscale_cli-0.76.0}/src/quickscale_cli/utils/dependency_utils.py +3 -3
  18. {quickscale_cli-0.74.2 → quickscale_cli-0.76.0}/src/quickscale_cli/utils/docker_utils.py +4 -4
  19. quickscale_cli-0.76.0/src/quickscale_cli/utils/module_wiring_manager.py +115 -0
  20. quickscale_cli-0.76.0/src/quickscale_cli/utils/project_identity.py +99 -0
  21. {quickscale_cli-0.74.2 → quickscale_cli-0.76.0}/src/quickscale_cli/utils/project_manager.py +5 -5
  22. {quickscale_cli-0.74.2 → quickscale_cli-0.76.0}/src/quickscale_cli/utils/railway_utils.py +144 -44
  23. quickscale_cli-0.74.2/PKG-INFO +0 -376
  24. quickscale_cli-0.74.2/README.md +0 -350
  25. quickscale_cli-0.74.2/src/quickscale_cli/commands/module_config.py +0 -906
  26. {quickscale_cli-0.74.2 → quickscale_cli-0.76.0}/src/quickscale_cli/__init__.py +0 -0
  27. {quickscale_cli-0.74.2 → quickscale_cli-0.76.0}/src/quickscale_cli/commands/__init__.py +0 -0
  28. {quickscale_cli-0.74.2 → quickscale_cli-0.76.0}/src/quickscale_cli/main.py +0 -0
  29. {quickscale_cli-0.74.2 → quickscale_cli-0.76.0}/src/quickscale_cli/schema/__init__.py +0 -0
  30. {quickscale_cli-0.74.2 → quickscale_cli-0.76.0}/src/quickscale_cli/schema/delta.py +0 -0
  31. {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
- [tool.poetry]
1
+ [project]
2
2
  name = "quickscale-cli"
3
- version = "0.74.2"
3
+ version = "0.76.0"
4
4
  description = "Command-line interface for QuickScale Django project generator"
5
- authors = ["Experto AI <victor@experto.ai>"]
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.12"
32
+ python = "^3.14"
27
33
  click = "^8.3.1"
28
- quickscale-core = "^0.74.2"
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.74.2"
2
+ __version__ = "0.76.0"
@@ -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(f"⏳ Generating project: {config.project.name}...")
93
-
94
- # Validate theme availability
95
- if config.project.theme == "showcase_htmx":
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
- generator.generate(config.project.name, output_path)
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(project_path: Path, module_name: str) -> bool:
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("\n💡 Create a configuration with: quickscale plan <name>", err=True)
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, project_name: str) -> 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 == project_name:
394
+ if config_path.parent.name == project_slug:
374
395
  return config_path.parent
375
- return Path.cwd() / project_name
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.name}")
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.name
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
- ) -> list[str]:
545
- """Embed modules and return list of successfully embedded 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(output_path, module_name):
555
- click.secho(
556
- f"⚠️ Module embedding failed for {module_name}, continuing...",
557
- fg="yellow",
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
- name=qs_config.project.name,
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, qs_config: QuickScaleConfig, no_docker: bool
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.name}")
712
+ click.echo(f" cd {qs_config.project.slug}")
637
713
 
638
714
  if qs_config.docker.start and not no_docker:
639
- click.echo(" # Docker services should be running")
640
- click.echo(" quickscale logs web # View logs")
641
- click.echo(" quickscale ps # Check status")
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.name)
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
- embedded_modules = _embed_modules_step(
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
- # Run post-generation steps
722
- _run_post_generation_steps(ctx.output_path)
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
- if not _apply_mutable_config(ctx.output_path, ctx.delta, ctx.manifests):
727
- click.secho("⚠️ Some config changes failed to apply", fg="yellow")
902
+ click.secho(
903
+ "✅ Mutable configuration changes applied via managed wiring",
904
+ fg="green",
905
+ )
728
906
 
729
907
  # Start Docker
730
- if not no_docker and ctx.qs_config.docker.start:
731
- if not _start_docker(
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
- click.secho("⚠️ Docker start failed, continuing...", fg="yellow")
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. Run poetry install
801
- 6. Run migrations
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))