diff --git a/bin/omarchy-agent b/bin/omarchy-agent index 0aa0273b..3705eefa 100755 --- a/bin/omarchy-agent +++ b/bin/omarchy-agent @@ -95,6 +95,14 @@ codex) command=(codex --approve-for-me) [[ -n ${prompt:-} ]] && command+=(-- "$prompt") ;; +cursor-agent) + # --yolo covers commands only; the workspace trust dialog has its own flag. + # A one-word prompt naming a subcommand (update, login, help) still runs that + # subcommand after a bare --, so the agent subcommand is named outright, and + # -- after it keeps a prompt starting with a dash from being read as an option. + command=(cursor-agent --yolo --trust) + [[ -n ${prompt:-} ]] && command+=(agent -- "$prompt") + ;; hermes) if [[ -n ${prompt:-} ]]; then command=(env -u HERMES_SESSION_SOURCE hermes chat --yolo --tui "--query=$prompt") diff --git a/bin/omarchy-default-agent b/bin/omarchy-default-agent index f832b22d..e79100e7 100755 --- a/bin/omarchy-default-agent +++ b/bin/omarchy-default-agent @@ -1,7 +1,7 @@ #!/bin/bash # omarchy:summary=Set and launch the default coding agent -# omarchy:args=[pi|omp|opencode|ori|claude|codex|grok|openclaw|agy|hermes|copilot|crush] +# omarchy:args=[pi|omp|opencode|ori|claude|codex|grok|openclaw|agy|hermes|copilot|crush|cursor-agent] # omarchy:examples=omarchy default agent | omarchy default agent codex | omarchy default agent claude installing=false @@ -36,8 +36,9 @@ openclaw) agent="openclaw"; name="OpenClaw"; agent_installer="omarchy-install-op agy | antigravity | antigravity-cli | gemini | gemini-cli) agent="agy"; name="Antigravity"; agent_package="antigravity-cli" ;; hermes) agent="hermes"; name="Hermes"; agent_installer="omarchy-install-hermes-cli" ;; copilot | github-copilot) agent="copilot"; name="GitHub Copilot" ;; +cursor | cursor-agent) agent="cursor-agent"; name="Cursor CLI" ;; *) - echo "Usage: omarchy-default-agent " + echo "Usage: omarchy-default-agent " exit 1 ;; esac @@ -56,8 +57,15 @@ if [[ -n ${agent_installer:-} ]]; then agent_present() { "$agent_installer" --check; } agent_install() { "$agent_installer" --now; } else - agent_present() { mise where "$agent_package" &>/dev/null; } - agent_install() { mise use -g "$agent_package"; } + # Anything at the wrapper's path other than the wrapper is the user's own + # install, such as the symlink Cursor's installer leaves. A mise copy would + # only shadow it, since the mise shims precede ~/.local/bin on PATH. + user_install() { + [[ -x $HOME/.local/bin/$agent ]] && + { [[ -L $HOME/.local/bin/$agent ]] || ! grep -q '^mise use -g' "$HOME/.local/bin/$agent"; } + } + agent_present() { user_install || mise where "$agent_package" &>/dev/null; } + agent_install() { user_install || mise use -g "$agent_package"; } fi if [[ $installing == "false" ]] && ! agent_present; then diff --git a/bin/omarchy-remove-preinstalls b/bin/omarchy-remove-preinstalls index 3dca7b22..5da7a4e6 100755 --- a/bin/omarchy-remove-preinstalls +++ b/bin/omarchy-remove-preinstalls @@ -17,6 +17,13 @@ if gum confirm "Are you sure you want to remove all preinstalled web apps, TUI w ~/.local/bin/gh ~/.local/bin/opencode ~/.local/bin/playwright ~/.local/bin/playwright-cli ~/.local/bin/pi \ ~/.local/bin/omp ~/.local/bin/ori ~/.local/bin/grok ~/.local/bin/crush ~/.local/bin/ghui ~/.local/bin/hunk + # Cursor's own installer links ~/.local/bin/cursor-agent as well, so only + # the mise wrapper omarchy-mise-install wrote is a preinstall. + if [[ -f ~/.local/bin/cursor-agent && ! -L ~/.local/bin/cursor-agent ]] && + grep -Eq '^mise use -g .*"cursor-agent"' ~/.local/bin/cursor-agent; then + rm -f ~/.local/bin/cursor-agent + fi + # Only the wrapper omarchy-install-hermes-cli wrote is a preinstall. Hermes # Desktop's command, an official install, or anything else at that path is # the user's, so it is the installer that decides whether the wrapper is its diff --git a/default/fonts/omarchy/README.md b/default/fonts/omarchy/README.md index e538684d..cb633829 100644 --- a/default/fonts/omarchy/README.md +++ b/default/fonts/omarchy/README.md @@ -15,6 +15,7 @@ The private-use glyphs in `omarchy.ttf` are: - `U+E90A` — Hermes, Font Awesome's staff-snake (CC BY 4.0) from , the mark Hermes serves as its favicon: their app icon is a portrait that reads as a smudge at menu size - `U+E90B` — Perplexity, from - `U+E90C` — OpenClaw, traced from the lobster mascot the openclaw package ships as `dist/control-ui/favicon.svg`, since upstream publishes no monochrome SVG +- `U+E90D` — Cursor, from The agent marks are monochrome so the menu can render them using the active theme's foreground and selection colors. diff --git a/default/fonts/omarchy/omarchy.ttf b/default/fonts/omarchy/omarchy.ttf index 7a26ed1d..8d6df783 100644 Binary files a/default/fonts/omarchy/omarchy.ttf and b/default/fonts/omarchy/omarchy.ttf differ diff --git a/default/omarchy/omarchy-menu.jsonc b/default/omarchy/omarchy-menu.jsonc index d824319e..a81b4baa 100644 --- a/default/omarchy/omarchy-menu.jsonc +++ b/default/omarchy/omarchy-menu.jsonc @@ -140,6 +140,7 @@ "setup.default.agent.codex": {"icon":"","iconFont":"omarchy","label":"Codex","checked":"[[ \"$(omarchy-default-agent)\" == \"codex\" ]]","action":"omarchy-default-agent codex"}, "setup.default.agent.copilot": {"icon":"","label":"Copilot","checked":"[[ \"$(omarchy-default-agent)\" == \"copilot\" ]]","action":"omarchy-default-agent copilot"}, "setup.default.agent.crush": {"icon":"󰋑","label":"Crush","checked":"[[ \"$(omarchy-default-agent)\" == \"crush\" ]]","action":"omarchy-default-agent crush"}, + "setup.default.agent.cursor-agent": {"icon":"","iconFont":"omarchy","label":"Cursor CLI","checked":"[[ \"$(omarchy-default-agent)\" == \"cursor-agent\" ]]","action":"omarchy-default-agent cursor-agent"}, "setup.default.agent.grok": {"icon":"","iconFont":"omarchy","label":"Grok","checked":"[[ \"$(omarchy-default-agent)\" == \"grok\" ]]","action":"omarchy-default-agent grok"}, "setup.default.agent.hermes": {"icon":"","iconFont":"omarchy","label":"Hermes","checked":"[[ \"$(omarchy-default-agent)\" == \"hermes\" ]]","action":"omarchy-default-agent hermes"}, "setup.default.agent.omp": {"icon":"","iconFont":"omarchy","label":"omp","checked":"[[ \"$(omarchy-default-agent)\" == \"omp\" ]]","action":"omarchy-default-agent omp"}, @@ -163,7 +164,7 @@ "setup.default.editor": {"icon":"","label":"Editor","title":"Default Editor"}, "setup.default.editor.neovim": {"icon":"","label":"Neovim","checked":"[[ \"$(omarchy-default-editor)\" == \"nvim\" ]]","action":"omarchy-default-editor nvim"}, "setup.default.editor.vscode": {"icon":"","label":"VSCode","checked":"[[ \"$(omarchy-default-editor)\" == \"code\" ]]","action":"omarchy-default-editor code"}, - "setup.default.editor.cursor": {"icon":"","label":"Cursor","checked":"[[ \"$(omarchy-default-editor)\" == \"cursor\" ]]","action":"omarchy-default-editor cursor"}, + "setup.default.editor.cursor": {"icon":"","iconFont":"omarchy","label":"Cursor","checked":"[[ \"$(omarchy-default-editor)\" == \"cursor\" ]]","action":"omarchy-default-editor cursor"}, "setup.default.editor.zed": {"icon":"","label":"Zed","checked":"[[ \"$(omarchy-default-editor)\" == \"zeditor\" ]]","action":"omarchy-default-editor zed"}, "setup.default.editor.sublime": {"icon":"","label":"Sublime Text","checked":"[[ \"$(omarchy-default-editor)\" == \"sublime_text\" ]]","action":"omarchy-default-editor sublime_text"}, "setup.default.editor.helix": {"icon":"","label":"Helix","checked":"[[ \"$(omarchy-default-editor)\" == \"helix\" ]]","action":"omarchy-default-editor helix"}, @@ -228,7 +229,7 @@ "install.service.bitwarden": {"icon":"󰟵","label":"Bitwarden","disabled":"omarchy-pkg-present bitwarden","action":"omarchy-install-and-launch Bitwarden 'bitwarden bitwarden-cli' bitwarden"}, "install.service.chromium-account": {"icon":"","label":"Chromium Account","when":"[[ -f ~/.config/chromium-flags.conf ]]","disabled":"grep -q oauth2-client-id ~/.config/chromium-flags.conf","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-chromium-google-account"}, "install.editor.vscode": {"icon":"","label":"VSCode","disabled":"omarchy-pkg-present visual-studio-code-bin","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-editor-vscode"}, - "install.editor.cursor": {"icon":"","label":"Cursor","disabled":"omarchy-pkg-present cursor-bin","action":"omarchy-install-and-launch Cursor cursor-bin cursor"}, + "install.editor.cursor": {"icon":"","iconFont":"omarchy","label":"Cursor","disabled":"omarchy-pkg-present cursor-bin","action":"omarchy-install-and-launch Cursor cursor-bin cursor"}, "install.editor.zed": {"icon":"","label":"Zed","disabled":"omarchy-pkg-present zed","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-editor-zed"}, "install.editor.sublime": {"icon":"","label":"Sublime Text","disabled":"omarchy-pkg-present sublime-text-4","action":"omarchy-install-and-launch 'Sublime Text' sublime-text-4 sublime_text"}, "install.editor.helix": {"icon":"","label":"Helix","disabled":"omarchy-pkg-present helix","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-editor-helix"}, diff --git a/install/user/mise.sh b/install/user/mise.sh index 4baeffce..ea118b54 100644 --- a/install/user/mise.sh +++ b/install/user/mise.sh @@ -9,6 +9,8 @@ omarchy-mise-install npm:playwright playwright omarchy-mise-install pi omarchy-mise-install github:can1357/oh-my-pi omp omarchy-mise-install npm:@xai-official/grok grok +# Cursor's own installer links the same path, so a re-provision keeps it. +omarchy-cmd-missing cursor-agent && omarchy-mise-install cursor-agent omarchy-mise-install npm:@kitlangton/ghui ghui omarchy-mise-install aqua:modem-dev/hunk hunk omarchy-mise-install github:basecamp/hey-cli hey diff --git a/manual/17-ai.md b/manual/17-ai.md index 90d3a10e..156c0912 100644 --- a/manual/17-ai.md +++ b/manual/17-ai.md @@ -15,6 +15,7 @@ Omarchy treats AI coding agents as first-class citizens, but it doesn't pick a f | `omp` | [Oh My Pi](https://github.com/can1357/oh-my-pi) | | `ori` | [Ori](https://openrouter.ai/docs/guides/ori/harness), OpenRouter's harness | | `hermes` | [Hermes](https://hermes-agent.nousresearch.com/), Nous Research's agent | +| `cursor-agent` | [Cursor CLI](https://cursor.com/cli) | `ori` is the odd one out: it runs the other harnesses against OpenRouter's whole model catalog, so `ori claude`, `ori codex`, or `ori opencode` start those agents on whichever model you point them at, and `ori code` is Ori's own agent. diff --git a/migrations/1788577553.sh b/migrations/1788577553.sh new file mode 100644 index 00000000..cc1a54ad --- /dev/null +++ b/migrations/1788577553.sh @@ -0,0 +1,8 @@ +echo "Install Cursor CLI via mise wrapper" + +# Cursor's own installer links ~/.local/bin/cursor-agent, so an existing +# command is the user's and stays. The wrapper resolves cursor-agent through +# mise's registry, which lists it from 2026.8.15 on. +if omarchy-cmd-missing cursor-agent && [[ ! -f $HOME/.local/state/omarchy/preinstalls-removed ]]; then + omarchy-mise-install cursor-agent +fi diff --git a/test/shell.d/default-agent-test.sh b/test/shell.d/default-agent-test.sh index 6a2f15b2..26fdad38 100644 --- a/test/shell.d/default-agent-test.sh +++ b/test/shell.d/default-agent-test.sh @@ -98,6 +98,7 @@ omp_package="github:can1357/oh-my-pi" crush_package="crush" agy_package="antigravity-cli" ori_package="github:OpenRouterLabs/ori-releases" +cursor_agent_package="cursor-agent" assert_lazy_stub() { local package=$1 @@ -116,16 +117,23 @@ assert_lazy_stub "$grok_package" grok assert_lazy_stub "$omp_package" omp assert_lazy_stub "$crush_package" crush assert_lazy_stub "$ori_package" ori +assert_lazy_stub "$cursor_agent_package" cursor-agent pass "custom agent lazy stubs preserve their mise packages" -source "$ROOT/install/user/mise.sh" +OMARCHY_TEST_MISSING_COMMAND=cursor-agent source "$ROOT/install/user/mise.sh" grep -Fx "$agy_package agy" "$stub_log" >/dev/null || fail "user setup creates the Antigravity lazy stub" grep -Fx "$grok_package grok" "$stub_log" >/dev/null || fail "user setup creates the Grok lazy stub" +grep -Fx "$cursor_agent_package" "$stub_log" >/dev/null || fail "user setup creates the Cursor CLI lazy stub" grep -Fx "$omp_package omp" "$stub_log" >/dev/null || fail "user setup creates the Oh My Pi lazy stub" grep -Fx "$crush_package" "$stub_log" >/dev/null || fail "user setup creates the Crush lazy stub" grep -Fx "$ori_package ori" "$stub_log" >/dev/null || fail "user setup creates the Ori lazy stub" pass "user setup creates the custom agent lazy stubs" +: >"$stub_log" +source "$ROOT/install/user/mise.sh" +grep -Fx "$cursor_agent_package" "$stub_log" >/dev/null && fail "user setup replaces an existing cursor-agent command" +pass "user setup keeps an existing Cursor CLI install" + : >"$stub_log" source "$ROOT/migrations/1785617047.sh" >/dev/null grep -Fx "$omp_package omp" "$stub_log" >/dev/null || fail "Oh My Pi migration creates a working lazy stub" @@ -134,6 +142,17 @@ grep -Fx "$omp_package omp" "$stub_log" >/dev/null || fail "Oh My Pi migration c source "$ROOT/migrations/1787342993.sh" >/dev/null grep -Fx "$ori_package ori" "$stub_log" >/dev/null || fail "Ori migration creates a working lazy stub" +: >"$stub_log" +export OMARCHY_TEST_MISSING_COMMAND=cursor-agent +source "$ROOT/migrations/1788577553.sh" >/dev/null +unset OMARCHY_TEST_MISSING_COMMAND +grep -Fx "$cursor_agent_package" "$stub_log" >/dev/null || fail "Cursor CLI migration creates a working lazy stub" + +: >"$stub_log" +source "$ROOT/migrations/1788577553.sh" >/dev/null +[[ ! -s $stub_log ]] || fail "Cursor CLI migration reinstalls an existing cursor-agent command" +pass "Cursor CLI migration preserves an existing Cursor CLI install" + : >"$stub_log" source "$ROOT/migrations/1785846769.sh" >/dev/null grep -Fx "$omp_package omp" "$stub_log" >/dev/null || fail "agent migration repairs the Oh My Pi lazy stub" @@ -216,6 +235,7 @@ touch "$test_home/.local/state/omarchy/preinstalls-removed" source "$ROOT/migrations/1785617047.sh" >/dev/null source "$ROOT/migrations/1785846769.sh" >/dev/null source "$ROOT/migrations/1787342993.sh" >/dev/null +OMARCHY_TEST_MISSING_COMMAND=cursor-agent source "$ROOT/migrations/1788577553.sh" >/dev/null [[ ! -s $stub_log ]] || fail "agent migrations respect the preinstall opt-out" [[ ! -e $test_home/.local/bin/omp ]] || fail "agent migration removes the obsolete Oh My Pi wrapper after opt-out" @@ -242,11 +262,20 @@ pass "agent migrations install working wrappers without overriding the preinstal touch "$test_home/.local/bin/agy" "$test_home/.local/bin/ori" omarchy-remove-preinstalls >/dev/null -for command in agy omp ori grok crush; do +for command in agy omp ori grok crush cursor-agent; do [[ ! -e $test_home/.local/bin/$command ]] || fail "Remove Preinstalls deletes the $command lazy stub" done pass "Remove Preinstalls deletes every optional agent lazy stub" +# Cursor's installer links the same path, so anything but the mise wrapper is +# the user's own install. +touch "$test_home/.local/bin/cursor-agent.official" +ln -s cursor-agent.official "$test_home/.local/bin/cursor-agent" +omarchy-remove-preinstalls >/dev/null +[[ -L $test_home/.local/bin/cursor-agent ]] || fail "Remove Preinstalls keeps an official Cursor CLI install" +rm -f "$test_home/.local/bin/cursor-agent" "$test_home/.local/bin/cursor-agent.official" +pass "Remove Preinstalls keeps an official Cursor CLI install" + [[ -z $(omarchy-default-agent) ]] || fail "default agent is unset until one is chosen" pass "default agent is unset until one is chosen" @@ -307,6 +336,8 @@ declare -A expected_agents=( [gemini-cli]="agy" [copilot]="copilot" [github-copilot]="copilot" + [cursor]="cursor-agent" + [cursor-agent]="cursor-agent" ) declare -A expected_packages=( @@ -320,6 +351,7 @@ declare -A expected_packages=( [grok]="$grok_package" [agy]="$agy_package" [copilot]="copilot" + [cursor-agent]="$cursor_agent_package" ) for selection in "${!expected_agents[@]}"; do @@ -380,6 +412,35 @@ mapfile -d '' -t agent_open_args <"$agent_open_log" fail "installed agent opens in a new terminal after selection" pass "installed agents select and open without notifications" +# Cursor's installer links the wrapper's path, and the mise shims precede +# ~/.local/bin, so a mise copy would shadow the user's own install. +touch "$test_home/.local/bin/cursor-agent.official" +chmod +x "$test_home/.local/bin/cursor-agent.official" +ln -s cursor-agent.official "$test_home/.local/bin/cursor-agent" +: >"$terminal_log" +: >"$mise_log" +: >"$agent_open_log" +omarchy-default-agent cursor-agent +[[ ! -s $terminal_log ]] || fail "an official Cursor CLI install needs no install terminal" +[[ ! -s $mise_log ]] || fail "an official Cursor CLI install is left to itself by mise" +[[ $(<"$agent_file") == "cursor-agent" ]] || fail "an official Cursor CLI install becomes the default" +mapfile -d '' -t agent_open_args <"$agent_open_log" +[[ ${#agent_open_args[@]} == 1 && ${agent_open_args[0]} == "omarchy-agent" ]] || + fail "an official Cursor CLI install opens after selection" +rm -f "$test_home/.local/bin/cursor-agent" "$test_home/.local/bin/cursor-agent.official" +printf '%s\n' copilot >"$agent_file" +pass "selecting an official Cursor CLI install skips mise" + +# A file nothing can run is not an install; the wrapper is still wanted. +touch "$test_home/.local/bin/cursor-agent" +: >"$terminal_log" +omarchy-default-agent cursor-agent +mapfile -d '' -t terminal_args <"$terminal_log" +[[ ${terminal_args[*]} == "omarchy-default-agent --install cursor-agent" ]] || + fail "a dead file at the wrapper's path still installs Cursor CLI" +rm -f "$test_home/.local/bin/cursor-agent" +pass "a dead file at the wrapper's path does not pass for an install" + : >"$agent_open_log" if omarchy-default-agent unsupported >"$test_tmp/invalid-output" 2>&1; then fail "default agent rejects unsupported providers" @@ -464,6 +525,7 @@ assert_launch claude claude --permission-mode auto -- "Review this project" assert_launch codex codex --approve-for-me -- "Review this project" assert_launch crush crush run "Review this project" assert_launch grok grok --permission-mode bypassPermissions -- "Review this project" +assert_launch cursor-agent cursor-agent --yolo --trust agent -- "Review this project" assert_launch hermes env -u HERMES_SESSION_SOURCE hermes chat --yolo --tui "--query=Review this project" assert_launch agy agy --dangerously-skip-permissions --prompt-interactive "Review this project" assert_launch copilot copilot --allow-all --interactive "Review this project" @@ -484,6 +546,7 @@ assert_bypass claude claude --permission-mode auto assert_bypass codex codex --approve-for-me assert_bypass crush crush --yolo assert_bypass grok grok --permission-mode bypassPermissions +assert_bypass cursor-agent cursor-agent --yolo --trust assert_bypass hermes hermes --yolo assert_bypass agy agy --dangerously-skip-permissions assert_bypass copilot copilot --allow-all diff --git a/test/shell.d/menu-test.sh b/test/shell.d/menu-test.sh index c65ec000..1365c3bf 100644 --- a/test/shell.d/menu-test.sh +++ b/test/shell.d/menu-test.sh @@ -228,6 +228,7 @@ const expectedAgents = { openclaw: { icon: '\ue90c', iconFont: 'omarchy', label: 'OpenClaw' }, copilot: { icon: '', label: 'Copilot' }, crush: { icon: '󰋑', label: 'Crush' }, + 'cursor-agent': { icon: '\ue90d', iconFont: 'omarchy', label: 'Cursor CLI' }, } assert( Object.entries(expectedAgents).every(([agent, expected]) => { @@ -246,7 +247,7 @@ assertDeepEqual( defaultItems .filter(item => item.parent === 'setup.default.agent') .map(item => item.label), - ['Antigravity', 'Claude', 'Codex', 'Copilot', 'Crush', 'Grok', 'Hermes', 'omp', 'OpenClaw', 'OpenCode', 'Ori', 'Pi'], + ['Antigravity', 'Claude', 'Codex', 'Copilot', 'Crush', 'Cursor CLI', 'Grok', 'Hermes', 'omp', 'OpenClaw', 'OpenCode', 'Ori', 'Pi'], 'menu sorts coding agents alphabetically' ) const expectedDefaults = { @@ -638,5 +639,5 @@ assert( JS font_charset=$(fc-query --format='%{charset}' "$ROOT/default/fonts/omarchy/omarchy.ttf") -[[ $font_charset == *"e900-e90c"* ]] || fail "Omarchy icon font includes every custom menu glyph" +[[ $font_charset == *"e900-e90d"* ]] || fail "Omarchy icon font includes every custom menu glyph" pass "Omarchy icon font includes the official agent marks"