rhiza 0.3.0__tar.gz → 0.5.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 (75) hide show
  1. {rhiza-0.3.0 → rhiza-0.5.0}/.github/actions/setup-project/action.yml +2 -24
  2. rhiza-0.5.0/.github/copilot-instructions.md +349 -0
  3. rhiza-0.5.0/.github/scripts/sync.sh +310 -0
  4. rhiza-0.5.0/.github/template.yml +16 -0
  5. {rhiza-0.3.0 → rhiza-0.5.0}/.github/workflows/deptry.yml +1 -1
  6. {rhiza-0.3.0 → rhiza-0.5.0}/.github/workflows/marimo.yml +1 -1
  7. rhiza-0.5.0/.github/workflows/structure.yml +73 -0
  8. {rhiza-0.3.0 → rhiza-0.5.0}/.github/workflows/sync.yml +1 -1
  9. rhiza-0.5.0/CLI.md +311 -0
  10. rhiza-0.5.0/PKG-INFO +773 -0
  11. rhiza-0.5.0/README.md +740 -0
  12. rhiza-0.5.0/USAGE.md +638 -0
  13. {rhiza-0.3.0 → rhiza-0.5.0}/pyproject.toml +17 -5
  14. rhiza-0.5.0/src/rhiza/__init__.py +9 -0
  15. rhiza-0.5.0/src/rhiza/__main__.py +10 -0
  16. rhiza-0.5.0/src/rhiza/cli.py +115 -0
  17. rhiza-0.5.0/src/rhiza/commands/__init__.py +5 -0
  18. rhiza-0.5.0/src/rhiza/commands/init.py +66 -0
  19. rhiza-0.5.0/src/rhiza/commands/materialize.py +140 -0
  20. rhiza-0.5.0/src/rhiza/commands/validate.py +140 -0
  21. rhiza-0.5.0/src/rhiza/models.py +103 -0
  22. rhiza-0.5.0/tests/test_cli_commands.py +112 -0
  23. rhiza-0.5.0/tests/test_commands/test_init.py +105 -0
  24. rhiza-0.5.0/tests/test_commands/test_materialize.py +342 -0
  25. rhiza-0.5.0/tests/test_commands/test_validate.py +356 -0
  26. rhiza-0.5.0/tests/test_models.py +183 -0
  27. {rhiza-0.3.0 → rhiza-0.5.0}/uv.lock +92 -4
  28. rhiza-0.3.0/.devcontainer/bootstrap.sh +0 -38
  29. rhiza-0.3.0/.devcontainer/devcontainer.json +0 -48
  30. rhiza-0.3.0/.github/workflows/devcontainer.yml +0 -137
  31. rhiza-0.3.0/.github/workflows/docker.yml +0 -63
  32. rhiza-0.3.0/PKG-INFO +0 -709
  33. rhiza-0.3.0/README.md +0 -679
  34. rhiza-0.3.0/assets/rhiza-logo.svg +0 -81
  35. rhiza-0.3.0/book/marimo/df.py +0 -37
  36. rhiza-0.3.0/book/minibook-templates/custom.html.jinja2 +0 -210
  37. rhiza-0.3.0/book/pdoc-templates/module.html.jinja2 +0 -19
  38. rhiza-0.3.0/docker/Dockerfile +0 -64
  39. rhiza-0.3.0/docker/Dockerfile.dockerignore +0 -17
  40. rhiza-0.3.0/docker/README.md +0 -31
  41. {rhiza-0.3.0 → rhiza-0.5.0}/.editorconfig +0 -0
  42. {rhiza-0.3.0 → rhiza-0.5.0}/.github/renovate.json +0 -0
  43. {rhiza-0.3.0 → rhiza-0.5.0}/.github/scripts/book.sh +0 -0
  44. {rhiza-0.3.0 → rhiza-0.5.0}/.github/scripts/bump.sh +0 -0
  45. {rhiza-0.3.0 → rhiza-0.5.0}/.github/scripts/customisations/build-extras.sh +0 -0
  46. {rhiza-0.3.0 → rhiza-0.5.0}/.github/scripts/customisations/post-release.sh +0 -0
  47. {rhiza-0.3.0 → rhiza-0.5.0}/.github/scripts/marimushka.sh +0 -0
  48. {rhiza-0.3.0 → rhiza-0.5.0}/.github/scripts/release.sh +0 -0
  49. {rhiza-0.3.0 → rhiza-0.5.0}/.github/scripts/update-readme-help.sh +0 -0
  50. {rhiza-0.3.0 → rhiza-0.5.0}/.github/workflows/book.yml +0 -0
  51. {rhiza-0.3.0 → rhiza-0.5.0}/.github/workflows/ci.yml +0 -0
  52. {rhiza-0.3.0 → rhiza-0.5.0}/.github/workflows/pre-commit.yml +0 -0
  53. {rhiza-0.3.0 → rhiza-0.5.0}/.github/workflows/release.yml +0 -0
  54. {rhiza-0.3.0 → rhiza-0.5.0}/.github/workflows/scripts/version_matrix.py +0 -0
  55. {rhiza-0.3.0 → rhiza-0.5.0}/.github/workflows/scripts/version_max.py +0 -0
  56. {rhiza-0.3.0 → rhiza-0.5.0}/.gitignore +0 -0
  57. {rhiza-0.3.0 → rhiza-0.5.0}/.pre-commit-config.yaml +0 -0
  58. {rhiza-0.3.0 → rhiza-0.5.0}/CODE_OF_CONDUCT.md +0 -0
  59. {rhiza-0.3.0 → rhiza-0.5.0}/CONTRIBUTING.md +0 -0
  60. {rhiza-0.3.0 → rhiza-0.5.0}/LICENSE +0 -0
  61. {rhiza-0.3.0 → rhiza-0.5.0}/Makefile +0 -0
  62. {rhiza-0.3.0/src/rhiza → rhiza-0.5.0/book/marimo}/.gitkeep +0 -0
  63. {rhiza-0.3.0 → rhiza-0.5.0}/pytest.ini +0 -0
  64. {rhiza-0.3.0 → rhiza-0.5.0}/ruff.toml +0 -0
  65. {rhiza-0.3.0 → rhiza-0.5.0}/tests/test_rhiza/README.md +0 -0
  66. {rhiza-0.3.0 → rhiza-0.5.0}/tests/test_rhiza/conftest.py +0 -0
  67. {rhiza-0.3.0 → rhiza-0.5.0}/tests/test_rhiza/test_bump_script.py +0 -0
  68. {rhiza-0.3.0 → rhiza-0.5.0}/tests/test_rhiza/test_docstrings.py +0 -0
  69. {rhiza-0.3.0 → rhiza-0.5.0}/tests/test_rhiza/test_git_repo_fixture.py +0 -0
  70. {rhiza-0.3.0 → rhiza-0.5.0}/tests/test_rhiza/test_makefile.py +0 -0
  71. {rhiza-0.3.0 → rhiza-0.5.0}/tests/test_rhiza/test_marimushka_script.py +0 -0
  72. {rhiza-0.3.0 → rhiza-0.5.0}/tests/test_rhiza/test_readme.py +0 -0
  73. {rhiza-0.3.0 → rhiza-0.5.0}/tests/test_rhiza/test_release_script.py +0 -0
  74. {rhiza-0.3.0 → rhiza-0.5.0}/tests/test_rhiza/test_structure.py +0 -0
  75. {rhiza-0.3.0 → rhiza-0.5.0}/tests/test_rhiza/test_updatereadme_script.py +0 -0
