zenith-template 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.
- zenith/__init__.py +6 -0
- zenith/cli.py +439 -0
- zenith/generator.py +109 -0
- zenith/models.py +178 -0
- zenith/template/cookiecutter.json +71 -0
- zenith/template/hooks/__init__.py +1 -0
- zenith/template/hooks/post_gen_project.py +470 -0
- zenith/template/hooks/pre_gen_project.py +117 -0
- zenith/template/{{cookiecutter.project_slug}}/.aider.conf.yml +16 -0
- zenith/template/{{cookiecutter.project_slug}}/.claude/settings.json +37 -0
- zenith/template/{{cookiecutter.project_slug}}/.codex/config.toml +11 -0
- zenith/template/{{cookiecutter.project_slug}}/.coveragerc +50 -0
- zenith/template/{{cookiecutter.project_slug}}/.cursor/rules/zenith.mdc +9 -0
- zenith/template/{{cookiecutter.project_slug}}/.devin/config.json +16 -0
- zenith/template/{{cookiecutter.project_slug}}/.editorconfig +31 -0
- zenith/template/{{cookiecutter.project_slug}}/.gemini/settings.json +3 -0
- zenith/template/{{cookiecutter.project_slug}}/.gitattributes +25 -0
- zenith/template/{{cookiecutter.project_slug}}/.github/ISSUE_TEMPLATE/bug_report.yml +56 -0
- zenith/template/{{cookiecutter.project_slug}}/.github/ISSUE_TEMPLATE/config.yml +5 -0
- zenith/template/{{cookiecutter.project_slug}}/.github/ISSUE_TEMPLATE/feature_request.yml +39 -0
- zenith/template/{{cookiecutter.project_slug}}/.github/PULL_REQUEST_TEMPLATE.md +38 -0
- zenith/template/{{cookiecutter.project_slug}}/.github/copilot-instructions.md +4 -0
- zenith/template/{{cookiecutter.project_slug}}/.github/dependabot.yml +31 -0
- zenith/template/{{cookiecutter.project_slug}}/.github/workflows/dependabot-automerge.yaml +32 -0
- zenith/template/{{cookiecutter.project_slug}}/.github/workflows/docs.yaml +56 -0
- zenith/template/{{cookiecutter.project_slug}}/.github/workflows/guard-dependencies.yaml +61 -0
- zenith/template/{{cookiecutter.project_slug}}/.github/workflows/publish.yaml +101 -0
- zenith/template/{{cookiecutter.project_slug}}/.github/workflows/python_tests.yaml +199 -0
- zenith/template/{{cookiecutter.project_slug}}/.github/workflows/test_redistribute.yaml +53 -0
- zenith/template/{{cookiecutter.project_slug}}/.gitignore +87 -0
- zenith/template/{{cookiecutter.project_slug}}/.pi/settings.json +8 -0
- zenith/template/{{cookiecutter.project_slug}}/.prettierrc.yaml +16 -0
- zenith/template/{{cookiecutter.project_slug}}/.python-version +1 -0
- zenith/template/{{cookiecutter.project_slug}}/.taplo.toml +26 -0
- zenith/template/{{cookiecutter.project_slug}}/.vscode/extensions.json +8 -0
- zenith/template/{{cookiecutter.project_slug}}/.zed/settings.json +18 -0
- zenith/template/{{cookiecutter.project_slug}}/.zed/tasks.json +14 -0
- zenith/template/{{cookiecutter.project_slug}}/.zizmor.yml +12 -0
- zenith/template/{{cookiecutter.project_slug}}/AGENTS.md +180 -0
- zenith/template/{{cookiecutter.project_slug}}/AI_POLICY.md +75 -0
- zenith/template/{{cookiecutter.project_slug}}/CODE_OF_CONDUCT.md +125 -0
- zenith/template/{{cookiecutter.project_slug}}/CONTRIBUTING.md +87 -0
- zenith/template/{{cookiecutter.project_slug}}/LICENSE +7 -0
- zenith/template/{{cookiecutter.project_slug}}/README.md +224 -0
- zenith/template/{{cookiecutter.project_slug}}/SECURITY.md +66 -0
- zenith/template/{{cookiecutter.project_slug}}/complexipy.toml +20 -0
- zenith/template/{{cookiecutter.project_slug}}/cosmic-ray.toml +17 -0
- zenith/template/{{cookiecutter.project_slug}}/docs/adr/0001-record-architecture-decisions.md +88 -0
- zenith/template/{{cookiecutter.project_slug}}/docs/adr/index.md +42 -0
- zenith/template/{{cookiecutter.project_slug}}/docs/adr/template.md +66 -0
- zenith/template/{{cookiecutter.project_slug}}/docs/assets/python-logo.svg +1 -0
- zenith/template/{{cookiecutter.project_slug}}/docs/assets/python-logo.typ +11 -0
- zenith/template/{{cookiecutter.project_slug}}/docs/changelog.md +20 -0
- zenith/template/{{cookiecutter.project_slug}}/docs/getting-started.md +168 -0
- zenith/template/{{cookiecutter.project_slug}}/docs/index.md +157 -0
- zenith/template/{{cookiecutter.project_slug}}/docs/javascripts/mathjax.js +19 -0
- zenith/template/{{cookiecutter.project_slug}}/docs/reference.md +37 -0
- zenith/template/{{cookiecutter.project_slug}}/docs/stylesheets/custom.css +145 -0
- zenith/template/{{cookiecutter.project_slug}}/docs/stylesheets/palette.css +64 -0
- zenith/template/{{cookiecutter.project_slug}}/opencode.json +24 -0
- zenith/template/{{cookiecutter.project_slug}}/poe_tasks.toml +435 -0
- zenith/template/{{cookiecutter.project_slug}}/prek.toml +135 -0
- zenith/template/{{cookiecutter.project_slug}}/pyproject.toml +132 -0
- zenith/template/{{cookiecutter.project_slug}}/pyrefly.toml +16 -0
- zenith/template/{{cookiecutter.project_slug}}/pytest.ini +35 -0
- zenith/template/{{cookiecutter.project_slug}}/ruff.toml +65 -0
- zenith/template/{{cookiecutter.project_slug}}/rumdl.toml +170 -0
- zenith/template/{{cookiecutter.project_slug}}/scripts/check_skills.py +362 -0
- zenith/template/{{cookiecutter.project_slug}}/scripts/verify_wheel.py +644 -0
- zenith/template/{{cookiecutter.project_slug}}/src/{{cookiecutter.package_name}}/.agents/skills/{{cookiecutter.project_slug}}/SKILL.md +27 -0
- zenith/template/{{cookiecutter.project_slug}}/src/{{cookiecutter.package_name}}/__init__.py +24 -0
- zenith/template/{{cookiecutter.project_slug}}/src/{{cookiecutter.package_name}}/__main__.py +10 -0
- zenith/template/{{cookiecutter.project_slug}}/src/{{cookiecutter.package_name}}/_lazy.py +91 -0
- zenith/template/{{cookiecutter.project_slug}}/src/{{cookiecutter.package_name}}/cli.py +72 -0
- zenith/template/{{cookiecutter.project_slug}}/src/{{cookiecutter.package_name}}/core.py +28 -0
- zenith/template/{{cookiecutter.project_slug}}/src/{{cookiecutter.package_name}}/py.typed +1 -0
- zenith/template/{{cookiecutter.project_slug}}/tach.toml +53 -0
- zenith/template/{{cookiecutter.project_slug}}/tests/__init__.py +3 -0
- zenith/template/{{cookiecutter.project_slug}}/tests/__snapshots__/test_core.ambr +4 -0
- zenith/template/{{cookiecutter.project_slug}}/tests/conftest.py +34 -0
- zenith/template/{{cookiecutter.project_slug}}/tests/test_cli.py +99 -0
- zenith/template/{{cookiecutter.project_slug}}/tests/test_core.py +40 -0
- zenith/template/{{cookiecutter.project_slug}}/typos.toml +22 -0
- zenith/template/{{cookiecutter.project_slug}}/zensical.toml +188 -0
- zenith/template.py +64 -0
- zenith/ui/__init__.py +3 -0
- zenith/ui/app.py +629 -0
- zenith_template-0.2.0.dist-info/METADATA +140 -0
- zenith_template-0.2.0.dist-info/RECORD +92 -0
- zenith_template-0.2.0.dist-info/WHEEL +4 -0
- zenith_template-0.2.0.dist-info/entry_points.txt +3 -0
- zenith_template-0.2.0.dist-info/licenses/LICENSE +21 -0
zenith/__init__.py
ADDED
zenith/cli.py
ADDED
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
"""Command-line interface entry points for Zenith."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import shlex
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import TYPE_CHECKING
|
|
10
|
+
|
|
11
|
+
from rich import box
|
|
12
|
+
from rich.console import Console
|
|
13
|
+
from rich.panel import Panel
|
|
14
|
+
|
|
15
|
+
from zenith import __version__
|
|
16
|
+
from zenith.generator import generate_project
|
|
17
|
+
from zenith.models import (
|
|
18
|
+
AI_TOOL_PRESETS,
|
|
19
|
+
PRESET_RECOMMENDED,
|
|
20
|
+
SUPPORTED_LICENSES,
|
|
21
|
+
SUPPORTED_PYTHON_VERSIONS,
|
|
22
|
+
ZenithConfig,
|
|
23
|
+
slugify,
|
|
24
|
+
)
|
|
25
|
+
from zenith.ui.app import ZenithApp
|
|
26
|
+
|
|
27
|
+
if TYPE_CHECKING:
|
|
28
|
+
from collections.abc import Sequence
|
|
29
|
+
|
|
30
|
+
console = Console()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
34
|
+
"""Build the command-line argument parser for Zenith.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
Configured ArgumentParser instance.
|
|
38
|
+
|
|
39
|
+
"""
|
|
40
|
+
parser = argparse.ArgumentParser(
|
|
41
|
+
prog="zenith-template",
|
|
42
|
+
description="Scaffold modern Python 3.12+ projects with Zenith.",
|
|
43
|
+
)
|
|
44
|
+
parser.add_argument(
|
|
45
|
+
"output_dir",
|
|
46
|
+
nargs="?",
|
|
47
|
+
default=".",
|
|
48
|
+
help="Directory where the new project folder will be created (default: .)",
|
|
49
|
+
)
|
|
50
|
+
parser.add_argument(
|
|
51
|
+
"--output-dir",
|
|
52
|
+
dest="output_dir_opt",
|
|
53
|
+
default=None,
|
|
54
|
+
help="Directory where the new project folder will be created.",
|
|
55
|
+
)
|
|
56
|
+
parser.add_argument(
|
|
57
|
+
"-v",
|
|
58
|
+
"--version",
|
|
59
|
+
action="version",
|
|
60
|
+
version=f"%(prog)s {__version__}",
|
|
61
|
+
help="Show program version number and exit.",
|
|
62
|
+
)
|
|
63
|
+
parser.add_argument(
|
|
64
|
+
"--no-input",
|
|
65
|
+
action="store_true",
|
|
66
|
+
help="Do not prompt for parameters; use configuration defaults or flags.",
|
|
67
|
+
)
|
|
68
|
+
parser.add_argument(
|
|
69
|
+
"--overwrite",
|
|
70
|
+
action="store_true",
|
|
71
|
+
help="Overwrite destination directory contents if it already exists.",
|
|
72
|
+
)
|
|
73
|
+
parser.add_argument(
|
|
74
|
+
"--name",
|
|
75
|
+
dest="project_name",
|
|
76
|
+
default=None,
|
|
77
|
+
help="Project Name (human-readable title).",
|
|
78
|
+
)
|
|
79
|
+
parser.add_argument(
|
|
80
|
+
"--slug",
|
|
81
|
+
dest="project_slug",
|
|
82
|
+
default=None,
|
|
83
|
+
help="Project Slug (directory and package name).",
|
|
84
|
+
)
|
|
85
|
+
parser.add_argument(
|
|
86
|
+
"--package",
|
|
87
|
+
dest="package_name",
|
|
88
|
+
default=None,
|
|
89
|
+
help="Python Package Name (import identifier).",
|
|
90
|
+
)
|
|
91
|
+
parser.add_argument(
|
|
92
|
+
"--description",
|
|
93
|
+
dest="description",
|
|
94
|
+
default=None,
|
|
95
|
+
help="Short project description.",
|
|
96
|
+
)
|
|
97
|
+
parser.add_argument(
|
|
98
|
+
"--author",
|
|
99
|
+
dest="author_name",
|
|
100
|
+
default=None,
|
|
101
|
+
help="Author full name.",
|
|
102
|
+
)
|
|
103
|
+
parser.add_argument(
|
|
104
|
+
"--email",
|
|
105
|
+
dest="author_email",
|
|
106
|
+
default=None,
|
|
107
|
+
help="Author email address.",
|
|
108
|
+
)
|
|
109
|
+
parser.add_argument(
|
|
110
|
+
"--github",
|
|
111
|
+
dest="github_username",
|
|
112
|
+
default=None,
|
|
113
|
+
help="GitHub username or organization.",
|
|
114
|
+
)
|
|
115
|
+
parser.add_argument(
|
|
116
|
+
"--python-version",
|
|
117
|
+
choices=SUPPORTED_PYTHON_VERSIONS,
|
|
118
|
+
default="3.12",
|
|
119
|
+
help="Target Python version (default: 3.12).",
|
|
120
|
+
)
|
|
121
|
+
parser.add_argument(
|
|
122
|
+
"--license",
|
|
123
|
+
choices=SUPPORTED_LICENSES,
|
|
124
|
+
default="MIT",
|
|
125
|
+
help="Open source license identifier (default: MIT).",
|
|
126
|
+
)
|
|
127
|
+
parser.add_argument(
|
|
128
|
+
"--ai-tools",
|
|
129
|
+
dest="ai_tools",
|
|
130
|
+
default=None,
|
|
131
|
+
help=(
|
|
132
|
+
"AI tool preset name (all, recommended, ide-only, cli-only, none)"
|
|
133
|
+
" or comma-separated tool names."
|
|
134
|
+
),
|
|
135
|
+
)
|
|
136
|
+
parser.add_argument(
|
|
137
|
+
"--include-cli",
|
|
138
|
+
"--cli",
|
|
139
|
+
dest="include_cli",
|
|
140
|
+
action=argparse.BooleanOptionalAction,
|
|
141
|
+
default=True,
|
|
142
|
+
help="Generate Typer command-line interface.",
|
|
143
|
+
)
|
|
144
|
+
parser.add_argument(
|
|
145
|
+
"--include-docs",
|
|
146
|
+
"--docs",
|
|
147
|
+
dest="include_docs",
|
|
148
|
+
action=argparse.BooleanOptionalAction,
|
|
149
|
+
default=True,
|
|
150
|
+
help="Include documentation website (docs/ & zensical.toml).",
|
|
151
|
+
)
|
|
152
|
+
parser.add_argument(
|
|
153
|
+
"--include-mutation",
|
|
154
|
+
"--mutation",
|
|
155
|
+
dest="include_mutation_testing",
|
|
156
|
+
action=argparse.BooleanOptionalAction,
|
|
157
|
+
default=True,
|
|
158
|
+
help="Include mutation testing tooling (cosmic-ray & mutmut).",
|
|
159
|
+
)
|
|
160
|
+
parser.add_argument(
|
|
161
|
+
"--include-ci",
|
|
162
|
+
"--ci",
|
|
163
|
+
dest="include_ci",
|
|
164
|
+
action=argparse.BooleanOptionalAction,
|
|
165
|
+
default=True,
|
|
166
|
+
help="Generate GitHub Actions workflows for testing and publishing.",
|
|
167
|
+
)
|
|
168
|
+
parser.add_argument(
|
|
169
|
+
"--include-skills",
|
|
170
|
+
"--skills",
|
|
171
|
+
dest="include_skills",
|
|
172
|
+
action=argparse.BooleanOptionalAction,
|
|
173
|
+
default=True,
|
|
174
|
+
help="Include agent skills suite (.agents/).",
|
|
175
|
+
)
|
|
176
|
+
parser.add_argument(
|
|
177
|
+
"--init-git",
|
|
178
|
+
"--git",
|
|
179
|
+
dest="init_git",
|
|
180
|
+
action=argparse.BooleanOptionalAction,
|
|
181
|
+
default=True,
|
|
182
|
+
help="Initialize Git repository in scaffolded project.",
|
|
183
|
+
)
|
|
184
|
+
parser.add_argument(
|
|
185
|
+
"--run-uv-sync",
|
|
186
|
+
"--sync",
|
|
187
|
+
dest="run_uv_sync",
|
|
188
|
+
action=argparse.BooleanOptionalAction,
|
|
189
|
+
default=False,
|
|
190
|
+
help="Run uv sync after scaffolding.",
|
|
191
|
+
)
|
|
192
|
+
parser.add_argument(
|
|
193
|
+
"--install-git-hooks",
|
|
194
|
+
"--hooks",
|
|
195
|
+
dest="install_git_hooks",
|
|
196
|
+
action=argparse.BooleanOptionalAction,
|
|
197
|
+
default=False,
|
|
198
|
+
help="Install Git hooks (prek install) after scaffolding.",
|
|
199
|
+
)
|
|
200
|
+
return parser
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _resolve_ai_tools(raw: str | None) -> tuple[str, ...]:
|
|
204
|
+
"""Parse raw string or preset into a tuple of AI tool identifiers.
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
raw: Raw preset name or comma-separated tool keys.
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
Tuple of active tool identifiers.
|
|
211
|
+
|
|
212
|
+
"""
|
|
213
|
+
if raw is None:
|
|
214
|
+
return PRESET_RECOMMENDED
|
|
215
|
+
|
|
216
|
+
normalized = raw.strip().lower()
|
|
217
|
+
if normalized in AI_TOOL_PRESETS:
|
|
218
|
+
return AI_TOOL_PRESETS[normalized]
|
|
219
|
+
|
|
220
|
+
items = [t.strip() for t in normalized.replace(";", ",").split(",") if t.strip()]
|
|
221
|
+
return tuple(items)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def config_from_args(args: argparse.Namespace) -> ZenithConfig:
|
|
225
|
+
"""Build a ZenithConfig dataclass instance from parsed command-line arguments.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
args: Parsed command-line namespace.
|
|
229
|
+
|
|
230
|
+
Returns:
|
|
231
|
+
Populated ZenithConfig instance.
|
|
232
|
+
|
|
233
|
+
"""
|
|
234
|
+
defaults = ZenithConfig()
|
|
235
|
+
raw_dir = getattr(args, "output_dir_opt", None) or getattr(
|
|
236
|
+
args,
|
|
237
|
+
"output_dir",
|
|
238
|
+
None,
|
|
239
|
+
)
|
|
240
|
+
output_dir = raw_dir if raw_dir and raw_dir != "." else defaults.output_dir
|
|
241
|
+
name = args.project_name or defaults.project_name
|
|
242
|
+
fallback_slug = slugify(name) if args.project_name else defaults.project_slug
|
|
243
|
+
slug = args.project_slug or fallback_slug
|
|
244
|
+
fallback_pkg = (
|
|
245
|
+
slugify(name, separator="_") if args.project_name else defaults.package_name
|
|
246
|
+
)
|
|
247
|
+
package = args.package_name or fallback_pkg
|
|
248
|
+
|
|
249
|
+
return ZenithConfig(
|
|
250
|
+
output_dir=output_dir,
|
|
251
|
+
project_name=name,
|
|
252
|
+
project_slug=slug,
|
|
253
|
+
package_name=package,
|
|
254
|
+
description=args.description or defaults.description,
|
|
255
|
+
author_name=args.author_name or defaults.author_name,
|
|
256
|
+
author_email=args.author_email or defaults.author_email,
|
|
257
|
+
github_username=args.github_username or defaults.github_username,
|
|
258
|
+
python_version=args.python_version,
|
|
259
|
+
license=args.license,
|
|
260
|
+
include_cli=args.include_cli,
|
|
261
|
+
include_ci_testing=args.include_ci,
|
|
262
|
+
include_ci_publishing=args.include_ci,
|
|
263
|
+
include_agent_skills=args.include_skills,
|
|
264
|
+
include_docs=getattr(args, "include_docs", defaults.include_docs),
|
|
265
|
+
include_mutation_testing=getattr(
|
|
266
|
+
args,
|
|
267
|
+
"include_mutation_testing",
|
|
268
|
+
defaults.include_mutation_testing,
|
|
269
|
+
),
|
|
270
|
+
init_git=getattr(args, "init_git", defaults.init_git),
|
|
271
|
+
run_uv_sync=getattr(args, "run_uv_sync", defaults.run_uv_sync),
|
|
272
|
+
install_git_hooks=getattr(
|
|
273
|
+
args,
|
|
274
|
+
"install_git_hooks",
|
|
275
|
+
defaults.install_git_hooks,
|
|
276
|
+
),
|
|
277
|
+
ai_tools=_resolve_ai_tools(args.ai_tools),
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def _build_feature_flags(config: ZenithConfig) -> list[str]:
|
|
282
|
+
"""Collect non-default command-line flags for project features and lifecycle.
|
|
283
|
+
|
|
284
|
+
Args:
|
|
285
|
+
config: Configuration holding feature toggles.
|
|
286
|
+
|
|
287
|
+
Returns:
|
|
288
|
+
List of CLI flag strings.
|
|
289
|
+
|
|
290
|
+
"""
|
|
291
|
+
has_custom_dir = Path(config.output_dir).resolve() != Path.cwd().resolve()
|
|
292
|
+
flag_conditions = (
|
|
293
|
+
(has_custom_dir, f"--output-dir {shlex.quote(config.output_dir)}"),
|
|
294
|
+
(not config.include_cli, "--no-cli"),
|
|
295
|
+
(not config.include_docs, "--no-docs"),
|
|
296
|
+
(not config.include_mutation_testing, "--no-mutation"),
|
|
297
|
+
(not (config.include_ci_testing or config.include_ci_publishing), "--no-ci"),
|
|
298
|
+
(not config.include_agent_skills, "--no-skills"),
|
|
299
|
+
(not config.init_git, "--no-git"),
|
|
300
|
+
(config.run_uv_sync, "--sync"),
|
|
301
|
+
(config.install_git_hooks, "--hooks"),
|
|
302
|
+
)
|
|
303
|
+
return [flag for condition, flag in flag_conditions if condition]
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def _resolve_ai_tools_arg(ai_tools: tuple[str, ...]) -> str:
|
|
307
|
+
"""Determine the CLI argument value representing selected AI tools.
|
|
308
|
+
|
|
309
|
+
Args:
|
|
310
|
+
ai_tools: Tuple of selected tool identifier strings.
|
|
311
|
+
|
|
312
|
+
Returns:
|
|
313
|
+
Preset name or comma-separated list of tools.
|
|
314
|
+
|
|
315
|
+
"""
|
|
316
|
+
if not ai_tools:
|
|
317
|
+
return "none"
|
|
318
|
+
for preset_name, preset_tools in AI_TOOL_PRESETS.items():
|
|
319
|
+
if set(ai_tools) == set(preset_tools):
|
|
320
|
+
return preset_name
|
|
321
|
+
return ",".join(ai_tools)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def build_reproduce_command(config: ZenithConfig) -> str:
|
|
325
|
+
"""Build the exact command to recreate this project via CLI.
|
|
326
|
+
|
|
327
|
+
Args:
|
|
328
|
+
config: Configuration used to render the project.
|
|
329
|
+
|
|
330
|
+
Returns:
|
|
331
|
+
Shell-quoted CLI command string.
|
|
332
|
+
|
|
333
|
+
"""
|
|
334
|
+
parts = [
|
|
335
|
+
"uvx zenith-template --no-input",
|
|
336
|
+
f"--name {shlex.quote(config.project_name)}",
|
|
337
|
+
f"--slug {shlex.quote(config.project_slug)}",
|
|
338
|
+
f"--package {shlex.quote(config.package_name)}",
|
|
339
|
+
f"--description {shlex.quote(config.description)}",
|
|
340
|
+
f"--author {shlex.quote(config.author_name)}",
|
|
341
|
+
f"--email {shlex.quote(config.author_email)}",
|
|
342
|
+
f"--github {shlex.quote(config.github_username)}",
|
|
343
|
+
f"--python-version {config.python_version}",
|
|
344
|
+
f"--license {config.license}",
|
|
345
|
+
]
|
|
346
|
+
parts.extend(_build_feature_flags(config))
|
|
347
|
+
tools_arg = _resolve_ai_tools_arg(config.ai_tools)
|
|
348
|
+
parts.append(f"--ai-tools {tools_arg}")
|
|
349
|
+
return " ".join(parts)
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def render_summary(project_dir: Path, config: ZenithConfig) -> None:
|
|
353
|
+
"""Print a success confirmation card with next steps.
|
|
354
|
+
|
|
355
|
+
Args:
|
|
356
|
+
project_dir: Path to the generated project directory.
|
|
357
|
+
config: Configuration used to render the project.
|
|
358
|
+
|
|
359
|
+
"""
|
|
360
|
+
tools_str = ", ".join(config.ai_tools) if config.ai_tools else "None"
|
|
361
|
+
recreate_cmd = build_reproduce_command(config)
|
|
362
|
+
|
|
363
|
+
next_steps: list[str] = [f"cd {config.project_slug}"]
|
|
364
|
+
if not config.run_uv_sync:
|
|
365
|
+
next_steps.append("uv sync")
|
|
366
|
+
next_steps.extend(["uv run poe test", "uv run poe code-quality"])
|
|
367
|
+
steps_formatted = "\n".join(
|
|
368
|
+
f" [#38bdf8]{idx}.[/] [#e0f2fe]{step}[/]"
|
|
369
|
+
for idx, step in enumerate(next_steps, 1)
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
summary_text = (
|
|
373
|
+
f"[bold #38bdf8]✦ Project successfully scaffolded at:[/]\n"
|
|
374
|
+
f" [#e0f2fe]{project_dir.resolve()}[/]\n\n"
|
|
375
|
+
f"[bold #38bdf8]✦ Active AI Tool Integrations:[/]\n"
|
|
376
|
+
f" [dim #94a3b8]{tools_str}[/]\n\n"
|
|
377
|
+
f"[bold #38bdf8]✦ Recreate via CLI:[/]\n"
|
|
378
|
+
f" [#38bdf8]{recreate_cmd}[/]\n\n"
|
|
379
|
+
f"[bold #38bdf8]✦ Next Steps:[/]\n"
|
|
380
|
+
f"{steps_formatted}"
|
|
381
|
+
)
|
|
382
|
+
panel = Panel(
|
|
383
|
+
summary_text,
|
|
384
|
+
title="[bold #38bdf8]✦ Zenith Project Initialized ✦[/]",
|
|
385
|
+
border_style="#38bdf8",
|
|
386
|
+
box=box.ROUNDED,
|
|
387
|
+
padding=(1, 2),
|
|
388
|
+
)
|
|
389
|
+
console.print(panel)
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
393
|
+
"""Run the Zenith CLI entry point.
|
|
394
|
+
|
|
395
|
+
Args:
|
|
396
|
+
argv: Optional command-line argument vector.
|
|
397
|
+
|
|
398
|
+
Returns:
|
|
399
|
+
Process exit status code.
|
|
400
|
+
|
|
401
|
+
"""
|
|
402
|
+
parser = build_parser()
|
|
403
|
+
args = parser.parse_args(argv if argv is not None else sys.argv[1:])
|
|
404
|
+
|
|
405
|
+
if not args.no_input and sys.stdin.isatty():
|
|
406
|
+
initial = config_from_args(args)
|
|
407
|
+
app = ZenithApp(initial_config=initial)
|
|
408
|
+
app.run()
|
|
409
|
+
if app.result is None:
|
|
410
|
+
console.print("[yellow]Project generation cancelled.[/yellow]")
|
|
411
|
+
return 130
|
|
412
|
+
config = app.result
|
|
413
|
+
else:
|
|
414
|
+
config = config_from_args(args)
|
|
415
|
+
|
|
416
|
+
try:
|
|
417
|
+
with console.status(
|
|
418
|
+
"[bold #38bdf8]✦ Initializing Zenith project...[/]",
|
|
419
|
+
spinner="dots",
|
|
420
|
+
) as status:
|
|
421
|
+
project_dir = generate_project(
|
|
422
|
+
config,
|
|
423
|
+
output_dir=config.output_dir,
|
|
424
|
+
overwrite=args.overwrite,
|
|
425
|
+
on_step=lambda msg: status.update(f"[bold #38bdf8]✦ {msg}[/]"),
|
|
426
|
+
)
|
|
427
|
+
except (FileExistsError, FileNotFoundError) as err:
|
|
428
|
+
console.print(f"[bold red]Error:[/bold red] {err}")
|
|
429
|
+
return 1
|
|
430
|
+
except KeyboardInterrupt:
|
|
431
|
+
console.print("\n[yellow]Operation aborted by user.[/yellow]")
|
|
432
|
+
return 130
|
|
433
|
+
else:
|
|
434
|
+
render_summary(project_dir, config)
|
|
435
|
+
return 0
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
if __name__ == "__main__":
|
|
439
|
+
sys.exit(main())
|
zenith/generator.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Execution of Cookiecutter project generation using programmatic API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import contextlib
|
|
6
|
+
import subprocess # ruff: ignore[suspicious-subprocess-import] -- post-scaffold lifecycle automation
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
from cookiecutter.main import cookiecutter
|
|
11
|
+
|
|
12
|
+
from zenith.template import get_template_path
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from collections.abc import Callable
|
|
16
|
+
|
|
17
|
+
from zenith.models import ZenithConfig
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _noop_step(_message: str) -> None:
|
|
21
|
+
"""Ignore step status notifications when no callback is provided."""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def run_cmd(cmd: list[str], cwd: Path) -> None:
|
|
25
|
+
"""Safely run a system command in the generated project directory.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
cmd: Command argument vector to execute.
|
|
29
|
+
cwd: Working directory for command execution.
|
|
30
|
+
|
|
31
|
+
"""
|
|
32
|
+
with contextlib.suppress(OSError, subprocess.SubprocessError):
|
|
33
|
+
subprocess.run( # ruff: ignore[subprocess-without-shell-equals-true] -- safe execution with argument list
|
|
34
|
+
cmd,
|
|
35
|
+
cwd=cwd,
|
|
36
|
+
check=False,
|
|
37
|
+
capture_output=True,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _run_post_scaffold(
|
|
42
|
+
project_dir: Path,
|
|
43
|
+
config: ZenithConfig,
|
|
44
|
+
on_step: Callable[[str], None] = _noop_step,
|
|
45
|
+
) -> None:
|
|
46
|
+
"""Execute optional post-scaffolding lifecycle automation steps.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
project_dir: Path to the generated project directory.
|
|
50
|
+
config: Configuration containing lifecycle flags.
|
|
51
|
+
on_step: Callback invoked with a human-readable description before each step.
|
|
52
|
+
|
|
53
|
+
"""
|
|
54
|
+
if config.init_git and not (project_dir / ".git").is_dir():
|
|
55
|
+
on_step("Initializing Git repository (git init)...")
|
|
56
|
+
run_cmd(["git", "init"], cwd=project_dir)
|
|
57
|
+
|
|
58
|
+
if config.run_uv_sync:
|
|
59
|
+
on_step("Synchronizing Python environment (uv sync)...")
|
|
60
|
+
run_cmd(["uv", "sync"], cwd=project_dir)
|
|
61
|
+
|
|
62
|
+
if config.install_git_hooks and (project_dir / ".git").is_dir():
|
|
63
|
+
on_step("Installing Git hooks (prek install)...")
|
|
64
|
+
run_cmd(["uv", "run", "prek", "install"], cwd=project_dir)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def generate_project(
|
|
68
|
+
config: ZenithConfig,
|
|
69
|
+
output_dir: Path | str = ".",
|
|
70
|
+
*,
|
|
71
|
+
overwrite: bool = False,
|
|
72
|
+
on_step: Callable[[str], None] = _noop_step,
|
|
73
|
+
) -> Path:
|
|
74
|
+
"""Generate a new Zenith project from the bundled template.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
config: Project configuration settings.
|
|
78
|
+
output_dir: Destination directory where project folder will be created.
|
|
79
|
+
overwrite: Whether to overwrite existing files in destination directory.
|
|
80
|
+
on_step: Callback invoked with a human-readable description before each step.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
Path to the scaffolded project directory.
|
|
84
|
+
|
|
85
|
+
Raises:
|
|
86
|
+
FileExistsError: If the target directory exists and overwrite is False.
|
|
87
|
+
|
|
88
|
+
"""
|
|
89
|
+
out_path = Path(output_dir).resolve()
|
|
90
|
+
target_dir = out_path / config.project_slug
|
|
91
|
+
|
|
92
|
+
if target_dir.is_dir() and not overwrite and any(target_dir.iterdir()):
|
|
93
|
+
msg = f"Target directory '{target_dir}' already exists and is not empty."
|
|
94
|
+
raise FileExistsError(msg)
|
|
95
|
+
|
|
96
|
+
template_path = get_template_path()
|
|
97
|
+
context = config.to_cookiecutter_context()
|
|
98
|
+
|
|
99
|
+
on_step(f"Scaffolding project template into {target_dir.name}...")
|
|
100
|
+
result = cookiecutter(
|
|
101
|
+
template=str(template_path),
|
|
102
|
+
extra_context=context,
|
|
103
|
+
no_input=True,
|
|
104
|
+
output_dir=str(out_path),
|
|
105
|
+
overwrite_if_exists=overwrite,
|
|
106
|
+
)
|
|
107
|
+
project_path = Path(result)
|
|
108
|
+
_run_post_scaffold(project_path, config, on_step=on_step)
|
|
109
|
+
return project_path
|