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.
Files changed (28) hide show
  1. {quickscale_cli-0.74.2 → quickscale_cli-0.75.0}/PKG-INFO +10 -6
  2. {quickscale_cli-0.74.2 → quickscale_cli-0.75.0}/README.md +8 -4
  3. {quickscale_cli-0.74.2 → quickscale_cli-0.75.0}/pyproject.toml +2 -2
  4. {quickscale_cli-0.74.2 → quickscale_cli-0.75.0}/src/quickscale_cli/_version.py +1 -1
  5. {quickscale_cli-0.74.2 → quickscale_cli-0.75.0}/src/quickscale_cli/commands/apply_command.py +202 -41
  6. {quickscale_cli-0.74.2 → quickscale_cli-0.75.0}/src/quickscale_cli/commands/deployment_commands.py +53 -15
  7. {quickscale_cli-0.74.2 → quickscale_cli-0.75.0}/src/quickscale_cli/commands/development_commands.py +17 -6
  8. {quickscale_cli-0.74.2 → quickscale_cli-0.75.0}/src/quickscale_cli/commands/module_commands.py +55 -20
  9. {quickscale_cli-0.74.2 → quickscale_cli-0.75.0}/src/quickscale_cli/commands/module_config.py +326 -263
  10. quickscale_cli-0.75.0/src/quickscale_cli/commands/module_wiring_specs.py +199 -0
  11. {quickscale_cli-0.74.2 → quickscale_cli-0.75.0}/src/quickscale_cli/commands/plan_command.py +187 -71
  12. {quickscale_cli-0.74.2 → quickscale_cli-0.75.0}/src/quickscale_cli/commands/remove_command.py +27 -82
  13. {quickscale_cli-0.74.2 → quickscale_cli-0.75.0}/src/quickscale_cli/commands/status_command.py +6 -3
  14. quickscale_cli-0.75.0/src/quickscale_cli/module_catalog.py +77 -0
  15. {quickscale_cli-0.74.2 → quickscale_cli-0.75.0}/src/quickscale_cli/schema/config_schema.py +95 -24
  16. {quickscale_cli-0.74.2 → quickscale_cli-0.75.0}/src/quickscale_cli/schema/state_schema.py +32 -4
  17. quickscale_cli-0.75.0/src/quickscale_cli/utils/module_wiring_manager.py +115 -0
  18. quickscale_cli-0.75.0/src/quickscale_cli/utils/project_identity.py +99 -0
  19. {quickscale_cli-0.74.2 → quickscale_cli-0.75.0}/src/quickscale_cli/utils/project_manager.py +5 -5
  20. {quickscale_cli-0.74.2 → quickscale_cli-0.75.0}/src/quickscale_cli/utils/railway_utils.py +133 -33
  21. {quickscale_cli-0.74.2 → quickscale_cli-0.75.0}/src/quickscale_cli/__init__.py +0 -0
  22. {quickscale_cli-0.74.2 → quickscale_cli-0.75.0}/src/quickscale_cli/commands/__init__.py +0 -0
  23. {quickscale_cli-0.74.2 → quickscale_cli-0.75.0}/src/quickscale_cli/main.py +0 -0
  24. {quickscale_cli-0.74.2 → quickscale_cli-0.75.0}/src/quickscale_cli/schema/__init__.py +0 -0
  25. {quickscale_cli-0.74.2 → quickscale_cli-0.75.0}/src/quickscale_cli/schema/delta.py +0 -0
  26. {quickscale_cli-0.74.2 → quickscale_cli-0.75.0}/src/quickscale_cli/utils/__init__.py +0 -0
  27. {quickscale_cli-0.74.2 → quickscale_cli-0.75.0}/src/quickscale_cli/utils/dependency_utils.py +0 -0
  28. {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.74.2
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.74.2,<0.75.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
  [![Coverage](https://img.shields.io/badge/coverage-90%25-green.svg)](https://github.com/Experto-AI/quickscale/actions/workflows/ci.yml)
31
31
  [![Python 3.12+](https://img.shields.io/badge/python-3.12+-blue.svg)](https://www.python.org/downloads/)
32
32
  [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
33
- [![Version](https://img.shields.io/badge/version-0.74.0-green.svg)](https://github.com/Experto-AI/quickscale/releases)
33
+ [![GitHub release (latest by date)](https://img.shields.io/github/v/release/Experto-AI/quickscale?color=green)](https://github.com/Experto-AI/quickscale/releases)
34
34
  [![Code style: Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](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 (showcase_html), no modules, Docker enabled
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 (web + database)
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 web
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
  [![Coverage](https://img.shields.io/badge/coverage-90%25-green.svg)](https://github.com/Experto-AI/quickscale/actions/workflows/ci.yml)
6
6
  [![Python 3.12+](https://img.shields.io/badge/python-3.12+-blue.svg)](https://www.python.org/downloads/)
7
7
  [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
8
- [![Version](https://img.shields.io/badge/version-0.74.0-green.svg)](https://github.com/Experto-AI/quickscale/releases)
8
+ [![GitHub release (latest by date)](https://img.shields.io/github/v/release/Experto-AI/quickscale?color=green)](https://github.com/Experto-AI/quickscale/releases)
9
9
  [![Code style: Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](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 (showcase_html), no modules, Docker enabled
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 (web + database)
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 web
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.74.2"
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.74.2"
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.74.2"
2
+ __version__ = "0.75.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,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(f"⏳ Generating project: {config.project.name}...")
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
- generator.generate(config.project.name, output_path)
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(project_path: Path, module_name: str) -> bool:
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("\n💡 Create a configuration with: quickscale plan <name>", err=True)
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, project_name: str) -> 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 == project_name:
411
+ if config_path.parent.name == project_slug:
374
412
  return config_path.parent
375
- return Path.cwd() / project_name
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.name}")
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.name
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
- ) -> list[str]:
545
- """Embed modules and return list of successfully embedded 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(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
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
- name=qs_config.project.name,
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, qs_config: QuickScaleConfig, no_docker: bool
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.name}")
690
+ click.echo(f" cd {qs_config.project.slug}")
637
691
 
638
692
  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")
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.name)
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
- embedded_modules = _embed_modules_step(
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
- # Run post-generation steps
722
- _run_post_generation_steps(ctx.output_path)
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
- if not _apply_mutable_config(ctx.output_path, ctx.delta, ctx.manifests):
727
- click.secho("⚠️ Some config changes failed to apply", fg="yellow")
873
+ click.secho(
874
+ "✅ Mutable configuration changes applied via managed wiring",
875
+ fg="green",
876
+ )
728
877
 
729
878
  # Start Docker
730
- if not no_docker and ctx.qs_config.docker.start:
731
- if not _start_docker(
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. Run poetry install
801
- 6. Run migrations
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))