simcon-toolkit 0.1.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.
- simcon_toolkit/__init__.py +40 -0
- simcon_toolkit/__main__.py +7 -0
- simcon_toolkit/_kit/LICENSE +202 -0
- simcon_toolkit/_kit/NOTICE +37 -0
- simcon_toolkit/_kit/assets/parts/clip_frame.stl +0 -0
- simcon_toolkit/_kit/assets/parts/simple_plate.stl +0 -0
- simcon_toolkit/_kit/packages/.ruff.toml +10 -0
- simcon_toolkit/_kit/packages/cadmould_cloud/__init__.py +8 -0
- simcon_toolkit/_kit/packages/cadmould_cloud/auth.py +681 -0
- simcon_toolkit/_kit/packages/cadmould_cloud/client.py +235 -0
- simcon_toolkit/_kit/packages/cadmould_geometry/__init__.py +5 -0
- simcon_toolkit/_kit/packages/cadmould_geometry/mesh.py +210 -0
- simcon_toolkit/_kit/packages/cadmould_geometry/stl.py +168 -0
- simcon_toolkit/_kit/packages/cadmould_results/__init__.py +30 -0
- simcon_toolkit/_kit/packages/cadmould_results/loader.py +288 -0
- simcon_toolkit/_kit/packages/cadmould_scoring/__init__.py +7 -0
- simcon_toolkit/_kit/packages/cadmould_scoring/metrics.py +519 -0
- simcon_toolkit/_kit/pyproject.toml +232 -0
- simcon_toolkit/_kit/templates/_shared/AGENTS.base.md +101 -0
- simcon_toolkit/_kit/templates/gate-study/.gitignore +18 -0
- simcon_toolkit/_kit/templates/gate-study/AGENTS.md +46 -0
- simcon_toolkit/_kit/templates/gate-study/GATING_STUDY_PLAYBOOK.md +219 -0
- simcon_toolkit/_kit/templates/gate-study/INITIAL_PROMPT.md +26 -0
- simcon_toolkit/_kit/templates/gate-study/README.md +137 -0
- simcon_toolkit/_kit/templates/gate-study/main.py +344 -0
- simcon_toolkit/_kit/templates/gate-study/pipeline.py +281 -0
- simcon_toolkit/_kit/templates/process-window/.gitignore +20 -0
- simcon_toolkit/_kit/templates/process-window/AGENTS.md +49 -0
- simcon_toolkit/_kit/templates/process-window/METHOD.md +155 -0
- simcon_toolkit/_kit/templates/process-window/README.md +176 -0
- simcon_toolkit/_kit/templates/process-window/configs/simple-plate.yaml +116 -0
- simcon_toolkit/_kit/templates/process-window/doe_spec.schema.md +249 -0
- simcon_toolkit/_kit/templates/process-window/main.py +82 -0
- simcon_toolkit/_kit/templates/process-window/process_window/__init__.py +5 -0
- simcon_toolkit/_kit/templates/process-window/process_window/centre.py +298 -0
- simcon_toolkit/_kit/templates/process-window/process_window/design.py +144 -0
- simcon_toolkit/_kit/templates/process-window/process_window/economics.py +367 -0
- simcon_toolkit/_kit/templates/process-window/process_window/emit.py +591 -0
- simcon_toolkit/_kit/templates/process-window/process_window/guardrails.py +153 -0
- simcon_toolkit/_kit/templates/process-window/process_window/harness.py +360 -0
- simcon_toolkit/_kit/templates/process-window/process_window/identity.py +92 -0
- simcon_toolkit/_kit/templates/process-window/process_window/inspect_part.py +184 -0
- simcon_toolkit/_kit/templates/process-window/process_window/kpis.py +355 -0
- simcon_toolkit/_kit/templates/process-window/process_window/material_card.py +163 -0
- simcon_toolkit/_kit/templates/process-window/process_window/probe_proxy.py +169 -0
- simcon_toolkit/_kit/templates/process-window/process_window/run_confirm.py +403 -0
- simcon_toolkit/_kit/templates/process-window/process_window/run_epsilon_floor.py +198 -0
- simcon_toolkit/_kit/templates/process-window/process_window/run_feedback.py +322 -0
- simcon_toolkit/_kit/templates/process-window/process_window/run_refine.py +279 -0
- simcon_toolkit/_kit/templates/process-window/process_window/run_screening.py +370 -0
- simcon_toolkit/_kit/templates/process-window/process_window/run_sweep.py +166 -0
- simcon_toolkit/_kit/templates/process-window/process_window/setup_campaign.py +312 -0
- simcon_toolkit/_kit/templates/process-window/process_window/surrogate.py +201 -0
- simcon_toolkit/_kit/templates/process-window/process_window/test_centre.py +169 -0
- simcon_toolkit/_kit/templates/process-window/process_window/test_design.py +113 -0
- simcon_toolkit/_kit/templates/process-window/process_window/test_guardrails.py +157 -0
- simcon_toolkit/_kit/templates/process-window/process_window/test_surrogate.py +127 -0
- simcon_toolkit/_kit/templates/process-window/process_window/units.py +152 -0
- simcon_toolkit/_kit/templates/quoting/.gitignore +24 -0
- simcon_toolkit/_kit/templates/quoting/AGENTS.md +58 -0
- simcon_toolkit/_kit/templates/quoting/INTERVIEW.md +147 -0
- simcon_toolkit/_kit/templates/quoting/METHOD.md +256 -0
- simcon_toolkit/_kit/templates/quoting/PROMPT.md +46 -0
- simcon_toolkit/_kit/templates/quoting/QUOTING_PLAYBOOK.md +245 -0
- simcon_toolkit/_kit/templates/quoting/README.md +158 -0
- simcon_toolkit/_kit/templates/quoting/main.py +484 -0
- simcon_toolkit/_kit/templates/quoting/parts/.gitkeep +0 -0
- simcon_toolkit/_kit/templates/quoting/quoting/__init__.py +11 -0
- simcon_toolkit/_kit/templates/quoting/quoting/costing.py +725 -0
- simcon_toolkit/_kit/templates/quoting/quoting/geometry.py +398 -0
- simcon_toolkit/_kit/templates/quoting/quoting/shop.py +193 -0
- simcon_toolkit/_kit/templates/quoting/quoting/state.py +260 -0
- simcon_toolkit/_kit/templates/quoting/quoting/study.py +577 -0
- simcon_toolkit/_kit/templates/quoting/quoting/toolkit.py +50 -0
- simcon_toolkit/_kit/templates/quoting/shop/README.md +43 -0
- simcon_toolkit/_kit/templates/quoting/shop/commercial.md +86 -0
- simcon_toolkit/_kit/templates/quoting/shop/lessons.md +94 -0
- simcon_toolkit/_kit/templates/quoting/shop/machines.md +68 -0
- simcon_toolkit/_kit/templates/quoting/shop/materials.md +92 -0
- simcon_toolkit/_kit/templates/quoting/shop/shop-profile.md +87 -0
- simcon_toolkit/_kit/templates/quoting/shop/tooling.md +145 -0
- simcon_toolkit/_kit/templates/run-one-simulation/.gitignore +16 -0
- simcon_toolkit/_kit/templates/run-one-simulation/AGENTS.md +41 -0
- simcon_toolkit/_kit/templates/run-one-simulation/README.md +133 -0
- simcon_toolkit/_kit/templates/run-one-simulation/main.py +216 -0
- simcon_toolkit/_kit/templates.toml +83 -0
- simcon_toolkit/choices.py +11 -0
- simcon_toolkit/cli.py +381 -0
- simcon_toolkit/generate.py +590 -0
- simcon_toolkit/instructions.py +152 -0
- simcon_toolkit/manifest.py +86 -0
- simcon_toolkit/project.py +356 -0
- simcon_toolkit/wizard.py +160 -0
- simcon_toolkit-0.1.0.dist-info/METADATA +48 -0
- simcon_toolkit-0.1.0.dist-info/RECORD +97 -0
- simcon_toolkit-0.1.0.dist-info/WHEEL +4 -0
- simcon_toolkit-0.1.0.dist-info/entry_points.txt +2 -0
simcon_toolkit/cli.py
ADDED
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
"""The command surface: one subcommand, one positional, five flags, one code path for all.
|
|
2
|
+
|
|
3
|
+
`init` is the only subcommand, and a bare `simcon-toolkit` prints the help and refuses
|
|
4
|
+
rather than guessing which one was meant. The subcommand is a namespace, not ceremony: it
|
|
5
|
+
leaves room beside `init` without ever making a project name ambiguous with a command.
|
|
6
|
+
|
|
7
|
+
There is no separate non-interactive mode. Every question has a flag, and only one answer is
|
|
8
|
+
REQUIRED: the template, because nothing else decides what the customer gets. The folder
|
|
9
|
+
name defaults to the template's name and the package manager to uv, both safe because a
|
|
10
|
+
clash stops the run before anything is written. So a command naming a template runs at once
|
|
11
|
+
on the defaults, whoever typed it. A command naming none opens the wizard for a person at a
|
|
12
|
+
terminal, which asks only what the flags left open, and is refused with the flag to pass
|
|
13
|
+
for anyone else. `--force` and `--list` answer no question.
|
|
14
|
+
|
|
15
|
+
Four conventions here are deliberate and each one looks like an oversight:
|
|
16
|
+
|
|
17
|
+
* `--package-manager` **carries no short form.** `-p` is `--python` in uv and `--project`
|
|
18
|
+
in pdm and hatch, and a Python scaffolder wants `--python` far more than it wants six
|
|
19
|
+
saved keystrokes on a flag typed once.
|
|
20
|
+
* `--yes` **is the documented name, with the Python-native spellings accepted quietly.**
|
|
21
|
+
cookiecutter teaches `--no-input` and copier teaches `--defaults`; neither should meet a
|
|
22
|
+
parse error, but the name a mould engineer and an assistant both reach for comes first.
|
|
23
|
+
* **Everything this module prints is ASCII.** (The wizard draws two glyphs, and only on a
|
|
24
|
+
terminal that can encode them.) A Windows console gets UTF-16, but REDIRECTED output
|
|
25
|
+
falls back to the machine's codepage, and an em dash is unencodable in cp437, cp850 and
|
|
26
|
+
cp932 — so one decorative character turns `simcon-toolkit init --list > list.txt` into a
|
|
27
|
+
traceback on a Japanese or Korean machine. This is the tool that must refuse nobody.
|
|
28
|
+
* **The catalogue is read late and its failure is caught.** `--help` and `--version` are
|
|
29
|
+
what someone reaches for when a command misbehaves, so they must survive a broken
|
|
30
|
+
installation rather than being the next thing that tracebacks.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
import argparse
|
|
36
|
+
import functools
|
|
37
|
+
import sys
|
|
38
|
+
from dataclasses import dataclass
|
|
39
|
+
from pathlib import Path
|
|
40
|
+
from typing import TYPE_CHECKING
|
|
41
|
+
|
|
42
|
+
if TYPE_CHECKING:
|
|
43
|
+
from simcon_toolkit.manifest import TemplateSpec
|
|
44
|
+
|
|
45
|
+
#: Handed to a caller who asked nothing. It is the simplest of the four — one script at its
|
|
46
|
+
#: root — which is the least surprising thing to give someone who supplied no opinion.
|
|
47
|
+
DEFAULT_TEMPLATE = "run-one-simulation"
|
|
48
|
+
|
|
49
|
+
# Imports nothing, so it is safe above the interpreter check.
|
|
50
|
+
from simcon_toolkit.choices import DEFAULT_PACKAGE_MANAGER, PACKAGE_MANAGERS # noqa: E402
|
|
51
|
+
|
|
52
|
+
#: Copying a template out of the installed package is `importlib.resources.as_file()` on a
|
|
53
|
+
#: directory, which is supported only from here.
|
|
54
|
+
MINIMUM_PYTHON = (3, 12)
|
|
55
|
+
|
|
56
|
+
#: The flags that live under `init`, so a command using one without it can be pointed there.
|
|
57
|
+
INIT_ONLY_FLAGS = frozenset(
|
|
58
|
+
{
|
|
59
|
+
"-t",
|
|
60
|
+
"--template",
|
|
61
|
+
"--package-manager",
|
|
62
|
+
"-y",
|
|
63
|
+
"--yes",
|
|
64
|
+
"--no-input",
|
|
65
|
+
"--defaults",
|
|
66
|
+
"-f",
|
|
67
|
+
"--force",
|
|
68
|
+
"-l",
|
|
69
|
+
"--list",
|
|
70
|
+
}
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
#: What argparse prints for a usage error, so one command never speaks in two voices.
|
|
74
|
+
PREFIX = "simcon-toolkit: error:"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class KitUnreadable(Exception):
|
|
78
|
+
"""The installed catalogue is absent, incomplete or corrupt — not the caller's doing."""
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclass(frozen=True)
|
|
82
|
+
class Choices:
|
|
83
|
+
"""What the run resolved to, once flags and defaults were applied."""
|
|
84
|
+
|
|
85
|
+
template: str
|
|
86
|
+
name: str
|
|
87
|
+
package_manager: str
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def check_interpreter(version: tuple[int, int] | None = None) -> str | None:
|
|
91
|
+
"""The refusal to print when this Python is too old, or None when it will do.
|
|
92
|
+
|
|
93
|
+
Both installers enforce the *floor* at resolution, so this is a backstop rather than
|
|
94
|
+
the first line of defence: it covers a run from a source checkout, an install forced
|
|
95
|
+
past the metadata, and a future release that drops the bound. It earns its lines
|
|
96
|
+
because what the installers produce instead is a resolver's unsatisfiable-requirements
|
|
97
|
+
dump, which names this package rather than the thing the reader has to change.
|
|
98
|
+
|
|
99
|
+
Nothing above it in this module may import the manifest: `tomllib` does not exist
|
|
100
|
+
below 3.11, so an import at module scope raises first and this sentence is never
|
|
101
|
+
reached on the versions that need it most.
|
|
102
|
+
|
|
103
|
+
The argument is resolved per call rather than defaulted, for the reason spelled out on
|
|
104
|
+
`is_interactive` — here it could not actually change, but one module should not teach
|
|
105
|
+
the rule and break it four functions apart.
|
|
106
|
+
"""
|
|
107
|
+
if version is None:
|
|
108
|
+
version = sys.version_info[:2]
|
|
109
|
+
if version >= MINIMUM_PYTHON:
|
|
110
|
+
return None
|
|
111
|
+
running = ".".join(str(part) for part in version)
|
|
112
|
+
wanted = ".".join(str(part) for part in MINIMUM_PYTHON)
|
|
113
|
+
return (
|
|
114
|
+
f"simcon-toolkit needs Python {wanted} or newer; this is Python {running}. "
|
|
115
|
+
f"Run it as `uvx --python {wanted} simcon-toolkit@latest init`, or install Python {wanted}."
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@functools.cache
|
|
120
|
+
def read_templates() -> dict[str, TemplateSpec]:
|
|
121
|
+
"""The catalogue, with every way it can be unreadable turned into one refusal.
|
|
122
|
+
|
|
123
|
+
A `_kit/` that EXISTS is not a `_kit/` that works, and only the first is checked when
|
|
124
|
+
the package resolves its root. `force-include` ships whatever is on disk at build time,
|
|
125
|
+
so a present-but-incomplete kit is a shape this project already worries about, and the
|
|
126
|
+
carefully worded message written for a missing kit misses it entirely.
|
|
127
|
+
|
|
128
|
+
Cached, because `--help` reads it for the epilog and the run reads it again; a failure
|
|
129
|
+
raises and is therefore never cached, so a broken kit is reported every time.
|
|
130
|
+
"""
|
|
131
|
+
where = "its installed location"
|
|
132
|
+
try:
|
|
133
|
+
from simcon_toolkit.manifest import MANIFEST, load_manifest
|
|
134
|
+
|
|
135
|
+
where = str(MANIFEST)
|
|
136
|
+
return load_manifest()
|
|
137
|
+
# Broad on purpose: a missing file, unreadable TOML and a manifest with no `templates`
|
|
138
|
+
# table are three exceptions with one honest cause and one useful thing to say.
|
|
139
|
+
except Exception as failure:
|
|
140
|
+
raise KitUnreadable(
|
|
141
|
+
f"simcon-toolkit cannot read its own template catalogue at {where} ({failure}). "
|
|
142
|
+
f"The installation is incomplete; reinstall with `uvx simcon-toolkit@latest init`."
|
|
143
|
+
) from failure
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _catalogue_epilog() -> str:
|
|
147
|
+
"""The template names for `--help`, or nothing. Never a reason for `--help` to fail."""
|
|
148
|
+
try:
|
|
149
|
+
return f"Templates: {', '.join(sorted(read_templates()))}."
|
|
150
|
+
except KitUnreadable:
|
|
151
|
+
return ""
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
155
|
+
"""Every command and flag the installer accepts, and nothing that was designed away."""
|
|
156
|
+
from simcon_toolkit import __version__
|
|
157
|
+
|
|
158
|
+
parser = argparse.ArgumentParser(
|
|
159
|
+
prog="simcon-toolkit",
|
|
160
|
+
description="Put a working SIMCON example project on your machine.",
|
|
161
|
+
)
|
|
162
|
+
parser.add_argument("--version", action="version", version=f"simcon-toolkit {__version__}")
|
|
163
|
+
commands = parser.add_subparsers(dest="command", metavar="<command>")
|
|
164
|
+
init = commands.add_parser(
|
|
165
|
+
"init",
|
|
166
|
+
help="create a project from a template",
|
|
167
|
+
description="Create a project from a template, ready to run.",
|
|
168
|
+
epilog=_catalogue_epilog(),
|
|
169
|
+
)
|
|
170
|
+
init.add_argument("name", nargs="?", help="folder to create (default: the template's own name)")
|
|
171
|
+
init.add_argument(
|
|
172
|
+
"-t",
|
|
173
|
+
"--template",
|
|
174
|
+
help=f"which workflow to start from; the one required answer (--yes picks {DEFAULT_TEMPLATE})",
|
|
175
|
+
)
|
|
176
|
+
init.add_argument(
|
|
177
|
+
"--package-manager",
|
|
178
|
+
choices=PACKAGE_MANAGERS,
|
|
179
|
+
help=f"which installer the project's instructions are written for (default: {DEFAULT_PACKAGE_MANAGER})",
|
|
180
|
+
)
|
|
181
|
+
# The aliases are real flags rather than hidden ones: someone who types what copier or
|
|
182
|
+
# cookiecutter taught them should find it in --help too, not discover it by luck.
|
|
183
|
+
init.add_argument(
|
|
184
|
+
"-y",
|
|
185
|
+
"--yes",
|
|
186
|
+
"--no-input",
|
|
187
|
+
"--defaults",
|
|
188
|
+
action="store_true",
|
|
189
|
+
help="ask nothing and use the documented defaults",
|
|
190
|
+
)
|
|
191
|
+
init.add_argument("-f", "--force", action="store_true", help="overwrite files the template would write")
|
|
192
|
+
init.add_argument("-l", "--list", action="store_true", help="list the templates and exit")
|
|
193
|
+
return parser
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def at_a_terminal() -> bool:
|
|
197
|
+
"""Whether a person can both see a question and answer it.
|
|
198
|
+
|
|
199
|
+
Both streams, not only stdin: `init > log.txt` at a terminal would otherwise wait on a
|
|
200
|
+
question written into the file, and prompt_toolkit raises on a redirected Windows stdout.
|
|
201
|
+
"""
|
|
202
|
+
# A closed descriptor leaves the stream as None rather than raising, so `init <&-` would
|
|
203
|
+
# otherwise die on an AttributeError before it had done anything.
|
|
204
|
+
return bool(sys.stdin and sys.stdout and sys.stdin.isatty() and sys.stdout.isatty())
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def is_interactive(args: argparse.Namespace, tty: bool | None = None) -> bool:
|
|
208
|
+
"""Whether to open the wizard: someone is there, and the one required answer is missing.
|
|
209
|
+
|
|
210
|
+
Checked before any prompt library is reached, never left to one: every library measured
|
|
211
|
+
blocks forever on an open stdin that nobody writes to, which is what an assistant's
|
|
212
|
+
captured shell usually hands a child process.
|
|
213
|
+
|
|
214
|
+
`tty` is resolved on each call, never as a default argument: a default is evaluated
|
|
215
|
+
once when this module is imported, which for a long-lived import binds whatever stdin
|
|
216
|
+
looked like then and answers the same way forever after.
|
|
217
|
+
"""
|
|
218
|
+
if tty is None:
|
|
219
|
+
tty = at_a_terminal()
|
|
220
|
+
return tty and not args.yes and args.template is None
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def resolve(args: argparse.Namespace) -> Choices:
|
|
224
|
+
"""Flags first, then the documented defaults — and nothing read off this machine.
|
|
225
|
+
|
|
226
|
+
`create-next-app`'s `--yes` consults a stored preference file, which is why it needs a
|
|
227
|
+
flag to reset one and why the same command produces different projects on two laptops.
|
|
228
|
+
Ours resolves to the documented defaults and nothing else.
|
|
229
|
+
"""
|
|
230
|
+
template = args.template or DEFAULT_TEMPLATE
|
|
231
|
+
return Choices(
|
|
232
|
+
template=template,
|
|
233
|
+
name=args.name or template,
|
|
234
|
+
package_manager=args.package_manager or DEFAULT_PACKAGE_MANAGER,
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _describe(spec: TemplateSpec) -> str:
|
|
239
|
+
"""One catalogue line, ASCII only — see the note on encodings at the top of this file."""
|
|
240
|
+
credentials = " | needs " + ", ".join(spec.needs) if spec.needs else ""
|
|
241
|
+
return f" {spec.name:<20} {spec.title}: {spec.description}{credentials}"
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def main(argv: list[str] | None = None) -> int:
|
|
245
|
+
"""Parse, validate, and write the project the caller asked for."""
|
|
246
|
+
refusal = check_interpreter()
|
|
247
|
+
if refusal is not None:
|
|
248
|
+
print(refusal, file=sys.stderr)
|
|
249
|
+
return 2
|
|
250
|
+
|
|
251
|
+
# Everything this module writes is ASCII, but a folder name or a kept file is the
|
|
252
|
+
# customer's, and stdout encodes strictly: a redirected run on a Japanese codepage would
|
|
253
|
+
# write the whole project and then die printing its name.
|
|
254
|
+
for stream in (sys.stdout, sys.stderr):
|
|
255
|
+
reconfigure = getattr(stream, "reconfigure", None)
|
|
256
|
+
if reconfigure is not None:
|
|
257
|
+
reconfigure(errors="backslashreplace")
|
|
258
|
+
|
|
259
|
+
if argv is None:
|
|
260
|
+
argv = sys.argv[1:]
|
|
261
|
+
if argv and argv[0] in INIT_ONLY_FLAGS:
|
|
262
|
+
# The flags moved under `init`; a command copied from an older page should be told
|
|
263
|
+
# where they went rather than meet argparse's bare "unrecognized arguments".
|
|
264
|
+
print(f"{PREFIX} {argv[0]} belongs to the init command: simcon-toolkit init {' '.join(argv)}", file=sys.stderr)
|
|
265
|
+
return 2
|
|
266
|
+
|
|
267
|
+
parser = build_parser()
|
|
268
|
+
args = parser.parse_args(argv)
|
|
269
|
+
if args.command is None:
|
|
270
|
+
# A refusal, so the help goes where refusals go; see `test_every_refusal_goes_to_stderr`.
|
|
271
|
+
parser.print_help(sys.stderr)
|
|
272
|
+
return 2
|
|
273
|
+
|
|
274
|
+
try:
|
|
275
|
+
templates = read_templates()
|
|
276
|
+
except KitUnreadable as unreadable:
|
|
277
|
+
print(f"{PREFIX} {unreadable}", file=sys.stderr)
|
|
278
|
+
return 2
|
|
279
|
+
|
|
280
|
+
if args.list:
|
|
281
|
+
print("Templates:")
|
|
282
|
+
for spec in sorted(templates.values(), key=lambda entry: entry.name):
|
|
283
|
+
print(_describe(spec))
|
|
284
|
+
return 0
|
|
285
|
+
|
|
286
|
+
# An empty name is a mistyped one, not an absent one. Without this, `-y ""` resolves to
|
|
287
|
+
# the template's own name and hands back a folder nobody asked for, reporting success.
|
|
288
|
+
if args.name is not None and not args.name.strip():
|
|
289
|
+
print(f"{PREFIX} the project name is empty. Give a folder name, or leave it out entirely.", file=sys.stderr)
|
|
290
|
+
return 2
|
|
291
|
+
|
|
292
|
+
tty = at_a_terminal()
|
|
293
|
+
if args.template is not None and args.template not in templates:
|
|
294
|
+
known = ", ".join(sorted(templates))
|
|
295
|
+
# Named first, because the one thing the reader knows is what they typed. The full
|
|
296
|
+
# list follows rather than a suggestion: four names fit on a line, and a guess that
|
|
297
|
+
# lands on the wrong one costs more than reading them.
|
|
298
|
+
print(f"{PREFIX} no template named {args.template!r}. Available: {known}", file=sys.stderr)
|
|
299
|
+
if not tty or args.yes:
|
|
300
|
+
return 2
|
|
301
|
+
# Someone is there to choose, so the menu is kinder than a second attempt at typing.
|
|
302
|
+
args.template = None
|
|
303
|
+
|
|
304
|
+
if is_interactive(args, tty):
|
|
305
|
+
from simcon_toolkit.wizard import Cancelled, ask, name_problem
|
|
306
|
+
|
|
307
|
+
if args.name is not None:
|
|
308
|
+
# Before the questions, not after them, and by the prompt's own rule: a name that
|
|
309
|
+
# can never work should not cost the reader two answers first.
|
|
310
|
+
problem = name_problem(args.name, force=args.force)
|
|
311
|
+
if problem is not None:
|
|
312
|
+
print(f"{PREFIX} {problem}", file=sys.stderr)
|
|
313
|
+
return 2
|
|
314
|
+
|
|
315
|
+
try:
|
|
316
|
+
template, name, package_manager = ask(
|
|
317
|
+
templates,
|
|
318
|
+
default_template=DEFAULT_TEMPLATE,
|
|
319
|
+
name=args.name,
|
|
320
|
+
package_manager=args.package_manager,
|
|
321
|
+
force=args.force,
|
|
322
|
+
)
|
|
323
|
+
except Cancelled:
|
|
324
|
+
print("Cancelled; nothing was written.", file=sys.stderr)
|
|
325
|
+
return 130
|
|
326
|
+
chosen = Choices(template=template, name=name, package_manager=package_manager)
|
|
327
|
+
elif args.template is None and not args.yes:
|
|
328
|
+
# The refusal is what an assistant reads to correct itself, so it names every way out.
|
|
329
|
+
print(f"{PREFIX} no template chosen, and there is no terminal to ask at.", file=sys.stderr)
|
|
330
|
+
print(
|
|
331
|
+
f"Pass --template <name> (--list shows them), or --yes for {DEFAULT_TEMPLATE} with every default.",
|
|
332
|
+
file=sys.stderr,
|
|
333
|
+
)
|
|
334
|
+
return 2
|
|
335
|
+
else:
|
|
336
|
+
chosen = resolve(args)
|
|
337
|
+
|
|
338
|
+
from simcon_toolkit import KIT_ROOT, __version__
|
|
339
|
+
from simcon_toolkit.generate import GeneratorError, WriteFailed, format_listing, generate_project
|
|
340
|
+
|
|
341
|
+
destination = Path(chosen.name)
|
|
342
|
+
try:
|
|
343
|
+
result = generate_project(
|
|
344
|
+
chosen.template,
|
|
345
|
+
destination,
|
|
346
|
+
force=args.force,
|
|
347
|
+
cli_version=__version__,
|
|
348
|
+
package_manager=chosen.package_manager,
|
|
349
|
+
)
|
|
350
|
+
except GeneratorError as refusal:
|
|
351
|
+
print(f"{PREFIX} {refusal}", file=sys.stderr)
|
|
352
|
+
# A write that failed part-way is not a mistake in the arguments.
|
|
353
|
+
return 1 if isinstance(refusal, WriteFailed) else 2
|
|
354
|
+
except OSError as failure:
|
|
355
|
+
# A file missing from our own kit is a broken installation, and saying "could not
|
|
356
|
+
# write" would send the reader to check their own disk for a fault that is ours.
|
|
357
|
+
if failure.filename and Path(failure.filename).resolve().is_relative_to(KIT_ROOT.resolve()):
|
|
358
|
+
print(f"{PREFIX} the installation is incomplete: {failure.filename} is missing.", file=sys.stderr)
|
|
359
|
+
print("Reinstall with `uvx simcon-toolkit@latest init`.", file=sys.stderr)
|
|
360
|
+
return 2
|
|
361
|
+
# A disk that is full or a folder we may not write to: not the caller's mistake in
|
|
362
|
+
# the arguments, so exit 1 rather than the usage error's 2.
|
|
363
|
+
print(f"{PREFIX} could not write {destination}: {failure.strerror or failure}", file=sys.stderr)
|
|
364
|
+
return 1
|
|
365
|
+
|
|
366
|
+
# Every resolved value, defaulted or not, because the transcript is all an assistant keeps.
|
|
367
|
+
print(
|
|
368
|
+
f"Created {chosen.template} in {destination}, package manager: {chosen.package_manager} "
|
|
369
|
+
f"({len(result.written)} files written)."
|
|
370
|
+
)
|
|
371
|
+
if result.overwritten:
|
|
372
|
+
print(f"Overwrote {len(result.overwritten)} existing file(s): {format_listing(result.overwritten)}")
|
|
373
|
+
if result.kept:
|
|
374
|
+
# Never removed, because only the reader knows whether they are their own work or
|
|
375
|
+
# something left from a different template.
|
|
376
|
+
print(f"Left {len(result.kept)} file(s) that are not part of this template: {format_listing(result.kept)}")
|
|
377
|
+
return 0
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
if __name__ == "__main__":
|
|
381
|
+
raise SystemExit(main())
|