Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 47 additions & 27 deletions site/.vitepress/config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -14,42 +14,54 @@ type Labels = {
mcp: string
guides: string
recipes: string
sharing: string
changes: string
debugging: string
security: string
project: string
architecture: string
rustPort: string
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` },
Expand All @@ -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',
Expand All @@ -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',
}

Expand All @@ -89,10 +104,13 @@ const ko: Labels = {
mcp: 'MCP 연동',
guides: '가이드',
recipes: '활용 예시',
sharing: 'AI에게 코드 보여 주기',
changes: '바뀐 부분 다루기',
debugging: '실패한 명령 디버깅',
security: '보안',
project: '프로젝트',
architecture: '구조',
rustPort: 'Rust 포팅',
rustPort: 'Rust와 Python 빌드',
contributing: '기여하기',
}

Expand All @@ -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' },
],
},
Expand All @@ -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' },
],
},
Expand Down
97 changes: 73 additions & 24 deletions site/scripts/check_reference.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"(?<![\w-])(--?[A-Za-z][\w-]*)")
IGNORED = {"--help"}

Expand All @@ -47,45 +55,86 @@ def walk(command: click.Command, path: list[str]) -> 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


Expand Down
Loading