xsync-cli 0.2.0__tar.gz → 0.2.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.
- {xsync_cli-0.2.0 → xsync_cli-0.2.1}/.gitignore +3 -0
- {xsync_cli-0.2.0 → xsync_cli-0.2.1}/PKG-INFO +63 -36
- {xsync_cli-0.2.0 → xsync_cli-0.2.1}/README.md +62 -35
- {xsync_cli-0.2.0 → xsync_cli-0.2.1}/pyproject.toml +1 -1
- {xsync_cli-0.2.0 → xsync_cli-0.2.1}/src/xsync_cli/cli.py +96 -23
- {xsync_cli-0.2.0 → xsync_cli-0.2.1}/tests/test_cli_profiles.py +31 -0
- {xsync_cli-0.2.0 → xsync_cli-0.2.1}/uv.lock +1 -1
- xsync_cli-0.2.0/docs/superpowers/plans/2026-09-13-xsync-cli.md +0 -3144
- xsync_cli-0.2.0/docs/superpowers/specs/2026-09-13-xsync-design.md +0 -499
- {xsync_cli-0.2.0 → xsync_cli-0.2.1}/LICENSE +0 -0
- {xsync_cli-0.2.0 → xsync_cli-0.2.1}/docs/verification-2026-09-13.md +0 -0
- {xsync_cli-0.2.0 → xsync_cli-0.2.1}/src/xsync_cli/__init__.py +0 -0
- {xsync_cli-0.2.0 → xsync_cli-0.2.1}/src/xsync_cli/adapters/__init__.py +0 -0
- {xsync_cli-0.2.0 → xsync_cli-0.2.1}/src/xsync_cli/adapters/claude.py +0 -0
- {xsync_cli-0.2.0 → xsync_cli-0.2.1}/src/xsync_cli/adapters/claude_config.py +0 -0
- {xsync_cli-0.2.0 → xsync_cli-0.2.1}/src/xsync_cli/adapters/codex.py +0 -0
- {xsync_cli-0.2.0 → xsync_cli-0.2.1}/src/xsync_cli/adapters/codex_config.py +0 -0
- {xsync_cli-0.2.0 → xsync_cli-0.2.1}/src/xsync_cli/adapters/codex_wiring.py +0 -0
- {xsync_cli-0.2.0 → xsync_cli-0.2.1}/src/xsync_cli/adapters/isolated.py +0 -0
- {xsync_cli-0.2.0 → xsync_cli-0.2.1}/src/xsync_cli/adapters/prompts/__init__.py +0 -0
- {xsync_cli-0.2.0 → xsync_cli-0.2.1}/src/xsync_cli/adapters/prompts/generic-codex.md +0 -0
- {xsync_cli-0.2.0 → xsync_cli-0.2.1}/src/xsync_cli/adapters/prompts/generic-codex.messages.json +0 -0
- {xsync_cli-0.2.0 → xsync_cli-0.2.1}/src/xsync_cli/adapters/prompts/gpt-5.4.md +0 -0
- {xsync_cli-0.2.0 → xsync_cli-0.2.1}/src/xsync_cli/adapters/prompts/gpt-5.4.messages.json +0 -0
- {xsync_cli-0.2.0 → xsync_cli-0.2.1}/src/xsync_cli/adapters/prompts/gpt-5.5.md +0 -0
- {xsync_cli-0.2.0 → xsync_cli-0.2.1}/src/xsync_cli/adapters/prompts/gpt-5.5.messages.json +0 -0
- {xsync_cli-0.2.0 → xsync_cli-0.2.1}/src/xsync_cli/adapters/rules/__init__.py +0 -0
- {xsync_cli-0.2.0 → xsync_cli-0.2.1}/src/xsync_cli/adapters/rules/claude.toml +0 -0
- {xsync_cli-0.2.0 → xsync_cli-0.2.1}/src/xsync_cli/adapters/rules/codex.toml +0 -0
- {xsync_cli-0.2.0 → xsync_cli-0.2.1}/src/xsync_cli/core/__init__.py +0 -0
- {xsync_cli-0.2.0 → xsync_cli-0.2.1}/src/xsync_cli/core/atomic.py +0 -0
- {xsync_cli-0.2.0 → xsync_cli-0.2.1}/src/xsync_cli/core/diff.py +0 -0
- {xsync_cli-0.2.0 → xsync_cli-0.2.1}/src/xsync_cli/core/filters.py +0 -0
- {xsync_cli-0.2.0 → xsync_cli-0.2.1}/src/xsync_cli/core/homes.py +0 -0
- {xsync_cli-0.2.0 → xsync_cli-0.2.1}/src/xsync_cli/core/model.py +0 -0
- {xsync_cli-0.2.0 → xsync_cli-0.2.1}/src/xsync_cli/core/profiles.py +0 -0
- {xsync_cli-0.2.0 → xsync_cli-0.2.1}/src/xsync_cli/core/term.py +0 -0
- {xsync_cli-0.2.0 → xsync_cli-0.2.1}/src/xsync_cli/sources/__init__.py +0 -0
- {xsync_cli-0.2.0 → xsync_cli-0.2.1}/src/xsync_cli/sources/openai_compat.py +0 -0
- {xsync_cli-0.2.0 → xsync_cli-0.2.1}/tests/__init__.py +0 -0
- {xsync_cli-0.2.0 → xsync_cli-0.2.1}/tests/fixtures/__init__.py +0 -0
- {xsync_cli-0.2.0 → xsync_cli-0.2.1}/tests/fixtures/ninerouter_models.json +0 -0
- {xsync_cli-0.2.0 → xsync_cli-0.2.1}/tests/fixtures/plain_openai_models.json +0 -0
- {xsync_cli-0.2.0 → xsync_cli-0.2.1}/tests/test_atomic.py +0 -0
- {xsync_cli-0.2.0 → xsync_cli-0.2.1}/tests/test_claude_config.py +0 -0
- {xsync_cli-0.2.0 → xsync_cli-0.2.1}/tests/test_claude_render.py +0 -0
- {xsync_cli-0.2.0 → xsync_cli-0.2.1}/tests/test_cli_claude.py +0 -0
- {xsync_cli-0.2.0 → xsync_cli-0.2.1}/tests/test_cli_codex.py +0 -0
- {xsync_cli-0.2.0 → xsync_cli-0.2.1}/tests/test_cli_launch.py +0 -0
- {xsync_cli-0.2.0 → xsync_cli-0.2.1}/tests/test_codex_config.py +0 -0
- {xsync_cli-0.2.0 → xsync_cli-0.2.1}/tests/test_codex_render.py +0 -0
- {xsync_cli-0.2.0 → xsync_cli-0.2.1}/tests/test_codex_wiring.py +0 -0
- {xsync_cli-0.2.0 → xsync_cli-0.2.1}/tests/test_diff.py +0 -0
- {xsync_cli-0.2.0 → xsync_cli-0.2.1}/tests/test_filters.py +0 -0
- {xsync_cli-0.2.0 → xsync_cli-0.2.1}/tests/test_homes.py +0 -0
- {xsync_cli-0.2.0 → xsync_cli-0.2.1}/tests/test_isolated.py +0 -0
- {xsync_cli-0.2.0 → xsync_cli-0.2.1}/tests/test_model.py +0 -0
- {xsync_cli-0.2.0 → xsync_cli-0.2.1}/tests/test_openai_compat.py +0 -0
- {xsync_cli-0.2.0 → xsync_cli-0.2.1}/tests/test_package_data.py +0 -0
- {xsync_cli-0.2.0 → xsync_cli-0.2.1}/tests/test_profiles.py +0 -0
- {xsync_cli-0.2.0 → xsync_cli-0.2.1}/tests/test_term.py +0 -0
- {xsync_cli-0.2.0 → xsync_cli-0.2.1}/tools/extract_prompts.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: xsync-cli
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: Sync the model list of an OpenAI-compatible endpoint into the Codex model catalog.
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -12,15 +12,29 @@ Description-Content-Type: text/markdown
|
|
|
12
12
|
|
|
13
13
|
# xsync-cli
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
Point Codex and Claude Code at any OpenAI-compatible endpoint, with every
|
|
16
|
+
model of that endpoint in the model picker.
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
model or drops a model. `xsync` writes the file for you.
|
|
18
|
+
```
|
|
19
|
+
$ xsync claude
|
|
21
20
|
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
claude · 9router
|
|
22
|
+
http://127.0.0.1:20128/v1
|
|
23
|
+
home: ~/.config/xsync/homes/9router/claude
|
|
24
|
+
✓ 35 models ready. Your real claude is untouched.
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
A harness keeps its model list in a settings file. A person must write that
|
|
28
|
+
file by hand, and the file becomes wrong when the endpoint adds a model or
|
|
29
|
+
drops one. xSync writes the file for you.
|
|
30
|
+
|
|
31
|
+
xSync opens the harness in a home of its own. The settings of the user stay
|
|
32
|
+
as they are. Add `apply` when you do want to change them.
|
|
33
|
+
|
|
34
|
+
| Harness | Needs |
|
|
35
|
+
|---|---|
|
|
36
|
+
| Codex | An endpoint that answers `GET /v1/models`. |
|
|
37
|
+
| Claude Code | The same, and `POST /v1/messages` for the requests. |
|
|
24
38
|
|
|
25
39
|
## Install
|
|
26
40
|
|
|
@@ -153,20 +167,22 @@ stops therefore leaves a working picker.
|
|
|
153
167
|
|
|
154
168
|
## Switch between endpoints
|
|
155
169
|
|
|
156
|
-
|
|
170
|
+
Each profile gets a home of its own. Therefore two endpoints never mix:
|
|
171
|
+
|
|
172
|
+
xsync codex --profile openrouter
|
|
173
|
+
xsync codex --profile 9router
|
|
157
174
|
|
|
158
|
-
|
|
175
|
+
A harness holds one model provider. Therefore `apply` writes one profile at
|
|
176
|
+
a time:
|
|
159
177
|
|
|
160
|
-
|
|
161
|
-
step.
|
|
178
|
+
xsync codex apply --profile openrouter
|
|
162
179
|
|
|
163
|
-
`
|
|
164
|
-
|
|
165
|
-
|
|
180
|
+
`apply` always points the harness at the endpoint of the profile, so the
|
|
181
|
+
model list and the endpoint always agree. A dry run does not point anything
|
|
182
|
+
at anything, so it warns when the two disagree:
|
|
166
183
|
|
|
167
|
-
|
|
168
|
-
but
|
|
169
|
-
run: xsync codex --profile openrouter --init
|
|
184
|
+
the rows would come from "openrouter" (https://openrouter.ai/api/v1)
|
|
185
|
+
but Claude Code talks to http://127.0.0.1:20128/v1
|
|
170
186
|
|
|
171
187
|
## The profile file
|
|
172
188
|
|
|
@@ -201,29 +217,35 @@ A glob pattern selects a model by name. The sign `*` means any characters.
|
|
|
201
217
|
| `exclude = ["*-image-*", "*-tts-*"]` | Drop the image models and the speech models. |
|
|
202
218
|
|
|
203
219
|
The `exclude` list always wins over the `include` list. Use the filters when
|
|
204
|
-
an endpoint serves many models that you never choose in
|
|
220
|
+
an endpoint serves many models that you never choose in a harness.
|
|
205
221
|
|
|
206
222
|
The match ignores the letter case.
|
|
207
223
|
|
|
208
224
|
## Safety
|
|
209
225
|
|
|
226
|
+
**The everyday commands write nothing outside `~/.config/xsync/`.** Only
|
|
227
|
+
`apply` touches `~/.codex` or `~/.claude`.
|
|
228
|
+
|
|
210
229
|
- The catalog write is atomic. xSync writes a temporary file, confirms the
|
|
211
230
|
JSON, and then replaces the target. It keeps one backup with the suffix
|
|
212
231
|
`.bak`.
|
|
213
|
-
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
232
|
+
- A harness writes its own settings file while it runs. Therefore `apply`
|
|
233
|
+
hashes the file before the edit and again before the replace. On a
|
|
234
|
+
difference it stops and writes nothing.
|
|
235
|
+
- Claude Code rewrites `settings.json` from its own memory. An `apply` that
|
|
236
|
+
runs while Claude Code is open can therefore disappear. Close Claude Code
|
|
237
|
+
first, or use `xsync claude` and change nothing.
|
|
238
|
+
- `apply --dry-run` writes nothing at all. It does not point the harness at
|
|
239
|
+
the endpoint either.
|
|
240
|
+
- `apply --reset` removes only what the state file records. It never
|
|
241
|
+
restores an old copy of the settings, because a harness adds its own
|
|
242
|
+
entries to that file over time.
|
|
243
|
+
- `apply --reset` keeps the backup `<name>.bak`. The backup is the only way
|
|
244
|
+
back after a reset.
|
|
245
|
+
- With no state file, `apply --reset` prints the keys that it would remove
|
|
246
|
+
and then stops. Add `--force` to continue. The command then asks a
|
|
247
|
+
question before it removes anything. Add `--yes` to answer the question in
|
|
248
|
+
a script.
|
|
227
249
|
- The setup hides the API key while you type it. It then shows the first
|
|
228
250
|
five characters only, and it never shows the length of the key.
|
|
229
251
|
- xSync never prints a whole API key.
|
|
@@ -240,9 +262,14 @@ keep the color in a pipe.
|
|
|
240
262
|
|---|---|---|
|
|
241
263
|
| `{base_url}/models` | yes | no |
|
|
242
264
|
| `~/.config/xsync/profiles.toml` | yes | yes |
|
|
243
|
-
| `~/.
|
|
244
|
-
| `~/.codex/config.toml` | yes | only with
|
|
245
|
-
| `~/.codex
|
|
265
|
+
| `~/.config/xsync/homes/...` | yes | yes |
|
|
266
|
+
| `~/.codex/config.toml` | yes | only with `apply` |
|
|
267
|
+
| `~/.codex/<profile>-models.json` | yes | only with `apply` |
|
|
268
|
+
| `~/.claude/settings.json` | yes | only with `apply` |
|
|
269
|
+
| `.xsync-state.json` in each home | yes | only with `apply` |
|
|
270
|
+
|
|
271
|
+
xSync copies the real settings file when it builds an isolated home. It
|
|
272
|
+
reads that file. It never writes it.
|
|
246
273
|
|
|
247
274
|
## The catalog fields
|
|
248
275
|
|
|
@@ -1,14 +1,28 @@
|
|
|
1
1
|
# xsync-cli
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
Point Codex and Claude Code at any OpenAI-compatible endpoint, with every
|
|
4
|
+
model of that endpoint in the model picker.
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
model or drops a model. `xsync` writes the file for you.
|
|
6
|
+
```
|
|
7
|
+
$ xsync claude
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
claude · 9router
|
|
10
|
+
http://127.0.0.1:20128/v1
|
|
11
|
+
home: ~/.config/xsync/homes/9router/claude
|
|
12
|
+
✓ 35 models ready. Your real claude is untouched.
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
A harness keeps its model list in a settings file. A person must write that
|
|
16
|
+
file by hand, and the file becomes wrong when the endpoint adds a model or
|
|
17
|
+
drops one. xSync writes the file for you.
|
|
18
|
+
|
|
19
|
+
xSync opens the harness in a home of its own. The settings of the user stay
|
|
20
|
+
as they are. Add `apply` when you do want to change them.
|
|
21
|
+
|
|
22
|
+
| Harness | Needs |
|
|
23
|
+
|---|---|
|
|
24
|
+
| Codex | An endpoint that answers `GET /v1/models`. |
|
|
25
|
+
| Claude Code | The same, and `POST /v1/messages` for the requests. |
|
|
12
26
|
|
|
13
27
|
## Install
|
|
14
28
|
|
|
@@ -141,20 +155,22 @@ stops therefore leaves a working picker.
|
|
|
141
155
|
|
|
142
156
|
## Switch between endpoints
|
|
143
157
|
|
|
144
|
-
|
|
158
|
+
Each profile gets a home of its own. Therefore two endpoints never mix:
|
|
159
|
+
|
|
160
|
+
xsync codex --profile openrouter
|
|
161
|
+
xsync codex --profile 9router
|
|
145
162
|
|
|
146
|
-
|
|
163
|
+
A harness holds one model provider. Therefore `apply` writes one profile at
|
|
164
|
+
a time:
|
|
147
165
|
|
|
148
|
-
|
|
149
|
-
step.
|
|
166
|
+
xsync codex apply --profile openrouter
|
|
150
167
|
|
|
151
|
-
`
|
|
152
|
-
|
|
153
|
-
|
|
168
|
+
`apply` always points the harness at the endpoint of the profile, so the
|
|
169
|
+
model list and the endpoint always agree. A dry run does not point anything
|
|
170
|
+
at anything, so it warns when the two disagree:
|
|
154
171
|
|
|
155
|
-
|
|
156
|
-
but
|
|
157
|
-
run: xsync codex --profile openrouter --init
|
|
172
|
+
the rows would come from "openrouter" (https://openrouter.ai/api/v1)
|
|
173
|
+
but Claude Code talks to http://127.0.0.1:20128/v1
|
|
158
174
|
|
|
159
175
|
## The profile file
|
|
160
176
|
|
|
@@ -189,29 +205,35 @@ A glob pattern selects a model by name. The sign `*` means any characters.
|
|
|
189
205
|
| `exclude = ["*-image-*", "*-tts-*"]` | Drop the image models and the speech models. |
|
|
190
206
|
|
|
191
207
|
The `exclude` list always wins over the `include` list. Use the filters when
|
|
192
|
-
an endpoint serves many models that you never choose in
|
|
208
|
+
an endpoint serves many models that you never choose in a harness.
|
|
193
209
|
|
|
194
210
|
The match ignores the letter case.
|
|
195
211
|
|
|
196
212
|
## Safety
|
|
197
213
|
|
|
214
|
+
**The everyday commands write nothing outside `~/.config/xsync/`.** Only
|
|
215
|
+
`apply` touches `~/.codex` or `~/.claude`.
|
|
216
|
+
|
|
198
217
|
- The catalog write is atomic. xSync writes a temporary file, confirms the
|
|
199
218
|
JSON, and then replaces the target. It keeps one backup with the suffix
|
|
200
219
|
`.bak`.
|
|
201
|
-
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
220
|
+
- A harness writes its own settings file while it runs. Therefore `apply`
|
|
221
|
+
hashes the file before the edit and again before the replace. On a
|
|
222
|
+
difference it stops and writes nothing.
|
|
223
|
+
- Claude Code rewrites `settings.json` from its own memory. An `apply` that
|
|
224
|
+
runs while Claude Code is open can therefore disappear. Close Claude Code
|
|
225
|
+
first, or use `xsync claude` and change nothing.
|
|
226
|
+
- `apply --dry-run` writes nothing at all. It does not point the harness at
|
|
227
|
+
the endpoint either.
|
|
228
|
+
- `apply --reset` removes only what the state file records. It never
|
|
229
|
+
restores an old copy of the settings, because a harness adds its own
|
|
230
|
+
entries to that file over time.
|
|
231
|
+
- `apply --reset` keeps the backup `<name>.bak`. The backup is the only way
|
|
232
|
+
back after a reset.
|
|
233
|
+
- With no state file, `apply --reset` prints the keys that it would remove
|
|
234
|
+
and then stops. Add `--force` to continue. The command then asks a
|
|
235
|
+
question before it removes anything. Add `--yes` to answer the question in
|
|
236
|
+
a script.
|
|
215
237
|
- The setup hides the API key while you type it. It then shows the first
|
|
216
238
|
five characters only, and it never shows the length of the key.
|
|
217
239
|
- xSync never prints a whole API key.
|
|
@@ -228,9 +250,14 @@ keep the color in a pipe.
|
|
|
228
250
|
|---|---|---|
|
|
229
251
|
| `{base_url}/models` | yes | no |
|
|
230
252
|
| `~/.config/xsync/profiles.toml` | yes | yes |
|
|
231
|
-
| `~/.
|
|
232
|
-
| `~/.codex/config.toml` | yes | only with
|
|
233
|
-
| `~/.codex
|
|
253
|
+
| `~/.config/xsync/homes/...` | yes | yes |
|
|
254
|
+
| `~/.codex/config.toml` | yes | only with `apply` |
|
|
255
|
+
| `~/.codex/<profile>-models.json` | yes | only with `apply` |
|
|
256
|
+
| `~/.claude/settings.json` | yes | only with `apply` |
|
|
257
|
+
| `.xsync-state.json` in each home | yes | only with `apply` |
|
|
258
|
+
|
|
259
|
+
xSync copies the real settings file when it builds an isolated home. It
|
|
260
|
+
reads that file. It never writes it.
|
|
234
261
|
|
|
235
262
|
## The catalog fields
|
|
236
263
|
|
|
@@ -488,36 +488,111 @@ def _print_report(report, profile: Profile, total: int) -> None:
|
|
|
488
488
|
print(" " + " ".join(parts))
|
|
489
489
|
|
|
490
490
|
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
491
|
+
HELP_GROUPS = (
|
|
492
|
+
(
|
|
493
|
+
"PROFILES",
|
|
494
|
+
(
|
|
495
|
+
("setup", "make a profile, and read the model list"),
|
|
496
|
+
("list", "show the profiles. The active one has a dot"),
|
|
497
|
+
("use <name>", "set the active profile"),
|
|
498
|
+
("remove <name>", "delete a profile"),
|
|
499
|
+
),
|
|
500
|
+
),
|
|
501
|
+
(
|
|
502
|
+
"HARNESSES",
|
|
503
|
+
(
|
|
504
|
+
("codex", "open a Codex on the profile, in its own home"),
|
|
505
|
+
("claude", "open a Claude Code the same way"),
|
|
506
|
+
("<harness> apply", "write the real harness of the user"),
|
|
507
|
+
),
|
|
508
|
+
),
|
|
509
|
+
(
|
|
510
|
+
"HELP",
|
|
511
|
+
(
|
|
512
|
+
("help", "show this page"),
|
|
513
|
+
("help <command>", "show the options of one command"),
|
|
514
|
+
),
|
|
515
|
+
),
|
|
516
|
+
)
|
|
517
|
+
|
|
518
|
+
HELP_OPTIONS = (
|
|
519
|
+
("--profile <name>", "use another profile for one run"),
|
|
520
|
+
("-- <args>", "send the arguments after -- to the harness"),
|
|
521
|
+
("apply --dry-run", "show the difference. Write nothing"),
|
|
522
|
+
("apply --reset", "remove everything that xsync wrote"),
|
|
523
|
+
)
|
|
524
|
+
|
|
525
|
+
HELP_FILES = (
|
|
526
|
+
("~/.config/xsync/profiles.toml", "the profiles"),
|
|
527
|
+
("~/.config/xsync/homes/", "one home for each profile and harness"),
|
|
528
|
+
("~/.codex, ~/.claude", "only `apply` writes these"),
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
HELP_EXIT = (
|
|
532
|
+
("0", "success"),
|
|
533
|
+
("1", "an error"),
|
|
534
|
+
("2", "the endpoint does not answer"),
|
|
535
|
+
)
|
|
496
536
|
|
|
497
|
-
xsync codex open a Codex on the profile, in its own home
|
|
498
|
-
xsync claude open a Claude Code the same way
|
|
499
|
-
xsync claude -- --model a/b arguments after -- go to the harness
|
|
500
537
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
538
|
+
def _version() -> str:
|
|
539
|
+
"""The installed version of the tool."""
|
|
540
|
+
try:
|
|
541
|
+
from importlib.metadata import version
|
|
542
|
+
|
|
543
|
+
return version("xsync-cli")
|
|
544
|
+
except Exception: # noqa: BLE001
|
|
545
|
+
return "unknown"
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
def render_help() -> str:
|
|
549
|
+
"""The help page of the tool."""
|
|
550
|
+
width = 30
|
|
551
|
+
lines: list[str] = []
|
|
552
|
+
|
|
553
|
+
lines.append("")
|
|
554
|
+
lines.append(f" {term.bold(term.cyan('xsync'))} {term.dim('v' + _version())}")
|
|
555
|
+
lines.append(
|
|
556
|
+
term.dim(
|
|
557
|
+
" Sync the models of an OpenAI-compatible endpoint into a harness."
|
|
558
|
+
)
|
|
559
|
+
)
|
|
560
|
+
lines.append("")
|
|
561
|
+
|
|
562
|
+
for title, rows in HELP_GROUPS:
|
|
563
|
+
lines.append(f" {term.bold(title)}")
|
|
564
|
+
for name, text in rows:
|
|
565
|
+
lines.append(f" {term.pad(term.green(name), width)}{term.dim(text)}")
|
|
566
|
+
lines.append("")
|
|
567
|
+
|
|
568
|
+
lines.append(f" {term.bold('OPTIONS')}")
|
|
569
|
+
for name, text in HELP_OPTIONS:
|
|
570
|
+
lines.append(f" {term.pad(term.yellow(name), width)}{term.dim(text)}")
|
|
571
|
+
lines.append("")
|
|
572
|
+
|
|
573
|
+
lines.append(f" {term.bold('FILES')}")
|
|
574
|
+
for name, text in HELP_FILES:
|
|
575
|
+
lines.append(f" {term.pad(name, width)}{term.dim(text)}")
|
|
576
|
+
lines.append("")
|
|
577
|
+
|
|
578
|
+
lines.append(f" {term.bold('EXIT CODES')}")
|
|
579
|
+
codes = " ".join(
|
|
580
|
+
f"{term.bold(code)} {term.dim(text)}" for code, text in HELP_EXIT
|
|
581
|
+
)
|
|
582
|
+
lines.append(f" {codes}")
|
|
583
|
+
lines.append("")
|
|
505
584
|
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
~/.codex, ~/.claude only `apply` writes these
|
|
585
|
+
lines.append(f" {term.dim('start here:')} {term.bold('xsync setup')}")
|
|
586
|
+
lines.append("")
|
|
587
|
+
return "\n".join(lines)
|
|
510
588
|
|
|
511
|
-
exit codes:
|
|
512
|
-
0 success 1 error 2 the endpoint does not answer
|
|
513
|
-
"""
|
|
514
589
|
|
|
515
590
|
|
|
516
591
|
def cmd_help(args: argparse.Namespace) -> int:
|
|
517
592
|
"""Show the help of the tool, or the help of one command."""
|
|
518
593
|
parser = build_parser()
|
|
519
594
|
if not args.topic:
|
|
520
|
-
|
|
595
|
+
print(render_help())
|
|
521
596
|
return EXIT_OK
|
|
522
597
|
|
|
523
598
|
actions = [
|
|
@@ -841,8 +916,6 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
841
916
|
parser = argparse.ArgumentParser(
|
|
842
917
|
prog="xsync",
|
|
843
918
|
description="Sync the model list of an OpenAI-compatible endpoint into a harness.",
|
|
844
|
-
epilog=EPILOG,
|
|
845
|
-
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
846
919
|
)
|
|
847
920
|
sub = parser.add_subparsers(dest="command", metavar="command")
|
|
848
921
|
|
|
@@ -906,7 +979,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
906
979
|
args = parser.parse_args(mine)
|
|
907
980
|
args.extra = theirs
|
|
908
981
|
if not getattr(args, "command", None):
|
|
909
|
-
|
|
982
|
+
print(render_help())
|
|
910
983
|
return EXIT_OK
|
|
911
984
|
try:
|
|
912
985
|
return args.func(args)
|
|
@@ -194,3 +194,34 @@ def test_the_help_command_can_explain_one_command(capsys):
|
|
|
194
194
|
def test_the_help_of_an_unknown_command_fails(capsys):
|
|
195
195
|
assert cli.main(["help", "nope"]) == 1
|
|
196
196
|
assert "nope" in capsys.readouterr().err
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def test_the_help_groups_the_commands(capsys):
|
|
200
|
+
cli.main([])
|
|
201
|
+
out = capsys.readouterr().out
|
|
202
|
+
for group in ("PROFILES", "HARNESSES", "FILES"):
|
|
203
|
+
assert group in out
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def test_the_help_names_the_version(capsys):
|
|
207
|
+
from importlib.metadata import version
|
|
208
|
+
|
|
209
|
+
cli.main([])
|
|
210
|
+
assert version("xsync-cli") in capsys.readouterr().out
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def test_the_help_shows_every_command(capsys):
|
|
214
|
+
cli.main([])
|
|
215
|
+
out = capsys.readouterr().out
|
|
216
|
+
for name in ("setup", "list", "use", "remove", "codex", "claude", "help"):
|
|
217
|
+
assert name in out
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def test_the_help_explains_apply(capsys):
|
|
221
|
+
cli.main([])
|
|
222
|
+
assert "apply" in capsys.readouterr().out
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def test_the_help_holds_no_escape_code_in_a_pipe(capsys):
|
|
226
|
+
cli.main([])
|
|
227
|
+
assert "\x1b[" not in capsys.readouterr().out
|