From 53c77cabf80530e483450c07b3ce974ead5dcb7e Mon Sep 17 00:00:00 2001 From: Evan Date: Thu, 3 Sep 2026 22:51:22 +0800 Subject: [PATCH 1/2] fix: quote zsh pip extras in install docs --- Makefile | 11 +++- README.ja.md | 1 + README.ko.md | 1 + README.md | 5 +- README.zh-CN.md | 1 + scripts/check_zsh_extras.py | 117 ++++++++++++++++++++++++++++++++++++ 6 files changed, 131 insertions(+), 5 deletions(-) create mode 100644 scripts/check_zsh_extras.py diff --git a/Makefile b/Makefile index d144963..21ba393 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: install run clean lint test check docs-check benchmark docker-smoke git-status git-diff git-log web +.PHONY: install run clean lint test check docs-check shell-check benchmark docker-smoke git-status git-diff git-log web # Prefer the known-stable Python 3.12, but use the active supported Python # 3.13 on clean runners that do not provide 3.12. Python 3.14 remains @@ -49,7 +49,7 @@ clean: # Syntax check lint: - $(VENV_PYTHON) -m compileall -q main.py main_debug.py server.py tools.py memory.py event_store.py task_engine.py learning_engine.py learning_benchmark.py migration.py scheduler.py skill_registry.py browser_manager.py instruction_loader.py agent_config.py subagents.py capability_tokens.py tool_registry.py dynamic_tools.py plugin_runtime.py scripts/generate_tool_inventory.py scripts/check_docs.py + $(VENV_PYTHON) -m compileall -q main.py main_debug.py server.py tools.py memory.py event_store.py task_engine.py learning_engine.py learning_benchmark.py migration.py scheduler.py skill_registry.py browser_manager.py instruction_loader.py agent_config.py subagents.py capability_tokens.py tool_registry.py dynamic_tools.py plugin_runtime.py scripts/generate_tool_inventory.py scripts/check_docs.py scripts/check_zsh_extras.py @echo "Python syntax OK." @echo "All files pass syntax check." @@ -71,6 +71,10 @@ benchmark: docs-check: $(VENV_PYTHON) scripts/generate_tool_inventory.py --check $(VENV_PYTHON) scripts/check_docs.py + $(VENV_PYTHON) scripts/check_zsh_extras.py + +shell-check: + $(VENV_PYTHON) scripts/check_zsh_extras.py docker-smoke: @command -v docker >/dev/null 2>&1 || { echo "Error: Docker is required for the container smoke test."; exit 1; } @@ -80,10 +84,11 @@ docker-smoke: # Quick verification check: @echo "Checking Python syntax..." - @$(VENV_PYTHON) -m py_compile main.py main_debug.py server.py tools.py memory.py event_store.py task_engine.py learning_engine.py learning_benchmark.py migration.py scheduler.py skill_registry.py browser_manager.py instruction_loader.py agent_config.py subagents.py capability_tokens.py tool_registry.py dynamic_tools.py plugin_runtime.py scripts/generate_tool_inventory.py scripts/check_docs.py + @$(VENV_PYTHON) -m py_compile main.py main_debug.py server.py tools.py memory.py event_store.py task_engine.py learning_engine.py learning_benchmark.py migration.py scheduler.py skill_registry.py browser_manager.py instruction_loader.py agent_config.py subagents.py capability_tokens.py tool_registry.py dynamic_tools.py plugin_runtime.py scripts/generate_tool_inventory.py scripts/check_docs.py scripts/check_zsh_extras.py @echo " Python modules: OK" @echo "Checking git tools..." @$(VENV_PYTHON) -c "from tools import AVAILABLE_TOOLS; git = [k for k in AVAILABLE_TOOLS if k.startswith('git_')]; print(f' {len(git)} git tools, {len(AVAILABLE_TOOLS)} total tools')" + @$(VENV_PYTHON) scripts/check_zsh_extras.py @echo "All checks passed." # Git helpers diff --git a/README.ja.md b/README.ja.md index a400222..76917b8 100644 --- a/README.ja.md +++ b/README.ja.md @@ -559,6 +559,7 @@ def register(): # クイック検証 make check make docs-check +make shell-check # 構文チェックのみ make lint diff --git a/README.ko.md b/README.ko.md index 9730c5b..d013fa9 100644 --- a/README.ko.md +++ b/README.ko.md @@ -559,6 +559,7 @@ def register(): # 빠른 검증 make check make docs-check +make shell-check # 구문 검사만 make lint diff --git a/README.md b/README.md index 04e41c4..2585e39 100644 --- a/README.md +++ b/README.md @@ -787,6 +787,7 @@ The file is auto-managed. Use `/provider` or `/api_key` in-chat to update it int # Quick verification make check make docs-check +make shell-check # Syntax lint only make lint @@ -826,8 +827,8 @@ GitHub Actions automatically runs on every push and PR: ```bash # Install from local directory (PyPI publishing coming soon) pip install . # core + CLI -pip install .[web] # + web UI -pip install .[all] # + Claude + Gemini + web +pip install '.[web]' # + web UI +pip install '.[all]' # + Claude + Gemini + web ``` --- diff --git a/README.zh-CN.md b/README.zh-CN.md index 8c75e41..7bccb0a 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -587,6 +587,7 @@ def register(): # 快速验证 make check make docs-check +make shell-check # 仅语法检查 make lint diff --git a/scripts/check_zsh_extras.py b/scripts/check_zsh_extras.py new file mode 100644 index 0000000..56f5fa2 --- /dev/null +++ b/scripts/check_zsh_extras.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python3 +"""Check pip extras in user-facing files and execute them safely under zsh.""" + +from __future__ import annotations + +import re +import subprocess +import sys +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +PIP_INSTALL = re.compile(r"\bpip(?:3)?\s+install\b[^\n]*") +LOCAL_EXTRA = re.compile(r"\.\[(?:web|all)\]") +README_FILES = sorted(ROOT.glob("README*.md")) + + +def _install_files() -> list[Path]: + """Return README and shell/install files that can be copied into zsh.""" + paths = list(README_FILES) + paths.extend(ROOT.glob("*.sh")) + paths.extend(ROOT.glob("*.zsh")) + paths.extend(ROOT.glob("setup.*")) + paths.extend(ROOT.glob("run.*")) + paths.extend(ROOT.glob("push.*")) + paths.append(ROOT / "Makefile") + paths.extend((ROOT / "scripts").glob("*.sh")) + paths.extend((ROOT / "scripts").glob("*.zsh")) + return sorted({path for path in paths if path.is_file()}) + + +def _quoted_extra(line: str, start: int, end: int) -> bool: + if start == 0 or end == len(line): + return False + quote = line[start - 1] + return quote in {"'", '"'} and line[end] == quote + + +def _check_install_files() -> list[str]: + errors: list[str] = [] + command_count = 0 + for path in _install_files(): + text = path.read_text(encoding="utf-8") + for line_number, line in enumerate(text.splitlines(), start=1): + command = PIP_INSTALL.search(line) + if not command: + continue + for extra in LOCAL_EXTRA.finditer(command.group(0)): + command_count += 1 + if not _quoted_extra(command.group(0), extra.start(), extra.end()): + errors.append( + f"{path.relative_to(ROOT)}:{line_number}: quote local extra {extra.group(0)}" + ) + if not errors: + print(f"Checked {command_count} local pip extra references; all are zsh-safe.") + return errors + + +def _run_zsh(args: list[str], script: str) -> subprocess.CompletedProcess[str]: + return subprocess.run( + ["zsh", "-f", *args, "-c", script], + cwd=ROOT, + text=True, + capture_output=True, + check=False, + ) + + +def _check_zsh() -> list[str]: + syntax = "pip install '.[web]'\npip install '.[all]'\n" + parsed = _run_zsh(["-n"], syntax) + if parsed.returncode: + return [f"zsh syntax check failed: {parsed.stderr.strip()}"] + + dry_run = r''' +set -eu +pip() { + if [[ "$#" != 3 || "$1" != install || "$2" != --dry-run ]]; then + print -u2 "unexpected pip arguments: $*" + return 1 + fi + case "$3" in + '.[web]'|'.[all]') + print -r -- "$3" + ;; + *) + print -u2 "unquoted or unexpected extra: $3" + return 1 + ;; + esac +} +pip install --dry-run '.[web]' +pip install --dry-run '.[all]' +''' + executed = _run_zsh([], dry_run) + if executed.returncode: + detail = executed.stderr.strip() or executed.stdout.strip() + return [f"zsh dry-run failed: {detail}"] + if executed.stdout.splitlines() != [".[web]", ".[all]"]: + return [f"zsh dry-run returned unexpected arguments: {executed.stdout!r}"] + print("Clean zsh syntax and pip --dry-run execution passed without glob expansion.") + return [] + + +def main() -> int: + errors = _check_install_files() + errors.extend(_check_zsh()) + if errors: + print("zsh extras check failed:", file=sys.stderr) + for error in errors: + print(f"- {error}", file=sys.stderr) + return 1 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From e7ebabcfe5c5290a38b9039f50891270b7762500 Mon Sep 17 00:00:00 2001 From: Evan Date: Thu, 3 Sep 2026 22:55:10 +0800 Subject: [PATCH 2/2] ci: install zsh for shell documentation checks --- .github/workflows/ci.yml | 2 ++ scripts/check_zsh_extras.py | 3 +++ 2 files changed, 5 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2eaa53f..f46265b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,6 +23,8 @@ jobs: - name: Install dependencies run: | + sudo apt-get update + sudo apt-get install --no-install-recommends -y zsh python -m pip install --upgrade pip pip install -r requirements.txt pip install fastapi uvicorn diff --git a/scripts/check_zsh_extras.py b/scripts/check_zsh_extras.py index 56f5fa2..6e76b8b 100644 --- a/scripts/check_zsh_extras.py +++ b/scripts/check_zsh_extras.py @@ -4,6 +4,7 @@ from __future__ import annotations import re +import shutil import subprocess import sys from pathlib import Path @@ -67,6 +68,8 @@ def _run_zsh(args: list[str], script: str) -> subprocess.CompletedProcess[str]: def _check_zsh() -> list[str]: + if shutil.which("zsh") is None: + return ["zsh executable not found; install zsh before running this check"] syntax = "pip install '.[web]'\npip install '.[all]'\n" parsed = _run_zsh(["-n"], syntax) if parsed.returncode: