diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..265c1f1d --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +bin/codex-* text eol=lf diff --git a/README.md b/README.md index b7c73433..e3a80404 100644 --- a/README.md +++ b/README.md @@ -28,12 +28,29 @@ alongside everything else. ## Install +Recommended (works on Windows, macOS, and Linux): + +```bash +git clone https://github.com//codex-shim +cd codex-shim +python3 -m pip install -e . +``` + +This installs `aiohttp` and registers `codex-shim`, `codex-app`, `codex-model`, +`codex-openrouter`, and `codex-minimax` as real entry-point scripts in your +Python install's `Scripts/` (Windows) or `bin/` (POSIX) directory. As long as +that directory is on `PATH`, the commands work from any shell — PowerShell, +cmd, Git Bash, bash, zsh. + +If `pip` reports the scripts were written to a directory not on `PATH`, add it. +On Windows, that is typically `%APPDATA%\Python\Python3xx\Scripts` (user +install) or `\Scripts` (system install). + +POSIX-only alternative — symlink the bash wrappers directly: + ```bash -git clone https://github.com//codex-shim ~/Documents/codex-shim -cd ~/Documents/codex-shim -python3 -m pip install --user aiohttp pytest # only runtime dep is aiohttp -ln -s "$PWD/bin/codex-shim" ~/.local/bin/codex-shim -ln -s "$PWD/bin/codex-app" ~/.local/bin/codex-app +ln -s "$PWD/bin/codex-shim" ~/.local/bin/codex-shim +ln -s "$PWD/bin/codex-app" ~/.local/bin/codex-app ln -s "$PWD/bin/codex-model" ~/.local/bin/codex-model ``` diff --git a/bin/codex-minimax b/bin/codex-minimax index 42c239c0..e216a90d 100755 --- a/bin/codex-minimax +++ b/bin/codex-minimax @@ -9,6 +9,7 @@ while [ -h "$SOURCE" ]; do done ROOT="$(cd -P "$(dirname "$SOURCE")/.." && pwd)" +export PATH="$HOME/.local/bin:$HOME/.hermes/node/bin:$HOME/.npm-global/bin:$HOME/.npm-global:$PATH" SETTINGS="${CODEX_SHIM_SETTINGS:-$ROOT/.codex-shim/minimax-settings.json}" PORT="${CODEX_SHIM_PORT:-8767}" PLACEHOLDER_KEY="REPLACE_WITH_MINIMAX_TOKEN_PLAN_KEY" diff --git a/bin/codex-openrouter b/bin/codex-openrouter index 62c5d1d9..fd209a12 100755 --- a/bin/codex-openrouter +++ b/bin/codex-openrouter @@ -9,6 +9,7 @@ while [ -h "$SOURCE" ]; do done ROOT="$(cd -P "$(dirname "$SOURCE")/.." && pwd)" +export PATH="$HOME/.local/bin:$HOME/.hermes/node/bin:$HOME/.npm-global/bin:$HOME/.npm-global:$PATH" SETTINGS="${CODEX_SHIM_SETTINGS:-$ROOT/.codex-shim/openrouter-settings.json}" PORT="${CODEX_SHIM_PORT:-8766}" PLACEHOLDER_KEY="REPLACE_WITH_OPENROUTER_API_KEY" diff --git a/bin/codex-shim b/bin/codex-shim index 93657bd5..5c6f2d1b 100755 --- a/bin/codex-shim +++ b/bin/codex-shim @@ -8,4 +8,11 @@ while [ -h "$SOURCE" ]; do done ROOT="$(cd -P "$(dirname "$SOURCE")/.." && pwd)" export PYTHONPATH="$ROOT${PYTHONPATH:+:$PYTHONPATH}" -exec python3 -m codex_shim.cli "$@" +PYTHON="${CODEX_SHIM_PYTHON:-}" +if [ -z "$PYTHON" ] && [ -x "$ROOT/.venv/bin/python" ]; then + PYTHON="$ROOT/.venv/bin/python" +fi +if [ -z "$PYTHON" ]; then + PYTHON="python3" +fi +exec "$PYTHON" -m codex_shim.cli "$@" diff --git a/codex_shim/cli.py b/codex_shim/cli.py index ca6555f9..7ddcd388 100644 --- a/codex_shim/cli.py +++ b/codex_shim/cli.py @@ -191,6 +191,50 @@ def main(argv: list[str] | None = None) -> int: return 2 +def codex_app_entry() -> int: + return main(["app", *sys.argv[1:]]) + + +def codex_model_entry() -> int: + args = sys.argv[1:] + if not args or args[0] == "list": + return main(["model", "list"]) + return main(["model", "use", args[0]]) + + +def codex_openrouter_entry() -> int: + args = sys.argv[1:] + if sys.platform == "win32": + return _exec_bash_shortcut("codex-openrouter", args) + if args and args[0] in {"setup", "--configure", "configure"}: + return main(["setup", "openrouter"]) + return main(["openrouter", *args]) + + +def codex_minimax_entry() -> int: + args = sys.argv[1:] + if sys.platform == "win32": + return _exec_bash_shortcut("codex-minimax", args) + if args and args[0] in {"setup", "--configure", "configure"}: + return main(["setup", "minimax"]) + return main(["minimax", *args]) + + +def _exec_bash_shortcut(name: str, args: list[str]) -> int: + script = PROJECT_ROOT / "bin" / name + os.execvp("bash", ["bash", _windows_path_to_wsl(script), *args]) + return 127 + + +def _windows_path_to_wsl(path: Path) -> str: + text = str(path.resolve()) + if len(text) >= 3 and text[1:3] == ":\\": + drive = text[0].lower() + rest = text[3:].replace("\\", "/") + return f"/mnt/{drive}/{rest}" + return text.replace("\\", "/") + + def list_providers() -> int: width = max(len(name) for name in PROVIDER_SPECS) for spec in PROVIDER_SPECS.values(): @@ -438,7 +482,7 @@ def start(settings_path: Path, port: int) -> int: env["PYTHONPATH"] = str(PROJECT_ROOT) + os.pathsep + env.get("PYTHONPATH", "") process = subprocess.Popen(cmd, cwd=str(PROJECT_ROOT), env=env, stdout=log, stderr=log, start_new_session=True) PID_PATH.write_text(str(process.pid)) - for _ in range(50): + for _ in range(300): if _healthy(port): print(f"Shim started on http://{DEFAULT_HOST}:{port} with pid {process.pid}.") print(f"Log: {LOG_PATH}") diff --git a/pyproject.toml b/pyproject.toml index ae491b28..8c37eb51 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,6 +9,10 @@ dependencies = [ [project.scripts] codex-shim = "codex_shim.cli:main" +codex-app = "codex_shim.cli:codex_app_entry" +codex-model = "codex_shim.cli:codex_model_entry" +codex-openrouter = "codex_shim.cli:codex_openrouter_entry" +codex-minimax = "codex_shim.cli:codex_minimax_entry" [tool.pytest.ini_options] testpaths = ["tests"] diff --git a/tests/test_cli_config.py b/tests/test_cli_config.py index 2c1826a9..6a9e2ab2 100644 --- a/tests/test_cli_config.py +++ b/tests/test_cli_config.py @@ -306,3 +306,18 @@ def test_provider_top_level_alias_runs_provider(tmp_path, monkeypatch): assert rc == 0 assert captured == {"provider": "test-provider", "args": ["."], "port": 9998} + + +def test_windows_provider_entry_delegates_to_bash_shortcut(monkeypatch): + captured = {} + + monkeypatch.setattr(cli.sys, "platform", "win32") + monkeypatch.setattr(cli.sys, "argv", ["codex-openrouter", "."]) + monkeypatch.setattr(cli.os, "execvp", lambda file, args: captured.update(file=file, args=args)) + + cli.codex_openrouter_entry() + + assert captured["file"] == "bash" + assert captured["args"][0] == "bash" + assert captured["args"][1].endswith("/bin/codex-openrouter") + assert captured["args"][2:] == ["."]