@@ -4,8 +4,7 @@
4
4
  # Action: Setup Project (composite action)
5
5
  #
6
6
  # Purpose: Bootstrap a Python project in GitHub Actions by:
7
- # - Installing Task, uv, and uvx into a local ./bin directory
8
- # - Optionally rendering the repository from a Copier template for tests
7
+ # - Installing Task, uv, and uvx
9
8
  # - Detecting presence of pyproject.toml and exposing it as an output
10
9
  # - Creating a virtual environment with uv and syncing dependencies
11
10
  #
@@ -43,28 +42,7 @@ runs:
43
42
  - name: Install uv
44
43
  uses: astral-sh/setup-uv@v7
45
44
  with:
46
- version: "0.9.17"
47
-
48
- - name: Render the project
49
- if: hashFiles('tests/resources/render.yml') != ''
50
- shell: bash
51
- run: |
52
- pip install copier
53
-
54
- # Render the current folder in-place
55
- copier copy . . \
56
- --data-file ./tests/resources/render.yml \
57
- --force \
58
- --overwrite \
59
- --quiet
60
-
61
- # Delete all remaining .jinja files
62
- find . -type f -name "*.jinja" -exec rm -f {} +
63
-
64
- # Delete all folders that still contain {{ ... }}
65
- find . -depth -type d -name "*{{*}}*" -exec rm -rf {} +
66
-
67
- ls -R
45
+ version: "0.9.18"
68
46
 
69
47
  - name: Check for pyproject.toml
70
48
  id: check_pyproject
