codex-sw 0.3.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 +16 -0
- package/LICENSE +21 -0
- package/README.md +57 -0
- package/docs/macos-manual-checklist.md +34 -0
- package/docs/publish.md +41 -0
- package/docs/upgrade.md +31 -0
- package/package.json +46 -0
- package/plugins/codex-switcher/.app.json +3 -0
- package/plugins/codex-switcher/.codex-plugin/plugin.json +48 -0
- package/plugins/codex-switcher/.mcp.json +3 -0
- package/plugins/codex-switcher/README.md +70 -0
- package/plugins/codex-switcher/assets/icon.png +0 -0
- package/plugins/codex-switcher/assets/logo.png +0 -0
- package/plugins/codex-switcher/assets/screenshot1.png +0 -0
- package/plugins/codex-switcher/hooks.json +3 -0
- package/plugins/codex-switcher/scripts/codex-sw +5 -0
- package/plugins/codex-switcher/scripts/codex-switcher +928 -0
- package/plugins/codex-switcher/scripts/test-switcher.sh +120 -0
- package/plugins/codex-switcher/skills/README.md +3 -0
- package/scripts/install.sh +15 -0
- package/scripts/uninstall.sh +37 -0
|
@@ -0,0 +1,928 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
STATE_DIR="${CODEX_SWITCHER_STATE_DIR:-$HOME/.codex-switcher}"
|
|
5
|
+
PROFILES_DIR="${CODEX_SWITCHER_PROFILES_DIR:-$HOME/.codex-profiles}"
|
|
6
|
+
APP_LOG="${CODEX_SWITCHER_APP_LOG:-$STATE_DIR/app.log}"
|
|
7
|
+
SWITCH_LOG="${CODEX_SWITCHER_SWITCH_LOG:-$STATE_DIR/switcher.log}"
|
|
8
|
+
APP_PID_FILE="$STATE_DIR/app.pid"
|
|
9
|
+
LOCK_DIR="$STATE_DIR/.lock"
|
|
10
|
+
LOCK_WAIT_SECONDS="${CODEX_SWITCHER_LOCK_WAIT_SECONDS:-10}"
|
|
11
|
+
|
|
12
|
+
SCRIPT_NAME="${CODEX_SWITCHER_INVOKED_AS:-$(basename "$0")}"
|
|
13
|
+
|
|
14
|
+
usage() {
|
|
15
|
+
cat <<'USAGE'
|
|
16
|
+
Usage:
|
|
17
|
+
codex-sw add <profile>
|
|
18
|
+
codex-sw remove <profile> [--force]
|
|
19
|
+
codex-sw list
|
|
20
|
+
codex-sw use <profile>
|
|
21
|
+
codex-sw switch <profile>
|
|
22
|
+
codex-sw current [cli|app]
|
|
23
|
+
codex-sw status
|
|
24
|
+
|
|
25
|
+
codex-sw exec -- <codex args...>
|
|
26
|
+
codex-sw login [profile]
|
|
27
|
+
codex-sw logout [profile]
|
|
28
|
+
codex-sw env [profile]
|
|
29
|
+
|
|
30
|
+
codex-sw app open [profile] [-- <app args...>]
|
|
31
|
+
codex-sw app use <profile> [-- <app args...>]
|
|
32
|
+
codex-sw app logout [profile]
|
|
33
|
+
codex-sw app status
|
|
34
|
+
codex-sw app stop
|
|
35
|
+
codex-sw app current
|
|
36
|
+
|
|
37
|
+
codex-sw init [--shell zsh|bash] [--dry-run]
|
|
38
|
+
codex-sw recover [--dry-run]
|
|
39
|
+
codex-sw check
|
|
40
|
+
codex-sw doctor [--fix]
|
|
41
|
+
|
|
42
|
+
Compatibility:
|
|
43
|
+
codex-switcher <same-commands>
|
|
44
|
+
|
|
45
|
+
Environment overrides:
|
|
46
|
+
CODEX_SWITCHER_STATE_DIR
|
|
47
|
+
CODEX_SWITCHER_PROFILES_DIR
|
|
48
|
+
CODEX_SWITCHER_APP_BIN
|
|
49
|
+
CODEX_SWITCHER_APP_LOG
|
|
50
|
+
CODEX_SWITCHER_SWITCH_LOG
|
|
51
|
+
CODEX_SWITCHER_LOCK_WAIT_SECONDS
|
|
52
|
+
USAGE
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
err() {
|
|
56
|
+
echo "Error: $*" >&2
|
|
57
|
+
exit 1
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
warn() {
|
|
61
|
+
echo "Warning: $*" >&2
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
now_utc() {
|
|
65
|
+
date -u +"%Y-%m-%dT%H:%M:%SZ"
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
redact() {
|
|
69
|
+
sed -E \
|
|
70
|
+
-e 's/sk-[A-Za-z0-9_-]{8,}/sk-***REDACTED***/g' \
|
|
71
|
+
-e 's/(access_token|refresh_token|id_token)=([^[:space:]]+)/\1=REDACTED/g'
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
log_event() {
|
|
75
|
+
local level="$1"
|
|
76
|
+
shift
|
|
77
|
+
local message="$*"
|
|
78
|
+
mkdir -p "$STATE_DIR"
|
|
79
|
+
chmod 700 "$STATE_DIR" 2>/dev/null || true
|
|
80
|
+
printf '%s [%s] %s\n' "$(now_utc)" "$level" "$message" | redact >> "$SWITCH_LOG"
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
ensure_dirs() {
|
|
84
|
+
mkdir -p "$STATE_DIR" "$PROFILES_DIR"
|
|
85
|
+
chmod 700 "$STATE_DIR" "$PROFILES_DIR" 2>/dev/null || true
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
perm_of() {
|
|
89
|
+
local path="$1"
|
|
90
|
+
if stat -f '%Lp' "$path" >/dev/null 2>&1; then
|
|
91
|
+
stat -f '%Lp' "$path"
|
|
92
|
+
else
|
|
93
|
+
stat -c '%a' "$path"
|
|
94
|
+
fi
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
validate_profile_name_noexit() {
|
|
98
|
+
local name="${1:-}"
|
|
99
|
+
[[ -n "$name" ]] || return 1
|
|
100
|
+
[[ "$name" =~ ^[A-Za-z0-9._-]+$ ]] || return 1
|
|
101
|
+
return 0
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
validate_profile_name() {
|
|
105
|
+
local name="${1:-}"
|
|
106
|
+
validate_profile_name_noexit "$name" || err "invalid profile '$name' (allowed: A-Z a-z 0-9 . _ -)"
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
default_profile_for() {
|
|
110
|
+
local target="$1"
|
|
111
|
+
if [[ "$target" == "cli" ]]; then
|
|
112
|
+
echo "default"
|
|
113
|
+
else
|
|
114
|
+
echo "app-default"
|
|
115
|
+
fi
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
profile_path() {
|
|
119
|
+
local name="$1"
|
|
120
|
+
echo "$PROFILES_DIR/$name"
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
ensure_profile() {
|
|
124
|
+
local name="$1"
|
|
125
|
+
local p
|
|
126
|
+
p="$(profile_path "$name")"
|
|
127
|
+
mkdir -p "$p"
|
|
128
|
+
chmod 700 "$p" 2>/dev/null || true
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
require_profile_exists() {
|
|
132
|
+
local name="$1"
|
|
133
|
+
local p
|
|
134
|
+
p="$(profile_path "$name")"
|
|
135
|
+
[[ -d "$p" ]] || err "profile '$name' not found. run: $SCRIPT_NAME add $name"
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
current_file() {
|
|
139
|
+
local target="$1"
|
|
140
|
+
echo "$STATE_DIR/current_${target}"
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
set_current() {
|
|
144
|
+
local target="$1"
|
|
145
|
+
local profile="$2"
|
|
146
|
+
local file tmp
|
|
147
|
+
file="$(current_file "$target")"
|
|
148
|
+
tmp="${file}.tmp"
|
|
149
|
+
printf '%s\n' "$profile" > "$tmp"
|
|
150
|
+
chmod 600 "$tmp" 2>/dev/null || true
|
|
151
|
+
mv "$tmp" "$file"
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
read_current() {
|
|
155
|
+
local target="$1"
|
|
156
|
+
local file value default
|
|
157
|
+
file="$(current_file "$target")"
|
|
158
|
+
default="$(default_profile_for "$target")"
|
|
159
|
+
|
|
160
|
+
if [[ ! -f "$file" ]]; then
|
|
161
|
+
echo "$default"
|
|
162
|
+
return 1
|
|
163
|
+
fi
|
|
164
|
+
|
|
165
|
+
value="$(head -n 1 "$file" | tr -d '\r' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')"
|
|
166
|
+
if ! validate_profile_name_noexit "$value"; then
|
|
167
|
+
echo "$default"
|
|
168
|
+
return 2
|
|
169
|
+
fi
|
|
170
|
+
|
|
171
|
+
echo "$value"
|
|
172
|
+
return 0
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
first_profile_name() {
|
|
176
|
+
local first
|
|
177
|
+
first="$(find "$PROFILES_DIR" -mindepth 1 -maxdepth 1 -type d -exec basename {} \; | sort | head -n 1)"
|
|
178
|
+
if [[ -n "$first" ]]; then
|
|
179
|
+
echo "$first"
|
|
180
|
+
return 0
|
|
181
|
+
fi
|
|
182
|
+
return 1
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
resolve_app_bin() {
|
|
186
|
+
if [[ -n "${CODEX_SWITCHER_APP_BIN:-}" ]]; then
|
|
187
|
+
[[ -x "$CODEX_SWITCHER_APP_BIN" ]] || err "CODEX_SWITCHER_APP_BIN is not executable: $CODEX_SWITCHER_APP_BIN"
|
|
188
|
+
echo "$CODEX_SWITCHER_APP_BIN"
|
|
189
|
+
return
|
|
190
|
+
fi
|
|
191
|
+
|
|
192
|
+
if [[ -x "/Applications/Codex.app/Contents/MacOS/Codex" ]]; then
|
|
193
|
+
echo "/Applications/Codex.app/Contents/MacOS/Codex"
|
|
194
|
+
return
|
|
195
|
+
fi
|
|
196
|
+
if [[ -x "$HOME/Applications/Codex.app/Contents/MacOS/Codex" ]]; then
|
|
197
|
+
echo "$HOME/Applications/Codex.app/Contents/MacOS/Codex"
|
|
198
|
+
return
|
|
199
|
+
fi
|
|
200
|
+
err "Codex.app binary not found. set CODEX_SWITCHER_APP_BIN manually"
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
acquire_lock() {
|
|
204
|
+
ensure_dirs
|
|
205
|
+
local start now lock_pid
|
|
206
|
+
start="$(date +%s)"
|
|
207
|
+
|
|
208
|
+
while true; do
|
|
209
|
+
if mkdir "$LOCK_DIR" 2>/dev/null; then
|
|
210
|
+
printf '%s\n' "$$" > "$LOCK_DIR/pid"
|
|
211
|
+
chmod 700 "$LOCK_DIR" 2>/dev/null || true
|
|
212
|
+
trap 'release_lock' EXIT INT TERM
|
|
213
|
+
return 0
|
|
214
|
+
fi
|
|
215
|
+
|
|
216
|
+
lock_pid=""
|
|
217
|
+
if [[ -f "$LOCK_DIR/pid" ]]; then
|
|
218
|
+
lock_pid="$(cat "$LOCK_DIR/pid" 2>/dev/null || true)"
|
|
219
|
+
fi
|
|
220
|
+
|
|
221
|
+
if [[ -n "$lock_pid" ]] && ! kill -0 "$lock_pid" >/dev/null 2>&1; then
|
|
222
|
+
rm -rf "$LOCK_DIR" >/dev/null 2>&1 || true
|
|
223
|
+
continue
|
|
224
|
+
fi
|
|
225
|
+
|
|
226
|
+
now="$(date +%s)"
|
|
227
|
+
if (( now - start >= LOCK_WAIT_SECONDS )); then
|
|
228
|
+
err "failed to acquire lock within ${LOCK_WAIT_SECONDS}s"
|
|
229
|
+
fi
|
|
230
|
+
sleep 0.1
|
|
231
|
+
done
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
release_lock() {
|
|
235
|
+
if [[ -d "$LOCK_DIR" ]] && [[ -f "$LOCK_DIR/pid" ]]; then
|
|
236
|
+
local lock_pid
|
|
237
|
+
lock_pid="$(cat "$LOCK_DIR/pid" 2>/dev/null || true)"
|
|
238
|
+
if [[ "$lock_pid" == "$$" ]]; then
|
|
239
|
+
rm -rf "$LOCK_DIR" >/dev/null 2>&1 || true
|
|
240
|
+
fi
|
|
241
|
+
fi
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
with_lock() {
|
|
245
|
+
acquire_lock
|
|
246
|
+
"$@"
|
|
247
|
+
release_lock
|
|
248
|
+
trap - EXIT INT TERM
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
run_codex_for_profile() {
|
|
252
|
+
local profile="$1"
|
|
253
|
+
shift
|
|
254
|
+
require_profile_exists "$profile"
|
|
255
|
+
CODEX_HOME="$(profile_path "$profile")" command codex "$@"
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
login_state_for_profile() {
|
|
259
|
+
local profile="$1"
|
|
260
|
+
local p rc
|
|
261
|
+
p="$(profile_path "$profile")"
|
|
262
|
+
if [[ ! -d "$p" ]]; then
|
|
263
|
+
echo "missing-profile"
|
|
264
|
+
return
|
|
265
|
+
fi
|
|
266
|
+
|
|
267
|
+
rc=0
|
|
268
|
+
CODEX_HOME="$p" command codex login status >/dev/null 2>&1 || rc=$?
|
|
269
|
+
if [[ "$rc" -eq 0 ]]; then
|
|
270
|
+
echo "logged-in"
|
|
271
|
+
else
|
|
272
|
+
echo "not-logged-in"
|
|
273
|
+
fi
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
kill_tree() {
|
|
277
|
+
local pid="$1"
|
|
278
|
+
local child
|
|
279
|
+
if [[ -z "$pid" ]]; then
|
|
280
|
+
return 0
|
|
281
|
+
fi
|
|
282
|
+
for child in $(pgrep -P "$pid" 2>/dev/null || true); do
|
|
283
|
+
kill_tree "$child"
|
|
284
|
+
done
|
|
285
|
+
kill "$pid" >/dev/null 2>&1 || true
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
app_is_running() {
|
|
289
|
+
if [[ ! -f "$APP_PID_FILE" ]]; then
|
|
290
|
+
return 1
|
|
291
|
+
fi
|
|
292
|
+
local pid
|
|
293
|
+
pid="$(cat "$APP_PID_FILE" 2>/dev/null || true)"
|
|
294
|
+
[[ -n "$pid" ]] || return 1
|
|
295
|
+
kill -0 "$pid" >/dev/null 2>&1
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
app_stop_managed() {
|
|
299
|
+
if [[ ! -f "$APP_PID_FILE" ]]; then
|
|
300
|
+
echo "No managed app process to stop"
|
|
301
|
+
return 0
|
|
302
|
+
fi
|
|
303
|
+
|
|
304
|
+
local pid
|
|
305
|
+
pid="$(cat "$APP_PID_FILE" 2>/dev/null || true)"
|
|
306
|
+
if [[ -z "$pid" ]]; then
|
|
307
|
+
rm -f "$APP_PID_FILE"
|
|
308
|
+
echo "No managed app process to stop"
|
|
309
|
+
return 0
|
|
310
|
+
fi
|
|
311
|
+
|
|
312
|
+
if kill -0 "$pid" >/dev/null 2>&1; then
|
|
313
|
+
log_event INFO "app_stop pid=$pid"
|
|
314
|
+
kill_tree "$pid"
|
|
315
|
+
sleep 0.3
|
|
316
|
+
fi
|
|
317
|
+
|
|
318
|
+
rm -f "$APP_PID_FILE"
|
|
319
|
+
echo "Stopped managed app process"
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
prompt_confirm_remove() {
|
|
323
|
+
local profile="$1"
|
|
324
|
+
if [[ ! -t 0 ]]; then
|
|
325
|
+
err "non-interactive terminal; rerun with --force"
|
|
326
|
+
fi
|
|
327
|
+
local answer
|
|
328
|
+
printf "Remove profile '%s'? This deletes %s [y/N]: " "$profile" "$(profile_path "$profile")" >&2
|
|
329
|
+
read -r answer
|
|
330
|
+
case "$answer" in
|
|
331
|
+
y|Y|yes|YES)
|
|
332
|
+
return 0
|
|
333
|
+
;;
|
|
334
|
+
*)
|
|
335
|
+
echo "Cancelled"
|
|
336
|
+
return 1
|
|
337
|
+
;;
|
|
338
|
+
esac
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
cmd_add() {
|
|
342
|
+
local profile="$1"
|
|
343
|
+
validate_profile_name "$profile"
|
|
344
|
+
ensure_profile "$profile"
|
|
345
|
+
log_event INFO "profile_add profile=$profile"
|
|
346
|
+
echo "Added profile: $profile"
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
cmd_remove() {
|
|
350
|
+
local profile="$1"
|
|
351
|
+
local force="${2:-false}"
|
|
352
|
+
local cli_cur app_cur
|
|
353
|
+
|
|
354
|
+
validate_profile_name "$profile"
|
|
355
|
+
require_profile_exists "$profile"
|
|
356
|
+
|
|
357
|
+
cli_cur="$(read_current cli || true)"
|
|
358
|
+
app_cur="$(read_current app || true)"
|
|
359
|
+
|
|
360
|
+
if [[ "$profile" == "$cli_cur" && "$force" != "true" ]]; then
|
|
361
|
+
err "profile '$profile' is current CLI profile; use --force to remove"
|
|
362
|
+
fi
|
|
363
|
+
if [[ "$profile" == "$app_cur" && "$force" != "true" ]]; then
|
|
364
|
+
err "profile '$profile' is current App profile; use --force to remove"
|
|
365
|
+
fi
|
|
366
|
+
|
|
367
|
+
if [[ "$force" != "true" ]]; then
|
|
368
|
+
prompt_confirm_remove "$profile" || return 0
|
|
369
|
+
fi
|
|
370
|
+
|
|
371
|
+
if [[ "$profile" == "$app_cur" ]]; then
|
|
372
|
+
app_stop_managed >/dev/null
|
|
373
|
+
set_current app "$(default_profile_for app)"
|
|
374
|
+
fi
|
|
375
|
+
if [[ "$profile" == "$cli_cur" ]]; then
|
|
376
|
+
set_current cli "$(default_profile_for cli)"
|
|
377
|
+
fi
|
|
378
|
+
|
|
379
|
+
rm -rf -- "$(profile_path "$profile")"
|
|
380
|
+
log_event INFO "profile_remove profile=$profile force=$force"
|
|
381
|
+
echo "Removed profile: $profile"
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
cmd_list() {
|
|
385
|
+
local cli_cur app_cur
|
|
386
|
+
cli_cur="$(read_current cli || true)"
|
|
387
|
+
app_cur="$(read_current app || true)"
|
|
388
|
+
|
|
389
|
+
if ! find "$PROFILES_DIR" -mindepth 1 -maxdepth 1 -type d | grep -q .; then
|
|
390
|
+
echo "No profiles yet. Use: $SCRIPT_NAME add <profile>"
|
|
391
|
+
return 0
|
|
392
|
+
fi
|
|
393
|
+
|
|
394
|
+
find "$PROFILES_DIR" -mindepth 1 -maxdepth 1 -type d -exec basename {} \; | sort | while IFS= read -r name; do
|
|
395
|
+
local marks
|
|
396
|
+
marks=""
|
|
397
|
+
[[ "$name" == "$cli_cur" ]] && marks="${marks} [cli-current]"
|
|
398
|
+
[[ "$name" == "$app_cur" ]] && marks="${marks} [app-current]"
|
|
399
|
+
echo "- $name$marks"
|
|
400
|
+
done
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
cmd_use() {
|
|
404
|
+
local profile="$1"
|
|
405
|
+
validate_profile_name "$profile"
|
|
406
|
+
ensure_profile "$profile"
|
|
407
|
+
set_current cli "$profile"
|
|
408
|
+
log_event INFO "cli_use profile=$profile"
|
|
409
|
+
echo "Switched CLI profile to: $profile"
|
|
410
|
+
echo "Run in current shell if needed:"
|
|
411
|
+
echo " export CODEX_HOME='$(profile_path "$profile")'"
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
cmd_switch() {
|
|
415
|
+
cmd_use "$1"
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
cmd_current() {
|
|
419
|
+
local target="${1:-all}"
|
|
420
|
+
case "$target" in
|
|
421
|
+
cli)
|
|
422
|
+
read_current cli || true
|
|
423
|
+
;;
|
|
424
|
+
app)
|
|
425
|
+
read_current app || true
|
|
426
|
+
;;
|
|
427
|
+
all)
|
|
428
|
+
echo "cli: $(read_current cli || true)"
|
|
429
|
+
echo "app: $(read_current app || true)"
|
|
430
|
+
;;
|
|
431
|
+
*)
|
|
432
|
+
err "invalid target '$target' (use cli|app)"
|
|
433
|
+
;;
|
|
434
|
+
esac
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
cmd_status() {
|
|
438
|
+
local cli_profile app_profile
|
|
439
|
+
local cli_ptr_state app_ptr_state
|
|
440
|
+
local cli_state app_state
|
|
441
|
+
local exit_code
|
|
442
|
+
|
|
443
|
+
cli_profile="$(read_current cli)" || cli_ptr_state=$?
|
|
444
|
+
app_profile="$(read_current app)" || app_ptr_state=$?
|
|
445
|
+
cli_ptr_state="${cli_ptr_state:-0}"
|
|
446
|
+
app_ptr_state="${app_ptr_state:-0}"
|
|
447
|
+
|
|
448
|
+
cli_state="$(login_state_for_profile "$cli_profile")"
|
|
449
|
+
app_state="$(login_state_for_profile "$app_profile")"
|
|
450
|
+
|
|
451
|
+
echo "cli_current: $cli_profile"
|
|
452
|
+
echo "app_current: $app_profile"
|
|
453
|
+
echo "cli($cli_profile): $cli_state"
|
|
454
|
+
echo "app($app_profile): $app_state"
|
|
455
|
+
|
|
456
|
+
exit_code=0
|
|
457
|
+
if [[ "$cli_ptr_state" -eq 2 || "$app_ptr_state" -eq 2 ]]; then
|
|
458
|
+
echo "hint: pointer file corrupted, run: $SCRIPT_NAME recover" >&2
|
|
459
|
+
exit_code=2
|
|
460
|
+
elif [[ "$cli_state" == "missing-profile" || "$app_state" == "missing-profile" ]]; then
|
|
461
|
+
echo "hint: missing profile directory, run: $SCRIPT_NAME recover" >&2
|
|
462
|
+
exit_code=2
|
|
463
|
+
elif [[ "$cli_state" != "logged-in" || "$app_state" != "logged-in" ]]; then
|
|
464
|
+
exit_code=1
|
|
465
|
+
fi
|
|
466
|
+
|
|
467
|
+
return "$exit_code"
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
cmd_exec() {
|
|
471
|
+
[[ "$#" -gt 0 ]] || err "missing codex args. example: $SCRIPT_NAME exec -- login status"
|
|
472
|
+
if [[ "$1" == "--" ]]; then
|
|
473
|
+
shift
|
|
474
|
+
fi
|
|
475
|
+
[[ "$#" -gt 0 ]] || err "missing codex args after --"
|
|
476
|
+
|
|
477
|
+
local profile
|
|
478
|
+
profile="$(read_current cli || true)"
|
|
479
|
+
ensure_profile "$profile"
|
|
480
|
+
CODEX_HOME="$(profile_path "$profile")" command codex "$@"
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
cmd_login() {
|
|
484
|
+
local profile="${1:-$(read_current cli || true)}"
|
|
485
|
+
validate_profile_name "$profile"
|
|
486
|
+
ensure_profile "$profile"
|
|
487
|
+
log_event INFO "cli_login profile=$profile"
|
|
488
|
+
run_codex_for_profile "$profile" login
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
cmd_logout() {
|
|
492
|
+
local profile="${1:-$(read_current cli || true)}"
|
|
493
|
+
validate_profile_name "$profile"
|
|
494
|
+
ensure_profile "$profile"
|
|
495
|
+
log_event INFO "cli_logout profile=$profile"
|
|
496
|
+
run_codex_for_profile "$profile" logout
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
cmd_env() {
|
|
500
|
+
local profile="${1:-$(read_current cli || true)}"
|
|
501
|
+
validate_profile_name "$profile"
|
|
502
|
+
ensure_profile "$profile"
|
|
503
|
+
echo "export CODEX_HOME='$(profile_path "$profile")'"
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
cmd_app_open() {
|
|
507
|
+
local profile="$1"
|
|
508
|
+
shift
|
|
509
|
+
local app_bin pid
|
|
510
|
+
|
|
511
|
+
validate_profile_name "$profile"
|
|
512
|
+
ensure_profile "$profile"
|
|
513
|
+
app_bin="$(resolve_app_bin)"
|
|
514
|
+
|
|
515
|
+
app_stop_managed >/dev/null
|
|
516
|
+
|
|
517
|
+
mkdir -p "$(dirname "$APP_LOG")"
|
|
518
|
+
nohup env CODEX_HOME="$(profile_path "$profile")" CODEX_SWITCHER_MANAGED=1 CODEX_SWITCHER_PROFILE="$profile" "$app_bin" "$@" >>"$APP_LOG" 2>&1 &
|
|
519
|
+
pid="$!"
|
|
520
|
+
printf '%s\n' "$pid" > "$APP_PID_FILE"
|
|
521
|
+
chmod 600 "$APP_PID_FILE" 2>/dev/null || true
|
|
522
|
+
|
|
523
|
+
set_current app "$profile"
|
|
524
|
+
log_event INFO "app_open profile=$profile pid=$pid args=$*"
|
|
525
|
+
echo "Opened Codex App with profile: $profile"
|
|
526
|
+
echo "App log: $APP_LOG"
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
cmd_app_use() {
|
|
530
|
+
local profile="$1"
|
|
531
|
+
shift
|
|
532
|
+
cmd_app_open "$profile" "$@"
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
cmd_app_logout() {
|
|
536
|
+
local profile="${1:-$(read_current app || true)}"
|
|
537
|
+
validate_profile_name "$profile"
|
|
538
|
+
ensure_profile "$profile"
|
|
539
|
+
log_event INFO "app_logout profile=$profile"
|
|
540
|
+
run_codex_for_profile "$profile" logout
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
cmd_app_status() {
|
|
544
|
+
local profile
|
|
545
|
+
profile="$(read_current app || true)"
|
|
546
|
+
echo "app_current: $profile"
|
|
547
|
+
if app_is_running; then
|
|
548
|
+
echo "app_process: running(pid=$(cat "$APP_PID_FILE"))"
|
|
549
|
+
return 0
|
|
550
|
+
fi
|
|
551
|
+
echo "app_process: not-running"
|
|
552
|
+
return 1
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
cmd_app_stop() {
|
|
556
|
+
app_stop_managed
|
|
557
|
+
log_event INFO "app_stop"
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
cmd_init() {
|
|
561
|
+
local shell_name=""
|
|
562
|
+
local dry_run="false"
|
|
563
|
+
local script_abs target_link rc_file block_start block_end
|
|
564
|
+
|
|
565
|
+
while [[ "$#" -gt 0 ]]; do
|
|
566
|
+
case "$1" in
|
|
567
|
+
--shell)
|
|
568
|
+
shift
|
|
569
|
+
[[ "$#" -gt 0 ]] || err "missing shell after --shell"
|
|
570
|
+
shell_name="$1"
|
|
571
|
+
;;
|
|
572
|
+
--dry-run)
|
|
573
|
+
dry_run="true"
|
|
574
|
+
;;
|
|
575
|
+
*)
|
|
576
|
+
err "unknown init option: $1"
|
|
577
|
+
;;
|
|
578
|
+
esac
|
|
579
|
+
shift
|
|
580
|
+
done
|
|
581
|
+
|
|
582
|
+
if [[ -z "$shell_name" ]]; then
|
|
583
|
+
shell_name="$(basename "${SHELL:-zsh}")"
|
|
584
|
+
fi
|
|
585
|
+
case "$shell_name" in
|
|
586
|
+
zsh)
|
|
587
|
+
rc_file="$HOME/.zshrc"
|
|
588
|
+
;;
|
|
589
|
+
bash)
|
|
590
|
+
rc_file="$HOME/.bashrc"
|
|
591
|
+
;;
|
|
592
|
+
*)
|
|
593
|
+
err "unsupported shell '$shell_name' (use zsh or bash)"
|
|
594
|
+
;;
|
|
595
|
+
esac
|
|
596
|
+
|
|
597
|
+
script_abs="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/$(basename "${BASH_SOURCE[0]}")"
|
|
598
|
+
target_link="$HOME/.local/bin/codex-sw"
|
|
599
|
+
block_start="# >>> codex-sw init >>>"
|
|
600
|
+
block_end="# <<< codex-sw init <<<"
|
|
601
|
+
|
|
602
|
+
if [[ "$dry_run" == "true" ]]; then
|
|
603
|
+
echo "[dry-run] mkdir -p $HOME/.local/bin"
|
|
604
|
+
echo "[dry-run] ln -sf $script_abs $target_link"
|
|
605
|
+
echo "[dry-run] ensure PATH block in $rc_file"
|
|
606
|
+
return 0
|
|
607
|
+
fi
|
|
608
|
+
|
|
609
|
+
mkdir -p "$HOME/.local/bin"
|
|
610
|
+
ln -sf "$script_abs" "$target_link"
|
|
611
|
+
|
|
612
|
+
if [[ ! -f "$rc_file" ]]; then
|
|
613
|
+
touch "$rc_file"
|
|
614
|
+
fi
|
|
615
|
+
|
|
616
|
+
if ! grep -qF "$block_start" "$rc_file"; then
|
|
617
|
+
cat >> "$rc_file" <<RC
|
|
618
|
+
$block_start
|
|
619
|
+
export PATH="\$HOME/.local/bin:\$PATH"
|
|
620
|
+
$block_end
|
|
621
|
+
RC
|
|
622
|
+
fi
|
|
623
|
+
|
|
624
|
+
log_event INFO "init shell=$shell_name rc_file=$rc_file"
|
|
625
|
+
echo "Initialized codex-sw for $shell_name"
|
|
626
|
+
echo "Run: source $rc_file"
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
cmd_recover() {
|
|
630
|
+
local dry_run="${1:-false}"
|
|
631
|
+
local cli_profile app_profile cli_ptr_state app_ptr_state first
|
|
632
|
+
|
|
633
|
+
cli_profile="$(read_current cli)" || cli_ptr_state=$?
|
|
634
|
+
app_profile="$(read_current app)" || app_ptr_state=$?
|
|
635
|
+
cli_ptr_state="${cli_ptr_state:-0}"
|
|
636
|
+
app_ptr_state="${app_ptr_state:-0}"
|
|
637
|
+
|
|
638
|
+
if [[ ! -d "$(profile_path "$cli_profile")" ]]; then
|
|
639
|
+
if first="$(first_profile_name)"; then
|
|
640
|
+
cli_profile="$first"
|
|
641
|
+
else
|
|
642
|
+
cli_profile="$(default_profile_for cli)"
|
|
643
|
+
ensure_profile "$cli_profile"
|
|
644
|
+
fi
|
|
645
|
+
cli_ptr_state=3
|
|
646
|
+
fi
|
|
647
|
+
|
|
648
|
+
if [[ ! -d "$(profile_path "$app_profile")" ]]; then
|
|
649
|
+
if [[ -d "$(profile_path "$cli_profile")" ]]; then
|
|
650
|
+
app_profile="$cli_profile"
|
|
651
|
+
elif first="$(first_profile_name)"; then
|
|
652
|
+
app_profile="$first"
|
|
653
|
+
else
|
|
654
|
+
app_profile="$(default_profile_for app)"
|
|
655
|
+
ensure_profile "$app_profile"
|
|
656
|
+
fi
|
|
657
|
+
app_ptr_state=3
|
|
658
|
+
fi
|
|
659
|
+
|
|
660
|
+
if [[ "$dry_run" == "true" ]]; then
|
|
661
|
+
echo "recover(dry-run): cli=$cli_profile app=$app_profile"
|
|
662
|
+
return 0
|
|
663
|
+
fi
|
|
664
|
+
|
|
665
|
+
set_current cli "$cli_profile"
|
|
666
|
+
set_current app "$app_profile"
|
|
667
|
+
log_event INFO "recover cli=$cli_profile app=$app_profile"
|
|
668
|
+
echo "Recovered pointers: cli=$cli_profile app=$app_profile"
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
scan_logs_for_secrets() {
|
|
672
|
+
local found=0
|
|
673
|
+
if [[ -f "$SWITCH_LOG" ]] && grep -E 'sk-[A-Za-z0-9_-]{8,}|(access_token|refresh_token|id_token)=' "$SWITCH_LOG" >/dev/null 2>&1; then
|
|
674
|
+
echo "redaction_check: failed ($SWITCH_LOG contains potential secrets)" >&2
|
|
675
|
+
found=1
|
|
676
|
+
fi
|
|
677
|
+
if [[ -f "$APP_LOG" ]] && grep -E 'sk-[A-Za-z0-9_-]{8,}|(access_token|refresh_token|id_token)=' "$APP_LOG" >/dev/null 2>&1; then
|
|
678
|
+
echo "redaction_check: failed ($APP_LOG contains potential secrets)" >&2
|
|
679
|
+
found=1
|
|
680
|
+
fi
|
|
681
|
+
return "$found"
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
cmd_check() {
|
|
685
|
+
command -v codex >/dev/null 2>&1 || err "codex command not found in PATH"
|
|
686
|
+
resolve_app_bin >/dev/null
|
|
687
|
+
|
|
688
|
+
ensure_dirs
|
|
689
|
+
local pstate
|
|
690
|
+
read_current cli >/dev/null || pstate=$?
|
|
691
|
+
if [[ "${pstate:-0}" -eq 2 ]]; then
|
|
692
|
+
err "current_cli pointer corrupted; run: $SCRIPT_NAME recover"
|
|
693
|
+
fi
|
|
694
|
+
pstate=0
|
|
695
|
+
read_current app >/dev/null || pstate=$?
|
|
696
|
+
if [[ "$pstate" -eq 2 ]]; then
|
|
697
|
+
err "current_app pointer corrupted; run: $SCRIPT_NAME recover"
|
|
698
|
+
fi
|
|
699
|
+
|
|
700
|
+
if ! scan_logs_for_secrets; then
|
|
701
|
+
err "log redaction check failed"
|
|
702
|
+
fi
|
|
703
|
+
|
|
704
|
+
echo "check: ok"
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
cmd_doctor() {
|
|
708
|
+
local fix="${1:-false}"
|
|
709
|
+
local issues=0
|
|
710
|
+
local state_perm profiles_perm
|
|
711
|
+
|
|
712
|
+
ensure_dirs
|
|
713
|
+
state_perm="$(perm_of "$STATE_DIR")"
|
|
714
|
+
profiles_perm="$(perm_of "$PROFILES_DIR")"
|
|
715
|
+
|
|
716
|
+
echo "state_dir: $STATE_DIR (perm=$state_perm)"
|
|
717
|
+
echo "profiles_dir: $PROFILES_DIR (perm=$profiles_perm)"
|
|
718
|
+
echo "cli_current: $(read_current cli || true)"
|
|
719
|
+
echo "app_current: $(read_current app || true)"
|
|
720
|
+
|
|
721
|
+
if ! command -v codex >/dev/null 2>&1; then
|
|
722
|
+
echo "- codex in PATH: missing"
|
|
723
|
+
issues=1
|
|
724
|
+
else
|
|
725
|
+
echo "- codex in PATH: ok"
|
|
726
|
+
fi
|
|
727
|
+
|
|
728
|
+
if resolve_app_bin >/dev/null 2>&1; then
|
|
729
|
+
echo "- codex app binary: ok ($(resolve_app_bin))"
|
|
730
|
+
else
|
|
731
|
+
echo "- codex app binary: missing"
|
|
732
|
+
issues=1
|
|
733
|
+
fi
|
|
734
|
+
|
|
735
|
+
if [[ "$state_perm" != "700" ]]; then
|
|
736
|
+
echo "- state dir permission not 700"
|
|
737
|
+
issues=1
|
|
738
|
+
if [[ "$fix" == "true" ]]; then
|
|
739
|
+
chmod 700 "$STATE_DIR" || true
|
|
740
|
+
fi
|
|
741
|
+
fi
|
|
742
|
+
|
|
743
|
+
if [[ "$profiles_perm" != "700" ]]; then
|
|
744
|
+
echo "- profiles dir permission not 700"
|
|
745
|
+
issues=1
|
|
746
|
+
if [[ "$fix" == "true" ]]; then
|
|
747
|
+
chmod 700 "$PROFILES_DIR" || true
|
|
748
|
+
fi
|
|
749
|
+
fi
|
|
750
|
+
|
|
751
|
+
if ! scan_logs_for_secrets; then
|
|
752
|
+
issues=1
|
|
753
|
+
else
|
|
754
|
+
echo "- redaction check: ok"
|
|
755
|
+
fi
|
|
756
|
+
|
|
757
|
+
local pstate=0
|
|
758
|
+
read_current cli >/dev/null || pstate=$?
|
|
759
|
+
if [[ "$pstate" -eq 2 ]]; then
|
|
760
|
+
echo "- current_cli pointer corrupted"
|
|
761
|
+
issues=1
|
|
762
|
+
fi
|
|
763
|
+
pstate=0
|
|
764
|
+
read_current app >/dev/null || pstate=$?
|
|
765
|
+
if [[ "$pstate" -eq 2 ]]; then
|
|
766
|
+
echo "- current_app pointer corrupted"
|
|
767
|
+
issues=1
|
|
768
|
+
fi
|
|
769
|
+
|
|
770
|
+
if [[ "$fix" == "true" ]]; then
|
|
771
|
+
with_lock cmd_recover false
|
|
772
|
+
chmod 700 "$STATE_DIR" "$PROFILES_DIR" 2>/dev/null || true
|
|
773
|
+
find "$PROFILES_DIR" -mindepth 1 -maxdepth 1 -type d -exec chmod 700 {} \; 2>/dev/null || true
|
|
774
|
+
issues=0
|
|
775
|
+
if ! scan_logs_for_secrets; then
|
|
776
|
+
issues=1
|
|
777
|
+
fi
|
|
778
|
+
echo "doctor --fix: completed"
|
|
779
|
+
fi
|
|
780
|
+
|
|
781
|
+
if [[ "$issues" -eq 0 ]]; then
|
|
782
|
+
echo "doctor: ok"
|
|
783
|
+
return 0
|
|
784
|
+
fi
|
|
785
|
+
|
|
786
|
+
echo "doctor: issues found"
|
|
787
|
+
if [[ "$fix" != "true" ]]; then
|
|
788
|
+
echo "Run: $SCRIPT_NAME doctor --fix"
|
|
789
|
+
fi
|
|
790
|
+
return 1
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
main() {
|
|
794
|
+
ensure_dirs
|
|
795
|
+
|
|
796
|
+
local cmd="${1:-}"
|
|
797
|
+
[[ -n "$cmd" ]] || {
|
|
798
|
+
usage
|
|
799
|
+
exit 0
|
|
800
|
+
}
|
|
801
|
+
shift || true
|
|
802
|
+
|
|
803
|
+
case "$cmd" in
|
|
804
|
+
add)
|
|
805
|
+
[[ "$#" -eq 1 ]] || err "usage: $SCRIPT_NAME add <profile>"
|
|
806
|
+
with_lock cmd_add "$1"
|
|
807
|
+
;;
|
|
808
|
+
remove)
|
|
809
|
+
[[ "$#" -ge 1 && "$#" -le 2 ]] || err "usage: $SCRIPT_NAME remove <profile> [--force]"
|
|
810
|
+
local force="false"
|
|
811
|
+
if [[ "${2:-}" == "--force" ]]; then
|
|
812
|
+
force="true"
|
|
813
|
+
elif [[ -n "${2:-}" ]]; then
|
|
814
|
+
err "unknown option: ${2:-}"
|
|
815
|
+
fi
|
|
816
|
+
with_lock cmd_remove "$1" "$force"
|
|
817
|
+
;;
|
|
818
|
+
list)
|
|
819
|
+
[[ "$#" -eq 0 ]] || err "usage: $SCRIPT_NAME list"
|
|
820
|
+
cmd_list
|
|
821
|
+
;;
|
|
822
|
+
use)
|
|
823
|
+
[[ "$#" -eq 1 ]] || err "usage: $SCRIPT_NAME use <profile>"
|
|
824
|
+
with_lock cmd_use "$1"
|
|
825
|
+
;;
|
|
826
|
+
switch)
|
|
827
|
+
[[ "$#" -eq 1 ]] || err "usage: $SCRIPT_NAME switch <profile>"
|
|
828
|
+
with_lock cmd_switch "$1"
|
|
829
|
+
;;
|
|
830
|
+
current)
|
|
831
|
+
[[ "$#" -le 1 ]] || err "usage: $SCRIPT_NAME current [cli|app]"
|
|
832
|
+
cmd_current "${1:-all}"
|
|
833
|
+
;;
|
|
834
|
+
status)
|
|
835
|
+
[[ "$#" -eq 0 ]] || err "usage: $SCRIPT_NAME status"
|
|
836
|
+
cmd_status
|
|
837
|
+
;;
|
|
838
|
+
exec)
|
|
839
|
+
cmd_exec "$@"
|
|
840
|
+
;;
|
|
841
|
+
login)
|
|
842
|
+
[[ "$#" -le 1 ]] || err "usage: $SCRIPT_NAME login [profile]"
|
|
843
|
+
with_lock cmd_login "${1:-}"
|
|
844
|
+
;;
|
|
845
|
+
logout)
|
|
846
|
+
[[ "$#" -le 1 ]] || err "usage: $SCRIPT_NAME logout [profile]"
|
|
847
|
+
with_lock cmd_logout "${1:-}"
|
|
848
|
+
;;
|
|
849
|
+
env)
|
|
850
|
+
[[ "$#" -le 1 ]] || err "usage: $SCRIPT_NAME env [profile]"
|
|
851
|
+
cmd_env "${1:-}"
|
|
852
|
+
;;
|
|
853
|
+
app)
|
|
854
|
+
local sub="${1:-}"
|
|
855
|
+
[[ -n "$sub" ]] || err "usage: $SCRIPT_NAME app <open|use|logout|status|stop|current> ..."
|
|
856
|
+
shift || true
|
|
857
|
+
case "$sub" in
|
|
858
|
+
open)
|
|
859
|
+
local profile="${1:-$(read_current app || true)}"
|
|
860
|
+
if [[ "$#" -gt 0 ]]; then shift; fi
|
|
861
|
+
if [[ "${1:-}" == "--" ]]; then shift; fi
|
|
862
|
+
with_lock cmd_app_open "$profile" "$@"
|
|
863
|
+
;;
|
|
864
|
+
use)
|
|
865
|
+
[[ "$#" -ge 1 ]] || err "usage: $SCRIPT_NAME app use <profile> [-- <app args...>]"
|
|
866
|
+
local profile="$1"
|
|
867
|
+
shift
|
|
868
|
+
if [[ "${1:-}" == "--" ]]; then shift; fi
|
|
869
|
+
with_lock cmd_app_use "$profile" "$@"
|
|
870
|
+
;;
|
|
871
|
+
logout)
|
|
872
|
+
[[ "$#" -le 1 ]] || err "usage: $SCRIPT_NAME app logout [profile]"
|
|
873
|
+
with_lock cmd_app_logout "${1:-}"
|
|
874
|
+
;;
|
|
875
|
+
status)
|
|
876
|
+
[[ "$#" -eq 0 ]] || err "usage: $SCRIPT_NAME app status"
|
|
877
|
+
cmd_app_status
|
|
878
|
+
;;
|
|
879
|
+
stop)
|
|
880
|
+
[[ "$#" -eq 0 ]] || err "usage: $SCRIPT_NAME app stop"
|
|
881
|
+
with_lock cmd_app_stop
|
|
882
|
+
;;
|
|
883
|
+
current)
|
|
884
|
+
[[ "$#" -eq 0 ]] || err "usage: $SCRIPT_NAME app current"
|
|
885
|
+
cmd_current app
|
|
886
|
+
;;
|
|
887
|
+
*)
|
|
888
|
+
err "unknown app subcommand: $sub"
|
|
889
|
+
;;
|
|
890
|
+
esac
|
|
891
|
+
;;
|
|
892
|
+
init)
|
|
893
|
+
cmd_init "$@"
|
|
894
|
+
;;
|
|
895
|
+
recover)
|
|
896
|
+
[[ "$#" -le 1 ]] || err "usage: $SCRIPT_NAME recover [--dry-run]"
|
|
897
|
+
local dry="false"
|
|
898
|
+
if [[ "${1:-}" == "--dry-run" ]]; then
|
|
899
|
+
dry="true"
|
|
900
|
+
elif [[ -n "${1:-}" ]]; then
|
|
901
|
+
err "unknown option: ${1:-}"
|
|
902
|
+
fi
|
|
903
|
+
with_lock cmd_recover "$dry"
|
|
904
|
+
;;
|
|
905
|
+
check)
|
|
906
|
+
[[ "$#" -eq 0 ]] || err "usage: $SCRIPT_NAME check"
|
|
907
|
+
cmd_check
|
|
908
|
+
;;
|
|
909
|
+
doctor)
|
|
910
|
+
[[ "$#" -le 1 ]] || err "usage: $SCRIPT_NAME doctor [--fix]"
|
|
911
|
+
local fix="false"
|
|
912
|
+
if [[ "${1:-}" == "--fix" ]]; then
|
|
913
|
+
fix="true"
|
|
914
|
+
elif [[ -n "${1:-}" ]]; then
|
|
915
|
+
err "unknown option: ${1:-}"
|
|
916
|
+
fi
|
|
917
|
+
cmd_doctor "$fix"
|
|
918
|
+
;;
|
|
919
|
+
help|-h|--help)
|
|
920
|
+
usage
|
|
921
|
+
;;
|
|
922
|
+
*)
|
|
923
|
+
err "unknown command: $cmd"
|
|
924
|
+
;;
|
|
925
|
+
esac
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
main "$@"
|