ud-kibot 0.1.1__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 (43) hide show
  1. ud_kibot-0.1.1/LICENSE +21 -0
  2. ud_kibot-0.1.1/PKG-INFO +98 -0
  3. ud_kibot-0.1.1/README.md +60 -0
  4. ud_kibot-0.1.1/pyproject.toml +78 -0
  5. ud_kibot-0.1.1/setup.cfg +4 -0
  6. ud_kibot-0.1.1/src/ud_kibot/__init__.py +3 -0
  7. ud_kibot-0.1.1/src/ud_kibot/__main__.py +4 -0
  8. ud_kibot-0.1.1/src/ud_kibot/assets/resolver.py +15 -0
  9. ud_kibot-0.1.1/src/ud_kibot/cli/__init__.py +0 -0
  10. ud_kibot-0.1.1/src/ud_kibot/cli/main.py +372 -0
  11. ud_kibot-0.1.1/src/ud_kibot/config/__init__.py +0 -0
  12. ud_kibot-0.1.1/src/ud_kibot/config/loader.py +55 -0
  13. ud_kibot-0.1.1/src/ud_kibot/config/lock.py +35 -0
  14. ud_kibot-0.1.1/src/ud_kibot/config/schema.py +76 -0
  15. ud_kibot-0.1.1/src/ud_kibot/config/user_schema.py +48 -0
  16. ud_kibot-0.1.1/src/ud_kibot/doctor/__init__.py +0 -0
  17. ud_kibot-0.1.1/src/ud_kibot/doctor/checks.py +84 -0
  18. ud_kibot-0.1.1/src/ud_kibot/hooks.py +36 -0
  19. ud_kibot-0.1.1/src/ud_kibot/kibot/__init__.py +0 -0
  20. ud_kibot-0.1.1/src/ud_kibot/kibot/invoke.py +123 -0
  21. ud_kibot-0.1.1/src/ud_kibot/pages.py +47 -0
  22. ud_kibot-0.1.1/src/ud_kibot/paths.py +63 -0
  23. ud_kibot-0.1.1/src/ud_kibot/template/__init__.py +0 -0
  24. ud_kibot-0.1.1/src/ud_kibot/template/installer.py +50 -0
  25. ud_kibot-0.1.1/src/ud_kibot/template/manifest.py +31 -0
  26. ud_kibot-0.1.1/src/ud_kibot/template/renderer.py +144 -0
  27. ud_kibot-0.1.1/src/ud_kibot/template/resolver.py +168 -0
  28. ud_kibot-0.1.1/src/ud_kibot/template/stage.py +30 -0
  29. ud_kibot-0.1.1/src/ud_kibot/validate.py +84 -0
  30. ud_kibot-0.1.1/src/ud_kibot.egg-info/PKG-INFO +98 -0
  31. ud_kibot-0.1.1/src/ud_kibot.egg-info/SOURCES.txt +41 -0
  32. ud_kibot-0.1.1/src/ud_kibot.egg-info/dependency_links.txt +1 -0
  33. ud_kibot-0.1.1/src/ud_kibot.egg-info/entry_points.txt +2 -0
  34. ud_kibot-0.1.1/src/ud_kibot.egg-info/requires.txt +15 -0
  35. ud_kibot-0.1.1/src/ud_kibot.egg-info/top_level.txt +1 -0
  36. ud_kibot-0.1.1/tests/test_cli.py +50 -0
  37. ud_kibot-0.1.1/tests/test_config.py +70 -0
  38. ud_kibot-0.1.1/tests/test_coverage_extra.py +144 -0
  39. ud_kibot-0.1.1/tests/test_hooks.py +33 -0
  40. ud_kibot-0.1.1/tests/test_release_utils.py +50 -0
  41. ud_kibot-0.1.1/tests/test_runtime.py +70 -0
  42. ud_kibot-0.1.1/tests/test_template.py +77 -0
  43. ud_kibot-0.1.1/tests/test_verify_release.py +36 -0
