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
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 8 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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."

Expand All @@ -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; }
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions README.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -559,6 +559,7 @@ def register():
# クイック検証
make check
make docs-check
make shell-check

# 構文チェックのみ
make lint
Expand Down
1 change: 1 addition & 0 deletions README.ko.md
Original file line number Diff line number Diff line change
Expand Up @@ -559,6 +559,7 @@ def register():
# 빠른 검증
make check
make docs-check
make shell-check

# 구문 검사만
make lint
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
```

---
Expand Down
1 change: 1 addition & 0 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -587,6 +587,7 @@ def register():
# 快速验证
make check
make docs-check
make shell-check

# 仅语法检查
make lint
Expand Down
120 changes: 120 additions & 0 deletions scripts/check_zsh_extras.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
#!/usr/bin/env python3
"""Check pip extras in user-facing files and execute them safely under zsh."""

from __future__ import annotations

import re
import shutil
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]:
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:
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())
Loading