Coverage for src / rhiza / commands / init.py: 100%

320 statements  

« prev     ^ index     » next       coverage.py v7.14.0, created at 2026-06-15 18:22 +0000

1"""Command to initialize or validate .rhiza/template.yml. 

2 

3This module provides the init command that creates or validates the 

4.rhiza/template.yml file, which defines where templates come from 

5and what paths are governed by Rhiza. 

6""" 

7 

8import importlib.resources 

9import keyword 

10import re 

11import subprocess # nosec B404 

12import sys 

13import urllib.error 

14import urllib.parse 

15from pathlib import Path 

16 

17import typer 

18from jinja2 import Template 

19from loguru import logger 

20 

21from rhiza.commands.list_repos import _DESC_WIDTH, _fetch_repos 

22from rhiza.commands.validate import validate 

23from rhiza.models import GitContext, GitHost 

24 

25 

26def _normalize_package_name(name: str) -> str: 

27 """Normalize a string into a valid Python package name. 

28 

29 Args: 

30 name: The input string (e.g., project name). 

31 

32 Returns: 

33 A valid Python identifier safe for use as a package name. 

34 """ 

35 name = re.sub(r"[^a-zA-Z0-9_]", "_", name) 

36 if name[0].isdigit(): 

37 name = f"_{name}" 

38 if keyword.iskeyword(name): 

39 name = f"{name}_" 

40 return name 

41 

42 

43def _validate_git_host(git_host: str | None) -> GitHost | None: 

44 """Validate git_host parameter. 

45 

46 Args: 

47 git_host: Git hosting platform. 

48 

49 Returns: 

50 Validated GitHost enum value or None. 

51 

52 Raises: 

53 ValueError: If git_host is invalid. 

54 """ 

55 if git_host is None: 

56 return None 

57 try: 

58 return GitHost(git_host.lower()) 

59 except ValueError: 

60 logger.error(f"Invalid git-host: {git_host}. Must be 'github' or 'gitlab'") 

61 raise ValueError(f"Invalid git-host: {git_host}. Must be 'github' or 'gitlab'") from None # noqa: TRY003 

62 

63 

64def _check_template_repository_reachable(template_repository: str, git_host: GitHost = GitHost.GITHUB) -> bool: 

65 """Check if the template repository is reachable via git ls-remote. 

66 

67 Args: 

68 template_repository: Repository in 'owner/repo' format. 

69 git_host: Git hosting platform ('github' or 'gitlab'). Defaults to 'github'. 

70 

71 Returns: 

72 True if the repository is reachable, False otherwise. 

73 """ 

74 host_urls = { 

75 GitHost.GITHUB: "https://github.com", 

76 GitHost.GITLAB: "https://gitlab.com", 

77 } 

78 base_url = host_urls.get(git_host, "https://github.com") 

79 repo_url = f"{base_url}/{template_repository}" 

80 

81 logger.debug(f"Checking reachability of template repository: {repo_url}") 

82 try: 

83 git_ctx = GitContext.default() 

84 result = subprocess.run( # nosec B603 # noqa: S603 

85 [git_ctx.executable, "ls-remote", "--exit-code", repo_url], 

86 capture_output=True, 

87 timeout=30, 

88 env=git_ctx.env, 

89 ) 

90 if result.returncode == 0: 

91 logger.success(f"Template repository is reachable: {template_repository}") 

92 return True 

93 else: 

94 logger.error(f"Template repository '{template_repository}' is not accessible at {repo_url}") 

95 logger.error("Please check that the repository exists and you have access to it.") 

96 return False 

97 except subprocess.TimeoutExpired: 

98 logger.error(f"Timed out while checking repository reachability: {repo_url}") 

99 logger.error("Please check your network connection and try again.") 

100 return False 

101 except RuntimeError as e: 

102 logger.warning(f"Could not verify template repository reachability: {e}") 

103 return True # Don't block init if git is unavailable 

104 

105 

106def _get_latest_tag(template_repository: str, git_host: GitHost | str = GitHost.GITHUB) -> str | None: 

107 """Fetch the latest version tag from the template repository via git ls-remote. 

108 

109 Args: 

110 template_repository: Repository in 'owner/repo' format. 

111 git_host: Git hosting platform. 

112 

113 Returns: 

114 Latest version tag (e.g. ``'v0.18.4'``), or ``None`` on error or when no 

115 version tags exist. 

116 """ 

