diff --git a/site/.vitepress/config.mts b/site/.vitepress/config.mts index 3ccef1f..6d84b53 100644 --- a/site/.vitepress/config.mts +++ b/site/.vitepress/config.mts @@ -14,6 +14,9 @@ type Labels = { mcp: string guides: string recipes: string + sharing: string + changes: string + debugging: string security: string project: string architecture: string @@ -21,35 +24,44 @@ type Labels = { contributing: string } +// Guides are sections of the recipes page; the Korean headings carry the same ASCII ids. function sidebar(prefix: string, t: Labels): DefaultTheme.Sidebar { return [ { text: t.start, items: [ - { text: t.gettingStarted, link: `${prefix}/getting-started` }, { text: t.installation, link: `${prefix}/installation` }, + { text: t.gettingStarted, link: `${prefix}/getting-started` }, ], }, { - text: t.reference, + text: t.guides, items: [ - { text: t.commands, link: `${prefix}/commands` }, - { text: t.configuration, link: `${prefix}/configuration` }, - { text: t.outputFormats, link: `${prefix}/output-formats` }, - { text: t.terminalCapture, link: `${prefix}/terminal-capture` }, + { + text: t.recipes, + link: `${prefix}/recipes`, + items: [ + { text: t.sharing, link: `${prefix}/recipes#share-one-folder-or-one-question-s-worth-of-files` }, + { text: t.changes, link: `${prefix}/recipes#review-a-refactor-with-mark-and-semdiff` }, + { text: t.debugging, link: `${prefix}/recipes#a-test-fails-and-you-do-not-know-why` }, + ], + }, { text: t.mcp, link: `${prefix}/mcp` }, ], }, { - text: t.guides, + text: t.reference, items: [ - { text: t.recipes, link: `${prefix}/recipes` }, - { text: t.security, link: `${prefix}/security` }, + { text: t.commands, link: `${prefix}/commands` }, + { text: t.configuration, link: `${prefix}/configuration` }, + { text: t.outputFormats, link: `${prefix}/output-formats` }, + { text: t.terminalCapture, link: `${prefix}/terminal-capture` }, ], }, { text: t.project, items: [ + { text: t.security, link: `${prefix}/security` }, { text: t.architecture, link: `${prefix}/architecture` }, { text: t.rustPort, link: `${prefix}/rust-port` }, { text: t.contributing, link: `${prefix}/contributing` }, @@ -59,8 +71,8 @@ function sidebar(prefix: string, t: Labels): DefaultTheme.Sidebar { } const en: Labels = { - start: 'Start', - gettingStarted: 'Getting Started', + start: 'Getting Started', + gettingStarted: 'Quick Start', installation: 'Installation', reference: 'Reference', commands: 'Command Reference', @@ -70,10 +82,13 @@ const en: Labels = { mcp: 'MCP Integration', guides: 'Guides', recipes: 'Recipes', + sharing: 'Sharing code with an AI', + changes: 'Working with changes', + debugging: 'Debugging a failed command', security: 'Security', project: 'Project', architecture: 'Architecture', - rustPort: 'Rust Port', + rustPort: 'Rust and Python Builds', contributing: 'Contributing', } @@ -89,10 +104,13 @@ const ko: Labels = { mcp: 'MCP 연동', guides: '가이드', recipes: '활용 예시', + sharing: 'AI에게 코드 보여 주기', + changes: '바뀐 부분 다루기', + debugging: '실패한 명령 디버깅', security: '보안', project: '프로젝트', architecture: '구조', - rustPort: 'Rust 포팅', + rustPort: 'Rust와 Python 빌드', contributing: '기여하기', } @@ -111,18 +129,19 @@ export default defineConfig({ 'Share the code context you choose with an AI: your project, a few files, what changed, or the command that failed.', themeConfig: { nav: [ - { text: 'Getting Started', link: '/getting-started', activeMatch: '/(getting-started|installation)' }, - { text: 'Commands', link: '/commands' }, - { text: 'Recipes', link: '/recipes' }, + { text: 'Getting Started', link: '/installation', activeMatch: '^/(getting-started|installation)' }, + { text: 'Guides', link: '/recipes', activeMatch: '^/(recipes|mcp)' }, + { + text: 'Reference', + link: '/commands', + activeMatch: '^/(commands|configuration|output-formats|terminal-capture)', + }, { text: 'Security', link: '/security' }, { text: 'More', items: [ - { text: 'Configuration', link: '/configuration' }, - { text: 'Terminal Capture', link: '/terminal-capture' }, - { text: 'MCP Integration', link: '/mcp' }, { text: 'Architecture', link: '/architecture' }, - { text: 'Rust Port', link: '/rust-port' }, + { text: 'Rust and Python Builds', link: '/rust-port' }, { text: 'Contributing', link: '/contributing' }, ], }, @@ -143,18 +162,19 @@ export default defineConfig({ 'AI에게 보여 줄 코드를 내가 고르게 해 주는 도구. 프로젝트 전체든, 파일 몇 개든, 바뀐 부분이든, 실패한 명령이든.', themeConfig: { nav: [ - { text: '빠른 시작', link: '/ko/getting-started', activeMatch: '/ko/(getting-started|installation)' }, - { text: '명령어', link: '/ko/commands' }, - { text: '활용 예시', link: '/ko/recipes' }, + { text: '시작하기', link: '/ko/installation', activeMatch: '^/ko/(getting-started|installation)' }, + { text: '가이드', link: '/ko/recipes', activeMatch: '^/ko/(recipes|mcp)' }, + { + text: '레퍼런스', + link: '/ko/commands', + activeMatch: '^/ko/(commands|configuration|output-formats|terminal-capture)', + }, { text: '보안', link: '/ko/security' }, { text: '더 보기', items: [ - { text: '설정', link: '/ko/configuration' }, - { text: '터미널 기록', link: '/ko/terminal-capture' }, - { text: 'MCP 연동', link: '/ko/mcp' }, { text: '구조', link: '/ko/architecture' }, - { text: 'Rust 포팅', link: '/ko/rust-port' }, + { text: 'Rust와 Python 빌드', link: '/ko/rust-port' }, { text: '기여하기', link: '/ko/contributing' }, ], }, diff --git a/site/scripts/check_reference.py b/site/scripts/check_reference.py index 45d5617..20f7faf 100644 --- a/site/scripts/check_reference.py +++ b/site/scripts/check_reference.py @@ -3,12 +3,19 @@ Each command has a heading such as ## `cdg scan` or ### `cdg term install`, and its options are listed in a table whose first cell holds the option names in backticks. -The check compares those names with what Typer registers, in both directions, for the -English page and its Korean translation. +An option that only the prebuilt binary (the Rust build) has gets a heading of its own +under the command instead of a table row, such as #### `--raw`, only in the binary. - uv run python site/scripts/check_reference.py +The check compares the tables with what Typer registers, in both directions, for the +English page and its Korean translation. With --rust-json it also compares the tables +plus the binary-only headings with the Rust build, read from the "rust" key of +docs/cli-matrix.json. + + uv run python site/scripts/check_reference.py [--rust-json docs/cli-matrix.json] """ +import argparse +import json import re import sys from pathlib import Path @@ -22,6 +29,7 @@ PAGES = [SITE / "commands.md", SITE / "ko" / "commands.md"] HEADING = re.compile(r"^#{2,4}\s+`cdg ([a-z][a-z -]*)`\s*$") +BINARY_ONLY = re.compile(r"^####\s") OPTION = re.compile(r"(? None: return found -def documented_options(page: Path) -> dict[str, set[str]]: - found: dict[str, set[str]] = {} +def documented_options(page: Path) -> tuple[dict[str, set[str]], dict[str, set[str]]]: + """Options in each command's table, and options under its binary-only headings.""" + table: dict[str, set[str]] = {} + binary: dict[str, set[str]] = {} current: str | None = None for line in page.read_text(encoding="utf-8").splitlines(): if m := HEADING.match(line): current = m[1].strip() - found.setdefault(current, set()) + table.setdefault(current, set()) + binary.setdefault(current, set()) + elif current and BINARY_ONLY.match(line): + for span in re.findall(r"`([^`]+)`", line): + binary[current].update(OPTION.findall(span)) elif current and line.startswith("|"): first_cell = line.split("|")[1] for span in re.findall(r"`([^`]+)`", first_cell): - found[current].update(OPTION.findall(span)) - return found + table[current].update(OPTION.findall(span)) + return table, binary + + +def compare( + label: str, build: str, docs: dict[str, set[str]], actual: dict[str, set[str]] +) -> list[str]: + problems = [] + for command in sorted(actual.keys() - docs.keys()): + problems.append(f"{label}: command `cdg {command}` is not documented") + for command in sorted(docs.keys() - actual.keys()): + problems.append( + f"{label}: `cdg {command}` is documented but the {build} has no such command" + ) + for command in sorted(actual.keys() & docs.keys()): + for option in sorted(actual[command] - docs[command]): + problems.append( + f"{label}: `cdg {command}` option {option} of the {build} is not documented" + ) + for option in sorted(docs[command] - actual[command]): + problems.append( + f"{label}: `cdg {command}` documents {option}, which the {build} does not have" + ) + return problems def main() -> int: - actual = cli_options() + parser = argparse.ArgumentParser( + description="Fail when the command reference and the CLI disagree." + ) + parser.add_argument( + "--rust-json", type=Path, help="docs/cli-matrix.json, to check the Rust build too" + ) + args = parser.parse_args() + + python = cli_options() + rust = ( + { + k: set(v) - IGNORED + for k, v in json.loads(args.rust_json.read_text(encoding="utf-8"))["rust"].items() + } + if args.rust_json + else None + ) problems: list[str] = [] for page in PAGES: - docs = documented_options(page) + table, binary = documented_options(page) label = page.relative_to(SITE).as_posix() - for command in sorted(actual.keys() - docs.keys()): - problems.append(f"{label}: command `cdg {command}` is not documented") - for command in sorted(docs.keys() - actual.keys()): - problems.append( - f"{label}: `cdg {command}` is documented but the CLI has no such command" - ) - for command in sorted(actual.keys() & docs.keys()): - for option in sorted(actual[command] - docs[command]): - problems.append(f"{label}: `cdg {command}` option {option} is not documented") - for option in sorted(docs[command] - actual[command]): + problems += compare(label, "Python build", table, python) + for command, options in binary.items(): + for option in sorted(options & python.get(command, set())): problems.append( - f"{label}: `cdg {command}` documents {option}, which the CLI does not have" + f"{label}: `cdg {command}` {option} is marked binary-only but the Python build has it" ) + if rust is not None: + both = {command: table[command] | binary[command] for command in table} + problems += compare(label, "Rust build", both, rust) for problem in problems: print(problem, file=sys.stderr) if not problems: - print( - f"reference matches the CLI: {len(actual)} commands, {sum(map(len, actual.values()))} options" - ) + counted = f"{len(python)} commands, {sum(map(len, python.values()))} options" + if rust is not None: + counted += f"; Rust build: {sum(map(len, rust.values()))} options" + print(f"reference matches the CLI: {counted}") return 1 if problems else 0