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.
- ferry_codex-0.1.1/.agents/plugins/marketplace.json +20 -0
- ferry_codex-0.1.1/LICENSE +21 -0
- ferry_codex-0.1.1/PKG-INFO +162 -0
- ferry_codex-0.1.1/README.md +137 -0
- ferry_codex-0.1.1/ferry_codex/__init__.py +5 -0
- ferry_codex-0.1.1/ferry_codex/build_backend.py +121 -0
- ferry_codex-0.1.1/ferry_codex/build_identity.py +8 -0
- ferry_codex-0.1.1/ferry_codex/cli.py +34 -0
- ferry_codex-0.1.1/ferry_codex/integration.py +445 -0
- ferry_codex-0.1.1/ferry_codex.egg-info/PKG-INFO +162 -0
- ferry_codex-0.1.1/ferry_codex.egg-info/SOURCES.txt +25 -0
- ferry_codex-0.1.1/ferry_codex.egg-info/dependency_links.txt +1 -0
- ferry_codex-0.1.1/ferry_codex.egg-info/entry_points.txt +2 -0
- ferry_codex-0.1.1/ferry_codex.egg-info/requires.txt +2 -0
- ferry_codex-0.1.1/ferry_codex.egg-info/top_level.txt +2 -0
- ferry_codex-0.1.1/plugins/ferry/.codex-plugin/plugin.json +19 -0
- ferry_codex-0.1.1/plugins/ferry/.mcp.json +10 -0
- ferry_codex-0.1.1/plugins/ferry/bin/ferry-mcp.py +10 -0
- ferry_codex-0.1.1/plugins/ferry/requirements.lock +32 -0
- ferry_codex-0.1.1/plugins/ferry/skills/ferry/SKILL.md +58 -0
- ferry_codex-0.1.1/plugins/ferry/skills/ferry/references/worker-brief.md +54 -0
- ferry_codex-0.1.1/plugins/ferry/src/ferry_mcp/__init__.py +3 -0
- ferry_codex-0.1.1/plugins/ferry/src/ferry_mcp/adapter.py +392 -0
- ferry_codex-0.1.1/plugins/ferry/src/ferry_mcp/advisory.py +165 -0
- ferry_codex-0.1.1/plugins/ferry/src/ferry_mcp/server.py +121 -0
- ferry_codex-0.1.1/pyproject.toml +58 -0
- 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
|
+

|
|
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
|
+

|
|
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,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())
|