quickscale-cli 0.75.0__tar.gz → 0.76.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. quickscale_cli-0.76.0/PKG-INFO +60 -0
  2. quickscale_cli-0.76.0/README.md +37 -0
  3. {quickscale_cli-0.75.0 → quickscale_cli-0.76.0}/pyproject.toml +18 -19
  4. {quickscale_cli-0.75.0 → quickscale_cli-0.76.0}/src/quickscale_cli/_version.py +1 -1
  5. {quickscale_cli-0.75.0 → quickscale_cli-0.76.0}/src/quickscale_cli/commands/apply_command.py +71 -22
  6. {quickscale_cli-0.75.0 → quickscale_cli-0.76.0}/src/quickscale_cli/commands/deployment_commands.py +6 -1
  7. {quickscale_cli-0.75.0 → quickscale_cli-0.76.0}/src/quickscale_cli/commands/development_commands.py +122 -2
  8. {quickscale_cli-0.75.0 → quickscale_cli-0.76.0}/src/quickscale_cli/commands/module_commands.py +70 -3
  9. {quickscale_cli-0.75.0 → quickscale_cli-0.76.0}/src/quickscale_cli/commands/module_config.py +411 -46
  10. {quickscale_cli-0.75.0 → quickscale_cli-0.76.0}/src/quickscale_cli/commands/module_wiring_specs.py +95 -3
  11. {quickscale_cli-0.75.0 → quickscale_cli-0.76.0}/src/quickscale_cli/commands/plan_command.py +214 -63
  12. {quickscale_cli-0.75.0 → quickscale_cli-0.76.0}/src/quickscale_cli/commands/status_command.py +1 -1
  13. {quickscale_cli-0.75.0 → quickscale_cli-0.76.0}/src/quickscale_cli/module_catalog.py +5 -0
  14. {quickscale_cli-0.75.0 → quickscale_cli-0.76.0}/src/quickscale_cli/schema/config_schema.py +17 -3
  15. {quickscale_cli-0.75.0 → quickscale_cli-0.76.0}/src/quickscale_cli/utils/dependency_utils.py +3 -3
  16. {quickscale_cli-0.75.0 → quickscale_cli-0.76.0}/src/quickscale_cli/utils/docker_utils.py +4 -4
  17. {quickscale_cli-0.75.0 → quickscale_cli-0.76.0}/src/quickscale_cli/utils/railway_utils.py +12 -12
  18. quickscale_cli-0.75.0/PKG-INFO +0 -380
  19. quickscale_cli-0.75.0/README.md +0 -354
  20. {quickscale_cli-0.75.0 → quickscale_cli-0.76.0}/src/quickscale_cli/__init__.py +0 -0
  21. {quickscale_cli-0.75.0 → quickscale_cli-0.76.0}/src/quickscale_cli/commands/__init__.py +0 -0
  22. {quickscale_cli-0.75.0 → quickscale_cli-0.76.0}/src/quickscale_cli/commands/remove_command.py +0 -0
  23. {quickscale_cli-0.75.0 → quickscale_cli-0.76.0}/src/quickscale_cli/main.py +0 -0
  24. {quickscale_cli-0.75.0 → quickscale_cli-0.76.0}/src/quickscale_cli/schema/__init__.py +0 -0
  25. {quickscale_cli-0.75.0 → quickscale_cli-0.76.0}/src/quickscale_cli/schema/delta.py +0 -0
  26. {quickscale_cli-0.75.0 → quickscale_cli-0.76.0}/src/quickscale_cli/schema/state_schema.py +0 -0
  27. {quickscale_cli-0.75.0 → quickscale_cli-0.76.0}/src/quickscale_cli/utils/__init__.py +0 -0
  28. {quickscale_cli-0.75.0 → quickscale_cli-0.76.0}/src/quickscale_cli/utils/module_wiring_manager.py +0 -0
  29. {quickscale_cli-0.75.0 → quickscale_cli-0.76.0}/src/quickscale_cli/utils/project_identity.py +0 -0
  30. {quickscale_cli-0.75.0 → quickscale_cli-0.76.0}/src/quickscale_cli/utils/project_manager.py +0 -0