117 if git_host == GitHost.GITLAB: 

118 repo_url = f"https://gitlab.com/{template_repository}" 

119 else: 

120 repo_url = f"https://github.com/{template_repository}" 

121 

122 logger.debug(f"Fetching latest tag from {repo_url}") 

123 try: 

124 git_ctx = GitContext.default() 

125 result = subprocess.run( # nosec B603 # noqa: S603 

126 [git_ctx.executable, "ls-remote", "--tags", repo_url], 

127 capture_output=True, 

128 text=True, 

129 timeout=30, 

130 env=git_ctx.env, 

131 ) 

132 if result.returncode != 0: 

133 logger.debug(f"git ls-remote --tags failed for {repo_url}") 

134 return None 

135 

136 version_tags = [] 

137 for line in result.stdout.splitlines(): 

138 if "^{}" in line: # skip dereferenced annotated-tag objects 

139 continue 

140 parts = line.split("\t") 

141 if len(parts) == 2 and parts[1].startswith("refs/tags/"): 

142 tag = parts[1][len("refs/tags/") :] 

143 if re.match(r"^v?\d+\.\d+", tag): 

144 version_tags.append(tag) 

145 

146 if not version_tags: 

147 logger.debug(f"No version tags found in {repo_url}") 

148 return None 

149 

150 latest = max(version_tags, key=lambda t: tuple(int(x) for x in re.findall(r"\d+", t))) 

151 logger.debug(f"Latest tag: {latest}") 

152 

153 except (subprocess.TimeoutExpired, RuntimeError, OSError) as exc: 

154 logger.debug(f"Could not fetch latest tag from {repo_url}: {exc}") 

155 return None 

156 else: 

157 return latest 

158 

159 

160def _get_github_username(target: Path) -> str: 

161 """Extract the GitHub/GitLab username (or org) from the origin remote URL. 

162 

163 Args: 

164 target: Repository root directory. 

165 

166 Returns: 

167 Username string, or ``"your-org"`` when detection fails. 

168 """ 

169 try: 

170 git_ctx = GitContext.default() 

171 result = subprocess.run( # nosec B603 # noqa: S603 

172 [git_ctx.executable, "remote", "get-url", "origin"], 

173 capture_output=True, 

174 text=True, 

175 cwd=target, 

176 env=git_ctx.env, 

177 ) 

178 if result.returncode != 0: 

179 return "your-org" 

180 url = result.stdout.strip() 

181 # ssh: git@github.com:username/repo.git 

182 ssh_match = re.match(r"git@[^:]+:([^/]+)/", url) 

183 if ssh_match: 

184 return ssh_match.group(1) 

185 # https: https://github.com/username/repo.git 

186 https_match = re.match(r"https?://[^/]+/([^/]+)/", url) 

187 if https_match: 

188 return https_match.group(1) 

189 except (RuntimeError, OSError): 

190 pass 

191 return "your-org" 

192 

193 

194def _detect_git_host(target: Path) -> GitHost | None: 

195 """Infer the git hosting platform from the repository's origin remote URL. 

196 

197 Args: 

198 target: Repository root directory. 

199 

200 Returns: 

201 Detected :class:`GitHost`, or ``None`` when detection is not possible 

202 (no git repo, no origin remote, or unrecognised host). 

203 """ 

204 try: 

205 git_ctx = GitContext.default() 

206 result = subprocess.run( # nosec B603 # noqa: S603 

207 [git_ctx.executable, "remote", "get-url", "origin"], 

208 capture_output=True, 

209 text=True, 

210 cwd=target, 

211 env=git_ctx.env, 

212 ) 

213 if result.returncode != 0: 

214 return None 

215 url = result.stdout.strip() 

216 

217 host: str | None = None 

218 if "://" in url: 

219 host = urllib.parse.urlparse(url).hostname 

220 else: 

221 # Handle SCP-like git remotes, e.g. git@github.com:org/repo.git 

222 match = re.match(r"^(?:[^@]+@)?([^:]+):.+$", url) 

223 if match: 

224 host = match.group(1) 

225 

226 host = host.lower() if host else None 

227 if host == "github.com": 

228 logger.debug(f"Detected git host: github (from {url})") 

229 return GitHost.GITHUB 

230 if host == "gitlab.com": 

231 logger.debug(f"Detected git host: gitlab (from {url})") 

232 return GitHost.GITLAB 

233 except (RuntimeError, OSError): 

