wizard-codegen 0.1.5__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.
@@ -0,0 +1,1068 @@
1
+ Metadata-Version: 2.4
2
+ Name: wizard-codegen
3
+ Version: 0.1.5
4
+ Summary: A powerful, template-driven code generation tool for Protocol Buffers
5
+ Requires-Python: >=3.10
6
+ Requires-Dist: click==8.3.1
7
+ Requires-Dist: jinja2==3.1.6
8
+ Requires-Dist: protobuf==6.33.2
9
+ Requires-Dist: pydantic-core==2.41.5
10
+ Requires-Dist: pydantic==2.12.5
11
+ Requires-Dist: pyyaml==6.0.3
12
+ Requires-Dist: rich==14.2.0
13
+ Requires-Dist: typer==0.20.1
14
+ Description-Content-Type: text/markdown
15
+
16
+ # ๐Ÿง™ Wizard Codegen
17
+
18
+ [![CircleCI](https://dl.circleci.com/status-badge/img/gh/ConsultingMD/wizard-codegen-experiment/tree/main.svg?style=svg&circle-token=CCIPRJ_6cYihJ2CPYtLrt8VjrCcSV_f02bbbce30f122a85da4e93588e8887e973128fd)](https://dl.circleci.com/status-badge/redirect/gh/ConsultingMD/wizard-codegen-experiment/tree/main)
19
+ [![codecov](https://codecov.io/gh/ConsultingMD/wizard-codegen-experiment/graph/badge.svg?token=NKzMlLPzqy)](https://codecov.io/gh/ConsultingMD/wizard-codegen-experiment)
20
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
21
+
22
+ > **A powerful, template-driven code generation tool for Protocol Buffers**
23
+
24
+ Wizard Codegen transforms your `.proto` definitions into typed code for multiple programming languages using customizable Jinja2 templates. Whether you're building React components, Swift structs, Kotlin data classes, or Go handlers โ€” this tool has you covered.
25
+
26
+ ---
27
+
28
+ ## ๐Ÿ“‘ Table of Contents
29
+
30
+ - [Features](#-features)
31
+ - [Quick Start](#-quick-start)
32
+ - [Installation](#-installation)
33
+ - [CLI Usage](#-cli-usage)
34
+ - [Configuration](#-configuration)
35
+ - [Proto Configuration](#proto-configuration)
36
+ - [Targets Configuration](#targets-configuration)
37
+ - [Hooks Configuration](#hooks-configuration)
38
+ - [Template Authoring](#-template-authoring)
39
+ - [Template Context](#template-context)
40
+ - [Name Transformations](#name-transformations)
41
+ - [Built-in Filters](#built-in-filters)
42
+ - [Custom Hooks](#-custom-hooks)
43
+ - [Architecture](#-architecture)
44
+ - [Developer Guide](#-developer-guide)
45
+ - [Project Structure](#project-structure)
46
+ - [Running Tests](#running-tests)
47
+ - [Adding New Target Languages](#adding-new-target-languages)
48
+ - [Contributing](#contributing)
49
+ - [Examples](#-examples)
50
+ - [Troubleshooting](#-troubleshooting)
51
+ - [License](#-license)
52
+
53
+ ---
54
+
55
+ ## โœจ Features
56
+
57
+ | Feature | Description |
58
+ |---------|-------------|
59
+ | **Multi-Language Support** | Generate code for TypeScript, Swift, Kotlin, Go, and more |
60
+ | **Jinja2 Templates** | Full power of Jinja2 templating with custom filters |
61
+ | **Flexible Filtering** | Target specific messages, enums, or services with `where` clauses |
62
+ | **Git Proto Sources** | Fetch proto definitions directly from Git repositories |
63
+ | **Multiple Write Modes** | `overwrite`, `append`, or `write-once` file generation |
64
+ | **Custom Hooks** | Extend with your own Jinja filters and helpers |
65
+ | **Name Transformations** | Auto-convert between `snake_case`, `kebab-case`, `PascalCase`, `camelCase`, `MACROCASE`, `MACRO_SNAKE_CASE` |
66
+ | **Dry Run Mode** | Preview changes without writing files |
67
+ | **Verbose Output** | Debug with detailed proto and context inspection |
68
+ | **Nested Type Support** | Full support for nested messages and enums |
69
+
70
+ ---
71
+
72
+ ## ๐Ÿš€ Quick Start
73
+
74
+ ```bash
75
+ # 1. Clone and setup
76
+ git clone <your-repo-url>
77
+ cd wizard-codegen
78
+ make deps
79
+
80
+ # 2. Create your configuration
81
+ cat > wizard/codegen.yaml << 'EOF'
82
+ proto:
83
+ cache_dir: ".cache/protos"
84
+ root: "path/to/your/protos"
85
+ files:
86
+ - "**/*.proto"
87
+
88
+ targets:
89
+ typescript:
90
+ templates: "wizard/templates/ts"
91
+ out: "src/generated/ts"
92
+ render:
93
+ - template: "form.j2"
94
+ for_each: "message"
95
+ where:
96
+ all:
97
+ - name: "*Form"
98
+ output: "components/{{ item.name.kebab_case }}/index.tsx"
99
+ EOF
100
+
101
+ # 3. Generate code
102
+ uv run wizard-codegen generate
103
+
104
+ # Or with verbose output
105
+ uv run wizard-codegen --verbose generate
106
+ ```
107
+
108
+ ---
109
+
110
+ ## ๐Ÿ“ฆ Installation
111
+
112
+ ### Prerequisites
113
+
114
+ - **Python 3.10+**
115
+ - **[uv](https://docs.astral.sh/uv/)** โ€” Fast Python package manager (installed via `ih-setup upgrade`)
116
+ - **protoc** (Protocol Buffer Compiler) โ€” [Installation Guide](https://grpc.io/docs/protoc-installation/)
117
+ - **Git** (for fetching proto sources from repositories)
118
+
119
+ ### Install via uv tool (Recommended)
120
+
121
+ ```bash
122
+ # Install protobuf compiler
123
+ brew install protobuf
124
+
125
+ # Install wizard-codegen from public PyPI
126
+ uv tool install wizard-codegen
127
+ ```
128
+
129
+ #### Upgrade
130
+
131
+ ```bash
132
+ uv tool upgrade wizard-codegen
133
+ ```
134
+
135
+ ### From Source
136
+
137
+ ```bash
138
+ # Clone the repository
139
+ git clone <repo-url>
140
+ cd wizard-codegen
141
+
142
+ # Install protobuf compiler
143
+ brew install protobuf
144
+
145
+ # Install dependencies
146
+ make deps
147
+
148
+ # Install the cli
149
+ make install
150
+
151
+ # Run the CLI
152
+ wizard-codegen --version
153
+ ```
154
+
155
+ #### Upgrade From Source
156
+ ```bash
157
+ make upgrade
158
+ ````
159
+
160
+ ### Dependencies
161
+
162
+ Dependencies are managed in `pyproject.toml` and locked in `uv.lock`:
163
+
164
+ ```
165
+ typer==0.20.1 # CLI framework
166
+ pydantic==2.12.5 # Configuration validation
167
+ jinja2==3.1.6 # Template engine
168
+ rich==14.2.0 # Beautiful terminal output
169
+ protobuf==6.33.2 # Proto descriptor handling
170
+ pyyaml==6.0.3 # YAML configuration
171
+ ```
172
+
173
+ ---
174
+
175
+ ## ๐Ÿ–ฅ๏ธ CLI Usage
176
+
177
+ The CLI is built with [Typer](https://typer.tiangolo.com/) and provides rich, colorful output.
178
+
179
+ ### Commands
180
+
181
+ #### `generate` โ€” Generate code from protos
182
+
183
+ ```bash
184
+ # Basic generation
185
+ wizard-codegen generate
186
+
187
+ # With custom config file
188
+ wizard-codegen --config path/to/codegen.yaml generate
189
+
190
+ # Dry run (preview without writing)
191
+ wizard-codegen --dry-run generate
192
+
193
+ # Verbose mode (detailed output)
194
+ wizard-codegen --verbose generate
195
+
196
+ # Combine flags
197
+ wizard-codegen --verbose --dry-run --config custom.yaml generate
198
+ ```
199
+
200
+ #### `list-protos` โ€” List discovered proto files
201
+
202
+ ```bash
203
+ wizard-codegen list-protos
204
+ ```
205
+
206
+ Output:
207
+ ```
208
+ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
209
+ โ”‚ Available proto schemas โ”‚
210
+ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
211
+ โ”‚ Name โ”‚ Path โ”‚
212
+ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
213
+ โ”‚ user โ”‚ user/user.proto โ”‚
214
+ โ”‚ user_form โ”‚ user/user_form.proto โ”‚
215
+ โ”‚ order โ”‚ order/order.proto โ”‚
216
+ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
217
+ ```
218
+
219
+ #### `validate` โ€” Validate templates and configuration
220
+
221
+ ```bash
222
+ wizard-codegen validate
223
+ ```
224
+
225
+ Validates:
226
+ - Configuration file syntax
227
+ - Proto file discovery
228
+ - Template syntax and variable resolution
229
+ - Descriptor set generation
230
+
231
+ ### Global Options
232
+
233
+ | Option | Short | Description |
234
+ |--------|-------|-------------|
235
+ | `--version` | `-V` | Show version and exit |
236
+ | `--config` | `-c` | Path to codegen YAML configuration (default: `wizard/codegen.yaml`) |
237
+ | `--verbose` | `-v` | Enable detailed logging and context printing |
238
+ | `--dry-run` | | Preview actions without writing files |
239
+ | `--local` | `-l` | Use local `proto.root` instead of git source |
240
+ | `--help` | | Show help message |
241
+
242
+ ---
243
+
244
+ ## โš™๏ธ Configuration
245
+
246
+ Configuration is defined in YAML format. The default location is `wizard/codegen.yaml`.
247
+
248
+ ### Complete Example
249
+
250
+ ```yaml
251
+ proto:
252
+ # Git source configuration (used by default)
253
+ source:
254
+ git: "git@github.com:YourOrg/proto-definitions.git"
255
+ ref: "latest-tag" # Default: auto-resolves to latest semver tag
256
+ # fds: "build/descriptor.pb" # Optional: use pre-built descriptor
257
+ include_info: true # Include source info in descriptors
258
+
259
+ # Local path (used with --local flag for development)
260
+ root: "../local-protos"
261
+
262
+ # Cache directory for git clones
263
+ cache_dir: ".cache/proto-common"
264
+
265
+ # Include paths (relative to proto root)
266
+ includes:
267
+ - "{proto_root}/shared"
268
+ - "{proto_root}/service/wizard"
269
+
270
+ # File patterns to discover
271
+ files:
272
+ - "pages/**/*.proto"
273
+ - "models/*.proto"
274
+
275
+ targets:
276
+ typescript:
277
+ templates: "wizard/templates/ts"
278
+ out: "src/generated/ts"
279
+ render:
280
+ # Generate form components for *Form messages
281
+ - template: "form.j2"
282
+ for_each: "message"
283
+ mode: "write-once"
284
+ where:
285
+ all:
286
+ - name: "*Form"
287
+ not:
288
+ - file: "*/shared/design_system/*"
289
+ output: "components/{{ item.name.kebab_case }}-form/index.tsx"
290
+
291
+ # Generate context providers for *Context messages
292
+ - template: "context.j2"
293
+ for_each: "message"
294
+ where:
295
+ any:
296
+ - name: "*Context"
297
+ not:
298
+ - file: "*/shared/design_system/*"
299
+ output: "contexts/{{ item.name.kebab_case }}/index.tsx"
300
+
301
+ swift:
302
+ templates: "wizard/templates/swift"
303
+ out: "ios/Generated"
304
+ render:
305
+ - template: "struct.j2"
306
+ for_each: "message"
307
+ output: "{{ item.name.pascal_case }}.swift"
308
+
309
+ kotlin:
310
+ templates: "wizard/templates/kotlin"
311
+ out: "android/generated"
312
+ render:
313
+ - template: "data_class.j2"
314
+ for_each: "message"
315
+ output: "{{ item.name.pascal_case }}.kt"
316
+
317
+ hooks:
318
+ root: "wizard"
319
+ module: "hook_sample" # Optional: custom filters module
320
+ ```
321
+
322
+ ### Proto Configuration
323
+
324
+ | Key | Type | Description |
325
+ |-----|------|-------------|
326
+ | `root` | `string` | Local path to proto files (used with `--local` flag) |
327
+ | `source.git` | `string` | Git repository URL for proto source |
328
+ | `source.ref` | `string` | Git ref: tag, branch, commit SHA, or `latest-tag` (default: `latest-tag`) |
329
+ | `source.fds` | `string` | Path to pre-built FileDescriptorSet (optional) |
330
+ | `source.include_info` | `bool` | Include source info in protoc output (default: `true`) |
331
+ | `cache_dir` | `string` | Directory for caching git clones |
332
+ | `includes` | `list[string]` | Include paths for proto imports |
333
+ | `files` | `list[string]` | Glob patterns for proto file discovery |
334
+
335
+ #### Special Ref: `latest-tag`
336
+
337
+ Use `ref: "latest-tag"` to automatically checkout the latest semver tag from the repository:
338
+
339
+ ```yaml
340
+ proto:
341
+ source:
342
+ git: "git@github.com:YourOrg/proto-definitions.git"
343
+ ref: "latest-tag" # Automatically resolves to latest semver tag (e.g., v2.1.0)
344
+ ```
345
+
346
+ This feature:
347
+ - Lists all tags in the repository
348
+ - Filters for valid semver tags (e.g., `v1.2.3`, `1.0.0`, `v2.0.0-beta`)
349
+ - Prefers stable releases over prereleases (e.g., `v1.0.0` > `v1.0.0-beta`)
350
+ - Checks out the highest version according to semver ordering
351
+
352
+ #### Proto Source Resolution
353
+
354
+ You can configure **both** a local path (`proto.root`) and a git source (`proto.source`) simultaneously. This allows developers to easily switch between tagged releases and local development:
355
+
356
+ **Default behavior (no flags):**
357
+ 1. `proto.source.git` โ€” clone/fetch from git repository
358
+ 2. Falls back to `proto.root` โ€” if no git source is configured
359
+ 3. Error โ€” if neither is available
360
+
361
+ **With `--local` flag:**
362
+ 1. `proto.root` โ€” use local filesystem path
363
+ 2. Error โ€” if `proto.root` is not configured or doesn't exist
364
+
365
+ This design enables a common workflow:
366
+ - **CI/Production**: Uses git source with `latest-tag` (the default ref)
367
+ - **Local Development**: Use `--local` flag to test against local proto changes
368
+
369
+ ```bash
370
+ # Use git source (default - fetches latest tag)
371
+ wizard-codegen generate
372
+
373
+ # Use local protos for development
374
+ wizard-codegen --local generate
375
+ ```
376
+
377
+ ### Targets Configuration
378
+
379
+ Each target represents a language/output configuration.
380
+
381
+ | Key | Type | Description |
382
+ |-----|------|-------------|
383
+ | `templates` | `string` | Path to Jinja2 template directory |
384
+ | `out` | `string` | Output directory for generated files |
385
+ | `render` | `list` | List of render rules |
386
+
387
+ #### Render Rules
388
+
389
+ | Key | Type | Description |
390
+ |-----|------|-------------|
391
+ | `template` | `string` | Template filename (relative to templates dir) |
392
+ | `output` | `string` | Output path pattern (Jinja2 template string) |
393
+ | `for_each` | `enum` | Iteration mode: `file`, `message`, `enum`, `service` |
394
+ | `mode` | `enum` | Write mode: `overwrite`, `append`, `write-once` |
395
+ | `where` | `object` | Filter predicates (optional) |
396
+
397
+ #### Write Modes
398
+
399
+ | Mode | Description |
400
+ |------|-------------|
401
+ | `overwrite` | Always replace existing files (default) |
402
+ | `append` | Add content to end of existing files |
403
+ | `write-once` | Only create if file doesn't exist |
404
+
405
+ #### Where Clauses (Filtering)
406
+
407
+ Filter which items to generate for:
408
+
409
+ ```yaml
410
+ where:
411
+ all: # AND conditions (all must match)
412
+ - name: "*Form"
413
+ - package: "wizard.*"
414
+ any: # OR conditions (at least one must match)
415
+ - name: "*Context"
416
+ - name: "*Provider"
417
+ not: # Exclude matching items
418
+ - file: "*/design_system/*"
419
+ - name: "*Internal"
420
+ ```
421
+
422
+ ##### Predicate Fields
423
+
424
+ | Field | Description | Example |
425
+ |-------|-------------|---------|
426
+ | `name` | Message/enum/service name | `"*Form"`, `"User*"`, `"Order"` |
427
+ | `package` | Proto package name | `"wizard.*"`, `"com.example.*"` |
428
+ | `file` | Proto file path | `"*/shared/*"`, `"user/*.proto"` |
429
+ | `full_name` | Fully qualified name | `".wizard.UserForm"` |
430
+ | `option.equals` | Match proto options | `{ key: "deprecated", value: true }` |
431
+
432
+ Patterns support:
433
+ - **Glob patterns**: `*`, `?`, `[abc]`
434
+ - **Regex patterns**: Any valid Python regex
435
+
436
+ ### Hooks Configuration
437
+
438
+ | Key | Type | Description |
439
+ |-----|------|-------------|
440
+ | `root` | `string` | Root directory for hooks module (default: `"wizard"`) |
441
+ | `module` | `string` | Python module name with `register()` function |
442
+
443
+ ---
444
+
445
+ ## ๐Ÿ“ Template Authoring
446
+
447
+ Templates use [Jinja2](https://jinja.palletsprojects.com/) syntax with custom extensions.
448
+
449
+ ### Template Context
450
+
451
+ When a template is rendered, it receives a rich context:
452
+
453
+ ```python
454
+ {
455
+ "proto_root": "/path/to/protos",
456
+
457
+ # Current item (when using for_each)
458
+ "item": {
459
+ "name": Name("UserForm"), # Name object with case transformations
460
+ "full_name": ".wizard.UserForm",
461
+ "file": "user/user_form.proto",
462
+ "package": "wizard",
463
+ "fields": [...], # List of field objects
464
+ "nested_messages": [...], # Nested message definitions
465
+ "nested_enums": [...], # Nested enum definitions
466
+ },
467
+
468
+ # All files (for cross-referencing)
469
+ "files": [...],
470
+
471
+ # Indexes for quick lookup
472
+ "message": { ".package.MessageName": {...}, ... },
473
+ "enum": { ".package.EnumName": {...}, ... },
474
+ "service": { ".package.ServiceName": {...}, ... },
475
+ "types": { ".package.TypeName": {...}, ... },
476
+ }
477
+ ```
478
+
479
+ #### File Object
480
+
481
+ ```python
482
+ {
483
+ "name": Name("user_form.proto"),
484
+ "basename": "user_form",
485
+ "package": "wizard",
486
+ "package_path": "wizard",
487
+ "imports": ["common/timestamp.proto"],
488
+ "messages": [...],
489
+ "enums": [...],
490
+ "services": [...],
491
+ "options": <FileOptions>,
492
+ }
493
+ ```
494
+
495
+ #### Message Object
496
+
497
+ ```python
498
+ {
499
+ "name": Name("UserForm"),
500
+ "full_name": ".wizard.UserForm",
501
+ "file": "user/user_form.proto",
502
+ "package": "wizard",
503
+ "fields": [
504
+ {
505
+ "name": Name("user_name"),
506
+ "number": 1,
507
+ "label": 1, # 1=optional, 2=required, 3=repeated
508
+ "type": 9, # Proto type number (9=string)
509
+ "type_name": "", # For message/enum refs: ".package.Type"
510
+ "json_name": "userName",
511
+ "field": <FieldDescriptor>,
512
+ },
513
+ ...
514
+ ],
515
+ "nested_messages": [...],
516
+ "nested_enums": [...],
517
+ }
518
+ ```
519
+
520
+ #### Enum Object
521
+
522
+ ```python
523
+ {
524
+ "name": Name("Status"),
525
+ "full_name": ".wizard.Status",
526
+ "file": "common/status.proto",
527
+ "package": "wizard",
528
+ "enum_values": [
529
+ {"name": "STATUS_UNKNOWN", "number": 0},
530
+ {"name": "STATUS_ACTIVE", "number": 1},
531
+ ...
532
+ ],
533
+ }
534
+ ```
535
+
536
+ #### Service Object
537
+
538
+ ```python
539
+ {
540
+ "name": Name("UserService"),
541
+ "file": "user/user_service.proto",
542
+ "package": "wizard",
543
+ "methods": [
544
+ {
545
+ "name": Name("GetUser"),
546
+ "input_type": ".wizard.GetUserRequest",
547
+ "output_type": ".wizard.User",
548
+ "client_streaming": False,
549
+ "server_streaming": False,
550
+ "options": <MethodOptions>,
551
+ },
552
+ ...
553
+ ],
554
+ }
555
+ ```
556
+
557
+ ### Name Transformations
558
+
559
+ Every name in the context is wrapped in a `Name` object that provides automatic case transformations:
560
+
561
+ ```jinja
562
+ {{ item.name.raw }} โ†’ UserFormRequest
563
+ {{ item.name.snake_case }} โ†’ user_form_request
564
+ {{ item.name.kebab_case }} โ†’ user-form-request
565
+ {{ item.name.pascal_case }} โ†’ UserFormRequest
566
+ {{ item.name.camel_case }} โ†’ userFormRequest
567
+ {{ item.name.macro_case }} โ†’ USERFORMREQUEST
568
+ {{ item.name.macro_snake_case }} โ†’ USER_FORM_REQUEST
569
+ ```
570
+
571
+ Use in output paths:
572
+ ```yaml
573
+ output: "{{ item.name.kebab_case }}/index.tsx"
574
+ # Generates: user-form-request/index.tsx
575
+ ```
576
+
577
+ Use in templates:
578
+ ```jinja
579
+ export interface {{ item.name.pascal_case }} {
580
+ {% for field in item.fields %}
581
+ {{ field.name.camel_case }}: {{ field | ts_type }};
582
+ {% endfor %}
583
+ }
584
+ ```
585
+
586
+ ### Built-in Filters
587
+
588
+ The following filters are available in all templates:
589
+
590
+ #### Common Filters
591
+
592
+ | Filter | Description | Example |
593
+ |--------|-------------|---------|
594
+ | `replace` | String replacement | `{{ name \| replace("_", "-") }}` |
595
+
596
+ #### TypeScript Filters (via hooks)
597
+
598
+ | Filter | Description | Example |
599
+ |--------|-------------|---------|
600
+ | `ts_type` | Proto to TS type | `{{ field \| ts_type }}` โ†’ `string`, `number`, `User` |
601
+ | `ts_type_optional` | TS type with undefined | `{{ field \| ts_type_optional }}` โ†’ `string \| undefined` |
602
+ | `is_ts_optional` | Check if optional | `{% if field \| is_ts_optional %}?{% endif %}` |
603
+ | `is_repeated` | Check if array | `{% if field \| is_repeated %}[]{% endif %}` |
604
+
605
+ #### Swift Filters (via hooks)
606
+
607
+ | Filter | Description | Example |
608
+ |--------|-------------|---------|
609
+ | `swift_type` | Proto to Swift type | `{{ field \| swift_type }}` โ†’ `String`, `Int32`, `User` |
610
+ | `swift_default` | Swift default value | `{{ field \| swift_default }}` โ†’ `""`, `0`, `[]` |
611
+
612
+ #### Kotlin Filters (via hooks)
613
+
614
+ | Filter | Description | Example |
615
+ |--------|-------------|---------|
616
+ | `kotlin_type` | Proto to Kotlin type | `{{ field \| kotlin_type }}` โ†’ `String`, `Int`, `User` |
617
+ | `kotlin_default` | Kotlin default value | `{{ field \| kotlin_default }}` โ†’ `""`, `0`, `emptyList()` |
618
+
619
+ #### Go Filters (via hooks)
620
+
621
+ | Filter | Description | Example |
622
+ |--------|-------------|---------|
623
+ | `go_type` | Proto to Go type | `{{ field \| go_type }}` โ†’ `string`, `int32`, `*User` |
624
+ | `go_zero` | Go zero value | `{{ field \| go_zero }}` โ†’ `""`, `0`, `nil` |
625
+ | `go_json_tag` | Go JSON struct tag | `{{ field \| go_json_tag }}` โ†’ `` `json:"userName,omitempty"` `` |
626
+
627
+ ### Template Example
628
+
629
+ ```jinja
630
+ // {{ item.name.pascal_case }}.swift
631
+ // Generated from: {{ item.file }}
632
+ // Package: {{ item.package }}
633
+
634
+ import Foundation
635
+
636
+ /// {{ item.name.pascal_case }} - Auto-generated from protobuf.
637
+ public struct {{ item.name.pascal_case }}: Codable, Equatable, Sendable {
638
+ {% for field in item.fields %}
639
+ /// {{ field.name.raw }} - Proto type: {{ field.type }}
640
+ public var {{ field.name.camel_case }}: {{ field | swift_type }}
641
+ {% endfor %}
642
+
643
+ public init(
644
+ {% for field in item.fields %}
645
+ {{ field.name.camel_case }}: {{ field | swift_type }} = {{ field | swift_default }}{% if not loop.last %},{% endif %}
646
+ {% endfor %}
647
+ ) {
648
+ {% for field in item.fields %}
649
+ self.{{ field.name.camel_case }} = {{ field.name.camel_case }}
650
+ {% endfor %}
651
+ }
652
+ }
653
+ ```
654
+
655
+ ---
656
+
657
+ ## ๐Ÿ”Œ Custom Hooks
658
+
659
+ Extend wizard-codegen with custom Jinja2 filters and helpers.
660
+
661
+ ### Creating a Hooks Module
662
+
663
+ 1. Create a Python file in your hooks root (default: `wizard/`):
664
+
665
+ ```python
666
+ # wizard/my_hooks.py
667
+
668
+ from jinja2 import Environment
669
+ from core import CodegenConfig
670
+
671
+ def custom_filter(value):
672
+ """Your custom filter logic."""
673
+ return str(value).upper()
674
+
675
+ def format_field_doc(field):
676
+ """Generate documentation for a field."""
677
+ return f"@param {field['name'].camel_case} - {field.get('json_name', '')}"
678
+
679
+ def register(env: Environment, *, target: str, config: CodegenConfig) -> None:
680
+ """
681
+ Register custom filters and globals.
682
+
683
+ Args:
684
+ env: Jinja2 environment to extend
685
+ target: Current target name (e.g., "typescript", "swift")
686
+ config: Full codegen configuration
687
+ """
688
+ # Register filters
689
+ env.filters["uppercase"] = custom_filter
690
+ env.filters["field_doc"] = format_field_doc
691
+
692
+ # Register globals (optional)
693
+ env.globals["TARGET"] = target
694
+
695
+ # Target-specific filters
696
+ if target == "typescript":
697
+ env.filters["ts_custom"] = lambda x: f"TS_{x}"
698
+ ```
699
+
700
+ 2. Reference it in your configuration:
701
+
702
+ ```yaml
703
+ hooks:
704
+ root: "wizard"
705
+ module: "my_hooks" # Loads wizard/my_hooks.py
706
+ ```
707
+
708
+ 3. Use in templates:
709
+
710
+ ```jinja
711
+ {{ item.name.raw | uppercase }}
712
+ {{ field | field_doc }}
713
+ Current target: {{ TARGET }}
714
+ ```
715
+
716
+ ### Type Mapping Example
717
+
718
+ Here's a complete example of type mapping hooks:
719
+
720
+ ```python
721
+ # wizard/type_mappings.py
722
+
723
+ from jinja2 import Environment
724
+
725
+ # Proto type constants
726
+ TYPE_STRING = 9
727
+ TYPE_BOOL = 8
728
+ TYPE_INT32 = 5
729
+ TYPE_INT64 = 3
730
+
731
+ TS_TYPES = {
732
+ TYPE_STRING: "string",
733
+ TYPE_BOOL: "boolean",
734
+ TYPE_INT32: "number",
735
+ TYPE_INT64: "bigint",
736
+ }
737
+
738
+ SWIFT_TYPES = {
739
+ TYPE_STRING: "String",
740
+ TYPE_BOOL: "Bool",
741
+ TYPE_INT32: "Int32",
742
+ TYPE_INT64: "Int64",
743
+ }
744
+
745
+ def ts_type(field):
746
+ if field.get("type_name"):
747
+ return field["type_name"].split(".")[-1]
748
+ return TS_TYPES.get(field.get("type"), "unknown")
749
+
750
+ def swift_type(field):
751
+ if field.get("type_name"):
752
+ return field["type_name"].split(".")[-1]
753
+ return SWIFT_TYPES.get(field.get("type"), "Any")
754
+
755
+ def register(env: Environment, *, target: str, config) -> None:
756
+ if target == "typescript":
757
+ env.filters["lang_type"] = ts_type
758
+ elif target == "swift":
759
+ env.filters["lang_type"] = swift_type
760
+ ```
761
+
762
+ ---
763
+
764
+ ## ๐Ÿ—๏ธ Architecture
765
+
766
+ ```
767
+ wizard-codegen/
768
+ โ”œโ”€โ”€ cli/ # CLI package
769
+ โ”‚ โ”œโ”€โ”€ __init__.py # Re-exports for backwards compatibility
770
+ โ”‚ โ””โ”€โ”€ main.py # CLI entry point (Typer app)
771
+ โ”œโ”€โ”€ core/ # Core business logic
772
+ โ”‚ โ”œโ”€โ”€ config.py # Configuration models (Pydantic)
773
+ โ”‚ โ”œโ”€โ”€ context_builder.py # Proto โ†’ Jinja context transformation
774
+ โ”‚ โ”œโ”€โ”€ filter.py # Where clause filtering
775
+ โ”‚ โ”œโ”€โ”€ renderer.py # Jinja2 template rendering
776
+ โ”‚ โ””โ”€โ”€ writer.py # File writing with modes
777
+ โ”œโ”€โ”€ proto/ # Protocol Buffer handling
778
+ โ”‚ โ”œโ”€โ”€ discover.py # Proto file discovery
779
+ โ”‚ โ”œโ”€โ”€ fds_loader.py # Descriptor set loading
780
+ โ”‚ โ”œโ”€โ”€ proto_source.py # Git checkout handling
781
+ โ”‚ โ””โ”€โ”€ protoc_runner.py # protoc execution
782
+ โ”œโ”€โ”€ hooks/ # Plugin system
783
+ โ”‚ โ””โ”€โ”€ hooks.py # Hook loading and protocol
784
+ โ”œโ”€โ”€ utils/ # Utilities
785
+ โ”‚ โ””โ”€โ”€ name.py # Name transformations
786
+ โ””โ”€โ”€ wizard/ # Example configuration
787
+ โ”œโ”€โ”€ codegen.yaml # Sample config
788
+ โ”œโ”€โ”€ hook_sample.py # Sample hooks
789
+ โ””โ”€โ”€ templates/ # Sample templates
790
+ ```
791
+
792
+ ### Pipeline Flow
793
+
794
+ ```
795
+ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
796
+ โ”‚ CLI (cli/main.py) โ”‚
797
+ โ”‚ parse args, load config โ”‚
798
+ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
799
+ โ”‚
800
+ โ–ผ
801
+ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
802
+ โ”‚ Proto Resolution (proto/) โ”‚
803
+ โ”‚ discover files โ†’ resolve git source โ†’ build descriptor set โ”‚
804
+ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
805
+ โ”‚
806
+ โ–ผ
807
+ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
808
+ โ”‚ Context Building (core/context_builder.py) โ”‚
809
+ โ”‚ FileDescriptorSet โ†’ Jinja-friendly dictionaries โ”‚
810
+ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
811
+ โ”‚
812
+ โ–ผ
813
+ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
814
+ โ”‚ Rendering (core/renderer.py) โ”‚
815
+ โ”‚ for each target โ†’ for each rule โ†’ filter items โ†’ render template โ”‚
816
+ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
817
+ โ”‚
818
+ โ–ผ
819
+ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
820
+ โ”‚ Writing (core/writer.py) โ”‚
821
+ โ”‚ apply write mode โ†’ hash comparison โ†’ write files โ”‚
822
+ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
823
+ ```
824
+
825
+ ---
826
+
827
+ ## ๐Ÿ‘ฉโ€๐Ÿ’ป Developer Guide
828
+
829
+ ### Project Structure
830
+
831
+ ```
832
+ wizard-codegen/
833
+ โ”œโ”€โ”€ cli/ # CLI package (main entry point)
834
+ โ”œโ”€โ”€ core/ # Core modules
835
+ โ”œโ”€โ”€ proto/ # Proto handling
836
+ โ”œโ”€โ”€ hooks/ # Plugin system
837
+ โ”œโ”€โ”€ utils/ # Utilities
838
+ โ”œโ”€โ”€ wizard/ # Example config and templates
839
+ โ”œโ”€โ”€ tests/ # Test suite
840
+ โ”‚ โ”œโ”€โ”€ fixtures/ # Test fixtures
841
+ โ”‚ โ”‚ โ”œโ”€โ”€ protos/ # Sample proto files
842
+ โ”‚ โ”‚ โ”œโ”€โ”€ templates/ # Test templates
843
+ โ”‚ โ”‚ โ”œโ”€โ”€ hooks/ # Test hooks
844
+ โ”‚ โ”‚ โ””โ”€โ”€ expected_outputs/ # Golden files
845
+ โ”‚ โ”œโ”€โ”€ test_*.py # Unit tests
846
+ โ”‚ โ””โ”€โ”€ test_e2e.py # End-to-end tests
847
+ โ”œโ”€โ”€ Makefile # Build automation
848
+ โ”œโ”€โ”€ pyproject.toml # Project config & dependencies
849
+ โ””โ”€โ”€ uv.lock # Locked dependencies
850
+ ```
851
+
852
+ ### Running Tests
853
+
854
+ ```bash
855
+ # Run all tests
856
+ make test
857
+
858
+ # Run with coverage
859
+ make coverage
860
+
861
+ # Run specific test file
862
+ make test PYTEST_ARGS=tests/test_context_builder.py
863
+
864
+ # Run specific test
865
+ make test PYTEST_ARGS="tests/test_e2e.py::TestFullPipelineGeneration -v"
866
+ ```
867
+
868
+ ### Code Coverage
869
+
870
+ [![codecov](https://codecov.io/gh/ConsultingMD/wizard-codegen-experiment/graphs/tree.svg?token=NKzMlLPzqy)](https://codecov.io/gh/ConsultingMD/wizard-codegen-experiment)
871
+
872
+ ### Test Categories
873
+
874
+ | Category | Description | Files |
875
+ |----------|-------------|-------|
876
+ | Unit Tests | Test individual functions | `test_config.py`, `test_filter.py`, `test_name.py` |
877
+ | Integration Tests | Test module interactions | `test_context_builder.py`, `test_renderer.py` |
878
+ | E2E Tests | Full pipeline tests | `test_e2e.py` |
879
+ | Fixture Tests | Golden file comparisons | `test_fixtures.py` |
880
+
881
+ ### Adding New Target Languages
882
+
883
+ 1. **Create templates** in `wizard/templates/<language>/`:
884
+
885
+ ```jinja
886
+ {# wizard/templates/rust/struct.j2 #}
887
+ // {{ item.name.pascal_case }}.rs
888
+
889
+ #[derive(Debug, Clone, Serialize, Deserialize)]
890
+ pub struct {{ item.name.pascal_case }} {
891
+ {% for field in item.fields %}
892
+ pub {{ field.name.snake_case }}: {{ field | rust_type }},
893
+ {% endfor %}
894
+ }
895
+ ```
896
+
897
+ 2. **Add type mapping hooks** (optional):
898
+
899
+ ```python
900
+ # wizard/rust_hooks.py
901
+
902
+ RUST_TYPES = {
903
+ 9: "String",
904
+ 8: "bool",
905
+ 5: "i32",
906
+ 3: "i64",
907
+ }
908
+
909
+ def rust_type(field):
910
+ if field.get("type_name"):
911
+ return field["type_name"].split(".")[-1]
912
+ return RUST_TYPES.get(field.get("type"), "Unknown")
913
+
914
+ def register(env, *, target, config):
915
+ if target == "rust":
916
+ env.filters["rust_type"] = rust_type
917
+ ```
918
+
919
+ 3. **Add target configuration**:
920
+
921
+ ```yaml
922
+ targets:
923
+ rust:
924
+ templates: "wizard/templates/rust"
925
+ out: "src/generated"
926
+ render:
927
+ - template: "struct.j2"
928
+ for_each: "message"
929
+ output: "{{ item.name.snake_case }}.rs"
930
+ ```
931
+
932
+ ### Contributing
933
+
934
+ 1. **Fork** the repository
935
+ 2. **Create** a feature branch: `git checkout -b feature/my-feature`
936
+ 3. **Write tests** for new functionality
937
+ 4. **Run** the test suite: `make test`
938
+ 5. **Submit** a pull request
939
+
940
+ #### Code Style
941
+
942
+ - Python 3.10+ type hints
943
+ - Black formatting (88 char line length)
944
+ - Docstrings for public functions
945
+ - Comprehensive tests for new features
946
+
947
+ ---
948
+
949
+ ## ๐Ÿ“š Examples
950
+
951
+ ### TypeScript React Form Component
952
+
953
+ ```jinja
954
+ {# templates/ts/form.j2 #}
955
+ import React, { useState } from 'react';
956
+
957
+ export interface {{ item.name.pascal_case }}Data {
958
+ {% for field in item.fields %}
959
+ {{ field.name.camel_case }}{% if field | is_ts_optional %}?{% endif %}: {{ field | ts_type }};
960
+ {% endfor %}
961
+ }
962
+
963
+ export const {{ item.name.pascal_case }}: React.FC = () => {
964
+ {% for field in item.fields %}
965
+ const [{{ field.name.camel_case }}, set{{ field.name.pascal_case }}] = useState<{{ field | ts_type }}>();
966
+ {% endfor %}
967
+
968
+ return (
969
+ <form>
970
+ {% for field in item.fields %}
971
+ <input
972
+ name="{{ field.name.snake_case }}"
973
+ value={ {{ field.name.camel_case }} ?? ''}
974
+ onChange={(e) => set{{ field.name.pascal_case }}(e.target.value)}
975
+ />
976
+ {% endfor %}
977
+ </form>
978
+ );
979
+ };
980
+ ```
981
+
982
+ ### Kotlin Data Class
983
+
984
+ ```jinja
985
+ {# templates/kotlin/data_class.j2 #}
986
+ package {{ item.package }}
987
+
988
+ import kotlinx.serialization.Serializable
989
+
990
+ @Serializable
991
+ data class {{ item.name.pascal_case }}(
992
+ {% for field in item.fields %}
993
+ val {{ field.name.camel_case }}: {{ field | kotlin_type }} = {{ field | kotlin_default }}{% if not loop.last %},{% endif %}
994
+ {% endfor %}
995
+ )
996
+ ```
997
+
998
+ ### Go Struct with JSON Tags
999
+
1000
+ ```jinja
1001
+ {# templates/go/struct.j2 #}
1002
+ package {{ item.package | replace(".", "_") }}
1003
+
1004
+ // {{ item.name.pascal_case }} - Generated from {{ item.file }}
1005
+ type {{ item.name.pascal_case }} struct {
1006
+ {% for field in item.fields %}
1007
+ {{ field.name.pascal_case }} {{ field | go_type }} {{ field | go_json_tag }}
1008
+ {% endfor %}
1009
+ }
1010
+ ```
1011
+
1012
+ ---
1013
+
1014
+ ## ๐Ÿ”ง Troubleshooting
1015
+
1016
+ ### Common Issues
1017
+
1018
+ #### "protoc is not available in PATH"
1019
+
1020
+ ```bash
1021
+ # macOS
1022
+ brew install protobuf
1023
+
1024
+ # Ubuntu/Debian
1025
+ apt install -y protobuf-compiler
1026
+
1027
+ # Verify installation
1028
+ protoc --version
1029
+ ```
1030
+
1031
+ #### "Config error: proto.root not found"
1032
+
1033
+ Either specify a local `proto.root` or configure `proto.source.git`:
1034
+
1035
+ ```yaml
1036
+ proto:
1037
+ root: "../my-protos" # Local path
1038
+ # OR
1039
+ source:
1040
+ git: "git@github.com:Org/protos.git"
1041
+ ref: "main"
1042
+ ```
1043
+
1044
+ #### "Failed to import hooks module"
1045
+
1046
+ Ensure your hooks module:
1047
+ 1. Is in the correct directory (`hooks.root` config)
1048
+ 2. Has a `register(env, *, target, config)` function
1049
+ 3. Has no import errors
1050
+
1051
+ ```bash
1052
+ # Test manually
1053
+ uv run python -c "import wizard.my_hooks"
1054
+ ```
1055
+
1056
+ #### "Template variable undefined"
1057
+
1058
+ - Use `--verbose` to inspect the context
1059
+ - Check your `for_each` setting matches the expected context
1060
+ - Verify field names in the proto file
1061
+
1062
+ <div align="center">
1063
+
1064
+ **Made with ๐Ÿง™ magic**
1065
+
1066
+ [Report Bug](../../issues) ยท [Request Feature](../../issues)
1067
+
1068
+ </div>