ferry-codex 0.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. ferry_codex-0.1.1/.agents/plugins/marketplace.json +20 -0
  2. ferry_codex-0.1.1/LICENSE +21 -0
  3. ferry_codex-0.1.1/PKG-INFO +162 -0
  4. ferry_codex-0.1.1/README.md +137 -0
  5. ferry_codex-0.1.1/ferry_codex/__init__.py +5 -0
  6. ferry_codex-0.1.1/ferry_codex/build_backend.py +121 -0
  7. ferry_codex-0.1.1/ferry_codex/build_identity.py +8 -0
  8. ferry_codex-0.1.1/ferry_codex/cli.py +34 -0
  9. ferry_codex-0.1.1/ferry_codex/integration.py +445 -0
  10. ferry_codex-0.1.1/ferry_codex.egg-info/PKG-INFO +162 -0
  11. ferry_codex-0.1.1/ferry_codex.egg-info/SOURCES.txt +25 -0
  12. ferry_codex-0.1.1/ferry_codex.egg-info/dependency_links.txt +1 -0
  13. ferry_codex-0.1.1/ferry_codex.egg-info/entry_points.txt +2 -0
  14. ferry_codex-0.1.1/ferry_codex.egg-info/requires.txt +2 -0
  15. ferry_codex-0.1.1/ferry_codex.egg-info/top_level.txt +2 -0
  16. ferry_codex-0.1.1/plugins/ferry/.codex-plugin/plugin.json +19 -0
  17. ferry_codex-0.1.1/plugins/ferry/.mcp.json +10 -0
  18. ferry_codex-0.1.1/plugins/ferry/bin/ferry-mcp.py +10 -0
  19. ferry_codex-0.1.1/plugins/ferry/requirements.lock +32 -0
  20. ferry_codex-0.1.1/plugins/ferry/skills/ferry/SKILL.md +58 -0
  21. ferry_codex-0.1.1/plugins/ferry/skills/ferry/references/worker-brief.md +54 -0
  22. ferry_codex-0.1.1/plugins/ferry/src/ferry_mcp/__init__.py +3 -0
  23. ferry_codex-0.1.1/plugins/ferry/src/ferry_mcp/adapter.py +392 -0
  24. ferry_codex-0.1.1/plugins/ferry/src/ferry_mcp/advisory.py +165 -0
  25. ferry_codex-0.1.1/plugins/ferry/src/ferry_mcp/server.py +121 -0
  26. ferry_codex-0.1.1/pyproject.toml +58 -0
  27. ferry_codex-0.1.1/setup.cfg +4 -0
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "ferry",
3
+ "interface": {
4
+ "displayName": "Ferry"
5
+ },
6
+ "plugins": [
7
+ {
8
+ "name": "ferry",
9
+ "source": {
10
+ "source": "local",
11
+ "path": "./plugins/ferry"
12
+ },
13
+ "policy": {
14
+ "installation": "AVAILABLE",
15
+ "authentication": "ON_INSTALL"
16
+ },
17
+ "category": "Productivity"
18
+ }
19
+ ]
20
+ }
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Scrimper contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,162 @@
1
+ Metadata-Version: 2.4
2
+ Name: ferry-codex
3
+ Version: 0.1.1
4
+ Summary: Keep Codex as the lead while bounded work runs on your own models.
5
+ License-Expression: MIT
6
+ Project-URL: Homepage, https://github.com/PunkGo/ferry
7
+ Project-URL: Repository, https://github.com/PunkGo/ferry
8
+ Project-URL: Issues, https://github.com/PunkGo/ferry/issues
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Environment :: Console
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3 :: Only
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Requires-Python: >=3.10
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Requires-Dist: mcp==2.0.0
23
+ Requires-Dist: packaging>=24
24
+ Dynamic: license-file
25
+
26
+ # Ferry
27
+
28
+ <p align="center">
29
+ <strong>Keep Codex as the lead. Delegate bounded work to your own models.</strong>
30
+ </p>
31
+
32
+ <p align="center">
33
+ English · <a href="README.zh-CN.md">简体中文</a>
34
+ </p>
35
+
36
+ Ferry adds controlled worker delegation to Codex. Your main Codex session keeps
37
+ the project context, scopes the task, and judges the result. Bounded execution
38
+ can stay on a native Codex worker or run on an explicitly selected custom model
39
+ such as DeepSeek.
40
+
41
+ If you like the lead-and-worker feel of Claude Code agent teams, Ferry brings
42
+ that delegation pattern to Codex while letting workers use providers already
43
+ configured on your machine. Ferry is not a second agent harness or a shared
44
+ peer-to-peer team runtime. Codex remains the lead and final judge.
45
+
46
+ > **Ferry work, not judgment.**
47
+
48
+ ## Why Ferry
49
+
50
+ Frontier models are worth using for hard reasoning, planning, and review. They
51
+ are not always the economical choice for a bounded implementation task.
52
+
53
+ Ferry lets you keep the Codex harness you already trust and spend expensive
54
+ model capacity where judgment matters. A lower-cost custom model can implement
55
+ a focused change; Codex then inspects the real diff, runs the checks, and decides
56
+ whether to accept, steer, rework, interrupt, or stop.
57
+
58
+ ![Codex leads while Ferry routes bounded work to a native or custom-model worker](diagrams/ferry-cost-control.svg)
59
+
60
+ ## Before you install: prove the provider in Codex
61
+
62
+ First, follow the official
63
+ [Codex custom-provider guide](https://developers.openai.com/codex/config-advanced#custom-model-providers)
64
+ to configure and test your provider. Ferry does not configure models, endpoints,
65
+ authentication, or credentials. The exact provider, model, and authentication
66
+ must already work in Codex before Ferry uses them.
67
+
68
+ ### DeepSeek example
69
+
70
+ After following the Codex guide, if your working provider is named `deepseek`,
71
+ verify it directly through Codex. Replace the model id if your configuration
72
+ uses a different one.
73
+
74
+ ```sh
75
+ export DEEPSEEK_API_KEY="YOUR_KEY"
76
+
77
+ codex exec -s read-only \
78
+ -c 'model_provider="deepseek"' \
79
+ -m deepseek-v4-pro \
80
+ 'Reply with exactly DEEPSEEK_CODEX_OK.'
81
+ ```
82
+
83
+ Continue only after Codex returns `DEEPSEEK_CODEX_OK`. Ferry will never silently
84
+ fall back to the owner model when an explicitly requested provider is missing,
85
+ misconfigured, or reports a different native provider identity.
86
+
87
+ ## Install
88
+
89
+ Requirements: Python 3.10+, `pipx`, and an existing Codex CLI installation.
90
+
91
+ ```sh
92
+ pipx install ferry-codex
93
+ ferry setup
94
+ ```
95
+
96
+ Start a fresh Codex session. Ferry reuses your host `codex` executable; it does
97
+ not bundle Python or install a second Codex CLI.
98
+
99
+ ## Use
100
+
101
+ Ask Codex in natural language. There are no Ferry worker commands to learn.
102
+
103
+ Delegate to a native Codex worker:
104
+
105
+ ```text
106
+ Use Ferry to delegate this bounded task to a native worker: update the parser,
107
+ run its focused tests, and do not touch unrelated files.
108
+ ```
109
+
110
+ Delegate to the DeepSeek provider you already proved in Codex:
111
+
112
+ ```text
113
+ Use Ferry with my configured DeepSeek provider and deepseek-v4-pro for this
114
+ bounded implementation. Keep this Codex thread as lead and independently verify
115
+ the resulting diff and tests.
116
+ ```
117
+
118
+ Correct or stop live work in the same conversation:
119
+
120
+ ```text
121
+ Steer the Ferry worker: preserve the public API and limit the change to src/parser.py.
122
+ ```
123
+
124
+ ```text
125
+ Interrupt the Ferry worker now.
126
+ ```
127
+
128
+ The worker's report is delivery data. Codex checks the actual worktree and runs
129
+ the real acceptance commands before accepting it.
130
+
131
+ ## Manage the integration
132
+
133
+ ```sh
134
+ ferry status
135
+ ```
136
+
137
+ Close every Codex session using Ferry before upgrading or uninstalling.
138
+
139
+ ```sh
140
+ pipx upgrade ferry-codex && ferry setup
141
+ ```
142
+
143
+ ```sh
144
+ ferry uninstall && pipx uninstall ferry-codex
145
+ ```
146
+
147
+ These commands manage only the Ferry package and Codex plugin integration. They
148
+ do not delete Codex threads, provider configuration, credentials, or project
149
+ files.
150
+
151
+ ## Support and security
152
+
153
+ Ferry's native-worker, OpenAI, DeepSeek, steer, interrupt, pipx lifecycle, and
154
+ Windows paths have versioned conformance evidence. See [SUPPORT.md](SUPPORT.md)
155
+ for the exact tested matrix.
156
+
157
+ Report security issues through [SECURITY.md](SECURITY.md). Contributions are
158
+ welcome after reading [CONTRIBUTING.md](CONTRIBUTING.md).
159
+
160
+ ## License
161
+
162
+ [MIT](LICENSE)
@@ -0,0 +1,137 @@
1
+ # Ferry
2
+
3
+ <p align="center">
4
+ <strong>Keep Codex as the lead. Delegate bounded work to your own models.</strong>
5
+ </p>
6
+
7
+ <p align="center">
8
+ English · <a href="README.zh-CN.md">简体中文</a>
9
+ </p>
10
+
11
+ Ferry adds controlled worker delegation to Codex. Your main Codex session keeps
12
+ the project context, scopes the task, and judges the result. Bounded execution
13
+ can stay on a native Codex worker or run on an explicitly selected custom model
14
+ such as DeepSeek.
15
+
16
+ If you like the lead-and-worker feel of Claude Code agent teams, Ferry brings
17
+ that delegation pattern to Codex while letting workers use providers already
18
+ configured on your machine. Ferry is not a second agent harness or a shared
19
+ peer-to-peer team runtime. Codex remains the lead and final judge.
20
+
21
+ > **Ferry work, not judgment.**
22
+
23
+ ## Why Ferry
24
+
25
+ Frontier models are worth using for hard reasoning, planning, and review. They
26
+ are not always the economical choice for a bounded implementation task.
27
+
28
+ Ferry lets you keep the Codex harness you already trust and spend expensive
29
+ model capacity where judgment matters. A lower-cost custom model can implement
30
+ a focused change; Codex then inspects the real diff, runs the checks, and decides
31
+ whether to accept, steer, rework, interrupt, or stop.
32
+
33
+ ![Codex leads while Ferry routes bounded work to a native or custom-model worker](diagrams/ferry-cost-control.svg)
34
+
35
+ ## Before you install: prove the provider in Codex
36
+
37
+ First, follow the official
38
+ [Codex custom-provider guide](https://developers.openai.com/codex/config-advanced#custom-model-providers)
39
+ to configure and test your provider. Ferry does not configure models, endpoints,
40
+ authentication, or credentials. The exact provider, model, and authentication
41
+ must already work in Codex before Ferry uses them.
42
+
43
+ ### DeepSeek example
44
+
45
+ After following the Codex guide, if your working provider is named `deepseek`,
46
+ verify it directly through Codex. Replace the model id if your configuration
47
+ uses a different one.
48
+
49
+ ```sh
50
+ export DEEPSEEK_API_KEY="YOUR_KEY"
51
+
52
+ codex exec -s read-only \
53
+ -c 'model_provider="deepseek"' \
54
+ -m deepseek-v4-pro \
55
+ 'Reply with exactly DEEPSEEK_CODEX_OK.'
56
+ ```
57
+
58
+ Continue only after Codex returns `DEEPSEEK_CODEX_OK`. Ferry will never silently
59
+ fall back to the owner model when an explicitly requested provider is missing,
60
+ misconfigured, or reports a different native provider identity.
61
+
62
+ ## Install
63
+
64
+ Requirements: Python 3.10+, `pipx`, and an existing Codex CLI installation.
65
+
66
+ ```sh
67
+ pipx install ferry-codex
68
+ ferry setup
69
+ ```
70
+
71
+ Start a fresh Codex session. Ferry reuses your host `codex` executable; it does
72
+ not bundle Python or install a second Codex CLI.
73
+
74
+ ## Use
75
+
76
+ Ask Codex in natural language. There are no Ferry worker commands to learn.
77
+
78
+ Delegate to a native Codex worker:
79
+
80
+ ```text
81
+ Use Ferry to delegate this bounded task to a native worker: update the parser,
82
+ run its focused tests, and do not touch unrelated files.
83
+ ```
84
+
85
+ Delegate to the DeepSeek provider you already proved in Codex:
86
+
87
+ ```text
88
+ Use Ferry with my configured DeepSeek provider and deepseek-v4-pro for this
89
+ bounded implementation. Keep this Codex thread as lead and independently verify
90
+ the resulting diff and tests.
91
+ ```
92
+
93
+ Correct or stop live work in the same conversation:
94
+
95
+ ```text
96
+ Steer the Ferry worker: preserve the public API and limit the change to src/parser.py.
97
+ ```
98
+
99
+ ```text
100
+ Interrupt the Ferry worker now.
101
+ ```
102
+
103
+ The worker's report is delivery data. Codex checks the actual worktree and runs
104
+ the real acceptance commands before accepting it.
105
+
106
+ ## Manage the integration
107
+
108
+ ```sh
109
+ ferry status
110
+ ```
111
+
112
+ Close every Codex session using Ferry before upgrading or uninstalling.
113
+
114
+ ```sh
115
+ pipx upgrade ferry-codex && ferry setup
116
+ ```
117
+
118
+ ```sh
119
+ ferry uninstall && pipx uninstall ferry-codex
120
+ ```
121
+
122
+ These commands manage only the Ferry package and Codex plugin integration. They
123
+ do not delete Codex threads, provider configuration, credentials, or project
124
+ files.
125
+
126
+ ## Support and security
127
+
128
+ Ferry's native-worker, OpenAI, DeepSeek, steer, interrupt, pipx lifecycle, and
129
+ Windows paths have versioned conformance evidence. See [SUPPORT.md](SUPPORT.md)
130
+ for the exact tested matrix.
131
+
132
+ Report security issues through [SECURITY.md](SECURITY.md). Contributions are
133
+ welcome after reading [CONTRIBUTING.md](CONTRIBUTING.md).
134
+
135
+ ## License
136
+
137
+ [MIT](LICENSE)
@@ -0,0 +1,5 @@
1
+ """Ferry's installed-distribution integration surface."""
2
+
3
+ from .build_identity import FULL_VERSION, PUBLIC_VERSION
4
+
5
+ __all__ = ["FULL_VERSION", "PUBLIC_VERSION"]
@@ -0,0 +1,121 @@
1
+ """Release-only build identity guard around the standards-based setuptools backend."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import re
7
+ import shutil
8
+ import subprocess
9
+ from contextlib import contextmanager
10
+ from pathlib import Path
11
+ from typing import Iterator
12
+
13
+ ROOT = Path(__file__).resolve().parents[1]
14
+ IDENTITY = ROOT / "ferry_codex" / "build_identity.py"
15
+ EGG_INFO = ROOT / "ferry_codex.egg-info"
16
+ BYTECODE = ROOT / "ferry_codex" / "__pycache__"
17
+ EGG_INFO_FILES = {"PKG-INFO", "SOURCES.txt", "dependency_links.txt", "entry_points.txt", "requires.txt", "top_level.txt"}
18
+
19
+
20
+ def _clear_backend_byproducts() -> None:
21
+ """Remove only the exact, regular setuptools/Python byproducts this backend creates."""
22
+ if EGG_INFO.exists() or EGG_INFO.is_symlink():
23
+ if EGG_INFO.is_symlink() or not EGG_INFO.is_dir():
24
+ raise RuntimeError(f"refusing unexpected generated path: {EGG_INFO}")
25
+ entries = list(EGG_INFO.rglob("*"))
26
+ files = {item.relative_to(EGG_INFO).as_posix() for item in entries if item.is_file()}
27
+ if any(item.is_symlink() or item.is_dir() or not item.is_file() for item in entries) or files - EGG_INFO_FILES:
28
+ raise RuntimeError(f"refusing unexpected setuptools byproduct contents: {EGG_INFO}")
29
+ shutil.rmtree(EGG_INFO)
30
+ if BYTECODE.exists() or BYTECODE.is_symlink():
31
+ if BYTECODE.is_symlink() or not BYTECODE.is_dir():
32
+ raise RuntimeError(f"refusing unexpected generated path: {BYTECODE}")
33
+ modules = {item.stem for item in (ROOT / "ferry_codex").glob("*.py")}
34
+ for item in BYTECODE.rglob("*"):
35
+ if item.is_symlink() or item.is_dir() or not item.is_file():
36
+ raise RuntimeError(f"refusing unexpected Python bytecode byproduct: {BYTECODE}")
37
+ match = re.fullmatch(r"([A-Za-z_][A-Za-z0-9_]*)\.cpython-\d+(?:\.opt-\d+)?\.pyc", item.name)
38
+ if match is None or match.group(1) not in modules:
39
+ raise RuntimeError(f"refusing unexpected Python bytecode byproduct: {BYTECODE}")
40
+ shutil.rmtree(BYTECODE)
41
+
42
+
43
+ def _run_hook(action):
44
+ """Run one PEP 517 hook without allowing cleanup to erase its original cause."""
45
+ try:
46
+ result = action()
47
+ except BaseException as primary:
48
+ try:
49
+ _clear_backend_byproducts()
50
+ except BaseException as cleanup:
51
+ raise RuntimeError(f"PEP 517 hook failed: {primary}; cleanup failed: {cleanup}") from primary
52
+ raise
53
+ _clear_backend_byproducts()
54
+ return result
55
+
56
+
57
+ def _release_commit() -> str:
58
+ _clear_backend_byproducts()
59
+ requested = os.environ.get("FERRY_BUILD_COMMIT")
60
+ if not requested or not re.fullmatch(r"[0-9a-f]{40}", requested):
61
+ raise RuntimeError("set FERRY_BUILD_COMMIT to the exact lowercase 40-character release commit")
62
+ observed = subprocess.run(("git", "-C", str(ROOT), "rev-parse", "HEAD"), check=True, text=True, stdout=subprocess.PIPE).stdout.strip()
63
+ dirty = subprocess.run(("git", "-C", str(ROOT), "status", "--porcelain"), check=True, text=True, stdout=subprocess.PIPE).stdout
64
+ if observed != requested or dirty:
65
+ raise RuntimeError("release artifacts require a clean checkout at FERRY_BUILD_COMMIT")
66
+ return requested
67
+
68
+
69
+ def _source_sdist_commit() -> str | None:
70
+ """Only an unpacked sdist may reuse its immutable embedded source commit."""
71
+ if (ROOT / ".git").exists() or not (ROOT / "PKG-INFO").is_file():
72
+ return None
73
+ match = re.search(r'SOURCE_COMMIT = "([0-9a-f]{40})"', IDENTITY.read_text(encoding="utf-8"))
74
+ return match.group(1) if match else None
75
+
76
+
77
+ @contextmanager
78
+ def _embedded_identity() -> Iterator[None]:
79
+ commit = _release_commit()
80
+ original = IDENTITY.read_text(encoding="utf-8")
81
+ replacement = re.sub(r'SOURCE_COMMIT = ".*"', f'SOURCE_COMMIT = "{commit}"', original)
82
+ IDENTITY.write_text(replacement, encoding="utf-8")
83
+ try:
84
+ yield
85
+ finally:
86
+ IDENTITY.write_text(original, encoding="utf-8")
87
+
88
+
89
+ def build_wheel(wheel_directory: str, config_settings=None, metadata_directory=None) -> str:
90
+ from setuptools import build_meta as backend
91
+ # A wheel directly from a checkout must bind to that clean checkout. A wheel
92
+ # from an sdist instead reuses the immutable identity already embedded there.
93
+ def action():
94
+ if _source_sdist_commit() is not None:
95
+ return backend.build_wheel(wheel_directory, config_settings, metadata_directory)
96
+ with _embedded_identity():
97
+ return backend.build_wheel(wheel_directory, config_settings, metadata_directory)
98
+ return _run_hook(action)
99
+
100
+
101
+ def build_sdist(sdist_directory: str, config_settings=None) -> str:
102
+ from setuptools import build_meta as backend
103
+ def action():
104
+ with _embedded_identity():
105
+ return backend.build_sdist(sdist_directory, config_settings)
106
+ return _run_hook(action)
107
+
108
+
109
+ def prepare_metadata_for_build_wheel(metadata_directory: str, config_settings=None) -> str:
110
+ from setuptools import build_meta as backend
111
+ return _run_hook(lambda: backend.prepare_metadata_for_build_wheel(metadata_directory, config_settings))
112
+
113
+
114
+ def get_requires_for_build_wheel(config_settings=None):
115
+ from setuptools import build_meta as backend
116
+ return _run_hook(lambda: backend.get_requires_for_build_wheel(config_settings))
117
+
118
+
119
+ def get_requires_for_build_sdist(config_settings=None):
120
+ from setuptools import build_meta as backend
121
+ return _run_hook(lambda: backend.get_requires_for_build_sdist(config_settings))
@@ -0,0 +1,8 @@
1
+ """Build identity embedded in source and replaced in release artifacts."""
2
+
3
+ PUBLIC_VERSION = "0.1.1"
4
+ # A release build replaces this with the exact clean source commit. Keeping the
5
+ # source marker explicit prevents an unpacked checkout from masquerading as an
6
+ # immutable distribution.
7
+ SOURCE_COMMIT = "3ca3f13b6ef4bb758af20421f9274a5fc4a27199"
8
+ FULL_VERSION = f"{PUBLIC_VERSION}+{SOURCE_COMMIT[:12]}"
@@ -0,0 +1,34 @@
1
+ """The intentionally small public Ferry console seam."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ from .build_identity import FULL_VERSION
10
+ from .integration import IntegrationError, setup, status, uninstall
11
+
12
+
13
+ def main(argv: list[str] | None = None) -> int:
14
+ parser = argparse.ArgumentParser(prog="ferry", description="Reconcile Ferry's installed Codex plugin integration.")
15
+ parser.add_argument("--version", action="version", version=FULL_VERSION)
16
+ parser.add_argument("--ferry-home", type=Path, help=argparse.SUPPRESS)
17
+ parser.add_argument("--codex", help=argparse.SUPPRESS)
18
+ subcommands = parser.add_subparsers(dest="command", required=True)
19
+ for name in ("setup", "status", "uninstall"):
20
+ subcommands.add_parser(name)
21
+ args = parser.parse_args(argv)
22
+ try:
23
+ {"setup": setup, "status": status, "uninstall": uninstall}[args.command](ferry_home=args.ferry_home, codex=args.codex)
24
+ except IntegrationError as exc:
25
+ print(f"ferry: {exc}", file=sys.stderr)
26
+ return 1
27
+ except Exception as exc:
28
+ print(f"ferry: {type(exc).__name__}: {exc}", file=sys.stderr)
29
+ return 1
30
+ return 0
31
+
32
+
33
+ if __name__ == "__main__":
34
+ raise SystemExit(main())