Skip to content

Commit be05fd5

Browse files
committed
Refactor macOS integration and testing for improved Finder app handling
- Updated documentation for macOS Open With functionality to clarify usage with file managers and Finder. - Enhanced Finder integration by creating identity-specific app handlers for better project routing. - Improved error handling in macOS Open With tests to cover various failure scenarios during app installation and restoration. - Introduced common test utilities to streamline tool resolution across multiple test scripts. - Added identity configuration tests to ensure proper handling of invalid defaults and model requests. - Refactored existing tests to utilize the new common utilities for consistency and maintainability.
1 parent 2b6f6dd commit be05fd5

14 files changed

Lines changed: 339 additions & 99 deletions

‎CONTRIBUTING.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@ Follow [INSTALLATION.md](INSTALLATION.md), then run:
2323
Keep the helper compatible with Bash 3.2, including the system Bash on macOS.
2424
The default tests must run without credentials or a live provider.
2525

26+
For focused checks, run `bash tests/identity-config.sh` for malformed defaults,
27+
large configurations, and model-request validation. On macOS,
28+
`bash tests/macos-open-with.sh` checks Finder app installation failures and
29+
backup recovery using temporary apps and intercepted registration calls. Both
30+
are included in `./tests/test.sh`.
31+
2632
The [CI workflow](.github/workflows/ci.yml) runs offline tests with command
2733
mocks on Linux and macOS. It does not install Codex or validate configuration
2834
against a real Codex release. With Codex installed, you can opt into doctor

‎INSTALLATION.md‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -182,8 +182,7 @@ On macOS, helper-launched VS Code instances have Dock hover names such as
182182
See [macOS Dock names](docs/projects-vscode.md#macos-dock-names) for reopening,
183183
the restriction on directly launching or pinning editor copies, and the
184184
original-editor opt-out. After quitting, reopen a configured project through
185-
its matching **Open With > VS Code - NAME** entry, the Finder Quick Action, or
186-
`codex-home`.
185+
the Finder **Quick Actions > Open in Codex Project** action or `codex-home`.
187186

188187
After project setup, enter the folder and run `codex-home run`. It uses the
189188
context selected by `direnv`; you do not need to type the identity name each

‎README.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,9 @@ terminal, start the selected context with `codex-home run`.
7070

7171
On macOS, the helper opens a named editor copy: hover over its running Dock
7272
icon to read `VS Code - NAME`. Projects using the same identity reuse its
73-
instance. For configured folders, Finder's **Open With > VS Code - NAME**
74-
entry routes through that identity. You can also use `codex-home` or Finder's
75-
**Quick Actions > Open in Codex Project**; do not pin or directly open the
73+
instance. In Finder, use **Quick Actions > Open in Codex Project** for a
74+
configured folder. File managers with folder **Open With** menus can use the
75+
matching **VS Code - NAME** handler. You can also use `codex-home`; do not pin or directly open the
7676
generated editor copy, which needs the helper's launch environment. See
7777
[macOS Dock names](docs/projects-vscode.md#macos-dock-names).
7878

‎bin/codex-home‎

Lines changed: 57 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -319,23 +319,22 @@ umgpt_models_file_value() {
319319

320320
umgpt_repository_default_model() {
321321
local scope="$1"
322-
local key model
322+
local key
323323
[[ -f "$UMGPT_REPOSITORY_MODELS_FILE" ]] ||
324324
die "repository U-M GPT defaults not found: $UMGPT_REPOSITORY_MODELS_FILE"
325325
case "$scope" in
326326
openai-only) key="umgpt" ;;
327327
low-sensitivity) key="umgpt_low" ;;
328328
*) die "unknown U-M GPT model scope: $scope" ;;
329329
esac
330-
model="$(umgpt_models_file_value "$UMGPT_REPOSITORY_MODELS_FILE" "$key")"
331-
printf '%s\n' "$model"
330+
umgpt_models_file_value "$UMGPT_REPOSITORY_MODELS_FILE" "$key"
332331
}
333332