@@ -0,0 +1,60 @@
1
+ Metadata-Version: 2.4
2
+ Name: quickscale-cli
3
+ Version: 0.76.0
4
+ Summary: Command-line interface for QuickScale Django project generator
5
+ License-Expression: Apache-2.0
6
+ Keywords: django,cli,project-generator,saas
7
+ Author: Experto AI
8
+ Author-email: victor@experto.ai
9
+ Requires-Python: >=3.14,<4.0
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.14
14
+ Classifier: Framework :: Django
15
+ Classifier: Framework :: Django :: 6.0
16
+ Requires-Dist: click (>=8.3.1,<9.0.0)
17
+ Requires-Dist: quickscale-core (>=0.76.0,<0.77.0)
18
+ Project-URL: Documentation, https://github.com/Experto-AI/quickscale/tree/main/docs
19
+ Project-URL: Homepage, https://github.com/Experto-AI/quickscale
20
+ Project-URL: Repository, https://github.com/Experto-AI/quickscale
21
+ Description-Content-Type: text/markdown
22
+
23
+ # quickscale-cli
24
+
25
+ Command-line interface package for QuickScale.
26
+
27
+ This package exposes the `quickscale` command used to plan, apply, inspect, and operate generated projects.
28
+
29
+ ## Command groups
30
+
31
+ - Project lifecycle: `plan`, `apply`, `status`, `remove`
32
+ - Local development: `up`, `down`, `ps`, `logs`, `shell`, `manage`
33
+ - Deployment: `deploy`
34
+ - Module workflows: `update`, `push`
35
+
36
+ ## Relationship to other packages
37
+
38
+ - `quickscale-cli` is the command surface
39
+ - `quickscale-core` contains the underlying scaffolding and template logic
40
+ - `quickscale` installs both together for convenience
41
+
42
+ ## Install
43
+
44
+ Usually installed via the meta-package:
45
+
46
+ ```bash
47
+ pip install quickscale
48
+ ```
49
+
50
+ Standalone package installs can use this package's published metadata and dependency constraints.
51
+
52
+ ## Documentation
53
+
54
+ - Repository overview: [QuickScale README](https://github.com/Experto-AI/quickscale/blob/main/README.md)
55
+ - Start guide: [START_HERE.md](https://github.com/Experto-AI/quickscale/blob/main/START_HERE.md)
56
+ - Technical decisions: [docs/technical/decisions.md](https://github.com/Experto-AI/quickscale/blob/main/docs/technical/decisions.md)
57
+ - CLI source entrypoint: [src/quickscale_cli/main.py](https://github.com/Experto-AI/quickscale/blob/main/quickscale_cli/src/quickscale_cli/main.py)
58
+
59
+ This README is package-local context only. Root documentation remains authoritative for repo-wide policy and scope.
60
+
@@ -0,0 +1,37 @@
1
+ # quickscale-cli
2
+
3
+ Command-line interface package for QuickScale.
4
+
5
+ This package exposes the `quickscale` command used to plan, apply, inspect, and operate generated projects.
6
+
7
+ ## Command groups
8
+
9
+ - Project lifecycle: `plan`, `apply`, `status`, `remove`
10
+ - Local development: `up`, `down`, `ps`, `logs`, `shell`, `manage`
11
+ - Deployment: `deploy`
12
+ - Module workflows: `update`, `push`
13
+
14
+ ## Relationship to other packages
15
+
16
+ - `quickscale-cli` is the command surface
17
+ - `quickscale-core` contains the underlying scaffolding and template logic
18
+ - `quickscale` installs both together for convenience
19
+
20
+ ## Install
21
+
22
+ Usually installed via the meta-package:
23
+
24
+ ```bash
25
+ pip install quickscale
26
+ ```
27
+
28
+ Standalone package installs can use this package's published metadata and dependency constraints.
29
+
30
+ ## Documentation
31
+
32
+ - Repository overview: [QuickScale README](https://github.com/Experto-AI/quickscale/blob/main/README.md)
33
+ - Start guide: [START_HERE.md](https://github.com/Experto-AI/quickscale/blob/main/START_HERE.md)
34
+ - Technical decisions: [docs/technical/decisions.md](https://github.com/Experto-AI/quickscale/blob/main/docs/technical/decisions.md)
35
+ - CLI source entrypoint: [src/quickscale_cli/main.py](https://github.com/Experto-AI/quickscale/blob/main/quickscale_cli/src/quickscale_cli/main.py)
36
+
37
+ This README is package-local context only. Root documentation remains authoritative for repo-wide policy and scope.
@@ -1,39 +1,42 @@
1
- [tool.poetry]
1
+ [project]
2
2
  name = "quickscale-cli"
3
- version = "0.75.0"
3
+ version = "0.76.0"
4
4
  description = "Command-line interface for QuickScale Django project generator"
5
- authors = ["Experto AI <victor@experto.ai>"]
5
+ requires-python = ">=3.14,<4.0"
6
+ readme = "README.md"
6
7
  license = "Apache-2.0"
8
+ authors = [{name = "Experto AI", email = "victor@experto.ai"}]
7
9
  keywords = ["django", "cli", "project-generator", "saas"]
8
- homepage = "https://github.com/Experto-AI/quickscale"
9
- repository = "https://github.com/Experto-AI/quickscale"
10
- documentation = "https://github.com/Experto-AI/quickscale/tree/main/docs"
11
- readme = "README.md"
12
10
  classifiers = [
13
11
  "Development Status :: 3 - Alpha",
14
12
  "Intended Audience :: Developers",
15
- "License :: OSI Approved :: Apache Software License",
16
13
  "Programming Language :: Python :: 3",
17
- "Programming Language :: Python :: 3.12",
18
- "Programming Language :: Python :: 3.13",
19
14
  "Programming Language :: Python :: 3.14",
20
15
  "Framework :: Django",
21
16
  "Framework :: Django :: 6.0",
22
17
  ]
18
+ dynamic = ["dependencies"]
19
+
20
+ [project.urls]
21
+ Homepage = "https://github.com/Experto-AI/quickscale"
22
+ Repository = "https://github.com/Experto-AI/quickscale"
23
+ Documentation = "https://github.com/Experto-AI/quickscale/tree/main/docs"
24
+
25
+ [project.scripts]
26
+ quickscale = "quickscale_cli.main:cli"
27
+
28
+ [tool.poetry]
23
29
  packages = [{include = "quickscale_cli", from = "src"}]
24
30
 
25
31
  [tool.poetry.dependencies]
26
- python = "^3.12"
32
+ python = "^3.14"
27
33
  click = "^8.3.1"
28
- quickscale-core = "^0.75.0"
34
+ quickscale-core = "^0.76.0"
29
35
 
30
36
  [tool.poetry.group.dev.dependencies]
31
37
  # Dev dependencies centralized in root pyproject.toml
32
38
  # Use: poetry install from repository root
33
39
 
34
- [tool.poetry.scripts]
35
- quickscale = "quickscale_cli.main:cli"
36
-
37
40
  [build-system]
38
41
  requires = ["poetry-core"]
39
42
  build-backend = "poetry.core.masonry.api"
@@ -46,10 +49,6 @@ python_functions = ["test_*"]
46
49
  addopts = [
47
50
  "--strict-markers",
48
51
  "--strict-config",
49
- "--cov=quickscale_cli",
50
- "--cov-report=term-missing",
51
- "--cov-report=html",
52
- "--cov-fail-under=90",
53
52
  "-m", "not e2e", # Skip E2E tests by default (run with: pytest -m e2e)
54
53
  ]
55
54
  markers = [
@@ -1,2 +1,2 @@
1
1
  # Auto-generated by scripts/version_tool.sh
2
- __version__ = "0.75.0"
2
+ __version__ = "0.76.0"
@@ -105,24 +105,6 @@ def _generate_project(config: QuickScaleConfig, output_path: Path) -> bool:
105
105
  f"(package: {config.project.package})..."
106
106
  )
107
107
 
108
- # Validate theme availability
109
- if config.project.theme == "showcase_htmx":
110
- click.secho(
111
- f"❌ Error: Theme '{config.project.theme}' is not yet implemented",
112
- fg="red",
113
- err=True,
114
- )
115
- click.echo(
116
- "\n💡 The 'showcase_htmx' theme is planned for a future release:",
117
- err=True,
118
- )
119
- click.echo(" - showcase_htmx: Coming in v0.78.0", err=True)
120
- click.echo(
121
- " - Use 'showcase_react' (default) or 'showcase_html' instead",
122
- err=True,
123
- )
124
- return False
125
-
126
108
  generator = ProjectGenerator(theme=config.project.theme)
127
109
  # mypy can resolve an older installed quickscale-core signature here.
128
110
  generate_project = cast(Any, generator.generate)
@@ -281,6 +263,7 @@ def _start_docker(
281
263
  cmd,
282
264
  project_path,
283
265
  "Starting Docker services",
266
+ capture=False,
284
267
  )
285
268
  return success
286
269
 
@@ -575,6 +558,45 @@ def _init_git_with_config(output_path: Path) -> None:
575
558
  click.secho("⚠️ Initial commit failed, continuing...", fg="yellow")
576
559
 
577
560
 
561
+ def _commit_pending_config_changes(output_path: Path) -> None:
562
+ """Commit pending QuickScale config changes before module embedding
563
+
564
+ Stages and commits quickscale.yml and .quickscale/ directory changes so
565
+ git subtree operations have a clean working directory. Called before
566
+ embedding modules in existing projects.
567
+
568
+ Args:
569
+ output_path: Path to the project directory
570
+
571
+ """
572
+ if is_working_directory_clean(output_path):
573
+ return
574
+
575
+ # Stage only QuickScale-managed config files
576
+ subprocess.run(
577
+ ["git", "add", "quickscale.yml", ".quickscale/"],
578
+ cwd=output_path,
579
+ capture_output=True,
580
+ check=False,
581
+ )
582
+
583
+ # Check if anything was staged
584
+ result = subprocess.run(
585
+ ["git", "diff", "--cached", "--name-only"],
586
+ cwd=output_path,
587
+ capture_output=True,
588
+ text=True,
589
+ check=False,
590
+ )
591
+
592
+ if result.stdout.strip():
593
+ _run_command(
594
+ ["git", "commit", "-m", "Update QuickScale configuration"],
595
+ output_path,
596
+ "Committing pending QuickScale configuration changes",
597
+ )
598
+
599
+
578
600
  def _embed_modules_step(
579
601
  output_path: Path,
580
602
  modules_to_embed: list[str],
@@ -824,6 +846,13 @@ def _execute_apply_steps(
824
846
  if ctx.existing_state
825
847
  else list(ctx.qs_config.modules.keys())
826
848
  )
849
+
850
+ # For existing projects, commit any pending QuickScale config changes
851
+ # (e.g. quickscale.yml updated by `quickscale plan`) before embedding
852
+ # modules, so git subtree has a clean working directory to operate on.
853
+ if ctx.existing_state is not None and modules_to_embed:
854
+ _commit_pending_config_changes(ctx.output_path)
855
+
827
856
  embed_result = _embed_modules_step(
828
857
  ctx.output_path, modules_to_embed, no_modules, ctx.existing_state
829
858
  )
@@ -882,16 +911,36 @@ def _execute_apply_steps(
882
911
  ctx.output_path, ctx.qs_config.docker.build, verbose_docker
883
912
  )
884
913
  if not docker_started:
885
- click.secho("⚠️ Docker start failed, continuing...", fg="yellow")
914
+ _save_project_state(
915
+ ctx.output_path,
916
+ ctx.qs_config,
917
+ ctx.existing_state,
918
+ embedded_modules,
919
+ ctx.delta,
920
+ )
921
+ _print_apply_failure_summary(
922
+ failed_step="docker startup",
923
+ reason="Docker auto-start failed. Run 'quickscale logs' to inspect the failing service.",
924
+ )
925
+ raise click.Abort()
886
926
 
887
927
  # For Docker auto-start projects, run migrations in the backend container
888
928
  # so database connectivity uses the internal Docker network.
889
929
  if should_auto_start_docker:
890
930
  if docker_started:
891
931
  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")
932
+ _save_project_state(
933
+ ctx.output_path,
934
+ ctx.qs_config,
935
+ ctx.existing_state,
936
+ embedded_modules,
937
+ ctx.delta,
938
+ )
939
+ _print_apply_failure_summary(
940
+ failed_step="database migrations",
941
+ reason="Migrations failed inside Docker backend container. Run 'quickscale logs backend' for details.",
942
+ )
943
+ raise click.Abort()
895
944
 
896
945
  # Save state
897
946
  _save_project_state(
@@ -457,7 +457,12 @@ def _display_summary(
457
457
  else:
458
458
  click.echo(" 3. Get URL: railway status")
459
459
  click.echo(
460
- f" 4. Create superuser: railway run --service {app_service} "
460
+ " 4. Create superuser automatically: set DJANGO_SUPERUSER_USERNAME, "
461
+ "DJANGO_SUPERUSER_EMAIL and DJANGO_SUPERUSER_PASSWORD variables in Railway "
462
+ "dashboard — start.sh will create it on next deploy"
463
+ )
464
+ click.echo(
465
+ f" Or manually: railway run --service {app_service} "
461
466
  f"python manage.py createsuperuser"
462
467
  )
463
468
  click.echo(" 5. Configure custom domain (optional): railway domain")
@@ -8,6 +8,7 @@ from pathlib import Path
8
8
 
9
9
  import click
10
10
 
11
+ from quickscale_cli.schema.config_schema import QuickScaleConfig
11
12
  from quickscale_cli.utils.docker_utils import (
12
13
  get_docker_compose_command,
13
14
  get_port_from_env,
@@ -140,6 +141,100 @@ def _handle_up_error(error: subprocess.CalledProcessError) -> None:
140
141
  )
141
142
 
142
143
 
144
+ def _run_migrations_after_up() -> None:
145
+ """Run Django migrations in backend container after services start"""
146
+ click.echo("⏳ Applying database migrations...")
147
+ container_name = get_backend_container_name()
148
+ _run_docker_exec_command(
149
+ container_name,
150
+ ["python", "manage.py", "migrate"],
151
+ capture=True,
152
+ )
153
+ click.secho("✅ Database migrations applied", fg="green")
154
+
155
+
156
+ def _superuser_exists_in_backend(container_name: str) -> bool | None:
157
+ """Check whether a Django superuser already exists
158
+
159
+ Returns:
160
+ True when at least one superuser exists
161
+ False when no superuser exists
162
+ None when status cannot be verified reliably
163
+ """
164
+ check_cmd = [
165
+ "docker",
166
+ "exec",
167
+ container_name,
168
+ "python",
169
+ "manage.py",
170
+ "shell",
171
+ "-c",
172
+ (
173
+ "from django.contrib.auth import get_user_model; import sys; "
174
+ "sys.exit(0 if get_user_model().objects.filter(is_superuser=True).exists() else 1)"
175
+ ),
176
+ ]
177
+ result = subprocess.run(check_cmd, capture_output=True, text=True)
178
+ if result.returncode == 0:
179
+ return True
180
+
181
+ stdout_output = (result.stdout or "").strip()
182
+ stderr_output = (result.stderr or "").strip()
183
+
184
+ if result.returncode == 1 and not stdout_output and not stderr_output:
185
+ return False
186
+
187
+ return None
188
+
189
+
190
+ def _handle_superuser_after_up(config: QuickScaleConfig | None) -> None:
191
+ """Create superuser if configured and one does not already exist"""
192
+ if config is None or config.docker is None:
193
+ return
194
+
195
+ if not getattr(config.docker, "create_superuser", False):
196
+ return
197
+
198
+ container_name = get_backend_container_name()
199
+ superuser_exists = _superuser_exists_in_backend(container_name)
200
+ interactive_session = is_interactive()
201
+
202
+ if superuser_exists is True:
203
+ click.echo("ℹ️ Superuser already exists. Skipping createsuperuser step.")
204
+ return
205
+
206
+ if superuser_exists is None:
207
+ click.secho("⚠️ Could not verify superuser status.", fg="yellow")
208
+ if not interactive_session:
209
+ click.echo(" Run: quickscale manage createsuperuser")
210
+ return
211
+ click.echo(" Proceeding with interactive superuser creation.")
212
+ elif not interactive_session:
213
+ click.secho(
214
+ "⚠️ Superuser creation is enabled but requires interactive input.",
215
+ fg="yellow",
216
+ )
217
+ click.echo(" Run: quickscale manage createsuperuser")
218
+ return
219
+
220
+ click.echo("👤 Creating Django superuser...")
221
+ _run_docker_exec_command(
222
+ container_name,
223
+ ["python", "manage.py", "createsuperuser"],
224
+ capture=False,
225
+ )
226
+
227
+
228
+ def _command_contains(error: subprocess.CalledProcessError, *tokens: str) -> bool:
229
+ """Check whether error command contains all tokens"""
230
+ cmd = error.cmd
231
+ if isinstance(cmd, list):
232
+ cmd_text = " ".join(str(part) for part in cmd)
233
+ else:
234
+ cmd_text = str(cmd)
235
+ return all(token in cmd_text for token in tokens)
236
+
237
+
143
238
  @click.command()
144
239
  @click.option("--build", is_flag=True, help="Rebuild containers before starting")
145
240
  @click.option("--no-cache", is_flag=True, help="Build without using cache")
@@ -180,8 +275,33 @@ def up(build: bool, no_cache: bool) -> None:
180
275
  if should_build:
181
276
  _update_last_build_timestamp()
182
277
 
278
+ _run_migrations_after_up()
279
+ _handle_superuser_after_up(config)
280
+
183
281
  except subprocess.CalledProcessError as e:
184
- _handle_up_error(e)
282
+ if _command_contains(e, "manage.py", "migrate"):
283
+ click.secho(
284
+ "❌ Error: Services started but database migration failed",
285
+ fg="red",
286
+ err=True,
287
+ )
288
+ if e.stderr:
289
+ click.echo(f"\nError output:\n{e.stderr}", err=True)
290
+ click.echo(
291
+ "💡 Tip: Run 'quickscale manage migrate' and inspect logs with 'quickscale logs backend'",
292
+ err=True,
293
+ )
294
+ elif _command_contains(e, "manage.py", "createsuperuser"):
295
+ click.secho(
296
+ "❌ Error: Services started but superuser creation failed",
297
+ fg="red",
298
+ err=True,
299
+ )
300
+ click.echo(
301
+ "💡 Tip: Run 'quickscale manage createsuperuser' manually", err=True
302
+ )
303
+ else:
304
+ _handle_up_error(e)
185
305
  sys.exit(1)
186
306
  except KeyboardInterrupt:
187
307
  click.echo("\n⚠️ Interrupted by user")
@@ -428,7 +548,7 @@ def _dependencies_changed_since_last_build() -> bool:
428
548
  )
429
549
  return changed
430
550
 
431
- except (json.JSONDecodeError, KeyError, OSError):
551
+ except json.JSONDecodeError, KeyError, OSError:
432
552
  # If we can't read state, don't warn (fail safe)
433
553
  return False
434
554
 
@@ -195,7 +195,7 @@ def _perform_module_embed(
195
195
  applier(project_path, config)
196
196
 
197
197
  # Install dependencies for modules that need it
198
- if module in ["auth", "blog", "listings", "crm", "forms"]:
198
+ if module in ["auth", "blog", "listings", "crm", "forms", "storage"]:
199
199
  if not _install_module_dependencies(project_path, module):
200
200
  return False
201
201
 
@@ -440,7 +440,7 @@ def _validate_update_environment() -> None:
440
440
 
441
441
  def _update_single_module(
442
442
  name: str, info: Any, default_remote: str, no_preview: bool
443
- ) -> None:
443
+ ) -> bool:
444
444
  """Update a single module via git subtree pull."""
445
445
  click.echo(f"\n📥 Updating {name} module...")
446
446
 
@@ -455,15 +455,62 @@ def _update_single_module(
455
455
  # Update version in config
456
456
  update_module_version(name, "v0.62.0") # Placeholder version
457
457
 
458
+ _commit_module_update(name, info.prefix)
459
+
458
460
  click.secho(f"✅ Updated {name} successfully", fg="green")
459
461
 
460
462
  if output and not no_preview:
461
463
  click.echo("\n📋 Changes summary:")
462
464
  click.echo(output[:500]) # Show first 500 chars
463
465
 
466
+ return True
467
+
464
468
  except GitError as e:
465
469
  click.secho(f"❌ Failed to update {name}: {e}", fg="red", err=True)
466
470
  click.echo(f"💡 Tip: Check for conflicts in modules/{name}/", err=True)
471
+ return False
472
+
473
+
474
+ def _commit_module_update(module_name: str, module_prefix: str) -> None:
475
+ """Create a commit for a successfully updated module."""
476
+ tracked_paths = [module_prefix]
477
+ config_path = Path(".quickscale") / "config.yml"
478
+ if config_path.exists():
479
+ tracked_paths.append(str(config_path))
480
+
481
+ try:
482
+ subprocess.run(
483
+ ["git", "add", *tracked_paths],
484
+ check=True,
485
+ capture_output=True,
486
+ text=True,
487
+ )
488
+ except subprocess.CalledProcessError as e:
489
+ raise GitError(f"Failed to stage {module_name} update commit: {e.stderr}")
490
+
491
+ cached_diff = subprocess.run(
492
+ ["git", "diff", "--cached", "--quiet"],
493
+ capture_output=True,
494
+ text=True,
495
+ )
496
+
497
+ if cached_diff.returncode == 0:
498
+ click.echo(f"ℹ️ No staged changes detected for {module_name}; skipping commit")
499
+ return
500
+
501
+ if cached_diff.returncode != 1:
502
+ raise GitError("Failed to inspect staged changes before module update commit")
503
+
504
+ commit_message = f"chore(modules): update {module_name} module"
505
+ try:
506
+ subprocess.run(
507
+ ["git", "commit", "-m", commit_message],
508
+ check=True,
509
+ capture_output=True,
510
+ text=True,
511
+ )
512
+ except subprocess.CalledProcessError as e:
513
+ raise GitError(f"Failed to commit {module_name} update: {e.stderr}")
467
514
 
468
515
 
469
516
  @click.command()
@@ -487,6 +534,7 @@ def update(no_preview: bool) -> None:
487
534
  - Updates ONLY modules you've explicitly installed
488
535
  - Shows a diff preview before updating (unless --no-preview)
489
536
  - Updates the installed version in config after successful update
537
+ - Commits each successful module update before continuing
490
538
  """
491
539
  try:
492
540
  _validate_update_environment()
@@ -514,15 +562,34 @@ def update(no_preview: bool) -> None:
514
562
  click.echo("❌ Update cancelled")
515
563
  return
516
564
 
565
+ failed_modules: list[str] = []
566
+
517
567
  # Update each module
518
568
  for name, info in config.modules.items():
519
- _update_single_module(name, info, config.default_remote, no_preview)
569
+ if not _update_single_module(name, info, config.default_remote, no_preview):
570
+ failed_modules.append(name)
571
+ break
572
+
573
+ if failed_modules:
574
+ click.secho(
575
+ "\n❌ Module update stopped due to failure",
576
+ fg="red",
577
+ bold=True,
578
+ err=True,
579
+ )
580
+ click.echo(
581
+ f"Failed module(s): {', '.join(failed_modules)}",
582
+ err=True,
583
+ )
584
+ raise click.Abort()
520
585
 
521
586
  click.secho("\n🎉 Module update complete!", fg="green", bold=True)
522
587
 
523
588
  except GitError as e:
524
589
  click.secho(f"❌ Git error: {e}", fg="red", err=True)
525
590
  raise click.Abort()
591
+ except click.Abort:
592
+ raise
526
593
  except Exception as e:
527
594
  click.secho(f"❌ Unexpected error: {e}", fg="red", err=True)
528
595
  raise click.Abort()