diff --git a/bin/omarchy-agent b/bin/omarchy-agent index 4c12562f..ad8c14e4 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 515514d2..c4a567db 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|claude|codex|grok|gemini|openclaw|hermes|copilot|crush] +# omarchy:args=[pi|omp|opencode|claude|codex|grok|gemini|openclaw|hermes|copilot|crush|cursor-agent] # omarchy:examples=omarchy default agent | omarchy default agent codex | omarchy default agent claude installing=false @@ -35,8 +35,9 @@ gemini | gemini-cli) agent="gemini"; name="Gemini" ;; openclaw) agent="openclaw"; name="OpenClaw"; agent_installer="omarchy-install-openclaw-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 @@ -55,8 +56,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 fa058e7e..5cee9516 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/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 c6f0ee34..51486feb 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 29519250..c6b76994 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 a2f7daac..a3a2943d 100644 --- a/default/omarchy/omarchy-menu.jsonc +++ b/default/omarchy/omarchy-menu.jsonc @@ -134,6 +134,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.gemini": {"icon":"󰫢","label":"Gemini","checked":"[[ \"$(omarchy-default-agent)\" == \"gemini\" ]]","action":"omarchy-default-agent gemini"}, "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"}, @@ -157,7 +158,7 @@ "setup.default.editor": {"icon":"","label":"Editor","title":"Default Editor"}, "setup.default.editor.neovim": {"icon":"","label":"Neovim","when":"omarchy-cmd-present nvim","checked":"[[ \"$(omarchy-default-editor)\" == \"nvim\" ]]","action":"omarchy-default-editor nvim"}, "setup.default.editor.vscode": {"icon":"","label":"VSCode","when":"omarchy-cmd-present code","checked":"[[ \"$(omarchy-default-editor)\" == \"code\" ]]","action":"omarchy-default-editor code"}, - "setup.default.editor.cursor": {"icon":"","label":"Cursor","when":"omarchy-cmd-present cursor","checked":"[[ \"$(omarchy-default-editor)\" == \"cursor\" ]]","action":"omarchy-default-editor cursor"}, + "setup.default.editor.cursor": {"icon":"","iconFont":"omarchy","label":"Cursor","when":"omarchy-cmd-present cursor","checked":"[[ \"$(omarchy-default-editor)\" == \"cursor\" ]]","action":"omarchy-default-editor cursor"}, "setup.default.editor.zed": {"icon":"","label":"Zed","when":"omarchy-cmd-present zeditor","checked":"[[ \"$(omarchy-default-editor)\" == \"zeditor\" ]]","action":"omarchy-default-editor zed"}, "setup.default.editor.sublime": {"icon":"","label":"Sublime Text","when":"omarchy-cmd-present sublime_text","checked":"[[ \"$(omarchy-default-editor)\" == \"sublime_text\" ]]","action":"omarchy-default-editor sublime_text"}, "setup.default.editor.helix": {"icon":"","label":"Helix","when":"omarchy-cmd-present helix","checked":"[[ \"$(omarchy-default-editor)\" == \"helix\" ]]","action":"omarchy-default-editor helix"}, @@ -222,7 +223,7 @@ "install.service.bitwarden": {"icon":"󰟵","label":"Bitwarden","when":"! 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 ]] && ! 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","when":"! omarchy-pkg-present visual-studio-code-bin","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-editor-vscode"}, - "install.editor.cursor": {"icon":"","label":"Cursor","when":"! omarchy-pkg-present cursor-bin","action":"omarchy-install-and-launch Cursor cursor-bin cursor"}, + "install.editor.cursor": {"icon":"","iconFont":"omarchy","label":"Cursor","when":"! omarchy-pkg-present cursor-bin","action":"omarchy-install-and-launch Cursor cursor-bin cursor"}, "install.editor.zed": {"icon":"","label":"Zed","when":"! omarchy-pkg-present zed","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-editor-zed"}, "install.editor.sublime": {"icon":"","label":"Sublime Text","when":"! omarchy-pkg-present sublime-text-4","action":"omarchy-install-and-launch 'Sublime Text' sublime-text-4 sublime_text"}, "install.editor.helix": {"icon":"","label":"Helix","when":"! 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 d71905c9..bee5ec82 100644 --- a/install/user/mise.sh +++ b/install/user/mise.sh @@ -13,6 +13,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 # Every line above writes a stub and cannot fail. This one can: it exits diff --git a/manual/17-ai.md b/manual/17-ai.md index 918822f3..bd85671b 100644 --- a/manual/17-ai.md +++ b/manual/17-ai.md @@ -13,8 +13,8 @@ Omarchy treats AI coding agents as first-class citizens, but it doesn't pick a f | `grok` | Grok CLI from xAI | | `pi` | [Mario Zechner's Pi](https://github.com/badlogic/pi-mono) | | `omp` | [Oh My Pi](https://github.com/can1357/oh-my-pi) | - | `hermes` | [Hermes](https://hermes-agent.nousresearch.com/), Nous Research's agent | +| `cursor-agent` | [Cursor CLI](https://cursor.com/cli) | To wrap an additional CLI the same way, run `omarchy-mise-install [command-name]`. The stubs are kept current along with everything else mise manages when you run `omarchy update` (or the `mup` alias). 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 f2640ef7..652d97c2 100644 --- a/test/shell.d/default-agent-test.sh +++ b/test/shell.d/default-agent-test.sh @@ -95,6 +95,7 @@ export OMARCHY_TEST_AGENT_MENU_LOG="$menu_log" grok_package="npm:@xai-official/grok" omp_package="github:can1357/oh-my-pi" crush_package="crush" +cursor_agent_package="cursor-agent" assert_lazy_stub() { local package=$1 @@ -112,18 +113,36 @@ assert_lazy_stub() { assert_lazy_stub "$grok_package" grok assert_lazy_stub "$omp_package" omp assert_lazy_stub "$crush_package" crush +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 "$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" 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" +: >"$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" @@ -136,6 +155,7 @@ touch "$test_home/.local/state/omarchy/preinstalls-removed" : >"$stub_log" source "$ROOT/migrations/1785617047.sh" >/dev/null source "$ROOT/migrations/1785846769.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" @@ -160,11 +180,20 @@ rm "$test_home/.local/state/omarchy/preinstalls-removed" pass "agent migrations install working wrappers without overriding the preinstall opt-out" omarchy-remove-preinstalls >/dev/null -for command in omp grok crush; do +for command in omp 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" @@ -220,6 +249,8 @@ declare -A expected_agents=( [gemini-cli]="gemini" [copilot]="copilot" [github-copilot]="copilot" + [cursor]="cursor-agent" + [cursor-agent]="cursor-agent" ) declare -A expected_packages=( @@ -232,6 +263,7 @@ declare -A expected_packages=( [grok]="$grok_package" [gemini]="gemini" [copilot]="copilot" + [cursor-agent]="$cursor_agent_package" ) for selection in "${!expected_agents[@]}"; do @@ -292,6 +324,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" @@ -376,6 +437,7 @@ 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 gemini gemini --yolo --prompt-interactive "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 copilot copilot --allow-all --interactive "Review this project" pass "agent launcher adapts initial prompts for every supported agent" @@ -395,6 +457,7 @@ assert_bypass codex codex --approve-for-me assert_bypass crush crush --yolo assert_bypass grok grok --permission-mode bypassPermissions assert_bypass gemini gemini --yolo +assert_bypass cursor-agent cursor-agent --yolo --trust assert_bypass hermes hermes --yolo assert_bypass copilot copilot --allow-all pass "agent launcher skips permission prompts for every supported agent" diff --git a/test/shell.d/menu-test.sh b/test/shell.d/menu-test.sh index f5d051f7..5eb00b69 100644 --- a/test/shell.d/menu-test.sh +++ b/test/shell.d/menu-test.sh @@ -207,6 +207,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]) => { @@ -225,7 +226,7 @@ assertDeepEqual( defaultItems .filter(item => item.parent === 'setup.default.agent') .map(item => item.label), - ['Claude', 'Codex', 'Copilot', 'Crush', 'Gemini', 'Grok', 'Hermes', 'omp', 'OpenClaw', 'OpenCode', 'Pi'], + ['Claude', 'Codex', 'Copilot', 'Crush', 'Cursor CLI', 'Gemini', 'Grok', 'Hermes', 'omp', 'OpenClaw', 'OpenCode', 'Pi'], 'menu sorts coding agents alphabetically' ) assert(!defaultById['install.ai.crush'], 'menu removes Crush from Install > AI') @@ -516,5 +517,5 @@ assert( JS font_charset=$(fc-query --format='%{charset}' "$ROOT/default/fonts/omarchy/omarchy.ttf") -[[ $font_charset == *"e900-e908 e90a-e90c"* ]] || fail "Omarchy icon font includes every custom menu glyph" +[[ $font_charset == *"e900-e908 e90a-e90d"* ]] || fail "Omarchy icon font includes every custom menu glyph" pass "Omarchy icon font includes the official agent marks" diff --git a/test/shell.d/release-ai-port-test.sh b/test/shell.d/release-ai-port-test.sh index 76d4aefd..d9996149 100644 --- a/test/shell.d/release-ai-port-test.sh +++ b/test/shell.d/release-ai-port-test.sh @@ -24,7 +24,7 @@ for (const [id, [pkg, glyph]] of Object.entries(selected)) { assert(fs.existsSync(path.join(root, `bin/omarchy-remove-ai-${id}`)), `${id} remover exists`) } assertDeepEqual(Object.keys(rows).filter(k => k.startsWith('remove.ai.')).sort(), Object.keys(selected).map(k => `remove.ai.${k}`).sort(), 'removal prerequisite imports no unselected apps') -assertDeepEqual(Object.keys(rows).filter(k => k.startsWith('setup.default.agent.')).map(k => k.split('.').pop()).sort(), ['claude', 'codex', 'copilot', 'crush', 'gemini', 'grok', 'hermes', 'omp', 'openclaw', 'opencode', 'pi'], 'baseline agent choices survive with only Hermes and OpenClaw added') +assertDeepEqual(Object.keys(rows).filter(k => k.startsWith('setup.default.agent.')).map(k => k.split('.').pop()).sort(), ['claude', 'codex', 'copilot', 'crush', 'cursor-agent', 'gemini', 'grok', 'hermes', 'omp', 'openclaw', 'opencode', 'pi'], 'baseline agent choices survive with only selected release agents added') assert(!fs.existsSync(path.join(root, 'bin/omarchy-theme-set-openclaw')), 'deferred OpenClaw theming is absent') JS