newstack 0.1.0__tar.gz → 0.3.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.
- newstack-0.3.0/CHANGELOG.md +63 -0
- {newstack-0.1.0 → newstack-0.3.0}/PKG-INFO +60 -14
- {newstack-0.1.0 → newstack-0.3.0}/README.md +59 -13
- {newstack-0.1.0 → newstack-0.3.0}/pyproject.toml +1 -1
- {newstack-0.1.0 → newstack-0.3.0}/src/newstack/__init__.py +1 -1
- newstack-0.3.0/src/newstack/adopt.py +160 -0
- {newstack-0.1.0 → newstack-0.3.0}/src/newstack/cli.py +102 -10
- newstack-0.3.0/src/newstack/data/stacks/django/stack.json +77 -0
- newstack-0.3.0/src/newstack/data/stacks/django/template/.github/workflows/ci.yml +31 -0
- newstack-0.3.0/src/newstack/data/stacks/django/template/.gitignore +7 -0
- newstack-0.3.0/src/newstack/data/stacks/django/template/README.md +67 -0
- newstack-0.3.0/src/newstack/data/stacks/django/template/config/__init__.py +0 -0
- newstack-0.3.0/src/newstack/data/stacks/django/template/config/asgi.py +9 -0
- newstack-0.3.0/src/newstack/data/stacks/django/template/config/settings.py +82 -0
- newstack-0.3.0/src/newstack/data/stacks/django/template/config/urls.py +9 -0
- newstack-0.3.0/src/newstack/data/stacks/django/template/config/wsgi.py +9 -0
- newstack-0.3.0/src/newstack/data/stacks/django/template/core/__init__.py +0 -0
- newstack-0.3.0/src/newstack/data/stacks/django/template/core/apps.py +6 -0
- newstack-0.3.0/src/newstack/data/stacks/django/template/core/migrations/0001_initial.py +32 -0
- newstack-0.3.0/src/newstack/data/stacks/django/template/core/migrations/__init__.py +0 -0
- newstack-0.3.0/src/newstack/data/stacks/django/template/core/models.py +12 -0
- newstack-0.3.0/src/newstack/data/stacks/django/template/core/urls.py +8 -0
- newstack-0.3.0/src/newstack/data/stacks/django/template/core/views.py +22 -0
- newstack-0.3.0/src/newstack/data/stacks/django/template/manage.py +23 -0
- newstack-0.3.0/src/newstack/data/stacks/django/template/pyproject.toml +43 -0
- newstack-0.3.0/src/newstack/data/stacks/django/template/tests/test_models.py +20 -0
- newstack-0.3.0/src/newstack/data/stacks/django/template/tests/test_views.py +30 -0
- {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/fastapi/stack.json +15 -0
- newstack-0.3.0/src/newstack/data/stacks/go-cli/stack.json +55 -0
- newstack-0.3.0/src/newstack/data/stacks/go-cli/template/.github/workflows/ci.yml +21 -0
- newstack-0.3.0/src/newstack/data/stacks/go-cli/template/.gitignore +15 -0
- newstack-0.3.0/src/newstack/data/stacks/go-cli/template/README.md +55 -0
- newstack-0.3.0/src/newstack/data/stacks/go-cli/template/go.mod +3 -0
- newstack-0.3.0/src/newstack/data/stacks/go-cli/template/internal/app/app.go +69 -0
- newstack-0.3.0/src/newstack/data/stacks/go-cli/template/internal/app/app_test.go +72 -0
- newstack-0.3.0/src/newstack/data/stacks/go-cli/template/main.go +12 -0
- {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/node-express-ts/stack.json +15 -0
- {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/python-cli/stack.json +17 -0
- {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/python-lib/stack.json +13 -0
- {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/react-vite-ts/stack.json +16 -0
- newstack-0.3.0/src/newstack/data/stacks/react-vite-ts/template/LICENSE +21 -0
- newstack-0.3.0/src/newstack/data/stacks/rust-cli/stack.json +63 -0
- newstack-0.3.0/src/newstack/data/stacks/rust-cli/template/.github/workflows/ci.yml +32 -0
- newstack-0.3.0/src/newstack/data/stacks/rust-cli/template/.gitignore +1 -0
- newstack-0.3.0/src/newstack/data/stacks/rust-cli/template/Cargo.toml +17 -0
- newstack-0.3.0/src/newstack/data/stacks/rust-cli/template/LICENSE +21 -0
- newstack-0.3.0/src/newstack/data/stacks/rust-cli/template/README.md +46 -0
- newstack-0.3.0/src/newstack/data/stacks/rust-cli/template/src/lib.rs +120 -0
- newstack-0.3.0/src/newstack/data/stacks/rust-cli/template/src/main.rs +20 -0
- newstack-0.3.0/src/newstack/data/stacks/rust-cli/template/tests/cli.rs +46 -0
- {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/static-site/stack.json +13 -0
- newstack-0.3.0/src/newstack/data/stacks/static-site/template/LICENSE +21 -0
- newstack-0.3.0/src/newstack/data/stacks/svelte-kit-ts/stack.json +31 -0
- newstack-0.3.0/src/newstack/data/stacks/svelte-kit-ts/template/.github/workflows/ci.yml +31 -0
- newstack-0.3.0/src/newstack/data/stacks/svelte-kit-ts/template/.gitignore +23 -0
- newstack-0.3.0/src/newstack/data/stacks/svelte-kit-ts/template/.npmrc +1 -0
- newstack-0.3.0/src/newstack/data/stacks/svelte-kit-ts/template/LICENSE +21 -0
- newstack-0.3.0/src/newstack/data/stacks/svelte-kit-ts/template/README.md +71 -0
- newstack-0.3.0/src/newstack/data/stacks/svelte-kit-ts/template/package.json +34 -0
- newstack-0.3.0/src/newstack/data/stacks/svelte-kit-ts/template/src/app.d.ts +13 -0
- newstack-0.3.0/src/newstack/data/stacks/svelte-kit-ts/template/src/app.html +13 -0
- newstack-0.3.0/src/newstack/data/stacks/svelte-kit-ts/template/src/lib/Counter.svelte +7 -0
- newstack-0.3.0/src/newstack/data/stacks/svelte-kit-ts/template/src/lib/Counter.test.ts +17 -0
- newstack-0.3.0/src/newstack/data/stacks/svelte-kit-ts/template/src/lib/assets/favicon.svg +1 -0
- newstack-0.3.0/src/newstack/data/stacks/svelte-kit-ts/template/src/lib/index.ts +1 -0
- newstack-0.3.0/src/newstack/data/stacks/svelte-kit-ts/template/src/routes/+layout.svelte +12 -0
- newstack-0.3.0/src/newstack/data/stacks/svelte-kit-ts/template/src/routes/+layout.ts +1 -0
- newstack-0.3.0/src/newstack/data/stacks/svelte-kit-ts/template/src/routes/+page.svelte +35 -0
- newstack-0.3.0/src/newstack/data/stacks/svelte-kit-ts/template/src/vitest-setup.ts +5 -0
- newstack-0.3.0/src/newstack/data/stacks/svelte-kit-ts/template/static/robots.txt +3 -0
- newstack-0.3.0/src/newstack/data/stacks/svelte-kit-ts/template/tsconfig.json +7 -0
- newstack-0.3.0/src/newstack/data/stacks/svelte-kit-ts/template/vite.config.ts +26 -0
- {newstack-0.1.0 → newstack-0.3.0}/src/newstack/registry.py +50 -0
- {newstack-0.1.0 → newstack-0.3.0}/src/newstack/render.py +12 -0
- newstack-0.3.0/tests/test_adopt.py +192 -0
- newstack-0.3.0/tests/test_docs.py +38 -0
- newstack-0.1.0/CHANGELOG.md +0 -22
- {newstack-0.1.0 → newstack-0.3.0}/.gitignore +0 -0
- {newstack-0.1.0 → newstack-0.3.0}/LICENSE +0 -0
- {newstack-0.1.0 → newstack-0.3.0}/src/newstack/__main__.py +0 -0
- {newstack-0.1.0/src/newstack/data/stacks/fastapi → newstack-0.3.0/src/newstack/data/stacks/django}/template/LICENSE +0 -0
- {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/fastapi/template/.github/workflows/ci.yml +0 -0
- {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/fastapi/template/.gitignore +0 -0
- {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/fastapi/template/Dockerfile +0 -0
- {newstack-0.1.0/src/newstack/data/stacks/node-express-ts → newstack-0.3.0/src/newstack/data/stacks/fastapi}/template/LICENSE +0 -0
- {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/fastapi/template/README.md +0 -0
- {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/fastapi/template/pyproject.toml +0 -0
- {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/fastapi/template/src/{{package_name}}/__init__.py +0 -0
- {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/fastapi/template/src/{{package_name}}/main.py +0 -0
- {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/fastapi/template/tests/test_api.py +0 -0
- {newstack-0.1.0/src/newstack/data/stacks/python-cli → newstack-0.3.0/src/newstack/data/stacks/go-cli}/template/LICENSE +0 -0
- {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/node-express-ts/template/.github/workflows/ci.yml +0 -0
- {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/node-express-ts/template/.gitignore +0 -0
- {newstack-0.1.0/src/newstack/data/stacks/python-lib → newstack-0.3.0/src/newstack/data/stacks/node-express-ts}/template/LICENSE +0 -0
- {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/node-express-ts/template/README.md +0 -0
- {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/node-express-ts/template/package.json +0 -0
- {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/node-express-ts/template/src/app.ts +0 -0
- {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/node-express-ts/template/src/server.ts +0 -0
- {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/node-express-ts/template/tests/app.test.ts +0 -0
- {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/node-express-ts/template/tsconfig.json +0 -0
- {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/python-cli/template/.github/workflows/ci.yml +0 -0
- {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/python-cli/template/.gitignore +0 -0
- {newstack-0.1.0/src/newstack/data/stacks/react-vite-ts → newstack-0.3.0/src/newstack/data/stacks/python-cli}/template/LICENSE +0 -0
- {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/python-cli/template/README.md +0 -0
- {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/python-cli/template/pyproject.toml +0 -0
- {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/python-cli/template/src/{{package_name}}/__init__.py +0 -0
- {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/python-cli/template/src/{{package_name}}/__main__.py +0 -0
- {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/python-cli/template/src/{{package_name}}/cli.py +0 -0
- {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/python-cli/template/tests/test_cli.py +0 -0
- {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/python-lib/template/.github/workflows/ci.yml +0 -0
- {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/python-lib/template/.gitignore +0 -0
- {newstack-0.1.0/src/newstack/data/stacks/static-site → newstack-0.3.0/src/newstack/data/stacks/python-lib}/template/LICENSE +0 -0
- {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/python-lib/template/README.md +0 -0
- {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/python-lib/template/pyproject.toml +0 -0
- {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/python-lib/template/src/{{package_name}}/__init__.py +0 -0
- {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/python-lib/template/src/{{package_name}}/core.py +0 -0
- {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/python-lib/template/tests/test_core.py +0 -0
- {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/react-vite-ts/template/.github/workflows/ci.yml +0 -0
- {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/react-vite-ts/template/.gitignore +0 -0
- {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/react-vite-ts/template/README.md +0 -0
- {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/react-vite-ts/template/index.html +0 -0
- {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/react-vite-ts/template/package.json +0 -0
- {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/react-vite-ts/template/src/App.test.tsx +0 -0
- {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/react-vite-ts/template/src/App.tsx +0 -0
- {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/react-vite-ts/template/src/main.tsx +0 -0
- {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/react-vite-ts/template/src/styles.css +0 -0
- {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/react-vite-ts/template/tsconfig.json +0 -0
- {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/react-vite-ts/template/vite.config.ts +0 -0
- {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/static-site/template/.github/workflows/deploy-pages.yml +0 -0
- {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/static-site/template/.gitignore +0 -0
- {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/static-site/template/README.md +0 -0
- {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/static-site/template/index.html +0 -0
- {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/static-site/template/main.js +0 -0
- {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/static-site/template/styles.css +0 -0
- {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/static-site/template/tests/site.test.mjs +0 -0
- {newstack-0.1.0 → newstack-0.3.0}/src/newstack/verify.py +0 -0
- {newstack-0.1.0 → newstack-0.3.0}/tests/test_cli.py +0 -0
- {newstack-0.1.0 → newstack-0.3.0}/tests/test_registry.py +0 -0
- {newstack-0.1.0 → newstack-0.3.0}/tests/test_render.py +0 -0
- {newstack-0.1.0 → newstack-0.3.0}/tests/test_stacks.py +0 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this project adheres to
|
|
5
|
+
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.3.0] - 2026-11-02
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **`newstack adopt <dir>`** — retrofit a stack's infrastructure onto a project that already
|
|
14
|
+
has code. Additive by construction: an existing file is never opened for writing, and any
|
|
15
|
+
change that would need a merge inside a file is reported as a manual step instead.
|
|
16
|
+
- Auto-detects the stack from the project's own markers (`go.mod`, `Cargo.toml`,
|
|
17
|
+
`manage.py`, `pyproject.toml`/`package.json` contents, a bare `index.html`); ambiguous
|
|
18
|
+
matches print candidates and exit 2; `--stack` overrides.
|
|
19
|
+
- Copies infrastructure only — workflows, `.gitignore`, lint/type config — never a
|
|
20
|
+
README, LICENSE, `pyproject.toml`, `package.json`, `go.mod` or source file.
|
|
21
|
+
- `--dry-run` reports without writing; re-running is a no-op; `--json` for scripting.
|
|
22
|
+
- `--with-tests` (experimental) also copies the stack's starter tests.
|
|
23
|
+
- Manifest keys `detect`, `detect_fallback`, `adopt` and `adopt_tests`, validated by the
|
|
24
|
+
registry and by tests that require every adoptable path to exist and to be infrastructure.
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
|
|
28
|
+
- `newstack new` and `adopt` share one step formatter, so an unknown `{placeholder}` in a
|
|
29
|
+
manifest's `next_steps` prints literally instead of raising `KeyError`.
|
|
30
|
+
|
|
31
|
+
## [0.2.0] - 2026-11-02
|
|
32
|
+
|
|
33
|
+
### Added
|
|
34
|
+
|
|
35
|
+
- Four new stacks, all verified the same way as the original six:
|
|
36
|
+
- `go-cli` — stdlib `flag` parsing, table tests, vet/test/build gates.
|
|
37
|
+
- `rust-cli` — std-only parsing, lib+bin split, fmt/clippy/test/release gates.
|
|
38
|
+
- `django` — Django 6.1, config/core split, SQLite, pytest-django, and a
|
|
39
|
+
migration-drift step that fails when a model changes without a migration.
|
|
40
|
+
- `svelte-kit-ts` — SvelteKit 3 with Svelte 5 runes, Vitest, static adapter build.
|
|
41
|
+
- CI runs one job per stack (ten in total), with Go and Rust toolchains installed only
|
|
42
|
+
for the jobs that need them.
|
|
43
|
+
- Docs drift tests: the README table and the CI matrix must both list every registered
|
|
44
|
+
stack, and the README may not hardcode a stack count.
|
|
45
|
+
|
|
46
|
+
### Changed
|
|
47
|
+
|
|
48
|
+
- Install instructions now point at PyPI (`uvx newstack`, `uv tool install newstack`).
|
|
49
|
+
|
|
50
|
+
## [0.1.0] - 2026-11-02
|
|
51
|
+
|
|
52
|
+
### Added
|
|
53
|
+
|
|
54
|
+
- `newstack new <stack> <path>` — render a stack, initialise git, first commit.
|
|
55
|
+
- `newstack new … --github [OWNER/REPO]` — create the GitHub repository and push.
|
|
56
|
+
- `newstack list` — every stack with its toolchain requirements, `--json` supported.
|
|
57
|
+
- `newstack check <stack…> | --all` — generate into a temp dir and run the stack's own
|
|
58
|
+
verification steps (`install`, `lint`, `test`, `build`), reported per step.
|
|
59
|
+
- `newstack doctor` — which toolchains are installed, which stacks can be verified here.
|
|
60
|
+
- Six stacks: `python-lib`, `python-cli`, `fastapi`, `react-vite-ts`, `node-express-ts`,
|
|
61
|
+
`static-site`.
|
|
62
|
+
- Stack registry is extensible through the `NEWSTACK_STACKS` environment variable.
|
|
63
|
+
- Zero runtime dependencies: standard library only.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: newstack
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Scaffold a new project on any popular stack — every generated starter is proven green in CI.
|
|
5
5
|
Project-URL: Homepage, https://github.com/DawnofGenX/newstack
|
|
6
6
|
Project-URL: Repository, https://github.com/DawnofGenX/newstack
|
|
@@ -25,6 +25,7 @@ Description-Content-Type: text/markdown
|
|
|
25
25
|
# newstack
|
|
26
26
|
|
|
27
27
|
[](https://github.com/DawnofGenX/newstack/actions/workflows/ci.yml)
|
|
28
|
+
[](https://pypi.org/project/newstack/)
|
|
28
29
|

|
|
29
30
|

|
|
30
31
|
[](LICENSE)
|
|
@@ -61,12 +62,10 @@ first day of work.
|
|
|
61
62
|
|
|
62
63
|
## Install
|
|
63
64
|
|
|
64
|
-
Not on PyPI yet — install straight from the repository:
|
|
65
|
-
|
|
66
65
|
```bash
|
|
67
|
-
|
|
68
|
-
# or
|
|
69
|
-
|
|
66
|
+
uvx newstack list # run without installing
|
|
67
|
+
uv tool install newstack # or put it on your PATH
|
|
68
|
+
pipx install newstack # any of the usual runners works
|
|
70
69
|
```
|
|
71
70
|
|
|
72
71
|
From a clone:
|
|
@@ -84,6 +83,7 @@ newstack new react-vite-ts ~/code/app
|
|
|
84
83
|
newstack new fastapi ~/code/api -d "Billing service" --author "Your Name"
|
|
85
84
|
newstack new python-lib ~/code/lib --dry-run # preview, write nothing
|
|
86
85
|
newstack new python-cli ~/code/tool --github # create the repo and push it too
|
|
86
|
+
newstack adopt ~/code/old-project --dry-run # retrofit CI/lint onto existing code
|
|
87
87
|
newstack check --all # prove every stack on this machine
|
|
88
88
|
newstack doctor # which toolchains do I have?
|
|
89
89
|
```
|
|
@@ -101,13 +101,46 @@ directory to CI-ready project.
|
|
|
101
101
|
| `python-lib` | src layout, hatchling build, pytest, ruff, 4-version CI matrix | `uv` |
|
|
102
102
|
| `python-cli` | console entry point on argparse, zero runtime deps, tested CLI | `uv` |
|
|
103
103
|
| `fastapi` | typed JSON API, pydantic models, TestClient tests, Dockerfile | `uv` |
|
|
104
|
+
| `django` | Django 6.1 app: config/core split, SQLite, pytest-django, migration drift check | `uv` |
|
|
104
105
|
| `react-vite-ts` | React 19 + Vite + TS, Vitest + Testing Library, typecheck + build | `node`, `npm` |
|
|
105
106
|
| `node-express-ts` | Express 5 API, typed handlers, supertest tests | `node`, `npm` |
|
|
106
|
-
| `
|
|
107
|
+
| `svelte-kit-ts` | SvelteKit + TS, Svelte 5 runes component, Vitest, static adapter build | `node`, `npm` |
|
|
108
|
+
| `go-cli` | stdlib-only Go CLI: `flag` parsing, table tests, vet/test/build gates | `go` |
|
|
109
|
+
| `rust-cli` | std-only Rust CLI: lib+bin split, fmt/clippy/test/release-build gates | `cargo` |
|
|
110
|
+
| `static-site` | HTML/CSS/JS page, `node:test` suite, GitHub Pages deploy workflow | `node` |
|
|
107
111
|
|
|
108
112
|
Each generated project ships its own `.github/workflows/` that is ready to run, plus a
|
|
109
113
|
README, an MIT licence and a `.gitignore`.
|
|
110
114
|
|
|
115
|
+
## Adopting an existing project
|
|
116
|
+
|
|
117
|
+
Most projects do not start from a scaffold. `adopt` retrofits a stack's *infrastructure*
|
|
118
|
+
onto a directory that already has code — and it is additive by construction: **an existing
|
|
119
|
+
file is never opened for writing.**
|
|
120
|
+
|
|
121
|
+
```console
|
|
122
|
+
$ newstack adopt ~/sites/analytics-dashboard
|
|
123
|
+
newstack 0.3.0 · adopt → /home/you/sites/analytics-dashboard
|
|
124
|
+
|
|
125
|
+
stack static-site (auto-detected)
|
|
126
|
+
added 2 files
|
|
127
|
+
+ .github/workflows/deploy-pages.yml
|
|
128
|
+
+ .gitignore
|
|
129
|
+
tests not copied — rerun with --with-tests if you want starter tests
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
- **Detection** reads the project's own markers (`go.mod`, `Cargo.toml`, `manage.py`, the
|
|
133
|
+
dependencies in `pyproject.toml` / `package.json`, a bare `index.html`). If two stacks
|
|
134
|
+
match, it prints the candidates and exits 2 rather than guessing; `--stack` overrides.
|
|
135
|
+
- **Scope**: workflows, `.gitignore`, and per-stack lint/type config only. It will not copy
|
|
136
|
+
a README, a LICENSE, a `pyproject.toml`, or source files — those carry your project's
|
|
137
|
+
identity, not infrastructure.
|
|
138
|
+
- **Merges are reported, not attempted.** If your `pyproject.toml` needs the stack's
|
|
139
|
+
`[tool.ruff]` block, adopt says so and leaves the file alone.
|
|
140
|
+
- `--dry-run` prints the same report without writing. Re-running is a no-op.
|
|
141
|
+
- `--with-tests` additionally copies the stack's *starter* tests — experimental, because
|
|
142
|
+
those tests import the sample module and usually need adapting to your code.
|
|
143
|
+
|
|
111
144
|
## The green guarantee
|
|
112
145
|
|
|
113
146
|
A template is only as good as its last successful run, so `newstack` treats
|
|
@@ -115,12 +148,16 @@ verification as a first-class command:
|
|
|
115
148
|
|
|
116
149
|
```console
|
|
117
150
|
$ newstack check --all
|
|
118
|
-
|
|
119
|
-
|
|
151
|
+
django PASS 1.5s 5 step(s)
|
|
152
|
+
fastapi PASS 1.4s 3 step(s)
|
|
153
|
+
go-cli PASS 0.3s 3 step(s)
|
|
154
|
+
node-express-ts PASS 6.1s 3 step(s)
|
|
120
155
|
python-cli PASS 0.5s 4 step(s)
|
|
121
|
-
python-lib PASS 0.
|
|
122
|
-
react-vite-ts PASS
|
|
156
|
+
python-lib PASS 0.5s 3 step(s)
|
|
157
|
+
react-vite-ts PASS 5.5s 4 step(s)
|
|
158
|
+
rust-cli PASS 0.5s 4 step(s)
|
|
123
159
|
static-site PASS 0.1s 3 step(s)
|
|
160
|
+
svelte-kit-ts PASS 7.1s 4 step(s)
|
|
124
161
|
```
|
|
125
162
|
|
|
126
163
|
(Real timings from a warm package cache — a cold run takes about a minute because
|
|
@@ -147,7 +184,10 @@ without maintaining one template per stack.
|
|
|
147
184
|
|
|
148
185
|
## Adding a stack
|
|
149
186
|
|
|
150
|
-
A stack is a directory with a manifest and a template tree
|
|
187
|
+
A stack is a directory with a manifest and a template tree. The manifest carries four
|
|
188
|
+
optional keys beyond the basics — `detect` (how a directory is recognised as this stack),
|
|
189
|
+
`detect_fallback` (a generic stack that must lose to a specific match), and the `adopt` /
|
|
190
|
+
`adopt_tests` allow-lists used by `newstack adopt`:
|
|
151
191
|
|
|
152
192
|
```text
|
|
153
193
|
src/newstack/data/stacks/my-stack/
|
|
@@ -169,10 +209,16 @@ src/newstack/data/stacks/my-stack/
|
|
|
169
209
|
"next_steps": ["cd {path}", "uv run pytest"],
|
|
170
210
|
"verify": [
|
|
171
211
|
{"name": "test", "cmd": "uv run pytest", "tools": ["uv"], "timeout": 600}
|
|
172
|
-
]
|
|
212
|
+
],
|
|
213
|
+
"detect": [{"file": "pyproject.toml", "contains": "my-framework"}],
|
|
214
|
+
"adopt": [".github/workflows/ci.yml", ".gitignore"],
|
|
215
|
+
"adopt_tests": ["tests/test_smoke.py"]
|
|
173
216
|
}
|
|
174
217
|
```
|
|
175
218
|
|
|
219
|
+
`adopt` entries must exist in the template and must never be an identity file
|
|
220
|
+
(`README.md`, `LICENSE`, `pyproject.toml`, `package.json`, `go.mod`, `Cargo.toml`) — a
|
|
221
|
+
test enforces both rules.<br>
|
|
176
222
|
`{{!` escapes a literal `{{`, and `${{ ... }}` (GitHub Actions) is left untouched.
|
|
177
223
|
Point `NEWSTACK_STACKS` at a directory of your own stacks to use private ones — same
|
|
178
224
|
manifest format, no fork required.
|
|
@@ -189,7 +235,7 @@ manifest format, no fork required.
|
|
|
189
235
|
## Roadmap
|
|
190
236
|
|
|
191
237
|
- `newstack update` — re-apply upstream template changes to an existing project.
|
|
192
|
-
- More stacks:
|
|
238
|
+
- More stacks: Vue, Next.js, Flask.
|
|
193
239
|
- `--license apache-2.0|bsd-3` and other variable choices beyond the current defaults.
|
|
194
240
|
|
|
195
241
|
## Licence
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# newstack
|
|
2
2
|
|
|
3
3
|
[](https://github.com/DawnofGenX/newstack/actions/workflows/ci.yml)
|
|
4
|
+
[](https://pypi.org/project/newstack/)
|
|
4
5
|

|
|
5
6
|

|
|
6
7
|
[](LICENSE)
|
|
@@ -37,12 +38,10 @@ first day of work.
|
|
|
37
38
|
|
|
38
39
|
## Install
|
|
39
40
|
|
|
40
|
-
Not on PyPI yet — install straight from the repository:
|
|
41
|
-
|
|
42
41
|
```bash
|
|
43
|
-
|
|
44
|
-
# or
|
|
45
|
-
|
|
42
|
+
uvx newstack list # run without installing
|
|
43
|
+
uv tool install newstack # or put it on your PATH
|
|
44
|
+
pipx install newstack # any of the usual runners works
|
|
46
45
|
```
|
|
47
46
|
|
|
48
47
|
From a clone:
|
|
@@ -60,6 +59,7 @@ newstack new react-vite-ts ~/code/app
|
|
|
60
59
|
newstack new fastapi ~/code/api -d "Billing service" --author "Your Name"
|
|
61
60
|
newstack new python-lib ~/code/lib --dry-run # preview, write nothing
|
|
62
61
|
newstack new python-cli ~/code/tool --github # create the repo and push it too
|
|
62
|
+
newstack adopt ~/code/old-project --dry-run # retrofit CI/lint onto existing code
|
|
63
63
|
newstack check --all # prove every stack on this machine
|
|
64
64
|
newstack doctor # which toolchains do I have?
|
|
65
65
|
```
|
|
@@ -77,13 +77,46 @@ directory to CI-ready project.
|
|
|
77
77
|
| `python-lib` | src layout, hatchling build, pytest, ruff, 4-version CI matrix | `uv` |
|
|
78
78
|
| `python-cli` | console entry point on argparse, zero runtime deps, tested CLI | `uv` |
|
|
79
79
|
| `fastapi` | typed JSON API, pydantic models, TestClient tests, Dockerfile | `uv` |
|
|
80
|
+
| `django` | Django 6.1 app: config/core split, SQLite, pytest-django, migration drift check | `uv` |
|
|
80
81
|
| `react-vite-ts` | React 19 + Vite + TS, Vitest + Testing Library, typecheck + build | `node`, `npm` |
|
|
81
82
|
| `node-express-ts` | Express 5 API, typed handlers, supertest tests | `node`, `npm` |
|
|
82
|
-
| `
|
|
83
|
+
| `svelte-kit-ts` | SvelteKit + TS, Svelte 5 runes component, Vitest, static adapter build | `node`, `npm` |
|
|
84
|
+
| `go-cli` | stdlib-only Go CLI: `flag` parsing, table tests, vet/test/build gates | `go` |
|
|
85
|
+
| `rust-cli` | std-only Rust CLI: lib+bin split, fmt/clippy/test/release-build gates | `cargo` |
|
|
86
|
+
| `static-site` | HTML/CSS/JS page, `node:test` suite, GitHub Pages deploy workflow | `node` |
|
|
83
87
|
|
|
84
88
|
Each generated project ships its own `.github/workflows/` that is ready to run, plus a
|
|
85
89
|
README, an MIT licence and a `.gitignore`.
|
|
86
90
|
|
|
91
|
+
## Adopting an existing project
|
|
92
|
+
|
|
93
|
+
Most projects do not start from a scaffold. `adopt` retrofits a stack's *infrastructure*
|
|
94
|
+
onto a directory that already has code — and it is additive by construction: **an existing
|
|
95
|
+
file is never opened for writing.**
|
|
96
|
+
|
|
97
|
+
```console
|
|
98
|
+
$ newstack adopt ~/sites/analytics-dashboard
|
|
99
|
+
newstack 0.3.0 · adopt → /home/you/sites/analytics-dashboard
|
|
100
|
+
|
|
101
|
+
stack static-site (auto-detected)
|
|
102
|
+
added 2 files
|
|
103
|
+
+ .github/workflows/deploy-pages.yml
|
|
104
|
+
+ .gitignore
|
|
105
|
+
tests not copied — rerun with --with-tests if you want starter tests
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
- **Detection** reads the project's own markers (`go.mod`, `Cargo.toml`, `manage.py`, the
|
|
109
|
+
dependencies in `pyproject.toml` / `package.json`, a bare `index.html`). If two stacks
|
|
110
|
+
match, it prints the candidates and exits 2 rather than guessing; `--stack` overrides.
|
|
111
|
+
- **Scope**: workflows, `.gitignore`, and per-stack lint/type config only. It will not copy
|
|
112
|
+
a README, a LICENSE, a `pyproject.toml`, or source files — those carry your project's
|
|
113
|
+
identity, not infrastructure.
|
|
114
|
+
- **Merges are reported, not attempted.** If your `pyproject.toml` needs the stack's
|
|
115
|
+
`[tool.ruff]` block, adopt says so and leaves the file alone.
|
|
116
|
+
- `--dry-run` prints the same report without writing. Re-running is a no-op.
|
|
117
|
+
- `--with-tests` additionally copies the stack's *starter* tests — experimental, because
|
|
118
|
+
those tests import the sample module and usually need adapting to your code.
|
|
119
|
+
|
|
87
120
|
## The green guarantee
|
|
88
121
|
|
|
89
122
|
A template is only as good as its last successful run, so `newstack` treats
|
|
@@ -91,12 +124,16 @@ verification as a first-class command:
|
|
|
91
124
|
|
|
92
125
|
```console
|
|
93
126
|
$ newstack check --all
|
|
94
|
-
|
|
95
|
-
|
|
127
|
+
django PASS 1.5s 5 step(s)
|
|
128
|
+
fastapi PASS 1.4s 3 step(s)
|
|
129
|
+
go-cli PASS 0.3s 3 step(s)
|
|
130
|
+
node-express-ts PASS 6.1s 3 step(s)
|
|
96
131
|
python-cli PASS 0.5s 4 step(s)
|
|
97
|
-
python-lib PASS 0.
|
|
98
|
-
react-vite-ts PASS
|
|
132
|
+
python-lib PASS 0.5s 3 step(s)
|
|
133
|
+
react-vite-ts PASS 5.5s 4 step(s)
|
|
134
|
+
rust-cli PASS 0.5s 4 step(s)
|
|
99
135
|
static-site PASS 0.1s 3 step(s)
|
|
136
|
+
svelte-kit-ts PASS 7.1s 4 step(s)
|
|
100
137
|
```
|
|
101
138
|
|
|
102
139
|
(Real timings from a warm package cache — a cold run takes about a minute because
|
|
@@ -123,7 +160,10 @@ without maintaining one template per stack.
|
|
|
123
160
|
|
|
124
161
|
## Adding a stack
|
|
125
162
|
|
|
126
|
-
A stack is a directory with a manifest and a template tree
|
|
163
|
+
A stack is a directory with a manifest and a template tree. The manifest carries four
|
|
164
|
+
optional keys beyond the basics — `detect` (how a directory is recognised as this stack),
|
|
165
|
+
`detect_fallback` (a generic stack that must lose to a specific match), and the `adopt` /
|
|
166
|
+
`adopt_tests` allow-lists used by `newstack adopt`:
|
|
127
167
|
|
|
128
168
|
```text
|
|
129
169
|
src/newstack/data/stacks/my-stack/
|
|
@@ -145,10 +185,16 @@ src/newstack/data/stacks/my-stack/
|
|
|
145
185
|
"next_steps": ["cd {path}", "uv run pytest"],
|
|
146
186
|
"verify": [
|
|
147
187
|
{"name": "test", "cmd": "uv run pytest", "tools": ["uv"], "timeout": 600}
|
|
148
|
-
]
|
|
188
|
+
],
|
|
189
|
+
"detect": [{"file": "pyproject.toml", "contains": "my-framework"}],
|
|
190
|
+
"adopt": [".github/workflows/ci.yml", ".gitignore"],
|
|
191
|
+
"adopt_tests": ["tests/test_smoke.py"]
|
|
149
192
|
}
|
|
150
193
|
```
|
|
151
194
|
|
|
195
|
+
`adopt` entries must exist in the template and must never be an identity file
|
|
196
|
+
(`README.md`, `LICENSE`, `pyproject.toml`, `package.json`, `go.mod`, `Cargo.toml`) — a
|
|
197
|
+
test enforces both rules.<br>
|
|
152
198
|
`{{!` escapes a literal `{{`, and `${{ ... }}` (GitHub Actions) is left untouched.
|
|
153
199
|
Point `NEWSTACK_STACKS` at a directory of your own stacks to use private ones — same
|
|
154
200
|
manifest format, no fork required.
|
|
@@ -165,7 +211,7 @@ manifest format, no fork required.
|
|
|
165
211
|
## Roadmap
|
|
166
212
|
|
|
167
213
|
- `newstack update` — re-apply upstream template changes to an existing project.
|
|
168
|
-
- More stacks:
|
|
214
|
+
- More stacks: Vue, Next.js, Flask.
|
|
169
215
|
- `--license apache-2.0|bsd-3` and other variable choices beyond the current defaults.
|
|
170
216
|
|
|
171
217
|
## Licence
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "newstack"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.0"
|
|
8
8
|
description = "Scaffold a new project on any popular stack — every generated starter is proven green in CI."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""Adopt an existing project: copy a stack's infrastructure, additively.
|
|
2
|
+
|
|
3
|
+
The rule that makes this safe: an existing file is never opened for writing. Anything
|
|
4
|
+
already present is reported as "kept", and anything that would need a merge inside a
|
|
5
|
+
file (a ruff block in someone's pyproject, a test script in their package.json) is
|
|
6
|
+
reported as a manual step instead of being attempted.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from .registry import Stack, load_stacks
|
|
15
|
+
from .render import TemplateError, render_tree
|
|
16
|
+
|
|
17
|
+
# Files adopt must never copy into someone else's project: they carry identity
|
|
18
|
+
# (licence, name, entrypoints) rather than infrastructure.
|
|
19
|
+
FORBIDDEN = frozenset(
|
|
20
|
+
{
|
|
21
|
+
"README.md",
|
|
22
|
+
"README.rst",
|
|
23
|
+
"LICENSE",
|
|
24
|
+
"LICENSE.md",
|
|
25
|
+
"pyproject.toml",
|
|
26
|
+
"package.json",
|
|
27
|
+
"go.mod",
|
|
28
|
+
"Cargo.toml",
|
|
29
|
+
}
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class PendingFile:
|
|
35
|
+
rel: str
|
|
36
|
+
payload: bytes
|
|
37
|
+
executable: bool = False
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class AdoptionPlan:
|
|
42
|
+
stack: Stack
|
|
43
|
+
root: Path
|
|
44
|
+
detected: bool = True
|
|
45
|
+
add: list[PendingFile] = field(default_factory=list)
|
|
46
|
+
keep: list[str] = field(default_factory=list)
|
|
47
|
+
manual: list[str] = field(default_factory=list)
|
|
48
|
+
tests_available: bool = False
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def added(self) -> list[str]:
|
|
52
|
+
return [item.rel for item in self.add]
|
|
53
|
+
|
|
54
|
+
def as_dict(self) -> dict:
|
|
55
|
+
return {
|
|
56
|
+
"stack": self.stack.name,
|
|
57
|
+
"root": str(self.root),
|
|
58
|
+
"detected": self.detected,
|
|
59
|
+
"added": self.added,
|
|
60
|
+
"kept": self.keep,
|
|
61
|
+
"manual": self.manual,
|
|
62
|
+
"tests_available": self.tests_available,
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def detect_stacks(root: Path) -> list[Stack]:
|
|
67
|
+
"""Stacks whose markers match *root*; generic fallbacks are dropped on a specific match."""
|
|
68
|
+
stacks = load_stacks()
|
|
69
|
+
matched = [stack for stack in stacks.values() if stack.detect and stack.matches(root)]
|
|
70
|
+
specific = [stack for stack in matched if not stack.detect_fallback]
|
|
71
|
+
fallbacks = [stack for stack in matched if stack.detect_fallback]
|
|
72
|
+
return specific or fallbacks
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _manual_steps(stack: Stack, root: Path) -> list[str]:
|
|
76
|
+
steps: list[str] = []
|
|
77
|
+
if stack.language == "python":
|
|
78
|
+
target = root / "pyproject.toml"
|
|
79
|
+
if not target.is_file():
|
|
80
|
+
steps.append("no pyproject.toml found: `newstack new` is a better fit than adopt")
|
|
81
|
+
else:
|
|
82
|
+
steps.append(
|
|
83
|
+
"merge the stack's [tool.ruff] / [tool.pytest.ini_options] blocks into "
|
|
84
|
+
"pyproject.toml by hand (adopt never edits existing files)"
|
|
85
|
+
)
|
|
86
|
+
elif stack.language in {"node", "html"}:
|
|
87
|
+
target = root / "package.json"
|
|
88
|
+
if target.is_file():
|
|
89
|
+
steps.append(
|
|
90
|
+
"add the stack's lint/test scripts to package.json by hand "
|
|
91
|
+
"(adopt never edits existing files)"
|
|
92
|
+
)
|
|
93
|
+
elif stack.language == "go":
|
|
94
|
+
if not (root / ".gitignore").exists():
|
|
95
|
+
steps.append("consider adding bin/ to your .gitignore for build output")
|
|
96
|
+
elif stack.language == "rust":
|
|
97
|
+
steps.append("Cargo.lock should be committed for binaries; .gitignore keeps /target out")
|
|
98
|
+
return steps
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def plan_adoption(
|
|
102
|
+
stack: Stack,
|
|
103
|
+
root: Path,
|
|
104
|
+
context: dict,
|
|
105
|
+
*,
|
|
106
|
+
with_tests: bool = False,
|
|
107
|
+
detected: bool = True,
|
|
108
|
+
) -> AdoptionPlan:
|
|
109
|
+
"""Work out what adopt would do, without touching the filesystem."""
|
|
110
|
+
if not stack.adopt:
|
|
111
|
+
raise TemplateError(f"stack '{stack.name}' declares no adoptable files")
|
|
112
|
+
|
|
113
|
+
wanted = list(stack.adopt)
|
|
114
|
+
if with_tests:
|
|
115
|
+
wanted.extend(stack.adopt_tests)
|
|
116
|
+
|
|
117
|
+
forbidden = sorted(set(wanted) & FORBIDDEN)
|
|
118
|
+
if forbidden:
|
|
119
|
+
raise TemplateError(
|
|
120
|
+
f"stack '{stack.name}' tries to adopt files that carry project identity: "
|
|
121
|
+
f"{', '.join(forbidden)}"
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
entries, _dirs = render_tree(stack.template_dir, context)
|
|
125
|
+
by_path = {rel: (payload, executable) for rel, payload, executable in entries}
|
|
126
|
+
|
|
127
|
+
missing = [rel for rel in wanted if rel not in by_path]
|
|
128
|
+
if missing:
|
|
129
|
+
raise TemplateError(
|
|
130
|
+
f"stack '{stack.name}' lists adoptable files that are not in its template: "
|
|
131
|
+
f"{', '.join(missing)}"
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
plan = AdoptionPlan(stack=stack, root=root, detected=detected)
|
|
135
|
+
plan.tests_available = bool(stack.adopt_tests) and with_tests
|
|
136
|
+
for rel in wanted:
|
|
137
|
+
if (root / rel).exists():
|
|
138
|
+
plan.keep.append(rel)
|
|
139
|
+
continue
|
|
140
|
+
payload, executable = by_path[rel]
|
|
141
|
+
plan.add.append(PendingFile(rel=rel, payload=payload, executable=executable))
|
|
142
|
+
|
|
143
|
+
plan.manual = _manual_steps(stack, root)
|
|
144
|
+
return plan
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def apply_plan(plan: AdoptionPlan) -> list[str]:
|
|
148
|
+
"""Write the planned files. Only touches paths that did not exist when planned."""
|
|
149
|
+
written: list[str] = []
|
|
150
|
+
for item in plan.add:
|
|
151
|
+
target = plan.root / item.rel
|
|
152
|
+
if target.exists(): # someone else created it since planning: leave it alone
|
|
153
|
+
plan.keep.append(item.rel)
|
|
154
|
+
continue
|
|
155
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
156
|
+
target.write_bytes(item.payload)
|
|
157
|
+
if item.executable:
|
|
158
|
+
target.chmod(0o755)
|
|
159
|
+
written.append(item.rel)
|
|
160
|
+
return written
|
|
@@ -12,6 +12,7 @@ from datetime import datetime, timezone
|
|
|
12
12
|
from pathlib import Path
|
|
13
13
|
|
|
14
14
|
from . import __version__
|
|
15
|
+
from .adopt import apply_plan, detect_stacks, plan_adoption
|
|
15
16
|
from .registry import RegistryError, Stack, get_stack, load_stacks
|
|
16
17
|
from .render import TemplateError, generate
|
|
17
18
|
from .verify import check_stack, missing_tools
|
|
@@ -72,6 +73,24 @@ def _git_config(key: str) -> str:
|
|
|
72
73
|
return value
|
|
73
74
|
|
|
74
75
|
|
|
76
|
+
class _SafeDict(dict):
|
|
77
|
+
"""format_map target that leaves unknown keys visible instead of raising."""
|
|
78
|
+
|
|
79
|
+
def __missing__(self, key: str) -> str:
|
|
80
|
+
return "{" + key + "}"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _format_step(step: str, context: dict, path: Path) -> str:
|
|
84
|
+
return step.format_map(
|
|
85
|
+
_SafeDict(
|
|
86
|
+
path=path,
|
|
87
|
+
project_name=context["project_name"],
|
|
88
|
+
package_name=context["package_name"],
|
|
89
|
+
npm_name=context["npm_name"],
|
|
90
|
+
)
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
75
94
|
def build_context(stack: Stack, destination: Path, args: argparse.Namespace) -> dict:
|
|
76
95
|
project_name = args.name or destination.name
|
|
77
96
|
author = args.author or _git_config("user.name") or "Your Name"
|
|
@@ -200,18 +219,77 @@ def _print_new_report(
|
|
|
200
219
|
print(f" {'github':<13} {report['github']}")
|
|
201
220
|
|
|
202
221
|
if stack.next_steps:
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
222
|
+
print("\n next:")
|
|
223
|
+
for step in stack.next_steps:
|
|
224
|
+
print(f" {_format_step(step, context, destination)}")
|
|
225
|
+
print()
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def cmd_adopt(args: argparse.Namespace) -> int:
|
|
229
|
+
root = Path(args.path).expanduser().resolve()
|
|
230
|
+
if not root.is_dir():
|
|
231
|
+
raise TemplateError(f"not a directory: {root}")
|
|
232
|
+
|
|
233
|
+
if args.stack:
|
|
234
|
+
stack = get_stack(args.stack)
|
|
235
|
+
detected = False
|
|
236
|
+
else:
|
|
237
|
+
candidates = detect_stacks(root)
|
|
238
|
+
if not candidates:
|
|
239
|
+
raise TemplateError(
|
|
240
|
+
"could not recognise this project's stack — pass --stack "
|
|
241
|
+
"(see `newstack list`)"
|
|
209
242
|
)
|
|
210
|
-
|
|
211
|
-
|
|
243
|
+
if len(candidates) > 1:
|
|
244
|
+
names = ", ".join(candidate.name for candidate in candidates)
|
|
245
|
+
print(f"newstack: this directory matches more than one stack: {names}", file=sys.stderr)
|
|
246
|
+
print("newstack: pass --stack <name> to choose one", file=sys.stderr)
|
|
247
|
+
return 2
|
|
248
|
+
stack = candidates[0]
|
|
249
|
+
detected = True
|
|
250
|
+
|
|
251
|
+
context = build_context(stack, root, args)
|
|
252
|
+
plan = plan_adoption(
|
|
253
|
+
stack, root, context, with_tests=args.with_tests, detected=detected
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
if not args.dry_run:
|
|
257
|
+
apply_plan(plan)
|
|
258
|
+
|
|
259
|
+
if args.json:
|
|
260
|
+
payload = plan.as_dict()
|
|
261
|
+
payload["dry_run"] = args.dry_run
|
|
262
|
+
print(json.dumps(payload, indent=2))
|
|
263
|
+
return 0
|
|
264
|
+
|
|
265
|
+
_print_adopt_report(plan, context, dry_run=args.dry_run)
|
|
266
|
+
return 0
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _print_adopt_report(plan, context: dict, *, dry_run: bool) -> None:
|
|
270
|
+
print(f"\nnewstack {__version__} · adopt → {plan.root}\n")
|
|
271
|
+
how = "auto-detected" if plan.detected else "--stack"
|
|
272
|
+
print(f" {'stack':<12} {plan.stack.name} ({how})")
|
|
273
|
+
verb = "would add" if dry_run else "added"
|
|
274
|
+
print(f" {verb:<12} {len(plan.add)} files")
|
|
275
|
+
for item in plan.add:
|
|
276
|
+
print(f" + {item.rel}")
|
|
277
|
+
if plan.keep:
|
|
278
|
+
print(f" {'kept':<12} {len(plan.keep)} existing files, left untouched")
|
|
279
|
+
for rel in plan.keep:
|
|
280
|
+
print(f" = {rel}")
|
|
281
|
+
if plan.manual:
|
|
282
|
+
print(f" {'manual':<12} {plan.manual[0]}")
|
|
283
|
+
for extra in plan.manual[1:]:
|
|
284
|
+
print(f" {'':<12} {extra}")
|
|
285
|
+
if not plan.add:
|
|
286
|
+
print(f" {'nothing':<12} the infrastructure files are already present")
|
|
287
|
+
if not plan.tests_available and plan.stack.adopt_tests:
|
|
288
|
+
print(f" {'tests':<12} not copied — rerun with --with-tests if you want starter tests")
|
|
289
|
+
if plan.stack.next_steps:
|
|
212
290
|
print("\n next:")
|
|
213
|
-
|
|
214
|
-
|
|
291
|
+
print(f" review the additions: git -C {plan.root} status --short")
|
|
292
|
+
print(" checks run on push: the workflow above is the only thing that changed")
|
|
215
293
|
print()
|
|
216
294
|
|
|
217
295
|
|
|
@@ -349,6 +427,20 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
349
427
|
new.add_argument("--json", action="store_true", help="machine readable output")
|
|
350
428
|
new.set_defaults(handler=cmd_new)
|
|
351
429
|
|
|
430
|
+
adopt = sub.add_parser(
|
|
431
|
+
"adopt", help="retrofit a stack's CI/lint files onto an existing project"
|
|
432
|
+
)
|
|
433
|
+
adopt.add_argument("path", help="directory of the existing project")
|
|
434
|
+
adopt.add_argument("--stack", help="force a stack instead of auto-detecting")
|
|
435
|
+
adopt.add_argument(
|
|
436
|
+
"--with-tests", action="store_true", help="also copy the stack's starter tests"
|
|
437
|
+
)
|
|
438
|
+
adopt.add_argument("--dry-run", action="store_true", help="report without writing anything")
|
|
439
|
+
adopt.add_argument("--author", help="author name for rendered files")
|
|
440
|
+
adopt.add_argument("--email", help="author email for rendered files")
|
|
441
|
+
adopt.add_argument("--json", action="store_true", help="machine readable output")
|
|
442
|
+
adopt.set_defaults(handler=cmd_adopt, name=None, description=None)
|
|
443
|
+
|
|
352
444
|
lst = sub.add_parser("list", help="list available stacks")
|
|
353
445
|
lst.add_argument("--json", action="store_true")
|
|
354
446
|
lst.set_defaults(handler=cmd_list)
|