spawnio 1.0.8__tar.gz → 1.0.9__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.
- {spawnio-1.0.8 → spawnio-1.0.9}/CONTRIBUTING.md +10 -1
- {spawnio-1.0.8 → spawnio-1.0.9}/PKG-INFO +5 -4
- {spawnio-1.0.8 → spawnio-1.0.9}/README.md +4 -3
- {spawnio-1.0.8 → spawnio-1.0.9}/docs/architecture.md +88 -30
- {spawnio-1.0.8 → spawnio-1.0.9}/docs/changelog.md +20 -0
- {spawnio-1.0.8 → spawnio-1.0.9}/docs/commands.md +41 -22
- {spawnio-1.0.8 → spawnio-1.0.9}/docs/getting-started.md +2 -2
- {spawnio-1.0.8 → spawnio-1.0.9}/pyproject.toml +12 -1
- {spawnio-1.0.8 → spawnio-1.0.9}/src/spawn/__init__.py +1 -1
- {spawnio-1.0.8 → spawnio-1.0.9}/src/spawn/cli/app.py +53 -65
- spawnio-1.0.9/src/spawn/cli/errors.py +29 -0
- spawnio-1.0.9/src/spawn/cli/noninteractive.py +125 -0
- {spawnio-1.0.8 → spawnio-1.0.9}/src/spawn/cli/prompts.py +7 -11
- spawnio-1.0.9/src/spawn/core/exceptions.py +50 -0
- spawnio-1.0.9/src/spawn/core/models.py +58 -0
- spawnio-1.0.8/src/spawn/cli/noninteractive.py → spawnio-1.0.9/src/spawn/core/planning.py +33 -91
- spawnio-1.0.9/src/spawn/core/project.py +59 -0
- {spawnio-1.0.8 → spawnio-1.0.9}/src/spawn/generators/custom_structure.py +38 -15
- spawnio-1.0.9/src/spawn/generators/destination.py +66 -0
- spawnio-1.0.9/src/spawn/generators/metadata.py +45 -0
- spawnio-1.0.9/src/spawn/generators/pipeline.py +32 -0
- {spawnio-1.0.8 → spawnio-1.0.9}/src/spawn/generators/project_files.py +2 -2
- {spawnio-1.0.8 → spawnio-1.0.9}/src/spawn/generators/project_generator.py +44 -43
- spawnio-1.0.9/src/spawn/github/exceptions.py +5 -0
- {spawnio-1.0.8 → spawnio-1.0.9}/src/spawn/github/publisher.py +2 -1
- {spawnio-1.0.8 → spawnio-1.0.9}/src/spawn/utils/git.py +9 -6
- spawnio-1.0.9/src/spawn/utils/redact.py +8 -0
- {spawnio-1.0.8 → spawnio-1.0.9}/src/spawn/utils/success.py +8 -2
- {spawnio-1.0.8 → spawnio-1.0.9}/src/spawn/utils/uv.py +7 -5
- {spawnio-1.0.8 → spawnio-1.0.9}/src/spawn/utils/validators.py +6 -4
- spawnio-1.0.9/tests/conftest.py +46 -0
- spawnio-1.0.9/tests/fixtures/__init__.py +0 -0
- spawnio-1.0.9/tests/fixtures/archetypes.py +11 -0
- spawnio-1.0.9/tests/fixtures/static/broken_meta/.spawn/meta.json +1 -0
- spawnio-1.0.9/tests/fixtures/static/meta_unknown_intent/.spawn/meta.json +11 -0
- spawnio-1.0.9/tests/fixtures/static/non_spawn_python_repo/pyproject.toml +3 -0
- spawnio-1.0.9/tests/fixtures/static/non_spawn_python_repo/src/pkg/__init__.py +1 -0
- spawnio-1.0.9/tests/fixtures/static/non_spawn_python_repo/tests/test_x.py +2 -0
- spawnio-1.0.9/tests/fixtures/trees/agent.txt +20 -0
- spawnio-1.0.9/tests/fixtures/trees/automation.txt +20 -0
- spawnio-1.0.9/tests/fixtures/trees/backend-api.txt +16 -0
- spawnio-1.0.9/tests/fixtures/trees/chatbot.txt +22 -0
- spawnio-1.0.9/tests/fixtures/trees/cli.txt +12 -0
- spawnio-1.0.9/tests/fixtures/trees/custom.txt +4 -0
- spawnio-1.0.9/tests/fixtures/trees/data.txt +12 -0
- spawnio-1.0.9/tests/fixtures/trees/mcp.txt +11 -0
- spawnio-1.0.9/tests/fixtures/trees/rag.txt +21 -0
- {spawnio-1.0.8 → spawnio-1.0.9}/tests/test_app.py +20 -22
- spawnio-1.0.9/tests/test_boundaries.py +35 -0
- spawnio-1.0.9/tests/test_create_flags.py +203 -0
- {spawnio-1.0.8 → spawnio-1.0.9}/tests/test_custom_structure.py +5 -3
- spawnio-1.0.9/tests/test_destination.py +238 -0
- spawnio-1.0.9/tests/test_docs.py +42 -0
- spawnio-1.0.9/tests/test_errors.py +219 -0
- spawnio-1.0.9/tests/test_fixtures.py +158 -0
- {spawnio-1.0.8 → spawnio-1.0.9}/tests/test_generator.py +4 -2
- {spawnio-1.0.8 → spawnio-1.0.9}/tests/test_models.py +44 -0
- spawnio-1.0.9/tests/test_pipeline.py +135 -0
- spawnio-1.0.9/tests/test_planning.py +160 -0
- spawnio-1.0.9/tests/test_project.py +88 -0
- spawnio-1.0.8/.coderabbit.yaml +0 -105
- spawnio-1.0.8/.github/ISSUE_TEMPLATE/bug_report.yml +0 -77
- spawnio-1.0.8/.github/ISSUE_TEMPLATE/config.yml +0 -5
- spawnio-1.0.8/.github/ISSUE_TEMPLATE/feature_request.yml +0 -28
- spawnio-1.0.8/.github/PULL_REQUEST_TEMPLATE.md +0 -24
- spawnio-1.0.8/.github/labeler.yml +0 -25
- spawnio-1.0.8/.github/workflows/ci.yml +0 -29
- spawnio-1.0.8/.github/workflows/pr-check.yml +0 -60
- spawnio-1.0.8/.github/workflows/publish.yml +0 -24
- spawnio-1.0.8/.python-version +0 -1
- spawnio-1.0.8/assets/preview.png +0 -0
- spawnio-1.0.8/src/spawn/core/exceptions.py +0 -8
- spawnio-1.0.8/src/spawn/core/models.py +0 -22
- spawnio-1.0.8/src/spawn/github/exceptions.py +0 -5
- spawnio-1.0.8/uv.lock +0 -333
- {spawnio-1.0.8 → spawnio-1.0.9}/.gitignore +0 -0
- {spawnio-1.0.8 → spawnio-1.0.9}/LICENSE +0 -0
- {spawnio-1.0.8 → spawnio-1.0.9}/src/spawn/cli/__init__.py +0 -0
- {spawnio-1.0.8 → spawnio-1.0.9}/src/spawn/core/__init__.py +0 -0
- {spawnio-1.0.8 → spawnio-1.0.9}/src/spawn/core/registry.py +0 -0
- {spawnio-1.0.8 → spawnio-1.0.9}/src/spawn/generators/__init__.py +0 -0
- {spawnio-1.0.8 → spawnio-1.0.9}/src/spawn/github/__init__.py +0 -0
- {spawnio-1.0.8 → spawnio-1.0.9}/src/spawn/github/validators.py +0 -0
- {spawnio-1.0.8 → spawnio-1.0.9}/src/spawn/templates/__init__.py +0 -0
- {spawnio-1.0.8 → spawnio-1.0.9}/src/spawn/templates/agent/__init__.py +0 -0
- {spawnio-1.0.8 → spawnio-1.0.9}/src/spawn/templates/agent/content.py +0 -0
- {spawnio-1.0.8 → spawnio-1.0.9}/src/spawn/templates/automation/__init__.py +0 -0
- {spawnio-1.0.8 → spawnio-1.0.9}/src/spawn/templates/automation/content.py +0 -0
- {spawnio-1.0.8 → spawnio-1.0.9}/src/spawn/templates/backend_api/__init__.py +0 -0
- {spawnio-1.0.8 → spawnio-1.0.9}/src/spawn/templates/backend_api/content.py +0 -0
- {spawnio-1.0.8 → spawnio-1.0.9}/src/spawn/templates/base.py +0 -0
- {spawnio-1.0.8 → spawnio-1.0.9}/src/spawn/templates/chatbot/__init__.py +0 -0
- {spawnio-1.0.8 → spawnio-1.0.9}/src/spawn/templates/chatbot/content.py +0 -0
- {spawnio-1.0.8 → spawnio-1.0.9}/src/spawn/templates/cli_application/__init__.py +0 -0
- {spawnio-1.0.8 → spawnio-1.0.9}/src/spawn/templates/cli_application/content.py +0 -0
- {spawnio-1.0.8 → spawnio-1.0.9}/src/spawn/templates/data_project/__init__.py +0 -0
- {spawnio-1.0.8 → spawnio-1.0.9}/src/spawn/templates/data_project/analysis/__init__.py +0 -0
- {spawnio-1.0.8 → spawnio-1.0.9}/src/spawn/templates/data_project/analysis/content.py +0 -0
- {spawnio-1.0.8 → spawnio-1.0.9}/src/spawn/templates/data_project/dashboard/__init__.py +0 -0
- {spawnio-1.0.8 → spawnio-1.0.9}/src/spawn/templates/data_project/dashboard/content.py +0 -0
- {spawnio-1.0.8 → spawnio-1.0.9}/src/spawn/templates/data_project/etl/__init__.py +0 -0
- {spawnio-1.0.8 → spawnio-1.0.9}/src/spawn/templates/data_project/etl/content.py +0 -0
- {spawnio-1.0.8 → spawnio-1.0.9}/src/spawn/templates/data_project/ml/__init__.py +0 -0
- {spawnio-1.0.8 → spawnio-1.0.9}/src/spawn/templates/data_project/ml/content.py +0 -0
- {spawnio-1.0.8 → spawnio-1.0.9}/src/spawn/templates/mcp_server/__init__.py +0 -0
- {spawnio-1.0.8 → spawnio-1.0.9}/src/spawn/templates/mcp_server/content.py +0 -0
- {spawnio-1.0.8 → spawnio-1.0.9}/src/spawn/templates/rag/__init__.py +0 -0
- {spawnio-1.0.8 → spawnio-1.0.9}/src/spawn/templates/rag/content.py +0 -0
- {spawnio-1.0.8 → spawnio-1.0.9}/src/spawn/templates/shared_content.py +0 -0
- {spawnio-1.0.8 → spawnio-1.0.9}/src/spawn/utils/__init__.py +0 -0
- {spawnio-1.0.8 → spawnio-1.0.9}/src/spawn/utils/banner.py +0 -0
- {spawnio-1.0.8 → spawnio-1.0.9}/src/spawn/utils/console.py +0 -0
- {spawnio-1.0.8 → spawnio-1.0.9}/src/spawn/utils/doctor.py +0 -0
- {spawnio-1.0.8 → spawnio-1.0.9}/src/spawn/utils/doctor_fix.py +0 -0
- {spawnio-1.0.8 → spawnio-1.0.9}/src/spawn/utils/theme.py +0 -0
- {spawnio-1.0.8 → spawnio-1.0.9}/tests/test_agent_generator.py +0 -0
- {spawnio-1.0.8 → spawnio-1.0.9}/tests/test_agent_template.py +0 -0
- {spawnio-1.0.8 → spawnio-1.0.9}/tests/test_automation_generator.py +0 -0
- {spawnio-1.0.8 → spawnio-1.0.9}/tests/test_automation_template.py +0 -0
- {spawnio-1.0.8 → spawnio-1.0.9}/tests/test_chatbot_generator.py +0 -0
- {spawnio-1.0.8 → spawnio-1.0.9}/tests/test_chatbot_template.py +0 -0
- {spawnio-1.0.8 → spawnio-1.0.9}/tests/test_cli_application_generator.py +0 -0
- {spawnio-1.0.8 → spawnio-1.0.9}/tests/test_cli_application_template.py +0 -0
- {spawnio-1.0.8 → spawnio-1.0.9}/tests/test_console.py +0 -0
- {spawnio-1.0.8 → spawnio-1.0.9}/tests/test_custom_structure_integration.py +0 -0
- {spawnio-1.0.8 → spawnio-1.0.9}/tests/test_data_project_analysis.py +0 -0
- {spawnio-1.0.8 → spawnio-1.0.9}/tests/test_data_project_dashboard.py +0 -0
- {spawnio-1.0.8 → spawnio-1.0.9}/tests/test_data_project_etl.py +0 -0
- {spawnio-1.0.8 → spawnio-1.0.9}/tests/test_data_project_gitignore.py +0 -0
- {spawnio-1.0.8 → spawnio-1.0.9}/tests/test_data_project_ml.py +0 -0
- {spawnio-1.0.8 → spawnio-1.0.9}/tests/test_doctor.py +0 -0
- {spawnio-1.0.8 → spawnio-1.0.9}/tests/test_doctor_fix.py +0 -0
- {spawnio-1.0.8 → spawnio-1.0.9}/tests/test_git.py +0 -0
- {spawnio-1.0.8 → spawnio-1.0.9}/tests/test_github_publisher.py +0 -0
- {spawnio-1.0.8 → spawnio-1.0.9}/tests/test_github_validators.py +0 -0
- {spawnio-1.0.8 → spawnio-1.0.9}/tests/test_mcp_server_generator.py +0 -0
- {spawnio-1.0.8 → spawnio-1.0.9}/tests/test_mcp_server_template.py +0 -0
- {spawnio-1.0.8 → spawnio-1.0.9}/tests/test_noninteractive.py +0 -0
- {spawnio-1.0.8 → spawnio-1.0.9}/tests/test_project_files.py +0 -0
- {spawnio-1.0.8 → spawnio-1.0.9}/tests/test_prompts.py +0 -0
- {spawnio-1.0.8 → spawnio-1.0.9}/tests/test_rag_generator.py +0 -0
- {spawnio-1.0.8 → spawnio-1.0.9}/tests/test_rag_template.py +0 -0
- {spawnio-1.0.8 → spawnio-1.0.9}/tests/test_registry.py +0 -0
- {spawnio-1.0.8 → spawnio-1.0.9}/tests/test_templates.py +0 -0
- {spawnio-1.0.8 → spawnio-1.0.9}/tests/test_uv.py +0 -0
- {spawnio-1.0.8 → spawnio-1.0.9}/tests/test_validators.py +0 -0
|
@@ -35,6 +35,15 @@ uv run pytest --cov=src/spawn --cov-report=term-missing
|
|
|
35
35
|
|
|
36
36
|
---
|
|
37
37
|
|
|
38
|
+
## Testing and fixtures
|
|
39
|
+
|
|
40
|
+
- **Factory:** the `spawn_project` fixture in `tests/conftest.py` generates a project offline: `spawn_project("backend-api", framework="fastapi")` returns its `Path`. The `no_toolchain` fixture stubs git, uv and package installs.
|
|
41
|
+
- **Golden trees:** `tests/fixtures/trees/<archetype>.txt` lists every file of a generated archetype. After an intentional output change, regenerate with `SPAWN_UPDATE_TREES=1 uv run pytest tests/test_fixtures.py` and review the diff.
|
|
42
|
+
- **Offline rule:** tests must not use the network, real `git` or real `uv`; use `no_toolchain`.
|
|
43
|
+
- **Adding an archetype:** add an entry to `tests/fixtures/archetypes.py`, run once with `SPAWN_UPDATE_TREES=1` to create its golden tree, and review it.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
38
47
|
## Linting and formatting
|
|
39
48
|
|
|
40
49
|
```bash
|
|
@@ -126,7 +135,7 @@ Add two test files mirroring the existing pattern:
|
|
|
126
135
|
| File | What to test |
|
|
127
136
|
|---|---|
|
|
128
137
|
| `tests/test_<slug>_template.py` | Template constants, `get_dependencies()`, `get_readme_content()`, `py_compile` on each Python starter file, no unescaped braces |
|
|
129
|
-
| `tests/test_<slug>_generator.py` | `ProjectGenerator().generate()` with mocked uv/git/install, directory structure, README content, `.spawn/meta.json` fields |
|
|
138
|
+
| `tests/test_<slug>_generator.py` | `generate_project(config)` (or `ProjectGenerator().generate()`) with mocked uv/git/install, directory structure, README content, `.spawn/meta.json` fields |
|
|
130
139
|
|
|
131
140
|
See `tests/test_mcp_server_template.py` and `tests/test_mcp_server_generator.py` for the exact mock patterns (`_mock_uv_and_install()` context manager, `_cfg()` helper, `patch.object(MyTemplate, "post_install")`).
|
|
132
141
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: spawnio
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.9
|
|
4
4
|
Summary: Spawn a ready-to-use Python project in seconds
|
|
5
5
|
License-File: LICENSE
|
|
6
6
|
Requires-Python: >=3.12
|
|
@@ -144,7 +144,7 @@ Running `spawn` with no arguments shows the banner, the installed version, and t
|
|
|
144
144
|
|
|
145
145
|
```
|
|
146
146
|
SPAWN — scaffold your next project
|
|
147
|
-
v1.0.
|
|
147
|
+
v1.0.9
|
|
148
148
|
|
|
149
149
|
Commands
|
|
150
150
|
create Scaffold a new project
|
|
@@ -710,7 +710,7 @@ spawn create --name my-api --template backend-api --framework fastapi --extras r
|
|
|
710
710
|
spawn create --config spawn.json
|
|
711
711
|
```
|
|
712
712
|
|
|
713
|
-
`--config` takes precedence over individual flags. Add `--dry-run` to either form to validate the config and print it without creating any files.
|
|
713
|
+
`--config` takes precedence over individual flags (an explicit `--no-git`/`--no-uv` is ignored with a warning). Add `--dry-run` to either form to validate the config and print it without creating any files.
|
|
714
714
|
|
|
715
715
|
> Custom Structure is interactive-only in this version.
|
|
716
716
|
|
|
@@ -791,7 +791,7 @@ spawn doctor --fix --license mit
|
|
|
791
791
|
|
|
792
792
|
```bash
|
|
793
793
|
spawn version
|
|
794
|
-
# → Spawn v1.0.
|
|
794
|
+
# → Spawn v1.0.9
|
|
795
795
|
```
|
|
796
796
|
|
|
797
797
|
### Publish to GitHub
|
|
@@ -825,6 +825,7 @@ All tests should pass. If they don't, please [open an issue](https://github.com/
|
|
|
825
825
|
|
|
826
826
|
| Version | Highlight |
|
|
827
827
|
|---|---|
|
|
828
|
+
| **v1.0.9** | Core hardening: documented exit-code contract (generation, toolchain and publish failures exit non-zero), `--no-uv` honored for templates (file-only extras still written), path-traversal protection for custom structures, `read_project_metadata` extension boundary |
|
|
828
829
|
| **v1.0.8** | `spawn doctor --fix` (`--dry-run`, `--yes`, `--license`), `--license` flag and prompt on `create`, `CHANGELOG.md` generated for every project, `pre-commit` and `mypy` extras across all templates, Custom Structure Mypy option |
|
|
829
830
|
| **v1.0.7** | Fixed UTF-8 console crash on Windows when stdout is redirected/piped, added Windows reserved device name validation (`CON`, `PRN`, `AUX`, `NUL`, `COM1`–`9`, `LPT1`–`9`), clarified Python 3.12 requirement |
|
|
830
831
|
| **v1.0.6** | Removed duplicate banner from `spawn create`; `spawn` alone now shows the banner and command overview |
|
|
@@ -133,7 +133,7 @@ Running `spawn` with no arguments shows the banner, the installed version, and t
|
|
|
133
133
|
|
|
134
134
|
```
|
|
135
135
|
SPAWN — scaffold your next project
|
|
136
|
-
v1.0.
|
|
136
|
+
v1.0.9
|
|
137
137
|
|
|
138
138
|
Commands
|
|
139
139
|
create Scaffold a new project
|
|
@@ -699,7 +699,7 @@ spawn create --name my-api --template backend-api --framework fastapi --extras r
|
|
|
699
699
|
spawn create --config spawn.json
|
|
700
700
|
```
|
|
701
701
|
|
|
702
|
-
`--config` takes precedence over individual flags. Add `--dry-run` to either form to validate the config and print it without creating any files.
|
|
702
|
+
`--config` takes precedence over individual flags (an explicit `--no-git`/`--no-uv` is ignored with a warning). Add `--dry-run` to either form to validate the config and print it without creating any files.
|
|
703
703
|
|
|
704
704
|
> Custom Structure is interactive-only in this version.
|
|
705
705
|
|
|
@@ -780,7 +780,7 @@ spawn doctor --fix --license mit
|
|
|
780
780
|
|
|
781
781
|
```bash
|
|
782
782
|
spawn version
|
|
783
|
-
# → Spawn v1.0.
|
|
783
|
+
# → Spawn v1.0.9
|
|
784
784
|
```
|
|
785
785
|
|
|
786
786
|
### Publish to GitHub
|
|
@@ -814,6 +814,7 @@ All tests should pass. If they don't, please [open an issue](https://github.com/
|
|
|
814
814
|
|
|
815
815
|
| Version | Highlight |
|
|
816
816
|
|---|---|
|
|
817
|
+
| **v1.0.9** | Core hardening: documented exit-code contract (generation, toolchain and publish failures exit non-zero), `--no-uv` honored for templates (file-only extras still written), path-traversal protection for custom structures, `read_project_metadata` extension boundary |
|
|
817
818
|
| **v1.0.8** | `spawn doctor --fix` (`--dry-run`, `--yes`, `--license`), `--license` flag and prompt on `create`, `CHANGELOG.md` generated for every project, `pre-commit` and `mypy` extras across all templates, Custom Structure Mypy option |
|
|
818
819
|
| **v1.0.7** | Fixed UTF-8 console crash on Windows when stdout is redirected/piped, added Windows reserved device name validation (`CON`, `PRN`, `AUX`, `NUL`, `COM1`–`9`, `LPT1`–`9`), clarified Python 3.12 requirement |
|
|
819
820
|
| **v1.0.6** | Removed duplicate banner from `spawn create`; `spawn` alone now shows the banner and command overview |
|
|
@@ -23,7 +23,7 @@ Entry point: `spawn.cli.app:main` (defined in `pyproject.toml` as `[project.scri
|
|
|
23
23
|
|
|
24
24
|
```
|
|
25
25
|
src/spawn/
|
|
26
|
-
├── __init__.py # __version__ via importlib.metadata, fallback "1.0.
|
|
26
|
+
├── __init__.py # __version__ via importlib.metadata, fallback "1.0.9"
|
|
27
27
|
├── cli/
|
|
28
28
|
│ ├── app.py # Typer app: create, version, doctor commands
|
|
29
29
|
│ ├── noninteractive.py # Build config from CLI flags or JSON config file
|
|
@@ -76,14 +76,18 @@ src/spawn/
|
|
|
76
76
|
### 4a. Project Creation Flow
|
|
77
77
|
|
|
78
78
|
1. User runs `spawn create`
|
|
79
|
-
2. `app.create()`
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
5. `
|
|
86
|
-
6.
|
|
79
|
+
2. `app.create()` builds a `ProjectConfig`:
|
|
80
|
+
- non-interactive (`--name` or `--config`): `cli/noninteractive.py` calls `core/planning.py::plan_project`, which validates the name, rejects an existing destination, and resolves template options, extras and license. Config-file mode ignores explicit `--no-git`/`--no-uv` with a warning.
|
|
81
|
+
- interactive: `cli/prompts.py::get_project_config()` collects the answers (menus derived from the registry) and builds the config directly
|
|
82
|
+
3. `--dry-run` prints the resolved config and stops; nothing is written
|
|
83
|
+
4. `generators/pipeline.py::generate_project(config)` dispatches on `config.template`:
|
|
84
|
+
`ProjectGenerator.generate()` for registry templates, `CustomStructureGenerator.generate()` for `"custom"`
|
|
85
|
+
5. Both generators call `generators/destination.py`: `resolve_destination` (single-directory name check), `assert_available` (fails with `FilesystemError` if the path exists in any form, including symlinks) and, for custom structures, `safe_join` (rejects traversal and absolute paths)
|
|
86
|
+
6. `ProjectGenerator` resolves the template via `instantiate_template(config)`, creates directories, writes starter files, README and `.gitignore`, runs `initialize_git()`, `initialize_uv()`, `install_packages()` and `post_install()` (the uv steps are skipped with `--no-uv`; file-only extras are still written and the rest are reported in one "Skipped without uv" warning), then `generators/metadata.py::write_project_meta`
|
|
87
|
+
7. `show_success()` renders the success panel
|
|
88
|
+
8. If Git was enabled and not in non-interactive/`--yes` mode, optionally `GitHubPublisher.publish()` stages, commits, and pushes
|
|
89
|
+
|
|
90
|
+
**Cleanup rule:** on any failure a generator removes the project directory only if that run created it (`destination.cleanup_created`, which also refuses symlinks, the cwd and its parents, the home directory and filesystem roots). A pre-existing directory is never touched.
|
|
87
91
|
|
|
88
92
|
```
|
|
89
93
|
spawn create
|
|
@@ -92,26 +96,23 @@ spawn create
|
|
|
92
96
|
show_banner() banner.py
|
|
93
97
|
│
|
|
94
98
|
▼
|
|
95
|
-
|
|
96
|
-
|
|
99
|
+
plan_project() planning.py (flags / --config)
|
|
100
|
+
get_project_config() prompts.py (interactive)
|
|
101
|
+
│ → ProjectConfig
|
|
97
102
|
▼
|
|
98
|
-
|
|
99
|
-
├──
|
|
100
|
-
├──
|
|
101
|
-
├──
|
|
102
|
-
├── write .gitignore
|
|
103
|
-
├── initialize_git() git.py (if use_git)
|
|
104
|
-
├── initialize_uv()
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
103
|
+
generate_project() pipeline.py
|
|
104
|
+
├── ProjectGenerator / CustomStructureGenerator
|
|
105
|
+
│ ├── resolve_destination + assert_available destination.py
|
|
106
|
+
│ ├── instantiate_template() registry.py (forwards framework + extras)
|
|
107
|
+
│ ├── mkdir + write files, README, .gitignore
|
|
108
|
+
│ ├── initialize_git() git.py (if use_git)
|
|
109
|
+
│ ├── initialize_uv() / install_packages() (if use_uv)
|
|
110
|
+
│ └── post_install() template (ruff/pytest config, Docker, CI)
|
|
111
|
+
└── write_project_meta() metadata.py → .spawn/meta.json
|
|
112
|
+
(called by ProjectGenerator, or by generate_project for custom)
|
|
108
113
|
│
|
|
109
114
|
▼
|
|
110
115
|
show_success() success.py
|
|
111
|
-
└── template_obj.next_steps (formatted with project_name)
|
|
112
|
-
│
|
|
113
|
-
▼
|
|
114
|
-
GitHubPublisher.publish() publisher.py (optional)
|
|
115
116
|
```
|
|
116
117
|
|
|
117
118
|
### 4b. Template System
|
|
@@ -173,13 +174,19 @@ and/or `.github/workflows/ci.yml` depending on `self.extras`.
|
|
|
173
174
|
### 4d. Error Handling
|
|
174
175
|
|
|
175
176
|
```
|
|
176
|
-
SpawnError
|
|
177
|
-
|
|
177
|
+
SpawnError exit 1 core/exceptions.py
|
|
178
|
+
├── InvalidInputError exit 1
|
|
179
|
+
│ └── StructureParseError exit 1
|
|
180
|
+
├── ConfigError exit 1
|
|
181
|
+
├── FilesystemError exit 3
|
|
182
|
+
├── ToolchainError exit 4
|
|
183
|
+
├── GenerationError exit 5
|
|
184
|
+
│ └── TemplateError exit 5
|
|
185
|
+
└── PublishError exit 6
|
|
186
|
+
└── GitHubPublishError exit 6 github/exceptions.py
|
|
178
187
|
```
|
|
179
188
|
|
|
180
|
-
`
|
|
181
|
-
`SpawnError`; any `BaseException` (including `SpawnError`) triggers `shutil.rmtree()` rollback
|
|
182
|
-
before re-raising. The CLI catches both exception types and prints `❌ {message}` in red.
|
|
189
|
+
Generators convert `OSError` to `FilesystemError`, remove only the directory the current run created, and re-raise. `cli/errors.py` owns the CLI handlers: `fail(e)` prints `❌ message` in red and exits with `e.exit_code`; `report_unexpected(e)` handles any other exception, printing a short message and exiting 10 (`UNEXPECTED_EXIT_CODE`), or re-raising for a full traceback when `SPAWN_DEBUG` is set to anything other than empty/`0`/`false`/`no`. Ctrl+C, EOF and declined prompts exit 130; Click usage errors exit 2. See [Exit codes](commands.md#exit-codes).
|
|
183
190
|
|
|
184
191
|
## 5. Adding a New Template
|
|
185
192
|
|
|
@@ -197,3 +204,54 @@ uv run pytest -v
|
|
|
197
204
|
uv run pytest tests/test_generator.py
|
|
198
205
|
uv run ruff check .
|
|
199
206
|
```
|
|
207
|
+
|
|
208
|
+
## 7. Pipeline
|
|
209
|
+
|
|
210
|
+
```
|
|
211
|
+
input -> plan_project -> ProjectConfig -> generate_project -> filesystem -> meta -> publish (optional)
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
| Step | Owner |
|
|
215
|
+
|---|---|
|
|
216
|
+
| Input (flags, `--config` JSON, prompts) | `cli/noninteractive.py`, `cli/prompts.py` |
|
|
217
|
+
| Plan: validate and resolve options | `core/planning.py::plan_project` |
|
|
218
|
+
| Canonical model | `core/models.py::ProjectConfig` |
|
|
219
|
+
| Generate: folders, files, git, uv, rollback | `generators/pipeline.py::generate_project` |
|
|
220
|
+
| Meta: `.spawn/meta.json` | `generators/metadata.py::write_project_meta` |
|
|
221
|
+
| Publish | `github/publisher.py::GitHubPublisher` (called from `cli/app.py`) |
|
|
222
|
+
|
|
223
|
+
## 8. Canonical model
|
|
224
|
+
|
|
225
|
+
`ProjectConfig` is the single model passed between stages.
|
|
226
|
+
|
|
227
|
+
| Field | Meaning |
|
|
228
|
+
|---|---|
|
|
229
|
+
| `name`, `template` | Project name; template slug (`"custom"` for pasted structures) |
|
|
230
|
+
| `framework`, `provider`, `cli_type`, `data_type` | Template options |
|
|
231
|
+
| `extras`, `license`, `generate_claude_md` | Optional tooling, license (`mit`/`none`), also write `CLAUDE.md` |
|
|
232
|
+
| `use_git`, `use_uv` | Toolchain steps |
|
|
233
|
+
| `custom_*` | Custom-structure entries, dependencies, dev setup, gitignore extras, source format |
|
|
234
|
+
| `destination` | Target directory (`Path` or `None`); excluded from equality |
|
|
235
|
+
|
|
236
|
+
`to_dict()` returns a JSON-serializable dict of every field (`destination` as a string).
|
|
237
|
+
|
|
238
|
+
`.spawn/meta.json` is a **historical record, not authoritative**: it stores `intent`, `framework`, `provider`, `spawn_version`, `created_at`, `generator`, `git`, `uv` and `source` only. It does not record `name`, `cli_type`, `data_type`, `extras`, `license` or `generate_claude_md`, and the project may have changed since. `ProjectConfig` is generation intent only; it cannot be reconstructed from metadata.
|
|
239
|
+
|
|
240
|
+
## 9. Extension boundary
|
|
241
|
+
|
|
242
|
+
Code that analyses existing projects must not depend on the CLI. It obtains state through `core/`, `generators/` and `templates/`:
|
|
243
|
+
|
|
244
|
+
| Need | Use |
|
|
245
|
+
|---|---|
|
|
246
|
+
| State of an existing project | `core.project.read_project_metadata(path)`: a `ProjectMetadata`, or `None` if there is no `.spawn/meta.json`; raises `ConfigError` if the file is unreadable or invalid. Read-only; does not validate the intent against the registry |
|
|
247
|
+
| Resolve options into a config | `core.planning.plan_project(...)` |
|
|
248
|
+
| Template catalogue | `core.registry.get_metadata()`, `list_templates()` |
|
|
249
|
+
| Create a project | `generators.pipeline.generate_project(config)` |
|
|
250
|
+
|
|
251
|
+
**Rule:** no module under `core/`, `generators/`, `templates/`, `utils/` or `github/` may import `spawn.cli`. `tests/test_boundaries.py` enforces this.
|
|
252
|
+
|
|
253
|
+
To add a template: subclass `BaseTemplate` in `templates/<slug>/` and add a `TemplateMetadata` entry to `TEMPLATES` (see section 5).
|
|
254
|
+
|
|
255
|
+
## 10. Error model
|
|
256
|
+
|
|
257
|
+
All Spawn errors derive from `SpawnError` (`core/exceptions.py`); each class carries the CLI `exit_code`. The CLI prints `❌ message` and exits with that code. See [Exit codes](commands.md#exit-codes).
|
|
@@ -3,6 +3,26 @@
|
|
|
3
3
|
All notable changes to Spawn are documented here.
|
|
4
4
|
Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|
5
5
|
|
|
6
|
+
## v1.0.9 — 2026
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- **Canonical `ProjectConfig`** — gains `destination` and `to_dict()`; `core/planning.py::plan_project` resolves options into a config and `generators/pipeline.py::generate_project` builds it.
|
|
11
|
+
- **`core/project.py::read_project_metadata`** — CLI-free, read-only `ProjectMetadata` from `.spawn/meta.json` (`None` if absent, `ConfigError` if invalid). Metadata is historical, not authoritative; `ProjectConfig` is generation intent only.
|
|
12
|
+
- **Fixtures and regression tests** — archetype factory, golden file trees (`SPAWN_UPDATE_TREES=1`), and an import-boundary test (nothing outside `cli/` imports `spawn.cli`).
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- `--no-uv` is now honored for template projects: uv init, `uv add` and extras that need `pyproject.toml` are skipped (one yellow "Skipped without uv: ..." line lists them); file-only extras such as `pre-commit` are still written.
|
|
17
|
+
- Interactive `spawn create --dry-run` now validates and prints the config without writing anything.
|
|
18
|
+
- `--config` combined with an explicit `--no-git`/`--no-uv` prints a warning that the config file takes precedence.
|
|
19
|
+
- **Error and exit-code contract** — generation, toolchain (git/uv) and publish failures now exit non-zero (3–6) instead of 0; unexpected errors exit 10 with a short message (`SPAWN_DEBUG=1` shows the traceback). Declining the final custom-structure prompt now cancels cleanly (130) instead of raising a traceback. Git/GitHub error text no longer includes URL credentials. See the Exit codes table in `docs/commands.md`.
|
|
20
|
+
|
|
21
|
+
### Security
|
|
22
|
+
|
|
23
|
+
- Custom-structure paths that escape the project directory (path traversal) are rejected.
|
|
24
|
+
- Failure cleanup only removes directories created by the current run.
|
|
25
|
+
|
|
6
26
|
## v1.0.8 — 2026
|
|
7
27
|
|
|
8
28
|
### Added
|
|
@@ -13,11 +13,12 @@
|
|
|
13
13
|
|
|
14
14
|
## `spawn` (no arguments)
|
|
15
15
|
|
|
16
|
-
Running `spawn` with no arguments prints the banner, the installed version, and the command list.
|
|
16
|
+
Running `spawn` with no arguments prints the ASCII banner, the installed version, and the command list. It takes no arguments and no options other than `--help`, `--install-completion` and `--show-completion`, exits 0, and writes nothing.
|
|
17
17
|
|
|
18
18
|
```
|
|
19
|
-
SPAWN — scaffold your next project
|
|
20
|
-
|
|
19
|
+
<ASCII SPAWN banner — "scaffold your next project">
|
|
20
|
+
|
|
21
|
+
v1.0.9
|
|
21
22
|
|
|
22
23
|
Commands
|
|
23
24
|
create Scaffold a new project
|
|
@@ -33,6 +34,13 @@ Run spawn COMMAND --help for details on a command.
|
|
|
33
34
|
|
|
34
35
|
Creates a new project directory, writes starter files, installs dependencies, and optionally runs `git init` and `uv init`.
|
|
35
36
|
|
|
37
|
+
**Contract**
|
|
38
|
+
|
|
39
|
+
- **Arguments:** none. Options and defaults are in the flag table under *Non-interactive mode* (`--git`, `--uv`, `--no-claude-md`, `--license mit`).
|
|
40
|
+
- **Success:** the project is created in `./<name>`, a success panel is shown, exit code 0. `--dry-run` validates and prints the resolved config, writes nothing, exit code 0.
|
|
41
|
+
- **Failure:** `❌ message` is printed and the command exits non-zero (see [Exit codes](#exit-codes)). A partially written project directory is removed.
|
|
42
|
+
- **Filesystem:** writes only a new directory `./<name>`. It fails if that directory already exists (exit 3) and never modifies existing files.
|
|
43
|
+
|
|
36
44
|
### Interactive mode
|
|
37
45
|
|
|
38
46
|
Run with no flags to enter the arrow-key prompt flow:
|
|
@@ -143,9 +151,9 @@ spawn create --config spawn.json
|
|
|
143
151
|
| `--extras` | none | Comma-separated extras, e.g. `ruff,pytest` |
|
|
144
152
|
| `--license` | `mit` | Project license (`mit` or `none`) |
|
|
145
153
|
| `--git` / `--no-git` | `--git` | Initialize a Git repository |
|
|
146
|
-
| `--uv` / `--no-uv` | `--uv` | Initialize uv and install dependencies |
|
|
154
|
+
| `--uv` / `--no-uv` | `--uv` | Initialize uv and install dependencies. With `--no-uv`, extras that need `pyproject.toml` are skipped (one yellow "Skipped without uv" line lists them); file-only extras such as `pre-commit` are still written |
|
|
147
155
|
| `--claude-md` / `--no-claude-md` | `--no-claude-md` | Also write `CLAUDE.md` alongside `AGENTS.md` |
|
|
148
|
-
| `--config` | — | Path to a JSON config file |
|
|
156
|
+
| `--config` | — | Path to a JSON config file. Takes precedence over other flags; an explicit `--no-git`/`--no-uv` is ignored with a yellow warning |
|
|
149
157
|
| `--yes` / `-y` | false | Skip the GitHub publish prompt |
|
|
150
158
|
| `--dry-run` | false | Validate and print the config without creating anything |
|
|
151
159
|
|
|
@@ -379,7 +387,7 @@ Every generated project includes:
|
|
|
379
387
|
| `CHANGELOG.md` | Keep a Changelog starter documenting the initial release |
|
|
380
388
|
| `LICENSE` | MIT license (when `--license mit`, the default) |
|
|
381
389
|
| `.gitignore` | Python defaults (`.venv/`, `__pycache__/`, `.pytest_cache/`, `.mypy_cache/`, `.ruff_cache/`, etc.) |
|
|
382
|
-
| `.spawn/meta.json` | `intent`, `framework`, `provider`, `spawn_version`, `created_at`, `git`, `uv` |
|
|
390
|
+
| `.spawn/meta.json` | `intent`, `framework`, `provider`, `spawn_version`, `created_at`, `generator`, `git`, `uv`, `source` |
|
|
383
391
|
|
|
384
392
|
If `--claude-md` was passed (or `"claude_md": true` in the config file), a `CLAUDE.md` file identical to `AGENTS.md` is also written.
|
|
385
393
|
|
|
@@ -390,7 +398,7 @@ If `--claude-md` was passed (or `"claude_md": true` in the config file), a `CLAU
|
|
|
390
398
|
"intent": "backend-api",
|
|
391
399
|
"framework": "fastapi",
|
|
392
400
|
"provider": null,
|
|
393
|
-
"spawn_version": "1.0.
|
|
401
|
+
"spawn_version": "1.0.9",
|
|
394
402
|
"created_at": "2026-01-01T00:00:00+00:00",
|
|
395
403
|
"generator": "blueprint",
|
|
396
404
|
"git": true,
|
|
@@ -422,14 +430,7 @@ In non-interactive mode or when `--yes` / `-y` is passed, the publish prompt is
|
|
|
422
430
|
|
|
423
431
|
### Error cases
|
|
424
432
|
|
|
425
|
-
All errors print `❌ message` in red
|
|
426
|
-
|
|
427
|
-
| Situation | Exit code |
|
|
428
|
-
|---|---|
|
|
429
|
-
| Successful creation | 0 |
|
|
430
|
-
| SpawnError (bad name, unknown template, etc.) | 0 (error printed) |
|
|
431
|
-
| Non-interactive validation error | 1 |
|
|
432
|
-
| Ctrl+C at any prompt | 130 |
|
|
433
|
+
All errors print `❌ message` in red and exit with the code for their error class; any partial directory is deleted on failure. See [Exit codes](#exit-codes) for the full table (for example: invalid input 1, usage error 2, existing destination 3, missing `git`/`uv` 4, generation error 5, Ctrl+C 130).
|
|
433
434
|
|
|
434
435
|
---
|
|
435
436
|
|
|
@@ -437,6 +438,14 @@ All errors print `❌ message` in red. Any partial directory is deleted on failu
|
|
|
437
438
|
|
|
438
439
|
Scores the current directory (or a given path) for project health. All checks are filesystem-based — nothing is executed or sent over the network.
|
|
439
440
|
|
|
441
|
+
**Contract**
|
|
442
|
+
|
|
443
|
+
- **Arguments:** `[PATH]`, default `.`.
|
|
444
|
+
- **Options:** `--fix`, `--dry-run`, `--yes` / `-y`, `--license TEXT`; all off/unset by default, and the last three only apply with `--fix`.
|
|
445
|
+
- **Success:** prints the health report (score out of 135, rating, recommendations), exit code 0. A low score is not a failure.
|
|
446
|
+
- **Failure:** a path that does not exist or is not a directory prints `❌ message`, exit code 1. Declining the `--fix` confirmation exits 130.
|
|
447
|
+
- **Filesystem:** plain `spawn doctor` is read-only. `spawn doctor --fix` only creates missing files and config; it never overwrites and never installs packages. `--fix --dry-run` writes nothing.
|
|
448
|
+
|
|
440
449
|
```bash
|
|
441
450
|
spawn doctor
|
|
442
451
|
spawn doctor ./path/to/project
|
|
@@ -511,20 +520,30 @@ spawn doctor --fix --license mit
|
|
|
511
520
|
|
|
512
521
|
## `spawn version`
|
|
513
522
|
|
|
514
|
-
Prints the installed version.
|
|
523
|
+
Prints the installed version. No arguments or options; exit code 0; writes nothing.
|
|
515
524
|
|
|
516
525
|
```bash
|
|
517
526
|
spawn version
|
|
518
|
-
# Spawn v1.0.
|
|
527
|
+
# Spawn v1.0.9
|
|
519
528
|
```
|
|
520
529
|
|
|
521
530
|
---
|
|
522
531
|
|
|
523
532
|
## Exit codes
|
|
524
533
|
|
|
525
|
-
|
|
|
534
|
+
| Code | Meaning |
|
|
526
535
|
|---|---|
|
|
527
|
-
|
|
|
528
|
-
|
|
|
529
|
-
|
|
|
530
|
-
|
|
|
536
|
+
| 0 | Success (including a valid `--dry-run`, and GitHub publishing skipped) |
|
|
537
|
+
| 1 | Invalid input or config (bad name, option value, config file, unknown template; `spawn doctor` bad path/flags) |
|
|
538
|
+
| 2 | CLI usage error (unknown option, reported by Click) |
|
|
539
|
+
| 3 | Filesystem error (directory already exists, OS error while writing) |
|
|
540
|
+
| 4 | Toolchain error (`git` or `uv` missing or failed) |
|
|
541
|
+
| 5 | Generation / template error |
|
|
542
|
+
| 6 | Publish error (GitHub publishing failed) |
|
|
543
|
+
| 10 | Unexpected error (re-run with `SPAWN_DEBUG=1` for a traceback) |
|
|
544
|
+
| 130 | Cancelled (Ctrl+C, EOF, or declining the final prompt) |
|
|
545
|
+
|
|
546
|
+
Failures that happen after the project was created (for example a failed
|
|
547
|
+
GitHub publish) exit non-zero while the generated project remains on disk.
|
|
548
|
+
`spawn doctor --fix` action failures are reported in the output and do not
|
|
549
|
+
change the exit code.
|
|
@@ -49,14 +49,14 @@ After installation, `spawn` is available anywhere on your machine.
|
|
|
49
49
|
|
|
50
50
|
```bash
|
|
51
51
|
spawn version
|
|
52
|
-
# Spawn v1.0.
|
|
52
|
+
# Spawn v1.0.9
|
|
53
53
|
```
|
|
54
54
|
|
|
55
55
|
Or just run `spawn` with no arguments to see the command overview:
|
|
56
56
|
|
|
57
57
|
```
|
|
58
58
|
SPAWN — scaffold your next project
|
|
59
|
-
v1.0.
|
|
59
|
+
v1.0.9
|
|
60
60
|
|
|
61
61
|
Commands
|
|
62
62
|
create Scaffold a new project
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "spawnio"
|
|
3
|
-
version = "1.0.
|
|
3
|
+
version = "1.0.9"
|
|
4
4
|
description = "Spawn a ready-to-use Python project in seconds"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.12"
|
|
@@ -20,6 +20,17 @@ build-backend = "hatchling.build"
|
|
|
20
20
|
[tool.hatch.build.targets.wheel]
|
|
21
21
|
packages = ["src/spawn"]
|
|
22
22
|
|
|
23
|
+
[tool.hatch.build.targets.sdist]
|
|
24
|
+
include = [
|
|
25
|
+
"/src",
|
|
26
|
+
"/tests",
|
|
27
|
+
"/docs",
|
|
28
|
+
"/README.md",
|
|
29
|
+
"/LICENSE",
|
|
30
|
+
"/pyproject.toml",
|
|
31
|
+
"/CONTRIBUTING.md",
|
|
32
|
+
]
|
|
33
|
+
|
|
23
34
|
|
|
24
35
|
[dependency-groups]
|
|
25
36
|
dev = [
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import datetime
|
|
2
|
-
import json
|
|
3
1
|
from pathlib import Path
|
|
4
2
|
|
|
5
3
|
import typer
|
|
@@ -8,9 +6,10 @@ from rich.prompt import Confirm, Prompt
|
|
|
8
6
|
from spawn import __version__
|
|
9
7
|
from spawn.cli.noninteractive import build_config_from_args, build_config_from_file
|
|
10
8
|
from spawn.cli.prompts import get_project_config
|
|
11
|
-
from spawn.
|
|
9
|
+
from spawn.cli.errors import fail, report_unexpected
|
|
10
|
+
from spawn.core.exceptions import InvalidInputError, SpawnError
|
|
12
11
|
from spawn.core.registry import instantiate_template
|
|
13
|
-
from spawn.generators.
|
|
12
|
+
from spawn.generators.pipeline import generate_project
|
|
14
13
|
from spawn.github.exceptions import GitHubPublishError
|
|
15
14
|
from spawn.github.publisher import GitHubPublisher
|
|
16
15
|
from spawn.utils.banner import show_banner
|
|
@@ -36,30 +35,23 @@ def main_callback(ctx: typer.Context) -> None:
|
|
|
36
35
|
)
|
|
37
36
|
|
|
38
37
|
|
|
39
|
-
def
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
"git": config.use_git,
|
|
52
|
-
"uv": config.use_uv,
|
|
53
|
-
"source": config.custom_source_format or "tree",
|
|
54
|
-
},
|
|
55
|
-
indent=2,
|
|
56
|
-
),
|
|
57
|
-
encoding="utf-8",
|
|
58
|
-
)
|
|
38
|
+
def _given_on_command_line(ctx: typer.Context, param: str) -> bool:
|
|
39
|
+
source = ctx.get_parameter_source(param)
|
|
40
|
+
return getattr(source, "name", None) == "COMMANDLINE"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _display_info(config) -> tuple[str, list[str]] | None:
|
|
44
|
+
if config.template == "custom":
|
|
45
|
+
return "Custom Structure", [f"cd {config.name}", "Start building your project"]
|
|
46
|
+
template_obj = instantiate_template(config)
|
|
47
|
+
if template_obj is None:
|
|
48
|
+
return None
|
|
49
|
+
return template_obj.name, template_obj.next_steps
|
|
59
50
|
|
|
60
51
|
|
|
61
52
|
@app.command()
|
|
62
53
|
def create(
|
|
54
|
+
ctx: typer.Context,
|
|
63
55
|
name: str = typer.Option(
|
|
64
56
|
None, "--name", help="Project name (enables non-interactive mode)"
|
|
65
57
|
),
|
|
@@ -126,7 +118,7 @@ def create(
|
|
|
126
118
|
)
|
|
127
119
|
else:
|
|
128
120
|
if template is None:
|
|
129
|
-
raise
|
|
121
|
+
raise InvalidInputError(
|
|
130
122
|
"--template is required when using --name without --config."
|
|
131
123
|
)
|
|
132
124
|
extras_list = (
|
|
@@ -149,55 +141,43 @@ def create(
|
|
|
149
141
|
)
|
|
150
142
|
|
|
151
143
|
except SpawnError as e:
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
144
|
+
fail(e)
|
|
145
|
+
|
|
146
|
+
if config_file is not None:
|
|
147
|
+
for param, value, flag in (
|
|
148
|
+
("git", git, "--no-git"),
|
|
149
|
+
("uv", uv, "--no-uv"),
|
|
150
|
+
):
|
|
151
|
+
if not value and _given_on_command_line(ctx, param):
|
|
152
|
+
console.print(
|
|
153
|
+
f"[yellow]{flag} ignored: --config takes precedence[/yellow]"
|
|
154
|
+
)
|
|
159
155
|
else:
|
|
160
|
-
|
|
156
|
+
try:
|
|
157
|
+
config = get_project_config()
|
|
158
|
+
except SpawnError as e:
|
|
159
|
+
fail(e)
|
|
161
160
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
161
|
+
if dry_run:
|
|
162
|
+
console.print("[green]✓ Config valid[/green]")
|
|
163
|
+
console.print(config)
|
|
164
|
+
return
|
|
165
165
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
dependencies=config.custom_dependencies,
|
|
172
|
-
dev_setup=config.custom_dev_setup,
|
|
173
|
-
gitignore_extra=config.custom_gitignore_extra,
|
|
174
|
-
generate_claude_md=config.generate_claude_md,
|
|
175
|
-
)
|
|
176
|
-
_write_custom_metadata(project_path, config)
|
|
177
|
-
next_steps = [
|
|
178
|
-
f"cd {config.name}",
|
|
179
|
-
"Start building your project",
|
|
180
|
-
]
|
|
166
|
+
try:
|
|
167
|
+
project_path = generate_project(config)
|
|
168
|
+
info = _display_info(config)
|
|
169
|
+
if info is not None:
|
|
170
|
+
template_name, next_steps = info
|
|
181
171
|
show_success(
|
|
182
172
|
project_name=config.name,
|
|
183
|
-
template_name=
|
|
173
|
+
template_name=template_name,
|
|
184
174
|
use_git=config.use_git,
|
|
185
175
|
next_steps=next_steps,
|
|
176
|
+
use_uv=config.use_uv,
|
|
186
177
|
)
|
|
187
|
-
else:
|
|
188
|
-
project_path = ProjectGenerator().generate(config)
|
|
189
|
-
template_obj = instantiate_template(config)
|
|
190
|
-
if template_obj is not None:
|
|
191
|
-
show_success(
|
|
192
|
-
project_name=config.name,
|
|
193
|
-
template_name=template_obj.name,
|
|
194
|
-
use_git=config.use_git,
|
|
195
|
-
next_steps=template_obj.next_steps,
|
|
196
|
-
)
|
|
197
178
|
|
|
198
179
|
except SpawnError as e:
|
|
199
|
-
|
|
200
|
-
return
|
|
180
|
+
fail(e)
|
|
201
181
|
|
|
202
182
|
if not config.use_git:
|
|
203
183
|
console.print(
|
|
@@ -225,10 +205,14 @@ def create(
|
|
|
225
205
|
console.print("[green]🚀 Published successfully![/green]")
|
|
226
206
|
|
|
227
207
|
except GitHubPublishError as e:
|
|
228
|
-
|
|
208
|
+
fail(e)
|
|
229
209
|
except (KeyboardInterrupt, EOFError, typer.Abort):
|
|
230
210
|
console.print("\n[yellow]Cancelled.[/yellow]")
|
|
231
211
|
raise typer.Exit(130)
|
|
212
|
+
except typer.Exit:
|
|
213
|
+
raise
|
|
214
|
+
except Exception as e:
|
|
215
|
+
report_unexpected(e)
|
|
232
216
|
|
|
233
217
|
|
|
234
218
|
@app.command()
|
|
@@ -360,6 +344,10 @@ def doctor(
|
|
|
360
344
|
except (KeyboardInterrupt, EOFError, typer.Abort):
|
|
361
345
|
console.print("\n[yellow]Cancelled.[/yellow]")
|
|
362
346
|
raise typer.Exit(130)
|
|
347
|
+
except typer.Exit:
|
|
348
|
+
raise
|
|
349
|
+
except Exception as e:
|
|
350
|
+
report_unexpected(e)
|
|
363
351
|
|
|
364
352
|
|
|
365
353
|
def main():
|