scaffold-ca-python 0.1.1__py3-none-any.whl

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 (109) hide show
  1. scaffold_ca_python/__init__.py +1 -0
  2. scaffold_ca_python/cli.py +39 -0
  3. scaffold_ca_python/commands/__init__.py +0 -0
  4. scaffold_ca_python/commands/delete_module.py +216 -0
  5. scaffold_ca_python/commands/generate_driven_adapter.py +182 -0
  6. scaffold_ca_python/commands/generate_entry_point.py +304 -0
  7. scaffold_ca_python/commands/generate_helper.py +135 -0
  8. scaffold_ca_python/commands/generate_model.py +134 -0
  9. scaffold_ca_python/commands/generate_pipeline.py +158 -0
  10. scaffold_ca_python/commands/generate_project.py +189 -0
  11. scaffold_ca_python/commands/generate_use_case.py +136 -0
  12. scaffold_ca_python/commands/update_project.py +84 -0
  13. scaffold_ca_python/commands/validate_structure.py +90 -0
  14. scaffold_ca_python/core/__init__.py +0 -0
  15. scaffold_ca_python/core/file_writer.py +128 -0
  16. scaffold_ca_python/core/module_builder.py +127 -0
  17. scaffold_ca_python/core/name_utils.py +59 -0
  18. scaffold_ca_python/core/project_detector.py +93 -0
  19. scaffold_ca_python/core/pyproject_writer.py +169 -0
  20. scaffold_ca_python/core/structure_validator.py +142 -0
  21. scaffold_ca_python/core/template_renderer.py +100 -0
  22. scaffold_ca_python/factory/__init__.py +16 -0
  23. scaffold_ca_python/factory/driven_adapters/__init__.py +0 -0
  24. scaffold_ca_python/factory/driven_adapters/da_generic.py +65 -0
  25. scaffold_ca_python/factory/driven_adapters/da_rest_consumer.py +64 -0
  26. scaffold_ca_python/factory/driven_adapters/da_secrets.py +64 -0
  27. scaffold_ca_python/factory/entry_points/__init__.py +0 -0
  28. scaffold_ca_python/factory/entry_points/ep_agent.py +91 -0
  29. scaffold_ca_python/factory/entry_points/ep_generic.py +75 -0
  30. scaffold_ca_python/factory/entry_points/ep_mcp.py +138 -0
  31. scaffold_ca_python/factory/entry_points/ep_restapi.py +133 -0
  32. scaffold_ca_python/factory/simple/__init__.py +0 -0
  33. scaffold_ca_python/factory/simple/delete_module_factory.py +85 -0
  34. scaffold_ca_python/factory/simple/helper_factory.py +67 -0
  35. scaffold_ca_python/factory/simple/model_factory.py +57 -0
  36. scaffold_ca_python/factory/simple/use_case_factory.py +59 -0
  37. scaffold_ca_python/models/__init__.py +0 -0
  38. scaffold_ca_python/models/context.py +60 -0
  39. scaffold_ca_python/models/file_operation.py +47 -0
  40. scaffold_ca_python/models/layer.py +41 -0
  41. scaffold_ca_python/models/violation.py +26 -0
  42. scaffold_ca_python/templates/__init__.py +0 -0
  43. scaffold_ca_python/templates/driven_adapter/generic/__init__.py.jinja2 +1 -0
  44. scaffold_ca_python/templates/driven_adapter/generic/adapter.py.jinja2 +18 -0
  45. scaffold_ca_python/templates/driven_adapter/generic/test_adapter.py.jinja2 +22 -0
  46. scaffold_ca_python/templates/driven_adapter/rest_consumer/__init__.py.jinja2 +1 -0
  47. scaffold_ca_python/templates/driven_adapter/rest_consumer/rest_consumer.py.jinja2 +27 -0
  48. scaffold_ca_python/templates/driven_adapter/rest_consumer/test_rest_consumer.py.jinja2 +24 -0
  49. scaffold_ca_python/templates/driven_adapter/secrets/__init__.py.jinja2 +1 -0
  50. scaffold_ca_python/templates/driven_adapter/secrets/secrets_adapter.py.jinja2 +37 -0
  51. scaffold_ca_python/templates/driven_adapter/secrets/test_secrets_adapter.py.jinja2 +26 -0
  52. scaffold_ca_python/templates/entry_point/agent/__init__.py.jinja2 +1 -0
  53. scaffold_ca_python/templates/entry_point/agent/agent.py.jinja2 +49 -0
  54. scaffold_ca_python/templates/entry_point/agent/card.py.jinja2 +15 -0
  55. scaffold_ca_python/templates/entry_point/agent/entrypoint_main.py.jinja2 +13 -0
  56. scaffold_ca_python/templates/entry_point/agent/test_agent.py.jinja2 +20 -0
  57. scaffold_ca_python/templates/entry_point/generic/__init__.py.jinja2 +1 -0
  58. scaffold_ca_python/templates/entry_point/generic/entrypoint_main.py.jinja2 +13 -0
  59. scaffold_ca_python/templates/entry_point/generic/handler.py.jinja2 +13 -0
  60. scaffold_ca_python/templates/entry_point/generic/test_handler.py.jinja2 +35 -0
  61. scaffold_ca_python/templates/entry_point/mcp/__init__.py.jinja2 +1 -0
  62. scaffold_ca_python/templates/entry_point/mcp/app.py.jinja2 +51 -0
  63. scaffold_ca_python/templates/entry_point/mcp/prompts.py.jinja2 +22 -0
  64. scaffold_ca_python/templates/entry_point/mcp/resources.py.jinja2 +22 -0
  65. scaffold_ca_python/templates/entry_point/mcp/server.py.jinja2 +27 -0
  66. scaffold_ca_python/templates/entry_point/mcp/test_app.py.jinja2 +32 -0
  67. scaffold_ca_python/templates/entry_point/mcp/test_prompts.py.jinja2 +40 -0
  68. scaffold_ca_python/templates/entry_point/mcp/test_resources.py.jinja2 +47 -0
  69. scaffold_ca_python/templates/entry_point/mcp/test_tools.py.jinja2 +40 -0
  70. scaffold_ca_python/templates/entry_point/mcp/tools.py.jinja2 +22 -0
  71. scaffold_ca_python/templates/entry_point/restapi/__init__.py.jinja2 +1 -0
  72. scaffold_ca_python/templates/entry_point/restapi/app.py.jinja2 +78 -0
  73. scaffold_ca_python/templates/entry_point/restapi/exception_handler.py.jinja2 +35 -0
  74. scaffold_ca_python/templates/entry_point/restapi/health.py.jinja2 +13 -0
  75. scaffold_ca_python/templates/entry_point/restapi/rest_controller.py.jinja2 +26 -0
  76. scaffold_ca_python/templates/entry_point/restapi/server.py.jinja2 +5 -0
  77. scaffold_ca_python/templates/entry_point/restapi/test_app.py.jinja2 +22 -0
  78. scaffold_ca_python/templates/entry_point/restapi/test_exception_handler.py.jinja2 +44 -0
  79. scaffold_ca_python/templates/entry_point/restapi/test_rest_controller.py.jinja2 +35 -0
  80. scaffold_ca_python/templates/entry_point/restapi/test_server.py.jinja2 +15 -0
  81. scaffold_ca_python/templates/helper/__init__.py.jinja2 +1 -0
  82. scaffold_ca_python/templates/helper/helper.py.jinja2 +7 -0
  83. scaffold_ca_python/templates/helper/test_helper.py.jinja2 +8 -0
  84. scaffold_ca_python/templates/model/model.py.jinja2 +9 -0
  85. scaffold_ca_python/templates/model/test_model.py.jinja2 +8 -0
  86. scaffold_ca_python/templates/pipeline/azure/azure_pipelines.yml.jinja2 +28 -0
  87. scaffold_ca_python/templates/pipeline/github/ci.yml.jinja2 +34 -0
  88. scaffold_ca_python/templates/project/README.jinja2 +30 -0
  89. scaffold_ca_python/templates/project/application/config/__init__.py.jinja2 +1 -0
  90. scaffold_ca_python/templates/project/application/config/config.py.jinja2 +12 -0
  91. scaffold_ca_python/templates/project/application/config/container.py.jinja2 +17 -0
  92. scaffold_ca_python/templates/project/application/config/driven_adapters_container.py.jinja2 +14 -0
  93. scaffold_ca_python/templates/project/application/config/resource_container.py.jinja2 +17 -0
  94. scaffold_ca_python/templates/project/application/config/usecases_container.py.jinja2 +16 -0
  95. scaffold_ca_python/templates/project/dockerfile.jinja2 +22 -0
  96. scaffold_ca_python/templates/project/dockerignore.jinja2 +19 -0
  97. scaffold_ca_python/templates/project/gitignore.jinja2 +64 -0
  98. scaffold_ca_python/templates/project/layer_init.jinja2 +1 -0
  99. scaffold_ca_python/templates/project/main.py.jinja2 +10 -0
  100. scaffold_ca_python/templates/project/mypy_ini.jinja2 +5 -0
  101. scaffold_ca_python/templates/project/pyproject_toml.jinja2 +66 -0
  102. scaffold_ca_python/templates/project/python_version.jinja2 +1 -0
  103. scaffold_ca_python/templates/use_case/test_use_case.py.jinja2 +12 -0
  104. scaffold_ca_python/templates/use_case/use_case.py.jinja2 +9 -0
  105. scaffold_ca_python-0.1.1.dist-info/METADATA +285 -0
  106. scaffold_ca_python-0.1.1.dist-info/RECORD +109 -0
  107. scaffold_ca_python-0.1.1.dist-info/WHEEL +4 -0
  108. scaffold_ca_python-0.1.1.dist-info/entry_points.txt +3 -0
  109. scaffold_ca_python-0.1.1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1 @@
