refactor(webpage): swap project/action order to webpage <project> <action>
Each managed project becomes its own click subgroup, so commands now read as `mytoolkit webpage mywebpage build` instead of `mytoolkit webpage build mywebpage`. List remains project-agnostic.
This commit is contained in:
+135
-108
@@ -2,13 +2,15 @@
|
||||
|
||||
Project metadata lives in `server.py` so that adding a new webpage there
|
||||
automatically wires it up here.
|
||||
|
||||
Command layout: `mytoolkit webpage <project> <action> [options]`
|
||||
e.g. `mytoolkit webpage mywebpage build --serve`.
|
||||
"""
|
||||
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import click
|
||||
|
||||
@@ -16,13 +18,12 @@ from bin.commands.server import manageable_services, get_service, no_color_env
|
||||
|
||||
|
||||
_PROJECTS = manageable_services()
|
||||
_NAMES = [s.name for s in _PROJECTS]
|
||||
|
||||
|
||||
def _run_npm(name: str, script: str, label: str) -> None:
|
||||
"""Run `npm run <script>` in the project's directory, streaming output."""
|
||||
svc = get_service(name)
|
||||
assert svc.project_dir is not None # _NAMES is manageable services only
|
||||
assert svc.project_dir is not None # _PROJECTS is manageable services only
|
||||
|
||||
if not svc.project_dir.exists():
|
||||
click.secho(f"Project directory not found: {svc.project_dir}", fg="red")
|
||||
@@ -38,59 +39,11 @@ def _run_npm(name: str, script: str, label: str) -> None:
|
||||
click.secho(f"{svc.name} {script} done in {elapsed:.1f}s", fg="green")
|
||||
|
||||
|
||||
@click.group(name="webpage")
|
||||
def webpage_cmd():
|
||||
"""Manage Docusaurus webpage projects."""
|
||||
pass
|
||||
# ---------------------------------------------------------------------------
|
||||
# Action implementations (per-project, called from project subgroup commands)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@webpage_cmd.command("list")
|
||||
def webpage_list():
|
||||
"""List managed webpage projects and their build status."""
|
||||
click.echo("")
|
||||
click.secho(
|
||||
f"{'Name':<16} {'Build':<12} {'Description':<36} {'Project'}",
|
||||
fg="bright_blue",
|
||||
bold=True,
|
||||
)
|
||||
click.echo("-" * 100)
|
||||
for svc in _PROJECTS:
|
||||
assert svc.project_dir is not None # _PROJECTS is manageable services only
|
||||
build_dir = svc.project_dir / "build"
|
||||
if build_dir.exists():
|
||||
build_status = time.strftime("%m-%d %H:%M", time.localtime(build_dir.stat().st_mtime))
|
||||
color = "green"
|
||||
else:
|
||||
build_status = "missing"
|
||||
color = "yellow"
|
||||
click.secho(
|
||||
f"{svc.name:<16} {build_status:<12} {svc.description:<36} {svc.project_dir}",
|
||||
fg=color,
|
||||
)
|
||||
click.echo("")
|
||||
|
||||
|
||||
@webpage_cmd.command("clear")
|
||||
@click.argument("name", type=click.Choice(_NAMES, case_sensitive=False))
|
||||
def webpage_clear(name):
|
||||
"""Clear Docusaurus build artifacts and cache (`npm run clear`)."""
|
||||
_run_npm(name, "clear", "Clearing")
|
||||
|
||||
|
||||
@webpage_cmd.command("rebuild")
|
||||
@click.argument("name", type=click.Choice(_NAMES, case_sensitive=False))
|
||||
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."""
|
||||
def _build_serve_impl(name: str, serve: bool, port: int | None) -> None:
|
||||
svc = get_service(name)
|
||||
assert svc.project_dir is not None
|
||||
|
||||
@@ -107,21 +60,7 @@ def webpage_build_serve(name, serve, port):
|
||||
raise click.Abort()
|
||||
|
||||
|
||||
@webpage_cmd.command("push")
|
||||
@click.argument("name", type=click.Choice(_NAMES, case_sensitive=False))
|
||||
@click.option(
|
||||
"-f",
|
||||
"--force",
|
||||
is_flag=True,
|
||||
help="Force push via --force-with-lease (refuses if upstream moved unexpectedly).",
|
||||
)
|
||||
@click.argument("push_args", nargs=-1)
|
||||
def webpage_push(name, force, push_args):
|
||||
"""Push the webpage project's source repo (`git push`).
|
||||
|
||||
Default pushes the current branch to its tracked upstream. Extra
|
||||
arguments are forwarded to `git push` (e.g. `origin main`).
|
||||
"""
|
||||
def _push_impl(name: str, force: bool, push_args: tuple[str, ...]) -> None:
|
||||
svc = get_service(name)
|
||||
assert svc.project_dir is not None
|
||||
|
||||
@@ -143,14 +82,7 @@ def webpage_push(name, force, push_args):
|
||||
click.secho(f"{svc.name} pushed.", fg="green")
|
||||
|
||||
|
||||
@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.
|
||||
"""
|
||||
def _deploy_impl(name: str, deploy_args: tuple[str, ...]) -> None:
|
||||
svc = get_service(name)
|
||||
assert svc.project_dir is not None
|
||||
|
||||
@@ -160,12 +92,10 @@ def webpage_deploy(name, deploy_args):
|
||||
|
||||
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:
|
||||
@@ -180,7 +110,6 @@ def webpage_deploy(name, deploy_args):
|
||||
except (subprocess.TimeoutExpired, FileNotFoundError):
|
||||
pass
|
||||
|
||||
# Fallback to git origin URL
|
||||
if not login:
|
||||
try:
|
||||
result = subprocess.run(
|
||||
@@ -213,25 +142,7 @@ def webpage_deploy(name, deploy_args):
|
||||
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."""
|
||||
def _pdf_build_impl(name, output, title, subtitle, date, lang) -> None:
|
||||
svc = get_service(name)
|
||||
assert svc.project_dir is not None
|
||||
|
||||
@@ -261,18 +172,13 @@ def pdf_build(name, output, title, subtitle, date, lang):
|
||||
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."""
|
||||
def _pdf_open_impl(name, output) -> None:
|
||||
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():
|
||||
@@ -285,11 +191,132 @@ def pdf_open(name, output):
|
||||
|
||||
if not pdf_path.exists():
|
||||
click.secho(f"PDF not found: {pdf_path}", fg="red")
|
||||
click.secho("Run 'mytoolkit webpage pdf build <name>' first.", fg="yellow")
|
||||
click.secho(f"Run 'mytoolkit webpage {svc.name} 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)
|
||||
# ---------------------------------------------------------------------------
|
||||
# Top-level group + project subgroup factory
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@click.group(name="webpage")
|
||||
def webpage_cmd():
|
||||
"""Manage Docusaurus webpage projects.
|
||||
|
||||
Usage: `mytoolkit webpage <project> <action> [options]`
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
@webpage_cmd.command("list")
|
||||
def webpage_list():
|
||||
"""List managed webpage projects and their build status."""
|
||||
click.echo("")
|
||||
click.secho(
|
||||
f"{'Name':<16} {'Build':<12} {'Description':<36} {'Project'}",
|
||||
fg="bright_blue",
|
||||
bold=True,
|
||||
)
|
||||
click.echo("-" * 100)
|
||||
for svc in _PROJECTS:
|
||||
assert svc.project_dir is not None
|
||||
build_dir = svc.project_dir / "build"
|
||||
if build_dir.exists():
|
||||
build_status = time.strftime("%m-%d %H:%M", time.localtime(build_dir.stat().st_mtime))
|
||||
color = "green"
|
||||
else:
|
||||
build_status = "missing"
|
||||
color = "yellow"
|
||||
click.secho(
|
||||
f"{svc.name:<16} {build_status:<12} {svc.description:<36} {svc.project_dir}",
|
||||
fg=color,
|
||||
)
|
||||
click.echo("")
|
||||
|
||||
|
||||
def _make_project_group(name: str, description: str) -> click.Group:
|
||||
"""Build a click subgroup for a single webpage project."""
|
||||
|
||||
@click.group(name=name, help=f"{description}.")
|
||||
def project_group():
|
||||
pass
|
||||
|
||||
@project_group.command("clear")
|
||||
def clear_cmd():
|
||||
"""Clear Docusaurus build artifacts and cache (`npm run clear`)."""
|
||||
_run_npm(name, "clear", "Clearing")
|
||||
|
||||
@project_group.command("rebuild")
|
||||
def rebuild_cmd():
|
||||
"""Clear cache then build (`npm run clear && npm run build`)."""
|
||||
_run_npm(name, "clear", "Clearing")
|
||||
_run_npm(name, "build", "Building")
|
||||
|
||||
@project_group.command("build")
|
||||
@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 build_cmd(serve, port):
|
||||
"""Build the project (`npm run build`). Use --serve to start serving."""
|
||||
_build_serve_impl(name, serve, port)
|
||||
|
||||
@project_group.command("push", context_settings={"ignore_unknown_options": True})
|
||||
@click.option(
|
||||
"-f",
|
||||
"--force",
|
||||
is_flag=True,
|
||||
help="Force push via --force-with-lease (refuses if upstream moved unexpectedly).",
|
||||
)
|
||||
@click.argument("push_args", nargs=-1, type=click.UNPROCESSED)
|
||||
def push_cmd(force, push_args):
|
||||
"""Push the project's source repo (`git push`).
|
||||
|
||||
Default pushes the current branch to its tracked upstream. Extra
|
||||
arguments are forwarded to `git push` (e.g. `origin main`).
|
||||
"""
|
||||
_push_impl(name, force, push_args)
|
||||
|
||||
@project_group.command("deploy", context_settings={"ignore_unknown_options": True})
|
||||
@click.argument("deploy_args", nargs=-1, type=click.UNPROCESSED)
|
||||
def deploy_cmd(deploy_args):
|
||||
"""Deploy to GitHub Pages (`npm run deploy`).
|
||||
|
||||
Automatically infers GIT_USER from `gh` CLI or git origin URL.
|
||||
"""
|
||||
_deploy_impl(name, deploy_args)
|
||||
|
||||
@project_group.group(name="pdf")
|
||||
def pdf_group():
|
||||
"""Export docs to PDF or open existing PDF."""
|
||||
pass
|
||||
|
||||
@pdf_group.command("build")
|
||||
@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_cmd(output, title, subtitle, date, lang):
|
||||
"""Export docs to PDF via project's scripts/export-docs-pdf.py."""
|
||||
_pdf_build_impl(name, output, title, subtitle, date, lang)
|
||||
|
||||
@pdf_group.command("open")
|
||||
@click.option("-o", "--output", help="PDF path to open")
|
||||
def pdf_open_cmd(output):
|
||||
"""Open the exported PDF with the default application."""
|
||||
_pdf_open_impl(name, output)
|
||||
|
||||
return project_group
|
||||
|
||||
|
||||
# Register one subgroup per managed project. Each project becomes
|
||||
# `mytoolkit webpage <project>` with its own action subcommands.
|
||||
for _svc in _PROJECTS:
|
||||
webpage_cmd.add_command(_make_project_group(_svc.name, _svc.description))
|
||||
|
||||
Reference in New Issue
Block a user