feat(shopify): add plugin for Shopify CLI (#14037)

This commit is contained in:
Robby Russell
2026-09-14 21:41:28 +02:00
committed by GitHub
parent e995678ca8
commit 53f663f3d8
3 changed files with 2360 additions and 0 deletions
+112
View File
@@ -0,0 +1,112 @@
# Shopify CLI plugin
This plugin adds aliases, helper functions and completion for the
[Shopify CLI](https://shopify.dev/docs/api/shopify-cli).
To use it, add `shopify` to the plugins array in your zshrc file:
```zsh
plugins=(... shopify)
```
## Requirements
[Shopify CLI](https://shopify.dev/docs/api/shopify-cli) 4.0 or newer, installed
and on your `PATH`. The plugin does nothing if it is not:
```zsh
npm install -g @shopify/cli
# or
brew tap shopify/shopify && brew install shopify-cli
```
## Aliases
| Alias | Command | Description |
| :----- | :-------- | :--------------------- |
| `shop` | `shopify` | The Shopify CLI itself |
## Functions
| Function | Description |
| :------------- | :--------------------------------------------------------------------------------- |
| `shopd` | Runs `theme dev`, `app dev` or `hydrogen dev`, whichever fits the current project |
| `shopi` | Shows what the current project is connected to |
| `shopify_here` | Prints the kind of Shopify project you are in and the commands worth running in it |
| `shopify` | Wraps the CLI to confirm before irreversible commands. See Settings |
`shopd` and `shopi` work out which topic applies by looking at the project you
are standing in, so you do not have to remember whether this directory is a
theme, an app or a Hydrogen storefront. They complete like the command they
stand in for, so in a theme `shopd --<TAB>` offers the flags of
`shopify theme dev`.
## Completion
Every command completes with a description of what it does, at every level, and
so do their flags, short forms and allowed values. `--environment` completes
from the `[environments.*]` sections of your local `shopify.theme.toml` or
`shopify.app.toml`.
The Shopify CLI ships no completion generator of its own, so `_shopify` is
generated from `shopify commands --json` and kept in this repository. It targets
Shopify CLI 4.x.
## Settings
Set these with `zstyle` in your zshrc, before Oh My Zsh is sourced.
### Confirming destructive commands
```zsh
zstyle ':omz:plugins:shopify' confirm-destructive no
```
By default the plugin asks for confirmation before three irreversible
operations:
- `shopify theme push` with `--live`/`-l` or `--publish`/`-p`, which overwrites
or publishes the live storefront
- `shopify theme delete`, which the CLI's own help describes as impossible to
undo
- `shopify app deploy --allow-deletes`, which can permanently remove extensions
Nothing else is intercepted, and the prompt is skipped when the command already
carries `--force`/`-f` or when stdin is not a terminal, so scripts, pipelines
and CI are unaffected. Set the style to `no` to turn the prompt off entirely.
### Completing theme IDs
```zsh
zstyle ':omz:plugins:shopify' dynamic-theme-completion yes
```
Off by default. When enabled, `--theme` is completed from
`shopify theme list --json`, showing each theme's name and role next to its ID
instead of making you copy IDs by hand. It is opt-in because it runs the CLI and
makes a network call when you press <kbd>Tab</kbd>, and because it needs you to
be authenticated. Results are cached for five minutes.
### Disabling the aliases
This works for every Oh My Zsh plugin:
```zsh
zstyle ':omz:plugins:shopify' aliases no
```
## Caveats
- `shopify` is a shell function here. Run `command shopify` to reach the binary
directly.
- When a project has its own copy of the CLI in `node_modules/.bin/shopify`,
that one is used in preference to the global install. App and Hydrogen
projects normally pin the CLI in `package.json`, and running a different
global version against them is a common source of confusing errors.
- `--password` and `--store-password` are unrelated, and you often need both.
`--password` is the CLI's own authentication token, from the Theme Access app
or the Admin API. `--store-password` is the password for a
password-protected storefront.
- `shopify theme push` deletes remote files that are missing locally, and
`shopify theme pull` deletes local files that are missing remotely, unless you
pass `--nodelete`/`-n`. The plugin does not prompt for these.
File diff suppressed because it is too large Load Diff
+203
View File
@@ -0,0 +1,203 @@
# Shopify CLI: completion, aliases and guard rails.
# https://shopify.dev/docs/api/shopify-cli
if (( ! $+commands[shopify] )); then
return
fi
# Prefer a project-local CLI: app and Hydrogen projects pin their own version.
# The walk stops at $HOME so a stray ~/node_modules cannot hijack every project.
# $commands avoids resolving back to the wrapper function below.
function _shopify_bin() {
local dir="$PWD"
while [[ -n "$dir" && "$dir" != "/" && "$dir" != "$HOME" ]]; do
if [[ -x "$dir/node_modules/.bin/shopify" ]]; then
print -r -- "$dir/node_modules/.bin/shopify"
return 0
fi
dir="${dir:h}"
done
print -r -- "$commands[shopify]"
}
# Print the project type in $PWD: hydrogen, app or theme.
# Hydrogen is tested first because those projects usually carry a
# shopify.app.toml too. shopify.theme.toml is not a theme marker: it only
# exists once environments are configured.
function _shopify_project_type() {
local dir="$PWD"
local -a app_config
while [[ -n "$dir" && "$dir" != "/" && "$dir" != "$HOME" ]]; do
if [[ -x "$dir/node_modules/.bin/h2" ]] ||
[[ -r "$dir/package.json" && "$(<"$dir/package.json")" == *'"@shopify/hydrogen"'* ]]; then
return 0
fi
app_config=("$dir"/shopify.app*.toml(N))
if (( $#app_config )); then
print -r -- app
return 0
fi
if [[ -f "$dir/config/settings_schema.json" && -f "$dir/layout/theme.liquid" ]]; then
print -r -- theme
return 0
fi
dir="${dir:h}"
done
return 1
}
# Does this invocation need confirming? Sets REPLY to the reason.
function _shopify_is_destructive() {
zstyle -T ':omz:plugins:shopify' confirm-destructive || return 1
# Nobody to answer: CI and pipelines are unaffected.
[[ -t 0 ]] || return 1
local topic="$1" subcommand="$2" arg
local -a long
local short=""
# Short flags can be bundled, so collect them as characters: -al counts as l.
for arg in "${@[3,-1]}"; do
[[ "$arg" == "--" ]] && break
case "$arg" in
--*) long+=("${arg%%=*}") ;;
-?*) short+="${${arg%%=*}#-}" ;;
esac
done
# An explicit force flag states the intent already.
if (( long[(I)--force] )) || [[ "$short" == *f* ]]; then
return 1
fi
case "$topic $subcommand" in
"theme push")
if (( long[(I)--live] || long[(I)--publish] )) || [[ "$short" == *[lp]* ]]; then
REPLY="this overwrites or publishes the live theme on the storefront."
return 0
fi
;;
"theme delete")
REPLY="deleting a theme cannot be undone."
return 0
;;
"app deploy")
if (( long[(I)--allow-deletes] )); then
REPLY="this can permanently remove app extensions."
return 0
fi
;;
esac
return 1
}
# Adds a confirmation prompt before irreversible operations. Everything else
# passes straight through, so `theme dev` stays interactive and pipes stay clean.
function shopify() {
local REPLY
if _shopify_is_destructive "$@"; then
print -u2 -- "shopify: $REPLY"
if ! read -q "?Continue? [y/N] "; then
print -u2 -- ""
return 130
fi
print -u2 -- ""
fi
"$(_shopify_bin)" "$@"
}
# Print the commands worth running in the current directory.
function shopify_here() {
local project_type
project_type="$(_shopify_project_type)"
case "$project_type" in
theme)
print -- "Shopify theme project."
print -- " shopify theme dev Preview locally with live reload"
print -- " shopify theme check Lint the theme"
print -- " shopify theme list List the themes on the store"
print -- " shopify theme pull Download a remote theme into this folder"
print -- " shopify theme push Upload this folder to a remote theme"
;;
app)
print -- "Shopify app project."
print -- " shopify app dev Run the app against a development store"
print -- " shopify app info Show how the app is configured"
print -- " shopify app deploy Deploy the app and its extensions"
print -- " shopify app logs Stream app logs"
;;
hydrogen)
print -- "Hydrogen storefront."
print -- " shopify hydrogen dev Run the storefront locally"
print -- " shopify hydrogen build Build for production"
print -- " shopify hydrogen deploy Deploy to Oxygen"
print -- " shopify hydrogen link Link this project to a storefront"
;;
*)
print -- "No Shopify project found here."
print -- " shopify theme init Start a new theme"
print -- " shopify app init Start a new app"
print -- " shopify hydrogen init Start a new Hydrogen storefront"
;;
esac
}
# Run the dev server for whichever kind of project this is.
function shopd() {
local project_type
if ! project_type="$(_shopify_project_type)"; then
print -u2 -- "shopd: not inside a Shopify theme, app or Hydrogen project."
return 1
fi
shopify "$project_type" dev "$@"
}
# Show what the current project is connected to.
function shopi() {
local project_type
if ! project_type="$(_shopify_project_type)"; then
print -u2 -- "shopi: not inside a Shopify theme, app or Hydrogen project."
return 1
fi
case "$project_type" in
hydrogen) shopify hydrogen list "$@" ;;
*) shopify "$project_type" info "$@" ;;
esac
}
# Complete the helpers like the commands they stand in for.
if (( $+functions[compdef] )); then
_shopd() {
local project_type
project_type="$(_shopify_project_type)" || return 1
words=(shopify "$project_type" dev ${words[2,-1]})
(( CURRENT += 2 ))
_shopify
}
_shopi() {
local project_type
project_type="$(_shopify_project_type)" || return 1
if [[ "$project_type" == hydrogen ]]; then
words=(shopify hydrogen list ${words[2,-1]})
else
words=(shopify "$project_type" info ${words[2,-1]})
fi
(( CURRENT += 2 ))
_shopify
}
compdef _shopd shopd
compdef _shopi shopi
fi
alias shop='shopify'