create-forge 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- create_forge/__init__.py +1 -0
- create_forge/cli.py +824 -0
- create_forge/compat.py +47 -0
- create_forge/config.py +126 -0
- create_forge/engine.py +228 -0
- create_forge/models.py +142 -0
- create_forge/pipeline.py +141 -0
- create_forge/prompts.py +231 -0
- create_forge/registry.py +36 -0
- create_forge/runner.py +148 -0
- create_forge/spec.py +192 -0
- create_forge/staging.py +180 -0
- create_forge/templates.toml +121 -0
- create_forge-0.2.0.dist-info/METADATA +207 -0
- create_forge-0.2.0.dist-info/RECORD +18 -0
- create_forge-0.2.0.dist-info/WHEEL +4 -0
- create_forge-0.2.0.dist-info/entry_points.txt +2 -0
- create_forge-0.2.0.dist-info/licenses/LICENSE +21 -0
create_forge/staging.py
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""Filesystem orchestration shared by the Copier and engine generation paths.
|
|
2
|
+
|
|
3
|
+
Deliberately engine-free: nothing here imports `forge_template`, not even
|
|
4
|
+
under `TYPE_CHECKING`. That keeps this module in the wheel and in the fast
|
|
5
|
+
test suite with no `engine` dependency group installed, and lets it serve
|
|
6
|
+
both `runner.scaffold()` (Copier writes straight to the destination; this
|
|
7
|
+
module only cleans up after a failure) and `pipeline.finalise_generation_request()`
|
|
8
|
+
(the engine path; this module stages and atomically finalises). See
|
|
9
|
+
[ADR 0015](../../docs/adr/0015-staged-filesystem-generation.md) and the
|
|
10
|
+
canonical [filesystem generation contract](../../docs/filesystem-generation.md)
|
|
11
|
+
for why the two paths differ and what each guarantees.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import contextlib
|
|
17
|
+
import shutil
|
|
18
|
+
import tempfile
|
|
19
|
+
import warnings
|
|
20
|
+
from pathlib import Path, PurePosixPath
|
|
21
|
+
from typing import TYPE_CHECKING
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from collections.abc import Iterable, Iterator
|
|
25
|
+
|
|
26
|
+
_STAGING_PREFIX = ".create-forge-"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class StagingError(Exception):
|
|
30
|
+
"""A filesystem operation failed in a way the user can act on."""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class DestinationConflictError(StagingError):
|
|
34
|
+
"""The destination already exists and is not empty."""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def ensure_available(dst: Path) -> None:
|
|
38
|
+
"""Reject a destination that already exists and has content.
|
|
39
|
+
|
|
40
|
+
Cheap and side-effect free -- callers run this before any compatibility
|
|
41
|
+
check or write, so an obvious conflict is reported before anything else
|
|
42
|
+
is attempted.
|
|
43
|
+
"""
|
|
44
|
+
if dst.exists() and any(dst.iterdir()):
|
|
45
|
+
msg = f"{dst} already exists and is not empty"
|
|
46
|
+
raise DestinationConflictError(msg)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _safe_relative_path(root: Path, target: str) -> Path:
|
|
50
|
+
"""Resolve one project-relative target under `root`, refusing escapes.
|
|
51
|
+
|
|
52
|
+
Targets are engine-owned strings using forward slashes (`RenderedFile`
|
|
53
|
+
documents them as project-relative). `PurePosixPath` parses them
|
|
54
|
+
platform-independently before `Path` joins them, so a target containing
|
|
55
|
+
backslashes is treated as a literal filename component, never as a
|
|
56
|
+
Windows separator.
|
|
57
|
+
"""
|
|
58
|
+
posix_target = PurePosixPath(target)
|
|
59
|
+
if posix_target.is_absolute() or posix_target.drive:
|
|
60
|
+
msg = f"refusing to write outside the staging directory: {target!r}"
|
|
61
|
+
raise StagingError(msg)
|
|
62
|
+
if ".." in posix_target.parts:
|
|
63
|
+
msg = f"refusing to write outside the staging directory: {target!r}"
|
|
64
|
+
raise StagingError(msg)
|
|
65
|
+
|
|
66
|
+
resolved_root = root.resolve()
|
|
67
|
+
destination = (root / Path(*posix_target.parts)).resolve()
|
|
68
|
+
if destination != resolved_root and resolved_root not in destination.parents:
|
|
69
|
+
msg = f"refusing to write outside the staging directory: {target!r}"
|
|
70
|
+
raise StagingError(msg)
|
|
71
|
+
return destination
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def write_files(root: Path, files: Iterable[tuple[str, bytes]]) -> None:
|
|
75
|
+
"""Write each (target, content) pair under `root`, creating parents.
|
|
76
|
+
|
|
77
|
+
Every target is validated before anything is written -- an absolute
|
|
78
|
+
path, a drive-qualified path, or a `..` segment anywhere in a single
|
|
79
|
+
target aborts the whole call with nothing written by it.
|
|
80
|
+
"""
|
|
81
|
+
resolved: list[tuple[Path, bytes]] = [
|
|
82
|
+
(_safe_relative_path(root, target), content) for target, content in files
|
|
83
|
+
]
|
|
84
|
+
for path, content in resolved:
|
|
85
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
86
|
+
try:
|
|
87
|
+
path.write_bytes(content)
|
|
88
|
+
except OSError as exc:
|
|
89
|
+
msg = f"could not write {path}: {exc}"
|
|
90
|
+
raise StagingError(msg) from exc
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _on_rm_error(func: object, path: str, exc_info: object) -> None:
|
|
94
|
+
"""`shutil.rmtree` error handler: clear read-only and retry once.
|
|
95
|
+
|
|
96
|
+
Accepted as both 3.12+'s `onexc` and 3.11's `onerror` -- both call the
|
|
97
|
+
handler with three positional arguments; the third (`exc_info`) is
|
|
98
|
+
unused by either signature this implements.
|
|
99
|
+
"""
|
|
100
|
+
del exc_info
|
|
101
|
+
target = Path(path)
|
|
102
|
+
target.chmod(0o700)
|
|
103
|
+
if callable(func):
|
|
104
|
+
func(path)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _remove_tree(path: Path) -> None:
|
|
108
|
+
"""Best-effort recursive removal that clears read-only files first.
|
|
109
|
+
|
|
110
|
+
Never raises: a failed cleanup must not mask the original error that
|
|
111
|
+
triggered it, so a residual directory is reported as a warning instead.
|
|
112
|
+
"""
|
|
113
|
+
if not path.exists():
|
|
114
|
+
return
|
|
115
|
+
try:
|
|
116
|
+
try:
|
|
117
|
+
# `onexc` only exists from Python 3.12; mypy is pinned to the 3.11
|
|
118
|
+
# stub (pyproject.toml's python_version), so this call needs an
|
|
119
|
+
# explicit ignore regardless of the interpreter mypy itself runs
|
|
120
|
+
# under.
|
|
121
|
+
shutil.rmtree(path, onexc=_on_rm_error) # type: ignore[call-arg]
|
|
122
|
+
except TypeError:
|
|
123
|
+
shutil.rmtree(path, onerror=_on_rm_error)
|
|
124
|
+
except OSError as exc:
|
|
125
|
+
warnings.warn(f"could not remove {path}: {exc}", RuntimeWarning, stacklevel=2)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@contextlib.contextmanager
|
|
129
|
+
def staged(dst: Path) -> Iterator[Path]:
|
|
130
|
+
"""Yield a staging directory adjacent to `dst`; finalise by atomic rename.
|
|
131
|
+
|
|
132
|
+
The staging directory is created next to `dst` with `tempfile.mkdtemp`,
|
|
133
|
+
not under the system temp directory -- same-volume placement is what
|
|
134
|
+
makes the finalising `Path.rename` an atomic directory rename on both
|
|
135
|
+
NTFS and POSIX rather than a copy. There is deliberately no cross-volume
|
|
136
|
+
copy fallback: that would silently trade the atomicity guarantee for
|
|
137
|
+
availability, so a cross-volume destination fails instead.
|
|
138
|
+
|
|
139
|
+
On success, an existing *empty* `dst` is removed first -- `os.rename`
|
|
140
|
+
will not replace a directory on Windows. On any exception, the staging
|
|
141
|
+
tree is removed and the original error propagates; `dst` is left exactly
|
|
142
|
+
as it was found.
|
|
143
|
+
"""
|
|
144
|
+
ensure_available(dst)
|
|
145
|
+
dst.parent.mkdir(parents=True, exist_ok=True)
|
|
146
|
+
staging_dir = Path(tempfile.mkdtemp(prefix=_STAGING_PREFIX, dir=dst.parent))
|
|
147
|
+
|
|
148
|
+
try:
|
|
149
|
+
yield staging_dir
|
|
150
|
+
if dst.exists():
|
|
151
|
+
dst.rmdir()
|
|
152
|
+
try:
|
|
153
|
+
staging_dir.rename(dst)
|
|
154
|
+
except OSError as exc:
|
|
155
|
+
msg = f"could not move {staging_dir} into place at {dst}: {exc}"
|
|
156
|
+
raise StagingError(msg) from exc
|
|
157
|
+
except BaseException:
|
|
158
|
+
_remove_tree(staging_dir)
|
|
159
|
+
raise
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@contextlib.contextmanager
|
|
163
|
+
def discard_on_failure(dst: Path) -> Iterator[None]:
|
|
164
|
+
"""Remove `dst` on failure, but only if this call is what created it.
|
|
165
|
+
|
|
166
|
+
Used around the Copier path, which writes straight to `dst` and cannot
|
|
167
|
+
safely be staged: templates run `_tasks` (`uv sync`, `pre-commit
|
|
168
|
+
install`) that bake `dst`'s absolute path into `.venv/pyvenv.cfg`,
|
|
169
|
+
console-script shims, and `.git/hooks/pre-commit`. Renaming a completed
|
|
170
|
+
Copier output afterward would silently break all three, so this context
|
|
171
|
+
manager only ever cleans up a failure at the path Copier already used --
|
|
172
|
+
it never stages or moves anything.
|
|
173
|
+
"""
|
|
174
|
+
pre_existing = dst.exists()
|
|
175
|
+
try:
|
|
176
|
+
yield
|
|
177
|
+
except BaseException:
|
|
178
|
+
if not pre_existing:
|
|
179
|
+
_remove_tree(dst)
|
|
180
|
+
raise
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# Bundled template registry.
|
|
2
|
+
#
|
|
3
|
+
# Adding a template here requires a new CLI release. Validated at import time by
|
|
4
|
+
# the Pydantic models in models.py, and in CI by tests/test_registry.py.
|
|
5
|
+
#
|
|
6
|
+
# Every `key` below must match a question in forge-template's copier.yml
|
|
7
|
+
# (invariant 1 in CLAUDE.md — Copier silently drops unknown `data` keys, so a
|
|
8
|
+
# typo here produces a scaffold that looks fine and is subtly wrong). Audited
|
|
9
|
+
# against forge-template@v0.1.1. Questions deliberately left unasked and
|
|
10
|
+
# falling through to their copier.yml default via runner.py's `defaults=True`:
|
|
11
|
+
# package_name, repo_name, repo_url, author_name, author_email,
|
|
12
|
+
# codeowners_team, python_all, python_version, python_min_version,
|
|
13
|
+
# python_matrix, versioning_resolved, initial_version, coverage_fail_under,
|
|
14
|
+
# dependency_updates, changelog_tool. All of them have a default, so this is
|
|
15
|
+
# safe — a test guarding it is tracked as a follow-up issue.
|
|
16
|
+
|
|
17
|
+
default_template = "library"
|
|
18
|
+
|
|
19
|
+
[[templates]]
|
|
20
|
+
id = "library"
|
|
21
|
+
name = "Library"
|
|
22
|
+
description = "An installable Python package. src layout, no service scaffolding."
|
|
23
|
+
url = "https://github.com/Sandsy09/forge-template"
|
|
24
|
+
status = "stable"
|
|
25
|
+
|
|
26
|
+
# ---- identity -------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
[[templates.prompts]]
|
|
29
|
+
key = "project_name"
|
|
30
|
+
kind = "text"
|
|
31
|
+
message = "Project name"
|
|
32
|
+
help = "Human readable, e.g. 'Credit Risk Utils'"
|
|
33
|
+
|
|
34
|
+
[[templates.prompts]]
|
|
35
|
+
key = "project_description"
|
|
36
|
+
kind = "text"
|
|
37
|
+
message = "Short description"
|
|
38
|
+
|
|
39
|
+
[[templates.prompts]]
|
|
40
|
+
key = "github_org"
|
|
41
|
+
kind = "text"
|
|
42
|
+
message = "GitHub organisation"
|
|
43
|
+
help = "Pre-filled from your forge config if set"
|
|
44
|
+
|
|
45
|
+
[[templates.prompts]]
|
|
46
|
+
key = "license"
|
|
47
|
+
kind = "select"
|
|
48
|
+
message = "License"
|
|
49
|
+
|
|
50
|
+
[[templates.prompts.choices]]
|
|
51
|
+
value = "proprietary"
|
|
52
|
+
label = "Proprietary"
|
|
53
|
+
hint = "internal use only"
|
|
54
|
+
|
|
55
|
+
[[templates.prompts.choices]]
|
|
56
|
+
value = "mit"
|
|
57
|
+
label = "MIT"
|
|
58
|
+
|
|
59
|
+
[[templates.prompts.choices]]
|
|
60
|
+
value = "apache-2.0"
|
|
61
|
+
label = "Apache-2.0"
|
|
62
|
+
|
|
63
|
+
# ---- the one question that constrains another -----------------------------
|
|
64
|
+
|
|
65
|
+
[[templates.prompts]]
|
|
66
|
+
key = "build_backend"
|
|
67
|
+
kind = "select"
|
|
68
|
+
message = "Build backend"
|
|
69
|
+
|
|
70
|
+
[[templates.prompts.choices]]
|
|
71
|
+
value = "uv_build"
|
|
72
|
+
label = "uv_build"
|
|
73
|
+
hint = "simplest; static version only"
|
|
74
|
+
|
|
75
|
+
[[templates.prompts.choices]]
|
|
76
|
+
value = "hatchling"
|
|
77
|
+
label = "Hatchling"
|
|
78
|
+
hint = "required for git-tag versioning"
|
|
79
|
+
|
|
80
|
+
[[templates.prompts]]
|
|
81
|
+
key = "versioning"
|
|
82
|
+
kind = "select"
|
|
83
|
+
message = "Version source"
|
|
84
|
+
depends_on = { build_backend = "hatchling" }
|
|
85
|
+
|
|
86
|
+
[[templates.prompts.choices]]
|
|
87
|
+
value = "static"
|
|
88
|
+
label = "Static in pyproject.toml"
|
|
89
|
+
hint = "explicit; uv version works"
|
|
90
|
+
|
|
91
|
+
[[templates.prompts.choices]]
|
|
92
|
+
value = "vcs"
|
|
93
|
+
label = "Git tags (hatch-vcs)"
|
|
94
|
+
hint = "tag and version cannot disagree"
|
|
95
|
+
|
|
96
|
+
# ---- tooling --------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
[[templates.prompts]]
|
|
99
|
+
key = "type_checking"
|
|
100
|
+
kind = "select"
|
|
101
|
+
message = "Type checker"
|
|
102
|
+
|
|
103
|
+
[[templates.prompts.choices]]
|
|
104
|
+
value = "mypy"
|
|
105
|
+
label = "mypy"
|
|
106
|
+
hint = "recommended default"
|
|
107
|
+
|
|
108
|
+
[[templates.prompts.choices]]
|
|
109
|
+
value = "pyright"
|
|
110
|
+
label = "pyright"
|
|
111
|
+
|
|
112
|
+
[[templates.prompts.choices]]
|
|
113
|
+
value = "both"
|
|
114
|
+
label = "Both"
|
|
115
|
+
hint = "strictest; two ignore dialects to manage"
|
|
116
|
+
|
|
117
|
+
[[templates.prompts]]
|
|
118
|
+
key = "use_docs"
|
|
119
|
+
kind = "confirm"
|
|
120
|
+
message = "Include a MkDocs documentation site?"
|
|
121
|
+
default = false
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: create-forge
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Scaffold modern Python projects from maintained templates.
|
|
5
|
+
Project-URL: Homepage, https://github.com/Sandsy09/create-forge
|
|
6
|
+
Project-URL: Repository, https://github.com/Sandsy09/create-forge
|
|
7
|
+
Project-URL: Issues, https://github.com/Sandsy09/create-forge/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/Sandsy09/create-forge/blob/main/CHANGELOG.md
|
|
9
|
+
Author: Alex
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: cookiecutter,copier,project,scaffold,template
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Environment :: Console
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
20
|
+
Classifier: Topic :: Software Development :: Code Generators
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Python: >=3.11
|
|
23
|
+
Requires-Dist: copier<10,>=9.4
|
|
24
|
+
Requires-Dist: pydantic>=2.10
|
|
25
|
+
Requires-Dist: questionary>=2.0
|
|
26
|
+
Requires-Dist: rich>=13.9
|
|
27
|
+
Requires-Dist: typer>=0.15
|
|
28
|
+
Provides-Extra: engine
|
|
29
|
+
Requires-Dist: forge-template<0.4,>=0.3.1; extra == 'engine'
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
# create-forge
|
|
33
|
+
|
|
34
|
+
[](https://github.com/Sandsy09/create-forge/actions/workflows/ci.yml)
|
|
35
|
+
|
|
36
|
+
Scaffold modern Python projects from maintained templates — and pull template
|
|
37
|
+
improvements back into projects you generated months ago.
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
uvx create-forge new
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
No install step. Requires [uv](https://docs.astral.sh/uv/) and git.
|
|
44
|
+
|
|
45
|
+
## Why
|
|
46
|
+
|
|
47
|
+
Most project generators are fire-and-forget: you scaffold once, and from that
|
|
48
|
+
moment your project drifts away from the template. Six months later the template
|
|
49
|
+
has better lint rules, a security fix in CI, and a newer toolchain — and no path
|
|
50
|
+
to get any of it into projects already in the wild.
|
|
51
|
+
|
|
52
|
+
create-forge is built on [Copier](https://copier.readthedocs.io/), which does a
|
|
53
|
+
three-way merge between the template version your project was generated from and
|
|
54
|
+
the latest one. Local edits survive; template changes arrive.
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
uvx create-forge update
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## What you get
|
|
61
|
+
|
|
62
|
+
Every generated project ships with:
|
|
63
|
+
|
|
64
|
+
- **[uv](https://docs.astral.sh/uv/)** for packaging and dependency management
|
|
65
|
+
- **[Ruff](https://docs.astral.sh/ruff/)** for linting and formatting
|
|
66
|
+
- **mypy** or **pyright** (or both) for type checking
|
|
67
|
+
- **pytest** with coverage
|
|
68
|
+
- **pre-commit** hooks, including Conventional Commits enforcement
|
|
69
|
+
- **GitHub Actions** CI, with a test matrix across your supported Python versions
|
|
70
|
+
- **Renovate** or **Dependabot** for dependency updates
|
|
71
|
+
- `README`, `CONTRIBUTING`, `SECURITY`, `CHANGELOG`, issue and PR templates
|
|
72
|
+
- Optionally: a MkDocs documentation site and ADR scaffolding
|
|
73
|
+
|
|
74
|
+
Choices you make at scaffold time — build backend, versioning strategy, license,
|
|
75
|
+
type checker — are remembered, so updates respect them.
|
|
76
|
+
|
|
77
|
+
## Usage
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
# Interactive
|
|
81
|
+
uvx create-forge new
|
|
82
|
+
|
|
83
|
+
# Named up front
|
|
84
|
+
uvx create-forge new "Credit Risk Utils"
|
|
85
|
+
|
|
86
|
+
# Non-interactive, for scripts and CI
|
|
87
|
+
uvx create-forge new "My Lib" --yes \
|
|
88
|
+
--data build_backend=hatchling \
|
|
89
|
+
--data versioning=vcs \
|
|
90
|
+
--data type_checking=both
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
| Command | What it does |
|
|
94
|
+
| --- | --- |
|
|
95
|
+
| `new` | Create a project |
|
|
96
|
+
| `list` | Show available templates |
|
|
97
|
+
| `update` | Pull template changes into an existing project |
|
|
98
|
+
| `doctor` | Check your environment can scaffold and update |
|
|
99
|
+
| `config` | Inspect or initialise your saved configuration |
|
|
100
|
+
|
|
101
|
+
Useful flags on `new`: `--template/-t`, `--path/-p`, `--data/-d`, `--yes/-y`,
|
|
102
|
+
`--ref`, `--dry-run`.
|
|
103
|
+
|
|
104
|
+
## Configuration
|
|
105
|
+
|
|
106
|
+
Optional. Saves retyping the same answers:
|
|
107
|
+
|
|
108
|
+
```toml
|
|
109
|
+
# ~/.config/create-forge/config.toml
|
|
110
|
+
author_name = "Your Name"
|
|
111
|
+
author_email = "you@example.com"
|
|
112
|
+
github_org = "your-org"
|
|
113
|
+
default_template = "library"
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
`create-forge config init` writes a commented starter file at that path
|
|
117
|
+
without overwriting one that already exists. `create-forge config show`
|
|
118
|
+
prints the resolved values and where each came from.
|
|
119
|
+
|
|
120
|
+
`github_org` pre-fills its prompt — you're still asked, just with the answer
|
|
121
|
+
already typed in. `author_name` and `author_email` aren't prompted for at all,
|
|
122
|
+
so a configured value is applied directly. `default_template` picks which
|
|
123
|
+
template `new` offers first, interactively or under `--yes`.
|
|
124
|
+
|
|
125
|
+
Every key can be overridden with an environment variable —
|
|
126
|
+
`FORGE_GITHUB_ORG` and so on — or a command line flag. Precedence is
|
|
127
|
+
config < environment < `--data` < an interactive answer.
|
|
128
|
+
|
|
129
|
+
## Templates
|
|
130
|
+
|
|
131
|
+
Run `create-forge list` for what your installed version offers. The registry is
|
|
132
|
+
bundled with each release, so new templates arrive when you update the tool.
|
|
133
|
+
|
|
134
|
+
To use your own template:
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
uvx create-forge new --template-url https://github.com/you/your-template
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
This describes the released v0.1.x architecture. Forge has accepted a future
|
|
141
|
+
[public-engine integration contract](docs/integration-contract.md) in which a
|
|
142
|
+
versioned `forge-template` package owns discovery and rendering. Its strict
|
|
143
|
+
[ProjectSpec protocol v1](https://github.com/Sandsy09/forge-template/blob/main/docs/project-spec.md)
|
|
144
|
+
and [component manifest protocol v1](https://github.com/Sandsy09/forge-template/blob/main/docs/component-manifests.md)
|
|
145
|
+
are now implemented behind the canonical
|
|
146
|
+
[stable template-engine API](https://github.com/Sandsy09/forge-template/blob/main/docs/template-engine-api.md)
|
|
147
|
+
([ADR 0029](https://github.com/Sandsy09/forge-template/blob/main/docs/adr/0029-stable-template-engine-api.md)).
|
|
148
|
+
The accepted
|
|
149
|
+
[Library archetype contract](https://github.com/Sandsy09/forge-template/blob/main/docs/library-archetype.md)
|
|
150
|
+
defines the first production component, implemented on `forge-template/main`
|
|
151
|
+
and released at `0.3.0`. The accepted
|
|
152
|
+
[CLI Application archetype contract](https://github.com/Sandsy09/forge-template/blob/main/docs/cli-application-archetype.md)
|
|
153
|
+
selects the optionless engine-owned `cli` archetype and derives its console
|
|
154
|
+
command from `ProjectSpec.project.repository_name`;
|
|
155
|
+
[FT-08.04](https://github.com/Sandsy09/forge-template/issues/4) implemented
|
|
156
|
+
it in the same `0.3.0` release, and
|
|
157
|
+
[CF-08.02](https://github.com/Sandsy09/create-forge/issues/10) exposes both
|
|
158
|
+
archetypes behind the hidden `new --engine-preview` flag's `--archetype`
|
|
159
|
+
option. Neither change alters this CLI's default `new` answers, registry, or
|
|
160
|
+
released dependency surface.
|
|
161
|
+
The engine now also defines in-memory
|
|
162
|
+
[generated-project validation](https://github.com/Sandsy09/forge-template/blob/main/docs/generated-project-validation.md)
|
|
163
|
+
([ADR 0030](https://github.com/Sandsy09/forge-template/blob/main/docs/adr/0030-generated-project-validation.md))
|
|
164
|
+
before rendered output is returned; `render_project` already calls it before
|
|
165
|
+
`--engine-preview` receives a result.
|
|
166
|
+
This repository now depends on a real, released `forge-template` range —
|
|
167
|
+
`>=0.3.1,<0.4`, published to PyPI as the optional `engine` extra
|
|
168
|
+
(`pip install 'create-forge[engine]'`; [#9](https://github.com/Sandsy09/create-forge/issues/9),
|
|
169
|
+
[ADR 0018](docs/adr/0018-pypi-distribution-and-the-first-engine-range.md)) —
|
|
170
|
+
rather than a development-only pin. That range is reachable only behind
|
|
171
|
+
`--engine-preview`; the current registry and `--template-url` behaviour
|
|
172
|
+
remain unchanged until the complete, tested cutover is released — at which
|
|
173
|
+
point `--engine-source`/`--engine-ref` (see the
|
|
174
|
+
[engine resolution contract](docs/engine-resolution.md)) take over this role,
|
|
175
|
+
not `--template-url`.
|
|
176
|
+
|
|
177
|
+
## Security
|
|
178
|
+
|
|
179
|
+
**create-forge executes code from the template it clones.** Copier templates can
|
|
180
|
+
declare post-generation tasks, and this tool runs them — that is how a generated
|
|
181
|
+
project arrives already git-initialised with hooks installed.
|
|
182
|
+
|
|
183
|
+
The template addresses are compiled into each release rather than fetched at
|
|
184
|
+
runtime or read from user configuration, so the only code trusted by default is
|
|
185
|
+
code published alongside the tool. `--template-url` bypasses that, and prompts
|
|
186
|
+
for confirmation before doing so. Point it only at repositories you trust.
|
|
187
|
+
|
|
188
|
+
Report vulnerabilities per [SECURITY.md](SECURITY.md) rather than in a public
|
|
189
|
+
issue.
|
|
190
|
+
|
|
191
|
+
## Using this at work
|
|
192
|
+
|
|
193
|
+
In v0.1.x, organisations needing custom executable templates can fork this
|
|
194
|
+
repository, point the bundled registry at their own templates, and maintain it
|
|
195
|
+
internally. The accepted target makes a downstream client of the
|
|
196
|
+
`forge-template` public engine the preferred route for organisation defaults
|
|
197
|
+
and constraints. Forks remain appropriate for genuinely custom executable
|
|
198
|
+
template content; see the [integration contract](docs/integration-contract.md).
|
|
199
|
+
|
|
200
|
+
## Contributing
|
|
201
|
+
|
|
202
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md). Issues and pull requests welcome.
|
|
203
|
+
Significant design decisions are recorded in [docs/adr/](docs/adr/).
|
|
204
|
+
|
|
205
|
+
## License
|
|
206
|
+
|
|
207
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
create_forge/__init__.py,sha256=2-hcPocqdDwT8xkrvZvaPhX8sCqs-lRWxW2KfRfTOjs,65
|
|
2
|
+
create_forge/cli.py,sha256=naFa_KDve7K0qF-bZ3RmiWbjw0uoWddoP8bJk0URE74,26882
|
|
3
|
+
create_forge/compat.py,sha256=CoeKBnouR4DeXSCGwL3hnf3BExnVLvc0OGtw0_xbnUY,2127
|
|
4
|
+
create_forge/config.py,sha256=D0C4hMBiUKN-AORhWkXM7U7ldwJcd4WWD06hdY4WV6w,4017
|
|
5
|
+
create_forge/engine.py,sha256=SX2bYqySTiiDLMzZSOyz6Qjh3EHDcXERTpNPZEi-bco,8953
|
|
6
|
+
create_forge/models.py,sha256=uBx5yimdIErNbX6mwn_0JSy-LS3b5_YBCfDjN22pPb4,4949
|
|
7
|
+
create_forge/pipeline.py,sha256=CfhsjvYicmUOHRIRrenionx6-vqDrEPP7XUeBAhntq0,5930
|
|
8
|
+
create_forge/prompts.py,sha256=PjPXNieMQssOFhshHXf78bG6S29lSF-wHLrAQrVK-Qo,6898
|
|
9
|
+
create_forge/registry.py,sha256=a3K0MWizORB0ghpjhBYlOCn_1kScglJYsJcKRQfWTlo,1144
|
|
10
|
+
create_forge/runner.py,sha256=KvvgxMVWcBOyXU4B0ZYKsMRJ_g5DbYjUhLds3vEz-kI,5572
|
|
11
|
+
create_forge/spec.py,sha256=lhAadGUMdp6mqdLh_ZeyWSqj8Hwla_OkYfRwPsPxP9Y,7620
|
|
12
|
+
create_forge/staging.py,sha256=7PDW_k8J6OY6O9Tpp2ujp2R8n7IJY1p1pgx1yYYAwj4,7014
|
|
13
|
+
create_forge/templates.toml,sha256=xjvgPGVGW9Q5td5GkbcI0Ake2oSetEA_S9r8BFwhdGY,3433
|
|
14
|
+
create_forge-0.2.0.dist-info/METADATA,sha256=6NTcfq__WoaJtxWZpXJ9hW63RiwY7cK4kGJD7nopcpY,8814
|
|
15
|
+
create_forge-0.2.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
|
|
16
|
+
create_forge-0.2.0.dist-info/entry_points.txt,sha256=KG4OcDHGWvh9JZKoeDJL3WcTlTN8Nj3q0Huq2WLrt7M,54
|
|
17
|
+
create_forge-0.2.0.dist-info/licenses/LICENSE,sha256=PuaS9iQNMxQolcG-Im2lGUD5wzytHvnOcmvhceUau-o,1067
|
|
18
|
+
create_forge-0.2.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Alex Sands
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|