Skip to content

feat(cli): symlink DMG CLI onto PATH + show launch path in diagnostics#129

Merged
stuffbucket merged 3 commits into
mainfrom
feat/cli-symlink-and-launch-path
Jun 17, 2026
Merged

feat(cli): symlink DMG CLI onto PATH + show launch path in diagnostics#129
stuffbucket merged 3 commits into
mainfrom
feat/cli-symlink-and-launch-path

Conversation

@stuffbucket

Copy link
Copy Markdown
Owner

Why

Two related gaps surfaced while cleaning up stale maximal installs on a dev machine:

  1. The DMG never put maximal on PATH. The signed .app ships its CLI at /Applications/Maximal.app/Contents/MacOS/maximal, which is on no default PATH. In the field, maximal in a terminal only ever resolved because of stale copies left behind by old curl/brew installs — exactly the thing that generates false results once you remove them. The first-launch shim that was supposed to handle this (uninstall.ts already had the teardown markers) was never actually wired up.
  2. Diagnostics couldn't tell a DMG launch from a brew/dev one — so a "which binary is actually running?" question needed shell forensics.

What

CLI-on-PATH shim (src/lib/cli-path.ts, called from run-server)

On maximal start, if the running binary is a .app-bundle CLI, symlink it into ~/.local/bin:

  • ~/.local/bin exists → treat as an existing XDG bin dir, leave shell profiles untouched (open terminals keep working, no restart).
  • doesn't exist → create it and append a marker-bracketed PATH block to ~/.zprofile (new terminals pick it up).
  • Always a symlink, never a copy — no binary duplication, tracks app updates automatically.
  • Refuses to clobber a real user file at the target; idempotent on every boot; no-op for Homebrew/dev launches. Windows unaffected (MSI already registers PATH via the registry).

Policy note: the shim deliberately does not inspect $PATH — a GUI-launched sidecar inherits launchd's minimal PATH, not the user's interactive shell PATH, so directory existence is the signal.

Uninstall hardening (src/uninstall.ts)

  • Shares the PATH-block markers with cli-path (no more duplicated constants).
  • Uses lstat so a broken symlink (app dragged to Trash) is still detected and removed instead of silently skipped.

Diagnostics (launch_path + launch_kind)

  • New schema fields capturing process.execPath and a coarse classification (dmg-app / homebrew / user-bin / dev / other).
  • Surfaced as a "Launched from" row in Settings → Diagnostics.
  • Settings window height bumped 640 → 760 (min 480 → 560) so the added row fits — the "resize the diagnostics window vertically" ask.

Tests

  • tests/cli-path.test.ts — classification across all install shapes (incl. the bun-interpreter-at-/opt/homebrew/bin/bun edge that must read as dev, not homebrew), fresh-machine create, existing-dir-no-profile-edit, idempotency, stale-relink, foreign-file refusal, PATH-block append/idempotency.
  • Updated diagnostics schema round-trip + live-route assertions.
  • Full suite green (936 pass / 0 fail), tsc (root + shell) clean, lint clean.

Not done here (follow-ups worth filing)

  • The tap README/install/ has no maximal entry, and there's no desktop Cask for the .dmg. Separate from this change.

The macOS .dmg ships its CLI inside the app bundle
(/Applications/Maximal.app/Contents/MacOS/maximal), which is on no
default PATH — so `maximal` only ever worked in a terminal via stray
copies left by old curl/brew installs. Add a first-launch shim
(lib/cli-path.ts, called from run-server) that symlinks the bundle CLI
into ~/.local/bin:

- If ~/.local/bin already exists, treat it as an existing XDG bin dir
  and leave shell profiles untouched (open terminals keep working —
  no restart needed).
- If it doesn't, create it and append a marker-bracketed PATH block to
  ~/.zprofile (new terminals pick it up).
- Always a symlink, never a copy: no duplication, tracks app updates.
- Refuses to clobber a real user file at the target; idempotent on
  every boot; no-op for Homebrew/dev launches (not an .app bundle).
  Windows is unaffected (MSI registers PATH via the registry).

uninstall now shares the PATH-block markers with cli-path and uses
lstat so a broken symlink (app trashed) is still removed.

Diagnostics: capture process.execPath as launch_path + a coarse
launch_kind (dmg-app / homebrew / user-bin / dev / other) so brew vs
DMG launches are distinguishable in a bug report, surfaced as a
"Launched from" row in Settings → Diagnostics. Bump the Settings
window height (640→760) so the added row fits.
Comment thread tests/cli-path.test.ts Fixed
CodeQL flagged a check-then-use file-system race (js/file-system-race)
where lstatSync preceded readFileSync on the same path. The content
assertion alone is sufficient — a symlink to APP_EXEC could never read
back as the user's original file — so drop the redundant lstat.
Add a Models section to Settings between API keys and Logs. Lists the
models available through Maximal grouped by kind (chat, embeddings),
each row showing key capabilities — context window, max output, and
vision/tools/streaming/reasoning flags. A toolbar shows the cache age
and a Refresh now button that forces an upstream re-fetch, since the
activity-based staleness timer is a freshness optimization, not a
correctness guarantee.

Backend: new auth-gated /settings/api/models route (GET list +
POST /refresh), with a schema-validated ModelSummary/ModelsListResponse
contract. Frontend: a React island mirroring the api-clients/apps
pattern, reusing existing disclosure/table/chip primitives and tokens.
Re-pulls on nav via maximal:models-refresh.
@stuffbucket
stuffbucket merged commit 1e70756 into main Jun 17, 2026
4 checks passed
stuffbucket added a commit that referenced this pull request Jun 17, 2026
#129 squash-merged the stacked models PR (#130) into a single
feat(cli) commit, so release-please could only see the CLI line.
Add the settings/models entry by hand so the 0.4.26 changelog
reflects both features that actually shipped.
stuffbucket added a commit that referenced this pull request Jun 17, 2026
* chore(main): release 0.4.26

* docs(changelog): credit the models settings feature in 0.4.26

#129 squash-merged the stacked models PR (#130) into a single
feat(cli) commit, so release-please could only see the CLI line.
Add the settings/models entry by hand so the 0.4.26 changelog
reflects both features that actually shipped.

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: stuffbucket <231133237+stuffbucket@users.noreply.github.com>
@stuffbucket
stuffbucket deleted the feat/cli-symlink-and-launch-path branch July 13, 2026 05:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants