codex-profile 0.2.0
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.
- package/CHANGELOG.md +77 -0
- package/LICENSE +21 -0
- package/README.md +476 -0
- package/SECURITY.md +33 -0
- package/bin/codex-profile +1080 -0
- package/package.json +51 -0
|
@@ -0,0 +1,1080 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
|
|
5
|
+
PROGRAM="${0##*/}"
|
|
6
|
+
VERSION="0.2.0"
|
|
7
|
+
CODEX_APP="${CODEX_APP:-/Applications/Codex.app}"
|
|
8
|
+
CODEX_APP_BIN="${CODEX_APP_BIN:-$CODEX_APP/Contents/MacOS/Codex}"
|
|
9
|
+
CODEX_BUNDLED_CLI="${CODEX_BUNDLED_CLI:-$CODEX_APP/Contents/Resources/codex}"
|
|
10
|
+
CODEX_PROFILE_QUIT_ATTEMPTS="${CODEX_PROFILE_QUIT_ATTEMPTS:-30}"
|
|
11
|
+
CODEX_PROFILE_QUIT_SLEEP="${CODEX_PROFILE_QUIT_SLEEP:-0.5}"
|
|
12
|
+
CODEX_PROFILE_UPGRADE_REPO="${CODEX_PROFILE_UPGRADE_REPO:-https://github.com/Ducksss/codex-profiles.git}"
|
|
13
|
+
CODEX_PROFILE_UPGRADE_REF="${CODEX_PROFILE_UPGRADE_REF:-main}"
|
|
14
|
+
|
|
15
|
+
usage() {
|
|
16
|
+
cat <<EOF
|
|
17
|
+
$PROGRAM - run Codex with isolated CODEX_HOME profiles
|
|
18
|
+
|
|
19
|
+
Usage:
|
|
20
|
+
$PROGRAM app <profile> [workspace]
|
|
21
|
+
$PROGRAM cli <profile> [codex-args...]
|
|
22
|
+
$PROGRAM login <profile> [codex-login-args...]
|
|
23
|
+
$PROGRAM init <profile>
|
|
24
|
+
$PROGRAM remove <profile> [--yes]
|
|
25
|
+
$PROGRAM status [profile]
|
|
26
|
+
$PROGRAM status --json [profile]
|
|
27
|
+
$PROGRAM path <profile>
|
|
28
|
+
$PROGRAM logs <profile> [--path|--tail [lines]]
|
|
29
|
+
$PROGRAM clone-config <source-profile> <target-profile> [--force]
|
|
30
|
+
$PROGRAM list
|
|
31
|
+
$PROGRAM doctor [--json]
|
|
32
|
+
$PROGRAM completions <bash|zsh|fish>
|
|
33
|
+
$PROGRAM upgrade [--dry-run] [--prefix <path>] [--ref <git-ref>]
|
|
34
|
+
$PROGRAM version
|
|
35
|
+
|
|
36
|
+
Examples:
|
|
37
|
+
$PROGRAM login personal
|
|
38
|
+
$PROGRAM init work
|
|
39
|
+
$PROGRAM app personal ~/Dev/my-project
|
|
40
|
+
$PROGRAM cli personal exec "review this repo"
|
|
41
|
+
$PROGRAM status
|
|
42
|
+
$PROGRAM logs personal --tail 50
|
|
43
|
+
$PROGRAM clone-config personal work
|
|
44
|
+
$PROGRAM upgrade
|
|
45
|
+
|
|
46
|
+
Profiles:
|
|
47
|
+
default -> ~/.codex
|
|
48
|
+
any other name -> ~/.codex-<profile>
|
|
49
|
+
|
|
50
|
+
Environment:
|
|
51
|
+
CODEX_APP Override Codex.app path.
|
|
52
|
+
CODEX_APP_BIN Override Codex Desktop binary path.
|
|
53
|
+
CODEX_CLI Override Codex CLI binary path.
|
|
54
|
+
CODEX_PROFILE_UPGRADE_REPO Override upgrade repository.
|
|
55
|
+
CODEX_PROFILE_UPGRADE_REF Override upgrade git ref.
|
|
56
|
+
CODEX_PROFILE_UPGRADE_CACHE Override upgrade cache checkout.
|
|
57
|
+
CODEX_PROFILE_UPGRADE_PREFIX Override upgrade install prefix.
|
|
58
|
+
EOF
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
die() {
|
|
62
|
+
printf 'Error: %s\n' "$*" >&2
|
|
63
|
+
exit 1
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
note() {
|
|
67
|
+
printf '%s\n' "$*"
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
is_valid_profile_name() {
|
|
71
|
+
local profile="$1"
|
|
72
|
+
|
|
73
|
+
[[ "$profile" =~ ^[A-Za-z0-9][A-Za-z0-9._-]*$ ]]
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
validate_profile() {
|
|
77
|
+
local profile="$1"
|
|
78
|
+
|
|
79
|
+
if ! is_valid_profile_name "$profile"; then
|
|
80
|
+
die "Invalid profile '$profile'. Use letters, numbers, dots, dashes, or underscores."
|
|
81
|
+
fi
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
codex_home_for_profile() {
|
|
85
|
+
local profile="$1"
|
|
86
|
+
|
|
87
|
+
validate_profile "$profile"
|
|
88
|
+
|
|
89
|
+
if [[ "$profile" == "default" ]]; then
|
|
90
|
+
printf '%s/.codex\n' "$HOME"
|
|
91
|
+
else
|
|
92
|
+
printf '%s/.codex-%s\n' "$HOME" "$profile"
|
|
93
|
+
fi
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
discovered_profile_is_managed() {
|
|
97
|
+
local profile="$1"
|
|
98
|
+
local dir="$2"
|
|
99
|
+
local expected
|
|
100
|
+
|
|
101
|
+
is_valid_profile_name "$profile" || return 1
|
|
102
|
+
expected="$(codex_home_for_profile "$profile")"
|
|
103
|
+
[[ "$expected" == "$dir" ]]
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
discover_profiles() {
|
|
107
|
+
local include_default="${1:-yes}"
|
|
108
|
+
local dir profile
|
|
109
|
+
|
|
110
|
+
if [[ "$include_default" == "yes" && -d "$HOME/.codex" ]]; then
|
|
111
|
+
printf 'default\n'
|
|
112
|
+
fi
|
|
113
|
+
|
|
114
|
+
for dir in "$HOME"/.codex-*; do
|
|
115
|
+
[[ -d "$dir" ]] || continue
|
|
116
|
+
profile="${dir##*/.codex-}"
|
|
117
|
+
discovered_profile_is_managed "$profile" "$dir" || continue
|
|
118
|
+
printf '%s\n' "$profile"
|
|
119
|
+
done
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
ensure_home() {
|
|
123
|
+
local codex_home="$1"
|
|
124
|
+
|
|
125
|
+
mkdir -p "$codex_home" || die "Cannot create directory: $codex_home"
|
|
126
|
+
[[ -d "$codex_home" ]] || die "Not a directory: $codex_home"
|
|
127
|
+
chmod 700 "$codex_home" || die "Cannot set private permissions on: $codex_home"
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
codex_desktop_running() {
|
|
131
|
+
pgrep -x Codex > /dev/null 2>&1 && return 0
|
|
132
|
+
pgrep -f "$CODEX_BUNDLED_CLI app-server" > /dev/null 2>&1 && return 0
|
|
133
|
+
return 1
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
codex_cli_error_message() {
|
|
137
|
+
if [[ -n "${CODEX_CLI:-}" ]]; then
|
|
138
|
+
printf 'CODEX_CLI is set but not executable: %s\n' "$CODEX_CLI"
|
|
139
|
+
return 0
|
|
140
|
+
fi
|
|
141
|
+
|
|
142
|
+
printf 'Codex CLI not found. Install Codex or set CODEX_CLI=/path/to/codex.\n'
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
try_find_codex_cli() {
|
|
146
|
+
if [[ -n "${CODEX_CLI:-}" ]]; then
|
|
147
|
+
[[ -x "$CODEX_CLI" ]] || return 1
|
|
148
|
+
printf '%s\n' "$CODEX_CLI"
|
|
149
|
+
return 0
|
|
150
|
+
fi
|
|
151
|
+
|
|
152
|
+
if command -v codex > /dev/null 2>&1; then
|
|
153
|
+
command -v codex
|
|
154
|
+
return 0
|
|
155
|
+
fi
|
|
156
|
+
|
|
157
|
+
if [[ -x "$CODEX_BUNDLED_CLI" ]]; then
|
|
158
|
+
printf '%s\n' "$CODEX_BUNDLED_CLI"
|
|
159
|
+
return 0
|
|
160
|
+
fi
|
|
161
|
+
|
|
162
|
+
return 1
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
find_codex_cli() {
|
|
166
|
+
local codex_cli
|
|
167
|
+
|
|
168
|
+
if codex_cli="$(try_find_codex_cli)"; then
|
|
169
|
+
printf '%s\n' "$codex_cli"
|
|
170
|
+
return 0
|
|
171
|
+
fi
|
|
172
|
+
|
|
173
|
+
die "$(codex_cli_error_message)"
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
quit_codex_app() {
|
|
177
|
+
local attempt
|
|
178
|
+
|
|
179
|
+
if ! codex_desktop_running; then
|
|
180
|
+
return 0
|
|
181
|
+
fi
|
|
182
|
+
|
|
183
|
+
note "Quitting existing Codex app..."
|
|
184
|
+
osascript -e 'tell application "Codex" to quit' > /dev/null 2>&1 || true
|
|
185
|
+
|
|
186
|
+
for ((attempt = 0; attempt < CODEX_PROFILE_QUIT_ATTEMPTS; attempt++)); do
|
|
187
|
+
if ! codex_desktop_running; then
|
|
188
|
+
return 0
|
|
189
|
+
fi
|
|
190
|
+
sleep "$CODEX_PROFILE_QUIT_SLEEP"
|
|
191
|
+
done
|
|
192
|
+
|
|
193
|
+
die "Codex or its app-server is still running. Quit Codex with Cmd+Q, then retry."
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
command_app() {
|
|
197
|
+
local profile="${1:-}"
|
|
198
|
+
[[ -n "$profile" ]] || die "Usage: $PROGRAM app <profile> [workspace]"
|
|
199
|
+
shift || true
|
|
200
|
+
|
|
201
|
+
local workspace="${1:-$PWD}"
|
|
202
|
+
local codex_home
|
|
203
|
+
|
|
204
|
+
[[ -x "$CODEX_APP_BIN" ]] || die "Codex Desktop binary not found at $CODEX_APP_BIN"
|
|
205
|
+
|
|
206
|
+
codex_home="$(codex_home_for_profile "$profile")"
|
|
207
|
+
ensure_home "$codex_home"
|
|
208
|
+
quit_codex_app
|
|
209
|
+
|
|
210
|
+
local log_dir="$codex_home/logs"
|
|
211
|
+
local log_file="$log_dir/desktop.log"
|
|
212
|
+
ensure_home "$log_dir"
|
|
213
|
+
(umask 077 && : > "$log_file")
|
|
214
|
+
chmod 600 "$log_file" 2> /dev/null || true
|
|
215
|
+
|
|
216
|
+
note "Launching Codex Desktop with CODEX_HOME=$codex_home"
|
|
217
|
+
note "Log: $log_file"
|
|
218
|
+
(umask 077 && nohup env CODEX_HOME="$codex_home" "$CODEX_APP_BIN" "$workspace" > "$log_file" 2>&1 &)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
command_cli() {
|
|
222
|
+
local profile="${1:-}"
|
|
223
|
+
[[ -n "$profile" ]] || die "Usage: $PROGRAM cli <profile> [codex-args...]"
|
|
224
|
+
shift || true
|
|
225
|
+
|
|
226
|
+
local codex_home codex_cli
|
|
227
|
+
codex_home="$(codex_home_for_profile "$profile")"
|
|
228
|
+
ensure_home "$codex_home"
|
|
229
|
+
codex_cli="$(find_codex_cli)"
|
|
230
|
+
|
|
231
|
+
exec env CODEX_HOME="$codex_home" "$codex_cli" "$@"
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
command_login() {
|
|
235
|
+
local profile="${1:-}"
|
|
236
|
+
[[ -n "$profile" ]] || die "Usage: $PROGRAM login <profile> [codex-login-args...]"
|
|
237
|
+
shift || true
|
|
238
|
+
|
|
239
|
+
local codex_home codex_cli
|
|
240
|
+
codex_home="$(codex_home_for_profile "$profile")"
|
|
241
|
+
ensure_home "$codex_home"
|
|
242
|
+
codex_cli="$(find_codex_cli)"
|
|
243
|
+
|
|
244
|
+
exec env CODEX_HOME="$codex_home" "$codex_cli" login "$@"
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
command_init() {
|
|
248
|
+
local profile="${1:-}"
|
|
249
|
+
[[ -n "$profile" ]] || die "Usage: $PROGRAM init <profile>"
|
|
250
|
+
shift || true
|
|
251
|
+
[[ "$#" -eq 0 ]] || die "Usage: $PROGRAM init <profile>"
|
|
252
|
+
|
|
253
|
+
local codex_home existed
|
|
254
|
+
codex_home="$(codex_home_for_profile "$profile")"
|
|
255
|
+
existed=no
|
|
256
|
+
[[ -d "$codex_home" ]] && existed=yes
|
|
257
|
+
|
|
258
|
+
ensure_home "$codex_home"
|
|
259
|
+
|
|
260
|
+
if [[ "$existed" == "yes" ]]; then
|
|
261
|
+
note "Already initialized $profile ($codex_home)"
|
|
262
|
+
else
|
|
263
|
+
note "Initialized $profile ($codex_home)"
|
|
264
|
+
fi
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
command_remove() {
|
|
268
|
+
local profile="${1:-}"
|
|
269
|
+
[[ -n "$profile" ]] || die "Usage: $PROGRAM remove <profile> [--yes]"
|
|
270
|
+
shift || true
|
|
271
|
+
|
|
272
|
+
local yes=no
|
|
273
|
+
while [[ "$#" -gt 0 ]]; do
|
|
274
|
+
case "$1" in
|
|
275
|
+
--yes | -y)
|
|
276
|
+
yes=yes
|
|
277
|
+
;;
|
|
278
|
+
*)
|
|
279
|
+
die "Usage: $PROGRAM remove <profile> [--yes]"
|
|
280
|
+
;;
|
|
281
|
+
esac
|
|
282
|
+
shift
|
|
283
|
+
done
|
|
284
|
+
|
|
285
|
+
local codex_home confirmation
|
|
286
|
+
codex_home="$(codex_home_for_profile "$profile")"
|
|
287
|
+
|
|
288
|
+
if [[ ! -d "$codex_home" ]]; then
|
|
289
|
+
note "Not initialized $profile ($codex_home)"
|
|
290
|
+
return 0
|
|
291
|
+
fi
|
|
292
|
+
|
|
293
|
+
if [[ "$yes" != "yes" ]]; then
|
|
294
|
+
printf "Type '%s' to permanently remove %s: " "$profile" "$codex_home" >&2
|
|
295
|
+
if ! IFS= read -r confirmation && [[ -z "$confirmation" ]]; then
|
|
296
|
+
die "Confirmation required."
|
|
297
|
+
fi
|
|
298
|
+
if [[ "$confirmation" != "$profile" ]]; then
|
|
299
|
+
die "Confirmation did not match; nothing removed."
|
|
300
|
+
fi
|
|
301
|
+
fi
|
|
302
|
+
|
|
303
|
+
rm -rf -- "$codex_home" || die "Cannot remove profile home: $codex_home"
|
|
304
|
+
note "Removed $profile ($codex_home)"
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
command_status_one() {
|
|
308
|
+
local profile="$1"
|
|
309
|
+
local codex_home codex_cli
|
|
310
|
+
local status_output status_code
|
|
311
|
+
|
|
312
|
+
codex_home="$(codex_home_for_profile "$profile")"
|
|
313
|
+
|
|
314
|
+
if [[ ! -d "$codex_home" ]]; then
|
|
315
|
+
printf '%s (%s): Not initialized\n' "$profile" "$codex_home"
|
|
316
|
+
return 0
|
|
317
|
+
fi
|
|
318
|
+
|
|
319
|
+
codex_cli="$(find_codex_cli)"
|
|
320
|
+
|
|
321
|
+
printf '%s (%s): ' "$profile" "$codex_home"
|
|
322
|
+
|
|
323
|
+
set +e
|
|
324
|
+
status_output="$(env CODEX_HOME="$codex_home" "$codex_cli" login status 2>&1)"
|
|
325
|
+
status_code=$?
|
|
326
|
+
set -e
|
|
327
|
+
|
|
328
|
+
printf '%s\n' "$status_output"
|
|
329
|
+
|
|
330
|
+
if [[ "$status_code" -eq 0 || "$status_output" == "Not logged in" ]]; then
|
|
331
|
+
return 0
|
|
332
|
+
fi
|
|
333
|
+
|
|
334
|
+
return "$status_code"
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
json_escape() {
|
|
338
|
+
local value="$1"
|
|
339
|
+
|
|
340
|
+
value=${value//\\/\\\\}
|
|
341
|
+
value=${value//\"/\\\"}
|
|
342
|
+
value=${value//$'\b'/\\b}
|
|
343
|
+
value=${value//$'\f'/\\f}
|
|
344
|
+
value=${value//$'\n'/\\n}
|
|
345
|
+
value=${value//$'\r'/\\r}
|
|
346
|
+
value=${value//$'\t'/\\t}
|
|
347
|
+
value=${value//$'\001'/\\u0001}
|
|
348
|
+
value=${value//$'\002'/\\u0002}
|
|
349
|
+
value=${value//$'\003'/\\u0003}
|
|
350
|
+
value=${value//$'\004'/\\u0004}
|
|
351
|
+
value=${value//$'\005'/\\u0005}
|
|
352
|
+
value=${value//$'\006'/\\u0006}
|
|
353
|
+
value=${value//$'\007'/\\u0007}
|
|
354
|
+
value=${value//$'\013'/\\u000b}
|
|
355
|
+
value=${value//$'\016'/\\u000e}
|
|
356
|
+
value=${value//$'\017'/\\u000f}
|
|
357
|
+
value=${value//$'\020'/\\u0010}
|
|
358
|
+
value=${value//$'\021'/\\u0011}
|
|
359
|
+
value=${value//$'\022'/\\u0012}
|
|
360
|
+
value=${value//$'\023'/\\u0013}
|
|
361
|
+
value=${value//$'\024'/\\u0014}
|
|
362
|
+
value=${value//$'\025'/\\u0015}
|
|
363
|
+
value=${value//$'\026'/\\u0016}
|
|
364
|
+
value=${value//$'\027'/\\u0017}
|
|
365
|
+
value=${value//$'\030'/\\u0018}
|
|
366
|
+
value=${value//$'\031'/\\u0019}
|
|
367
|
+
value=${value//$'\032'/\\u001a}
|
|
368
|
+
value=${value//$'\033'/\\u001b}
|
|
369
|
+
value=${value//$'\034'/\\u001c}
|
|
370
|
+
value=${value//$'\035'/\\u001d}
|
|
371
|
+
value=${value//$'\036'/\\u001e}
|
|
372
|
+
value=${value//$'\037'/\\u001f}
|
|
373
|
+
printf '%s' "$value"
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
json_string() {
|
|
377
|
+
printf '"'
|
|
378
|
+
json_escape "$1"
|
|
379
|
+
printf '"'
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
json_status_object() {
|
|
383
|
+
local profile="$1"
|
|
384
|
+
local codex_home codex_cli status_output status_code state
|
|
385
|
+
|
|
386
|
+
codex_home="$(codex_home_for_profile "$profile")"
|
|
387
|
+
|
|
388
|
+
if [[ ! -d "$codex_home" ]]; then
|
|
389
|
+
printf '{"name":'
|
|
390
|
+
json_string "$profile"
|
|
391
|
+
printf ',"home":'
|
|
392
|
+
json_string "$codex_home"
|
|
393
|
+
printf ',"state":"not_initialized","status":"Not initialized","exit_code":0}'
|
|
394
|
+
return 0
|
|
395
|
+
fi
|
|
396
|
+
|
|
397
|
+
if ! codex_cli="$(try_find_codex_cli)"; then
|
|
398
|
+
printf '{"name":'
|
|
399
|
+
json_string "$profile"
|
|
400
|
+
printf ',"home":'
|
|
401
|
+
json_string "$codex_home"
|
|
402
|
+
printf ',"state":"error","status":'
|
|
403
|
+
json_string "$(codex_cli_error_message)"
|
|
404
|
+
printf ',"exit_code":1}'
|
|
405
|
+
return 1
|
|
406
|
+
fi
|
|
407
|
+
|
|
408
|
+
set +e
|
|
409
|
+
status_output="$(env CODEX_HOME="$codex_home" "$codex_cli" login status 2>&1)"
|
|
410
|
+
status_code=$?
|
|
411
|
+
set -e
|
|
412
|
+
|
|
413
|
+
if [[ "$status_code" -eq 0 ]]; then
|
|
414
|
+
state=ok
|
|
415
|
+
elif [[ "$status_output" == "Not logged in" ]]; then
|
|
416
|
+
state=not_logged_in
|
|
417
|
+
else
|
|
418
|
+
state=error
|
|
419
|
+
fi
|
|
420
|
+
|
|
421
|
+
printf '{"name":'
|
|
422
|
+
json_string "$profile"
|
|
423
|
+
printf ',"home":'
|
|
424
|
+
json_string "$codex_home"
|
|
425
|
+
printf ',"state":'
|
|
426
|
+
json_string "$state"
|
|
427
|
+
printf ',"status":'
|
|
428
|
+
json_string "$status_output"
|
|
429
|
+
printf ',"exit_code":%s}' "$status_code"
|
|
430
|
+
|
|
431
|
+
if [[ "$state" == "error" ]]; then
|
|
432
|
+
return "$status_code"
|
|
433
|
+
fi
|
|
434
|
+
|
|
435
|
+
return 0
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
json_emit_status_profiles_array() {
|
|
439
|
+
local requested_profile="${1:-}"
|
|
440
|
+
local status_code=0
|
|
441
|
+
local first=yes
|
|
442
|
+
local profile object object_status
|
|
443
|
+
|
|
444
|
+
printf '['
|
|
445
|
+
|
|
446
|
+
if [[ -n "$requested_profile" ]]; then
|
|
447
|
+
set +e
|
|
448
|
+
object="$(json_status_object "$requested_profile")"
|
|
449
|
+
object_status=$?
|
|
450
|
+
set -e
|
|
451
|
+
printf '%s' "$object"
|
|
452
|
+
printf ']'
|
|
453
|
+
return "$object_status"
|
|
454
|
+
fi
|
|
455
|
+
|
|
456
|
+
for profile in default $(discover_profiles no); do
|
|
457
|
+
if [[ "$first" == "yes" ]]; then
|
|
458
|
+
first=no
|
|
459
|
+
else
|
|
460
|
+
printf ','
|
|
461
|
+
fi
|
|
462
|
+
|
|
463
|
+
set +e
|
|
464
|
+
object="$(json_status_object "$profile")"
|
|
465
|
+
object_status=$?
|
|
466
|
+
set -e
|
|
467
|
+
printf '%s' "$object"
|
|
468
|
+
|
|
469
|
+
if [[ "$object_status" -ne 0 ]]; then
|
|
470
|
+
status_code="$object_status"
|
|
471
|
+
fi
|
|
472
|
+
done
|
|
473
|
+
|
|
474
|
+
printf ']'
|
|
475
|
+
return "$status_code"
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
command_status_json() {
|
|
479
|
+
local profile="${1:-}"
|
|
480
|
+
local status_code=0
|
|
481
|
+
|
|
482
|
+
printf '{"profiles":'
|
|
483
|
+
set +e
|
|
484
|
+
json_emit_status_profiles_array "$profile"
|
|
485
|
+
status_code=$?
|
|
486
|
+
set -e
|
|
487
|
+
printf '}\n'
|
|
488
|
+
|
|
489
|
+
return "$status_code"
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
command_status() {
|
|
493
|
+
local status_code=0
|
|
494
|
+
local json=no
|
|
495
|
+
local profile=""
|
|
496
|
+
|
|
497
|
+
while [[ "$#" -gt 0 ]]; do
|
|
498
|
+
case "$1" in
|
|
499
|
+
--json | -j)
|
|
500
|
+
json=yes
|
|
501
|
+
;;
|
|
502
|
+
-*)
|
|
503
|
+
die "Unknown status option '$1'."
|
|
504
|
+
;;
|
|
505
|
+
*)
|
|
506
|
+
[[ -z "$profile" ]] || die "Usage: $PROGRAM status [--json] [profile]"
|
|
507
|
+
profile="$1"
|
|
508
|
+
;;
|
|
509
|
+
esac
|
|
510
|
+
shift
|
|
511
|
+
done
|
|
512
|
+
|
|
513
|
+
if [[ "$json" == "yes" ]]; then
|
|
514
|
+
command_status_json "$profile"
|
|
515
|
+
return $?
|
|
516
|
+
fi
|
|
517
|
+
|
|
518
|
+
if [[ -n "$profile" ]]; then
|
|
519
|
+
command_status_one "$profile"
|
|
520
|
+
return $?
|
|
521
|
+
fi
|
|
522
|
+
|
|
523
|
+
command_status_one default || status_code=$?
|
|
524
|
+
|
|
525
|
+
local profile
|
|
526
|
+
for profile in $(discover_profiles no); do
|
|
527
|
+
command_status_one "$profile" || status_code=$?
|
|
528
|
+
done
|
|
529
|
+
|
|
530
|
+
return "$status_code"
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
command_list() {
|
|
534
|
+
discover_profiles yes
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
command_path() {
|
|
538
|
+
local profile="${1:-}"
|
|
539
|
+
[[ -n "$profile" ]] || die "Usage: $PROGRAM path <profile>"
|
|
540
|
+
codex_home_for_profile "$profile"
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
command_logs() {
|
|
544
|
+
local profile="${1:-}"
|
|
545
|
+
[[ -n "$profile" ]] || die "Usage: $PROGRAM logs <profile> [--path|--tail [lines]]"
|
|
546
|
+
shift || true
|
|
547
|
+
|
|
548
|
+
local mode="cat"
|
|
549
|
+
local lines=50
|
|
550
|
+
|
|
551
|
+
while [[ "$#" -gt 0 ]]; do
|
|
552
|
+
case "$1" in
|
|
553
|
+
--path)
|
|
554
|
+
mode="path"
|
|
555
|
+
;;
|
|
556
|
+
--tail)
|
|
557
|
+
mode="tail"
|
|
558
|
+
if [[ -n "${2:-}" && "$2" != -* ]]; then
|
|
559
|
+
lines="$2"
|
|
560
|
+
shift
|
|
561
|
+
fi
|
|
562
|
+
;;
|
|
563
|
+
-*)
|
|
564
|
+
die "Unknown logs option '$1'."
|
|
565
|
+
;;
|
|
566
|
+
*)
|
|
567
|
+
die "Usage: $PROGRAM logs <profile> [--path|--tail [lines]]"
|
|
568
|
+
;;
|
|
569
|
+
esac
|
|
570
|
+
shift
|
|
571
|
+
done
|
|
572
|
+
|
|
573
|
+
[[ "$lines" =~ ^[0-9]+$ ]] || die "Tail line count must be a non-negative integer."
|
|
574
|
+
|
|
575
|
+
local codex_home log_file
|
|
576
|
+
codex_home="$(codex_home_for_profile "$profile")"
|
|
577
|
+
log_file="$codex_home/logs/desktop.log"
|
|
578
|
+
|
|
579
|
+
if [[ "$mode" == "path" ]]; then
|
|
580
|
+
printf '%s\n' "$log_file"
|
|
581
|
+
return 0
|
|
582
|
+
fi
|
|
583
|
+
|
|
584
|
+
[[ -f "$log_file" ]] || die "No desktop log for $profile ($log_file)."
|
|
585
|
+
|
|
586
|
+
if [[ "$mode" == "tail" ]]; then
|
|
587
|
+
tail -n "$lines" "$log_file"
|
|
588
|
+
else
|
|
589
|
+
cat "$log_file"
|
|
590
|
+
fi
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
config_file_looks_sensitive() {
|
|
594
|
+
local file="$1"
|
|
595
|
+
|
|
596
|
+
grep -Eiq '(auth|token|secret|password|passwd|credential|api[_-]?key|access[_-]?key|private[_-]?key|oauth|bearer)' "$file"
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
command_clone_config() {
|
|
600
|
+
local source_profile="" target_profile="" force=no
|
|
601
|
+
|
|
602
|
+
while [[ "$#" -gt 0 ]]; do
|
|
603
|
+
case "$1" in
|
|
604
|
+
--force | -f)
|
|
605
|
+
force=yes
|
|
606
|
+
;;
|
|
607
|
+
-*)
|
|
608
|
+
die "Usage: $PROGRAM clone-config <source-profile> <target-profile> [--force]"
|
|
609
|
+
;;
|
|
610
|
+
*)
|
|
611
|
+
if [[ -z "$source_profile" ]]; then
|
|
612
|
+
source_profile="$1"
|
|
613
|
+
elif [[ -z "$target_profile" ]]; then
|
|
614
|
+
target_profile="$1"
|
|
615
|
+
else
|
|
616
|
+
die "Usage: $PROGRAM clone-config <source-profile> <target-profile> [--force]"
|
|
617
|
+
fi
|
|
618
|
+
;;
|
|
619
|
+
esac
|
|
620
|
+
shift
|
|
621
|
+
done
|
|
622
|
+
|
|
623
|
+
[[ -n "$source_profile" && -n "$target_profile" ]] || die "Usage: $PROGRAM clone-config <source-profile> <target-profile> [--force]"
|
|
624
|
+
[[ "$source_profile" != "$target_profile" ]] || die "Source and target profiles must be different."
|
|
625
|
+
|
|
626
|
+
local source_home target_home
|
|
627
|
+
source_home="$(codex_home_for_profile "$source_profile")"
|
|
628
|
+
target_home="$(codex_home_for_profile "$target_profile")"
|
|
629
|
+
|
|
630
|
+
[[ -d "$source_home" ]] || die "Source profile is not initialized: $source_profile ($source_home)"
|
|
631
|
+
ensure_home "$target_home"
|
|
632
|
+
|
|
633
|
+
local safe_files=("config.toml" "AGENTS.md" "instructions.md" "custom-instructions.md")
|
|
634
|
+
local file source_file target_file copied failed
|
|
635
|
+
copied=0
|
|
636
|
+
failed=0
|
|
637
|
+
|
|
638
|
+
for file in "${safe_files[@]}"; do
|
|
639
|
+
source_file="$source_home/$file"
|
|
640
|
+
target_file="$target_home/$file"
|
|
641
|
+
[[ -f "$source_file" ]] || continue
|
|
642
|
+
|
|
643
|
+
if [[ -L "$source_file" ]]; then
|
|
644
|
+
note "Refusing to copy $file because it is a symlink."
|
|
645
|
+
failed=1
|
|
646
|
+
continue
|
|
647
|
+
fi
|
|
648
|
+
|
|
649
|
+
if config_file_looks_sensitive "$source_file"; then
|
|
650
|
+
note "Refusing to copy $file because it contains sensitive-looking keys."
|
|
651
|
+
failed=1
|
|
652
|
+
continue
|
|
653
|
+
fi
|
|
654
|
+
|
|
655
|
+
if [[ -L "$target_file" ]]; then
|
|
656
|
+
note "Refusing to overwrite $file because the target is a symlink."
|
|
657
|
+
failed=1
|
|
658
|
+
continue
|
|
659
|
+
fi
|
|
660
|
+
|
|
661
|
+
if [[ -e "$target_file" && "$force" != "yes" ]]; then
|
|
662
|
+
note "Refusing to overwrite $file in $target_home. Use --force to overwrite."
|
|
663
|
+
failed=1
|
|
664
|
+
continue
|
|
665
|
+
fi
|
|
666
|
+
|
|
667
|
+
install -m 600 "$source_file" "$target_file" || die "Cannot copy $file to $target_home"
|
|
668
|
+
note "Copied $file"
|
|
669
|
+
copied=$((copied + 1))
|
|
670
|
+
done
|
|
671
|
+
|
|
672
|
+
if [[ "$failed" -ne 0 ]]; then
|
|
673
|
+
return 1
|
|
674
|
+
fi
|
|
675
|
+
|
|
676
|
+
if [[ "$copied" -eq 0 ]]; then
|
|
677
|
+
note "No safe config files found to clone from $source_profile."
|
|
678
|
+
fi
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
command_version() {
|
|
682
|
+
printf 'codex-profile %s\n' "$VERSION"
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
upgrade_cache_dir() {
|
|
686
|
+
if [[ -n "${CODEX_PROFILE_UPGRADE_CACHE:-}" ]]; then
|
|
687
|
+
printf '%s\n' "$CODEX_PROFILE_UPGRADE_CACHE"
|
|
688
|
+
elif [[ -n "${XDG_CACHE_HOME:-}" ]]; then
|
|
689
|
+
printf '%s/codex-profile/source\n' "$XDG_CACHE_HOME"
|
|
690
|
+
else
|
|
691
|
+
printf '%s/.cache/codex-profile/source\n' "$HOME"
|
|
692
|
+
fi
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
upgrade_prefix() {
|
|
696
|
+
printf '%s\n' "${CODEX_PROFILE_UPGRADE_PREFIX:-$HOME/.local}"
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
upgrade_print_plan() {
|
|
700
|
+
local repo="$1"
|
|
701
|
+
local ref="$2"
|
|
702
|
+
local cache="$3"
|
|
703
|
+
local prefix="$4"
|
|
704
|
+
|
|
705
|
+
note "Upgrade plan"
|
|
706
|
+
note "Repository: $repo"
|
|
707
|
+
note "Ref: $ref"
|
|
708
|
+
note "Cache: $cache"
|
|
709
|
+
note "Install prefix: $prefix"
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
version_core() {
|
|
713
|
+
local value="$1"
|
|
714
|
+
|
|
715
|
+
value="${value#codex-profile }"
|
|
716
|
+
value="${value%%$'\n'*}"
|
|
717
|
+
value="${value%%-*}"
|
|
718
|
+
printf '%s\n' "$value"
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
version_is_older_than_current() {
|
|
722
|
+
local candidate="$1"
|
|
723
|
+
local current="$VERSION"
|
|
724
|
+
local candidate_core current_core
|
|
725
|
+
local candidate_parts current_parts candidate_part current_part index
|
|
726
|
+
|
|
727
|
+
candidate_core="$(version_core "$candidate")"
|
|
728
|
+
current_core="$(version_core "$current")"
|
|
729
|
+
|
|
730
|
+
IFS=. read -r -a candidate_parts <<< "$candidate_core"
|
|
731
|
+
IFS=. read -r -a current_parts <<< "$current_core"
|
|
732
|
+
|
|
733
|
+
for index in 0 1 2; do
|
|
734
|
+
candidate_part="${candidate_parts[$index]:-0}"
|
|
735
|
+
current_part="${current_parts[$index]:-0}"
|
|
736
|
+
|
|
737
|
+
[[ "$candidate_part" =~ ^[0-9]+$ ]] || return 1
|
|
738
|
+
[[ "$current_part" =~ ^[0-9]+$ ]] || return 1
|
|
739
|
+
|
|
740
|
+
if ((10#$candidate_part < 10#$current_part)); then
|
|
741
|
+
return 0
|
|
742
|
+
fi
|
|
743
|
+
if ((10#$candidate_part > 10#$current_part)); then
|
|
744
|
+
return 1
|
|
745
|
+
fi
|
|
746
|
+
done
|
|
747
|
+
|
|
748
|
+
return 1
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
script_declared_version_output() {
|
|
752
|
+
local script="$1"
|
|
753
|
+
local declared_version
|
|
754
|
+
|
|
755
|
+
declared_version="$(sed -n 's/^VERSION="\([^"]*\)".*/\1/p' "$script" | sed -n '1p')"
|
|
756
|
+
[[ -n "$declared_version" ]] || return 1
|
|
757
|
+
printf 'codex-profile %s\n' "$declared_version"
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
upgrade_checkout_ref() {
|
|
761
|
+
local cache="$1"
|
|
762
|
+
local ref="$2"
|
|
763
|
+
|
|
764
|
+
git -C "$cache" fetch --tags --prune origin
|
|
765
|
+
if git -C "$cache" show-ref --verify --quiet "refs/remotes/origin/$ref"; then
|
|
766
|
+
git -C "$cache" checkout --quiet -B "$ref" "origin/$ref"
|
|
767
|
+
else
|
|
768
|
+
git -C "$cache" checkout --quiet "$ref"
|
|
769
|
+
fi
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
command_upgrade() {
|
|
773
|
+
local dry_run=no
|
|
774
|
+
local prefix
|
|
775
|
+
local ref="$CODEX_PROFILE_UPGRADE_REF"
|
|
776
|
+
local repo="$CODEX_PROFILE_UPGRADE_REPO"
|
|
777
|
+
local cache
|
|
778
|
+
prefix="$(upgrade_prefix)"
|
|
779
|
+
cache="$(upgrade_cache_dir)"
|
|
780
|
+
|
|
781
|
+
while [[ "$#" -gt 0 ]]; do
|
|
782
|
+
case "$1" in
|
|
783
|
+
--dry-run | -n)
|
|
784
|
+
dry_run=yes
|
|
785
|
+
;;
|
|
786
|
+
--prefix)
|
|
787
|
+
[[ -n "${2:-}" ]] || die "Usage: $PROGRAM upgrade [--dry-run] [--prefix <path>] [--ref <git-ref>]"
|
|
788
|
+
prefix="$2"
|
|
789
|
+
shift
|
|
790
|
+
;;
|
|
791
|
+
--ref)
|
|
792
|
+
[[ -n "${2:-}" ]] || die "Usage: $PROGRAM upgrade [--dry-run] [--prefix <path>] [--ref <git-ref>]"
|
|
793
|
+
ref="$2"
|
|
794
|
+
shift
|
|
795
|
+
;;
|
|
796
|
+
*)
|
|
797
|
+
die "Usage: $PROGRAM upgrade [--dry-run] [--prefix <path>] [--ref <git-ref>]"
|
|
798
|
+
;;
|
|
799
|
+
esac
|
|
800
|
+
shift
|
|
801
|
+
done
|
|
802
|
+
|
|
803
|
+
[[ -n "$repo" ]] || die "Upgrade repository cannot be empty."
|
|
804
|
+
[[ -n "$ref" ]] || die "Upgrade ref cannot be empty."
|
|
805
|
+
[[ -n "$cache" ]] || die "Upgrade cache cannot be empty."
|
|
806
|
+
[[ -n "$prefix" ]] || die "Upgrade prefix cannot be empty."
|
|
807
|
+
[[ "$repo" != -* ]] || die "Upgrade repository must not start with '-'."
|
|
808
|
+
[[ "$ref" != -* ]] || die "Upgrade ref must not start with '-'."
|
|
809
|
+
|
|
810
|
+
upgrade_print_plan "$repo" "$ref" "$cache" "$prefix"
|
|
811
|
+
|
|
812
|
+
if [[ "$dry_run" == "yes" ]]; then
|
|
813
|
+
return 0
|
|
814
|
+
fi
|
|
815
|
+
|
|
816
|
+
command -v git > /dev/null 2>&1 || die "git is required to upgrade codex-profile."
|
|
817
|
+
command -v make > /dev/null 2>&1 || die "make is required to upgrade codex-profile."
|
|
818
|
+
|
|
819
|
+
if [[ -e "$cache" && ! -d "$cache/.git" ]]; then
|
|
820
|
+
die "Upgrade cache exists but is not a git checkout: $cache"
|
|
821
|
+
fi
|
|
822
|
+
|
|
823
|
+
if [[ ! -d "$cache/.git" ]]; then
|
|
824
|
+
mkdir -p "$(dirname "$cache")" || die "Cannot create upgrade cache parent: $(dirname "$cache")"
|
|
825
|
+
git clone "$repo" "$cache"
|
|
826
|
+
upgrade_checkout_ref "$cache" "$ref"
|
|
827
|
+
else
|
|
828
|
+
local origin dirty
|
|
829
|
+
origin="$(git -C "$cache" remote get-url origin 2> /dev/null || true)"
|
|
830
|
+
[[ "$origin" == "$repo" ]] || die "Cached upgrade checkout origin differs from $repo: $origin"
|
|
831
|
+
|
|
832
|
+
dirty="$(git -C "$cache" status --porcelain)"
|
|
833
|
+
[[ -z "$dirty" ]] || die "Cached upgrade checkout has local changes: $cache"
|
|
834
|
+
|
|
835
|
+
upgrade_checkout_ref "$cache" "$ref"
|
|
836
|
+
fi
|
|
837
|
+
|
|
838
|
+
[[ -f "$cache/Makefile" ]] || die "Upgrade checkout is missing Makefile: $cache"
|
|
839
|
+
[[ -f "$cache/bin/codex-profile" ]] || die "Upgrade checkout is missing bin/codex-profile: $cache"
|
|
840
|
+
|
|
841
|
+
local candidate_version
|
|
842
|
+
candidate_version="$(script_declared_version_output "$cache/bin/codex-profile" || true)"
|
|
843
|
+
[[ -n "$candidate_version" ]] || die "Refusing to install candidate without a declared VERSION."
|
|
844
|
+
if [[ -n "$candidate_version" ]] && version_is_older_than_current "$candidate_version"; then
|
|
845
|
+
die "Refusing to install older $candidate_version over codex-profile $VERSION."
|
|
846
|
+
fi
|
|
847
|
+
|
|
848
|
+
make -C "$cache" install PREFIX="$prefix"
|
|
849
|
+
|
|
850
|
+
local installed version_output
|
|
851
|
+
installed="$prefix/bin/codex-profile"
|
|
852
|
+
[[ -x "$installed" ]] || die "Upgrade did not install executable: $installed"
|
|
853
|
+
version_output="$("$installed" version 2> /dev/null || "$installed" --version 2> /dev/null || true)"
|
|
854
|
+
[[ -n "$version_output" ]] || version_output="codex-profile version unknown"
|
|
855
|
+
note "Installed $version_output to $installed"
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
command_completions() {
|
|
859
|
+
local shell="${1:-}"
|
|
860
|
+
[[ -n "$shell" ]] || die "Usage: $PROGRAM completions <bash|zsh|fish>"
|
|
861
|
+
shift || true
|
|
862
|
+
[[ "$#" -eq 0 ]] || die "Usage: $PROGRAM completions <bash|zsh|fish>"
|
|
863
|
+
|
|
864
|
+
case "$shell" in
|
|
865
|
+
bash)
|
|
866
|
+
cat <<'EOF'
|
|
867
|
+
_codex_profile()
|
|
868
|
+
{
|
|
869
|
+
local cur command profiles
|
|
870
|
+
COMPREPLY=()
|
|
871
|
+
cur="${COMP_WORDS[COMP_CWORD]}"
|
|
872
|
+
command="${COMP_WORDS[1]}"
|
|
873
|
+
|
|
874
|
+
if [[ "$COMP_CWORD" -eq 1 ]]; then
|
|
875
|
+
COMPREPLY=( $(compgen -W "app cli login init remove status path logs clone-config list doctor completions upgrade version help" -- "$cur") )
|
|
876
|
+
return 0
|
|
877
|
+
fi
|
|
878
|
+
|
|
879
|
+
case "$command" in
|
|
880
|
+
app|cli|login|init|remove|status|path|logs)
|
|
881
|
+
if [[ "$COMP_CWORD" -eq 2 ]]; then
|
|
882
|
+
profiles="$(codex-profile list 2>/dev/null)"
|
|
883
|
+
COMPREPLY=( $(compgen -W "default personal work $profiles" -- "$cur") )
|
|
884
|
+
fi
|
|
885
|
+
;;
|
|
886
|
+
clone-config)
|
|
887
|
+
if [[ "$COMP_CWORD" -eq 2 || "$COMP_CWORD" -eq 3 ]]; then
|
|
888
|
+
profiles="$(codex-profile list 2>/dev/null)"
|
|
889
|
+
COMPREPLY=( $(compgen -W "default personal work $profiles" -- "$cur") )
|
|
890
|
+
fi
|
|
891
|
+
;;
|
|
892
|
+
completions)
|
|
893
|
+
if [[ "$COMP_CWORD" -eq 2 ]]; then
|
|
894
|
+
COMPREPLY=( $(compgen -W "bash zsh fish" -- "$cur") )
|
|
895
|
+
fi
|
|
896
|
+
;;
|
|
897
|
+
esac
|
|
898
|
+
}
|
|
899
|
+
complete -F _codex_profile codex-profile
|
|
900
|
+
EOF
|
|
901
|
+
;;
|
|
902
|
+
zsh)
|
|
903
|
+
cat <<'EOF'
|
|
904
|
+
#compdef codex-profile
|
|
905
|
+
|
|
906
|
+
_codex_profile() {
|
|
907
|
+
local -a commands profiles shells
|
|
908
|
+
commands=(
|
|
909
|
+
app cli login init remove status path logs clone-config list doctor completions upgrade version help
|
|
910
|
+
)
|
|
911
|
+
profiles=(${(f)"$(codex-profile list 2>/dev/null)"} default personal work)
|
|
912
|
+
shells=(bash zsh fish)
|
|
913
|
+
|
|
914
|
+
if (( CURRENT == 2 )); then
|
|
915
|
+
_describe 'command' commands
|
|
916
|
+
return
|
|
917
|
+
fi
|
|
918
|
+
|
|
919
|
+
case "$words[2]" in
|
|
920
|
+
app|cli|login|init|remove|status|path|logs)
|
|
921
|
+
if (( CURRENT == 3 )); then
|
|
922
|
+
_describe 'profile' profiles
|
|
923
|
+
fi
|
|
924
|
+
;;
|
|
925
|
+
clone-config)
|
|
926
|
+
if (( CURRENT == 3 || CURRENT == 4 )); then
|
|
927
|
+
_describe 'profile' profiles
|
|
928
|
+
fi
|
|
929
|
+
;;
|
|
930
|
+
completions)
|
|
931
|
+
if (( CURRENT == 3 )); then
|
|
932
|
+
_describe 'shell' shells
|
|
933
|
+
fi
|
|
934
|
+
;;
|
|
935
|
+
esac
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
_codex_profile "$@"
|
|
939
|
+
EOF
|
|
940
|
+
;;
|
|
941
|
+
fish)
|
|
942
|
+
cat <<'EOF'
|
|
943
|
+
complete -c codex-profile -f
|
|
944
|
+
complete -c codex-profile -n '__fish_is_first_arg' -a 'app cli login init remove status path logs clone-config list doctor completions upgrade version help'
|
|
945
|
+
complete -c codex-profile -n '__fish_seen_subcommand_from app cli login init remove status path logs' -a '(codex-profile list 2>/dev/null) default personal work'
|
|
946
|
+
complete -c codex-profile -n '__fish_seen_subcommand_from clone-config' -a '(codex-profile list 2>/dev/null) default personal work'
|
|
947
|
+
complete -c codex-profile -n '__fish_seen_subcommand_from completions' -a 'bash zsh fish'
|
|
948
|
+
EOF
|
|
949
|
+
;;
|
|
950
|
+
*)
|
|
951
|
+
die "Unsupported shell '$shell'. Use bash, zsh, or fish."
|
|
952
|
+
;;
|
|
953
|
+
esac
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
command_doctor() {
|
|
957
|
+
local json=no
|
|
958
|
+
local codex_cli
|
|
959
|
+
|
|
960
|
+
while [[ "$#" -gt 0 ]]; do
|
|
961
|
+
case "$1" in
|
|
962
|
+
--json | -j)
|
|
963
|
+
json=yes
|
|
964
|
+
;;
|
|
965
|
+
*)
|
|
966
|
+
die "Usage: $PROGRAM doctor [--json]"
|
|
967
|
+
;;
|
|
968
|
+
esac
|
|
969
|
+
shift
|
|
970
|
+
done
|
|
971
|
+
|
|
972
|
+
if [[ "$json" == "yes" ]]; then
|
|
973
|
+
local desktop_found=false cli_version status_code=0
|
|
974
|
+
[[ -x "$CODEX_APP_BIN" ]] && desktop_found=true
|
|
975
|
+
|
|
976
|
+
printf '{"desktop":{"found":%s,"path":' "$desktop_found"
|
|
977
|
+
json_string "$CODEX_APP_BIN"
|
|
978
|
+
printf '}'
|
|
979
|
+
|
|
980
|
+
if codex_cli="$(try_find_codex_cli)"; then
|
|
981
|
+
cli_version="$("$codex_cli" --version 2>&1 || true)"
|
|
982
|
+
printf ',"cli":{"found":true,"path":'
|
|
983
|
+
json_string "$codex_cli"
|
|
984
|
+
printf ',"version":'
|
|
985
|
+
json_string "$cli_version"
|
|
986
|
+
printf '},"status":{"skipped":false,"profiles":'
|
|
987
|
+
set +e
|
|
988
|
+
json_emit_status_profiles_array
|
|
989
|
+
status_code=$?
|
|
990
|
+
set -e
|
|
991
|
+
printf '}}\n'
|
|
992
|
+
return "$status_code"
|
|
993
|
+
fi
|
|
994
|
+
|
|
995
|
+
printf ',"cli":{"found":false,"path":null,"version":null},"status":{"skipped":true,"reason":'
|
|
996
|
+
json_string "$(codex_cli_error_message)"
|
|
997
|
+
printf '}}\n'
|
|
998
|
+
return 0
|
|
999
|
+
fi
|
|
1000
|
+
|
|
1001
|
+
note "Codex profile doctor"
|
|
1002
|
+
note ""
|
|
1003
|
+
|
|
1004
|
+
if [[ -x "$CODEX_APP_BIN" ]]; then
|
|
1005
|
+
note "Desktop: $CODEX_APP_BIN"
|
|
1006
|
+
else
|
|
1007
|
+
note "Desktop: missing ($CODEX_APP_BIN)"
|
|
1008
|
+
fi
|
|
1009
|
+
|
|
1010
|
+
if codex_cli="$(find_codex_cli 2> /dev/null)"; then
|
|
1011
|
+
note "CLI: $codex_cli"
|
|
1012
|
+
"$codex_cli" --version || true
|
|
1013
|
+
else
|
|
1014
|
+
note "CLI: missing"
|
|
1015
|
+
note ""
|
|
1016
|
+
note "Status: skipped"
|
|
1017
|
+
return 0
|
|
1018
|
+
fi
|
|
1019
|
+
|
|
1020
|
+
note ""
|
|
1021
|
+
command_status
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
main() {
|
|
1025
|
+
local command="${1:-help}"
|
|
1026
|
+
shift || true
|
|
1027
|
+
|
|
1028
|
+
case "$command" in
|
|
1029
|
+
app)
|
|
1030
|
+
command_app "$@"
|
|
1031
|
+
;;
|
|
1032
|
+
cli)
|
|
1033
|
+
command_cli "$@"
|
|
1034
|
+
;;
|
|
1035
|
+
login)
|
|
1036
|
+
command_login "$@"
|
|
1037
|
+
;;
|
|
1038
|
+
init)
|
|
1039
|
+
command_init "$@"
|
|
1040
|
+
;;
|
|
1041
|
+
remove)
|
|
1042
|
+
command_remove "$@"
|
|
1043
|
+
;;
|
|
1044
|
+
status)
|
|
1045
|
+
command_status "$@"
|
|
1046
|
+
;;
|
|
1047
|
+
path)
|
|
1048
|
+
command_path "$@"
|
|
1049
|
+
;;
|
|
1050
|
+
logs)
|
|
1051
|
+
command_logs "$@"
|
|
1052
|
+
;;
|
|
1053
|
+
clone-config)
|
|
1054
|
+
command_clone_config "$@"
|
|
1055
|
+
;;
|
|
1056
|
+
list)
|
|
1057
|
+
command_list
|
|
1058
|
+
;;
|
|
1059
|
+
doctor)
|
|
1060
|
+
command_doctor "$@"
|
|
1061
|
+
;;
|
|
1062
|
+
completions)
|
|
1063
|
+
command_completions "$@"
|
|
1064
|
+
;;
|
|
1065
|
+
upgrade)
|
|
1066
|
+
command_upgrade "$@"
|
|
1067
|
+
;;
|
|
1068
|
+
version | --version)
|
|
1069
|
+
command_version
|
|
1070
|
+
;;
|
|
1071
|
+
help | -h | --help)
|
|
1072
|
+
usage
|
|
1073
|
+
;;
|
|
1074
|
+
*)
|
|
1075
|
+
die "Unknown command '$command'. See '$PROGRAM help'."
|
|
1076
|
+
;;
|
|
1077
|
+
esac
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
main "$@"
|