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.
Files changed (140) hide show
  1. newstack-0.3.0/CHANGELOG.md +63 -0
  2. {newstack-0.1.0 → newstack-0.3.0}/PKG-INFO +60 -14
  3. {newstack-0.1.0 → newstack-0.3.0}/README.md +59 -13
  4. {newstack-0.1.0 → newstack-0.3.0}/pyproject.toml +1 -1
  5. {newstack-0.1.0 → newstack-0.3.0}/src/newstack/__init__.py +1 -1
  6. newstack-0.3.0/src/newstack/adopt.py +160 -0
  7. {newstack-0.1.0 → newstack-0.3.0}/src/newstack/cli.py +102 -10
  8. newstack-0.3.0/src/newstack/data/stacks/django/stack.json +77 -0
  9. newstack-0.3.0/src/newstack/data/stacks/django/template/.github/workflows/ci.yml +31 -0
  10. newstack-0.3.0/src/newstack/data/stacks/django/template/.gitignore +7 -0
  11. newstack-0.3.0/src/newstack/data/stacks/django/template/README.md +67 -0
  12. newstack-0.3.0/src/newstack/data/stacks/django/template/config/__init__.py +0 -0
  13. newstack-0.3.0/src/newstack/data/stacks/django/template/config/asgi.py +9 -0
  14. newstack-0.3.0/src/newstack/data/stacks/django/template/config/settings.py +82 -0
  15. newstack-0.3.0/src/newstack/data/stacks/django/template/config/urls.py +9 -0
  16. newstack-0.3.0/src/newstack/data/stacks/django/template/config/wsgi.py +9 -0
  17. newstack-0.3.0/src/newstack/data/stacks/django/template/core/__init__.py +0 -0
  18. newstack-0.3.0/src/newstack/data/stacks/django/template/core/apps.py +6 -0
  19. newstack-0.3.0/src/newstack/data/stacks/django/template/core/migrations/0001_initial.py +32 -0
  20. newstack-0.3.0/src/newstack/data/stacks/django/template/core/migrations/__init__.py +0 -0
  21. newstack-0.3.0/src/newstack/data/stacks/django/template/core/models.py +12 -0
  22. newstack-0.3.0/src/newstack/data/stacks/django/template/core/urls.py +8 -0
  23. newstack-0.3.0/src/newstack/data/stacks/django/template/core/views.py +22 -0
  24. newstack-0.3.0/src/newstack/data/stacks/django/template/manage.py +23 -0
  25. newstack-0.3.0/src/newstack/data/stacks/django/template/pyproject.toml +43 -0
  26. newstack-0.3.0/src/newstack/data/stacks/django/template/tests/test_models.py +20 -0
  27. newstack-0.3.0/src/newstack/data/stacks/django/template/tests/test_views.py +30 -0
  28. {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/fastapi/stack.json +15 -0
  29. newstack-0.3.0/src/newstack/data/stacks/go-cli/stack.json +55 -0
  30. newstack-0.3.0/src/newstack/data/stacks/go-cli/template/.github/workflows/ci.yml +21 -0
  31. newstack-0.3.0/src/newstack/data/stacks/go-cli/template/.gitignore +15 -0
  32. newstack-0.3.0/src/newstack/data/stacks/go-cli/template/README.md +55 -0
  33. newstack-0.3.0/src/newstack/data/stacks/go-cli/template/go.mod +3 -0
  34. newstack-0.3.0/src/newstack/data/stacks/go-cli/template/internal/app/app.go +69 -0
  35. newstack-0.3.0/src/newstack/data/stacks/go-cli/template/internal/app/app_test.go +72 -0
  36. newstack-0.3.0/src/newstack/data/stacks/go-cli/template/main.go +12 -0
  37. {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/node-express-ts/stack.json +15 -0
  38. {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/python-cli/stack.json +17 -0
  39. {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/python-lib/stack.json +13 -0
  40. {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/react-vite-ts/stack.json +16 -0
  41. newstack-0.3.0/src/newstack/data/stacks/react-vite-ts/template/LICENSE +21 -0
  42. newstack-0.3.0/src/newstack/data/stacks/rust-cli/stack.json +63 -0
  43. newstack-0.3.0/src/newstack/data/stacks/rust-cli/template/.github/workflows/ci.yml +32 -0
  44. newstack-0.3.0/src/newstack/data/stacks/rust-cli/template/.gitignore +1 -0
  45. newstack-0.3.0/src/newstack/data/stacks/rust-cli/template/Cargo.toml +17 -0
  46. newstack-0.3.0/src/newstack/data/stacks/rust-cli/template/LICENSE +21 -0
  47. newstack-0.3.0/src/newstack/data/stacks/rust-cli/template/README.md +46 -0
  48. newstack-0.3.0/src/newstack/data/stacks/rust-cli/template/src/lib.rs +120 -0
  49. newstack-0.3.0/src/newstack/data/stacks/rust-cli/template/src/main.rs +20 -0
  50. newstack-0.3.0/src/newstack/data/stacks/rust-cli/template/tests/cli.rs +46 -0
  51. {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/static-site/stack.json +13 -0
  52. newstack-0.3.0/src/newstack/data/stacks/static-site/template/LICENSE +21 -0
  53. newstack-0.3.0/src/newstack/data/stacks/svelte-kit-ts/stack.json +31 -0
  54. newstack-0.3.0/src/newstack/data/stacks/svelte-kit-ts/template/.github/workflows/ci.yml +31 -0
  55. newstack-0.3.0/src/newstack/data/stacks/svelte-kit-ts/template/.gitignore +23 -0
  56. newstack-0.3.0/src/newstack/data/stacks/svelte-kit-ts/template/.npmrc +1 -0
  57. newstack-0.3.0/src/newstack/data/stacks/svelte-kit-ts/template/LICENSE +21 -0
  58. newstack-0.3.0/src/newstack/data/stacks/svelte-kit-ts/template/README.md +71 -0
  59. newstack-0.3.0/src/newstack/data/stacks/svelte-kit-ts/template/package.json +34 -0
  60. newstack-0.3.0/src/newstack/data/stacks/svelte-kit-ts/template/src/app.d.ts +13 -0
  61. newstack-0.3.0/src/newstack/data/stacks/svelte-kit-ts/template/src/app.html +13 -0
  62. newstack-0.3.0/src/newstack/data/stacks/svelte-kit-ts/template/src/lib/Counter.svelte +7 -0
  63. newstack-0.3.0/src/newstack/data/stacks/svelte-kit-ts/template/src/lib/Counter.test.ts +17 -0
  64. newstack-0.3.0/src/newstack/data/stacks/svelte-kit-ts/template/src/lib/assets/favicon.svg +1 -0
  65. newstack-0.3.0/src/newstack/data/stacks/svelte-kit-ts/template/src/lib/index.ts +1 -0
  66. newstack-0.3.0/src/newstack/data/stacks/svelte-kit-ts/template/src/routes/+layout.svelte +12 -0
  67. newstack-0.3.0/src/newstack/data/stacks/svelte-kit-ts/template/src/routes/+layout.ts +1 -0
  68. newstack-0.3.0/src/newstack/data/stacks/svelte-kit-ts/template/src/routes/+page.svelte +35 -0
  69. newstack-0.3.0/src/newstack/data/stacks/svelte-kit-ts/template/src/vitest-setup.ts +5 -0
  70. newstack-0.3.0/src/newstack/data/stacks/svelte-kit-ts/template/static/robots.txt +3 -0
  71. newstack-0.3.0/src/newstack/data/stacks/svelte-kit-ts/template/tsconfig.json +7 -0
  72. newstack-0.3.0/src/newstack/data/stacks/svelte-kit-ts/template/vite.config.ts +26 -0
  73. {newstack-0.1.0 → newstack-0.3.0}/src/newstack/registry.py +50 -0
  74. {newstack-0.1.0 → newstack-0.3.0}/src/newstack/render.py +12 -0
  75. newstack-0.3.0/tests/test_adopt.py +192 -0
  76. newstack-0.3.0/tests/test_docs.py +38 -0
  77. newstack-0.1.0/CHANGELOG.md +0 -22
  78. {newstack-0.1.0 → newstack-0.3.0}/.gitignore +0 -0
  79. {newstack-0.1.0 → newstack-0.3.0}/LICENSE +0 -0
  80. {newstack-0.1.0 → newstack-0.3.0}/src/newstack/__main__.py +0 -0
  81. {newstack-0.1.0/src/newstack/data/stacks/fastapi → newstack-0.3.0/src/newstack/data/stacks/django}/template/LICENSE +0 -0
  82. {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/fastapi/template/.github/workflows/ci.yml +0 -0
  83. {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/fastapi/template/.gitignore +0 -0
  84. {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/fastapi/template/Dockerfile +0 -0
  85. {newstack-0.1.0/src/newstack/data/stacks/node-express-ts → newstack-0.3.0/src/newstack/data/stacks/fastapi}/template/LICENSE +0 -0
  86. {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/fastapi/template/README.md +0 -0
  87. {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/fastapi/template/pyproject.toml +0 -0
  88. {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/fastapi/template/src/{{package_name}}/__init__.py +0 -0
  89. {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/fastapi/template/src/{{package_name}}/main.py +0 -0
  90. {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/fastapi/template/tests/test_api.py +0 -0
  91. {newstack-0.1.0/src/newstack/data/stacks/python-cli → newstack-0.3.0/src/newstack/data/stacks/go-cli}/template/LICENSE +0 -0
  92. {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/node-express-ts/template/.github/workflows/ci.yml +0 -0
  93. {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/node-express-ts/template/.gitignore +0 -0
  94. {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
  95. {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/node-express-ts/template/README.md +0 -0
  96. {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/node-express-ts/template/package.json +0 -0
  97. {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/node-express-ts/template/src/app.ts +0 -0
  98. {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/node-express-ts/template/src/server.ts +0 -0
  99. {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/node-express-ts/template/tests/app.test.ts +0 -0
  100. {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/node-express-ts/template/tsconfig.json +0 -0
  101. {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/python-cli/template/.github/workflows/ci.yml +0 -0
  102. {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/python-cli/template/.gitignore +0 -0
  103. {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
  104. {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/python-cli/template/README.md +0 -0
  105. {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/python-cli/template/pyproject.toml +0 -0
  106. {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/python-cli/template/src/{{package_name}}/__init__.py +0 -0
  107. {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/python-cli/template/src/{{package_name}}/__main__.py +0 -0
  108. {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/python-cli/template/src/{{package_name}}/cli.py +0 -0
  109. {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/python-cli/template/tests/test_cli.py +0 -0
  110. {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/python-lib/template/.github/workflows/ci.yml +0 -0
  111. {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/python-lib/template/.gitignore +0 -0
  112. {newstack-0.1.0/src/newstack/data/stacks/static-site → newstack-0.3.0/src/newstack/data/stacks/python-lib}/template/LICENSE +0 -0
  113. {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/python-lib/template/README.md +0 -0
  114. {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/python-lib/template/pyproject.toml +0 -0
  115. {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/python-lib/template/src/{{package_name}}/__init__.py +0 -0
  116. {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/python-lib/template/src/{{package_name}}/core.py +0 -0
  117. {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/python-lib/template/tests/test_core.py +0 -0
  118. {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/react-vite-ts/template/.github/workflows/ci.yml +0 -0
  119. {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/react-vite-ts/template/.gitignore +0 -0
  120. {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/react-vite-ts/template/README.md +0 -0
  121. {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/react-vite-ts/template/index.html +0 -0
  122. {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/react-vite-ts/template/package.json +0 -0
  123. {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/react-vite-ts/template/src/App.test.tsx +0 -0
  124. {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/react-vite-ts/template/src/App.tsx +0 -0
  125. {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/react-vite-ts/template/src/main.tsx +0 -0
  126. {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/react-vite-ts/template/src/styles.css +0 -0
  127. {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/react-vite-ts/template/tsconfig.json +0 -0
  128. {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/react-vite-ts/template/vite.config.ts +0 -0
  129. {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/static-site/template/.github/workflows/deploy-pages.yml +0 -0
  130. {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/static-site/template/.gitignore +0 -0
  131. {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/static-site/template/README.md +0 -0
  132. {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/static-site/template/index.html +0 -0
  133. {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/static-site/template/main.js +0 -0
  134. {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/static-site/template/styles.css +0 -0
  135. {newstack-0.1.0 → newstack-0.3.0}/src/newstack/data/stacks/static-site/template/tests/site.test.mjs +0 -0
  136. {newstack-0.1.0 → newstack-0.3.0}/src/newstack/verify.py +0 -0
  137. {newstack-0.1.0 → newstack-0.3.0}/tests/test_cli.py +0 -0
  138. {newstack-0.1.0 → newstack-0.3.0}/tests/test_registry.py +0 -0
  139. {newstack-0.1.0 → newstack-0.3.0}/tests/test_render.py +0 -0
  140. {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.1.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
  [![CI](https://github.com/DawnofGenX/newstack/actions/workflows/ci.yml/badge.svg)](https://github.com/DawnofGenX/newstack/actions/workflows/ci.yml)
28
+ [![PyPI](https://img.shields.io/pypi/v/newstack)](https://pypi.org/project/newstack/)
28
29
  ![python](https://img.shields.io/badge/python-3.10%2B-blue)
29
30
  ![dependencies](https://img.shields.io/badge/runtime%20dependencies-0-brightgreen)
30
31
  [![license](https://img.shields.io/badge/license-MIT-blue)](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
- uv tool install git+https://github.com/DawnofGenX/newstack
68
- # or, without installing:
69
- uvx --from git+https://github.com/DawnofGenX/newstack newstack list
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
- | `static-site` | HTML/CSS/JS page with a GitHub Pages deploy workflow | `node` |
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
- fastapi PASS 1.0s 3 step(s)
119
- node-express-ts PASS 2.2s 3 step(s)
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.4s 3 step(s)
122
- react-vite-ts PASS 3.2s 4 step(s)
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: Go service, Rust CLI, Django, Vue, SvelteKit.
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
  [![CI](https://github.com/DawnofGenX/newstack/actions/workflows/ci.yml/badge.svg)](https://github.com/DawnofGenX/newstack/actions/workflows/ci.yml)
4
+ [![PyPI](https://img.shields.io/pypi/v/newstack)](https://pypi.org/project/newstack/)
4
5
  ![python](https://img.shields.io/badge/python-3.10%2B-blue)
5
6
  ![dependencies](https://img.shields.io/badge/runtime%20dependencies-0-brightgreen)
6
7
  [![license](https://img.shields.io/badge/license-MIT-blue)](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
- uv tool install git+https://github.com/DawnofGenX/newstack
44
- # or, without installing:
45
- uvx --from git+https://github.com/DawnofGenX/newstack newstack list
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
- | `static-site` | HTML/CSS/JS page with a GitHub Pages deploy workflow | `node` |
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
- fastapi PASS 1.0s 3 step(s)
95
- node-express-ts PASS 2.2s 3 step(s)
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.4s 3 step(s)
98
- react-vite-ts PASS 3.2s 4 step(s)
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: Go service, Rust CLI, Django, Vue, SvelteKit.
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.1.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"
@@ -1,4 +1,4 @@
1
1
  """newstack — scaffold a new project on any popular stack, proven green in CI."""
2
2
 
3
- __version__ = "0.1.0"
3
+ __version__ = "0.3.0"
4
4
  __all__ = ["__version__"]
@@ -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
- steps = [
204
- step.format(
205
- path=destination,
206
- package_name=context["package_name"],
207
- npm_name=context["npm_name"],
208
- project_name=context["project_name"],
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
- for step in stack.next_steps
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
- for step in steps:
214
- print(f" {step}")
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)