234 return None 

235 else: 

236 return None 

237 

238 

239def _prompt_git_host() -> GitHost: 

240 """Prompt user for git hosting platform. 

241 

242 Returns: 

243 Git hosting platform choice as a GitHost enum value. 

244 """ 

245 if sys.stdin.isatty(): 

246 logger.info("Where will your project be hosted?") 

247 git_host = typer.prompt( 

248 "Target Git hosting platform (github/gitlab)", 

249 type=str, 

250 default="github", 

251 ).lower() 

252 

253 while git_host not in GitHost._value2member_map_: 

254 logger.warning(f"Invalid choice: {git_host}. Please choose 'github' or 'gitlab'") 

255 git_host = typer.prompt( 

256 "Target Git hosting platform (github/gitlab)", 

257 type=str, 

258 default="github", 

259 ).lower() 

260 else: 

261 git_host = "github" 

262 logger.debug("Non-interactive mode detected, defaulting to github") 

263 

264 return GitHost(git_host) 

265 

266 

267def _prompt_template_repository() -> str | None: 

268 """Prompt the user to select a template repository from a list of rhiza-tagged repos. 

269 

270 Fetches repositories tagged with 'rhiza' from the GitHub API and presents 

271 them as a numbered list. In non-interactive or offline scenarios the function 

272 returns None so the caller falls back to the language default. 

273 

274 Returns: 

275 The selected repository in 'owner/repo' format, or None if the user 

276 accepts the default or selection is not possible. 

277 """ 

278 if not sys.stdin.isatty(): 

279 logger.debug("Non-interactive mode detected, skipping template repository selection") 

280 return None 

281 

282 try: 

283 repos = _fetch_repos() 

284 except urllib.error.URLError as exc: 

285 logger.debug(f"Could not fetch repository list: {exc}") 

286 return None 

287 

288 if not repos: 

289 return None 

290 

291 # Display a compact numbered list 

292 typer.echo("\nAvailable template repositories:") 

293 for i, repo in enumerate(repos, start=1): 

294 desc = repo.description[:_DESC_WIDTH] if repo.description else "" 

295 typer.echo(f" {i:>2} {repo.full_name:<30} {desc}") 

296 

297 typer.echo("") 

298 selection = typer.prompt( 

299 "Select a template repository by number", 

300 default="1", 

301 ).strip() 

302 

303 try: 

304 idx = int(selection) 

305 if 1 <= idx <= len(repos): 

306 chosen = repos[idx - 1].full_name 

307 logger.info(f"Selected template repository: {chosen}") 

308 return chosen 

309 else: 

310 logger.warning(f"Invalid selection '{idx}', using default repository") 

311 return None 

312 except ValueError: 

313 logger.warning(f"Invalid input '{selection}', using default repository") 

314 return None 

315 

316 

317def _get_default_profile_for_host(git_host: GitHost | str) -> str: 

318 """Return the profile name that matches the git hosting platform. 

319 

320 Args: 

321 git_host: Git hosting platform. 

322 

323 Returns: 

324 Profile name (e.g. ``"gitlab-project"`` or ``"github-project"``). 

325 """ 

326 if git_host == GitHost.GITLAB: 

327 return "gitlab-project" 

328 return "github-project" 

329 

330 

331def _display_path(path: Path, target: Path) -> Path: 

332 """Return *path* relative to *target* when possible, otherwise the absolute path. 

333 

334 Args: 

335 path: Path to display. 

336 target: Base directory used as the reference point. 

337 

338 Returns: 

339 A relative or absolute Path suitable for log messages. 

340 """ 

341 return path.relative_to(target) if path.is_relative_to(target) else path 

342 

343 

344def _create_template_file( 

345 target: Path, 

346 git_host: GitHost | str, 

347 language: str = "python", 

348 template_repository: str | None = None, 

349 template_branch: str | None = None, 

350 template_file: Path | None = None, 

351) -> None: 

352 """Create default template.yml file. 

353 

354 Args: 

355 target: Target repository path. 

356 git_host: Git hosting platform. 

357 language: Programming language for the project (default: python). 

358 template_repository: Custom template repository (format: owner/repo). 

359 template_branch: Custom template branch. 

360 template_file: Optional explicit path to write template.yml. When 

361 ``None`` the default ``<target>/.rhiza/template.yml`` is used. 

362 """ 

363 if template_file is None: 

364 rhiza_dir = target / ".rhiza" 