@@ -0,0 +1,349 @@
1
+ # GitHub Copilot Instructions for rhiza-cli
2
+
3
+ ## Project Overview
4
+
5
+ Rhiza is a command-line interface (CLI) tool for managing reusable configuration templates for modern Python projects. It provides commands for initializing, validating, and materializing configuration templates across projects.
6
+
7
+ **Repository:** <https://github.com/jebel-quant/rhiza-cli>
8
+
9
+ ## Technology Stack
10
+
11
+ - **Language:** Python 3.11+ (supports 3.11, 3.12, 3.13, 3.14)
12
+ - **Package Manager:** uv (fast Python package installer and resolver)
13
+ - **CLI Framework:** Typer
14
+ - **Testing:** pytest with coverage reporting
15
+ - **Linting/Formatting:** Ruff
16
+ - **Build System:** Hatchling
17
+ - **Pre-commit Hooks:** YAML/TOML validation, Ruff, markdownlint, actionlint
18
+
19
+ ## Project Structure
20
+
21
+ ```text
22
+ rhiza-cli/
23
+ ├── src/rhiza/ # Main source code
24
+ │ ├── cli.py # CLI entry points (Typer app)
25
+ │ └── commands/ # Command implementations
26
+ ├── tests/ # Test suite
27
+ ├── book/ # Documentation and Marimo notebooks
28
+ ├── .github/ # GitHub workflows and scripts
29
+ ├── pyproject.toml # Project configuration
30
+ ├── ruff.toml # Linting configuration
31
+ └── Makefile # Development tasks
32
+ ```
33
+
34
+ ## Coding Standards
35
+
36
+ ### Python Style
37
+
38
+ - **Line length:** Maximum 120 characters
39
+ - **Quotes:** Use double quotes for strings
40
+ - **Indentation:** 4 spaces (no tabs)
41
+ - **Docstrings:** Google style convention (required for all public modules, classes, and functions)
42
+ - **Type hints:** Not strictly enforced but encouraged
43
+ - **Import sorting:** Automatic via isort (part of Ruff)
44
+
45
+ ### Linting Rules
46
+
47
+ The project uses Ruff with the following rule sets:
48
+
49
+ - **D** (pydocstyle): Docstring style enforcement
50
+ - **E** (pycodestyle): PEP 8 style guide errors
51
+ - **F** (pyflakes): Logical error detection
52
+ - **I** (isort): Import sorting
53
+ - **N** (pep8-naming): PEP 8 naming conventions
54
+ - **W** (pycodestyle): PEP 8 warnings
55
+ - **UP** (pyupgrade): Modern Python syntax
56
+
57
+ **Exception:** Tests allow assert statements (S101 ignored in tests/)
58
+
59
+ ### Docstring Requirements
60
+
61
+ - All public modules, classes, functions, and methods must have docstrings
62
+ - Use Google docstring convention
63
+ - Include magic methods like `__init__` (D105, D107 enforced)
64
+ - Use multi-line format with summary line, then blank line, then details
65
+
66
+ Example:
67
+
68
+ ```python
69
+ def my_function(arg1: str, arg2: int) -> bool:
70
+ """Short summary of what the function does.
71
+
72
+ Longer description if needed. Explain complex behavior,
73
+ side effects, or important context.
74
+
75
+ Args:
76
+ arg1: Description of arg1
77
+ arg2: Description of arg2
78
+
79
+ Returns:
80
+ Description of return value (bool)
81
+ """
82
+ return True
83
+ ```
84
+
85
+ ## Development Workflow
86
+
87
+ ### Setup
88
+
89
+ ```bash
90
+ make install # Install dependencies with uv
91
+ ```
92
+
93
+ ### Common Commands
94
+
95
+ ```bash
96
+ make fmt # Run linters and formatters (pre-commit)
97
+ make test # Run tests with coverage
98
+ make docs # Generate documentation with pdoc
99
+ make clean # Clean build artifacts
100
+ make help # Show all available commands
101
+ ```
102
+
103
+ ### Testing
104
+
105
+ - Use pytest for all tests
106
+ - Place tests in `tests/` directory
107
+ - Test files should match pattern `test_*.py`
108
+ - Aim for good coverage of new code
109
+ - Run tests with `make test` before submitting changes
110
+
111
+ ### Pre-commit Hooks
112
+
113
+ The project uses pre-commit hooks that run automatically on commit:
114
+
115
+ - YAML/TOML validation
116
+ - Ruff linting and formatting
117
+ - Markdown linting (MD013 disabled for long lines)
118
+ - GitHub workflow validation
119
+ - Renovate config validation
120
+ - README.md auto-update with Makefile help
121
+
122
+ ## Architecture Notes
123
+
124
+ ### CLI Structure
125
+
126
+ The CLI uses Typer for command definitions. Commands are thin wrappers in `cli.py` that delegate to implementations in `rhiza.commands.*`:
127
+
128
+ - `init`: Initialize or validate `.github/template.yml`
129
+ - `materialize` (alias `inject`): Apply templates to a target repository
130
+ - `validate`: Validate template configuration
131
+
132
+ ### Command Implementation Pattern
133
+
134
+ 1. Command defined in `src/rhiza/cli.py` using Typer decorators
135
+ 2. Implementation logic in `src/rhiza/commands/*.py`
136
+ 3. Commands use `loguru` for logging
137
+ 4. Use `Path` from `pathlib` for file operations
138
+
139
+ ## Best Practices
140
+
141
+ 1. **Minimal changes:** Make surgical, focused changes
142
+ 2. **Type hints:** Use when they improve clarity
143
+ 3. **Error handling:** Use appropriate exceptions, log errors clearly
144
+ 4. **Documentation:** Update docstrings when changing function signatures
145
+ 5. **Tests:** Add tests for new functionality
146
+ 6. **Imports:** Keep imports organized (isort handles this automatically)
147
+ 7. **File headers:** Include repository attribution comment at top of new files:
148
+
149
+ ```python
150
+ # This file is part of the jebel-quant/rhiza repository
151
+ # (https://github.com/jebel-quant/rhiza).
152
+ #
153
+ ```
154
+
155
+ ## Dependencies
156
+
157
+ ### Core Dependencies
158
+
159
+ See `pyproject.toml` for exact versions. Key dependencies:
160
+
161
+ - `typer` - CLI framework
162
+ - `loguru` - Logging
163
+ - `PyYAML` - YAML parsing
164
+
165
+ ### Development Dependencies
166
+
167
+ See `pyproject.toml` for complete list. Key dev dependencies:
168
+
169
+ - `pytest`, `pytest-cov`, `pytest-html` - Testing
170
+ - `pre-commit` - Git hooks
171
+ - `marimo` - Notebook support
172
+ - `pdoc` - Documentation generation
173
+
174
+ ## Common Patterns
175
+
176
+ ### Path Handling
177
+
178
+ ```python
179
+ from pathlib import Path
180
+
181
+ target = Path(".") # Use Path objects, not strings
182
+ if target.exists():
183
+ # Do something
184
+ ```
185
+
186
+ ### Logging
187
+
188
+ ```python
189
+ from loguru import logger
190
+
191
+ logger.info("Starting operation")
192
+ logger.error("Something went wrong")
193
+ ```
194
+
195
+ ### CLI Arguments
196
+
197
+ ```python
198
+ import typer
199
+
200
+ @app.command()
201
+ def my_command(
202
+ target: Path = typer.Argument(
203
+ default=Path("."),
204
+ exists=True,
205
+ help="Description"
206
+ ),
207
+ ):
208
+ """Command docstring."""
209
+ ```
210
+
211
+ ## Security Considerations
212
+
213
+ - **No secrets in code:** Never commit API keys, passwords, or sensitive data
214
+ - **Path traversal:** Always use `Path.resolve()` to normalize paths and prevent directory traversal attacks
215
+ - **Input validation:** Validate all user inputs, especially file paths and command arguments
216
+ - **YAML parsing:** Use safe YAML loading (PyYAML uses safe loading by default)
217
+ - **File permissions:** Be mindful of file permissions when creating files
218
+
219
+ ## Error Handling Patterns
220
+
221
+ ### Exception Handling
222
+
223
+ ```python
224
+ from loguru import logger
225
+ from pathlib import Path
226
+
227
+ def safe_operation(path: Path):
228
+ """Safe operation with proper error handling."""
229
+ try:
230
+ # Normalize path to prevent traversal
231
+ path = path.resolve()
232
+
233
+ if not path.exists():
234
+ logger.error(f"Path does not exist: {path}")
235
+ raise FileNotFoundError(f"Path not found: {path}")
236
+
237
+ # Perform operation
238
+ return True
239
+
240
+ except PermissionError as e:
241
+ logger.error(f"Permission denied: {e}")
242
+ raise
243
+ except Exception as e:
244
+ logger.error(f"Unexpected error: {e}")
245
+ raise
246
+ ```
247
+
248
+ ### CLI Exit Codes
249
+
250
+ Use Typer's `Exit` for non-zero exit codes on errors:
251
+
252
+ ```python
253
+ import typer
254
+
255
+ if not success:
256
+ raise typer.Exit(code=1)
257
+ ```
258
+
259
+ ## Common Tasks
260
+
261
+ ### Adding a New Command
262
+
263
+ 1. Create a new file in `src/rhiza/commands/` (e.g., `newcommand.py`)
264
+ 2. Implement the command logic with proper docstrings
265
+ 3. Add a wrapper in `src/rhiza/cli.py` using Typer decorators
266
+ 4. Add tests in `tests/` for the new command
267
+ 5. Update documentation if needed
268
+
269
+ Example:
270
+
271
+ ```python
272
+ # In src/rhiza/commands/newcommand.py
273
+ from pathlib import Path
274
+ from loguru import logger
275
+
276
+ def my_new_command(target: Path):
277
+ """Execute the new command.
278
+
279
+ Parameters
280
+ ----------
281
+ target:
282
+ Path to the target directory.
283
+ """
284
+ target = target.resolve()
285
+ logger.info(f"Running new command on: {target}")
286
+ # Implementation here
287
+ ```
288
+
289
+ ```python
290
+ # In src/rhiza/cli.py
291
+ from rhiza.commands.newcommand import my_new_command
292
+
293
+ @app.command()
294
+ def newcommand(
295
+ target: Path = typer.Argument(
296
+ default=Path("."),
297
+ exists=True,
298
+ file_okay=False,
299
+ dir_okay=True,
300
+ help="Target directory"
301
+ ),
302
+ ):
303
+ """Short description of the command."""
304
+ my_new_command(target)
305
+ ```
306
+
307
+ ### Running the CLI in Development
308
+
309
+ ```bash
310
+ # Install in editable mode
311
+ make install
312
+
313
+ # Run the CLI
314
+ uv run rhiza --help
315
+ uv run rhiza init
316
+ uv run rhiza materialize --branch main
317
+ ```
318
+
319
+ ## Troubleshooting
320
+
321
+ ### Common Issues
322
+
323
+ **Import errors after adding dependencies:**
324
+ - Run `make install` to sync dependencies
325
+ - Ensure `pyproject.toml` is updated with new dependencies
326
+
327
+ **Linting failures:**
328
+ - Run `make fmt` to auto-fix most issues
329
+ - Check `ruff.toml` for configured rules
330
+ - Ensure docstrings follow Google convention
331
+
332
+ **Test failures:**
333
+ - Run `make test` to see detailed output
334
+ - Check test coverage report in `_tests/html-coverage/`
335
+ - Ensure new code has corresponding tests
336
+
337
+ **Pre-commit hook failures:**
338
+ - Run `make fmt` to fix formatting issues
339
+ - Check `.pre-commit-config.yaml` for hook configuration
340
+ - Install hooks with `uv run pre-commit install`
341
+
342
+ ## When Making Changes
343
+
344
+ 1. Run `make fmt` to ensure code follows style guidelines
345
+ 2. Run `make test` to verify tests pass
346
+ 3. Update docstrings if changing public APIs
347
+ 4. Add tests for new functionality
348
+ 5. Keep changes focused and minimal
349
+ 6. Follow existing code patterns and conventions
@@ -0,0 +1,310 @@
1
+ #!/bin/sh
2
+ # Sync configuration files from template repository
3
+ # - Reads configuration from .github/template.yml
4
+ # - Downloads specified files from the template repository
5
+ # - Copies them to the current repository
6
+ #
7
+ # This script is POSIX-sh compatible and provides manual sync capability
8
+ # for repositories that use jebel-quant/rhiza as a template.
9
+
10
+ set -e
11
+
12
+ BLUE="\033[36m"
13
+ RED="\033[31m"
14
+ GREEN="\033[32m"
15
+ YELLOW="\033[33m"
16
+ RESET="\033[0m"
17
+
18
+ TEMPLATE_CONFIG=".github/template.yml"
19
+ TEMP_DIR="/tmp/rhiza-sync-$$"
20
+
21
+ show_usage() {
22
+ printf "Usage: %s [OPTIONS]\n\n" "$0"
23
+ printf "Sync configuration files from the template repository.\n\n"
24
+ printf "Options:\n"
25
+ printf " -h, --help Show this help message\n"
26
+ printf " --dry-run Show what would be synced without making changes\n\n"
27
+ printf "Configuration:\n"
28
+ printf " Reads from %s to determine:\n" "$TEMPLATE_CONFIG"
29
+ printf " - template-repository: Source repository (e.g., 'jebel-quant/rhiza')\n"
30
+ printf " - template-branch: Branch to sync from (e.g., 'main')\n"
31
+ printf " - include: Files/directories to sync\n"
32
+ printf " - exclude: Files/directories to skip (optional)\n\n"
33
+ printf "Example %s:\n" "$TEMPLATE_CONFIG"
34
+ printf " template-repository: \"jebel-quant/rhiza\"\n"
35
+ printf " template-branch: \"main\"\n"
36
+ printf " include: |\n"
37
+ printf " .github\n"
38
+ printf " Makefile\n"
39
+ printf " ruff.toml\n"
40
+ }
41
+
42
+ DRY_RUN=""
43
+ while [ $# -gt 0 ]; do
44
+ case "$1" in
45
+ -h|--help)
46
+ show_usage
47
+ exit 0
48
+ ;;
49
+ --dry-run)
50
+ DRY_RUN="true"
51
+ shift
52
+ ;;
53
+ *)
54
+ printf "%b[ERROR] Unknown option: %s%b\n" "$RED" "$1" "$RESET"
55
+ show_usage
56
+ exit 1
57
+ ;;
58
+ esac
59
+ done
60
+
61
+ # Check if template.yml exists
62
+ if [ ! -f "$TEMPLATE_CONFIG" ]; then
63
+ printf "%b[ERROR] Template configuration not found: %s%b\n" "$RED" "$TEMPLATE_CONFIG" "$RESET"
64
+ printf "\nThis repository is not configured for template syncing.\n"
65
+ printf "Create %s with the following content:\n\n" "$TEMPLATE_CONFIG"
66
+ printf " template-repository: \"jebel-quant/rhiza\"\n"
67
+ printf " template-branch: \"main\"\n"
68
+ printf " include: |\n"
69
+ printf " .github\n"
70
+ printf " Makefile\n"
71
+ printf " ruff.toml\n"
72
+ exit 1
73
+ fi
74
+
75
+ # Parse template.yml to extract configuration
76
+ # This is a simple parser that works with basic YAML structure
77
+ printf "%b[INFO] Reading configuration from %s...%b\n" "$BLUE" "$TEMPLATE_CONFIG" "$RESET"
78
+
79
+ TEMPLATE_REPO=""
80
+ TEMPLATE_BRANCH=""
81
+ INCLUDE_LIST=""
82
+ EXCLUDE_LIST=""
83
+
84
+ # Simple YAML parser
85
+ in_multiline=""
86
+
87
+ while IFS= read -r line || [ -n "$line" ]; do
88
+ # Skip comments and empty lines
89
+ case "$line" in
90
+ \#*|"") continue ;;
91
+ esac
92
+
93
+ # Check if line starts with spaces by looking at first character
94
+ first_char=$(printf '%s' "$line" | cut -c1)
95
+
96
+ if [ "$first_char" = " " ] || [ "$first_char" = "$(printf '\t')" ]; then
97
+ # This is indented content (part of a multiline block)
98
+ if [ -n "$in_multiline" ]; then
99
+ # Remove leading spaces
100
+ item=$(echo "$line" | sed 's/^[[:space:]]*//')
101
+ if [ -n "$item" ]; then
102
+ if [ "$in_multiline" = "include" ]; then
103
+ INCLUDE_LIST="${INCLUDE_LIST}${item}
104
+ "
105
+ elif [ "$in_multiline" = "exclude" ]; then
106
+ EXCLUDE_LIST="${EXCLUDE_LIST}${item}
107
+ "
108
+ fi
109
+ fi
110
+ fi
111
+ else
112
+ # This is a key line (not indented)
113
+ case "$line" in
114
+ template-repository:*)
115
+ TEMPLATE_REPO=$(echo "$line" | sed 's/^template-repository:[[:space:]]*//' | sed 's/"//g' | sed "s/'//g")
116
+ in_multiline=""
117
+ ;;
118
+ template-branch:*)
119
+ TEMPLATE_BRANCH=$(echo "$line" | sed 's/^template-branch:[[:space:]]*//' | sed 's/"//g' | sed "s/'//g")
120
+ in_multiline=""
121
+ ;;
122
+ include:*)
123
+ in_multiline="include"
124
+ ;;
125
+ exclude:*)
126
+ in_multiline="exclude"
127
+ ;;
128
+ esac
129
+ fi
130
+ done < "$TEMPLATE_CONFIG"
131
+
132
+ # Validate required fields
133
+ if [ -z "$TEMPLATE_REPO" ]; then
134
+ printf "%b[ERROR] template-repository not found in %s%b\n" "$RED" "$TEMPLATE_CONFIG" "$RESET"
135
+ exit 1
136
+ fi
137
+
138
+ if [ -z "$TEMPLATE_BRANCH" ]; then
139
+ printf "%b[WARN] template-branch not specified, using 'main'%b\n" "$YELLOW" "$RESET"
140
+ TEMPLATE_BRANCH="main"
141
+ fi
142
+
143
+ if [ -z "$INCLUDE_LIST" ]; then
144
+ printf "%b[ERROR] include list is empty in %s%b\n" "$RED" "$TEMPLATE_CONFIG" "$RESET"
145
+ exit 1
146
+ fi
147
+
148
+ printf "%b[INFO] Template repository: %s%b\n" "$GREEN" "$TEMPLATE_REPO" "$RESET"
149
+ printf "%b[INFO] Template branch: %s%b\n" "$GREEN" "$TEMPLATE_BRANCH" "$RESET"
150
+ printf "%b[INFO] Files to sync:%b\n" "$GREEN" "$RESET"
151
+ echo "$INCLUDE_LIST" | while IFS= read -r item; do
152
+ [ -n "$item" ] && printf " - %s\n" "$item"
153
+ done || true
154
+
155
+ if [ -n "$EXCLUDE_LIST" ]; then
156
+ printf "%b[INFO] Files to exclude:%b\n" "$YELLOW" "$RESET"
157
+ echo "$EXCLUDE_LIST" | while IFS= read -r item; do
158
+ [ -n "$item" ] && printf " - %s\n" "$item"
159
+ done || true
160
+ fi
161
+
162
+ if [ -n "$DRY_RUN" ]; then
163
+ printf "\n%b[DRY RUN] No changes will be made%b\n" "$YELLOW" "$RESET"
164
+ exit 0
165
+ fi
166
+
167
+ # Create temporary directory
168
+ mkdir -p "$TEMP_DIR"
169
+ trap 'rm -rf "$TEMP_DIR"' EXIT INT TERM
170
+
171
+ # Backup this script to avoid being overwritten during sync
172
+ SELF_SCRIPT=".github/scripts/sync.sh"
173
+ if [ -f "$SELF_SCRIPT" ]; then
174
+ cp "$SELF_SCRIPT" "$TEMP_DIR/sync.sh.bak"
175
+ fi
176
+
177
+ # Clone the template repository
178
+ printf "\n%b[INFO] Cloning template repository...%b\n" "$BLUE" "$RESET"
179
+ REPO_URL="https://github.com/${TEMPLATE_REPO}.git"
180
+
181
+ if ! git clone --depth 1 --branch "$TEMPLATE_BRANCH" "$REPO_URL" "$TEMP_DIR/template" 2>/dev/null; then
182
+ printf "%b[ERROR] Failed to clone template repository from %s%b\n" "$RED" "$REPO_URL" "$RESET"
183
+ exit 1
184
+ fi
185
+
186
+ # Function to check if a file path should be excluded
187
+ is_file_excluded() {
188
+ file_path="$1"
189
+ if [ -z "$EXCLUDE_LIST" ]; then
190
+ return 1 # Not excluded (false)
191
+ fi
192
+
193
+ while IFS= read -r exclude_item || [ -n "$exclude_item" ]; do
194
+ [ -z "$exclude_item" ] && continue
195
+ if [ "$file_path" = "$exclude_item" ]; then
196
+ return 0 # Is excluded (true)
197
+ fi
198
+ done <<EOF_EXCLUDE_CHECK
199
+ $EXCLUDE_LIST
200
+ EOF_EXCLUDE_CHECK
201
+
202
+ return 1 # Not excluded (false)
203
+ }
204
+
205
+ # Copy files from template to current directory
206
+ printf "%b[INFO] Syncing files...%b\n" "$BLUE" "$RESET"
207
+
208
+ synced_count=0
209
+ skipped_count=0
210
+ # Track whether .github (containing this script) was synced and whether a direct self update was requested
211
+ synced_dotgithub="false"
212
+ deferred_self_update="false"
213
+
214
+ # Use here-document instead of pipeline to avoid subshell
215
+ while IFS= read -r item || [ -n "$item" ]; do
216
+ [ -z "$item" ] && continue
217
+
218
+ # Check if this item is in the exclude list
219
+ if is_file_excluded "$item"; then
220
+ printf " %b[SKIP]%b %s (excluded)\n" "$YELLOW" "$RESET" "$item"
221
+ skipped_count=$((skipped_count + 1))
222
+ continue
223
+ fi
224
+
225
+ # Defer updating this script to the very end to avoid mid-run overwrite
226
+ if [ "$item" = ".github/scripts/sync.sh" ]; then
227
+ deferred_self_update="true"
228
+ printf " %b[DEFER]%b %s (will update at end)\n" "$YELLOW" "$RESET" "$item"
229
+ continue
230
+ fi
231
+
232
+ src_path="$TEMP_DIR/template/$item"
233
+ dest_path="./$item"
234
+
235
+ if [ -e "$src_path" ]; then
236
+ # Create parent directory if needed
237
+ dest_dir=$(dirname "$dest_path")
238
+ mkdir -p "$dest_dir"
239
+
240
+ # Copy the file or directory
241
+ if [ -d "$src_path" ]; then
242
+ # Ensure destination directory exists
243
+ mkdir -p "$dest_path"
244
+ # Copy contents of the source directory into the destination directory
245
+ # to avoid nesting (e.g., .github/.github or tests/tests)
246
+ cp -R "$src_path"/. "$dest_path"/
247
+ # Mark if we synced the .github directory so we can safely update sync.sh at the end
248
+ if [ "$item" = ".github" ]; then
249
+ synced_dotgithub="true"
250
+ fi
251
+
252
+ # Remove excluded files from the copied directory
253
+ if [ -n "$EXCLUDE_LIST" ]; then
254
+ while IFS= read -r exclude_item || [ -n "$exclude_item" ]; do
255
+ [ -z "$exclude_item" ] && continue
256
+ # Check if the excluded item is a child of the current item
257
+ # e.g., if item=".github" and exclude_item=".github/workflows/docker.yml"
258
+ case "$exclude_item" in
259
+ "$item"/*)
260
+ # This is a nested file that should be excluded
261
+ excluded_file_path="./$exclude_item"
262
+ if [ -e "$excluded_file_path" ]; then
263
+ rm -rf "$excluded_file_path"
264
+ printf " %b[EXCLUDE]%b %s (removed from synced directory)\n" "$YELLOW" "$RESET" "$exclude_item"
265
+ fi
266
+ ;;
267
+ esac
268
+ done <<EOF_EXCLUDE_NESTED
269
+ $EXCLUDE_LIST
270
+ EOF_EXCLUDE_NESTED
271
+ fi
272
+
273
+ # If we just synced the .github directory, restore this script immediately to avoid mid-run overwrite issues
274
+ if [ "$item" = ".github" ] && [ -f "$TEMP_DIR/sync.sh.bak" ]; then
275
+ cp "$TEMP_DIR/sync.sh.bak" "$SELF_SCRIPT"
276
+ fi
277
+ printf " %b[SYNC]%b %s (directory contents)\n" "$GREEN" "$RESET" "$item"
278
+ else
279
+ cp "$src_path" "$dest_path"
280
+ printf " %b[SYNC]%b %s\n" "$GREEN" "$RESET" "$item"
281
+ fi
282
+ synced_count=$((synced_count + 1))
283
+ else
284
+ printf " %b[WARN]%b %s (not found in template)\n" "$YELLOW" "$RESET" "$item"
285
+ skipped_count=$((skipped_count + 1))
286
+ fi
287
+ done <<EOF_INCLUDE
288
+ $INCLUDE_LIST
289
+ EOF_INCLUDE
290
+
291
+ # Finalize self-update of sync.sh if applicable
292
+ TEMPLATE_SELF_SH="$TEMP_DIR/template/.github/scripts/sync.sh"
293
+ if [ -f "$TEMPLATE_SELF_SH" ] && { [ "$deferred_self_update" = "true" ] || [ "$synced_dotgithub" = "true" ]; }; then
294
+ if is_file_excluded ".github/scripts/sync.sh"; then
295
+ printf " %b[SKIP]%b .github/scripts/sync.sh (excluded from final update)\n" "$YELLOW" "$RESET"
296
+ else
297
+ cp "$TEMPLATE_SELF_SH" "$SELF_SCRIPT"
298
+ chmod +x "$SELF_SCRIPT" 2>/dev/null || true
299
+ printf " %b[SYNC]%b .github/scripts/sync.sh (finalized)\n" "$GREEN" "$RESET"
300
+ fi
301
+ fi
302
+
303
+ printf "\n%b[INFO] Sync complete!%b\n" "$GREEN" "$RESET"
304
+ printf " Synced: %d files/directories\n" "$synced_count"
305
+ if [ "$skipped_count" -gt 0 ]; then
306
+ printf " Skipped: %d files/directories\n" "$skipped_count"
307
+ fi
308
+
309
+ printf "\n%b[INFO] Review the changes with: git status%b\n" "$BLUE" "$RESET"
310
+ printf "%b[INFO] Commit the changes with: git add . && git commit -m 'chore: sync template files'%b\n" "$BLUE" "$RESET"