quickscale-cli 0.85.0__tar.gz → 0.86.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 (39) hide show
  1. {quickscale_cli-0.85.0 → quickscale_cli-0.86.0}/PKG-INFO +2 -2
  2. {quickscale_cli-0.85.0 → quickscale_cli-0.86.0}/pyproject.toml +2 -2
  3. {quickscale_cli-0.85.0 → quickscale_cli-0.86.0}/src/quickscale_cli/_version.py +1 -1
  4. {quickscale_cli-0.85.0 → quickscale_cli-0.86.0}/src/quickscale_cli/commands/apply_command.py +156 -6
  5. quickscale_cli-0.86.0/src/quickscale_cli/commands/implied_module_defaults.py +23 -0
  6. {quickscale_cli-0.85.0 → quickscale_cli-0.86.0}/src/quickscale_cli/commands/module_config.py +10 -0
  7. {quickscale_cli-0.85.0 → quickscale_cli-0.86.0}/src/quickscale_cli/commands/module_wiring_specs.py +54 -0
  8. {quickscale_cli-0.85.0 → quickscale_cli-0.86.0}/src/quickscale_cli/commands/plan_command.py +48 -0
  9. {quickscale_cli-0.85.0 → quickscale_cli-0.86.0}/src/quickscale_cli/module_catalog.py +5 -0
  10. {quickscale_cli-0.85.0 → quickscale_cli-0.86.0}/README.md +0 -0
  11. {quickscale_cli-0.85.0 → quickscale_cli-0.86.0}/src/quickscale_cli/__init__.py +0 -0
  12. {quickscale_cli-0.85.0 → quickscale_cli-0.86.0}/src/quickscale_cli/analytics_contract.py +0 -0
  13. {quickscale_cli-0.85.0 → quickscale_cli-0.86.0}/src/quickscale_cli/auth_contract.py +0 -0
  14. {quickscale_cli-0.85.0 → quickscale_cli-0.86.0}/src/quickscale_cli/backups_contract.py +0 -0
  15. {quickscale_cli-0.85.0 → quickscale_cli-0.86.0}/src/quickscale_cli/beta_migration.py +0 -0
  16. {quickscale_cli-0.85.0 → quickscale_cli-0.86.0}/src/quickscale_cli/billing_contract.py +0 -0
  17. {quickscale_cli-0.85.0 → quickscale_cli-0.86.0}/src/quickscale_cli/commands/__init__.py +0 -0
  18. {quickscale_cli-0.85.0 → quickscale_cli-0.86.0}/src/quickscale_cli/commands/deployment_commands.py +0 -0
  19. {quickscale_cli-0.85.0 → quickscale_cli-0.86.0}/src/quickscale_cli/commands/development_commands.py +0 -0
  20. {quickscale_cli-0.85.0 → quickscale_cli-0.86.0}/src/quickscale_cli/commands/dr_commands.py +0 -0
  21. {quickscale_cli-0.85.0 → quickscale_cli-0.86.0}/src/quickscale_cli/commands/module_commands.py +0 -0
  22. {quickscale_cli-0.85.0 → quickscale_cli-0.86.0}/src/quickscale_cli/commands/remove_command.py +0 -0
  23. {quickscale_cli-0.85.0 → quickscale_cli-0.86.0}/src/quickscale_cli/commands/status_command.py +0 -0
  24. {quickscale_cli-0.85.0 → quickscale_cli-0.86.0}/src/quickscale_cli/crm_contract.py +0 -0
  25. {quickscale_cli-0.85.0 → quickscale_cli-0.86.0}/src/quickscale_cli/main.py +0 -0
  26. {quickscale_cli-0.85.0 → quickscale_cli-0.86.0}/src/quickscale_cli/notifications_contract.py +0 -0
  27. {quickscale_cli-0.85.0 → quickscale_cli-0.86.0}/src/quickscale_cli/schema/__init__.py +0 -0
  28. {quickscale_cli-0.85.0 → quickscale_cli-0.86.0}/src/quickscale_cli/schema/config_schema.py +0 -0
  29. {quickscale_cli-0.85.0 → quickscale_cli-0.86.0}/src/quickscale_cli/schema/delta.py +0 -0
  30. {quickscale_cli-0.85.0 → quickscale_cli-0.86.0}/src/quickscale_cli/schema/state_schema.py +0 -0
  31. {quickscale_cli-0.85.0 → quickscale_cli-0.86.0}/src/quickscale_cli/social_contract.py +0 -0
  32. {quickscale_cli-0.85.0 → quickscale_cli-0.86.0}/src/quickscale_cli/utils/__init__.py +0 -0
  33. {quickscale_cli-0.85.0 → quickscale_cli-0.86.0}/src/quickscale_cli/utils/dependency_utils.py +0 -0
  34. {quickscale_cli-0.85.0 → quickscale_cli-0.86.0}/src/quickscale_cli/utils/docker_utils.py +0 -0
  35. {quickscale_cli-0.85.0 → quickscale_cli-0.86.0}/src/quickscale_cli/utils/module_dependency_sync.py +0 -0
  36. {quickscale_cli-0.85.0 → quickscale_cli-0.86.0}/src/quickscale_cli/utils/module_wiring_manager.py +0 -0
  37. {quickscale_cli-0.85.0 → quickscale_cli-0.86.0}/src/quickscale_cli/utils/project_identity.py +0 -0
  38. {quickscale_cli-0.85.0 → quickscale_cli-0.86.0}/src/quickscale_cli/utils/project_manager.py +0 -0
  39. {quickscale_cli-0.85.0 → quickscale_cli-0.86.0}/src/quickscale_cli/utils/railway_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: quickscale-cli