365 template_file = rhiza_dir / "template.yml" 

366 

367 if template_file.exists(): 

368 return 

369 

370 template_file.parent.mkdir(parents=True, exist_ok=True) 

371 logger.info(f"Creating default {_display_path(template_file, target)}") 

372 logger.debug("Using default template configuration") 

373 

374 # Use custom template repository/branch if provided, otherwise use language defaults 

375 if template_repository: 

376 repo = template_repository 

377 logger.info(f"Using custom template repository: {repo}") 

378 else: 

379 # Default repositories by language 

380 repo = "jebel-quant/rhiza-go" if language == "go" else "jebel-quant/rhiza" 

381 logger.debug(f"Using default repository for {language}: {repo}") 

382 

383 if template_branch: 

384 branch = template_branch 

385 logger.info(f"Using custom template branch: {branch}") 

386 else: 

387 latest = _get_latest_tag(repo, git_host) 

388 if latest: 

389 branch = latest 

390 logger.info(f"Using latest tag: {branch}") 

391 else: 

392 branch = "main" 

393 logger.warning("Could not determine latest tag, falling back to 'main'") 

394 

395 profile = _get_default_profile_for_host(git_host) 

396 logger.info(f"Using profile: {profile}") 

397 

398 jinja_src = importlib.resources.files("rhiza").joinpath("_templates/basic/template.yml.jinja2").read_text() 

399 rendered = Template(jinja_src, keep_trailing_newline=True).render( 

400 template_repository=repo, 

401 template_branch=branch, 

402 git_host=str(git_host), 

403 language=language, 

404 profile=profile, 

405 ) 

406 

407 logger.debug(f"Writing default template to: {template_file}") 

408 template_file.write_text(rendered) 

409 

410 logger.success(f"✓ Created {_display_path(template_file, target)}") 

411 logger.info(""" 

412Next steps: 

413 1. Review and customize .rhiza/template.yml to match your project needs 

414 2. Run 'uvx rhiza sync' to inject templates into your repository 

415""") 

416 

417 

418def _create_python_package(target: Path, project_name: str, package_name: str) -> None: 

419 """Create basic Python package structure. 

420 

421 Args: 

422 target: Target repository path. 

423 project_name: Project name. 

424 package_name: Package name. 

425 """ 

426 src_folder = target / "src" / package_name 

427 test_folder = target / "tests" 

428 

429 if (target / "src").exists(): 

430 return 

431 

432 logger.info(f"Creating Python package structure: {src_folder}") 

433 src_folder.mkdir(parents=True) 

434 

435 logger.info(f"Creating test folder: {test_folder}") 

436 test_folder.mkdir(parents=True) 

437 

438 # Create __init__.py 

439 init_file = src_folder / "__init__.py" 

440 logger.debug(f"Creating {init_file}") 

441 init_file.touch() 

442 

443 template_content = importlib.resources.files("rhiza").joinpath("_templates/basic/__init__.py.jinja2").read_text() 

444 template = Template(template_content, keep_trailing_newline=True) 

445 code = template.render(project_name=project_name) 

446 init_file.write_text(code) 

447 

448 # Create main.py 

449 main_file = src_folder / "main.py" 

450 logger.debug(f"Creating {main_file} with example code") 

451 main_file.touch() 

452 

453 template_content = importlib.resources.files("rhiza").joinpath("_templates/basic/main.py.jinja2").read_text() 

454 template = Template(template_content, keep_trailing_newline=True) 

455 code = template.render(project_name=project_name) 

456 main_file.write_text(code) 

457 logger.success(f"Created Python package structure in {src_folder}") 

458 

459 # Create main.py 

460 test_file = test_folder / "test_main.py" 

461 logger.debug(f"Creating {test_file} with example code") 

462 test_file.touch() 

463 

464 template_content = importlib.resources.files("rhiza").joinpath("_templates/basic/test_main.py.jinja2").read_text() 

465 template = Template(template_content, keep_trailing_newline=True) 

466 code = template.render(project_name=package_name) 

467 test_file.write_text(code) 

468 # logger.success(f"Created Python package structure in {src_folder}") 

469 

470 

471def _create_pyproject_toml( 

472 target: Path, 

473 project_name: str, 

474 package_name: str, 

475 with_dev_dependencies: bool, 

476 github_username: str = "your-org", 

477) -> None: 