1
+
@@ -0,0 +1,39 @@
1
+ import typer
2
+
3
+ from scaffold_ca_python.commands import (
4
+ delete_module,
5
+ generate_driven_adapter,
6
+ generate_entry_point,
7
+ generate_helper,
8
+ generate_model,
9
+ generate_pipeline,
10
+ generate_project,
11
+ generate_use_case,
12
+ update_project,
13
+ validate_structure,
14
+ )
15
+
16
+ app = typer.Typer(
17
+ name="scaffold-ca-python",
18
+ help="Scaffold production-ready Clean Architecture Python projects.",
19
+ add_completion=False,
20
+ rich_markup_mode="rich",
21
+ )
22
+
23
+ generate_project.register(app)
24
+ generate_model.register(app)
25
+ generate_use_case.register(app)
26
+ generate_driven_adapter.register(app)
27
+ generate_entry_point.register(app)
28
+ generate_helper.register(app)
29
+ generate_pipeline.register(app)
30
+ delete_module.register(app)
31
+ update_project.register(app)
32
+ validate_structure.register(app)
33
+
34
+
35
+ @app.callback(invoke_without_command=True)
36
+ def _main(ctx: typer.Context) -> None:
37
+ if ctx.invoked_subcommand is None:
38
+ typer.echo(ctx.get_help())
39
+ raise typer.Exit(0)
File without changes
@@ -0,0 +1,216 @@
1
+ """delete_module: safely remove a previously generated module and its test mirror (T039)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import tomllib
6
+ from pathlib import Path
7
+ from typing import Annotated
8
+
9
+ import typer
10
+ from rich.console import Console
11
+
12
+ from scaffold_ca_python.core.module_builder import ModuleBuilder
13
+ from scaffold_ca_python.core.name_utils import ScaffoldError, to_snake_case, validate_name
14
+ from scaffold_ca_python.core.project_detector import find_project_root, resolve_tests_root
15
+ from scaffold_ca_python.factory import ModuleFactory
16
+ from scaffold_ca_python.factory.simple.delete_module_factory import DeleteModuleFactory
17
+ from scaffold_ca_python.models.context import ModuleContext, ProjectContext
18
+ from scaffold_ca_python.models.layer import Layer
19
+
20
+ console = Console()
21
+
22
+ _REGISTRY: dict[str, type[ModuleFactory]] = {
23
+ "delete": DeleteModuleFactory,
24
+ }
25
+
26
+ _DM_HELP = "Delete a previously generated module and its test mirror. Alias 'dm'."
27
+ _DM_EPILOG = "Examples:\n\n * scaffold dm --name Order\n\n * scaffold delete-module --name MyAdapter\n\n"
28
+
29
+
30
+ # ---------------------------------------------------------------------------
31
+ # Module discovery helpers
32
+ # ---------------------------------------------------------------------------
33
+
34
+
35
+ def _find_module_targets(project_root: Path, snake: str) -> list[tuple[Path, Path | None]]:
36
+ """Return a list of (target_path, test_path_or_None) for all matches of *snake*.
37
+
38
+ Searches across:
39
+ 1. domain/model/<snake>.py
40
+ 2. domain/usecase/<snake>.py
41
+ 3. infrastructure/driven_adapters/<snake>/
42
+ 4. infrastructure/entry_points/<snake>/
43
+ 5. infrastructure/helpers/<snake>/
44
+ """
45
+ pkg = _get_python_package(project_root)
46
+ src_root = project_root / "src" / pkg
47
+ tests_root = resolve_tests_root(project_root)
48
+
49
+ candidates: list[tuple[Path, Path | None]] = []
50
+
51
+ # Single-file layers (domain/model, domain/usecase)
52
+ for src_rel, test_rel in [
53
+ (src_root / "domain" / "model" / f"{snake}.py", tests_root / "domain" / "model" / f"test_{snake}.py"),
54
+ (src_root / "domain" / "usecase" / f"{snake}.py", tests_root / "domain" / "usecase" / f"test_{snake}.py"),
55
+ ]:
56
+ if src_rel.exists():
57
+ candidates.append((src_rel, test_rel if test_rel.exists() else None))
58
+
59
+ # Directory-based layers (infrastructure sub-dirs)
60
+ for src_rel, test_rel in [
61
+ (
62
+ src_root / "infrastructure" / "driven_adapters" / snake,
63
+ tests_root / "infrastructure" / "driven_adapters" / snake,
64
+ ),
65
+ (
66
+ src_root / "infrastructure" / "entry_points" / snake,
67
+ tests_root / "infrastructure" / "entry_points" / snake,
68
+ ),
69
+ (
70
+ src_root / "infrastructure" / "helpers" / snake,
71
+ tests_root / "infrastructure" / "helpers" / snake,
72
+ ),
73
+ ]:
74
+ if src_rel.exists():
75
+ candidates.append((src_rel, test_rel if test_rel.exists() else None))
76
+
77
+ return candidates
78
+
79
+
80
+ def _get_python_package(root: Path) -> str:
81
+ pyproject = root / "pyproject.toml"
82
+ with pyproject.open("rb") as fh:
83
+ data = tomllib.load(fh)
84
+ section = data.get("tool", {}).get("scaffold-ca-python", {})
85
+ name = section.get("name", root.name)
86
+ import re
87
+
88
+ s = name.replace("-", "_")
89
+ s = re.sub(r"([A-Z]+)([A-Z][a-z])", r"\1_\2", s)
90
+ s = re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", s)
91
+ return s.lower()
92
+
93
+
94
+ # ---------------------------------------------------------------------------
95
+ # Implementation
96
+ # ---------------------------------------------------------------------------
97
+
98
+
99
+ def _delete_module_impl(name: str, confirm: bool, dry_run: bool) -> None:
100
+ # Validate name
101
+ try:
102
+ validate_name(name)
103
+ except ScaffoldError as exc:
104
+ console.print(f"[red]Error:[/red] {exc}")
105
+ raise typer.Exit(code=1) from None
106
+
107
+ # Locate project root
108
+ try:
109
+ project_root = find_project_root()
110
+ except ScaffoldError:
111
+ console.print("[red]Error:[/red] No scaffold-ca-python project found. Run 'scaffold ca' first.")
112
+ raise typer.Exit(code=1) from None
113
+
114
+ project_ctx = _load_project_context(project_root)
115
+
116
+ # Create a placeholder ModuleContext for the factory (layer doesn't matter for delete)
117
+ module_ctx = ModuleContext(name=name, layer=Layer.HELPERS, project=project_ctx)
118
+
119
+ # Find targets
120
+ snake = to_snake_case(name)
121
+ targets = _find_module_targets(project_root, snake)
122
+
123
+ if not targets:
124
+ console.print(
125
+ f"[red]Error:[/red] No module named '{snake}' found in project.\n"
126
+ "[dim]Hint:[/dim] Check spelling with 'scaffold vs' to list modules."
127
+ )
128
+ raise typer.Exit(code=1) from None
129
+
130
+ # dry_run OR no --confirm → preview mode only
131
+ if dry_run or not confirm:
132
+ console.print("The following files would be deleted:\n")
133
+ for src_path, test_path in targets:
134
+ try:
135
+ display = src_path.relative_to(project_root)
136
+ except ValueError:
137
+ display = src_path
138
+ console.print(f" {display}")
139
+ if test_path:
140
+ try:
141
+ display = test_path.relative_to(project_root)
142
+ except ValueError:
143
+ display = test_path
144
+ console.print(f" {display}")
145
+ console.print("\nRun with --confirm to proceed.")
146
+ return
147
+
148
+ # Use factory for deletion (only when confirm=True and not dry_run)
149
+ builder = ModuleBuilder(
150
+ project_root=project_root,
151
+ project_ctx=project_ctx,
152
+ module_ctx=module_ctx,
153
+ dry_run=False,
154
+ )
155
+
156
+ factory_class = _REGISTRY["delete"]
157
+ factory = factory_class()
158
+ factory.build(builder)
159
+
160
+ # Persist deletion
161
+ builder.persist()
162
+ console.print(f"[green]✓[/green] Deleted module(s) for [bold]{name}[/bold].")
163
+
164
+
165
+ def register(app: typer.Typer) -> None:
166
+ """Register dm / delete-module command onto *app*."""
167
+
168
+ @app.command(
169
+ "delete-module",
170
+ help=_DM_HELP,
171
+ epilog=_DM_EPILOG,
172
+ )
173
+ @app.command("dm", hidden=True, help=_DM_HELP, epilog=_DM_EPILOG)
174
+ def delete_module(
175
+ ctx: typer.Context,
176
+ name: Annotated[
177
+ str | None, typer.Option("--name", help="Module name (PascalCase).", rich_help_panel="Required")
178
+ ] = None,
179
+ confirm: Annotated[
180
+ bool,
181
+ typer.Option(
182
+ "--confirm/--no-confirm",
183
+ help="Skip confirmation dialog and delete immediately.",
184
+ rich_help_panel="Options",
185
+ ),
186
+ ] = False,
187
+ dry_run: Annotated[
188
+ bool,
189
+ typer.Option(
190
+ "--dry-run/--no-dry-run",
191
+ help="Preview deletion without removing files.",
192
+ rich_help_panel="Options",
193
+ show_default=True,
194
+ ),
195
+ ] = False,
196
+ ) -> None:
197
+ """Delete a module and its test mirror."""
198
+ if name is None:
199
+ typer.echo(ctx.get_help())
200
+ raise typer.Exit(0)
201
+ _delete_module_impl(name, confirm, dry_run)
202
+
203
+
204
+ # ---------------------------------------------------------------------------
205
+ # Helpers
206
+ # ---------------------------------------------------------------------------
207
+
208
+
209
+ def _load_project_context(root: Path) -> ProjectContext:
210
+ pyproject = root / "pyproject.toml"
211
+ with pyproject.open("rb") as fh:
212
+ data = tomllib.load(fh)
213
+ section = data.get("tool", {}).get("scaffold-ca-python", {})
214
+ return ProjectContext(
215
+ name=section.get("name", root.name),
216
+ )
@@ -0,0 +1,182 @@
1
+ """generate_driven_adapter: scaffold a driven adapter and test stub (T030)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import tomllib
6
+ from pathlib import Path
7
+ from typing import Annotated
8
+
9
+ import typer
10
+ from rich.console import Console
11
+ from rich.tree import Tree
12
+
13
+ from scaffold_ca_python.core.module_builder import ModuleBuilder
14
+ from scaffold_ca_python.core.name_utils import ScaffoldError, to_snake_case, validate_name
15
+ from scaffold_ca_python.core.project_detector import find_project_root
16
+ from scaffold_ca_python.factory import ModuleFactory
17
+ from scaffold_ca_python.factory.driven_adapters.da_generic import DrivenAdapterGeneric
18
+ from scaffold_ca_python.factory.driven_adapters.da_rest_consumer import DrivenAdapterRestConsumer
19
+ from scaffold_ca_python.factory.driven_adapters.da_secrets import DrivenAdapterSecrets
20
+ from scaffold_ca_python.models.context import ModuleContext, ProjectContext
21
+ from scaffold_ca_python.models.layer import Layer
22
+
23
+ console = Console()
24
+
25
+ _REGISTRY: dict[str, type[ModuleFactory]] = {
26
+ "rest-consumer": DrivenAdapterRestConsumer,
27
+ "secrets": DrivenAdapterSecrets,
28
+ "generic": DrivenAdapterGeneric,
29
+ }
30
+
31
+ _GDA_HELP = "Scaffold a driven adapter and test stub. Alias 'gda'."
32
+ _GDA_EPILOG = (
33
+ "Types:\n\n"
34
+ " * rest-consumer HTTP outbound client (httpx)\n\n"
35
+ " * secrets AWS Secrets Manager (boto3)\n\n"
36
+ " * generic Custom adapter (--name required)\n\n"
37
+ " ==================================================\n\n"
38
+ "Examples:\n\n"
39
+ " * scaffold gda --type rest-consumer\n\n"
40
+ " * scaffold generate-driven-adapter --type generic --name MyAdapter\n\n"
41
+ )
42
+
43
+
44
+ def _generate_driven_adapter_impl(type_: str, name: str | None, dry_run: bool) -> None: # noqa: ANN001
45
+ # --- Validate type ---
46
+ if type_ not in _REGISTRY:
47
+ console.print(f"[red]Error:[/red] Unknown type '{type_}'. Allowed: {', '.join(_REGISTRY.keys())}.")
48
+ raise typer.Exit(code=1) from None
49
+
50
+ # --- Validate name requirement for generic ---
51
+ if type_ == "generic" and not name:
52
+ console.print("[red]Error:[/red] --name is required when --type is generic.")
53
+ raise typer.Exit(code=1) from None
54
+
55
+ # --- Validate name if provided ---
56
+ if name:
57
+ try:
58
+ validate_name(name)
59
+ except ScaffoldError as exc:
60
+ console.print(f"[red]Error:[/red] {exc}")
61
+ raise typer.Exit(code=1) from None
62
+
63
+ # --- Locate project root ---
64
+ try:
65
+ project_root = find_project_root()
66
+ except ScaffoldError:
67
+ console.print("[red]Error:[/red] No scaffold-ca-python project found. Run 'scaffold ca' first.")
68
+ raise typer.Exit(code=1) from None
69
+
70
+ project_ctx = _load_project_context(project_root)
71
+
72
+ # --- Determine module context and subdir from type/name ---
73
+ if type_ == "rest-consumer":
74
+ subdir = "rest_consumer"
75
+ module_ctx = ModuleContext(name="rest_consumer", layer=Layer.DRIVEN_ADAPTERS, project=project_ctx)
76
+ elif type_ == "secrets":
77
+ subdir = "secrets"
78
+ module_ctx = ModuleContext(name="secrets", layer=Layer.DRIVEN_ADAPTERS, project=project_ctx)
79
+ else: # generic
80
+ assert name is not None # validated above
81
+ subdir = to_snake_case(name)
82
+ module_ctx = ModuleContext(name=name, layer=Layer.DRIVEN_ADAPTERS, project=project_ctx)
83
+
84
+ pkg = project_ctx.python_package
85
+ src_dir = project_root / "src" / pkg / "infrastructure" / "driven_adapters" / subdir
86
+
87
+ # --- Duplicate guard ---
88
+ if src_dir.exists():
89
+ console.print(
90
+ f"[red]Error:[/red] Directory '{src_dir.relative_to(project_root)}/' already exists.\n"
91
+ "[dim]Hint:[/dim] Choose a different name or remove the existing directory first."
92
+ )
93
+ raise typer.Exit(code=1) from None
94
+
95
+ # --- Create builder and get factory from registry ---
96
+ builder = ModuleBuilder(
97
+ project_root=project_root,
98
+ project_ctx=project_ctx,
99
+ module_ctx=module_ctx,
100
+ dry_run=dry_run,
101
+ )
102
+
103
+ # Get factory class and instantiate
104
+ factory_class = _REGISTRY[type_]
105
+ factory = factory_class()
106
+
107
+ # Invoke factory to build via ModuleBuilder
108
+ factory.build(builder)
109
+
110
+ # Persist all operations
111
+ created = builder.persist()
112
+
113
+ # --- Display results ---
114
+ if dry_run:
115
+ tree = Tree(f"[bold]{subdir}[/bold] (dry run)")
116
+ for p in sorted(created):
117
+ tree.add(str(p.relative_to(project_root)))
118
+ console.print(tree)
119
+ if builder._dependencies:
120
+ console.print(f"[dim]Would add to [project.dependencies]: {', '.join(builder._dependencies)}[/dim]")
121
+ return
122
+
123
+ console.print(f"[green]✓[/green] Driven adapter [bold]{subdir}[/bold] created. Created {len(created)} file(s).")
124
+
125
+
126
+ def register(app: typer.Typer) -> None:
127
+ """Register gda / generate-driven-adapter commands onto *app*."""
128
+
129
+ @app.command(
130
+ "generate-driven-adapter",
131
+ help=_GDA_HELP,
132
+ epilog=_GDA_EPILOG,
133
+ )
134
+ @app.command("gda", hidden=True, help=_GDA_HELP, epilog=_GDA_EPILOG)
135
+ def generate_driven_adapter(
136
+ ctx: typer.Context,
137
+ type_: Annotated[
138
+ str | None,
139
+ typer.Option(
140
+ "--type",
141
+ help="Adapter type: rest-consumer, secrets, generic.",
142
+ rich_help_panel="Required",
143
+ ),
144
+ ] = None,
145
+ name: Annotated[
146
+ str | None,
147
+ typer.Option(
148
+ "--name",
149
+ help="Adapter name (required for --type generic).",
150
+ rich_help_panel="Options",
151
+ ),
152
+ ] = None,
153
+ dry_run: Annotated[
154
+ bool,
155
+ typer.Option(
156
+ "--dry-run/--no-dry-run",
157
+ help="Preview without writing.",
158
+ rich_help_panel="Options",
159
+ show_default=True,
160
+ ),
161
+ ] = False,
162
+ ) -> None:
163
+ """Scaffold a driven adapter inside infrastructure/driven_adapters/."""
164
+ if type_ is None:
165
+ typer.echo(ctx.get_help())
166
+ raise typer.Exit(0)
167
+ _generate_driven_adapter_impl(type_, name, dry_run)
168
+
169
+
170
+ # ---------------------------------------------------------------------------
171
+ # Helpers
172
+ # ---------------------------------------------------------------------------
173
+
174
+
175
+ def _load_project_context(root: Path) -> ProjectContext:
176
+ pyproject = root / "pyproject.toml"
177
+ with pyproject.open("rb") as fh:
178
+ data = tomllib.load(fh)
179
+ section = data.get("tool", {}).get("scaffold-ca-python", {})
180
+ return ProjectContext(
181
+ name=section.get("name", root.name),
182
+ )