simplified-shopify-cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Dean Schulz
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,281 @@
1
+ # Simplified Shopify CLI
2
+
3
+ Short Shopify theme commands with store aliases you configure in a JSON dotfile.
4
+ Installing the package provides **`sd`, `sp`, `sl`, `si`, `spl`, `spa`, `sc`, `sf`,
5
+ and `lo` directly**. The `sshop` command remains available for compatibility.
6
+
7
+ This is the **0.1.0 release**, licensed under MIT, with no runtime
8
+ dependencies. Source: [deanschulz4/Shopify-CLI-Simplified](https://github.com/deanschulz4/Shopify-CLI-Simplified).
9
+
10
+ Install from npm using the instructions below. See [release notes and verification](docs/RELEASING.md).
11
+
12
+ ## Install
13
+
14
+ Requires Node.js 22.12+ and Shopify CLI on your PATH. Authentication remains
15
+ managed by Shopify CLI. Install Shopify CLI if you do not already have it:
16
+
17
+ ```sh
18
+ npm install -g @shopify/cli
19
+ ```
20
+
21
+ Install the published package (including zsh on macOS):
22
+
23
+ ```sh
24
+ npm install -g simplified-shopify-cli
25
+ shelp
26
+ ```
27
+
28
+ To install from a source checkout instead, run `npm install -g .` from the project directory.
29
+
30
+ Or try it without a global installation:
31
+
32
+ ```sh
33
+ node bin/sshop.js --help
34
+ node bin/sshop.js --config examples/sshop.example.json sd demo "My Theme" --dry-run
35
+ ```
36
+
37
+ ## Configure your stores
38
+
39
+ Create a personal config usable from any project:
40
+
41
+ ```sh
42
+ sshop init --global
43
+ sshop stores add demo example-store --global
44
+ sshop stores add sandbox example-sandbox.myshopify.com --global
45
+ sshop stores
46
+ ```
47
+
48
+ This creates `~/.sshop.json`. For project-specific configuration, run the same
49
+ commands without `--global` from the project root. Example:
50
+
51
+ ```json
52
+ {
53
+ "version": 1,
54
+ "stores": {
55
+ "demo": "example-store.myshopify.com",
56
+ "sandbox": "example-sandbox.myshopify.com"
57
+ },
58
+ "defaultStore": "sandbox",
59
+ "defaults": {
60
+ "nodelete": true,
61
+ "themeEditorSync": true
62
+ }
63
+ }
64
+ ```
65
+
66
+ Store values accept Shopify store prefixes, full `myshopify.com` domains, or
67
+ their HTTPS URLs. Custom storefront domains are not accepted. Config is data;
68
+ it cannot contain shell commands. Keep authentication tokens in Shopify's normal
69
+ authentication flow or environment variables, outside this file.
70
+
71
+ ### Config precedence
72
+
73
+ 1. Built-in defaults: preserve unmatched local files during pull; enable Theme Editor sync during dev.
74
+ 2. Personal `~/.sshop.json`.
75
+ 3. The nearest `.sshop.json`, searching upward from your current directory.
76
+
77
+ Project aliases override personal aliases with the same name. Other aliases
78
+ remain available. Set `defaultStore` to `null` to clear an inherited default.
79
+ With no configured default or explicit store, Shopify resolves its own store.
80
+
81
+ `sshop --config /path/to/config.json …` or `SSHOP_CONFIG` selects a single isolated
82
+ config instead of merging. An explicit missing config is an error.
83
+ `sshop config` shows the effective settings and exact source files.
84
+
85
+ Writes always target the current directory, or the personal file with `--global`,
86
+ or the explicitly selected file. They never silently modify an ancestor's config.
87
+ `init` refuses to overwrite an existing file. `stores add` explicitly replaces
88
+ an existing alias; `stores remove ALIAS` removes it only from the selected file.
89
+ Removing a project override can expose the personal alias again.
90
+
91
+ Add `.sshop.json` to each theme project's `.gitignore` if it contains personal
92
+ store mappings. This package excludes local configuration and legacy source
93
+ from its distributable archive.
94
+
95
+ ## Shopify command cheat sheet
96
+
97
+ ### Rules
98
+
99
+ - Run theme commands from your theme project folder.
100
+ - Replace `<store_abr>` with your configured store abbreviation.
101
+ - Select the current store once with `si <store_abr>`. After it succeeds, omit `<store_abr>` in commands such as `sl`, `sd "My Theme"`, `sp -p`, `sp -d`, and `si -j`.
102
+ - Run `si` to check the current store; specify another abbreviation to switch.
103
+ - Store aliases are still required when adding or removing aliases.
104
+
105
+ ### Flags
106
+
107
+ - `-g` = `--global` for `sinit`, `sstores add/remove`, and `simport`.
108
+ - `-d` = `--development` for `sp`, `spl`, `spa`, and `si`.
109
+ - Both long and short forms work.
110
+
111
+ ### Important
112
+
113
+ - A configured `defaultStore` overrides the remembered Shopify store. `si` does not change `defaultStore`; clear it or set it to the intended store.
114
+ - The selected store is not permanently bound to your project.
115
+ - `si --dry-run` does not select a store.
116
+ - `sinit` creates configuration only.
117
+
118
+ This follows Shopify's [connecting-to-a-store behavior](https://shopify.dev/docs/storefronts/themes/tools/cli#connecting-to-a-store).
119
+
120
+ ### Commands
121
+
122
+ | Command | What it does |
123
+ | --- | --- |
124
+ | `si <store_abr>` | Select the current Shopify store and show theme information; omit `<store_abr>` in later theme commands |
125
+ | `sl` | List all themes |
126
+ | `sd "Theme Name"` or `sd Theme_Name` | Start development on a named theme with editor sync |
127
+ | `sd` | Start development using a Shopify development theme |
128
+ | `sp "Theme Name"` or `sp theme_name` | Pull a named theme; preserve unmatched local files |
129
+ | `sp` | Pull the live theme; preserve unmatched local files |
130
+ | `sp -p` | Pull only live template/config JSON |
131
+ | `spl "My Theme"` | Pull only template/config JSON from a named theme |
132
+ | `spa` | Pull the live theme; may delete unmatched local files |
133
+ | `sp -d` | Pull the development theme |
134
+ | `sc` | Check theme code for issues |
135
+ | `sf` | Automatically fix supported theme issues |
136
+ | `lo` | Log out of Shopify |
137
+
138
+ ### Store aliases and setup
139
+
140
+ | Command | What it does |
141
+ | --- | --- |
142
+ | `sstores` | List available store aliases |
143
+ | `sstores add <store_abr> example-store -g` | Add/update a personal store alias |
144
+ | `sstores remove <store_abr> -g` | Remove a personal store alias |
145
+ | `sconfig` | Show effective settings and config locations |
146
+ | `sinit` | Create a project-specific config |
147
+ | `sinit -g` | Create a personal config; refuses to overwrite |
148
+ | `simport ./base_custom_cli.sh -g` | Import aliases from the original file |
149
+ | `shelp` | Show command help |
150
+
151
+ ### Useful options
152
+
153
+ | Example | What it does |
154
+ | --- | --- |
155
+ | `sd "My Theme" --dry-run` | Preview the command without running Shopify |
156
+ | `sd -s <store_abr> -t "My Theme"` | Explicitly select store and theme |
157
+ | `sd -p 9293` | Use a different development port |
158
+ | `sl -j` | Return the theme list as JSON |
159
+ | `sd --config "./custom.json"` | Use a specific configuration file |
160
+
161
+ ### Remember
162
+
163
+ - Quotes are required for theme names containing spaces. Use straight quotes (`"`) when copying commands into your terminal.
164
+ - Pull (`sp`) overwrites matching local files. Preserving unmatched files does not prevent existing files from being overwritten.
165
+ - Dev (`sd`) uploads and continuously syncs changes to the selected remote theme.
166
+ - `sd <store_abr> -p 9293` sets the development port; `sp <store_abr> -p` pulls only template/config JSON.
167
+ - `si <store_abr> -j` returns theme information as JSON; `sl <store_abr> -j` returns the theme list as JSON.
168
+ - `sf` modifies local files. Use `--dry-run` to inspect command targeting before running Shopify.
169
+
170
+ ### Argument handling and compatibility
171
+
172
+ All commands above work without an `sshop` prefix. The prefixed equivalents
173
+ remain supported, for example `sshop dev`, `sshop pull`, and `sshop sd`.
174
+
175
+ A single positional argument to `sd`/`sp` is a store if it matches a configured
176
+ alias or looks like a URL/domain; otherwise it is a theme. With two positional
177
+ arguments, they mean store and theme. Use `--store` for an unmapped prefix with
178
+ no theme; use `--theme` when a theme name matches an alias.
179
+
180
+ Wrapper flags use separate values (`--store demo`, not `--store=demo`). Additional
181
+ Shopify flags go after `--`. Put targeting flags and `--dry-run` before `--`.
182
+ The original `sd -- --port 9293` and `sl -- --json` forms remain supported;
183
+ `--port` and `--json` also work directly. Nodelete/editor-sync defaults are
184
+ configured in the dotfile.
185
+
186
+ `--dry-run` prints the command without starting Shopify and redacts password
187
+ values. Arguments are forwarded as an array, without `eval` or shell expansion.
188
+ Normal commands inherit Shopify's interactive terminal and exit code.
189
+ Shopify's live-theme protections remain in place.
190
+
191
+ ## Shell setup
192
+
193
+ ### macOS / Bash / Zsh
194
+
195
+ No shell setup is needed for the installed executable commands. If you still load
196
+ old aliases/functions from `base_custom_cli.sh`, they take precedence over
197
+ executables. Remove the old Shopify definitions, or load the compatibility
198
+ functions after the old file using `eval "$(sshop shell)"` in your shell profile.
199
+ Restart the shell. General helpers in the original file are unaffected.
200
+
201
+ ### Windows Command Prompt and PowerShell
202
+
203
+ Install with `npm.cmd install -g simplified-shopify-cli`. Command Prompt can use `sd`, `sp`, etc.
204
+ In PowerShell, use `sd.cmd`, `sp.cmd`, `sl.cmd`, etc. to avoid conflicts with
205
+ built-in aliases and npm PowerShell script execution policy restrictions:
206
+
207
+ ```powershell
208
+ sinit.cmd --global
209
+ sstores.cmd add demo example-store --global
210
+ sd.cmd demo "My Theme" --dry-run
211
+ ```
212
+
213
+ For exactly `sd`, `sp`, `sl`, etc. in PowerShell, add this one-time setup to your
214
+ PowerShell profile (provided profile scripts are permitted in your environment):
215
+
216
+ ```powershell
217
+ . ([scriptblock]::Create((sshop.cmd shell powershell | Out-String)))
218
+ ```
219
+
220
+ It replaces matching PowerShell aliases with functions forwarding to the installed
221
+ `.cmd` commands. This includes `sp`, `sl`, and `si`; their normal PowerShell
222
+ meanings will change in that session. Remove the profile line and restart to
223
+ restore the original behavior. No execution-policy changes are made by this package.
224
+
225
+ Personal config on Windows is `%USERPROFILE%\.sshop.json`. Shopify CLI must be
226
+ installed globally with npm. In WSL, install Node and both CLIs inside WSL and
227
+ follow the Bash instructions.
228
+
229
+ **Verification:** the [initial CI run](https://github.com/deanschulz4/Shopify-CLI-Simplified/actions/runs/34994515508)
230
+ passed the Node 22/24 matrix on macOS, Linux, and Windows, including installed
231
+ commands, real Shopify CLI help invocation, and Windows PowerShell shortcuts.
232
+ Each subsequent change must pass its own CI run. Live Shopify login/pull/dev
233
+ workflows remain a separate pre-release check.
234
+
235
+ ## Migrate the original file
236
+
237
+ ```sh
238
+ sshop import ./base_custom_cli.sh --global
239
+ ```
240
+
241
+ Import reads literal entries from `map_store()` without sourcing the shell file.
242
+ Identical duplicate mappings are collapsed; conflicting duplicates or existing
243
+ aliases with different destinations stop the import. Other shell logic is ignored.
244
+ Use a local import first if you want to inspect the result before changing your
245
+ personal config.
246
+
247
+ The original file stays unchanged as a local reference. General terminal,
248
+ Node/nvm, BigCommerce, and NopCommerce helpers remain there; this first package
249
+ focuses on Shopify. The old `sync` helper remains there too: its hard-coded
250
+ `origin/master` merge and checkout behavior needs a separate configurable design.
251
+ It is not installed by this package. The old `fix` permissions alias and `gitfix`
252
+ are also not exported. `clis` is still the ordinary Shopify installation command.
253
+
254
+ ## Development and verification
255
+
256
+ ```sh
257
+ npm run release:check
258
+ npm pack
259
+ ```
260
+
261
+ Tests cover config resolution, migration, argument handling, shell forwarding,
262
+ and subprocess exit codes using a fake Shopify executable. They do not contact
263
+ stores. The CI workflow checks Node 22/24 on macOS, Linux, and Windows and runs
264
+ `npm run release:check` to inspect and install the actual npm archive.
265
+ Native Windows dispatch supports the global npm installation of Shopify CLI.
266
+
267
+ Shopify command behavior follows the official [theme pull](https://shopify.dev/docs/api/shopify-cli/theme/theme-pull)
268
+ and [theme dev](https://shopify.dev/docs/api/shopify-cli/theme/theme-dev) documentation.
269
+
270
+ ## Uninstall
271
+
272
+ ```sh
273
+ npm uninstall -g simplified-shopify-cli
274
+ ```
275
+
276
+ Remove the optional shell startup line and restart your shell. Config files stay
277
+ in place for reuse; remove them yourself when no longer needed.
278
+
279
+ ## License
280
+
281
+ [MIT](LICENSE). The copyright attribution is public; personal store configuration is excluded from the npm package.
package/bin/lo.js ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import { runEntry } from '../src/entry.js';
3
+ await runEntry('logout');
package/bin/sc.js ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import { runEntry } from '../src/entry.js';
3
+ await runEntry('check');
package/bin/sconfig.js ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import { runEntry } from '../src/entry.js';
3
+ await runEntry('config');
package/bin/sd.js ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import { runEntry } from '../src/entry.js';
3
+ await runEntry('dev');
package/bin/sf.js ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import { runEntry } from '../src/entry.js';
3
+ await runEntry('fix');
package/bin/shelp.js ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import { runEntry } from '../src/entry.js';
3
+ await runEntry('help');
package/bin/si.js ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import { runEntry } from '../src/entry.js';
3
+ await runEntry('info');
package/bin/simport.js ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import { runEntry } from '../src/entry.js';
3
+ await runEntry('import');
package/bin/sinit.js ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import { runEntry } from '../src/entry.js';
3
+ await runEntry('init');
package/bin/sl.js ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import { runEntry } from '../src/entry.js';
3
+ await runEntry('list');
package/bin/sp.js ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import { runEntry } from '../src/entry.js';
3
+ await runEntry('pull');
package/bin/spa.js ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import { runEntry } from '../src/entry.js';
3
+ await runEntry('pull-all');
package/bin/spl.js ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import { runEntry } from '../src/entry.js';
3
+ await runEntry('pull-json');
package/bin/sshop.js ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ import { main } from '../src/cli.js';
3
+ try {
4
+ process.exitCode = await main(process.argv.slice(2));
5
+ } catch (error) {
6
+ console.error(`sshop: ${error.message}`);
7
+ process.exitCode = 1;
8
+ }
package/bin/sstores.js ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import { runEntry } from '../src/entry.js';
3
+ await runEntry('stores');
@@ -0,0 +1,38 @@
1
+ # Release 0.1.0
2
+
3
+ Initial release: configurable store aliases, personal/project JSON configuration,
4
+ direct commands, optional Bash/Zsh and PowerShell integration, literal legacy
5
+ alias import, dry-run output, development port shorthand (`sd -p`), JSON output
6
+ (`sl -j`, `si -j`), global configuration (`-g`), and development selection (`-d`).
7
+
8
+ ## Verification
9
+
10
+ - Node 22/24 CI on macOS, Linux, and Windows checks the installed archive,
11
+ command entry points, real Shopify CLI help, and PowerShell shortcuts.
12
+ - `npm run release:check` checks syntax, runs tests, builds and installs an
13
+ isolated archive, and scans for known credential formats and private domains.
14
+ - Live Shopify login, pull, development sync, and Ctrl+C against a disposable
15
+ store/theme have not been verified as part of this release. Automated tests
16
+ exercise argument forwarding without modifying stores.
17
+
18
+ Personal `.sshop.json` and legacy `base_custom_cli.sh` are ignored and excluded
19
+ from npm. The LICENSE contains the owner's public copyright attribution.
20
+ Pattern checks reduce leakage risk; they are not a complete security audit.
21
+
22
+ ## Publishing procedure
23
+
24
+ 1. Update the version and documentation; run `npm run release:check`.
25
+ 2. Commit and require the current commit's CI run to pass.
26
+ 3. Build an archive with `npm pack`, inspect `npm publish ARCHIVE --dry-run`,
27
+ then publish that archive with `npm publish ARCHIVE --access public`.
28
+ 4. Verify registry integrity, install the exact released version, and tag its
29
+ source commit. npm versions are immutable; corrections need a new version.
30
+
31
+ ## Install
32
+
33
+ ```sh
34
+ npm install -g simplified-shopify-cli
35
+ ```
36
+
37
+ Use `sd`, `sp`, `sl`, and the other commands directly. A CDN is not required
38
+ for this Node-based CLI. Authentication remains managed by Shopify CLI.
@@ -0,0 +1,12 @@
1
+ {
2
+ "version": 1,
3
+ "stores": {
4
+ "demo": "example-store.myshopify.com",
5
+ "sandbox": "example-sandbox.myshopify.com"
6
+ },
7
+ "defaultStore": "sandbox",
8
+ "defaults": {
9
+ "nodelete": true,
10
+ "themeEditorSync": true
11
+ }
12
+ }
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "simplified-shopify-cli",
3
+ "version": "0.1.0",
4
+ "description": "Configurable store aliases and shortcuts for Shopify theme development",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "engines": {
8
+ "node": ">=22.12.0"
9
+ },
10
+ "bin": {
11
+ "sshop": "bin/sshop.js",
12
+ "sd": "bin/sd.js",
13
+ "sp": "bin/sp.js",
14
+ "sl": "bin/sl.js",
15
+ "si": "bin/si.js",
16
+ "spl": "bin/spl.js",
17
+ "spa": "bin/spa.js",
18
+ "sc": "bin/sc.js",
19
+ "sf": "bin/sf.js",
20
+ "lo": "bin/lo.js",
21
+ "sstores": "bin/sstores.js",
22
+ "sconfig": "bin/sconfig.js",
23
+ "sinit": "bin/sinit.js",
24
+ "simport": "bin/simport.js",
25
+ "shelp": "bin/shelp.js"
26
+ },
27
+ "files": [
28
+ "bin/",
29
+ "src/",
30
+ "shell/",
31
+ "examples/",
32
+ "docs/",
33
+ "README.md",
34
+ "LICENSE"
35
+ ],
36
+ "scripts": {
37
+ "test": "node --test",
38
+ "check": "node --check bin/sshop.js && node --check src/cli.js && node --check src/config.js && node --check src/commands.js",
39
+ "prepack": "npm run check && npm test",
40
+ "release:check": "npm run check && npm test && node scripts/check-package.js"
41
+ },
42
+ "keywords": [
43
+ "shopify",
44
+ "cli",
45
+ "themes",
46
+ "aliases"
47
+ ],
48
+ "repository": {
49
+ "type": "git",
50
+ "url": "git+https://github.com/deanschulz4/Shopify-CLI-Simplified.git"
51
+ },
52
+ "homepage": "https://github.com/deanschulz4/Shopify-CLI-Simplified#readme",
53
+ "bugs": {
54
+ "url": "https://github.com/deanschulz4/Shopify-CLI-Simplified/issues"
55
+ },
56
+ "publishConfig": {
57
+ "access": "public",
58
+ "registry": "https://registry.npmjs.org/"
59
+ }
60
+ }
@@ -0,0 +1,15 @@
1
+ # Optional: dot-source from your PowerShell profile to use bare names such as sp.
2
+ # PowerShell has built-in aliases for some of these names. Replace those aliases
3
+ # in the current session only; removing the profile line and restarting restores them.
4
+ foreach ($shortcutName in @('sl', 'si', 'sp', 'sd', 'spl', 'spa', 'sc', 'sf', 'lo')) {
5
+ Remove-Item "Alias:$shortcutName" -Force -ErrorAction SilentlyContinue
6
+ }
7
+ function global:sl { & sl.cmd @args }
8
+ function global:si { & si.cmd @args }
9
+ function global:sp { & sp.cmd @args }
10
+ function global:sd { & sd.cmd @args }
11
+ function global:spl { & spl.cmd @args }
12
+ function global:spa { & spa.cmd @args }
13
+ function global:sc { & sc.cmd @args }
14
+ function global:sf { & sf.cmd @args }
15
+ function global:lo { & lo.cmd @args }
@@ -0,0 +1,12 @@
1
+ # Optional Bash/Zsh shortcuts. Source after your previous aliases to replace these names.
2
+ # Remove this source line to uninstall shortcuts. Requires the installed commands on PATH.
3
+ unalias sl si sp sd spl spa sc sf lo 2>/dev/null || :
4
+ function sl { command sl "$@"; }
5
+ function si { command si "$@"; }
6
+ function sp { command sp "$@"; }
7
+ function sd { command sd "$@"; }
8
+ function spl { command spl "$@"; }
9
+ function spa { command spa "$@"; }
10
+ function sc { command sc "$@"; }
11
+ function sf { command sf "$@"; }
12
+ function lo { command lo "$@"; }
package/src/cli.js ADDED
@@ -0,0 +1,144 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { spawn } from 'node:child_process';
5
+ import { filename, loadConfig, readConfig, writeConfig, normalizeStore, validateAlias, importAliases } from './config.js';
6
+ import { buildCommand, formatCommand } from './commands.js';
7
+
8
+ const help = `Simplified Shopify CLI (sshop)
9
+
10
+ Usage: sshop [--config FILE] COMMAND [arguments]
11
+
12
+ init [--global] Create an empty config (never overwrites)
13
+ stores List merged store aliases
14
+ stores add ALIAS STORE [--global] Add/update a store in the selected config
15
+ stores remove ALIAS [--global] Remove a store from the selected config
16
+ import FILE [--global] Import literal map_store aliases
17
+ config Show effective config and source files
18
+ shell [powershell] Print shell functions (optional)
19
+
20
+ list|sl [store] List all themes
21
+ info|si [store] Show theme environment
22
+ dev|sd [store] [theme] Develop with Theme Editor sync
23
+ pull|sp [store] [theme] [-p] Pull theme, preserving unmatched local files
24
+ pull-json|spl [store] [theme] Pull templates/config JSON only
25
+ pull-all|spa [store] [theme] Pull without --nodelete
26
+ check|sc Run Theme Check
27
+ fix|sf Auto-correct Theme Check findings
28
+ logout|lo Log out of Shopify
29
+
30
+ Options: --store/-s STORE, --theme/-t THEME, --dry-run
31
+ Dev options: --port/-p PORT (example: sd demo -p 9293)
32
+ List/info options: --json/-j (example: sl demo -j)
33
+ Pull options: --live, --development/-d, --json-only/-p
34
+ Info options: --development/-d
35
+ Config writes: --global/-g (init, stores add/remove, import)
36
+ Additional Shopify flags go after -- (example: -- --port 9293).
37
+ Select the current store with si STORE; then omit STORE on later theme commands.
38
+ A configured defaultStore overrides the remembered Shopify store. Use si to check.
39
+ Use explicit --store for an unmapped store prefix with no theme.
40
+ Pull defaults to the live theme. Dev without a theme uses a development theme.
41
+ Config order: ~/.sshop.json, nearest .sshop.json; --config or SSHOP_CONFIG isolates.
42
+ Direct commands: sd, sp, sl, si, spl, spa, sc, sf, lo.
43
+ Setup commands: sinit, sstores, sconfig, simport, shelp.
44
+ `;
45
+
46
+ export async function main(argv) {
47
+ const args = [...argv];
48
+ let explicit = process.env.SSHOP_CONFIG;
49
+ if (args[0] === '--config') {
50
+ explicit = args[1];
51
+ if (!explicit || explicit.startsWith('-')) throw new Error('Provide a file after --config.');
52
+ args.splice(0, 2);
53
+ }
54
+ const command = args.shift();
55
+ if (!command || ['--help', '-h', 'help'].includes(command)) { console.log(help); return 0; }
56
+ if (command === '--version') {
57
+ console.log(JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version); return 0;
58
+ }
59
+ if (command === 'shell') {
60
+ if (args.length > 1 || (args.length === 1 && args[0] !== 'powershell')) throw new Error('Usage: sshop shell [powershell]');
61
+ console.log(fs.readFileSync(new URL(args[0] === 'powershell' ? '../shell/shortcuts.ps1' : '../shell/shortcuts.sh', import.meta.url), 'utf8')); return 0;
62
+ }
63
+ const mutation = command === 'init' || command === 'import' || (command === 'stores' && ['add', 'remove'].includes(args[0]));
64
+ if (mutation) {
65
+ const normalized = args.map(arg => arg === '-g' ? '--global' : arg);
66
+ const global = normalized.includes('--global');
67
+ if (global && explicit) throw new Error('Choose --global or --config, not both.');
68
+ if (normalized.filter(arg => arg === '--global').length > 1) throw new Error('Repeated --global option.');
69
+ const values = normalized.filter(arg => arg !== '--global');
70
+ const file = explicit ? path.resolve(explicit) : path.join(global ? os.homedir() : process.cwd(), filename);
71
+ if (command === 'init') {
72
+ if (values.length) throw new Error('Usage: sshop init [--global]');
73
+ writeConfig(file, { version: 1, stores: {}, defaults: { nodelete: true, themeEditorSync: true } }, { create: true });
74
+ } else {
75
+ const config = fs.existsSync(file) ? readConfig(file) : { version: 1, stores: {} };
76
+ if (command === 'import') {
77
+ if (values.length !== 1) throw new Error('Usage: sshop import FILE [--global]');
78
+ const stores = importAliases(fs.readFileSync(values[0], 'utf8'));
79
+ for (const [alias, store] of Object.entries(stores)) {
80
+ if (Object.hasOwn(config.stores, alias) && normalizeStore(config.stores[alias]) !== store) throw new Error(`Import would overwrite alias ${alias}. Remove or update it explicitly first.`);
81
+ }
82
+ config.stores = { ...config.stores, ...stores };
83
+ console.log(`Imported ${Object.keys(stores).length} unique store aliases.`);
84
+ } else if (values[0] === 'add') {
85
+ if (values.length !== 3) throw new Error('Usage: sshop stores add ALIAS STORE [--global]');
86
+ validateAlias(values[1]); config.stores[values[1]] = normalizeStore(values[2]);
87
+ } else {
88
+ if (values.length !== 2) throw new Error('Usage: sshop stores remove ALIAS [--global]');
89
+ if (!Object.hasOwn(config.stores, values[1])) throw new Error(`Alias ${values[1]} does not exist in ${file}.`);
90
+ delete config.stores[values[1]];
91
+ if (config.defaultStore === values[1]) config.defaultStore = null;
92
+ }
93
+ writeConfig(file, config, { create: !fs.existsSync(file) });
94
+ }
95
+ console.log(`Saved ${file}`); return 0;
96
+ }
97
+ const loaded = loadConfig({ explicit });
98
+ if (command === 'config') {
99
+ if (args.length) throw new Error('Usage: sshop config');
100
+ console.log(JSON.stringify(loaded, null, 2)); return 0;
101
+ }
102
+ if (command === 'stores') {
103
+ if (args.length && !(args.length === 1 && args[0] === 'list')) throw new Error('Usage: sshop stores [list|add|remove]');
104
+ const entries = Object.entries(loaded.config.stores).sort(([a], [b]) => a.localeCompare(b));
105
+ console.log(entries.length ? entries.map(([alias, store]) => `${alias}\t${normalizeStore(store)}`).join('\n') : 'No store aliases yet. Run: sshop stores add demo example-store');
106
+ return 0;
107
+ }
108
+ const built = buildCommand(command, args, loaded.config);
109
+ if (built.dryRun) { console.log(formatCommand(built.args)); return 0; }
110
+ return runShopify(built.args);
111
+ }
112
+
113
+ export async function runShopify(args, { platform = process.platform } = {}) {
114
+ // Use argument arrays, never eval or a shell command string.
115
+ // Windows npm exposes shopify.cmd; invoke the package's JS entry with Node instead.
116
+ let executable = 'shopify';
117
+ if (platform === 'win32') {
118
+ const directories = (process.env.PATH || '').split(path.delimiter);
119
+ let entry;
120
+ for (const directory of directories) {
121
+ const root = path.join(directory, 'node_modules', '@shopify', 'cli');
122
+ const manifest = path.join(root, 'package.json');
123
+ if (!fs.existsSync(manifest)) continue;
124
+ const pkg = JSON.parse(fs.readFileSync(manifest, 'utf8'));
125
+ const bin = typeof pkg.bin === 'string' ? pkg.bin : pkg.bin?.shopify;
126
+ if (bin && fs.existsSync(path.join(root, bin))) { entry = path.join(root, bin); break; }
127
+ }
128
+ if (!entry) throw new Error('Shopify CLI not found. Install it globally with npm install -g @shopify/cli.');
129
+ executable = process.execPath;
130
+ args = [entry, ...args];
131
+ }
132
+ return new Promise((resolve, reject) => {
133
+ const child = spawn(executable, args, { stdio: 'inherit', shell: false });
134
+ const forwardInterrupt = () => { if (!child.killed) child.kill('SIGINT'); };
135
+ const forwardTerminate = () => { if (!child.killed) child.kill('SIGTERM'); };
136
+ process.on('SIGINT', forwardInterrupt);
137
+ process.on('SIGTERM', forwardTerminate);
138
+ const cleanup = () => { process.off('SIGINT', forwardInterrupt); process.off('SIGTERM', forwardTerminate); };
139
+ child.once('error', error => {
140
+ cleanup(); reject(new Error(error.code === 'ENOENT' ? 'Shopify CLI not found. Install it with npm install -g @shopify/cli.' : error.message));
141
+ });
142
+ child.once('exit', (code, signal) => { cleanup(); resolve(code ?? (signal === 'SIGINT' ? 130 : 143)); });
143
+ });
144
+ }
@@ -0,0 +1,101 @@
1
+ import { normalizeStore } from './config.js';
2
+
3
+ const names = { sl: 'list', si: 'info', sp: 'pull', sd: 'dev', spl: 'pull-json', spa: 'pull-all', sc: 'check', sf: 'fix', lo: 'logout' };
4
+ export function buildCommand(name, input, config) {
5
+ const command = names[name] || name;
6
+ if (!['list', 'info', 'pull', 'dev', 'pull-json', 'pull-all', 'check', 'fix', 'logout'].includes(command)) throw new Error(`Unknown command: ${name}. Run sshop --help.`);
7
+ const separator = input.indexOf('--');
8
+ const args = separator < 0 ? input : input.slice(0, separator);
9
+ const extra = separator < 0 ? [] : input.slice(separator + 1);
10
+ const positionals = [];
11
+ let store, theme, selector, json = command === 'pull-json';
12
+ let dryRun = false, port, jsonOutput = false;
13
+ for (let i = 0; i < args.length; i++) {
14
+ const arg = args[i];
15
+ if (arg === '--dry-run') { dryRun = true; continue; }
16
+ if (arg === '-j' || arg === '--json') {
17
+ if (!['list', 'info'].includes(command)) throw new Error('--json/-j applies only to list and info. For JSON-only pulls, use -p.');
18
+ if (jsonOutput) throw new Error('JSON output specified more than once.');
19
+ jsonOutput = true;
20
+ continue;
21
+ }
22
+ if (arg === '--port' || (arg === '-p' && command === 'dev')) {
23
+ if (command !== 'dev') throw new Error('--port applies only to dev.');
24
+ if (port !== undefined) throw new Error('Port specified more than once.');
25
+ port = args[++i];
26
+ if (!port || !/^\d+$/.test(port) || Number(port) < 1 || Number(port) > 65535) throw new Error('Port must be a number from 1 to 65535. Example: sd demo -p 9293');
27
+ continue;
28
+ }
29
+ if (arg === '-p' || arg === '--json-only') { json = true; continue; }
30
+ if (arg === '--live' || arg === '--development' || arg === '-d') {
31
+ if (selector) throw new Error('Choose only one of --live or --development.');
32
+ selector = arg === '-d' ? '--development' : arg; continue;
33
+ }
34
+ if (['--store', '-s', '--theme', '-t'].includes(arg)) {
35
+ const value = args[++i];
36
+ if (!value || value.startsWith('-')) throw new Error(`Missing value for ${arg}.`);
37
+ if (arg === '--store' || arg === '-s') {
38
+ if (store !== undefined) throw new Error('Store specified more than once.');
39
+ store = value;
40
+ } else {
41
+ if (theme !== undefined) throw new Error('Theme specified more than once.');
42
+ theme = value;
43
+ }
44
+ continue;
45
+ }
46
+ if (arg.startsWith('-')) throw new Error(`Unknown option: ${arg}. Put additional Shopify flags after --.`);
47
+ positionals.push(arg);
48
+ }
49
+ const pull = ['pull', 'pull-json', 'pull-all'].includes(command);
50
+ const thematic = pull || command === 'dev';
51
+ if (command === 'list' || command === 'info') {
52
+ if (positionals.length > 1 || (store && positionals.length)) throw new Error('Provide a single store.');
53
+ store ??= positionals.shift();
54
+ } else if (thematic) {
55
+ if (!store && positionals.length && (positionals.length > 1 || Object.hasOwn(config.stores, positionals[0]) || /[./]/.test(positionals[0]))) store = positionals.shift();
56
+ if (positionals.length > 1 || (theme && positionals.length)) throw new Error('Use [store] [theme], or explicit --store and --theme.');
57
+ theme ??= positionals.shift();
58
+ } else if (positionals.length || store || theme || selector || json) {
59
+ throw new Error(`${command} does not accept store, theme, or pull options.`);
60
+ }
61
+ if (json && !pull) throw new Error('--json-only applies only to pull.');
62
+ if (selector && !pull && !(command === 'info' && selector === '--development')) throw new Error('--live applies only to pull; --development/-d applies to pull and info.');
63
+ if (theme && selector) throw new Error('Use either --theme or a theme selector, not both.');
64
+ if (theme && !thematic && command !== 'info') throw new Error(`${command} does not accept a theme.`);
65
+ store ??= config.defaultStore;
66
+ const output = command === 'logout' ? ['auth', 'logout'] : ['theme', command === 'fix' ? 'check' : pull ? 'pull' : command];
67
+ if (store && (thematic || ['list', 'info'].includes(command))) output.push('--store', normalizeStore(Object.hasOwn(config.stores, store) ? config.stores[store] : store));
68
+ if (theme) output.push('--theme', theme);
69
+ if (pull) {
70
+ if (!theme) output.push(selector || '--live');
71
+ if (command !== 'pull-all' && config.defaults.nodelete !== false) output.push('--nodelete');
72
+ if (json) output.push('--only', 'templates/*.json', '--only', 'config/*.json');
73
+ }
74
+ if (command === 'info' && selector) output.push(selector);
75
+ if (command === 'dev' && config.defaults.themeEditorSync !== false) output.push('--theme-editor-sync');
76
+ if (port !== undefined) {
77
+ if (extra.some(arg => arg === '--port' || arg.startsWith('--port='))) throw new Error('Port specified more than once.');
78
+ output.push('--port', port);
79
+ }
80
+ if (command === 'fix') output.push('--auto-correct');
81
+ if (jsonOutput) {
82
+ if (extra.some(arg => arg === '-j' || arg === '--json' || arg.startsWith('--json='))) throw new Error('JSON output specified more than once.');
83
+ output.push('--json');
84
+ }
85
+ // Prevent ambiguous duplicate targeting and accidental reinterpretation of wrapper defaults.
86
+ const reserved = /^(?:--(?:store|theme|live|development|nodelete|theme-editor-sync)|-[stldn])(?:=|$)/;
87
+ if (extra.some(arg => reserved.test(arg) || /^-[st].+/.test(arg))) throw new Error('Pass store/theme/selector options before --; set nodelete and themeEditorSync in config.');
88
+ return { args: [...output, ...extra], dryRun };
89
+ }
90
+
91
+ export function formatCommand(args) {
92
+ const redacted = [];
93
+ let secret = false;
94
+ for (const arg of args) {
95
+ if (secret) { redacted.push('[redacted]'); secret = false; continue; }
96
+ if (/^--(?:password|store-password)=/.test(arg)) { redacted.push(`${arg.split('=')[0]}=[redacted]`); continue; }
97
+ redacted.push(arg);
98
+ secret = ['--password', '--store-password'].includes(arg);
99
+ }
100
+ return ['shopify', ...redacted].map(arg => /^[a-zA-Z0-9_./:-]+$/.test(arg) ? arg : `'${arg.replaceAll("'", "'\\''")}'`).join(' ');
101
+ }
package/src/config.js ADDED
@@ -0,0 +1,90 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { randomUUID } from 'node:crypto';
5
+
6
+ export const filename = '.sshop.json';
7
+ const own = (object, key) => Object.hasOwn(object, key);
8
+ export function normalizeStore(value) {
9
+ if (typeof value !== 'string') throw new Error('Store must be a string.');
10
+ const slug = value.trim().toLowerCase().replace(/^https?:\/\//, '').replace(/\/$/, '').replace(/\.myshopify\.com$/, '');
11
+ if (!/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(slug) || slug.length > 63) {
12
+ throw new Error(`Invalid Shopify store: ${value}. Use a store prefix or a myshopify.com domain.`);
13
+ }
14
+ return `${slug}.myshopify.com`;
15
+ }
16
+ export function validateAlias(alias) {
17
+ if (!/^[a-z][a-z0-9_-]*$/.test(alias) || ['__proto__', 'prototype', 'constructor'].includes(alias)) {
18
+ throw new Error('Alias must start with a lowercase letter and contain only lowercase letters, digits, _ or -.');
19
+ }
20
+ }
21
+ export function validateConfig(config) {
22
+ if (!config || typeof config !== 'object' || Array.isArray(config)) throw new Error('Config must be a JSON object.');
23
+ for (const key of Object.keys(config)) {
24
+ if (!['version', 'stores', 'defaultStore', 'defaults'].includes(key)) throw new Error(`Unknown config key: ${key}`);
25
+ }
26
+ if (config.version !== 1) throw new Error('Config version must be 1.');
27
+ if (!config.stores || typeof config.stores !== 'object' || Array.isArray(config.stores)) throw new Error('Config stores must be an object.');
28
+ for (const [alias, store] of Object.entries(config.stores)) { validateAlias(alias); normalizeStore(store); }
29
+ if (config.defaultStore !== undefined && config.defaultStore !== null) {
30
+ if (typeof config.defaultStore !== 'string' || !config.defaultStore.trim()) throw new Error('defaultStore must be a store alias, domain, or null.');
31
+ if (!own(config.stores, config.defaultStore)) normalizeStore(config.defaultStore);
32
+ }
33
+ if (config.defaults !== undefined) {
34
+ if (!config.defaults || typeof config.defaults !== 'object' || Array.isArray(config.defaults)) throw new Error('defaults must be an object.');
35
+ for (const [key, value] of Object.entries(config.defaults)) {
36
+ if (!['nodelete', 'themeEditorSync'].includes(key) || typeof value !== 'boolean') throw new Error(`Invalid default: ${key}. Supported boolean defaults: nodelete, themeEditorSync.`);
37
+ }
38
+ }
39
+ return config;
40
+ }
41
+ export function readConfig(file) {
42
+ try { return validateConfig(JSON.parse(fs.readFileSync(file, 'utf8'))); }
43
+ catch (error) { throw new Error(`${file}: ${error.message}`); }
44
+ }
45
+ export function loadConfig({ cwd = process.cwd(), home = os.homedir(), explicit = process.env.SSHOP_CONFIG } = {}) {
46
+ const globalFile = path.join(home, filename);
47
+ let files = [];
48
+ if (explicit) {
49
+ files = [path.resolve(cwd, explicit)];
50
+ } else {
51
+ if (fs.existsSync(globalFile)) files.push(globalFile);
52
+ let directory = path.resolve(cwd);
53
+ while (true) {
54
+ const candidate = path.join(directory, filename);
55
+ if (fs.existsSync(candidate)) { if (!files.includes(candidate)) files.push(candidate); break; }
56
+ const parent = path.dirname(directory);
57
+ if (parent === directory) break;
58
+ directory = parent;
59
+ }
60
+ }
61
+ let config = { version: 1, stores: {}, defaults: { nodelete: true, themeEditorSync: true } };
62
+ for (const file of files) {
63
+ const next = readConfig(file);
64
+ config = { ...config, ...next, stores: { ...config.stores, ...next.stores }, defaults: { ...config.defaults, ...next.defaults } };
65
+ }
66
+ return { config, files };
67
+ }
68
+ export function writeConfig(file, config, { create = false } = {}) {
69
+ validateConfig(config);
70
+ const data = `${JSON.stringify(config, null, 2)}\n`;
71
+ if (create) { fs.writeFileSync(file, data, { flag: 'wx', mode: 0o600 }); return; }
72
+ const temporary = `${file}.${randomUUID()}.tmp`;
73
+ try { fs.writeFileSync(temporary, data, { flag: 'wx', mode: 0o600 }); fs.renameSync(temporary, file); }
74
+ finally { if (fs.existsSync(temporary)) fs.unlinkSync(temporary); }
75
+ }
76
+ // Parse only literal map_store case entries. Never source or execute the legacy file.
77
+ export function importAliases(source) {
78
+ const block = source.match(/map_store\(\)\s*\{([\s\S]*?)\n\}/)?.[1];
79
+ if (!block) throw new Error('No map_store() block found.');
80
+ const stores = {};
81
+ for (const match of block.matchAll(/^\s*([a-z][a-z0-9_-]*)\)\s*printf\s+'%s'\s+'([^']+)'\s*;;\s*$/gm)) {
82
+ const [, alias, store] = match;
83
+ validateAlias(alias);
84
+ const normalized = normalizeStore(store);
85
+ if (own(stores, alias) && stores[alias] !== normalized) throw new Error(`Conflicting legacy alias: ${alias}`);
86
+ stores[alias] = normalized;
87
+ }
88
+ if (!Object.keys(stores).length) throw new Error('No supported literal store mappings found.');
89
+ return stores;
90
+ }
package/src/entry.js ADDED
@@ -0,0 +1,13 @@
1
+ import { main } from './cli.js';
2
+
3
+ export async function runEntry(command, input = process.argv.slice(2)) {
4
+ const args = [...input];
5
+ const global = [];
6
+ if (args[0] === '--config') global.push(...args.splice(0, 2));
7
+ try {
8
+ process.exitCode = await main([...global, ...(['--help', '-h', '--version'].includes(args[0]) ? args : [command, ...args])]);
9
+ } catch (error) {
10
+ console.error(`${command}: ${error.message}`);
11
+ process.exitCode = 1;
12
+ }
13
+ }