478 """Create pyproject.toml file. 

479 

480 Args: 

481 target: Target repository path. 

482 project_name: Project name. 

483 package_name: Package name. 

484 with_dev_dependencies: Whether to include dev dependencies. 

485 github_username: GitHub/GitLab username or org extracted from the origin remote. 

486 """ 

487 pyproject_file = target / "pyproject.toml" 

488 if pyproject_file.exists(): 

489 return 

490 

491 logger.info("Creating pyproject.toml with basic project metadata") 

492 pyproject_file.touch() 

493 

494 template_content = importlib.resources.files("rhiza").joinpath("_templates/basic/pyproject.toml.jinja2").read_text() 

495 template = Template(template_content, keep_trailing_newline=True) 

496 code = template.render( 

497 project_name=project_name, 

498 package_name=package_name, 

499 with_dev_dependencies=with_dev_dependencies, 

500 github_username=github_username, 

501 ) 

502 pyproject_file.write_text(code) 

503 logger.success("Created pyproject.toml") 

504 

505 

506def _create_uv_lock(target: Path) -> None: 

507 """Run ``uv lock`` to generate the initial uv.lock file. 

508 

509 Args: 

510 target: Repository root directory. 

511 """ 

512 lock_file = target / "uv.lock" 

513 if lock_file.exists(): 

514 return 

515 

516 logger.info("Generating uv.lock") 

517 try: 

518 result = subprocess.run( # nosec B603 B607 

519 ["uv", "lock"], # noqa: S607 

520 cwd=target, 

521 capture_output=True, 

522 text=True, 

523 ) 

524 if result.returncode == 0: 

525 logger.success("Created uv.lock") 

526 else: 

527 logger.warning(f"uv lock failed (exit {result.returncode}): {result.stderr.strip()}") 

528 except (OSError, FileNotFoundError): 

529 logger.warning("uv not found — skipping uv.lock generation. Run 'uv lock' manually.") 

530 

531 

532def _create_makefile(target: Path) -> None: 

533 """Create a minimal Makefile that bootstraps ``make sync`` before rhiza.mk exists. 

534 

535 Args: 

536 target: Target repository path. 

537 """ 

538 makefile = target / "Makefile" 

539 if makefile.exists(): 

540 return 

541 

542 logger.info("Creating Makefile") 

543 template_content = importlib.resources.files("rhiza").joinpath("_templates/basic/Makefile.jinja2").read_text() 

544 makefile.write_text(Template(template_content, keep_trailing_newline=True).render()) 

545 logger.success("Created Makefile") 

546 

547 

548def _create_mkdocs_yml(target: Path, project_name: str, username: str, git_host: GitHost | None = None) -> None: 

549 """Create mkdocs.yml file. 

550 

551 Args: 

552 target: Target repository path. 

553 project_name: Project name. 

554 username: GitHub/GitLab username or org extracted from the origin remote. 

555 git_host: Git hosting platform; controls repo and pages URLs. 

556 """ 

557 mkdocs_file = target / "mkdocs.yml" 

558 if mkdocs_file.exists(): 

559 return 

560 

561 if git_host == GitHost.GITLAB: 

562 repo_host = "gitlab.com" 

563 pages_host = "gitlab.io" 

564 else: 

565 repo_host = "github.com" 

566 pages_host = "github.io" 

567 

568 logger.info("Creating mkdocs.yml") 

569 template_content = importlib.resources.files("rhiza").joinpath("_templates/basic/mkdocs.yml.jinja2").read_text() 

570 mkdocs_file.write_text( 

571 Template(template_content, keep_trailing_newline=True).render( 

572 project_name=project_name, 

573 username=username, 

574 repo_host=repo_host, 

575 pages_host=pages_host, 

576 ) 

577 ) 

578 logger.success("Created mkdocs.yml") 

579 

580 

581def _create_readme(target: Path) -> None: 

582 """Create README.md file. 

583 

584 Args: 

585 target: Target repository path. 

586 """ 

587 readme_file = target / "README.md" 

588 if readme_file.exists(): 

589 return 

590 

591 logger.info("Creating README.md") 

592 readme_file.touch() 

593 logger.success("Created README.md") 

594 

595 

596def init( 

597 target: Path, 

598 project_name: str | None = None, 

599 package_name: str | None = None, 

600 with_dev_dependencies: bool = False, 

601 git_host: str | None = None, 

602 language: str = "python", 

603 template_repository: str | None = None, 

604 template_branch: str | None = None, 

605 template_file: Path | None = None, 

606) -> bool: 

