diff --git a/bin/commands/webpage.py b/bin/commands/webpage.py index 32d1c57..572cd2e 100644 --- a/bin/commands/webpage.py +++ b/bin/commands/webpage.py @@ -1,11 +1,14 @@ -"""Webpage project utilities (Docusaurus build / clear / etc.). +"""Webpage project utilities (Docusaurus build / clear / deploy / pdf / etc.). Project metadata lives in `server.py` so that adding a new webpage there automatically wires it up here. """ +import re +import shutil import subprocess import time +from pathlib import Path import click @@ -67,13 +70,6 @@ def webpage_list(): click.echo("") -@webpage_cmd.command("build") -@click.argument("name", type=click.Choice(_NAMES, case_sensitive=False)) -def webpage_build(name): - """Build a webpage project (`npm run build`).""" - _run_npm(name, "build", "Building") - - @webpage_cmd.command("clear") @click.argument("name", type=click.Choice(_NAMES, case_sensitive=False)) def webpage_clear(name): @@ -87,3 +83,177 @@ def webpage_rebuild(name): """Clear cache then build (`npm run clear && npm run build`).""" _run_npm(name, "clear", "Clearing") _run_npm(name, "build", "Building") + + +@webpage_cmd.command("build") +@click.argument("name", type=click.Choice(_NAMES, case_sensitive=False)) +@click.option("--serve", "-s", is_flag=True, help="Serve the built site after successful build") +@click.option("--port", "-p", type=int, help="Port for serve (default: Docusaurus default)") +def webpage_build_serve(name, serve, port): + """Build a webpage project (`npm run build`). Use --serve to start serving.""" + svc = get_service(name) + assert svc.project_dir is not None + + _run_npm(name, "build", "Building") + + if serve: + click.secho(f"Serving {svc.name} ...", fg="cyan") + cmd = ["npx", "docusaurus", "serve", "--no-open"] + if port is not None: + cmd += ["--port", str(port)] + proc = subprocess.run(cmd, cwd=svc.project_dir, env=no_color_env()) + if proc.returncode != 0: + click.secho(f"Serve failed for {svc.name}", fg="red") + raise click.Abort() + + +@webpage_cmd.command("deploy") +@click.argument("name", type=click.Choice(_NAMES, case_sensitive=False)) +@click.argument("deploy_args", nargs=-1) +def webpage_deploy(name, deploy_args): + """Deploy a webpage project to GitHub Pages (`npm run deploy`). + + Automatically infers GIT_USER from `gh` CLI or git origin URL. + """ + svc = get_service(name) + assert svc.project_dir is not None + + if not svc.project_dir.exists(): + click.secho(f"Project directory not found: {svc.project_dir}", fg="red") + raise click.Abort() + + env = no_color_env() + + # Infer GIT_USER if not already set + if not env.get("GIT_USER"): + use_ssh = env.get("USE_SSH", "").lower() == "true" + if not use_ssh: + login = "" + # Try gh CLI + gh_path = shutil.which("gh") + if gh_path: + try: + result = subprocess.run( + [gh_path, "api", "user", "--jq", ".login"], + capture_output=True, + text=True, + timeout=5, + ) + if result.returncode == 0: + login = result.stdout.strip() + except (subprocess.TimeoutExpired, FileNotFoundError): + pass + + # Fallback to git origin URL + if not login: + try: + result = subprocess.run( + ["git", "-C", str(svc.project_dir), "remote", "get-url", "origin"], + capture_output=True, + text=True, + timeout=5, + ) + if result.returncode == 0: + origin = result.stdout.strip() + m = re.search(r"github\.com[:/]([^/]+)/", origin) + if m: + login = m.group(1) + except (subprocess.TimeoutExpired, FileNotFoundError): + pass + + if login: + env["GIT_USER"] = login + click.secho(f"Inferred GIT_USER={login}", fg="bright_black") + + click.secho(f"Deploying {svc.name} ({svc.description}) ...", fg="cyan") + cmd = ["npm", "run", "deploy"] + if deploy_args: + cmd.append("--") + cmd.extend(deploy_args) + proc = subprocess.run(cmd, cwd=svc.project_dir, env=env) + if proc.returncode != 0: + click.secho(f"Deploy failed for {svc.name}", fg="red") + raise click.Abort() + click.secho(f"{svc.name} deployed.", fg="green") + + +# --------------------------------------------------------------------------- +# PDF subcommand +# --------------------------------------------------------------------------- + +@click.group(name="pdf", invoke_without_command=False) +def pdf_cmd(): + """Export docs to PDF or open existing PDF.""" + pass + + +@pdf_cmd.command("build") +@click.argument("name", type=click.Choice(_NAMES, case_sensitive=False)) +@click.option("-o", "--output", help="Output PDF path") +@click.option("-t", "--title", help="Cover page title") +@click.option("-s", "--subtitle", help="Cover page subtitle") +@click.option("-d", "--date", help="Cover page date") +@click.option("-l", "--lang", type=click.Choice(["zh", "en", "both"]), help="Language (mywebpage only)") +def pdf_build(name, output, title, subtitle, date, lang): + """Export docs to PDF via project's scripts/export-docs-pdf.py.""" + svc = get_service(name) + assert svc.project_dir is not None + + script = svc.project_dir / "scripts" / "export-docs-pdf.py" + if not script.exists(): + click.secho(f"PDF export script not found: {script}", fg="red") + click.secho("Expected: scripts/export-docs-pdf.py in the project directory.", fg="yellow") + raise click.Abort() + + cmd = ["python3", str(script)] + if output: + cmd += ["--output", output] + if title: + cmd += ["--title", title] + if subtitle: + cmd += ["--subtitle", subtitle] + if date: + cmd += ["--date", date] + if lang: + cmd += ["--lang", lang] + + click.secho(f"Exporting PDF for {svc.name} ...", fg="cyan") + proc = subprocess.run(cmd, cwd=svc.project_dir, env=no_color_env()) + if proc.returncode != 0: + click.secho(f"PDF export failed for {svc.name}", fg="red") + raise click.Abort() + click.secho(f"{svc.name} PDF exported.", fg="green") + + +@pdf_cmd.command("open") +@click.argument("name", type=click.Choice(_NAMES, case_sensitive=False)) +@click.option("-o", "--output", help="PDF path to open") +def pdf_open(name, output): + """Open the exported PDF with the default application.""" + svc = get_service(name) + assert svc.project_dir is not None + + if output: + pdf_path = svc.project_dir / output + else: + # Try to infer default path from the export script's default + script = svc.project_dir / "scripts" / "export-docs-pdf.py" + pdf_path = None + if script.exists(): + content = script.read_text(encoding="utf-8") + m = re.search(r'(?:DEFAULT_OUTPUT|default)\s*=\s*["\']([^"\']+)["\']', content) + if m: + pdf_path = svc.project_dir / m.group(1) + if pdf_path is None: + pdf_path = svc.project_dir / "static" / "pdf" / f"{svc.name}-docs.pdf" + + if not pdf_path.exists(): + click.secho(f"PDF not found: {pdf_path}", fg="red") + click.secho("Run 'mytoolkit webpage pdf build ' first.", fg="yellow") + raise click.Abort() + + click.secho(f"Opening {pdf_path} ...", fg="cyan") + subprocess.run(["open", str(pdf_path)], check=False) + + +webpage_cmd.add_command(pdf_cmd)