quickscale-cli 0.83.0__tar.gz → 0.85.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 (40) hide show
  1. quickscale_cli-0.85.0/PKG-INFO +43 -0
  2. quickscale_cli-0.85.0/README.md +19 -0
  3. {quickscale_cli-0.83.0 → quickscale_cli-0.85.0}/pyproject.toml +5 -4
  4. {quickscale_cli-0.83.0 → quickscale_cli-0.85.0}/src/quickscale_cli/_version.py +1 -1
  5. {quickscale_cli-0.83.0 → quickscale_cli-0.85.0}/src/quickscale_cli/backups_contract.py +3 -0
  6. quickscale_cli-0.85.0/src/quickscale_cli/billing_contract.py +183 -0
  7. {quickscale_cli-0.83.0 → quickscale_cli-0.85.0}/src/quickscale_cli/commands/apply_command.py +42 -2
  8. {quickscale_cli-0.83.0 → quickscale_cli-0.85.0}/src/quickscale_cli/commands/development_commands.py +33 -8
  9. {quickscale_cli-0.83.0 → quickscale_cli-0.85.0}/src/quickscale_cli/commands/module_commands.py +10 -3
  10. {quickscale_cli-0.83.0 → quickscale_cli-0.85.0}/src/quickscale_cli/commands/module_config.py +116 -0
  11. {quickscale_cli-0.83.0 → quickscale_cli-0.85.0}/src/quickscale_cli/commands/plan_command.py +8 -3
  12. {quickscale_cli-0.83.0 → quickscale_cli-0.85.0}/src/quickscale_cli/commands/status_command.py +12 -6
  13. {quickscale_cli-0.83.0 → quickscale_cli-0.85.0}/src/quickscale_cli/module_catalog.py +9 -7
  14. {quickscale_cli-0.83.0 → quickscale_cli-0.85.0}/src/quickscale_cli/schema/config_schema.py +81 -12
  15. {quickscale_cli-0.83.0 → quickscale_cli-0.85.0}/src/quickscale_cli/utils/dependency_utils.py +8 -25
  16. {quickscale_cli-0.83.0 → quickscale_cli-0.85.0}/src/quickscale_cli/utils/docker_utils.py +12 -10
  17. quickscale_cli-0.83.0/PKG-INFO +0 -61
  18. quickscale_cli-0.83.0/README.md +0 -38
  19. {quickscale_cli-0.83.0 → quickscale_cli-0.85.0}/src/quickscale_cli/__init__.py +0 -0
  20. {quickscale_cli-0.83.0 → quickscale_cli-0.85.0}/src/quickscale_cli/analytics_contract.py +0 -0
  21. {quickscale_cli-0.83.0 → quickscale_cli-0.85.0}/src/quickscale_cli/auth_contract.py +0 -0
  22. {quickscale_cli-0.83.0 → quickscale_cli-0.85.0}/src/quickscale_cli/beta_migration.py +0 -0
  23. {quickscale_cli-0.83.0 → quickscale_cli-0.85.0}/src/quickscale_cli/commands/__init__.py +0 -0
  24. {quickscale_cli-0.83.0 → quickscale_cli-0.85.0}/src/quickscale_cli/commands/deployment_commands.py +0 -0
  25. {quickscale_cli-0.83.0 → quickscale_cli-0.85.0}/src/quickscale_cli/commands/dr_commands.py +0 -0
  26. {quickscale_cli-0.83.0 → quickscale_cli-0.85.0}/src/quickscale_cli/commands/module_wiring_specs.py +0 -0
  27. {quickscale_cli-0.83.0 → quickscale_cli-0.85.0}/src/quickscale_cli/commands/remove_command.py +0 -0
  28. {quickscale_cli-0.83.0 → quickscale_cli-0.85.0}/src/quickscale_cli/crm_contract.py +0 -0
  29. {quickscale_cli-0.83.0 → quickscale_cli-0.85.0}/src/quickscale_cli/main.py +0 -0
  30. {quickscale_cli-0.83.0 → quickscale_cli-0.85.0}/src/quickscale_cli/notifications_contract.py +0 -0
  31. {quickscale_cli-0.83.0 → quickscale_cli-0.85.0}/src/quickscale_cli/schema/__init__.py +0 -0
  32. {quickscale_cli-0.83.0 → quickscale_cli-0.85.0}/src/quickscale_cli/schema/delta.py +0 -0
  33. {quickscale_cli-0.83.0 → quickscale_cli-0.85.0}/src/quickscale_cli/schema/state_schema.py +0 -0
  34. {quickscale_cli-0.83.0 → quickscale_cli-0.85.0}/src/quickscale_cli/social_contract.py +0 -0
  35. {quickscale_cli-0.83.0 → quickscale_cli-0.85.0}/src/quickscale_cli/utils/__init__.py +0 -0
  36. {quickscale_cli-0.83.0 → quickscale_cli-0.85.0}/src/quickscale_cli/utils/module_dependency_sync.py +0 -0
  37. {quickscale_cli-0.83.0 → quickscale_cli-0.85.0}/src/quickscale_cli/utils/module_wiring_manager.py +0 -0
  38. {quickscale_cli-0.83.0 → quickscale_cli-0.85.0}/src/quickscale_cli/utils/project_identity.py +0 -0
  39. {quickscale_cli-0.83.0 → quickscale_cli-0.85.0}/src/quickscale_cli/utils/project_manager.py +0 -0
  40. {quickscale_cli-0.83.0 → quickscale_cli-0.85.0}/src/quickscale_cli/utils/railway_utils.py +0 -0