ud_kibot-0.1.1/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 UltraDuino contributors
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.
@@ -0,0 +1,98 @@
1
+ Metadata-Version: 2.4
2
+ Name: ud-kibot
3
+ Version: 0.1.1
4
+ Summary: UltraDuino KiCad/KiBot project automation CLI
5
+ Author-email: UltraDuino <ops@ultraduino.org>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://gitlab.com/ultraduino/ud-kibot
8
+ Project-URL: Repository, https://gitlab.com/ultraduino/ud-kibot.git
9
+ Project-URL: Documentation, https://gitlab.com/ultraduino/ud-kibot/-/blob/develop/README.md
10
+ Keywords: kicad,kibot,gitlab,pcb,ci,cli
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: Microsoft :: Windows
15
+ Classifier: Operating System :: POSIX :: Linux
16
+ Classifier: Operating System :: MacOS
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Topic :: Software Development :: Build Tools
21
+ Requires-Python: >=3.11
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Requires-Dist: typer>=0.12
25
+ Requires-Dist: pydantic>=2.0
26
+ Requires-Dist: jinja2>=3.1
27
+ Requires-Dist: pyyaml>=6.0
28
+ Requires-Dist: rich>=13.0
29
+ Requires-Dist: packaging>=24.0
30
+ Provides-Extra: dev
31
+ Requires-Dist: pytest>=8.0; extra == "dev"
32
+ Requires-Dist: pytest-cov>=5.0; extra == "dev"
33
+ Requires-Dist: ruff>=0.8; extra == "dev"
34
+ Provides-Extra: release
35
+ Requires-Dist: build>=1.2; extra == "release"
36
+ Requires-Dist: twine>=5.0; extra == "release"
37
+ Dynamic: license-file
38
+
39
+ # ud-kibot
40
+
41
+ UltraDuino CLI for KiCad/KiBot project automation.
42
+
43
+ ## Install
44
+
45
+ ```bash
46
+ pip install ud-kibot
47
+ ```
48
+
49
+ Development install:
50
+
51
+ ```bash
52
+ cd ud-kibot
53
+ pip install -e ".[dev,release]"
54
+ pre-commit install
55
+ pre-commit install --hook-type commit-msg
56
+ ```
57
+
58
+ ## Quick start
59
+
60
+ ```bash
61
+ ud-kibot config init
62
+ cd ../ucube_left_card
63
+ ud-kibot init . --board ucube_left_card --skip-pcb-scaffold
64
+ ud-kibot install . --template-path ../ultraduino-kibot-template
65
+ ud-kibot doctor .
66
+ ```
67
+
68
+ ## Development
69
+
70
+ ```bash
71
+ make lint
72
+ make test
73
+ ```
74
+
75
+ Coverage gate: **80%** (`pyproject.toml`).
76
+
77
+ ## Release flow
78
+
79
+ Same model as `esys-ci-host`:
80
+
81
+ 1. Update `[Unreleased]` in `CHANGELOG.md`
82
+ 2. `make bump-release` (or `python scripts/bump_release.py`)
83
+ 3. Merge to `master`, wait for green CI
84
+ 4. `make tag-release` (or `python scripts/tag_release.py --yes`)
85
+ 5. `git push origin vX.Y.Z`
86
+
87
+ Tag pipelines build the wheel, smoke-install it, publish to GitLab PyPI, then optionally TestPyPI/PyPI.
88
+
89
+ ## User configuration
90
+
91
+ - Linux/macOS: `~/.config/ud-kibot/config.yaml`
92
+ - Windows: `%APPDATA%\ud-kibot\config.yaml`
93
+
94
+ Default template: `https://gitlab.com/ultraduino/ultraduino-kibot-template.git`
95
+
96
+ ## License
97
+
98
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,60 @@
1
+ # ud-kibot
2
+
3
+ UltraDuino CLI for KiCad/KiBot project automation.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install ud-kibot
9
+ ```
10
+
11
+ Development install:
12
+
13
+ ```bash
14
+ cd ud-kibot
15
+ pip install -e ".[dev,release]"
16
+ pre-commit install
17
+ pre-commit install --hook-type commit-msg
18
+ ```
19
+
20
+ ## Quick start
21
+
22
+ ```bash
23
+ ud-kibot config init
24
+ cd ../ucube_left_card
25
+ ud-kibot init . --board ucube_left_card --skip-pcb-scaffold
26
+ ud-kibot install . --template-path ../ultraduino-kibot-template
27
+ ud-kibot doctor .
28
+ ```
29
+
30
+ ## Development
31
+
32
+ ```bash
33
+ make lint
34
+ make test
35
+ ```
36
+
37
+ Coverage gate: **80%** (`pyproject.toml`).
38
+
39
+ ## Release flow
40
+
41
+ Same model as `esys-ci-host`:
42
+
43
+ 1. Update `[Unreleased]` in `CHANGELOG.md`
44
+ 2. `make bump-release` (or `python scripts/bump_release.py`)
45
+ 3. Merge to `master`, wait for green CI
46
+ 4. `make tag-release` (or `python scripts/tag_release.py --yes`)
47
+ 5. `git push origin vX.Y.Z`
48
+
49
+ Tag pipelines build the wheel, smoke-install it, publish to GitLab PyPI, then optionally TestPyPI/PyPI.
50
+
51
+ ## User configuration
52
+
53
+ - Linux/macOS: `~/.config/ud-kibot/config.yaml`
54
+ - Windows: `%APPDATA%\ud-kibot\config.yaml`
55
+
56
+ Default template: `https://gitlab.com/ultraduino/ultraduino-kibot-template.git`
57
+
58
+ ## License
59
+
60
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,78 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "ud-kibot"
7
+ dynamic = ["version"]
8
+ description = "UltraDuino KiCad/KiBot project automation CLI"
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ authors = [{ name = "UltraDuino", email = "ops@ultraduino.org" }]
14
+ keywords = ["kicad", "kibot", "gitlab", "pcb", "ci", "cli"]
15
+ classifiers = [
16
+ "Development Status :: 3 - Alpha",
17
+ "Environment :: Console",
18
+ "Intended Audience :: Developers",
19
+ "Operating System :: Microsoft :: Windows",
20
+ "Operating System :: POSIX :: Linux",
21
+ "Operating System :: MacOS",
22
+ "Programming Language :: Python :: 3",
23
+ "Programming Language :: Python :: 3.11",
24
+ "Programming Language :: Python :: 3.12",
25
+ "Topic :: Software Development :: Build Tools",
26
+ ]
27
+ dependencies = [
28
+ "typer>=0.12",
29
+ "pydantic>=2.0",
30
+ "jinja2>=3.1",
31
+ "pyyaml>=6.0",
32
+ "rich>=13.0",
33
+ "packaging>=24.0",
34
+ ]
35
+
36
+ [project.urls]
37
+ Homepage = "https://gitlab.com/ultraduino/ud-kibot"
38
+ Repository = "https://gitlab.com/ultraduino/ud-kibot.git"
39
+ Documentation = "https://gitlab.com/ultraduino/ud-kibot/-/blob/develop/README.md"
40
+
41
+ [project.optional-dependencies]
42
+ dev = [
43
+ "pytest>=8.0",
44
+ "pytest-cov>=5.0",
45
+ "ruff>=0.8",
46
+ ]
47
+ release = [
48
+ "build>=1.2",
49
+ "twine>=5.0",
50
+ ]
51
+
52
+ [project.scripts]
53
+ ud-kibot = "ud_kibot.cli.main:app"
54
+
55
+ [tool.setuptools.packages.find]
56
+ where = ["src"]
57
+
58
+ [tool.setuptools.dynamic]
59
+ version = { attr = "ud_kibot.__version__" }
60
+
61
+ [tool.pytest.ini_options]
62
+ testpaths = ["tests"]
63
+ pythonpath = ["src"]
64
+
65
+ [tool.ruff]
66
+ line-length = 100
67
+ target-version = "py311"
68
+ src = ["src"]
69
+
70
+ [tool.ruff.lint]
71
+ select = ["E", "F", "I", "UP"]
72
+
73
+ [tool.coverage.run]
74
+ source = ["ud_kibot"]
75
+
76
+ [tool.coverage.report]
77
+ fail_under = 80
78
+ show_missing = true
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ """UltraDuino KiCad/KiBot automation CLI."""
2
+
3
+ __version__ = "0.1.1"
@@ -0,0 +1,4 @@
1
+ from ud_kibot.cli.main import app
2
+
3
+ if __name__ == "__main__":
4
+ app()
@@ -0,0 +1,15 @@
1
+ """Resolve project logo: project override wins over template default."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+
8
+ def resolve_logo(project_dir: Path, template_dir: Path) -> Path:
9
+ project_logo = project_dir / "assets" / "logo.png"
10
+ if project_logo.is_file():
11
+ return project_logo
12
+ template_logo = template_dir / "assets" / "logo.png"
13
+ if template_logo.is_file():
14
+ return template_logo
15
+ return project_logo
File without changes
@@ -0,0 +1,372 @@
1
+ """Typer CLI entry point."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ import typer
8
+ from rich.console import Console
9
+ from rich.table import Table
10
+
11
+ from ud_kibot import __version__
12
+ from ud_kibot.config.loader import load_user_config, save_user_config
13
+ from ud_kibot.config.schema import (
14
+ BoardMeta,
15
+ CiMeta,
16
+ DocumentationMeta,
17
+ GitLabCiMeta,
18
+ KibotMeta,
19
+ KicadMeta,
20
+ ProjectConfig,
21
+ ProjectMeta,
22
+ SiteMeta,
23
+ TemplatePin,
24
+ )
25
+ from ud_kibot.config.user_schema import (
26
+ DefaultsConfig,
27
+ TemplateSourceConfig,
28
+ UserConfig,
29
+ )
30
+ from ud_kibot.doctor.checks import doctor_project
31
+ from ud_kibot.hooks import install_pre_commit_hooks
32
+ from ud_kibot.kibot.invoke import prepare_and_run
33
+ from ud_kibot.pages import assemble_pages
34
+ from ud_kibot.paths import normalize_project_dir
35
+ from ud_kibot.template.installer import install_project_scaffold
36
+ from ud_kibot.template.renderer import render_kibot_main
37
+ from ud_kibot.template.resolver import (
38
+ ensure_template_cached,
39
+ fetch_git_template,
40
+ resolve_template_for_project,
41
+ template_source_for_id,
42
+ )
43
+ from ud_kibot.validate import validate_project
44
+
45
+ app = typer.Typer(
46
+ name="ud-kibot",
47
+ help="UltraDuino KiCad/KiBot project automation.",
48
+ no_args_is_help=True,
49
+ )
50
+ config_app = typer.Typer(help="User configuration.")
51
+ template_app = typer.Typer(help="Template cache management.")
52
+ app.add_typer(config_app, name="config")
53
+ app.add_typer(template_app, name="template")
54
+
55
+ console = Console()
56
+
57
+
58
+ def _version_callback(value: bool) -> None:
59
+ if value:
60
+ typer.echo(__version__)
61
+ raise typer.Exit()
62
+
63
+
64
+ @app.callback()
65
+ def main_callback(
66
+ version: bool = typer.Option(
67
+ False,
68
+ "--version",
69
+ "-V",
70
+ callback=_version_callback,
71
+ is_eager=True,
72
+ help="Show version.",
73
+ ),
74
+ ) -> None:
75
+ """Root callback."""
76
+
77
+
78
+ def _resolve_path(path: Path) -> Path:
79
+ return normalize_project_dir(path)
80
+
81
+
82
+ def _resolve_template_path(template_path: Path | None) -> Path | None:
83
+ return template_path.resolve() if template_path else None
84
+
85
+
86
+ @config_app.command("init")
87
+ def config_init(
88
+ non_interactive: bool = typer.Option(
89
+ False, "--non-interactive", help="Skip prompts; accept defaults."
90
+ ),
91
+ accept_defaults: bool = typer.Option(
92
+ False, "--accept-defaults", help="Alias for non-interactive with defaults."
93
+ ),
94
+ ) -> None:
95
+ """Initialize user configuration."""
96
+ existing = load_user_config()
97
+ if existing.first_run_complete and not non_interactive and not accept_defaults:
98
+ overwrite = typer.confirm("User config already exists. Overwrite?", default=False)
99
+ if not overwrite:
100
+ raise typer.Exit()
101
+
102
+ if non_interactive or accept_defaults:
103
+ config = UserConfig.create_default()
104
+ else:
105
+ use_default = typer.confirm(
106
+ "Use ultraduino-kibot-template as default template?\n"
107
+ " https://gitlab.com/ultraduino/ultraduino-kibot-template.git",
108
+ default=True,
109
+ )
110
+ if use_default:
111
+ config = UserConfig.create_default()
112
+ else:
113
+ template_id = typer.prompt("Template id", default="custom-template")
114
+ git_url = typer.prompt("Template git URL")
115
+ config = UserConfig(
116
+ first_run_complete=True,
117
+ defaults=DefaultsConfig(template_id=template_id),
118
+ templates={
119
+ template_id: TemplateSourceConfig(git_url=git_url, default_ref="master"),
120
+ },
121
+ )
122
+
123
+ path = save_user_config(config)
124
+ console.print(f"[green]Wrote user config:[/green] {path}")
125
+
126
+
127
+ @config_app.command("show")
128
+ def config_show() -> None:
129
+ """Show user configuration."""
130
+ from ud_kibot.paths import user_config_path
131
+
132
+ config = load_user_config()
133
+ console.print(config.model_dump_json(indent=2))
134
+ console.print(f"\nPath: {user_config_path()}")
135
+
136
+
137
+ @app.command("init")
138
+ def project_init(
139
+ path: Path = typer.Argument(Path("."), help="Project directory."),
140
+ board: str = typer.Option(..., "--board", "-b", help="KiCad project basename."),
141
+ title: str = typer.Option("", "--title", help="Board title."),
142
+ skip_pcb_scaffold: bool = typer.Option(
143
+ False, "--skip-pcb-scaffold", help="Do not create pcb/ scaffold."
144
+ ),
145
+ ) -> None:
146
+ """Initialize a PCB project with ud-kibot.yaml and directories."""
147
+ project_dir = _resolve_path(path)
148
+ project_dir.mkdir(parents=True, exist_ok=True)
149
+ pcb_dir = project_dir / "pcb"
150
+ pcb_dir.mkdir(exist_ok=True)
151
+
152
+ if not skip_pcb_scaffold:
153
+ (pcb_dir / "lib").mkdir(exist_ok=True)
154
+ (pcb_dir / "Templates").mkdir(exist_ok=True)
155
+
156
+ for sub in ("assets", "kibot/local", "scripts", ".ud-kibot"):
157
+ (project_dir / sub).mkdir(parents=True, exist_ok=True)
158
+
159
+ config_path = project_dir / "ud-kibot.yaml"
160
+ if not config_path.is_file():
161
+ board_title = title or board.replace("_", " ").title()
162
+ config = ProjectConfig(
163
+ template=TemplatePin(),
164
+ project=ProjectMeta(id=board, name=board_title),
165
+ board=BoardMeta(basename=board, title=board_title),
166
+ kicad=KicadMeta(),
167
+ kibot=KibotMeta(),
168
+ documentation=DocumentationMeta(site=SiteMeta(title=f"UltraDuino · {board}")),
169
+ ci=CiMeta(gitlab=GitLabCiMeta()),
170
+ )
171
+ from ud_kibot.config.loader import save_project_config
172
+
173
+ save_project_config(project_dir, config)
174
+ console.print(f"[green]Created[/green] {config_path}")
175
+ else:
176
+ console.print(f"[yellow]Exists[/yellow] {config_path}")
177
+
178
+
179
+ @app.command("install")
180
+ def project_install(
181
+ path: Path = typer.Argument(Path("."), help="Project directory."),
182
+ template_path: Path | None = typer.Option(
183
+ None, "--template-path", help="Local template checkout (dev)."
184
+ ),
185
+ force: bool = typer.Option(False, "--force", help="Overwrite scaffold files."),
186
+ skip_hooks: bool = typer.Option(False, "--skip-hooks", help="Do not run pre-commit install."),
187
+ ) -> None:
188
+ """Bind project to template and install minimal scaffold."""
189
+ project_dir = _resolve_path(path)
190
+ written = install_project_scaffold(
191
+ project_dir,
192
+ template_path=_resolve_template_path(template_path),
193
+ force=force,
194
+ )
195
+ console.print(f"[green]Installed template scaffold ({len(written)} files).[/green]")
196
+ for item in written:
197
+ console.print(f" - {item.relative_to(project_dir)}")
198
+ if not skip_hooks:
199
+ _report_hook_install(project_dir)
200
+
201
+
202
+ @app.command("install-hooks")
203
+ def project_install_hooks(
204
+ path: Path = typer.Argument(Path("."), help="Project directory."),
205
+ ) -> None:
206
+ """Install pre-commit and commit-msg hooks for a PCB project."""
207
+ _report_hook_install(_resolve_path(path))
208
+
209
+
210
+ def _report_hook_install(project_dir: Path) -> None:
211
+ ok, message = install_pre_commit_hooks(project_dir)
212
+ if ok:
213
+ console.print(f"[green]{message}[/green]")
214
+ else:
215
+ console.print(f"[yellow]Pre-commit hooks not installed: {message}[/yellow]")
216
+ console.print(" Install pre-commit, then run: ud-kibot install-hooks .")
217
+
218
+
219
+ @app.command("upgrade")
220
+ def project_upgrade(
221
+ path: Path = typer.Argument(Path("."), help="Project directory."),
222
+ template_path: Path | None = typer.Option(None, "--template-path"),
223
+ force: bool = typer.Option(False, "--force"),
224
+ ) -> None:
225
+ """Refresh template cache and re-render project files."""
226
+ project_install(path=path, template_path=template_path, force=force)
227
+
228
+
229
+ @app.command("repair")
230
+ def project_repair(
231
+ path: Path = typer.Argument(Path("."), help="Project directory."),
232
+ template_path: Path | None = typer.Option(None, "--template-path"),
233
+ ) -> None:
234
+ """Re-fetch template, rewrite lock, and re-render."""
235
+ project_dir = _resolve_path(path)
236
+ ensure_template_cached(project_dir, template_path=_resolve_template_path(template_path))
237
+ install_project_scaffold(
238
+ project_dir,
239
+ template_path=_resolve_template_path(template_path),
240
+ force=True,
241
+ )
242
+ console.print("[green]Repair complete.[/green]")
243
+
244
+
245
+ @app.command("render-config")
246
+ def render_config(
247
+ path: Path = typer.Argument(Path("."), help="Project directory."),
248
+ template_path: Path | None = typer.Option(None, "--template-path"),
249
+ ) -> None:
250
+ """Render KiBot main config and refresh scaffold renders."""
251
+ project_dir = _resolve_path(path)
252
+ template_dir, _ = resolve_template_for_project(
253
+ project_dir, template_path=_resolve_template_path(template_path)
254
+ )
255
+ output = render_kibot_main(project_dir, template_dir)
256
+ console.print(f"[green]Rendered[/green] {output}")
257
+
258
+
259
+ @template_app.command("fetch")
260
+ def template_fetch(
261
+ template_id: str | None = typer.Argument(None, help="Template id."),
262
+ project: Path | None = typer.Option(None, "--project", help="Fetch template for a project."),
263
+ template_path: Path | None = typer.Option(None, "--template-path"),
264
+ ref: str | None = typer.Option(None, "--ref"),
265
+ ) -> None:
266
+ """Fetch template into global cache."""
267
+ if project is not None:
268
+ directory = ensure_template_cached(
269
+ _resolve_path(project), template_path=_resolve_template_path(template_path)
270
+ )
271
+ console.print(f"[green]Cached[/green] {directory}")
272
+ return
273
+
274
+ user = load_user_config()
275
+ tid = template_id or user.defaults.template_id
276
+ if tid not in user.templates:
277
+ raise typer.BadParameter(f"Unknown template id: {tid}")
278
+ git_url, default_ref = template_source_for_id(user, tid)
279
+ directory = fetch_git_template(tid, git_url, ref or default_ref)
280
+ from ud_kibot.template.manifest import load_manifest
281
+
282
+ manifest = load_manifest(directory)
283
+ console.print(f"[green]Fetched[/green] {manifest.id}@{manifest.version} → {directory}")
284
+
285
+
286
+ @app.command("validate")
287
+ def project_validate(
288
+ path: Path = typer.Argument(Path("."), help="Project directory."),
289
+ template_path: Path | None = typer.Option(None, "--template-path"),
290
+ strict: bool = typer.Option(False, "--strict"),
291
+ ) -> None:
292
+ """Validate project configuration and template binding."""
293
+ issues = validate_project(
294
+ _resolve_path(path),
295
+ template_path=_resolve_template_path(template_path),
296
+ strict=strict,
297
+ )
298
+ exit_code = 0
299
+ for issue in issues:
300
+ if issue.level == "error":
301
+ console.print(f"[red]ERROR[/red] {issue.message}")
302
+ exit_code = 1
303
+ else:
304
+ console.print(f"[yellow]WARN[/yellow] {issue.message}")
305
+ if exit_code:
306
+ raise typer.Exit(code=exit_code)
307
+ console.print("[green]Validation passed.[/green]")
308
+
309
+
310
+ @app.command("doctor")
311
+ def project_doctor(
312
+ path: Path = typer.Argument(Path("."), help="Project directory."),
313
+ template_path: Path | None = typer.Option(None, "--template-path"),
314
+ ) -> None:
315
+ """Run health checks."""
316
+ results = doctor_project(
317
+ _resolve_path(path), template_path=_resolve_template_path(template_path)
318
+ )
319
+ table = Table("Check", "Status", "Detail")
320
+ failed = False
321
+ non_fatal = frozenset({"docker", "user-config"})
322
+ for item in results:
323
+ ok = item.ok
324
+ if item.name == "validate" and not ok:
325
+ failed = True
326
+ if item.name == "validate-warning":
327
+ status = "warn"
328
+ else:
329
+ status = "ok" if ok else "fail"
330
+ if not ok and item.name not in non_fatal:
331
+ failed = True
332
+ table.add_row(item.name, status, item.detail)
333
+ console.print(table)
334
+ if failed:
335
+ raise typer.Exit(code=1)
336
+
337
+
338
+ @app.command("run-kibot")
339
+ def run_kibot(
340
+ path: Path = typer.Argument(Path("."), help="Project directory."),
341
+ variant: str | None = typer.Option(None, "--variant", "-v"),
342
+ group: str | None = typer.Option(None, "--group", "-g"),
343
+ skip_pre: str | None = typer.Option(None, "--skip-pre"),
344
+ template_path: Path | None = typer.Option(None, "--template-path"),
345
+ no_docker: bool = typer.Option(False, "--no-docker"),
346
+ ) -> None:
347
+ """Render config and run KiBot."""
348
+ code = prepare_and_run(
349
+ _resolve_path(path),
350
+ template_path=_resolve_template_path(template_path),
351
+ variant=variant,
352
+ group=group,
353
+ skip_pre=skip_pre,
354
+ no_docker=no_docker,
355
+ )
356
+ raise typer.Exit(code=code)
357
+
358
+
359
+ @app.command("assemble-pages")
360
+ def assemble_pages_cmd(
361
+ path: Path = typer.Argument(Path("."), help="Project directory."),
362
+ template_path: Path | None = typer.Option(None, "--template-path"),
363
+ ) -> None:
364
+ """Assemble GitLab Pages site into public/."""
365
+ public = assemble_pages(
366
+ _resolve_path(path), template_path=_resolve_template_path(template_path)
367
+ )
368
+ console.print(f"[green]Pages assembled at[/green] {public}")
369
+
370
+
371
+ if __name__ == "__main__":
372
+ app()
File without changes