shopify-store-sync-mcp 0.0.0-stage → 0.3.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 +21 -0
- package/README.md +414 -3
- package/dist/apply.js +185 -0
- package/dist/apply.js.map +1 -0
- package/dist/cli.js +256 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.js +107 -0
- package/dist/config.js.map +1 -0
- package/dist/diff.js +101 -0
- package/dist/diff.js.map +1 -0
- package/dist/engine.js +126 -0
- package/dist/engine.js.map +1 -0
- package/dist/index.js +33 -0
- package/dist/index.js.map +1 -0
- package/dist/plan.js +162 -0
- package/dist/plan.js.map +1 -0
- package/dist/refs.js +330 -0
- package/dist/refs.js.map +1 -0
- package/dist/resources/collections.js +109 -0
- package/dist/resources/collections.js.map +1 -0
- package/dist/resources/index.js +29 -0
- package/dist/resources/index.js.map +1 -0
- package/dist/resources/menus.js +81 -0
- package/dist/resources/menus.js.map +1 -0
- package/dist/resources/metafieldDefinitions.js +143 -0
- package/dist/resources/metafieldDefinitions.js.map +1 -0
- package/dist/resources/metafields.js +85 -0
- package/dist/resources/metafields.js.map +1 -0
- package/dist/resources/metaobjectDefinitions.js +153 -0
- package/dist/resources/metaobjectDefinitions.js.map +1 -0
- package/dist/resources/metaobjects.js +91 -0
- package/dist/resources/metaobjects.js.map +1 -0
- package/dist/resources/pages.js +48 -0
- package/dist/resources/pages.js.map +1 -0
- package/dist/resources/redirects.js +28 -0
- package/dist/resources/redirects.js.map +1 -0
- package/dist/resources/shopPolicies.js +30 -0
- package/dist/resources/shopPolicies.js.map +1 -0
- package/dist/resources/translations.js +86 -0
- package/dist/resources/translations.js.map +1 -0
- package/dist/resources/types.js +16 -0
- package/dist/resources/types.js.map +1 -0
- package/dist/resources/util.js +23 -0
- package/dist/resources/util.js.map +1 -0
- package/dist/server.js +269 -0
- package/dist/server.js.map +1 -0
- package/dist/shopify/cliClient.js +185 -0
- package/dist/shopify/cliClient.js.map +1 -0
- package/dist/shopify/client.js +123 -0
- package/dist/shopify/client.js.map +1 -0
- package/dist/snapshot.js +29 -0
- package/dist/snapshot.js.map +1 -0
- package/dist/snapshotFiles.js +100 -0
- package/dist/snapshotFiles.js.map +1 -0
- package/dist/themes.js +88 -0
- package/dist/themes.js.map +1 -0
- package/package.json +66 -6
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Youssef Idder
|
|
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
CHANGED
|
@@ -1,3 +1,414 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="docs/images/hero.png" alt="shopify-store-sync-mcp: diff and sync configuration between Shopify stores from your AI assistant" width="100%">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<p align="center">
|
|
6
|
+
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-57c26e" alt="MIT license"></a>
|
|
7
|
+
<img src="https://img.shields.io/badge/node-%3E%3D20-6cb6ff" alt="Node 20+">
|
|
8
|
+
<img src="https://img.shields.io/badge/MCP-server-a371f7" alt="MCP server">
|
|
9
|
+
<img src="https://img.shields.io/badge/Shopify%20Admin%20API-2026--10-e3b341" alt="Shopify Admin API 2026-10">
|
|
10
|
+
</p>
|
|
11
|
+
|
|
12
|
+
# shopify-store-sync-mcp
|
|
13
|
+
|
|
14
|
+
**Keep your Shopify stores in sync by asking for it.** Compare a dev store with production, see every difference field by field, review a dry-run plan, then apply it, from Claude, Cursor or any MCP client.
|
|
15
|
+
|
|
16
|
+
It copies the store setup that usually gets rebuilt by hand: **metafield and metaobject definitions, metaobject entries, collections, pages, menus, redirects, store policies, metafield values and translations**, with the images they use. References between them (a size guide pointing at a product, a menu linking to a page) are carried across by handle, so they land on the right item in the target store instead of a broken ID.
|
|
17
|
+
|
|
18
|
+
<table>
|
|
19
|
+
<tr>
|
|
20
|
+
<td width="33%" valign="top"><b>See it before it happens</b><br>Field-level diffs and a dry-run plan for every sync. Nothing is written until you apply a reviewed plan.</td>
|
|
21
|
+
<td width="33%" valign="top"><b>Safe on production</b><br>Writes are off by default, protected stores need typed confirmation, drifted items are skipped, and nothing is ever deleted.</td>
|
|
22
|
+
<td width="33%" valign="top"><b>References that survive</b><br>Product, collection, page, metaobject and definition IDs are remapped by handle between stores.</td>
|
|
23
|
+
</tr>
|
|
24
|
+
</table>
|
|
25
|
+
|
|
26
|
+
> **What you need:** Node 20+ and access to the stores' admin. The quickest way to connect needs no app at all: run `shopify-store-sync-mcp connect my-store`, approve in the browser, done (it uses [Shopify CLI](https://shopify.dev/docs/api/shopify-cli)'s own sign-in). For scheduled or unattended runs, a private app works too. [Setup below](#1-connect-your-stores).
|
|
27
|
+
|
|
28
|
+
<p align="center">
|
|
29
|
+
<img src="docs/images/demo.png" alt="diff_stores, plan_sync and apply_plan output: 6 created, 3 updated, 1 conflict skipped, protected store refused without confirmation" width="100%">
|
|
30
|
+
</p>
|
|
31
|
+
|
|
32
|
+
<sub>Real output of the server's tools on two sample stores. Regenerate with <code>npm run docs:demo && npm run docs:images</code>.</sub>
|
|
33
|
+
|
|
34
|
+
## Use cases
|
|
35
|
+
|
|
36
|
+
- **Dev → staging → production.** Build a new metaobject type and its entries on a dev store, check the diff against production, ship it with one reviewed plan.
|
|
37
|
+
- **Multi-region brands.** Keep the EU, UK and US stores on the same definitions, menus and redirects without exporting CSVs.
|
|
38
|
+
- **New market launch.** Seed a new store with the setup of an existing one, then see exactly what still differs.
|
|
39
|
+
- **Drift audits.** Ask which stores have fallen out of line, without changing anything (every tool except `apply_plan` is read-only).
|
|
40
|
+
|
|
41
|
+
## Why this exists
|
|
42
|
+
|
|
43
|
+
Most Shopify Plus brands run more than one store: a dev store, a staging store, one per region or currency. Anyone who has kept them in step knows the routine: rebuild metaobject definitions by hand, copy FAQ entries one by one, re-link every menu item, export and import redirects through CSV. It's slow, and it breaks in quiet ways, because **every ID is different in every store**.
|
|
44
|
+
|
|
45
|
+
There are plenty of Shopify Admin MCP servers that read and write a *single* store. This one is built for the job *between* stores:
|
|
46
|
+
|
|
47
|
+
| | Generic Admin MCP servers | shopify-store-sync-mcp |
|
|
48
|
+
|---|---|---|
|
|
49
|
+
| Compare two stores | ❌ | ✅ field-level diff |
|
|
50
|
+
| Cross-store ID remapping | ❌ you copy raw IDs | ✅ products, collections, pages, blogs, metaobjects, definitions, customer account pages and shop policies |
|
|
51
|
+
| Dry run before writing | ❌ | ✅ every write goes through a reviewed plan |
|
|
52
|
+
| Detects changes made after planning | ❌ | ✅ re-reads the target and skips anything that drifted |
|
|
53
|
+
| Deletes | sometimes | **never** |
|
|
54
|
+
| Production guardrails | ❌ | ✅ writes are off by default; protected stores need typed confirmation |
|
|
55
|
+
| Audit trail | ❌ | ✅ JSONL log of every write |
|
|
56
|
+
|
|
57
|
+
## What it syncs
|
|
58
|
+
|
|
59
|
+
| Resource | Keyed by | Notes |
|
|
60
|
+
|---|---|---|
|
|
61
|
+
| `metafield_definitions` | `OWNER_TYPE.namespace.key` | Name, description, pinning, storefront and customer account access, capabilities, validations. A type change is reported as a conflict, since Shopify can't change a definition's type in place. |
|
|
62
|
+
| `metaobject_definitions` | `type` | Schema, access, capabilities. Fields are added or updated, never removed. Self-referencing definitions are handled. |
|
|
63
|
+
| `pages` | `handle` | Title, body HTML, template, published state. |
|
|
64
|
+
| `metaobjects` | `type/handle` | All field values, publish status. References to products, collections, pages and other metaobjects are remapped. |
|
|
65
|
+
| `menus` | `handle` | Full item tree (3 levels), including customer account menus. Links to resources are remapped; custom URLs are kept; links to things that can't be matched are left out and reported. |
|
|
66
|
+
| `redirects` | `path` | Thousands are fine. |
|
|
67
|
+
| `collections` | `handle` | Title, description, sort order, template, SEO and smart-collection rules (rules on a metafield definition are remapped). Manual collections keep their own product list; sales-channel publishing is not copied. Manual versus smart is reported as a conflict. |
|
|
68
|
+
| `shop_policies` | `type` | Refund, privacy, terms, shipping and contact policy text. |
|
|
69
|
+
| `metafields` *(opt-in)* | `OWNER/handle/namespace.key` | Metafield **values** on products and collections, landed on the item with the same handle. Reference values are remapped. Filter with `namespaces`. |
|
|
70
|
+
| `translations` *(opt-in)* | `locale:Type/handle` | Translations of pages, collections, metaobjects, menus and policies. Registered against the target's own text, after the content itself is synced. The plan warns when a language isn't added to the target. |
|
|
71
|
+
|
|
72
|
+
`metafields` and `translations` can be large, so they're read only when you ask for them (`resources: ["metafields"]`). App-owned (`app--…`) and Shopify-reserved (`shopify--…`) data is skipped and listed in the report.
|
|
73
|
+
|
|
74
|
+
**Files and images** referenced by any of the above are matched by file name in the target. When one is missing, the plan says so and `apply_plan` uploads it from the source store.
|
|
75
|
+
|
|
76
|
+
## Tools
|
|
77
|
+
|
|
78
|
+
| Tool | Writes? | What it does |
|
|
79
|
+
|---|---|---|
|
|
80
|
+
| `list_stores` | no | Configured stores and their write permissions. Secrets are never returned. |
|
|
81
|
+
| `list_resource_types` | no | What can be synced and how it's keyed. |
|
|
82
|
+
| `snapshot_store` | no | Read one store's configuration in portable form. |
|
|
83
|
+
| `diff_stores` | no | Only-in-source, changed (field by field), only-in-target. |
|
|
84
|
+
| `plan_sync` | no | Dry-run plan: creates, updates, conflicts, missing references. Returns a `plan_id`. |
|
|
85
|
+
| `get_plan` | no | Show a plan, or list recent ones. |
|
|
86
|
+
| `apply_plan` | **yes** | Execute a plan, with every safety check below. |
|
|
87
|
+
| `export_snapshot` | local files only | Write a store's configuration to a folder for git ([config as code](#config-as-code)). |
|
|
88
|
+
| `diff_themes` | no | Compare theme templates and settings between two stores ([theme settings](#theme-settings-diff)). |
|
|
89
|
+
|
|
90
|
+
Every tool except `apply_plan` is read-only toward Shopify and annotated as such, so clients can auto-approve them.
|
|
91
|
+
|
|
92
|
+
What `diff_stores` reports, field by field (real output):
|
|
93
|
+
|
|
94
|
+
<p align="center">
|
|
95
|
+
<img src="docs/images/field-diff.png" alt="Field-level diff: page body, menu items, definition name, and a type conflict reported but not forced" width="100%">
|
|
96
|
+
</p>
|
|
97
|
+
|
|
98
|
+
## Quick start
|
|
99
|
+
|
|
100
|
+
### 1. Connect your stores
|
|
101
|
+
|
|
102
|
+
This server is a program you run yourself, so it can't borrow the Shopify login of your AI client. Pick one of three ways to give it access.
|
|
103
|
+
|
|
104
|
+
**Option 1: sign in with Shopify CLI (fastest, no app to create).** Works on any store where your Shopify account can approve app access, including client stores you collaborate on.
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
npm install -g @shopify/cli
|
|
108
|
+
npx shopify-store-sync-mcp connect my-dev-store # read-only
|
|
109
|
+
npx shopify-store-sync-mcp connect my-staging-store --writes
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Each `connect` opens Shopify in your browser; click **Install** to approve. It asks for read-only scopes unless you pass `--writes` (or the store is configured with `"allowWrites": true`), then prints the line to add to `stores.json`:
|
|
113
|
+
|
|
114
|
+
```json
|
|
115
|
+
{ "stores": { "my-dev-store": { "domain": "my-dev-store.myshopify.com", "auth": "shopify-cli" } } }
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Requests then run through `shopify store execute`, Shopify CLI's supported command, using the sign-in it saved. Each request starts Shopify CLI (about a second), so a full diff takes tens of seconds instead of one or two with an app. That sign-in belongs to you and expires after a while; when it does, the tool says so and you run `connect` again. Because of that, use one of the options below for scheduled or unattended runs.
|
|
119
|
+
|
|
120
|
+
**Option 2: a Dev Dashboard app (best for automation on your own or your organization's stores).** One app covers every store in the same Shopify organization, and tokens are fetched and refreshed for you.
|
|
121
|
+
|
|
122
|
+
1. Open the [Dev Dashboard](https://dev.shopify.com/dashboard), go to **Apps**, click **Create app**, then **Create app manually**.
|
|
123
|
+
2. Give it a name, for example `Store Sync`. Any URL works as the App URL (it's never opened); untick **Embed app in Shopify admin**.
|
|
124
|
+
3. In **Scopes**, paste the scopes from the table below for what you'll sync, then click **Create app** and **Release**.
|
|
125
|
+
4. Install it on each store: open `https://admin.shopify.com/store/<store-handle>/oauth/install?client_id=<your client ID>` and click **Install**.
|
|
126
|
+
5. Under the app's **Settings**, copy the **Client ID** and **Client secret** into environment variables (step 2 reads them from there).
|
|
127
|
+
|
|
128
|
+
Adding a scope later means releasing a new version and approving the update on each store (same install link).
|
|
129
|
+
|
|
130
|
+
**Option 3: an access token from a custom app (a store owned by someone else, for automation).** Client credentials only work on stores in the app's own organization; elsewhere Shopify answers `shop_not_permitted`. For unattended runs on such a store, ask the owner to create a custom app in their admin (**Settings > Apps > Develop apps**), grant the scopes below, install it and share the Admin API access token (`shpat_...`). Use it as `accessToken` in step 2.
|
|
131
|
+
|
|
132
|
+
| Resource | Read scope | Write scope |
|
|
133
|
+
|---|---|---|
|
|
134
|
+
| metafield definitions | `read_products` (and the owner type's read scope) | `write_products` (and the owner type's write scope) |
|
|
135
|
+
| metaobjects and definitions | `read_metaobjects`, `read_metaobject_definitions` | `write_metaobjects`, `write_metaobject_definitions` |
|
|
136
|
+
| pages | `read_online_store_pages` | `write_online_store_pages` |
|
|
137
|
+
| menus | `read_online_store_navigation` | `write_online_store_navigation` |
|
|
138
|
+
| redirects | `read_online_store_navigation` | `write_online_store_navigation` |
|
|
139
|
+
| collections | `read_products` | `write_products` |
|
|
140
|
+
| shop policies | `read_legal_policies` | `write_legal_policies` |
|
|
141
|
+
| metafield values | `read_products` | `write_products` |
|
|
142
|
+
| translations | `read_translations`, `read_locales` | `write_translations` |
|
|
143
|
+
| files | `read_files` | `write_files` (to upload missing files) |
|
|
144
|
+
| theme diff | `read_themes` | none, never writes |
|
|
145
|
+
| ID remapping | `read_products`, `read_content` | none |
|
|
146
|
+
|
|
147
|
+
Everything at once (the set used for the live tests), ready to paste:
|
|
148
|
+
|
|
149
|
+
```text
|
|
150
|
+
read_products,write_products,read_metaobjects,write_metaobjects,read_metaobject_definitions,write_metaobject_definitions,read_online_store_pages,write_online_store_pages,read_online_store_navigation,write_online_store_navigation,read_legal_policies,write_legal_policies,read_translations,write_translations,read_locales,read_files,write_files,read_themes,read_content
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Give source stores read-only scopes when you can.
|
|
154
|
+
|
|
155
|
+
### 2. Describe your stores
|
|
156
|
+
|
|
157
|
+
Create `stores.json`. Secrets can come from environment variables with `env:NAME`:
|
|
158
|
+
|
|
159
|
+
```json
|
|
160
|
+
{
|
|
161
|
+
"stores": {
|
|
162
|
+
"dev": {
|
|
163
|
+
"domain": "my-brand-dev.myshopify.com",
|
|
164
|
+
"accessToken": "env:DEV_TOKEN",
|
|
165
|
+
"label": "Development"
|
|
166
|
+
},
|
|
167
|
+
"eu": {
|
|
168
|
+
"domain": "my-brand-eu.myshopify.com",
|
|
169
|
+
"clientId": "env:EU_CLIENT_ID",
|
|
170
|
+
"clientSecret": "env:EU_CLIENT_SECRET",
|
|
171
|
+
"allowWrites": true,
|
|
172
|
+
"protected": true
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
| Option | Default | Meaning |
|
|
179
|
+
|---|---|---|
|
|
180
|
+
| `domain` | required | `*.myshopify.com` domain |
|
|
181
|
+
| `auth` | | `"shopify-cli"`: use the sign-in saved by `connect` (no token needed), **or** |
|
|
182
|
+
| `accessToken` | | Static Admin API token, **or** |
|
|
183
|
+
| `clientId` + `clientSecret` | | Dev Dashboard app; tokens are fetched and refreshed automatically |
|
|
184
|
+
| `apiVersion` | `2026-10` | Admin API version |
|
|
185
|
+
| `allowWrites` | `false` | Must be `true` for `apply_plan` to write to this store |
|
|
186
|
+
| `protected` | `false` | `apply_plan` also requires `confirm: "<domain>"` |
|
|
187
|
+
|
|
188
|
+
You can also pass the whole config as JSON in `SHOPIFY_STORES`, or point `SHOPIFY_STORES_FILE` at a file elsewhere. For a single store, `SHOPIFY_STORE_DOMAIN` plus `SHOPIFY_ACCESS_TOKEN` works.
|
|
189
|
+
|
|
190
|
+
### 3. Add it to your MCP client
|
|
191
|
+
|
|
192
|
+
**Claude Desktop** (`claude_desktop_config.json`):
|
|
193
|
+
|
|
194
|
+
```json
|
|
195
|
+
{
|
|
196
|
+
"mcpServers": {
|
|
197
|
+
"shopify-store-sync": {
|
|
198
|
+
"command": "npx",
|
|
199
|
+
"args": ["-y", "shopify-store-sync-mcp"],
|
|
200
|
+
"env": {
|
|
201
|
+
"SHOPIFY_STORES_FILE": "/absolute/path/to/stores.json",
|
|
202
|
+
"DEV_TOKEN": "shpat_...",
|
|
203
|
+
"EU_CLIENT_ID": "...",
|
|
204
|
+
"EU_CLIENT_SECRET": "..."
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
**Claude Code**
|
|
212
|
+
|
|
213
|
+
```bash
|
|
214
|
+
claude mcp add shopify-store-sync \
|
|
215
|
+
-e SHOPIFY_STORES_FILE=/absolute/path/to/stores.json \
|
|
216
|
+
-e DEV_TOKEN=shpat_... \
|
|
217
|
+
-- npx -y shopify-store-sync-mcp
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
**Cursor** (`.cursor/mcp.json`) takes the same `command`, `args` and `env` as Claude Desktop.
|
|
221
|
+
|
|
222
|
+
### 4. Ask
|
|
223
|
+
|
|
224
|
+
- *"Compare dev and eu. Only metaobjects and menus."*
|
|
225
|
+
- *"Which product metafield definitions in namespace custom are missing from eu?"* (uses `include: ["PRODUCT.custom.*"]`)
|
|
226
|
+
- *"Plan a sync of the faq and size_guide metaobjects from dev to eu."*
|
|
227
|
+
- *"Apply only ops 1 to 4 of that plan."*
|
|
228
|
+
- *"Copy the French translations from dev to eu."* (uses `resources: ["translations"], locales: ["fr"]`)
|
|
229
|
+
- *"Export eu's configuration to ./store-config so I can commit it."*
|
|
230
|
+
- *"Has eu drifted from ./store-config?"*
|
|
231
|
+
- *"What's different in the theme settings between dev and eu?"*
|
|
232
|
+
|
|
233
|
+
## Command line
|
|
234
|
+
|
|
235
|
+
Everything also works without an AI client. Run a command instead of starting the server:
|
|
236
|
+
|
|
237
|
+
```bash
|
|
238
|
+
npx shopify-store-sync-mcp stores
|
|
239
|
+
npx shopify-store-sync-mcp diff dev eu --resources pages,menus
|
|
240
|
+
npx shopify-store-sync-mcp plan dev eu
|
|
241
|
+
npx shopify-store-sync-mcp apply <plan-id> --confirm eu.myshopify.com
|
|
242
|
+
npx shopify-store-sync-mcp export eu ./store-config
|
|
243
|
+
npx shopify-store-sync-mcp themes dev eu
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
`--json` gives machine-readable output and `--exit-code` makes `diff` and `themes` exit with 2 when they find differences, handy for a scheduled drift check. Run `npx shopify-store-sync-mcp help` for every option.
|
|
247
|
+
|
|
248
|
+
<p align="center">
|
|
249
|
+
<img src="docs/images/cli.png" alt="CLI output of diff, plan and themes commands" width="100%">
|
|
250
|
+
</p>
|
|
251
|
+
|
|
252
|
+
## Config as code
|
|
253
|
+
|
|
254
|
+
Export a store's configuration as one small JSON file per item, with sorted keys and no store-specific IDs, then commit it:
|
|
255
|
+
|
|
256
|
+
```bash
|
|
257
|
+
npx shopify-store-sync-mcp export prod ./store-config
|
|
258
|
+
git add store-config && git commit -m "Snapshot prod configuration"
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
Any command or tool that takes a source also takes `dir:<path>`:
|
|
262
|
+
|
|
263
|
+
- **Spot drift:** `diff dir:./store-config prod` shows what changed in production since the commit.
|
|
264
|
+
- **Review changes:** edit the files or re-export from staging, and review the change as a pull request.
|
|
265
|
+
- **Restore or promote:** `plan dir:./store-config prod`, then `apply`, with all the usual safety checks.
|
|
266
|
+
|
|
267
|
+
<p align="center">
|
|
268
|
+
<img src="docs/images/config-as-code.png" alt="Exported store-config folder and one translation file" width="100%">
|
|
269
|
+
</p>
|
|
270
|
+
|
|
271
|
+
## Theme settings diff
|
|
272
|
+
|
|
273
|
+
`diff_themes` (or `themes` on the command line) compares the published themes of two stores: JSON templates, section groups and `config/settings_data.json`, setting by setting. Pass theme ids to compare other themes, and `files` to narrow it down (for example `templates/product*.json`). It is read-only: themes are never written to.
|
|
274
|
+
|
|
275
|
+
## How it works
|
|
276
|
+
|
|
277
|
+
<p align="center">
|
|
278
|
+
<img src="docs/images/how-it-works.png" alt="Snapshot, diff, plan, apply, with safety checks before every write" width="100%">
|
|
279
|
+
</p>
|
|
280
|
+
|
|
281
|
+
## How it stays safe
|
|
282
|
+
|
|
283
|
+
1. **Read-only by default.** `allowWrites` is `false` until you set it, per store.
|
|
284
|
+
2. **Plan, then apply.** `apply_plan` only runs a plan produced by `plan_sync`. Plans expire after an hour.
|
|
285
|
+
3. **Drift protection.** Before writing, the target is re-read. Any item that changed since planning is skipped, not overwritten.
|
|
286
|
+
4. **No deletes.** Items that exist only in the target are listed, never removed. Fields that exist only in the target are left alone.
|
|
287
|
+
5. **Protected stores.** Production needs the exact domain typed as confirmation.
|
|
288
|
+
6. **Conflicts aren't forced.** A metafield definition whose type differs is reported and skipped.
|
|
289
|
+
7. **Audit log.** Every write is appended to `~/.shopify-store-sync/audit.log` (override with `SYNC_AUDIT_LOG`).
|
|
290
|
+
|
|
291
|
+
## How ID remapping works
|
|
292
|
+
|
|
293
|
+
Every Shopify ID is store-specific, so the server never compares or copies raw IDs. When it reads a store, each `gid://shopify/...` value is rewritten into a portable reference:
|
|
294
|
+
|
|
295
|
+
```
|
|
296
|
+
gid://shopify/Product/8123456 → shopify://Product/linen-shirt
|
|
297
|
+
gid://shopify/Metaobject/2918 → shopify://Metaobject/faq/returns
|
|
298
|
+
gid://shopify/MetaobjectDefinition/77 → shopify://MetaobjectDefinition/faq
|
|
299
|
+
gid://shopify/CustomerAccountPage/12 → shopify://CustomerAccountPage/orders
|
|
300
|
+
gid://shopify/ShopPolicy/5 → shopify://ShopPolicy/PRIVACY_POLICY
|
|
301
|
+
gid://shopify/Menu/41 → shopify://Menu/main-menu
|
|
302
|
+
gid://shopify/MetafieldDefinition/9 → shopify://MetafieldDefinition/PRODUCT.custom.fabric
|
|
303
|
+
gid://shopify/MediaImage/730 → shopify://File/size-chart.png
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
This covers JSON list values (`list.product_reference`) and definition validations (`metaobject_definition_id`). When it writes, references are resolved by handle in the target. A reference that doesn't exist there is flagged in the plan before anything is written.
|
|
307
|
+
|
|
308
|
+
Things in the same plan can depend on each other, for example a metaobject pointing at another one being created, or a definition that references itself. These are written in two passes: first without the pending references, then complete.
|
|
309
|
+
|
|
310
|
+
Files (images, generic files and videos) are matched by file name and uploaded to the target when missing. Collection rules that filter on a metafield are remapped to the target's definition.
|
|
311
|
+
|
|
312
|
+
**Not remapped yet:** product variants and articles. Their values are left out of the sync and listed as warnings, never copied as broken IDs.
|
|
313
|
+
|
|
314
|
+
Two references followed from the source store to the target, as the sample sync above wrote them:
|
|
315
|
+
|
|
316
|
+
<p align="center">
|
|
317
|
+
<img src="docs/images/remapping.png" alt="gid://shopify/Product/101 in dev becomes shopify://Product/linen-shirt, then gid://shopify/Product/9101 in eu" width="100%">
|
|
318
|
+
</p>
|
|
319
|
+
|
|
320
|
+
## Tested against live stores
|
|
321
|
+
|
|
322
|
+
Besides the 60 automated tests, every feature has been run end to end between two real Shopify development stores (a Shopify Plus source and a second store as target), through the real command line and the real MCP server over stdio, authenticating with a Dev Dashboard app and the client-credentials grant:
|
|
323
|
+
|
|
324
|
+
| Feature | Live result |
|
|
325
|
+
|---|---|
|
|
326
|
+
| Diff of all default resource types | both stores read in about 10 seconds |
|
|
327
|
+
| Create: metafield and metaobject definitions, metaobjects, smart collection, page, redirect, French translation | applied, second diff empty |
|
|
328
|
+
| File reference on a metaobject | image uploaded to the target by file name and linked |
|
|
329
|
+
| Update: page title and body, collection description | applied, second diff empty |
|
|
330
|
+
| Shop policies | synced; a policy Shopify manages automatically is reported with how to unblock it |
|
|
331
|
+
| Metafield values, including a `product_reference` | applied, reference remapped to the target's own product |
|
|
332
|
+
| Values on products missing from the target | skipped up front with the reason |
|
|
333
|
+
| Export to a folder, then diff that folder against the store | no differences |
|
|
334
|
+
| Theme settings diff | read-only, 15 files compared in about 1 second |
|
|
335
|
+
| Safety: write to a read-only store | refused |
|
|
336
|
+
| Safety: target edited after the plan was made | step skipped, nothing overwritten |
|
|
337
|
+
| Shopify CLI sign-in on Windows (CLI 4.9.4), both stores | `connect` read-only and `--writes`; plan, apply (7 creates incl. file upload and translation), update and re-diff all exact; also through the MCP server over stdio |
|
|
338
|
+
|
|
339
|
+
Behaviours of the live API found this way are covered by tests: `nodes` returns `null` for customer account pages (they're looked up by listing instead), `ShopPolicy.type` has to be aliased next to `Metaobject.type`, Shopify refuses edits to auto-managed policies, and a failed token request carries its reason (such as `app_not_installed`) in an HTML page.
|
|
340
|
+
|
|
341
|
+
## Development
|
|
342
|
+
|
|
343
|
+
```bash
|
|
344
|
+
npm install
|
|
345
|
+
npm test # vitest: unit, sync engine against an in-memory Shopify, and MCP protocol tests
|
|
346
|
+
npm run typecheck
|
|
347
|
+
npm run build
|
|
348
|
+
|
|
349
|
+
# Validate every GraphQL operation against Shopify's real Admin schema
|
|
350
|
+
npm run validate:graphql # defaults to 2026-10
|
|
351
|
+
npm run validate:graphql -- 2027-01
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
Layout:
|
|
355
|
+
|
|
356
|
+
```
|
|
357
|
+
src/
|
|
358
|
+
index.ts entrypoint: MCP server on stdio, or the CLI when given a command
|
|
359
|
+
engine.ts everything the tools and the CLI do, in one place
|
|
360
|
+
server.ts MCP tools
|
|
361
|
+
cli.ts command line
|
|
362
|
+
snapshotFiles.ts export and read snapshot folders (config as code)
|
|
363
|
+
themes.ts read-only theme settings diff
|
|
364
|
+
config.ts store config loading and validation
|
|
365
|
+
shopify/client.ts Admin GraphQL client (token refresh, throttling, retries)
|
|
366
|
+
refs.ts GID <-> portable reference mapping
|
|
367
|
+
snapshot.ts read a store into portable form
|
|
368
|
+
diff.ts field-level diff
|
|
369
|
+
plan.ts dry-run plans and plan storage
|
|
370
|
+
apply.ts guarded, drift-checked, two-pass writes
|
|
371
|
+
resources/ one adapter per resource type
|
|
372
|
+
test/
|
|
373
|
+
fakeShop.ts in-memory Admin API used by the tests
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
Adding a resource type means writing one adapter (`fetch`, `create`, `update`) in `src/resources/`. See [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
377
|
+
|
|
378
|
+
## Roadmap
|
|
379
|
+
|
|
380
|
+
- [x] Translations per locale
|
|
381
|
+
- [x] File references, re-uploaded by URL
|
|
382
|
+
- [x] Collections with smart-collection rules
|
|
383
|
+
- [x] Shop policies, and metafield values on products and collections
|
|
384
|
+
- [x] Snapshots as files, for git-tracked store config
|
|
385
|
+
- [x] Command line
|
|
386
|
+
- [x] Read-only theme settings diff
|
|
387
|
+
- [ ] Move collection rules to Shopify's new `sources` model (`ruleSet` is deprecated but supported through 2027-01)
|
|
388
|
+
- [ ] Menu item (link) translations
|
|
389
|
+
- [ ] Metafield values on variants, pages and the shop
|
|
390
|
+
- [ ] Markets and market web presences
|
|
391
|
+
- [ ] Optional deletes, behind an explicit flag
|
|
392
|
+
|
|
393
|
+
Ideas and PRs are welcome. Issues tagged `good first issue` are a good place to start.
|
|
394
|
+
|
|
395
|
+
## Related projects
|
|
396
|
+
|
|
397
|
+
Syncing definitions between stores isn't a new idea, and these tools are worth knowing:
|
|
398
|
+
|
|
399
|
+
| Project | Type | What it covers |
|
|
400
|
+
|---|---|---|
|
|
401
|
+
| [groupthought/metasync](https://github.com/groupthought/metasync) | CLI | Metafield and metaobject definitions plus resource data; dry run by default; has a delete mode |
|
|
402
|
+
| [TeamDijon/meta-sync](https://github.com/TeamDijon/meta-sync) | CLI | Metafield and metaobject definitions and entries; manifest-based copy; bulk mode deletes the target first |
|
|
403
|
+
| [shopify-metadata-migrator](https://github.com/warriorachilles/shopify-metadata-migrator) | CLI | Copies metafield and metaobject definitions |
|
|
404
|
+
| MetaMaintain, Metora, Rewind Staging | Shopify apps (paid or freemium) | Definition and entry sync through an embedded app |
|
|
405
|
+
|
|
406
|
+
What this project adds: it runs inside your AI client as an MCP server (or as a CLI) with a private app you create yourself, so there's no third-party app to install from the App Store and no data leaves your machine except to Shopify. You can ask for a diff in plain language; it also covers pages, menus, redirects, collections, policies, metafield values and translations, remaps references and files across stores, keeps store configuration in git, checks for drift before writing and never deletes. If you only need a scripted one-shot copy of definitions, one of the CLIs above may be all you need.
|
|
407
|
+
|
|
408
|
+
## Disclaimer
|
|
409
|
+
|
|
410
|
+
Not affiliated with, endorsed by or sponsored by Shopify Inc. "Shopify" is a trademark of Shopify Inc. Test on development stores first; you are responsible for what you apply to production.
|
|
411
|
+
|
|
412
|
+
## License
|
|
413
|
+
|
|
414
|
+
[MIT](LICENSE) © Youssef Idder
|
package/dist/apply.js
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { appendFileSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { hashData } from "./diff.js";
|
|
4
|
+
import { collectRefs, mapStrings, RefToGidResolver } from "./refs.js";
|
|
5
|
+
import { ADAPTERS } from "./resources/index.js";
|
|
6
|
+
import { takeSnapshot } from "./snapshot.js";
|
|
7
|
+
import { stateDir } from "./plan.js";
|
|
8
|
+
export class ApplyRefused extends Error {
|
|
9
|
+
}
|
|
10
|
+
export function checkApplyAllowed(plan, target, opts) {
|
|
11
|
+
const now = opts.now ?? new Date();
|
|
12
|
+
if (plan.target.alias !== target.alias || plan.target.domain !== target.domain) {
|
|
13
|
+
throw new ApplyRefused("plan target doesn't match the configured store");
|
|
14
|
+
}
|
|
15
|
+
if (plan.status !== "pending")
|
|
16
|
+
throw new ApplyRefused(`plan was already ${plan.status.replace("_", " ")}; create a new plan`);
|
|
17
|
+
if (now > new Date(plan.expiresAt))
|
|
18
|
+
throw new ApplyRefused("plan has expired; run plan_sync again so it reflects the current target");
|
|
19
|
+
if (!target.allowWrites) {
|
|
20
|
+
throw new ApplyRefused(`writes are disabled for store "${target.alias}". Set "allowWrites": true in its config to enable them.`);
|
|
21
|
+
}
|
|
22
|
+
if (target.protected && opts.confirm !== target.domain) {
|
|
23
|
+
throw new ApplyRefused(`store "${target.alias}" is protected. Pass confirm="${target.domain}" to apply.`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function audit(file, entry) {
|
|
27
|
+
try {
|
|
28
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
29
|
+
appendFileSync(file, `${JSON.stringify({ at: new Date().toISOString(), ...entry })}\n`, { mode: 0o600 });
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
/* auditing must never break a sync */
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const touches = (v, refs) => [...collectRefs(v)].some((r) => refs.has(r));
|
|
36
|
+
/** Remove metaobject fields whose value contains any of `refs`. */
|
|
37
|
+
function stripFieldsWithRefs(data, refs) {
|
|
38
|
+
const fields = { ...(data.fields ?? {}) };
|
|
39
|
+
for (const [k, v] of Object.entries(fields)) {
|
|
40
|
+
if (touches(v, refs))
|
|
41
|
+
delete fields[k];
|
|
42
|
+
}
|
|
43
|
+
return { ...data, fields };
|
|
44
|
+
}
|
|
45
|
+
/** Remove metaobject definition fields whose validations reference any of `refs` (e.g. a self-reference). */
|
|
46
|
+
function stripFieldDefsWithRefs(data, refs) {
|
|
47
|
+
return { ...data, fields: (data.fields ?? []).filter((f) => !touches(f, refs)) };
|
|
48
|
+
}
|
|
49
|
+
/** Kinds that can be written in two passes: first without the unresolved parts, then complete. */
|
|
50
|
+
const PARTIAL = {
|
|
51
|
+
metaobjects: stripFieldsWithRefs,
|
|
52
|
+
metaobject_definitions: stripFieldDefsWithRefs,
|
|
53
|
+
};
|
|
54
|
+
/** A menu link to something that can't be matched is left out rather than sent with an empty target. */
|
|
55
|
+
function dropMenuItems(items, refs) {
|
|
56
|
+
return items
|
|
57
|
+
.filter((i) => !(i.resourceId && refs.has(i.resourceId)))
|
|
58
|
+
.map((i) => (i.items ? { ...i, items: dropMenuItems(i.items, refs) } : i));
|
|
59
|
+
}
|
|
60
|
+
/** Drop values that can never be resolved (files, variants...) instead of sending a broken ref. */
|
|
61
|
+
function dropUnmappable(data, kind) {
|
|
62
|
+
const unmappable = new Set([...collectRefs(data)].filter((r) => r.startsWith("shopify://unresolved/")));
|
|
63
|
+
if (!unmappable.size)
|
|
64
|
+
return data;
|
|
65
|
+
if (kind === "metaobjects")
|
|
66
|
+
return stripFieldsWithRefs(data, unmappable);
|
|
67
|
+
if (kind === "menus")
|
|
68
|
+
return { ...data, items: dropMenuItems(data.items ?? [], unmappable) };
|
|
69
|
+
return mapStrings(data, (s) => (unmappable.has(s) ? "" : s));
|
|
70
|
+
}
|
|
71
|
+
export async function applyPlan(plan, client, opts = {}) {
|
|
72
|
+
checkApplyAllowed(plan, client.store, opts);
|
|
73
|
+
const log = opts.auditLog ?? process.env.SYNC_AUDIT_LOG ?? join(stateDir(), "audit.log");
|
|
74
|
+
const resolver = new RefToGidResolver(client, new Map(Object.entries(plan.files ?? {})));
|
|
75
|
+
const results = [];
|
|
76
|
+
const selected = plan.ops.filter((o) => !opts.only?.length || opts.only.includes(o.seq));
|
|
77
|
+
const runnable = [];
|
|
78
|
+
for (const op of selected) {
|
|
79
|
+
if (op.action === "conflict")
|
|
80
|
+
results.push({ seq: op.seq, kind: op.kind, key: op.key, outcome: "skipped", message: op.reason });
|
|
81
|
+
else
|
|
82
|
+
runnable.push(op);
|
|
83
|
+
}
|
|
84
|
+
// Drift check: re-read the target and skip anything that changed since the plan was made.
|
|
85
|
+
const kinds = [...new Set(runnable.map((o) => o.kind))];
|
|
86
|
+
const current = await takeSnapshot(client, kinds);
|
|
87
|
+
const currentByKind = new Map(kinds.map((k) => [k, new Map((current.resources[k]?.items ?? []).map((i) => [i.key, i]))]));
|
|
88
|
+
const ready = [];
|
|
89
|
+
for (const op of runnable) {
|
|
90
|
+
const now = currentByKind.get(op.kind)?.get(op.key);
|
|
91
|
+
if (op.action === "create" && now) {
|
|
92
|
+
results.push({ seq: op.seq, kind: op.kind, key: op.key, outcome: "skipped", message: "already exists in target now; re-run plan_sync" });
|
|
93
|
+
}
|
|
94
|
+
else if (op.action === "update" && (!now || hashData(now.data) !== op.targetHash)) {
|
|
95
|
+
results.push({ seq: op.seq, kind: op.kind, key: op.key, outcome: "skipped", message: "target changed since the plan was made; re-run plan_sync" });
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
ready.push(op);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const retry = [];
|
|
102
|
+
/** Current target state, refreshed before pass 2 so updates see what pass 1 created. */
|
|
103
|
+
let pass2Targets = new Map();
|
|
104
|
+
const run = async (op, pass) => {
|
|
105
|
+
const adapter = ADAPTERS[op.kind];
|
|
106
|
+
let data = dropUnmappable(op.data, op.kind);
|
|
107
|
+
try {
|
|
108
|
+
const { value, missing } = await resolver.rewrite(data);
|
|
109
|
+
data = value;
|
|
110
|
+
const strip = PARTIAL[op.kind];
|
|
111
|
+
if (missing.length) {
|
|
112
|
+
if (strip && pass === 1) {
|
|
113
|
+
// Probably points at something later in this plan (or at itself): write what we can now, finish in pass 2.
|
|
114
|
+
data = strip(dropUnmappable(op.data, op.kind), new Set(missing));
|
|
115
|
+
data = (await resolver.rewrite(data)).value;
|
|
116
|
+
retry.push(op);
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
throw new Error(`references not found in target: ${missing.join(", ")}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const target = (pass === 2 && pass2Targets.get(`${op.kind}:${op.key}`)) || {
|
|
123
|
+
key: op.key,
|
|
124
|
+
id: op.targetId ?? "",
|
|
125
|
+
data: op.targetData ?? {},
|
|
126
|
+
};
|
|
127
|
+
let outcome;
|
|
128
|
+
if (op.action === "create" && pass === 1) {
|
|
129
|
+
const id = await adapter.create({ client, resolver }, op.key, data);
|
|
130
|
+
const ref = adapter.refFor?.(op.key);
|
|
131
|
+
if (ref)
|
|
132
|
+
resolver.remember(ref, id);
|
|
133
|
+
outcome = "created";
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
await adapter.update({ client, resolver }, target, data);
|
|
137
|
+
outcome = op.action === "create" ? "created" : "updated";
|
|
138
|
+
}
|
|
139
|
+
const msg = retry.includes(op) && pass === 1 ? "partially written; finishing references in a second pass" : undefined;
|
|
140
|
+
upsertResult({ seq: op.seq, kind: op.kind, key: op.key, outcome, message: msg });
|
|
141
|
+
}
|
|
142
|
+
catch (err) {
|
|
143
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
144
|
+
if (PARTIAL[op.kind] && pass === 1 && !retry.includes(op))
|
|
145
|
+
retry.push(op);
|
|
146
|
+
upsertResult({ seq: op.seq, kind: op.kind, key: op.key, outcome: "failed", message });
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
const upsertResult = (r) => {
|
|
150
|
+
const i = results.findIndex((x) => x.seq === r.seq);
|
|
151
|
+
if (i >= 0)
|
|
152
|
+
results[i] = r;
|
|
153
|
+
else
|
|
154
|
+
results.push(r);
|
|
155
|
+
audit(log, { plan: plan.id, target: plan.target.domain, source: plan.source.domain, ...r });
|
|
156
|
+
};
|
|
157
|
+
for (const op of ready)
|
|
158
|
+
await run(op, 1);
|
|
159
|
+
if (retry.length) {
|
|
160
|
+
const retryKinds = [...new Set(retry.map((o) => o.kind))];
|
|
161
|
+
const fresh = await takeSnapshot(client, retryKinds);
|
|
162
|
+
pass2Targets = new Map(retryKinds.flatMap((k) => (fresh.resources[k]?.items ?? []).map((i) => [`${k}:${i.key}`, i])));
|
|
163
|
+
for (const op of retry) {
|
|
164
|
+
// An op that failed outright in pass 1 and still doesn't exist must be created, not updated.
|
|
165
|
+
if (op.action === "create" && !pass2Targets.has(`${op.kind}:${op.key}`))
|
|
166
|
+
await run({ ...op }, 1);
|
|
167
|
+
else
|
|
168
|
+
await run(op, 2);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
results.sort((a, b) => a.seq - b.seq);
|
|
172
|
+
if (resolver.uploaded.length)
|
|
173
|
+
audit(log, { plan: plan.id, target: plan.target.domain, uploadedFiles: resolver.uploaded });
|
|
174
|
+
const failed = results.filter((r) => r.outcome === "failed").length;
|
|
175
|
+
const done = results.filter((r) => r.outcome === "created" || r.outcome === "updated").length;
|
|
176
|
+
if (opts.only?.length && selected.length < plan.ops.length) {
|
|
177
|
+
// Partial run: keep the plan usable for the remaining ops (applied ones will show as drifted).
|
|
178
|
+
plan.status = "pending";
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
plan.status = failed === 0 ? "applied" : done > 0 ? "partially_applied" : "failed";
|
|
182
|
+
}
|
|
183
|
+
return results;
|
|
184
|
+
}
|
|
185
|
+
//# sourceMappingURL=apply.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"apply.js","sourceRoot":"","sources":["../src/apply.ts"],"names":[],"mappings":"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"}
|