334333
ensure_umgpt_models_file() {
335334
local openai_model low_model home candidate base_url
336335
if [[ ! -e "$UMGPT_MODELS_FILE" ]]; then
337-
openai_model="$(umgpt_repository_default_model "openai-only")"
338-
low_model="$(umgpt_repository_default_model "low-sensitivity")"
336+
openai_model="$(umgpt_repository_default_model "openai-only")" || return
337+
low_model="$(umgpt_repository_default_model "low-sensitivity")" || return
339338
home="$(identity_home "umgpt")"
340339
if [[ -f "$home/config.toml" ]]; then
341340
candidate="$(config_value "$home/config.toml" "model")"
@@ -362,15 +361,14 @@ ensure_umgpt_models_file() {
362361

363362
umgpt_default_model() {
364363
local scope="$1"
365-
local key model
366-
ensure_umgpt_models_file
364+
local key
365+
ensure_umgpt_models_file || return
367366
case "$scope" in
368367
openai-only) key="umgpt" ;;
369368
low-sensitivity) key="umgpt_low" ;;
370369
*) die "unknown U-M GPT model scope: $scope" ;;
371370
esac
372-
model="$(umgpt_models_file_value "$UMGPT_MODELS_FILE" "$key")"
373-
printf '%s\n' "$model"
371+
umgpt_models_file_value "$UMGPT_MODELS_FILE" "$key"
374372
}
375373