3
- Version: 0.85.0
3
+ Version: 0.86.0
4
4
  Summary: Command-line interface for QuickScale Django project generator
5
5
  License-Expression: Apache-2.0
6
6
  Keywords: django,cli,project-generator,saas
@@ -15,7 +15,7 @@ Classifier: Programming Language :: Python :: 3.14
15
15
  Classifier: Framework :: Django
16
16
  Classifier: Framework :: Django :: 6.0
17
17
  Requires-Dist: click (>=8.3.1,<9.0.0)
18
- Requires-Dist: quickscale-core (>=0.85.0,<0.86.0)
18
+ Requires-Dist: quickscale-core (>=0.86.0,<0.87.0)
19
19
  Project-URL: Documentation, https://github.com/Experto-AI/quickscale/tree/main/docs
20
20
  Project-URL: Homepage, https://github.com/Experto-AI/quickscale
21
21
  Project-URL: Repository, https://github.com/Experto-AI/quickscale
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "quickscale-cli"
3
- version = "0.85.0"
3
+ version = "0.86.0"
4
4
  description = "Command-line interface for QuickScale Django project generator"
5
5
  requires-python = ">=3.13,<3.15"
6
6
  readme = "README.md"
@@ -32,7 +32,7 @@ packages = [{include = "quickscale_cli", from = "src"}]
32
32
  [tool.poetry.dependencies]
33
33
  python = ">=3.13,<3.15"
34
34
  click = "^8.3.1"
35
- quickscale-core = "^0.85.0"
35
+ quickscale-core = "^0.86.0"
36
36
 
37
37
  [tool.poetry.group.dev.dependencies]
38
38
  # Dev dependencies centralized in root pyproject.toml
@@ -1,2 +1,2 @@
1
1
  # Auto-generated by scripts/version_tool.sh
2
- __version__ = "0.85.0"
2
+ __version__ = "0.86.0"
@@ -21,7 +21,14 @@ from quickscale_cli.analytics_contract import (
21
21
  resolve_analytics_module_options,
22
22
  validate_analytics_module_options,
23
23
  )
