mcp-magento-cms 1.0.3 → 1.2.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/README.md +173 -82
- package/package.json +4 -3
- package/src/assignStores.js +150 -0
- package/src/index.js +840 -89
- package/src/magentoClient.js +95 -4
- package/src/pageBuilder.js +66 -68
- package/src/pageBuilderElements.js +789 -0
- package/src/pageBuilderRepair.js +266 -0
- package/src/pageBuilderRules.js +127 -0
- package/src/pageBuilderValidate.js +164 -0
- package/src/projectRules.js +95 -0
package/README.md
CHANGED
|
@@ -1,74 +1,73 @@
|
|
|
1
1
|
# Magento CMS MCP (Cursor)
|
|
2
2
|
|
|
3
|
-
Create/update Magento **CMS Pages / Blocks** with Page Builder **
|
|
3
|
+
Create/update Magento **CMS Pages / Blocks** with Page Builder from Cursor chat — including **validate/repair** for stage-safe HTML and **raw Page Builder HTML** saves for complex layouts.
|
|
4
4
|
|
|
5
|
-
- **AI:** Cursor plans
|
|
6
|
-
- **This server:** Magento REST via
|
|
7
|
-
- **
|
|
5
|
+
- **AI:** Cursor plans content and diagnostics
|
|
6
|
+
- **This server:** Magento REST via Integration Access Token
|
|
7
|
+
- **Local:** run from this repo path in `mcp.json` (point `MAGENTO_BASE_URL` at the target store)
|
|
8
|
+
- **Published:** `npx -y mcp-magento-cms` (npm)
|
|
8
9
|
|
|
9
10
|
Requires **Node.js 18+** and [Cursor](https://cursor.com).
|
|
10
11
|
|
|
11
|
-
|
|
12
|
+
## CMS workflow (mandatory for agents & teams)
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
| Tool | Purpose |
|
|
16
|
-
|------|---------|
|
|
17
|
-
| `magento_health` | Test base URL + token |
|
|
18
|
-
| `get_cms_page` | Load by `page_id` |
|
|
19
|
-
| `create_cms_page` | Create from title + content |
|
|
20
|
-
| `update_cms_page` | Update by `page_id` |
|
|
21
|
-
| `get_cms_block` / `create_cms_block` / `update_cms_block` | Blocks |
|
|
22
|
-
| `list_section_types` | Supported layouts (MVP: `simple_text`) |
|
|
23
|
-
|
|
24
|
-
## Setup (any machine)
|
|
14
|
+
**Use this MCP as the only way to create or update Magento CMS pages/blocks.**
|
|
25
15
|
|
|
26
|
-
|
|
16
|
+
| Do | Do not |
|
|
17
|
+
|----|--------|
|
|
18
|
+
| `pagebuilder_rules` → `compose_pagebuilder` / `create_cms_page` / `set_pagebuilder_html` | Project PHP `tools/compose-*.php` / `deploy-*.php` |
|
|
19
|
+
| `validate_pagebuilder` / `repair_pagebuilder` before save | Direct DB edits to `cms_page` / `cms_block` content |
|
|
20
|
+
| Improve **this package** when something is missing or wrong | Bypass MCP with one-off scripts |
|
|
27
21
|
|
|
28
|
-
|
|
22
|
+
Preferred path: `sections[]` → `compose_pagebuilder` (or `create_cms_page` with `sections`) → validate → save. Use `set_pagebuilder_html` only for intentional raw master HTML. After save, verify on the storefront.
|
|
29
23
|
|
|
30
|
-
|
|
24
|
+
Reload/restart the MCP server in Cursor after changing this package so teammates pick up new rules and tools.
|
|
31
25
|
|
|
32
|
-
|
|
33
|
-
2. Name it (e.g. `MCP CMS`) and set your admin password when prompted
|
|
34
|
-
3. Open the **API** tab → set **Resource Access** to **Custom**
|
|
35
|
-
4. Under **Content → Elements**, select:
|
|
26
|
+
**REST field note (Magento 2.4.8):** `cmsPage` create/update use `active` (not `is_active`) and reject `store_id` / unknown fields. The client maps `is_active` → `active` so tool args stay familiar.
|
|
36
27
|
|
|
37
|
-
|
|
38
|
-
|--------|--------|
|
|
39
|
-
| **Pages** | Required (parent) |
|
|
40
|
-
| → **Save Page** | Create / update pages |
|
|
41
|
-
| → **Edit Page Design** | Page Builder / design fields |
|
|
42
|
-
| → **Delete Page** | Optional (MCP does not delete today) |
|
|
43
|
-
| **Blocks** | Required (create / update blocks) |
|
|
28
|
+
**Store views:** Magento REST cannot set `cms_page_store` / `cms_block_store`. On create, this MCP assigns **All Store Views** (`store_id = 0`) by default via Magento PHP (`scripts/assign-stores.php` + `ddev exec` when `.ddev` exists). Pass `store_ids: [1, 2]` (etc.) only when the user asks for specific store view(s). Requires `MAGENTO_ROOT` (or infer from `MAGENTO_CMS_PROJECT_RULES` → project root).
|
|
44
29
|
|
|
45
|
-
|
|
30
|
+
**Page layout:** On create, default `page_layout` is **`cms-full-width`** (admin label: Page — Full Width). Pass `page_layout` only when the user asks for a different layout (e.g. `1column`).
|
|
46
31
|
|
|
47
|
-
|
|
32
|
+
## Shared rules (all projects — read first)
|
|
48
33
|
|
|
49
|
-
|
|
34
|
+
Returned by `pagebuilder_rules` for **every** store. This package stays free of store-specific hex / class catalogs; those go in optional project rules.
|
|
50
35
|
|
|
51
|
-
|
|
36
|
+
### Page Builder (stage-safe)
|
|
52
37
|
|
|
53
|
-
**
|
|
38
|
+
1. Rows: use **Full Width** or **Contained**. **Never Full Bleed** unless the user explicitly asks.
|
|
39
|
+
2. When edge-to-edge hero is intentional: `allow_full_bleed=true` on validate/set/repair.
|
|
40
|
+
3. `full-width` rows **MUST** include `.row-full-width-inner[data-element=inner]`.
|
|
41
|
+
4. `contained` rows **MUST** include `[data-element=inner]`.
|
|
42
|
+
5. Converting `full-bleed` → `full-width` without adding the inner wrapper breaks admin Page Builder.
|
|
43
|
+
6. Every `[data-pb-style=ID]` in `<style>` must match an element — **orphans crash** the stage.
|
|
44
|
+
7. Every column needs `width:%` in the style block.
|
|
45
|
+
8. Avoid raw `<svg>` inside Text — use **Image** (or theme CSS) for icons.
|
|
46
|
+
9. Hero CTAs: Banner **Show Button=Always** + Link URL.
|
|
47
|
+
10. Links: prefer `{{store url=path}}` with an unquoted path (avoid `"` in href).
|
|
48
|
+
11. Prefer native content types; CMS body = page content only (layout owns chrome).
|
|
54
49
|
|
|
55
|
-
|
|
56
|
-
bin/magento config:set oauth/consumer/enable_integration_as_bearer 1
|
|
57
|
-
bin/magento cache:flush config
|
|
58
|
-
```
|
|
50
|
+
### CMS media + theme conventions (all projects)
|
|
59
51
|
|
|
60
|
-
|
|
52
|
+
12. **One flat media folder per CMS page** under `pub/media/wysiwyg/` — no `cards/`, `icons/`, `products/`, `related/` subfolders.
|
|
53
|
+
13. **Folder name = CMS page URL key** (identifier), e.g. `wysiwyg/markets/market-industrial/hero.jpg`.
|
|
54
|
+
14. After renaming media folders, update all `{{media url=…}}` in `cms_page` / `cms_block`, then flush cache.
|
|
55
|
+
15. Sibling pages share **one** Magento_Cms LESS partial + shared classes — do not copy near-identical LESS into theme `_sources.less` per page.
|
|
56
|
+
16. Page-only LESS → theme `Magento_Cms/web/css/source/` + `_module.less` import.
|
|
57
|
+
17. Brand primary/secondary once in `_theme.less` (`@theme__color__primary` / `@theme__color__secondary`); use vars, not hex, in page LESS.
|
|
58
|
+
18. **No module-level re-aliases** of theme vars; **no redundant `font-family`** when body already uses the theme base font.
|
|
59
|
+
19. Scope page CSS with layout body class and/or content markers (`:has(.…)`).
|
|
61
60
|
|
|
62
|
-
|
|
61
|
+
Use tools `pagebuilder_rules`, `validate_pagebuilder`, and `repair_pagebuilder`.
|
|
63
62
|
|
|
64
|
-
|
|
63
|
+
## Per-store project rules (optional)
|
|
65
64
|
|
|
66
|
-
-
|
|
67
|
-
- Local ddev: `https://your-project.ddev.site` (`ddev describe` shows the URL)
|
|
65
|
+
Keep the published MCP **general**. Put store-specific values (brand hex, Vendor/theme, URL keys, class prefixes) in the **Magento project**, not in this package.
|
|
68
66
|
|
|
69
|
-
|
|
67
|
+
**Recommended (project-wise Cursor config):**
|
|
70
68
|
|
|
71
|
-
|
|
69
|
+
1. Commit `.cursor/magento-cms-project-rules.json` in the Magento repo.
|
|
70
|
+
2. Add project MCP config at `.cursor/mcp.json` (Customize → MCPs → workspace) with `MAGENTO_ROOT` only — rules auto-load from `${MAGENTO_ROOT}/.cursor/magento-cms-project-rules.json`.
|
|
72
71
|
|
|
73
72
|
```json
|
|
74
73
|
{
|
|
@@ -77,68 +76,160 @@ Use your storefront base URL **without** a trailing slash, for example:
|
|
|
77
76
|
"command": "npx",
|
|
78
77
|
"args": ["-y", "mcp-magento-cms"],
|
|
79
78
|
"env": {
|
|
80
|
-
"MAGENTO_BASE_URL": "https://
|
|
81
|
-
"MAGENTO_ACCESS_TOKEN": "
|
|
79
|
+
"MAGENTO_BASE_URL": "https://your-store.example/",
|
|
80
|
+
"MAGENTO_ACCESS_TOKEN": "${env:MAGENTO_ACCESS_TOKEN}",
|
|
81
|
+
"MAGENTO_ROOT": "${workspaceFolder}"
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
86
|
```
|
|
87
87
|
|
|
88
|
-
|
|
88
|
+
Optional override: `MAGENTO_CMS_PROJECT_RULES=/path/to/file.json` if the rules file is not under `.cursor/`.
|
|
89
|
+
|
|
90
|
+
`pagebuilder_rules` appends project rules after shared rules. JSON shape:
|
|
89
91
|
|
|
90
92
|
```json
|
|
91
|
-
|
|
93
|
+
{
|
|
94
|
+
"rules": ["store-only convention"],
|
|
95
|
+
"detail": { "brand_tokens": "optional longer notes" }
|
|
96
|
+
}
|
|
92
97
|
```
|
|
93
98
|
|
|
94
|
-
|
|
99
|
+
Markdown/text files are also supported (one rule per non-empty, non-`#` line). Projects with no rules file still get all shared conventions.
|
|
100
|
+
|
|
95
101
|
|
|
96
|
-
|
|
102
|
+
## Compose with sections[]
|
|
97
103
|
|
|
98
|
-
|
|
99
|
-
|----------|---------|---------|
|
|
100
|
-
| `MAGENTO_STORE_CODE` | (empty) | REST store code |
|
|
101
|
-
| `MAGENTO_ADMIN_PATH` | `admin` | Admin path used in edit links |
|
|
104
|
+
Prefer structured trees over project-specific PHP. Theme CSS class names go in `css_classes` (project owns naming).
|
|
102
105
|
|
|
103
|
-
|
|
106
|
+
```js
|
|
107
|
+
// Tool: compose_pagebuilder OR create_cms_page({ title, sections })
|
|
108
|
+
{
|
|
109
|
+
"sections": [
|
|
110
|
+
{
|
|
111
|
+
"type": "row",
|
|
112
|
+
"appearance": "full-bleed",
|
|
113
|
+
"children": [{
|
|
114
|
+
"type": "banner",
|
|
115
|
+
"background_image": "{{media url=wysiwyg/hero.jpg}}",
|
|
116
|
+
"content_html": "<h1>Title</h1>",
|
|
117
|
+
"show_button": "always",
|
|
118
|
+
"button_text": "Contact",
|
|
119
|
+
"link_url": "#contact"
|
|
120
|
+
}]
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
"type": "row",
|
|
124
|
+
"appearance": "contained",
|
|
125
|
+
"children": [
|
|
126
|
+
{ "type": "heading", "text": "Section", "heading_type": "h2" },
|
|
127
|
+
{ "type": "text", "html": "<p>Body</p>" },
|
|
128
|
+
{
|
|
129
|
+
"type": "columns",
|
|
130
|
+
"columns": [
|
|
131
|
+
{ "children": [
|
|
132
|
+
{ "type": "image", "src": "{{media url=wysiwyg/a.jpg}}", "alt": "A" },
|
|
133
|
+
{ "type": "text", "html": "<p>Card</p>" }
|
|
134
|
+
]}
|
|
135
|
+
]
|
|
136
|
+
},
|
|
137
|
+
{ "type": "buttons", "items": [{ "text": "Shop", "url": "{{store url=shop}}" }] },
|
|
138
|
+
{ "type": "divider" },
|
|
139
|
+
{ "type": "products", "skus": ["SKU1", "SKU2"] },
|
|
140
|
+
{ "type": "block", "identifier": "my-cms-block" }
|
|
141
|
+
]
|
|
142
|
+
}
|
|
143
|
+
]
|
|
144
|
+
}
|
|
145
|
+
```
|
|
104
146
|
|
|
105
|
-
|
|
147
|
+
Native types: `row`, `columns` / `column-group` / `column`, `text`, `heading`, `image`, `buttons`, `banner`, `html`, `divider`, `video`, `map`, `tabs`, `slider`, `products`, `block`.
|
|
106
148
|
|
|
107
|
-
|
|
149
|
+
Call `list_section_types` for the full parameter catalog. For intentional edge-to-edge heroes use `appearance: "full-bleed"` and `allow_full_bleed: true` on save.
|
|
108
150
|
|
|
109
|
-
|
|
151
|
+
## Tools
|
|
110
152
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
153
|
+
| Tool | Purpose |
|
|
154
|
+
|------|---------|
|
|
155
|
+
| `magento_health` | Test base URL + token |
|
|
156
|
+
| `pagebuilder_rules` | Return Full Width / inner / orphan-style rules |
|
|
157
|
+
| `list_section_types` | Native element catalog + composition patterns |
|
|
158
|
+
| `compose_pagebuilder` | Build master-format HTML from `sections[]` (all PB types); optional save |
|
|
159
|
+
| `search_cms` | Find pages/blocks by identifier |
|
|
160
|
+
| `get_cms_page` / `get_cms_block` | Load entity (`include_content`, `validate`) |
|
|
161
|
+
| `validate_pagebuilder` | Stage-safety checks (html / page_id / block_id) |
|
|
162
|
+
| `repair_pagebuilder` | Fix orphans, full-bleed, inners, widths, SVG (`apply=true` to save) |
|
|
163
|
+
| `set_pagebuilder_html` | Save **raw** PB HTML without wiping to simple_text |
|
|
164
|
+
| `create_cms_page` / `update_cms_page` | Simple Row→Text composer |
|
|
165
|
+
| `create_cms_block` / `update_cms_block` | Simple Row→Text composer |
|
|
114
166
|
|
|
115
|
-
|
|
167
|
+
**Important:** `update_cms_page` / `update_cms_block` with `content` rebuild as **simple_text** only. For complex pages use `set_pagebuilder_html` or `repair_pagebuilder` with `apply=true`.
|
|
116
168
|
|
|
117
|
-
|
|
118
|
-
Create a CMS page titled "MCP Demo" with this content:
|
|
119
|
-
First paragraph.
|
|
120
|
-
Second paragraph.
|
|
121
|
-
```
|
|
169
|
+
## Local Cursor config (this machine)
|
|
122
170
|
|
|
123
|
-
```
|
|
124
|
-
|
|
171
|
+
```json
|
|
172
|
+
{
|
|
173
|
+
"mcpServers": {
|
|
174
|
+
"magento-cms": {
|
|
175
|
+
"command": "node",
|
|
176
|
+
"args": [
|
|
177
|
+
"/home/ajaykumar.prajapati@brainvire.com/htdocs/mcp-magento-cms/src/index.js"
|
|
178
|
+
],
|
|
179
|
+
"env": {
|
|
180
|
+
"MAGENTO_BASE_URL": "https://your-store.ddev.site",
|
|
181
|
+
"MAGENTO_ACCESS_TOKEN": "YOUR_TOKEN",
|
|
182
|
+
"NODE_TLS_REJECT_UNAUTHORIZED": "0"
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
125
187
|
```
|
|
126
188
|
|
|
127
|
-
|
|
189
|
+
After changing this package, **reload MCP** in Cursor (Settings → MCP → refresh / restart).
|
|
190
|
+
|
|
191
|
+
## Setup (published npx)
|
|
128
192
|
|
|
129
|
-
|
|
130
|
-
- Give the Magento integration **CMS-only** permissions
|
|
193
|
+
### 1. Magento Integration Access Token
|
|
131
194
|
|
|
132
|
-
|
|
195
|
+
1. **System → Extensions → Integrations → Add New Integration**
|
|
196
|
+
2. API → Custom → Content → Pages (Save/Edit Design) + Blocks
|
|
197
|
+
3. Activate → copy Access Token
|
|
133
198
|
|
|
134
|
-
|
|
135
|
-
- Magento 2 with REST and an Integration token
|
|
136
|
-
- Network access from that machine to `MAGENTO_BASE_URL`
|
|
199
|
+
Enable Bearer tokens (2.4.4+):
|
|
137
200
|
|
|
138
|
-
|
|
201
|
+
```bash
|
|
202
|
+
bin/magento config:set oauth/consumer/enable_integration_as_bearer 1
|
|
203
|
+
bin/magento cache:flush config
|
|
204
|
+
```
|
|
139
205
|
|
|
140
|
-
|
|
206
|
+
### 2. mcp.json with npx
|
|
207
|
+
|
|
208
|
+
```json
|
|
209
|
+
{
|
|
210
|
+
"mcpServers": {
|
|
211
|
+
"magento-cms": {
|
|
212
|
+
"command": "npx",
|
|
213
|
+
"args": ["-y", "mcp-magento-cms"],
|
|
214
|
+
"env": {
|
|
215
|
+
"MAGENTO_BASE_URL": "https://www.example.com",
|
|
216
|
+
"MAGENTO_ACCESS_TOKEN": "YOUR_INTEGRATION_ACCESS_TOKEN"
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Local ddev TLS: add `"NODE_TLS_REJECT_UNAUTHORIZED": "0"`.
|
|
224
|
+
|
|
225
|
+
## Scripts
|
|
141
226
|
|
|
142
227
|
```bash
|
|
143
|
-
npm
|
|
228
|
+
npm run test:compose
|
|
229
|
+
npm run test:validate
|
|
230
|
+
npm run health
|
|
144
231
|
```
|
|
232
|
+
|
|
233
|
+
## Version
|
|
234
|
+
|
|
235
|
+
`1.2.0` — compose all native Page Builder content types via `sections[]` + validate/repair/raw HTML.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-magento-cms",
|
|
3
|
-
"version": "1.0
|
|
4
|
-
"description": "Cursor MCP: Magento CMS pages/blocks
|
|
3
|
+
"version": "1.2.0",
|
|
4
|
+
"description": "Cursor MCP: Magento CMS pages/blocks + Page Builder compose/validate/repair for all native content types (zero npm deps)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
7
7
|
"bin": {
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
"scripts": {
|
|
15
15
|
"start": "node src/index.js",
|
|
16
16
|
"health": "node scripts/health-check.js",
|
|
17
|
-
"test:compose": "node scripts/test-compose.js"
|
|
17
|
+
"test:compose": "node scripts/test-compose.js",
|
|
18
|
+
"test:validate": "node scripts/test-validate-repair.js"
|
|
18
19
|
},
|
|
19
20
|
"engines": {
|
|
20
21
|
"node": ">=18"
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Assign CMS page/block to Magento store view(s).
|
|
3
|
+
*
|
|
4
|
+
* Magento 2.4.8 REST cmsPage/cmsBlock reject store_id on create/update, so we
|
|
5
|
+
* assign via Magento bootstrap PHP when MAGENTO_ROOT is set (or inferred from
|
|
6
|
+
* MAGENTO_CMS_PROJECT_RULES → project/.cursor/… → project root).
|
|
7
|
+
*
|
|
8
|
+
* Default: [0] = All Store Views.
|
|
9
|
+
* Override: pass store_ids from the tool args when the user names specific stores.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { spawnSync } from 'node:child_process';
|
|
13
|
+
import fs from 'node:fs';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
import { fileURLToPath } from 'node:url';
|
|
16
|
+
|
|
17
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
|
|
19
|
+
export function resolveMagentoRoot() {
|
|
20
|
+
const fromEnv = String(process.env.MAGENTO_ROOT || '').trim();
|
|
21
|
+
if (fromEnv && fs.existsSync(path.join(fromEnv, 'app/bootstrap.php'))) {
|
|
22
|
+
return path.resolve(fromEnv);
|
|
23
|
+
}
|
|
24
|
+
const rules = String(process.env.MAGENTO_CMS_PROJECT_RULES || '').trim();
|
|
25
|
+
if (rules) {
|
|
26
|
+
const candidate = path.resolve(path.dirname(rules), '..');
|
|
27
|
+
if (fs.existsSync(path.join(candidate, 'app/bootstrap.php'))) {
|
|
28
|
+
return candidate;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @param {object} args
|
|
36
|
+
* @returns {number[]}
|
|
37
|
+
*/
|
|
38
|
+
export function resolveStoreIds(args = {}) {
|
|
39
|
+
let ids = [];
|
|
40
|
+
if (Array.isArray(args.store_ids) && args.store_ids.length) {
|
|
41
|
+
ids = args.store_ids.map((n) => Number(n)).filter((n) => Number.isFinite(n));
|
|
42
|
+
} else if (typeof args.store_ids === 'string' && args.store_ids.trim()) {
|
|
43
|
+
ids = args.store_ids
|
|
44
|
+
.split(',')
|
|
45
|
+
.map((s) => Number(s.trim()))
|
|
46
|
+
.filter((n) => Number.isFinite(n));
|
|
47
|
+
} else if (Array.isArray(args.store_codes) && args.store_codes.length) {
|
|
48
|
+
const codes = args.store_codes.map((c) => String(c).toLowerCase());
|
|
49
|
+
if (codes.includes('all') || codes.includes('*')) {
|
|
50
|
+
return [0];
|
|
51
|
+
}
|
|
52
|
+
// Numeric codes still accepted via store_ids; unknown codes → default all
|
|
53
|
+
return [0];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (!ids.length) {
|
|
57
|
+
return [0];
|
|
58
|
+
}
|
|
59
|
+
if (ids.includes(0)) {
|
|
60
|
+
return [0];
|
|
61
|
+
}
|
|
62
|
+
return [...new Set(ids)];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* @param {'page'|'block'} type
|
|
67
|
+
* @param {number} id
|
|
68
|
+
* @param {number[]} storeIds
|
|
69
|
+
*/
|
|
70
|
+
export function assignCmsStores(type, id, storeIds = [0]) {
|
|
71
|
+
const root = resolveMagentoRoot();
|
|
72
|
+
if (!root) {
|
|
73
|
+
return {
|
|
74
|
+
ok: false,
|
|
75
|
+
skipped: true,
|
|
76
|
+
error:
|
|
77
|
+
'Store assignment skipped: set MAGENTO_ROOT in mcp.json (path to Magento) so pages default to All Store Views (store_id 0).',
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const srcScript = path.join(__dirname, '..', 'scripts', 'assign-stores.php');
|
|
82
|
+
if (!fs.existsSync(srcScript)) {
|
|
83
|
+
return { ok: false, error: `Missing ${srcScript}` };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const stores = (storeIds?.length ? storeIds : [0]).join(',');
|
|
87
|
+
const useDdev =
|
|
88
|
+
['1', 'true', 'yes'].includes(
|
|
89
|
+
String(process.env.MAGENTO_USE_DDEV || '').toLowerCase()
|
|
90
|
+
) || fs.existsSync(path.join(root, '.ddev'));
|
|
91
|
+
|
|
92
|
+
// Copy script into Magento var/ so ddev exec can run it inside the project mount
|
|
93
|
+
const destDir = path.join(root, 'var');
|
|
94
|
+
const destScript = path.join(destDir, 'mcp-assign-stores.php');
|
|
95
|
+
try {
|
|
96
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
97
|
+
fs.copyFileSync(srcScript, destScript);
|
|
98
|
+
} catch (e) {
|
|
99
|
+
return { ok: false, error: `Cannot stage assign script: ${e.message}` };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const args = [
|
|
103
|
+
'var/mcp-assign-stores.php',
|
|
104
|
+
`--type=${type}`,
|
|
105
|
+
`--id=${id}`,
|
|
106
|
+
`--stores=${stores}`,
|
|
107
|
+
];
|
|
108
|
+
|
|
109
|
+
let result;
|
|
110
|
+
if (useDdev) {
|
|
111
|
+
result = spawnSync('ddev', ['exec', 'php', ...args], {
|
|
112
|
+
cwd: root,
|
|
113
|
+
encoding: 'utf8',
|
|
114
|
+
timeout: 120000,
|
|
115
|
+
});
|
|
116
|
+
} else {
|
|
117
|
+
const phpBin = String(process.env.MAGENTO_PHP || 'php').trim() || 'php';
|
|
118
|
+
result = spawnSync(phpBin, [destScript, `--root=${root}`, ...args.slice(1)], {
|
|
119
|
+
encoding: 'utf8',
|
|
120
|
+
timeout: 120000,
|
|
121
|
+
env: process.env,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (result.error) {
|
|
126
|
+
return { ok: false, error: result.error.message };
|
|
127
|
+
}
|
|
128
|
+
const out = String(result.stdout || '').trim();
|
|
129
|
+
const err = String(result.stderr || '').trim();
|
|
130
|
+
if (result.status !== 0) {
|
|
131
|
+
return { ok: false, error: err || out || `exit ${result.status}` };
|
|
132
|
+
}
|
|
133
|
+
try {
|
|
134
|
+
const jsonLine = out
|
|
135
|
+
.split('\n')
|
|
136
|
+
.map((l) => l.trim())
|
|
137
|
+
.filter(Boolean)
|
|
138
|
+
.pop();
|
|
139
|
+
const parsed = JSON.parse(jsonLine);
|
|
140
|
+
return {
|
|
141
|
+
ok: !!parsed.ok,
|
|
142
|
+
assigned_stores: parsed.assigned_stores,
|
|
143
|
+
requested_stores: parsed.requested_stores,
|
|
144
|
+
magento_root: root,
|
|
145
|
+
via: useDdev ? 'ddev' : 'php',
|
|
146
|
+
};
|
|
147
|
+
} catch {
|
|
148
|
+
return { ok: true, assigned_stores: storeIds, raw: out, magento_root: root };
|
|
149
|
+
}
|
|
150
|
+
}
|