@@ -0,0 +1,43 @@
1
+ Metadata-Version: 2.4
2
+ Name: quickscale-cli
3
+ Version: 0.85.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.13,<3.15
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Programming Language :: Python :: 3.14
15
+ Classifier: Framework :: Django
16
+ Classifier: Framework :: Django :: 6.0
17
+ Requires-Dist: click (>=8.3.1,<9.0.0)
18
+ Requires-Dist: quickscale-core (>=0.85.0,<0.86.0)
19
+ Project-URL: Documentation, https://github.com/Experto-AI/quickscale/tree/main/docs
20
+ Project-URL: Homepage, https://github.com/Experto-AI/quickscale
21
+ Project-URL: Repository, https://github.com/Experto-AI/quickscale
22
+ Description-Content-Type: text/markdown
23
+
24
+ # quickscale-cli
25
+
26
+ Command-line interface for the [QuickScale](https://github.com/Experto-AI/quickscale) Django project generator.
27
+
28
+ ## Installation
29
+
30
+ ```bash
31
+ pip install quickscale-cli
32
+ ```
33
+
34
+ Or install the full bundle:
35
+
36
+ ```bash
37
+ pip install quickscale
38
+ ```
39
+
40
+ ## Documentation
41
+
42
+ See the [main repository README](../README.md) and [docs/](../docs/) for full documentation.
43
+
@@ -0,0 +1,19 @@
1
+ # quickscale-cli
2
+
3
+ Command-line interface for the [QuickScale](https://github.com/Experto-AI/quickscale) Django project generator.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install quickscale-cli
9
+ ```
10
+
11
+ Or install the full bundle:
12
+
13
+ ```bash
14
+ pip install quickscale
15
+ ```
16
+
17
+ ## Documentation
18
+
19
+ See the [main repository README](../README.md) and [docs/](../docs/) for full documentation.
@@ -1,8 +1,8 @@
1
1
  [project]
2
2
  name = "quickscale-cli"
3
- version = "0.83.0"
3
+ version = "0.85.0"
4
4
  description = "Command-line interface for QuickScale Django project generator"
5
- requires-python = ">=3.12,<3.14"
5
+ requires-python = ">=3.13,<3.15"
6
6
  readme = "README.md"
7
7
  license = "Apache-2.0"
8
8
  authors = [{name = "Experto AI", email = "victor@experto.ai"}]
@@ -11,6 +11,7 @@ classifiers = [
11
11
  "Development Status :: 3 - Alpha",
12
12
  "Intended Audience :: Developers",
13
13
  "Programming Language :: Python :: 3",
14
+ "Programming Language :: Python :: 3.13",
14
15
  "Programming Language :: Python :: 3.14",
15
16
  "Framework :: Django",
16
17
  "Framework :: Django :: 6.0",
@@ -29,9 +30,9 @@ quickscale = "quickscale_cli.main:cli"
29
30
  packages = [{include = "quickscale_cli", from = "src"}]
30
31
 
31
32
  [tool.poetry.dependencies]
32
- python = ">=3.12,<3.14"
33
+ python = ">=3.13,<3.15"
33
34
  click = "^8.3.1"
34
- quickscale-core = "^0.83.0"
35
+ quickscale-core = "^0.85.0"
35
36
 
36
37
  [tool.poetry.group.dev.dependencies]
37
38
  # Dev dependencies centralized in root pyproject.toml
@@ -1,2 +1,2 @@
1
1
  # Auto-generated by scripts/version_tool.sh
2
- __version__ = "0.83.0"
2
+ __version__ = "0.85.0"
@@ -10,6 +10,7 @@ from collections.abc import Mapping
10
10
  import re
11
11
  from typing import Any
12
12
 
13
+ from quickscale_cli.billing_contract import normalize_billing_module_options
13
14
  from quickscale_cli.analytics_contract import normalize_analytics_module_options
14
15
  from quickscale_cli.auth_contract import normalize_auth_module_options
15
16
  from quickscale_cli.crm_contract import normalize_crm_module_options
@@ -116,6 +117,8 @@ def sanitize_module_options(
116
117
  return normalize_auth_module_options(options)
117
118
  if module_name == "backups":
118
119
  return normalize_backups_module_options(options)
120
+ if module_name == "billing":
121
+ return normalize_billing_module_options(options)
119
122
  if module_name == "crm":
120
123
  return normalize_crm_module_options(options)
121
124
  if module_name == "notifications":
@@ -0,0 +1,183 @@
1
+ """Shared billing-module configuration helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping
6
+ import re
7
+ from typing import Any
8
+
9
+ DEFAULT_BILLING_PUBLISHABLE_KEY_ENV_VAR = "STRIPE_PUBLISHABLE_KEY"
10
+ DEFAULT_BILLING_SECRET_KEY_ENV_VAR = "STRIPE_SECRET_KEY"
11
+ DEFAULT_BILLING_WEBHOOK_SECRET_ENV_VAR = "QUICKSCALE_BILLING_WEBHOOK_SECRET"
12
+ DEFAULT_BILLING_CURRENCY = "usd"
13
+
14
+ BILLING_ENV_VAR_OPTION_NAMES = (
15
+ "publishable_key_env_var",
16
+ "secret_key_env_var",
17
+ "webhook_secret_env_var",
18
+ )
19
+ BILLING_MODULE_OPTION_KEYS = frozenset(
20
+ {
21
+ "enabled",
22
+ *BILLING_ENV_VAR_OPTION_NAMES,
23
+ "billing_currency",
24
+ }
25
+ )
26
+ BILLING_SUPPORTED_CURRENCIES = (
27
+ "aud",
28
+ "brl",
29
+ "cad",
30
+ "chf",
31
+ "czk",
32
+ "dkk",
33
+ "eur",
34
+ "gbp",
35
+ "hkd",
36
+ "huf",
37
+ "inr",
38
+ "jpy",
39
+ "mxn",
40
+ "myr",
41
+ "nok",
42
+ "nzd",
43
+ "php",
44
+ "pln",
45
+ "ron",
46
+ "sek",
47
+ "sgd",
48
+ "thb",
49
+ "try",
50
+ "usd",
51
+ "zar",
52
+ )
53
+
54
+ _BILLING_ENV_VAR_NAME_PATTERN = re.compile(r"^[A-Z][A-Z0-9_]*$")
55
+
56
+
57
+ def default_billing_module_options() -> dict[str, Any]:
58
+ """Return the default planner/apply contract for billing."""
59
+ return {
60
+ "enabled": True,
61
+ "publishable_key_env_var": DEFAULT_BILLING_PUBLISHABLE_KEY_ENV_VAR,
62
+ "secret_key_env_var": DEFAULT_BILLING_SECRET_KEY_ENV_VAR,
63
+ "webhook_secret_env_var": DEFAULT_BILLING_WEBHOOK_SECRET_ENV_VAR,
64
+ "billing_currency": DEFAULT_BILLING_CURRENCY,
65
+ }
66
+
67
+
68
+ def normalize_billing_module_options(
69
+ options: Mapping[str, Any] | None,
70
+ ) -> dict[str, Any]:
71
+ """Return billing options with normalized env-var and currency values."""
72
+ normalized = dict(options or {})
73
+
74
+ for option_name in BILLING_ENV_VAR_OPTION_NAMES:
75
+ if option_name in normalized:
76
+ normalized[option_name] = str(normalized[option_name]).strip()
77
+
78
+ if "billing_currency" in normalized:
79
+ normalized["billing_currency"] = (
80
+ str(normalized["billing_currency"]).strip().lower()
81
+ )
82
+
83
+ return normalized
84
+
85
+
86
+ def resolve_billing_module_options(
87
+ options: Mapping[str, Any] | None,
88
+ ) -> dict[str, Any]:
89
+ """Merge billing options with defaults and normalized overrides."""
90
+ resolved = default_billing_module_options()
91
+ resolved.update(normalize_billing_module_options(options))
92
+ for option_name in BILLING_ENV_VAR_OPTION_NAMES:
93
+ resolved[option_name] = str(resolved[option_name]).strip()
94
+ resolved["billing_currency"] = str(resolved["billing_currency"]).strip().lower()
95
+ return resolved
96
+
97
+
98
+ def validate_billing_env_var_reference(option_name: str, value: Any) -> str | None:
99
+ """Validate a billing env-var reference field."""
100
+ candidate = str(value).strip()
101
+ if not candidate:
102
+ return None
103
+
104
+ qualified_option = f"modules.billing.{option_name}"
105
+ if not _BILLING_ENV_VAR_NAME_PATTERN.fullmatch(candidate):
106
+ return (
107
+ f"{qualified_option} must be an environment variable name matching "
108
+ "^[A-Z][A-Z0-9_]*$"
109
+ )
110
+ return None
111
+
112
+
113
+ def validate_billing_currency(value: Any) -> str | None:
114
+ """Validate the configured billing currency code."""
115
+ candidate = str(value).strip().lower()
116
+ if not candidate:
117
+ return "modules.billing.billing_currency cannot be blank"
118
+
119
+ if candidate not in BILLING_SUPPORTED_CURRENCIES:
120
+ return (
121
+ "modules.billing.billing_currency must be one of the supported "
122
+ "QuickScale billing currency codes: "
123
+ + ", ".join(BILLING_SUPPORTED_CURRENCIES)
124
+ )
125
+ return None
126
+
127
+
128
+ def validate_billing_module_options(options: Mapping[str, Any] | None) -> list[str]:
129
+ """Return validation issues for billing module options."""
130
+ resolved = resolve_billing_module_options(options)
131
+ issues: list[str] = []
132
+
133
+ if not isinstance(resolved.get("enabled", True), bool):
134
+ issues.append("modules.billing.enabled must be a boolean")
135
+
136
+ for option_name in BILLING_ENV_VAR_OPTION_NAMES:
137
+ issue = validate_billing_env_var_reference(
138
+ option_name,
139
+ resolved.get(option_name, ""),
140
+ )
141
+ if issue:
142
+ issues.append(issue)
143
+
144
+ currency_issue = validate_billing_currency(resolved.get("billing_currency", ""))
145
+ if currency_issue:
146
+ issues.append(currency_issue)
147
+
148
+ return issues
149
+
150
+
151
+ def billing_production_targeted(options: Mapping[str, Any] | None) -> bool:
152
+ """Return whether billing is configured for live runtime ownership."""
153
+ resolved = resolve_billing_module_options(options)
154
+ if not bool(resolved.get("enabled", True)):
155
+ return False
156
+
157
+ if validate_billing_currency(resolved.get("billing_currency", "")):
158
+ return False
159
+
160
+ for option_name in BILLING_ENV_VAR_OPTION_NAMES:
161
+ candidate = str(resolved.get(option_name, "")).strip()
162
+ if not candidate or validate_billing_env_var_reference(option_name, candidate):
163
+ return False
164
+
165
+ return True
166
+
167
+
168
+ __all__ = [
169
+ "BILLING_ENV_VAR_OPTION_NAMES",
170
+ "BILLING_MODULE_OPTION_KEYS",
171
+ "BILLING_SUPPORTED_CURRENCIES",
172
+ "DEFAULT_BILLING_CURRENCY",
173
+ "DEFAULT_BILLING_PUBLISHABLE_KEY_ENV_VAR",
174
+ "DEFAULT_BILLING_SECRET_KEY_ENV_VAR",
175
+ "DEFAULT_BILLING_WEBHOOK_SECRET_ENV_VAR",
176
+ "billing_production_targeted",
177
+ "default_billing_module_options",
178
+ "normalize_billing_module_options",
179
+ "resolve_billing_module_options",
180
+ "validate_billing_currency",
181
+ "validate_billing_env_var_reference",
182
+ "validate_billing_module_options",
183
+ ]
@@ -21,6 +21,7 @@ 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
25
  from quickscale_cli.backups_contract import (
25
26
  BACKUPS_REMOTE_ACCESS_KEY_ID_ENV_VAR_OPTION,
26
27
  BACKUPS_REMOTE_SECRET_ACCESS_KEY_ENV_VAR_OPTION,
@@ -470,13 +471,13 @@ def _start_docker(
470
471
 
471
472
 
472
473
  def _abort_for_not_ready_modules(module_names: list[str], *, source: str) -> None:
473
- """Abort apply when placeholder modules appear in config or applied state."""
474
+ """Abort apply when non-public-ready modules appear in config or applied state."""
474
475
  not_ready = find_not_ready_modules(module_names)
475
476
  if not not_ready:
476
477
  return
477
478
 
478
479
  click.secho(
479
- f"\n❌ {source} references placeholder modules that are not ready:",
480
+ f"\n❌ {source} references modules that are not public-ready:",
480
481
  fg="red",
481
482
  err=True,
482
483
  bold=True,
@@ -720,6 +721,45 @@ def _load_and_validate_config(config_path: Path) -> QuickScaleConfig:
720
721
 
721
722
  def _validate_module_prerequisites(qs_config: QuickScaleConfig) -> None:
722
723
  """Validate actionable module-specific prerequisites before apply proceeds."""
724
+ billing_config = qs_config.modules.get("billing")
725
+ if billing_config is not None:
726
+ billing_issues = validate_billing_module_options(billing_config.options or {})
727
+ if billing_issues:
728
+ click.secho(
729
+ "\n❌ Billing module configuration is incomplete for apply:",
730
+ fg="red",
731
+ err=True,
732
+ )
733
+ for issue in billing_issues:
734
+ click.echo(f" • {issue}", err=True)
735
+ click.echo(
736
+ "\n💡 Re-run 'quickscale plan --reconfigure --configure-modules' or "
737
+ "edit quickscale.yml to correct the billing option values. "
738
+ "Billing apply requires valid env-var references and a supported "
739
+ "currency code.",
740
+ err=True,
741
+ )
742
+ raise click.Abort()
743
+
744
+ if "auth" not in qs_config.modules:
745
+ click.secho(
746
+ "\n❌ Billing requires the auth module before apply can continue:",
747
+ fg="red",
748
+ err=True,
749
+ )
750
+ click.echo(
751
+ " • Add 'auth' under modules in quickscale.yml before applying billing.",
752
+ err=True,
753
+ )
754
+ click.echo(
755
+ "\n💡 Billing relies on the QuickScale auth login flow and "
756
+ "AUTH_USER_MODEL. Re-run 'quickscale plan --reconfigure "
757
+ "--configure-modules' or edit quickscale.yml to add auth, then "
758
+ "re-run 'quickscale apply'.",
759
+ err=True,
760
+ )
761
+ raise click.Abort()
762
+
723
763
  _abort_for_not_ready_modules(
724
764
  list(qs_config.modules.keys()), source="quickscale.yml"
725
765
  )
@@ -10,6 +10,7 @@ import click
10
10
 
11
11
  from quickscale_cli.schema.config_schema import QuickScaleConfig
12
12
  from quickscale_cli.utils.docker_utils import (
13
+ DockerComposePluginRequiredError,
13
14
  get_docker_compose_command,
14
15
  get_port_from_env,
15
16
  is_docker_running,
@@ -74,8 +75,32 @@ def _show_port_conflict_error(port: int | str) -> None:
74
75
  )
75
76
 
76
77
 
78
+ def _show_compose_v2_required_error() -> None:
79
+ """Display a user-friendly error when Docker Compose v2 is unavailable."""
80
+ click.secho(
81
+ "❌ Error: Docker Compose v2 plugin is required",
82
+ fg="red",
83
+ err=True,
84
+ )
85
+ click.echo(
86
+ "💡 Tip: Install or update Docker so the 'docker compose' command is "
87
+ "available. QuickScale keeps the generated 'docker-compose.yml' file "
88
+ "name, but the CLI uses the Docker Compose v2 plugin.",
89
+ err=True,
90
+ )
91
+
92
+
93
+ def _require_docker_compose_command() -> list[str]:
94
+ """Return the Docker Compose v2 command or exit with remediation text."""
95
+ try:
96
+ return get_docker_compose_command()
97
+ except DockerComposePluginRequiredError:
98
+ _show_compose_v2_required_error()
99
+ sys.exit(1)
100
+
101
+
77
102
  def _run_docker_compose_up(compose_cmd: list, build: bool, no_cache: bool) -> None:
78
- """Execute docker-compose up with appropriate flags."""
103
+ """Execute docker compose up with appropriate flags."""
79
104
  if build or no_cache:
80
105
  cmd = compose_cmd + ["--progress", "plain", "up", "-d"]
81
106
  else:
@@ -101,7 +126,7 @@ def _run_docker_compose_up(compose_cmd: list, build: bool, no_cache: bool) -> No
101
126
 
102
127
 
103
128
  def _handle_up_error(error: subprocess.CalledProcessError) -> None:
104
- """Handle docker-compose up errors with user-friendly messages."""
129
+ """Handle docker compose up errors with user-friendly messages."""
105
130
  error_output = error.stderr if error.stderr else ""
106
131
  stdout_output = error.stdout if error.stdout else ""
107
132
  full_output = error_output + stdout_output
@@ -122,7 +147,7 @@ def _handle_up_error(error: subprocess.CalledProcessError) -> None:
122
147
  click.echo(
123
148
  f"\n💡 To resolve this issue, try one of the following:\n"
124
149
  f" 1. Stop existing containers: quickscale down\n"
125
- f" 2. Remove orphaned containers: docker-compose down --remove-orphans\n"
150
+ f" 2. Remove orphaned containers: docker compose down --remove-orphans\n"
126
151
  f" 3. Find and kill the process: lsof -ti:{conflict_port} | xargs kill -9\n"
127
152
  f" 4. Find process details: sudo lsof -i:{conflict_port}\n"
128
153
  f" 5. Or use: sudo fuser -k {conflict_port}/tcp",
@@ -272,14 +297,14 @@ def up(build: bool, no_cache: bool) -> None:
272
297
  )
273
298
  click.secho(" quickscale down && quickscale up --build\n", fg="cyan")
274
299
 
275
- # Check if required port is available BEFORE calling docker-compose
300
+ # Check if required port is available BEFORE calling docker compose.
276
301
  port = get_port_from_env()
277
302
  if not is_port_available(port):
278
303
  _show_port_conflict_error(port)
279
304
  sys.exit(1)
280
305
 
281
306
  try:
282
- compose_cmd = get_docker_compose_command()
307
+ compose_cmd = _require_docker_compose_command()
283
308
  _run_docker_compose_up(compose_cmd, should_build, no_cache)
284
309
 
285
310
  # Update build timestamp if build was performed
@@ -326,7 +351,7 @@ def down(volumes: bool) -> None:
326
351
  _validate_project_and_docker()
327
352
 
328
353
  try:
329
- compose_cmd = get_docker_compose_command()
354
+ compose_cmd = _require_docker_compose_command()
330
355
  cmd = compose_cmd + ["down", "--remove-orphans"]
331
356
 
332
357
  if volumes:
@@ -468,7 +493,7 @@ def logs(service: str | None, follow: bool, tail: str | None, timestamps: bool)
468
493
  _validate_project_and_docker()
469
494
 
470
495
  try:
471
- compose_cmd = get_docker_compose_command()
496
+ compose_cmd = _require_docker_compose_command()
472
497
  cmd = compose_cmd + ["logs"]
473
498
 
474
499
  if follow:
@@ -503,7 +528,7 @@ def ps() -> None:
503
528
  _validate_project_and_docker()
504
529
 
505
530
  try:
506
- compose_cmd = get_docker_compose_command()
531
+ compose_cmd = _require_docker_compose_command()
507
532
  cmd = compose_cmd + ["ps"]
508
533
  subprocess.run(cmd, check=True)
509
534
 
@@ -208,8 +208,12 @@ def _sync_state_module_version(project_path: Path, module: str, version: str) ->
208
208
  state_manager.save(state)
209
209
 
210
210
 
211
- def _validate_module_readiness(module: str) -> bool:
212
- """Reject placeholder modules from embed/apply flows."""
211
+ def _validate_module_readiness(
212
+ module: str,
213
+ *,
214
+ execution_mode: ModuleExecutionMode = STANDALONE_MODULE_EXECUTION_MODE,
215
+ ) -> bool:
216
+ """Reject non-public modules from standalone embed/update flows."""
213
217
  readiness_reason = get_module_readiness_reason(module)
214
218
  if readiness_reason is None:
215
219
  return True
@@ -416,7 +420,10 @@ def embed_module(
416
420
  if not _validate_module_not_exists(project_path, module):
417
421
  return False
418
422
 
419
- if not _validate_module_readiness(module):
423
+ if not _validate_module_readiness(
424
+ module,
425
+ execution_mode=execution_mode,
426
+ ):
420
427
  return False
421
428
 
422
429
  branch = f"splits/{module}-module"
@@ -21,6 +21,11 @@ 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 (
25
+ default_billing_module_options,
26
+ resolve_billing_module_options,
27
+ validate_billing_module_options,
28
+ )
24
29
  from quickscale_cli.auth_contract import (
25
30
  default_auth_module_options,
26
31
  resolve_auth_module_options,
@@ -1722,6 +1727,116 @@ def apply_analytics_configuration(
1722
1727
  )
1723
1728
 
1724
1729
 
1730
+ # ============================================================================
1731
+ # BILLING MODULE CONFIGURATION
1732
+ # ============================================================================
1733
+
1734
+
1735
+ def get_default_billing_config() -> dict[str, Any]:
1736
+ """Return default configuration for the billing module."""
1737
+ return default_billing_module_options()
1738
+
1739
+
1740
+ def _raise_for_invalid_billing_config(config: Mapping[str, Any]) -> None:
1741
+ """Abort with actionable messaging when billing config is invalid."""
1742
+ issues = validate_billing_module_options(config)
1743
+ if not issues:
1744
+ return
1745
+
1746
+ click.secho("\n❌ Invalid billing module configuration:", fg="red", err=True)
1747
+ for issue in issues:
1748
+ click.echo(f" • {issue}", err=True)
1749
+ raise click.Abort()
1750
+
1751
+
1752
+ def configure_billing_module(
1753
+ non_interactive: bool = False,
1754
+ existing_config: Mapping[str, Any] | None = None,
1755
+ ) -> dict[str, Any]:
1756
+ """Configure billing module settings interactively or with defaults."""
1757
+ defaults = resolve_billing_module_options(existing_config)
1758
+
1759
+ if non_interactive:
1760
+ click.echo("\n⚙️ Using default billing module configuration...")
1761
+ click.echo(" • Runtime: " + ("Enabled" if defaults["enabled"] else "Disabled"))
1762
+ click.echo(
1763
+ " • Publishable key env var: " + str(defaults["publishable_key_env_var"])
1764
+ )
1765
+ click.echo(" • Secret key env var: " + str(defaults["secret_key_env_var"]))
1766
+ click.echo(
1767
+ " • Webhook secret env var: " + str(defaults["webhook_secret_env_var"])
1768
+ )
1769
+ click.echo(" • Billing currency: " + str(defaults["billing_currency"]))
1770
+ _raise_for_invalid_billing_config(defaults)
1771
+ return defaults
1772
+
1773
+ click.echo("\n⚙️ Configuring billing module...")
1774
+ click.echo(
1775
+ "Billing is public-ready in quickscale plan, quickscale.yml, and "
1776
+ "quickscale apply flows. These settings configure the managed Stripe "
1777
+ "runtime, while quickscale apply still requires auth before billing "
1778
+ "can be enabled.\n"
1779
+ )
1780
+
1781
+ config = resolve_billing_module_options(
1782
+ {
1783
+ "enabled": click.confirm(
1784
+ "Enable the billing runtime?",
1785
+ default=bool(defaults["enabled"]),
1786
+ ),
1787
+ "publishable_key_env_var": click.prompt(
1788
+ "Stripe publishable key environment variable",
1789
+ default=str(defaults["publishable_key_env_var"]),
1790
+ show_default=True,
1791
+ ).strip(),
1792
+ "secret_key_env_var": click.prompt(
1793
+ "Stripe secret key environment variable",
1794
+ default=str(defaults["secret_key_env_var"]),
1795
+ show_default=True,
1796
+ ).strip(),
1797
+ "webhook_secret_env_var": click.prompt(
1798
+ "Stripe webhook secret environment variable",
1799
+ default=str(defaults["webhook_secret_env_var"]),
1800
+ show_default=True,
1801
+ ).strip(),
1802
+ "billing_currency": click.prompt(
1803
+ "Billing currency (ISO 4217)",
1804
+ default=str(defaults["billing_currency"]),
1805
+ show_default=True,
1806
+ ).strip(),
1807
+ }
1808
+ )
1809
+
1810
+ _raise_for_invalid_billing_config(config)
1811
+ return config
1812
+
1813
+
1814
+ def apply_billing_configuration(
1815
+ project_path: Path,
1816
+ config: dict[str, Any],
1817
+ *,
1818
+ execution_mode: ModuleExecutionMode = STANDALONE_MODULE_EXECUTION_MODE,
1819
+ ) -> None:
1820
+ """Apply billing module configuration via managed wiring files."""
1821
+ resolved = resolve_billing_module_options(config)
1822
+ _raise_for_invalid_billing_config(resolved)
1823
+ _regenerate_wiring_for_execution_mode(
1824
+ project_path,
1825
+ "billing",
1826
+ resolved,
1827
+ execution_mode=execution_mode,
1828
+ )
1829
+
1830
+ click.echo("\n📋 Configuration applied:")
1831
+ click.echo(" • Runtime: " + ("Enabled" if resolved["enabled"] else "Disabled"))
1832
+ click.echo(
1833
+ " • Publishable key env var: " + str(resolved["publishable_key_env_var"])
1834
+ )
1835
+ click.echo(" • Secret key env var: " + str(resolved["secret_key_env_var"]))
1836
+ click.echo(" • Webhook secret env var: " + str(resolved["webhook_secret_env_var"]))
1837
+ click.echo(" • Billing currency: " + str(resolved["billing_currency"]))
1838
+
1839
+
1725
1840
  # ============================================================================
1726
1841
  # SOCIAL MODULE CONFIGURATION
1727
1842
  # ============================================================================
@@ -1870,6 +1985,7 @@ MODULE_CONFIGURATORS = {
1870
1985
  "forms": (configure_forms_module, apply_forms_configuration),
1871
1986
  "storage": (configure_storage_module, apply_storage_configuration),
1872
1987
  "backups": (configure_backups_module, apply_backups_configuration),
1988
+ "billing": (configure_billing_module, apply_billing_configuration),
1873
1989
  "notifications": (
1874
1990
  configure_notifications_module,
1875
1991
  apply_notifications_configuration,
@@ -52,7 +52,12 @@ def _get_module_choices(
52
52
  def _format_module_choice(entry: ModuleCatalogEntry) -> str:
53
53
  """Format a catalog entry for interactive display."""
54
54
  if not entry.ready:
55
- return f"{entry.name} - {entry.description} (placeholder, not ready)"
55
+ if entry.name == "billing":
56
+ return (
57
+ f"{entry.name} - {entry.description} "
58
+ "(internal packaged Phase 1 foundation, not public-ready)"
59
+ )
60
+ return f"{entry.name} - {entry.description} (placeholder, not public-ready)"
56
61
  if entry.experimental:
57
62
  return f"{entry.name} - {entry.description} (experimental)"
58
63
  return f"{entry.name} - {entry.description}"
@@ -171,7 +176,7 @@ def _select_modules(*, include_experimental: bool = False) -> list[str]:
171
176
 
172
177
  if include_experimental and any(not entry.ready for entry in available_modules):
173
178
  click.echo(
174
- "\n Placeholder modules are shown for visibility only and cannot be selected"
179
+ "\n Non-public modules are shown for visibility only and cannot be selected"
175
180
  )
176
181
 
177
182
  click.echo(
@@ -1051,7 +1056,7 @@ def _save_config_with_validation(yaml_content: str, output_path: Path) -> None:
1051
1056
  @click.option(
1052
1057
  "--include-experimental",
1053
1058
  is_flag=True,
1054
- help="Show placeholder module directories (billing, teams) in the picker",
1059
+ help="Show non-public module inventory entries (currently teams) in the picker",
1055
1060
  )
1056
1061
  @click.option(
1057
1062
  "--configure-modules",
@@ -120,13 +120,13 @@ def _load_module_manifests(
120
120
 
121
121
 
122
122
  def _abort_for_not_ready_modules(module_names: list[str], *, source: str) -> None:
123
- """Abort status when placeholder modules appear in config or applied state."""
123
+ """Abort status when non-public-ready modules appear in config or applied state."""
124
124
  not_ready = find_not_ready_modules(module_names)
125
125
  if not not_ready:
126
126
  return
127
127
 
128
128
  click.secho(
129
- f"\n❌ Unsupported placeholder module state detected in {source}:",
129
+ f"\n❌ Unsupported non-public-ready module state detected in {source}:",
130
130
  fg="red",
131
131
  err=True,
132
132
  bold=True,
@@ -136,11 +136,17 @@ def _abort_for_not_ready_modules(module_names: list[str], *, source: str) -> Non
136
136
  if reason is not None:
137
137
  click.echo(f" • {reason}", err=True)
138
138
 
139
- click.echo(
140
- "\n💡 Billing and teams remain placeholder directories and cannot "
141
- "participate in public QuickScale plan/apply/status workflows yet.",
142
- err=True,
139
+ guidance = (
140
+ "\n💡 Resolve the non-public-ready module state above before running "
141
+ "'quickscale status' again."
143
142
  )
143
+ if "teams" in not_ready:
144
+ guidance += (
145
+ " Teams remains placeholder inventory and cannot participate in "
146
+ "public QuickScale workflows yet."
147
+ )
148
+
149
+ click.echo(guidance, err=True)
144
150
  raise click.Abort()
145
151
 
146
152
 
@@ -67,9 +67,8 @@ MODULE_CATALOG: tuple[ModuleCatalogEntry, ...] = (
67
67
  ),
68
68
  ModuleCatalogEntry(
69
69
  name="billing",
70
- description="Stripe integration",
71
- ready=False,
72
- experimental=True,
70
+ description="Stripe integration with credits-first pricing and dashboard routes",
71
+ ready=True,
73
72
  ),
74
73
  ModuleCatalogEntry(
75
74
  name="teams",
@@ -114,13 +113,16 @@ def find_not_ready_modules(module_names: Iterable[str]) -> list[str]:
114
113
 
115
114
 
116
115
  def get_module_readiness_reason(module_name: str) -> str | None:
117
- """Return an actionable readiness error for placeholder modules."""
116
+ """Return an actionable readiness error for non-public-ready modules."""
118
117
  entry = get_module_entry(module_name)
119
118
  if entry is None or entry.ready:
120
119
  return None
121
120
 
121
+ module_display_name = module_name.replace("_", " ").title()
122
122
  return (
123
- f"Module '{module_name}' is a placeholder and is not a valid shipped-module "
124
- "selection yet. Billing and teams remain discoverable in docs and module "
125
- "inventory only until they ship."
123
+ f"Module '{module_name}' remains placeholder inventory only and is not a "
124
+ "public-ready QuickScale module yet. "
125
+ f"{module_display_name} remains excluded from public quickscale plan, "
126
+ "quickscale.yml, quickscale apply, and quickscale status workflows until "
127
+ "it ships."
126
128
  )
@@ -11,6 +11,12 @@ from typing import Any
11
11
 
12
12
  import yaml
13
13
 
14
+ from quickscale_cli.billing_contract import (
15
+ BILLING_ENV_VAR_OPTION_NAMES,
16
+ BILLING_MODULE_OPTION_KEYS,
17
+ validate_billing_currency,
18
+ validate_billing_env_var_reference,
19
+ )
14
20
  from quickscale_cli.auth_contract import (
15
21
  AUTH_AUTHENTICATION_METHOD_OPTION,
16
22
  AUTH_AUTHENTICATION_METHOD_VALUES,
@@ -239,6 +245,67 @@ def _validate_auth_module_options(
239
245
  )
240
246
 
241
247
 
248
+ def _validate_billing_module_options(
249
+ module_options: dict[str, Any],
250
+ yaml_content: str,
251
+ ) -> None:
252
+ """Validate billing config keys and value shapes before readiness gating."""
253
+ billing_option_keys = set(BILLING_MODULE_OPTION_KEYS)
254
+
255
+ for option_name in module_options:
256
+ if option_name not in billing_option_keys:
257
+ line = _find_line_number(yaml_content, option_name)
258
+ suggestion = _suggest_similar_key(option_name, billing_option_keys)
259
+ suggestion_text = (
260
+ f"Use modules.billing.{suggestion} instead."
261
+ if suggestion
262
+ else "Supported keys: "
263
+ + ", ".join(
264
+ f"modules.billing.{key}" for key in sorted(billing_option_keys)
265
+ )
266
+ )
267
+ raise ConfigValidationError(
268
+ f"Unknown key '{option_name}' in modules.billing section",
269
+ line=line,
270
+ suggestion=suggestion_text,
271
+ )
272
+
273
+ if "enabled" in module_options and not isinstance(module_options["enabled"], bool):
274
+ raise ConfigValidationError(
275
+ "'modules.billing.enabled' must be a boolean (true/false)",
276
+ line=_find_line_number(yaml_content, "enabled"),
277
+ )
278
+
279
+ for option_name in BILLING_ENV_VAR_OPTION_NAMES:
280
+ if option_name not in module_options:
281
+ continue
282
+
283
+ issue = validate_billing_env_var_reference(
284
+ option_name,
285
+ module_options[option_name],
286
+ )
287
+ if issue:
288
+ raise ConfigValidationError(
289
+ issue,
290
+ line=_find_line_number(yaml_content, option_name),
291
+ suggestion=(
292
+ "Use uppercase environment-variable names such as "
293
+ "STRIPE_SECRET_KEY."
294
+ ),
295
+ )
296
+
297
+ if "billing_currency" in module_options:
298
+ currency_issue = validate_billing_currency(
299
+ module_options["billing_currency"],
300
+ )
301
+ if currency_issue:
302
+ raise ConfigValidationError(
303
+ currency_issue,
304
+ line=_find_line_number(yaml_content, "billing_currency"),
305
+ suggestion="Use a supported ISO 4217 code such as usd or eur.",
306
+ )
307
+
308
+
242
309
  def _validate_version(data: dict, yaml_content: str) -> None:
243
310
  """Validate the version field."""
244
311
  if "version" not in data:
@@ -426,18 +493,6 @@ def _validate_modules_section(data: dict, yaml_content: str) -> dict[str, Module
426
493
  or f"Available modules: {', '.join(sorted(AVAILABLE_MODULES))}",
427
494
  )
428
495
 
429
- readiness_reason = get_module_readiness_reason(module_name)
430
- if readiness_reason is not None:
431
- line = _find_line_number(yaml_content, module_name)
432
- raise ConfigValidationError(
433
- readiness_reason,
434
- line=line,
435
- suggestion=(
436
- "Remove it from quickscale.yml and choose a shipped module: "
437
- + ", ".join(sorted(READY_MODULES))
438
- ),
439
- )
440
-
441
496
  if module_options is None:
442
497
  module_options = {}
443
498
  elif not isinstance(module_options, dict):
@@ -449,6 +504,20 @@ def _validate_modules_section(data: dict, yaml_content: str) -> dict[str, Module
449
504
 
450
505
  if module_name == "auth":
451
506
  _validate_auth_module_options(module_options, yaml_content)
507
+ if module_name == "billing":
508
+ _validate_billing_module_options(module_options, yaml_content)
509
+
510
+ readiness_reason = get_module_readiness_reason(module_name)
511
+ if readiness_reason is not None:
512
+ line = _find_line_number(yaml_content, module_name)
513
+ raise ConfigValidationError(
514
+ readiness_reason,
515
+ line=line,
516
+ suggestion=(
517
+ "Remove it from quickscale.yml and choose a shipped module: "
518
+ + ", ".join(sorted(READY_MODULES))
519
+ ),
520
+ )
452
521
 
453
522
  modules[module_name] = ModuleConfig(name=module_name, options=module_options)
454
523
 
@@ -42,13 +42,13 @@ class DependencyStatus(NamedTuple):
42
42
 
43
43
 
44
44
  def check_python_version() -> DependencyStatus:
45
- """Check Python version (3.14+ required)."""
45
+ """Check Python version (3.13+ required)."""
46
46
  import sys
47
47
 
48
48
  version = (
49
49
  f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
50
50
  )
51
- meets_requirement = sys.version_info >= (3, 14)
51
+ meets_requirement = sys.version_info >= (3, 13)
52
52
 
53
53
  return DependencyStatus(
54
54
  name="Python",
@@ -151,7 +151,7 @@ def check_git_installed() -> DependencyStatus:
151
151
 
152
152
 
153
153
  def check_docker_installed() -> DependencyStatus:
154
- """Check if Docker and Docker Compose are installed."""
154
+ """Check if Docker and the Docker Compose v2 plugin are installed."""
155
155
 
156
156
  if _should_skip_dependency_checks():
157
157
  return _skipped_dependency(
@@ -176,45 +176,28 @@ def check_docker_installed() -> DependencyStatus:
176
176
  parts = version_str.split()
177
177
  version = parts[2].rstrip(",") if len(parts) >= 3 else None
178
178
 
179
- # Also check for Docker Compose (either v2 plugin or v1 standalone)
180
- compose_available = False
179
+ # Docker-backed workflows require the Compose v2 plugin.
181
180
  try:
182
- # Try docker compose v2 (plugin)
183
181
  subprocess.run(
184
182
  ["docker", "compose", "version"],
185
183
  capture_output=True,
186
184
  check=True,
187
185
  timeout=2,
188
186
  )
189
- compose_available = True
190
187
  except (
191
188
  subprocess.SubprocessError,
192
189
  FileNotFoundError,
193
190
  subprocess.TimeoutExpired,
194
191
  ):
195
- # Try docker-compose v1 (standalone)
196
- try:
197
- subprocess.run(
198
- ["docker-compose", "--version"],
199
- capture_output=True,
200
- check=True,
201
- timeout=2,
202
- )
203
- compose_available = True
204
- except (
205
- subprocess.SubprocessError,
206
- FileNotFoundError,
207
- subprocess.TimeoutExpired,
208
- ):
209
- pass
210
-
211
- if not compose_available:
212
192
  return DependencyStatus(
213
193
  name="Docker",
214
194
  installed=False,
215
195
  version=version,
216
196
  required=False,
217
- purpose="Containerized development (quickscale up/down) - Docker Compose not found",
197
+ purpose=(
198
+ "Containerized development (quickscale up/down) - "
199
+ "Docker Compose v2 plugin not found"
200
+ ),
218
201
  )
219
202
 
220
203
  return DependencyStatus(
@@ -7,6 +7,10 @@ import time
7
7
  from pathlib import Path
8
8
 
9
9
 
10
+ class DockerComposePluginRequiredError(RuntimeError):
11
+ """Raised when the Docker Compose v2 plugin is unavailable."""
12
+
13
+
10
14
  def is_interactive() -> bool:
11
15
  """Check if running in an interactive terminal (has TTY)."""
12
16
  return sys.stdout.isatty() and sys.stdin.isatty()
@@ -32,24 +36,22 @@ def find_docker_compose() -> Path | None:
32
36
 
33
37
 
34
38
  def get_docker_compose_command() -> list[str]:
35
- """Get the appropriate docker compose command.
36
-
37
- Tries 'docker compose' (v2 plugin) first, falls back to 'docker-compose' (v1 standalone).
38
- Both are fully supported.
39
- """
40
- # Try docker compose first (v2 plugin)
39
+ """Get the Docker Compose v2 command."""
41
40
  try:
42
41
  subprocess.run(
43
42
  ["docker", "compose", "version"], capture_output=True, check=True, timeout=2
44
43
  )
45
- return ["docker", "compose"]
46
44
  except (
47
45
  subprocess.SubprocessError,
48
46
  FileNotFoundError,
49
47
  subprocess.TimeoutExpired,
50
- ):
51
- # Fall back to docker-compose (v1 standalone)
52
- return ["docker-compose"]
48
+ ) as error:
49
+ raise DockerComposePluginRequiredError(
50
+ "Docker Compose v2 is required. Install or update Docker so the "
51
+ "'docker compose' command is available."
52
+ ) from error
53
+
54
+ return ["docker", "compose"]
53
55
 
54
56
 
55
57
  def get_container_status(container_name: str) -> str | None:
@@ -1,61 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: quickscale-cli
3
- Version: 0.83.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.12,<3.14
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.83.0,<0.84.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, recover, and operate generated projects.
28
-
29
- ## Command groups
30
-
31
- - Project lifecycle: `plan`, `apply`, `status`, `remove`
32
- - Disaster recovery & promotion: `dr capture`, `dr plan`, `dr execute`, `dr report`
33
- - Local development: `up`, `down`, `ps`, `logs`, `shell`, `manage`
34
- - Deployment: `deploy`
35
- - Module workflows: `update`, `push`
36
-
37
- ## Relationship to other packages
38
-
39
- - `quickscale-cli` is the command surface
40
- - `quickscale-core` contains the underlying scaffolding and template logic
41
- - `quickscale` installs both together for convenience
42
-
43
- ## Install
44
-
45
- Usually installed via the meta-package:
46
-
47
- ```bash
48
- pip install quickscale
49
- ```
50
-
51
- Standalone package installs can use this package's published metadata and dependency constraints.
52
-
53
- ## Documentation
54
-
55
- - Repository overview: [QuickScale README](https://github.com/Experto-AI/quickscale/blob/main/README.md)
56
- - Start guide: [START_HERE.md](https://github.com/Experto-AI/quickscale/blob/main/START_HERE.md)
57
- - Technical decisions: [docs/technical/decisions.md](https://github.com/Experto-AI/quickscale/blob/main/docs/technical/decisions.md)
58
- - CLI source entrypoint: [src/quickscale_cli/main.py](https://github.com/Experto-AI/quickscale/blob/main/quickscale_cli/src/quickscale_cli/main.py)
59
-
60
- This README is package-local context only. Root documentation remains authoritative for repo-wide policy and scope.
61
-
@@ -1,38 +0,0 @@
1
- # quickscale-cli
2
-
3
- Command-line interface package for QuickScale.
4
-
5
- This package exposes the `quickscale` command used to plan, apply, inspect, recover, and operate generated projects.
6
-
7
- ## Command groups
8
-
9
- - Project lifecycle: `plan`, `apply`, `status`, `remove`
10
- - Disaster recovery & promotion: `dr capture`, `dr plan`, `dr execute`, `dr report`
11
- - Local development: `up`, `down`, `ps`, `logs`, `shell`, `manage`
12
- - Deployment: `deploy`
13
- - Module workflows: `update`, `push`
14
-
15
- ## Relationship to other packages
16
-
17
- - `quickscale-cli` is the command surface
18
- - `quickscale-core` contains the underlying scaffolding and template logic
19
- - `quickscale` installs both together for convenience
20
-
21
- ## Install
22
-
23
- Usually installed via the meta-package:
24
-
25
- ```bash
26
- pip install quickscale
27
- ```
28
-
29
- Standalone package installs can use this package's published metadata and dependency constraints.
30
-
31
- ## Documentation
32
-
33
- - Repository overview: [QuickScale README](https://github.com/Experto-AI/quickscale/blob/main/README.md)
34
- - Start guide: [START_HERE.md](https://github.com/Experto-AI/quickscale/blob/main/START_HERE.md)
35
- - Technical decisions: [docs/technical/decisions.md](https://github.com/Experto-AI/quickscale/blob/main/docs/technical/decisions.md)
36
- - CLI source entrypoint: [src/quickscale_cli/main.py](https://github.com/Experto-AI/quickscale/blob/main/quickscale_cli/src/quickscale_cli/main.py)
37
-
38
- This README is package-local context only. Root documentation remains authoritative for repo-wide policy and scope.