-
Notifications
You must be signed in to change notification settings - Fork 297
Expand file tree
/
Copy pathinstall.sh
More file actions
executable file
·400 lines (377 loc) · 14.7 KB
/
Copy pathinstall.sh
File metadata and controls
executable file
·400 lines (377 loc) · 14.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
#!/bin/sh
set -eu
repository_url=${CODEX_ROUTER_REPOSITORY_URL:-https://github.com/duolahypercho/codex-router.git}
default_data_dir=${XDG_DATA_HOME:-$HOME/.local/share}
install_dir=$default_data_dir/codex-router
prepare_only=false
configure_provider_keys=
guided=auto
providers=
migrate_known=false
smoke_test=false
with_tray=false
no_tray=false
no_provider=false
no_discovery=false
previous_revision=
force=false
target=codex
cursor_public_url=
cursor_hostname=
usage() {
cat <<'EOF'
Usage: install.sh [options]
Install external model routes for Codex, DeepSeek Harness, Gemini CLI, Cursor, Claude Code, or OpenClaw.
Options:
--install-dir PATH Stable checkout used by the background service
--target APP Install for "codex" (default), "dsh" (DeepSeek Harness),
"gemini" (Gemini CLI), "cursor", "claude", or "openclaw"
--cursor-public-url URL
Stable HTTPS tunnel origin for Cursor App; forward it
to the local Cursor edge port printed during setup
--cursor-hostname HOST
Public hostname for a router-managed Cloudflare named tunnel
--prepare-only Install dependencies without changing either app
--api-key Alias for --kimi-api-key
--kimi-api-key Prompt securely for a Kimi Platform API key
--deepseek-api-key Prompt securely for a DeepSeek API key
--grok-api-key Prompt securely for an xAI API key
--anthropic-api-key Prompt securely for an Anthropic API key
--guided Walk through provider selection and authentication
--auto Use configured credentials without questions
--providers LIST Enable comma-separated provider ids (or "configured")
--migrate-known Snapshot and replace recognized earlier router installs
--smoke-test Make one small billed request per enabled provider
--with-tray Also build and launch the desktop companion app
--no-tray Never offer the desktop companion app
--no-provider Install idle: no provider is selected or configured
--no-discovery With --no-provider: never read credentials, the Keychain,
or other CLIs' sessions; Codex traffic gets a local error
--force Discard edits to tracked files in the managed checkout
before updating it. Untracked files are never touched.
-h, --help Show this help
When run from a checkout, this script installs that checkout. When piped from
GitHub, it clones or updates ~/.local/share/codex-router first.
EOF
}
die() {
printf 'codex-router: %s\n' "$*" >&2
exit 1
}
# The single restore path for a step that runs after the pull. Every such step
# can leave the checkout on code the machine cannot run -- `npm ci` empties
# node_modules before it refills it -- so failing one has to return the managed
# checkout to the revision the service was last known to work on, exactly as a
# failed setup does. Steps that run before the pull have nothing to restore and
# keep using die() directly.
restore_previous_revision() {
if [ -n "$previous_revision" ]; then
git -C "$repo_dir" switch --detach "$previous_revision" >/dev/null 2>&1 || true
die "$1; the managed source checkout was restored to $previous_revision. Re-run this installer to retry the update from main."
fi
die "$1"
}
# A failed setup rolls HEAD back with `switch --detach`, so the next run
# finds no current branch. `src/update.mjs` and install.ps1 already switch
# that state back to main before pulling; refusing here is how a Homebrew
# user who then ran this script stayed on refs/codex-router/rollback (#761).
ensure_main_branch() {
current_branch=$(git -C "$1" branch --show-current || true)
if [ "$current_branch" != "main" ]; then
if [ -z "$current_branch" ]; then
git -C "$1" switch main >/dev/null 2>&1 ||
die "$1 is in a detached HEAD state and could not be restored to main; run 'git switch main' there and retry."
current_branch=$(git -C "$1" branch --show-current || true)
fi
[ "$current_branch" = "main" ] ||
die "$1 must be on its main branch before updating"
fi
}
# Mirrors DIRTY_PREVIEW_LIMIT in src/update.mjs and $DirtyPreviewLimit in
# install.ps1. test/installer-scripts.test.mjs compares all three, so they
# cannot drift apart.
dirty_preview_limit=10
# Mirrors localModifications() in src/update.mjs. Only tracked edits are at
# stake: a fast-forward pull never replaces an untracked file, and git refuses
# the rare collision on its own with a precise message. Counting untracked
# files as "local changes" only ever stranded people -- one stray file in the
# checkout and every later self-update was refused, with nothing in the error
# to say which file or how to get past it.
local_modifications() {
status_output=$(git -C "$1" status --porcelain --untracked-files=no) || return 1
printf '%s\n' "$status_output" | sed -e 's/^[[:space:]]*//' -e '/^$/d'
}
# Mirrors localModificationsMessage() in src/update.mjs. Naming the files and
# both ways forward is the whole point: the old message named neither, so
# anyone blocked by a single stray edit had nothing to act on.
local_modifications_message() {
changes=$1
directory=$2
count=$(printf '%s\n' "$changes" | wc -l)
count=$((count))
if [ "$count" -eq 1 ]; then
counted="1 tracked file"
else
counted="$count tracked files"
fi
printf 'The checkout has local changes to %s; refusing to replace them during update:\n' "$counted"
printf '%s\n' "$changes" | head -n "$dirty_preview_limit" | sed 's/^/ /'
if [ "$count" -gt "$dirty_preview_limit" ]; then
printf ' ...and %s more\n' "$((count - dirty_preview_limit))"
fi
printf '\n'
printf 'Keep them: git -C %s stash\n' "$directory"
printf 'Discard them: re-run the same command with --force\n'
}
while [ "$#" -gt 0 ]; do
case "$1" in
--target)
[ "$#" -ge 2 ] || die "--target requires codex, dsh, gemini, cursor, claude, or openclaw"
target=$2
shift 2
;;
--install-dir)
[ "$#" -ge 2 ] || die "--install-dir requires a path"
install_dir=$2
shift 2
;;
--cursor-public-url)
[ "$#" -ge 2 ] || die "--cursor-public-url requires an HTTPS origin"
cursor_public_url=$2
shift 2
;;
--cursor-hostname)
[ "$#" -ge 2 ] || die "--cursor-hostname requires a public hostname"
cursor_hostname=$2
shift 2
;;
--prepare-only)
prepare_only=true
shift
;;
--api-key)
configure_provider_keys="$configure_provider_keys kimi-api"
shift
;;
--kimi-api-key)
configure_provider_keys="$configure_provider_keys kimi-api"
shift
;;
--deepseek-api-key)
configure_provider_keys="$configure_provider_keys deepseek"
shift
;;
--grok-api-key)
configure_provider_keys="$configure_provider_keys grok-api"
shift
;;
--anthropic-api-key)
configure_provider_keys="$configure_provider_keys anthropic-api"
shift
;;
--guided)
guided=true
shift
;;
--auto)
guided=false
shift
;;
--providers)
[ "$#" -ge 2 ] || die "--providers requires a comma-separated list"
providers=$2
guided=false
shift 2
;;
--with-tray)
with_tray=true
shift
;;
--no-tray)
no_tray=true
shift
;;
--no-provider)
no_provider=true
shift
;;
--no-discovery)
no_discovery=true
shift
;;
--migrate-known)
migrate_known=true
shift
;;
--smoke-test)
smoke_test=true
shift
;;
--force)
force=true
shift
;;
-h|--help)
usage
exit 0
;;
*)
die "unknown option: $1"
;;
esac
done
case "$target" in
codex|dsh|gemini|cursor|claude|openclaw) ;;
*) die "--target must be codex, dsh, gemini, cursor, claude, or openclaw" ;;
esac
if [ -n "$cursor_public_url" ]; then
[ "$target" = cursor ] || die "--cursor-public-url applies to --target cursor only"
MODEL_ROUTER_CURSOR_PUBLIC_BASE_URL=$cursor_public_url
export MODEL_ROUTER_CURSOR_PUBLIC_BASE_URL
fi
if [ -n "$cursor_hostname" ]; then
[ "$target" = cursor ] || die "--cursor-hostname applies to --target cursor only"
[ -z "$cursor_public_url" ] || die "use either --cursor-hostname or --cursor-public-url, not both"
MODEL_ROUTER_CURSOR_TUNNEL_HOSTNAME=$cursor_hostname
export MODEL_ROUTER_CURSOR_TUNNEL_HOSTNAME
fi
# Legacy migration replaces an older router's managed Codex config block, and
# the native catalog is the ChatGPT-plan model list Codex adopts. Neither has a
# counterpart in the harness, whose integration is one settings section.
if [ "$target" != codex ] && [ "$migrate_known" = true ]; then
die "--migrate-known applies only to the Codex target"
fi
# An idle install is exactly "no providers", so naming providers or pasting
# keys alongside it is a contradiction; and --no-discovery alone would select
# providers that can never authenticate.
if [ "$no_provider" = true ] && { [ -n "$providers" ] || [ -n "$configure_provider_keys" ] || [ "$guided" = true ]; }; then
die "--no-provider cannot be combined with --guided, --providers, or key flags"
fi
if [ "$no_discovery" = true ] && [ "$no_provider" != true ]; then
die "--no-discovery requires --no-provider"
fi
MODEL_ROUTER_TARGET=$target
export MODEL_ROUTER_TARGET
repo_dir=
case "$0" in
install.sh|*/install.sh)
candidate_dir=$(CDPATH= cd -- "$(dirname -- "$0")" 2>/dev/null && pwd || true)
if [ -n "$candidate_dir" ] &&
[ -x "$candidate_dir/bin/install" ] &&
[ -f "$candidate_dir/package.json" ] &&
grep -q '"name": "codex-model-router"' "$candidate_dir/package.json"; then
repo_dir=$candidate_dir
fi
;;
esac
if [ -z "$repo_dir" ]; then
command -v git >/dev/null 2>&1 || die "git is required to download codex-router"
if [ -d "$install_dir/.git" ]; then
origin_url=$(git -C "$install_dir" remote get-url origin 2>/dev/null || true)
case "$origin_url" in
"$repository_url"|https://github.com/duolahypercho/codex-router|https://github.com/duolahypercho/codex-router.git|git@github.com:duolahypercho/codex-router.git)
;;
*)
die "$install_dir already contains a different Git repository"
;;
esac
# Mirrors requireReplaceableCheckout() in src/update.mjs, including its
# refusal to reach for `git clean`: --force restores files git already
# tracks and leaves untracked files exactly where they are, because an
# update has no business deleting work git was never asked to track.
changes=$(local_modifications "$install_dir") ||
die "unable to read the Git status of $install_dir"
if [ -n "$changes" ]; then
if [ "$force" != true ]; then
local_modifications_message "$changes" "$install_dir" >&2
exit 1
fi
git -C "$install_dir" reset --hard HEAD ||
die "unable to discard the local changes in $install_dir"
fi
ensure_main_branch "$install_dir"
printf 'Updating %s...\n' "$install_dir"
previous_revision=$(git -C "$install_dir" rev-parse HEAD)
git -C "$install_dir" update-ref refs/codex-router/rollback "$previous_revision"
git -C "$install_dir" pull --ff-only origin main
elif [ -e "$install_dir" ]; then
die "$install_dir already exists and is not a codex-router checkout"
else
mkdir -p "$(dirname -- "$install_dir")"
printf 'Cloning codex-router to %s...\n' "$install_dir"
git clone --depth 1 "$repository_url" "$install_dir"
fi
repo_dir=$install_dir
fi
if [ "$prepare_only" = true ]; then
"$repo_dir/bin/install" --prepare-only
exit 0
fi
command -v node >/dev/null 2>&1 ||
die "Node.js 22.19+ is required; install Node.js 24 LTS from https://nodejs.org/"
command -v npm >/dev/null 2>&1 ||
die "npm is required and is normally included with Node.js"
# The key prompt imports modules from node_modules, which a fresh clone does
# not have yet: bin/install installs them, and it runs later. Doing it here is
# what makes the prompt work at all, and the failure has to reach the restore
# path rather than abort under `set -e`.
if [ -n "$configure_provider_keys" ]; then
node "$repo_dir/src/node-dependency-install.mjs" ||
restore_previous_revision "installing Node dependencies failed"
fi
for provider_id in $configure_provider_keys; do
"$repo_dir/bin/provider-key" "$provider_id" set
done
if [ "$guided" = auto ]; then
if [ "$no_provider" = true ]; then
guided=false
elif [ -t 1 ] && [ -r /dev/tty ] && [ -w /dev/tty ]; then
guided=true
else
guided=false
fi
fi
set --
if [ "$guided" = true ]; then set -- "$@" --guided; fi
if [ -n "$providers" ]; then set -- "$@" --providers "$providers"; fi
if [ "$migrate_known" = true ]; then set -- "$@" --migrate-known; fi
if [ "$smoke_test" = true ]; then set -- "$@" --smoke-test; fi
if [ "$with_tray" = true ]; then set -- "$@" --with-tray; fi
if [ "$no_tray" = true ]; then set -- "$@" --no-tray; fi
if [ "$no_provider" = true ]; then set -- "$@" --no-provider; fi
if [ "$no_discovery" = true ]; then set -- "$@" --no-discovery; fi
setup_status=0
"$repo_dir/bin/setup" "$@" || setup_status=$?
# Exit 2 means setup left configuration unfinished (a declined prompt, a
# missing credential) and says nothing about the code that was just pulled.
# Rolling back there discards the update the user ran this for, and if the
# unfinished step is itself the bug being fixed, every retry repeats it. Any
# other non-zero status still restores the checkout, so the running service is
# never left on half-applied code by an unrecognized failure.
if [ "$setup_status" -eq 2 ]; then
printf 'setup did not finish configuring; the update was kept. Re-run setup to continue, or ./bin/rollback to return to the previous revision.\n' >&2
exit 2
elif [ "$setup_status" -ne 0 ]; then
restore_previous_revision "setup failed"
fi
case "$target" in
dsh)
printf '\nCodex Router is installed for DeepSeek Harness. Its route is live on the next request; no restart is needed.\n'
;;
gemini)
printf '\nCodex Router is installed for Gemini CLI. The next gemini invocation reads the new route.\n'
;;
cursor)
printf '\nCodex Router is installed for Cursor. Run cursor-router-agent for the CLI; fully quit and reopen Cursor App for its router/... models.\n'
;;
claude)
printf '\nCodex Router is installed for Claude Code. Run claude-router and choose a codex_router/anthropic/... model.\n'
;;
openclaw)
printf '\nCodex Router installed OpenClaw and published every routed model under its codex-router provider. Run openclaw to start.\n'
;;
*)
printf '\nCodex Router is installed. Fully quit Codex, reopen it, and start a new task.\n'
printf 'The model picker will show only the providers you enabled while preserving native GPT models.\n'
;;
esac