607 """Initialize or validate .rhiza/template.yml in the target repository. 

608 

609 Creates a default .rhiza/template.yml file if it doesn't exist, 

610 or validates an existing one. 

611 

612 Args: 

613 target: Path to the target directory. Defaults to the current working directory. 

614 project_name: Custom project name. Defaults to target directory name. 

615 package_name: Custom package name. Defaults to normalized project name. 

616 with_dev_dependencies: Include development dependencies in pyproject.toml. 

617 git_host: Target Git hosting platform ("github" or "gitlab"). Determines which 

618 CI/CD configuration files to include. If None, will prompt user interactively. 

619 language: Programming language for the project (default: python). 

620 Supported: python, go. Determines which project files to create. 

621 template_repository: Custom template repository (format: owner/repo). 

622 Defaults to 'jebel-quant/rhiza' for Python or 'jebel-quant/rhiza-go' for Go. 

623 template_branch: Custom template branch. Defaults to 'main'. 

624 template_file: Optional explicit path to write template.yml. When 

625 ``None`` the default ``<target>/.rhiza/template.yml`` is used. 

626 

627 Returns: 

628 bool: True if validation passes, False otherwise. 

629 """ 

630 target = target.resolve() 

631 git_host = _validate_git_host(git_host) 

632 

633 git_ctx = GitContext.default() 

634 result = subprocess.run( # nosec B603 # noqa: S603 

635 [git_ctx.executable, "rev-parse", "--git-dir"], 

636 capture_output=True, 

637 cwd=target, 

638 env=git_ctx.env, 

639 ) 

640 if result.returncode != 0: 

641 logger.error(f"{target} is not a git repository. Run 'git init' first.") 

642 return False 

643 

644 logger.info(f"Initializing Rhiza configuration in: {target}") 

645 logger.info(f"Project language: {language}") 

646 

647 # Create .rhiza directory (always; project structure lives there regardless of 

648 # where template.yml is placed) 

649 rhiza_dir = target / ".rhiza" 

650 logger.debug(f"Ensuring directory exists: {rhiza_dir}") 

651 rhiza_dir.mkdir(parents=True, exist_ok=True) 

652 

653 # Determine git host: explicit arg > remote URL detection > interactive prompt 

654 if git_host is None: 

655 git_host = _detect_git_host(target) 

656 if git_host is not None: 

657 logger.info(f"Detected git host from remote URL: {git_host}") 

658 else: 

659 git_host = _prompt_git_host() 

660 

661 # When no template repository is specified and no config file exists yet, 

662 # offer the user an interactive selection from discovered rhiza repos. 

663 resolved_template_file = template_file if template_file is not None else target / ".rhiza" / "template.yml" 

664 if template_repository is None and not resolved_template_file.exists(): 

665 template_repository = _prompt_template_repository() 

666 

667 # Validate template repository reachability early if a custom one is specified 

668 if template_repository is not None and not _check_template_repository_reachable(template_repository, git_host): 

669 return False 

670 

671 # Create template file with language 

672 _create_template_file(target, git_host, language, template_repository, template_branch, template_file) 

673 

674 # Bootstrap project structure based on language 

675 if language == "python": 

676 # Python-specific setup 

677 if project_name is None: 

678 project_name = target.name 

679 if package_name is None: 

680 package_name = _normalize_package_name(project_name) 

681 

682 logger.debug(f"Project name: {project_name}") 

683 logger.debug(f"Package name: {package_name}") 

684 

685 github_username = _get_github_username(target) 

686 _create_python_package(target, project_name, package_name) 

687 _create_pyproject_toml(target, project_name, package_name, with_dev_dependencies, github_username) 

688 _create_uv_lock(target) 

689 _create_makefile(target) 

690 _create_mkdocs_yml(target, project_name, github_username, git_host) 

691 _create_readme(target) 

692 elif language == "go": 

693 # Go-specific setup - just create README, user should run go mod init 

694 _create_readme(target) 

695 logger.info("For Go projects, run 'go mod init <module-name>' to initialize the module") 

696 else: 

697 # Unknown language - just create README 

698 logger.warning(f"Unknown language '{language}', creating minimal structure") 

699 _create_readme(target) 

700 

701 # Validate the template file 

702 logger.debug("Validating template configuration") 

703 return validate(target, template_file=template_file)