376374
replace_identity_model() {
@@ -624,6 +622,9 @@ create_umgpt() {
624622
fi
625623
local name="${2:-umgpt}"
626624
validate_umgpt_model "openai-only" "$model"
625+
if [[ "$name" == "umgpt" && "$explicit" -eq 1 && -e "$UMGPT_MODELS_FILE" ]]; then
626+
umgpt_models_file_value "$UMGPT_MODELS_FILE" umgpt >/dev/null || return
627+
fi
627628
create_api_identity \
628629
"$name" \
629630
"$UMGPT_BASE_URL" \
@@ -649,6 +650,9 @@ create_umgpt_low() {
649650
fi
650651
local name="${2:-umgpt-low}"
651652
validate_umgpt_model "low-sensitivity" "$model"
653+
if [[ "$name" == "umgpt-low" && "$explicit" -eq 1 && -e "$UMGPT_MODELS_FILE" ]]; then
654+
umgpt_models_file_value "$UMGPT_MODELS_FILE" umgpt_low >/dev/null || return
655+
fi
652656
create_api_identity \
653657
"$name" \
654658
"$UMGPT_BASE_URL" \
@@ -673,7 +677,8 @@ config_values() {
673677
}
674678

675679
config_value() {
676-
config_values "$1" "$2" | head -n 1
680+
# Drain the producer so large configs cannot fail with SIGPIPE under pipefail.
681+
config_values "$1" "$2" | sed -n '1p'
677682
}
678683

679684
set_umgpt_scope() {
@@ -744,16 +749,18 @@ load_secret_env() {
744749
fetch_model_ids() {
745750
local base_url="$1"
746751
local api_key="$2"
747-
local url response ids status
752+
local url quoted_url quoted_header response ids status
748753

749754
command -v curl >/dev/null 2>&1 || die "curl is required for model discovery"
750755
command -v jq >/dev/null 2>&1 || die "jq is required for model discovery"
751756
url="${base_url%/}/models"
757+
quoted_url="$(curl_config_quote "$url")" || return
758+
quoted_header="$(curl_config_quote "Authorization: Bearer $api_key")" || return
752759

753760
response="$(
754761
{
755-
printf 'url = %s\n' "$(curl_config_quote "$url")"
756-
printf 'header = %s\n' "$(curl_config_quote "Authorization: Bearer $api_key")"
762+
printf 'url = %s\n' "$quoted_url"
763+
printf 'header = %s\n' "$quoted_header"
757764
printf '%s\n' 'silent' 'show-error' 'fail-with-body'
758765
} | curl --config -
759766
)" || {
@@ -1822,8 +1829,8 @@ ensure_macos_vscode_open_with() (
18221829
local plist_buddy="/usr/libexec/PlistBuddy"
18231830
local runtime_app="$home/vscode-editor/VS Code - $name.app"
18241831
[[ "${CODEX_MACOS_SKIP_REGISTER:-0}" != "1" ]] || return 0
1825-
app_dir="$HOME/Applications"
1826-
app="$(macos_vscode_open_with_path "$name")"
1832+
app="$(macos_vscode_open_with_path "$name")" || die "could not determine the VS Code Finder app path"
1833+
app_dir="$(dirname "$app")"
18271834
source="$SCRIPT_DIR/VSCodeIdentity.applescript"
18281835
[[ -f "$source" ]] || source="$REPOSITORY_ROOT/macos/VSCodeIdentity.applescript"
18291836
[[ -f "$source" ]] || die "missing the VS Code Finder handler source"
@@ -1832,7 +1839,7 @@ ensure_macos_vscode_open_with() (
18321839
[[ -x /usr/bin/codesign && -x "$plist_buddy" ]] || die "required macOS application tools were not found"
18331840
[[ ! -L "$app_dir" ]] || die "refusing a symbolic link at $app_dir"
18341841
mkdir -p "$app_dir" || die "could not create $app_dir"
1835-
app_dir="$(cd "$app_dir" && pwd -P)"
1842+
app_dir="$(cd "$app_dir" && pwd -P)" || die "could not open $app_dir"
18361843
app="$app_dir/VS Code - $name.app"
18371844
[[ ! -L "$app" ]] || die "refusing a symbolic link at $app"
18381845
metadata="$(shasum -a 256 "$source" "$SCRIPT_DIR/$(basename "${BASH_SOURCE[0]}")" "$SCRIPT_DIR/../config/umgpt-models.toml")" || return 1
@@ -1850,40 +1857,52 @@ ensure_macos_vscode_open_with() (
18501857
build="$(mktemp -d "$app_dir/.vscode-open-with.XXXXXX")" || die "could not stage the VS Code Finder app"
18511858
cleanup_vscode_open_with() {
18521859
local status=$?
1853-
if [[ "$status" -ne 0 && -d "$build/previous.app" && ! -e "$app" ]]; then
1860+
if [[ "$status" -ne 0 && -d "$build/previous.app" && ! -e "$app" && ! -L "$app" ]]; then
18541861
mv "$build/previous.app" "$app" || true
18551862
fi
1856-
rm -rf -- "$build"
1863+
if [[ "$status" -ne 0 && -e "$build/previous.app" ]]; then
1864+
printf 'Previous Finder app preserved at %s; restore it to %s after resolving the installation failure.\n' \
1865+
"$build/previous.app" "$app" >&2
1866+
else
1867+
rm -rf -- "$build"
1868+
fi
18571869
return "$status"
18581870
}
1859-
trap cleanup_vscode_open_with EXIT HUP INT TERM
1871+
trap cleanup_vscode_open_with EXIT
1872+
trap 'exit 129' HUP
1873+
trap 'exit 130' INT
1874+
trap 'exit 143' TERM
18601875
candidate="$build/VS Code - $name.app"
18611876
osacompile -o "$candidate" "$source" || die "could not compile the VS Code Finder app"
1862-
mkdir -p "$candidate/Contents/config"
1863-
cp "$SCRIPT_DIR/$(basename "${BASH_SOURCE[0]}")" "$candidate/Contents/Resources/codex-home"
1864-
cp "$SCRIPT_DIR/VSCodeIdentity.applescript" "$candidate/Contents/Resources/VSCodeIdentity.applescript" 2>/dev/null || \
1865-
cp "$REPOSITORY_ROOT/macos/VSCodeIdentity.applescript" "$candidate/Contents/Resources/VSCodeIdentity.applescript"
1866-
cp "$SCRIPT_DIR/../config/umgpt-models.toml" "$candidate/Contents/config/umgpt-models.toml"
1867-
printf '%s\n' "$name" >"$candidate/Contents/Resources/identity"
1868-
printf '%s\n' "$metadata" >"$candidate/Contents/Resources/codex-contexts-open-with"
1877+
# Callers may handle failure with ||, which disables Bash's errexit here.
1878+
mkdir -p "$candidate/Contents/config" || die "could not create the Finder app config directory"
1879+
cp "$SCRIPT_DIR/$(basename "${BASH_SOURCE[0]}")" "$candidate/Contents/Resources/codex-home" ||
1880+
die "could not bundle the Finder app helper"
1881+
cp "$source" "$candidate/Contents/Resources/VSCodeIdentity.applescript" ||
1882+
die "could not bundle the Finder handler source"
1883+
cp "$SCRIPT_DIR/../config/umgpt-models.toml" "$candidate/Contents/config/umgpt-models.toml" ||
1884+
die "could not bundle the U-M GPT model defaults"
1885+
printf '%s\n' "$name" >"$candidate/Contents/Resources/identity" || die "could not record the Finder app identity"
1886+
printf '%s\n' "$metadata" >"$candidate/Contents/Resources/codex-contexts-open-with" ||
1887+
die "could not record the Finder app ownership marker"
18691888
info_plist="$candidate/Contents/Info.plist"
18701889
"$plist_buddy" -c 'Delete :CFBundleIdentifier' "$info_plist" >/dev/null 2>&1 || true
1871-
"$plist_buddy" -c "Add :CFBundleIdentifier string org.codex-contexts.vscode.$name" "$info_plist"
1890+
"$plist_buddy" -c "Add :CFBundleIdentifier string org.codex-contexts.vscode.$name" "$info_plist" || die "could not configure the Finder app identifier"
18721891
"$plist_buddy" -c 'Delete :CFBundleName' "$info_plist" >/dev/null 2>&1 || true
1873-
"$plist_buddy" -c "Add :CFBundleName string VS Code - $name" "$info_plist"
1892+
"$plist_buddy" -c "Add :CFBundleName string VS Code - $name" "$info_plist" || die "could not configure the Finder app name"
18741893
"$plist_buddy" -c 'Delete :CFBundleDisplayName' "$info_plist" >/dev/null 2>&1 || true
1875-
"$plist_buddy" -c "Add :CFBundleDisplayName string VS Code - $name" "$info_plist"
1894+
"$plist_buddy" -c "Add :CFBundleDisplayName string VS Code - $name" "$info_plist" || die "could not configure the Finder app display name"
18761895
"$plist_buddy" -c 'Delete :LSUIElement' "$info_plist" >/dev/null 2>&1 || true
1877-
"$plist_buddy" -c 'Add :LSUIElement bool true' "$info_plist"
1896+
"$plist_buddy" -c 'Add :LSUIElement bool true' "$info_plist" || die "could not configure the Finder app UI behavior"
18781897
"$plist_buddy" -c 'Delete :CFBundleDocumentTypes' "$info_plist" >/dev/null 2>&1 || true
1879-
"$plist_buddy" -c 'Add :CFBundleDocumentTypes array' "$info_plist"
1880-
"$plist_buddy" -c 'Add :CFBundleDocumentTypes:0 dict' "$info_plist"
1881-
"$plist_buddy" -c 'Add :CFBundleDocumentTypes:0:CFBundleTypeName string Folder' "$info_plist"
1882-
"$plist_buddy" -c 'Add :CFBundleDocumentTypes:0:CFBundleTypeRole string Editor' "$info_plist"
1883-
"$plist_buddy" -c 'Add :CFBundleDocumentTypes:0:LSHandlerRank string Alternate' "$info_plist"
1884-
"$plist_buddy" -c 'Add :CFBundleDocumentTypes:0:LSItemContentTypes array' "$info_plist"
1885-
"$plist_buddy" -c 'Add :CFBundleDocumentTypes:0:LSItemContentTypes:0 string public.folder' "$info_plist"
1886-
chmod 700 "$candidate/Contents/Resources/codex-home"
1898+
"$plist_buddy" -c 'Add :CFBundleDocumentTypes array' "$info_plist" || die "could not configure Finder document types"
1899+
"$plist_buddy" -c 'Add :CFBundleDocumentTypes:0 dict' "$info_plist" || die "could not configure the Finder folder handler"
1900+
"$plist_buddy" -c 'Add :CFBundleDocumentTypes:0:CFBundleTypeName string Folder' "$info_plist" || die "could not configure the Finder folder type"
1901+
"$plist_buddy" -c 'Add :CFBundleDocumentTypes:0:CFBundleTypeRole string Editor' "$info_plist" || die "could not configure the Finder folder role"
1902+
"$plist_buddy" -c 'Add :CFBundleDocumentTypes:0:LSHandlerRank string Alternate' "$info_plist" || die "could not configure the Finder handler rank"
1903+
"$plist_buddy" -c 'Add :CFBundleDocumentTypes:0:LSItemContentTypes array' "$info_plist" || die "could not configure Finder content types"
1904+
"$plist_buddy" -c 'Add :CFBundleDocumentTypes:0:LSItemContentTypes:0 string public.folder' "$info_plist" || die "could not configure the Finder folder content type"
1905+
chmod 700 "$candidate/Contents/Resources/codex-home" || die "could not make the Finder app helper executable"
18871906
/usr/bin/codesign --force --deep --sign - "$candidate" >/dev/null || die "could not sign the VS Code Finder app"
18881907
macos_vscode_unregister_editor "$runtime_app"
18891908
if [[ -e "$app" ]]; then mv "$app" "$build/previous.app" || die "could not stage the old VS Code Finder app"; fi

‎docs/macos-open-with.md‎

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -33,14 +33,14 @@ The bundled helper opens a named copy of VS Code for each identity, such as
3333
`VS Code - work` or `VS Code - default`. Hover over its running Dock icon to
3434
read the identity, or click it to focus the editor. Projects using a running
3535
identity are sent to that instance. The installer also creates identity-specific
36-
Finder handlers named `VS Code - NAME`. Choose **Open With > VS Code - NAME** to
37-
open a folder through the matching identity. The folder must have an approved
36+
app handlers named `VS Code - NAME` for file managers that expose folder handlers
37+
in an **Open With** menu, such as ForkLift. The folder must have an approved
3838
`.envrc` selecting that identity and its generated workspace. If it selects a
3939
different identity, the handler reports the mismatch instead of opening the
40-
wrong account. Use **Codex Project** when you need to set up or change a folder
41-
context. Do not pin or directly relaunch the generated editor copies under
42-
`~/.codex-homes`; those copies need the helper's environment and launch
43-
arguments. See
40+
wrong account. Finder exposes folder actions through **Quick Actions** and
41+
**Services**; use **Open in Codex Project** there. Do not pin or directly
42+
relaunch the generated editor copies under `~/.codex-homes`; those copies need
43+
the helper's environment and launch arguments. See
4444
[macOS Dock names](projects-vscode.md#macos-dock-names) for copy refresh and
4545
the original-editor opt-out.
4646

@@ -72,13 +72,16 @@ In Finder:
7272
folder, choose an identity directly.
7373

7474
You can also select a folder and use **Finder > Services > Open in Codex
75-
Project**. If **Open With > Codex Project** is available, it opens the same app;
76-
folder context menus do not consistently offer that route. Do not choose
77-
**Change All**; Finder should remain the normal default handler for folders.
78-
79-
For an already configured project, **Open With > VS Code - NAME** routes the
80-
folder through the matching identity. The handler passes it to `codex-home`,
81-
which opens the project workspace and uses the named Dock instance.
75+
Project**. In Finder, this is the supported folder route; the app offers the
76+
current context and setup/change actions. File managers such as ForkLift may
77+
also expose **Open With > VS Code - NAME** for a direct identity-specific
78+
launch. Do not choose **Change All**; Finder should remain the normal default
79+
handler for folders.
80+
81+
In file managers that provide an **Open With** menu for folders, choose
82+
**VS Code - NAME** to route an already configured project through that
83+
identity. The handler passes it to `codex-home`, which opens the project
84+
workspace and uses the named Dock instance.
8285

8386
When the folder has no `.envrc`, the app goes straight to identity selection.
8487
When it is already configured, the menu offers:

‎docs/projects-vscode.md‎

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -267,20 +267,21 @@ the source application. Where cloning is unavailable, macOS makes a normal
267267
copy, which can use a full application's worth of storage per identity. The
268268
original VS Code installation remains available for ordinary use.
269269

270-
On macOS, the Finder installer creates a small handler app at
271-
`~/Applications/VS Code - NAME.app` for each configured identity. It appears in
272-
**Open With** and forwards the selected folder to `codex-home vscode NAME`.
273-
The actual editor copy remains under `CODEX_HOME/vscode-editor`, where the
274-
helper keeps its separate Dock name without registering it as a direct folder
275-
opener.
270+
On macOS, the Finder integration creates a small handler app at
271+
`~/Applications/VS Code - NAME.app` for each configured identity. File managers
272+
that expose folder handlers in an **Open With** menu, such as ForkLift, can use
273+
it to forward the folder to `codex-home vscode NAME`. In Finder, use **Quick
274+
Actions > Open in Codex Project** or **Services > Open in Codex Project**; that
275+
app reads the folder's current context. The actual editor copy remains under
276+
`CODEX_HOME/vscode-editor` and keeps its separate Dock name.
276277

277278
Hover over a running editor's Dock icon to read its identity. Clicking that
278279
running icon focuses the editor. The icon artwork remains VS Code's, while
279280
the workspace title and status-bar color also identify the selected context.
280281

281-
**After quitting, start the identity through the Finder Quick Action, its
282-
matching Open With entry, or `codex-home`.** Do not pin or directly open the
283-
generated editor copies: macOS launches them without the helper's
282+
**After quitting, start the identity through the Finder Quick Action, a
283+
file-manager Open With handler, or `codex-home`.** Do not pin or directly open
284+
the generated editor copies: macOS launches them without the helper's
284285
`CODEX_HOME`, approved environment, or `--user-data-dir` arguments.
285286

286287
Update the original VS Code installation and let the helper refresh its

‎docs/troubleshooting.md‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,15 +39,16 @@ copies under `$CODEX_HOME/vscode-editor/`; hover over a running icon to read
3939
colors also identify the context.
4040

4141
Quit the intended identity and reopen its project through the helper, Finder's
42-
**Open With > VS Code - NAME** handler, or the **Codex Project** Quick Action.
43-
Each named Finder handler checks that the folder's `.envrc` selects the same
44-
identity before opening it. If `CODEX_VSCODE_DOCK_LABEL=0` is set, the helper
42+
**Open in Codex Project** Quick Action/Service, or a file manager's
43+
**Open With > VS Code - NAME** handler. The identity-specific handler checks
44+
that the folder's `.envrc` selects the same identity before opening it. If
45+
`CODEX_VSCODE_DOCK_LABEL=0` is set, the helper
4546
uses the original installed editor without the named editor copy. An
4647
already-running identity keeps its original application until it quits.
4748

4849
Clicking a running Dock icon focuses that editor. **After quitting, use
49-
Finder > Quick Actions > Open in Codex Project, Open With > VS Code - NAME,
50-
or `codex-home vscode`.** Do not pin or directly launch the generated editor
50+
Finder > Quick Actions > Open in Codex Project, a file manager's Open With
51+
> VS Code - NAME, or `codex-home vscode`.** Do not pin or directly launch the generated editor
5152
copies under the identity home: a direct macOS launch does not supply the
5253
identity environment and isolated user-data arguments.
5354

‎tests/common.sh‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
#!/usr/bin/env bash
2+
3+
resolve_codex_home_tool() {
4+
local bin_dir="$1" candidate
5+
local -a matches=()
6+
for candidate in "$bin_dir"/*home; do
7+
[[ -f "$candidate" && -x "$candidate" ]] && matches+=("$candidate")
8+
done
9+
if [[ "${#matches[@]}" -ne 1 ]]; then
10+
printf 'test setup failure: expected one executable *home tool in %s; found %s\n' \
11+
"$bin_dir" "${#matches[@]}" >&2
12+
return 1
13+
fi
14+
printf '%s\n' "${matches[0]}"
15+
}

0 commit comments

Comments
 (0)