24
- from quickscale_cli.billing_contract import validate_billing_module_options
24
+ from quickscale_cli.billing_contract import (
25
+ DEFAULT_BILLING_CURRENCY,
26
+ DEFAULT_BILLING_PUBLISHABLE_KEY_ENV_VAR,
27
+ DEFAULT_BILLING_SECRET_KEY_ENV_VAR,
28
+ DEFAULT_BILLING_WEBHOOK_SECRET_ENV_VAR,
29
+ resolve_billing_module_options,
30
+ validate_billing_module_options,
31
+ )
25
32
  from quickscale_cli.backups_contract import (
26
33
  BACKUPS_REMOTE_ACCESS_KEY_ID_ENV_VAR_OPTION,
27
34
  BACKUPS_REMOTE_SECRET_ACCESS_KEY_ENV_VAR_OPTION,
@@ -40,6 +47,9 @@ from quickscale_cli.notifications_contract import (
40
47
  resolve_notifications_module_options,
41
48
  validate_notifications_module_options,
42
49
  )
50
+ from quickscale_cli.commands.implied_module_defaults import (
51
+ get_implied_module_default_configs,
52
+ )
43
53
  from quickscale_cli.module_catalog import (
44
54
  find_not_ready_modules,
45
55
  get_module_readiness_reason,
@@ -64,6 +74,7 @@ from quickscale_cli.utils.module_dependency_sync import (
64
74
  from quickscale_cli.utils.module_wiring_manager import regenerate_managed_wiring
65
75
  from quickscale_cli.schema.config_schema import (
66
76
  ConfigValidationError,
77
+ ModuleConfig,
67
78
  QuickScaleConfig,
68
79
  generate_yaml,
69
80
  validate_config,
@@ -681,6 +692,21 @@ def _sanitize_loaded_module_configs(qs_config: QuickScaleConfig) -> list[str]:
681
692
  return sanitized_modules
682
693
 
683
694
 
695
+ def _materialize_implied_module_configs(qs_config: QuickScaleConfig) -> list[str]:
696
+ """Add explicit module config blocks required by selected modules."""
697
+ implied_configs = get_implied_module_default_configs(qs_config.modules.keys())
698
+ if not implied_configs:
699
+ return []
700
+
701
+ for module_name, options in implied_configs.items():
702
+ qs_config.modules[module_name] = ModuleConfig(
703
+ name=module_name,
704
+ options=sanitize_module_options(module_name, options),
705
+ )
706
+
707
+ return list(implied_configs.keys())
708
+
709
+
684
710
  def _load_and_validate_config(config_path: Path) -> QuickScaleConfig:
685
711
  """Load and validate configuration from file."""
686
712
  if not config_path.exists():
@@ -699,16 +725,24 @@ def _load_and_validate_config(config_path: Path) -> QuickScaleConfig:
699
725
  qs_config = validate_config(yaml_content)
700
726
  original_data = yaml.safe_load(yaml_content) or {}
701
727
  original_modules = original_data.get("modules") or {}
702
- _sanitize_loaded_module_configs(qs_config)
728
+ sanitized_modules = _sanitize_loaded_module_configs(qs_config)
729
+ implied_modules = _materialize_implied_module_configs(qs_config)
703
730
  normalized_yaml = generate_yaml(qs_config)
704
731
  normalized_data = yaml.safe_load(normalized_yaml) or {}
705
732
  normalized_modules = normalized_data.get("modules") or {}
706
733
  if normalized_modules != original_modules:
707
734
  config_path.write_text(normalized_yaml)
708
- click.secho(
709
- "✅ Sanitized legacy module config keys in quickscale.yml",
710
- fg="green",
711
- )
735
+ if sanitized_modules:
736
+ click.secho(
737
+ "✅ Sanitized legacy module config keys in quickscale.yml",
738
+ fg="green",
739
+ )
740
+ if implied_modules:
741
+ click.secho(
742
+ "✅ Added implied module config to quickscale.yml: "
743
+ + ", ".join(implied_modules),
744
+ fg="green",
745
+ )
712
746
  _validate_module_prerequisites(qs_config)
713
747
  return qs_config
714
748
  except ConfigValidationError as e:
@@ -721,6 +755,25 @@ def _load_and_validate_config(config_path: Path) -> QuickScaleConfig:
721
755
 
722
756
  def _validate_module_prerequisites(qs_config: QuickScaleConfig) -> None:
723
757
  """Validate actionable module-specific prerequisites before apply proceeds."""
758
+ if "orgs" in qs_config.modules and "auth" not in qs_config.modules:
759
+ click.secho(
760
+ "\n❌ Organizations requires the auth module before apply can continue:",
761
+ fg="red",
762
+ err=True,
763
+ )
764
+ click.echo(
765
+ " • Add 'auth' under modules in quickscale.yml before applying orgs.",
766
+ err=True,
767
+ )
768
+ click.echo(
769
+ "\n💡 Organizations relies on the QuickScale auth login flow and "
770
+ "django-allauth account adapter hooks. Re-run 'quickscale plan "
771
+ "--reconfigure --configure-modules' or edit quickscale.yml to add "
772
+ "auth, then re-run 'quickscale apply'.",
773
+ err=True,
774
+ )
775
+ raise click.Abort()
776
+
724
777
  billing_config = qs_config.modules.get("billing")
725
778
  if billing_config is not None:
726
779
  billing_issues = validate_billing_module_options(billing_config.options or {})
@@ -1044,6 +1097,95 @@ def _sync_analytics_env_example(
1044
1097
  return True
1045
1098
 
1046
1099
 
1100
+ def _render_billing_env_example_block(
1101
+ options: Mapping[str, Any] | None,
1102
+ ) -> str:
1103
+ """Render the managed billing section for `.env.example`."""
1104
+ resolved = resolve_billing_module_options(options)
1105
+ publishable_key_env_var = str(
1106
+ resolved.get(
1107
+ "publishable_key_env_var",
1108
+ DEFAULT_BILLING_PUBLISHABLE_KEY_ENV_VAR,
1109
+ )
1110
+ or DEFAULT_BILLING_PUBLISHABLE_KEY_ENV_VAR
1111
+ ).strip()
1112
+ secret_key_env_var = str(
1113
+ resolved.get(
1114
+ "secret_key_env_var",
1115
+ DEFAULT_BILLING_SECRET_KEY_ENV_VAR,
1116
+ )
1117
+ or DEFAULT_BILLING_SECRET_KEY_ENV_VAR
1118
+ ).strip()
1119
+ webhook_secret_env_var = str(
1120
+ resolved.get(
1121
+ "webhook_secret_env_var",
1122
+ DEFAULT_BILLING_WEBHOOK_SECRET_ENV_VAR,
1123
+ )
1124
+ or DEFAULT_BILLING_WEBHOOK_SECRET_ENV_VAR
1125
+ ).strip()
1126
+ billing_currency = str(
1127
+ resolved.get("billing_currency", DEFAULT_BILLING_CURRENCY)
1128
+ or DEFAULT_BILLING_CURRENCY
1129
+ ).strip()
1130
+
1131
+ return "\n".join(
1132
+ [
1133
+ "# QuickScale Billing (managed)",
1134
+ "# Runtime Stripe variables for the backend billing module.",
1135
+ f"# Billing currency from quickscale.yml: {billing_currency}",
1136
+ f"{publishable_key_env_var}=",
1137
+ f"{secret_key_env_var}=",
1138
+ f"{webhook_secret_env_var}=",
1139
+ "# End QuickScale Billing",
1140
+ ]
1141
+ )
1142
+
1143
+
1144
+ def _sync_billing_env_example(
1145
+ output_path: Path,
1146
+ qs_config: QuickScaleConfig,
1147
+ ) -> bool:
1148
+ """Keep `.env.example` aligned with the billing env-var names."""
1149
+ billing_config = qs_config.modules.get("billing")
1150
+ env_example_path = output_path / ".env.example"
1151
+ if billing_config is None or not env_example_path.exists():
1152
+ return True
1153
+
1154
+ start_marker = "# QuickScale Billing (managed)"
1155
+ end_marker = "# End QuickScale Billing"
1156
+ rendered_block = _render_billing_env_example_block(billing_config.options or {})
1157
+
1158
+ try:
1159
+ content = env_example_path.read_text()
1160
+ except OSError as e:
1161
+ click.secho(
1162
+ f"⚠️ Failed to read .env.example for billing wiring: {e}",
1163
+ fg="yellow",
1164
+ )
1165
+ return False
1166
+
1167
+ if start_marker in content and end_marker in content:
1168
+ before, remainder = content.split(start_marker, maxsplit=1)
1169
+ _, after = remainder.split(end_marker, maxsplit=1)
1170
+ replacement = rendered_block + after
1171
+ updated_content = before + replacement
1172
+ else:
1173
+ suffix = "" if content.endswith("\n") else "\n"
1174
+ updated_content = content + suffix + "\n" + rendered_block + "\n"
1175
+
1176
+ try:
1177
+ env_example_path.write_text(updated_content)
1178
+ except OSError as e:
1179
+ click.secho(
1180
+ f"⚠️ Failed to update .env.example for billing wiring: {e}",
1181
+ fg="yellow",
1182
+ )
1183
+ return False
1184
+
1185
+ click.secho("✅ Updated .env.example with billing env vars", fg="green")
1186
+ return True
1187
+
1188
+
1047
1189
  def _normalize_backups_gitignore_entry(local_directory: str) -> str | None:
1048
1190
  """Return a safe repo-relative ignore entry for backup artifacts."""
1049
1191
  if any(
@@ -2219,6 +2361,14 @@ def _execute_apply_steps(
2219
2361
  reason="Unable to update .env.example with the configured analytics env-var names.",
2220
2362
  )
2221
2363
 
2364
+ if not _sync_billing_env_example(ctx.output_path, ctx.qs_config):
2365
+ _abort_after_post_embed_failure(
2366
+ ctx,
2367
+ embedded_modules,
2368
+ failed_step="billing env example sync",
2369
+ reason="Unable to update .env.example with the configured billing env-var names.",
2370
+ )
2371
+
2222
2372
  if not _sync_project_module_dependencies_for_apply(
2223
2373
  ctx.output_path,
2224
2374
  ctx.qs_config,
@@ -0,0 +1,23 @@
1
+ """Helpers for materializing implicit module configs in CLI flows."""
2
+
3
+ from collections.abc import Collection
4
+ from typing import Any
5
+
6
+ from quickscale_cli.commands.module_config import get_default_notifications_config
7
+
8
+
9
+ def get_implied_module_default_configs(
10
+ module_names: Collection[str],
11
+ ) -> dict[str, dict[str, Any]]:
12
+ """Return module config blocks that should be made explicit automatically."""
13
+ names = set(module_names)
14
+ implied_configs: dict[str, dict[str, Any]] = {}
15
+
16
+ if "billing" in names and "orgs" not in names:
17
+ implied_configs["orgs"] = {}
18
+ names.add("orgs")
19
+
20
+ if "orgs" in names and "notifications" not in names:
21
+ implied_configs["notifications"] = get_default_notifications_config()
22
+
23
+ return implied_configs
@@ -1973,6 +1973,16 @@ def apply_social_configuration(
1973
1973
  click.echo(" • Providers: " + ", ".join(list(resolved["provider_allowlist"])))
1974
1974
 
1975
1975
 
1976
+ # ============================================================================
1977
+ # ORGS MODULE
1978
+ # ============================================================================
1979
+
1980
+
1981
+ def get_default_orgs_config() -> dict[str, Any]:
1982
+ """Return default configuration for the orgs module."""
1983
+ return {"mode": "solo"}
1984
+
1985
+
1976
1986
  # ============================================================================
1977
1987
  # MODULE CONFIGURATORS REGISTRY
1978
1988
  # ============================================================================
@@ -16,6 +16,13 @@ from quickscale_cli.backups_contract import (
16
16
  DEFAULT_BACKUPS_REMOTE_SECRET_ACCESS_KEY_ENV_VAR,
17
17
  normalize_backups_module_options,
18
18
  )
19
+ from quickscale_cli.billing_contract import (
20
+ DEFAULT_BILLING_CURRENCY,
21
+ DEFAULT_BILLING_PUBLISHABLE_KEY_ENV_VAR,
22
+ DEFAULT_BILLING_SECRET_KEY_ENV_VAR,
23
+ DEFAULT_BILLING_WEBHOOK_SECRET_ENV_VAR,
24
+ normalize_billing_module_options,
25
+ )
19
26
  from quickscale_cli.notifications_contract import (
20
27
  NOTIFICATIONS_LIVE_EMAIL_BACKEND,
21
28
  notifications_runtime_email_backend,
@@ -106,6 +113,25 @@ def _auth_wiring(options: Mapping[str, Any]) -> ModuleWiringSpec:
106
113
  )
107
114
 
108
115
 
116
+ def _orgs_wiring(options: Mapping[str, Any]) -> ModuleWiringSpec:
117
+ mode = str(options.get("mode", "solo")).strip().lower() or "solo"
118
+ if mode not in {"solo", "saas"}:
119
+ mode = "solo"
120
+
121
+ root_include = (("", "quickscale_modules_orgs.urls"),)
122
+
123
+ return ModuleWiringSpec(
124
+ apps=("quickscale_modules_orgs",),
125
+ middleware=("quickscale_modules_orgs.middleware.TenantMiddleware",),
126
+ settings={
127
+ "ACCOUNT_ADAPTER": "quickscale_modules_orgs.adapters.OrgsAccountAdapter",
128
+ "QUICKSCALE_MODE": mode,
129
+ },
130
+ pre_home_url_includes=root_include if mode == "solo" else (),
131
+ url_includes=root_include if mode == "saas" else (),
132
+ )
133
+
134
+
109
135
  def _blog_wiring(options: Mapping[str, Any]) -> ModuleWiringSpec:
110
136
  posts_per_page = int(options.get("posts_per_page", 10))
111
137
  enable_rss = bool(options.get("enable_rss", True))
@@ -607,8 +633,36 @@ def _social_wiring(
607
633
  )
608
634
 
609
635
 
636
+ def _billing_wiring(options: Mapping[str, Any]) -> ModuleWiringSpec:
637
+ opts = normalize_billing_module_options(options)
638
+
639
+ settings: dict[str, Any] = {
640
+ "QUICKSCALE_BILLING_ENABLED": bool(opts.get("enabled", True)),
641
+ "QUICKSCALE_BILLING_PUBLISHABLE_KEY_ENV_VAR": str(
642
+ opts.get("publishable_key_env_var", DEFAULT_BILLING_PUBLISHABLE_KEY_ENV_VAR)
643
+ ),
644
+ "QUICKSCALE_BILLING_SECRET_KEY_ENV_VAR": str(
645
+ opts.get("secret_key_env_var", DEFAULT_BILLING_SECRET_KEY_ENV_VAR)
646
+ ),
647
+ "QUICKSCALE_BILLING_WEBHOOK_SECRET_ENV_VAR": str(
648
+ opts.get("webhook_secret_env_var", DEFAULT_BILLING_WEBHOOK_SECRET_ENV_VAR)
649
+ ),
650
+ "QUICKSCALE_BILLING_CURRENCY": str(
651
+ opts.get("billing_currency", DEFAULT_BILLING_CURRENCY)
652
+ ),
653
+ }
654
+
655
+ return ModuleWiringSpec(
656
+ apps=("rest_framework", "quickscale_modules_billing"),
657
+ settings=settings,
658
+ url_includes=(("", "quickscale_modules_billing.urls"),),
659
+ )
660
+
661
+
610
662
  MODULE_WIRING_BUILDERS = {
611
663
  "auth": _auth_wiring,
664
+ "orgs": _orgs_wiring,
665
+ "billing": _billing_wiring,
612
666
  "blog": _blog_wiring,
613
667
  "listings": _listings_wiring,
614
668
  "crm": _crm_wiring,
@@ -13,6 +13,9 @@ import click
13
13
 
14
14
  from quickscale_cli.auth_contract import format_auth_desired_config_contract
15
15
  from quickscale_cli.backups_contract import sanitize_module_options
16
+ from quickscale_cli.commands.implied_module_defaults import (
17
+ get_implied_module_default_configs,
18
+ )
16
19
  from quickscale_cli.commands.module_config import MODULE_CONFIGURATORS
17
20
  from quickscale_cli.module_catalog import (
18
21
  ModuleCatalogEntry,
@@ -236,6 +239,32 @@ def _merge_module_names(*module_groups: list[str]) -> list[str]:
236
239
  return merged
237
240
 
238
241
 
242
+ def _materialize_implied_module_configs(
243
+ module_names: list[str],
244
+ module_options: dict[str, dict[str, Any]],
245
+ ) -> tuple[list[str], dict[str, dict[str, Any]], list[str]]:
246
+ """Add explicit config blocks for modules that are implied by selection."""
247
+ present_module_names = _merge_module_names(
248
+ module_names, list(module_options.keys())
249
+ )
250
+ implied_configs = get_implied_module_default_configs(present_module_names)
251
+ if not implied_configs:
252
+ return present_module_names, module_options, []
253
+
254
+ merged_module_names = _merge_module_names(
255
+ present_module_names,
256
+ list(implied_configs.keys()),
257
+ )
258
+ merged_module_options = dict(module_options)
259
+ for module_name, options in implied_configs.items():
260
+ merged_module_options[module_name] = _copy_module_options(
261
+ options,
262
+ module_name=module_name,
263
+ )
264
+
265
+ return merged_module_names, merged_module_options, list(implied_configs.keys())
266
+
267
+
239
268
  def _get_existing_module_options(
240
269
  project_path: Path,
241
270
  existing_config: QuickScaleConfig | None,
@@ -655,6 +684,15 @@ def _handle_add_modules(
655
684
  for module_name in new_modules:
656
685
  existing_options.setdefault(module_name, {})
657
686
 
687
+ all_modules, existing_options, implied_modules = (
688
+ _materialize_implied_module_configs(
689
+ all_modules,
690
+ existing_options,
691
+ )
692
+ )
693
+ if implied_modules:
694
+ new_modules = _merge_module_names(new_modules, implied_modules)
695
+
658
696
  if existing_config is not None:
659
697
  docker_config = existing_config.docker
660
698
  project_config = existing_config.project
@@ -816,6 +854,11 @@ def _handle_reconfigure(
816
854
  )
817
855
  )
818
856
 
857
+ all_modules, module_options, _ = _materialize_implied_module_configs(
858
+ all_modules,
859
+ module_options,
860
+ )
861
+
819
862
  # Configure Docker
820
863
  docker_start, docker_build, docker_create_superuser = _configure_docker()
821
864
 
@@ -1134,6 +1177,11 @@ def plan(
1134
1177
  )
1135
1178
  )
1136
1179
 
1180
+ selected_modules, module_options, _ = _materialize_implied_module_configs(
1181
+ selected_modules,
1182
+ module_options,
1183
+ )
1184
+
1137
1185
  # Build configuration
1138
1186
  modules = _build_module_configs(selected_modules, module_options)
1139
1187
 
@@ -70,6 +70,11 @@ MODULE_CATALOG: tuple[ModuleCatalogEntry, ...] = (
70
70
  description="Stripe integration with credits-first pricing and dashboard routes",
71
71
  ready=True,
72
72
  ),
73
+ ModuleCatalogEntry(
74
+ name="orgs",
75
+ description="Organizations and multi-tenant foundations with memberships and invitations",
76
+ ready=True,
77
+ ),
73
78
  ModuleCatalogEntry(
74
79
  name="teams",
75
80
  description="Multi-tenancy and team management",