@diviops/mcp-server 1.5.11 → 1.5.13

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 CHANGED
@@ -85,18 +85,36 @@ See [server-reference.md](../docs/server-reference.md) for per-tool descriptions
85
85
  The package also ships a standalone command-line preset emitter, `diviops-preset`,
86
86
  that produces byte-canonical Divi 5.5.x preset JSON gated by the verified-attrs
87
87
  registry (`data/verified-attrs.json`). It is independent of the MCP stdio server —
88
- run it directly:
88
+ run it directly. Current commands:
89
+
90
+ | Command | Emits |
91
+ |---|---|
92
+ | `diviops-preset button [options]` | `divi/button` group preset |
93
+ | `diviops-preset heading-font [options]` | `divi/font` group preset for `divi/heading` (Pattern A — Google Fonts — or Pattern B — local-hosted) |
94
+ | `diviops-preset text-body-font [options]` | `divi/font-body` group preset for `divi/text` — **Pattern A (Google Fonts) only**; Pattern B for body-text has no registered canonical shape and is refused |
89
95
 
90
96
  ```bash
91
97
  diviops-preset button --name "Primary" --bg-color gcid-primary-color \
92
98
  --bg-color-hover gcid-secondary-color --radius 8px \
93
99
  --font-family Inter --font-weight 600 --font-color gcid-body-color
100
+
101
+ diviops-preset heading-font --name "Heading H1" --pattern google \
102
+ --font-family Inter --font-weight 700 \
103
+ --font-color gcid-heading-color --font-size 48px
104
+
105
+ diviops-preset text-body-font --name "Body Text" --pattern google \
106
+ --font-family Inter --font-weight 400 \
107
+ --font-color gcid-body-color --font-size 16px
94
108
  ```
95
109
 
96
110
  `--dry-run` (the default) composes and prints the canonical JSON with no
97
111
  credentials and no network. `--apply` posts to the existing `/preset/create`
98
112
  REST route, reusing the same `WP_URL` / `WP_USER` / `WP_APP_PASSWORD` env vars.
99
- The current scope is one emitter — `divi/button` group presets. See the
113
+
114
+ The CLI's coverage is intentionally narrow: only the (module, group, variant)
115
+ combinations whose canonical shape is VB-verified in the registry are
116
+ emittable. It is **not** an all-module or all-font-family emitter — each
117
+ additional vertical slice lands with its own verified evidence. See the
100
118
  [preset-cli reference](https://github.com/oaris-dev/diviops/blob/main/diviops-server/src/preset-cli/README.md)
101
119
  for the full command reference (the `src/` tree is not part of the published
102
120
  npm package — this link resolves on the repository).
@@ -39,6 +39,14 @@ test("unknown command exits 1 (invalid input)", async () => {
39
39
  assert.equal(code, EXIT.INVALID_INPUT);
40
40
  assert.match(io.stderr.join("\n"), /Unknown command/);
41
41
  });
42
+ test("--help advertises the heading-font command", async () => {
43
+ const io = capture();
44
+ const code = await run(["--help"], io);
45
+ assert.equal(code, EXIT.OK);
46
+ const help = io.stdout.join("\n");
47
+ assert.match(help, /heading-font/);
48
+ assert.match(help, /--pattern <google\|local>/);
49
+ });
42
50
  test("unknown flag exits 1 (invalid input)", async () => {
43
51
  const io = capture();
44
52
  const code = await run(["button", "--name", "X", "--bogus", "y"], io);
@@ -138,6 +146,383 @@ test("parseArgs: --radius-sync rejects values outside on|off", () => {
138
146
  const parsed = parseArgs(["button", "--name", "P", "--radius-sync", "maybe"]);
139
147
  assert.throws(() => buildButtonInput(parsed), /radius-sync must be/);
140
148
  });
149
+ // ------------------------------------------------------------------
150
+ // heading-font command — CLI integration (parse → emit → dry-run JSON)
151
+ // ------------------------------------------------------------------
152
+ test("heading-font dry-run (Pattern A) emits canonical JSON, exit 0", async () => {
153
+ const io = capture();
154
+ const code = await run([
155
+ "heading-font",
156
+ "--name",
157
+ "H1",
158
+ "--pattern",
159
+ "google",
160
+ "--font-family",
161
+ "Inter",
162
+ "--font-weight",
163
+ "700",
164
+ "--font-color",
165
+ "gcid-heading-color",
166
+ "--font-size",
167
+ "48px",
168
+ ], io);
169
+ assert.equal(code, EXIT.OK);
170
+ const parsed = JSON.parse(io.stdout.join("\n"));
171
+ assert.equal(parsed.type, "group");
172
+ assert.equal(parsed.dry_run, true);
173
+ assert.equal(parsed.module_name, "divi/heading");
174
+ assert.equal(parsed.group_name, "divi/font");
175
+ assert.equal(parsed.group_id, "designTitleText");
176
+ const value = parsed.attrs.title.decoration.font.font.desktop.value;
177
+ assert.equal(value.family, "Inter");
178
+ assert.equal(value.weight, "700");
179
+ assert.equal(value.size, "48px");
180
+ assert.equal(value.color, '$variable({"type":"color","value":{"name":"gcid-heading-color","settings":{}}})$');
181
+ });
182
+ test("heading-font dry-run (Pattern B) emits no `weight` key", async () => {
183
+ const io = capture();
184
+ const code = await run([
185
+ "heading-font",
186
+ "--name",
187
+ "H1-local",
188
+ "--pattern",
189
+ "local",
190
+ "--font-family",
191
+ "Sora 700",
192
+ "--font-color",
193
+ "gcid-heading-color",
194
+ "--font-size",
195
+ "48px",
196
+ ], io);
197
+ assert.equal(code, EXIT.OK);
198
+ const parsed = JSON.parse(io.stdout.join("\n"));
199
+ const value = parsed.attrs.title.decoration.font.font.desktop.value;
200
+ assert.equal(value.family, "Sora 700");
201
+ assert.equal("weight" in value, false, "Pattern B emits no weight key");
202
+ });
203
+ test("heading-font without --pattern exits 1 (invalid input)", async () => {
204
+ const io = capture();
205
+ const code = await run([
206
+ "heading-font",
207
+ "--name",
208
+ "H1",
209
+ "--font-family",
210
+ "Inter",
211
+ "--font-weight",
212
+ "700",
213
+ ], io);
214
+ assert.equal(code, EXIT.INVALID_INPUT);
215
+ assert.match(io.stderr.join("\n"), /--pattern/);
216
+ assert.match(io.stderr.join("\n"), /google\|local/);
217
+ });
218
+ test("heading-font without --name exits 1", async () => {
219
+ const io = capture();
220
+ const code = await run(["heading-font", "--pattern", "google", "--font-family", "Inter"], io);
221
+ assert.equal(code, EXIT.INVALID_INPUT);
222
+ assert.match(io.stderr.join("\n"), /requires --name/);
223
+ });
224
+ test("heading-font --pattern local + --font-weight is refused (exit 1)", async () => {
225
+ const io = capture();
226
+ const code = await run([
227
+ "heading-font",
228
+ "--name",
229
+ "H1-bad",
230
+ "--pattern",
231
+ "local",
232
+ "--font-family",
233
+ "Sora 700",
234
+ "--font-weight",
235
+ "700",
236
+ ], io);
237
+ assert.equal(code, EXIT.INVALID_INPUT);
238
+ assert.match(io.stderr.join("\n"), /Pattern B/);
239
+ });
240
+ test("heading-font --pattern with an invalid value exits 1", async () => {
241
+ const io = capture();
242
+ const code = await run([
243
+ "heading-font",
244
+ "--name",
245
+ "H1",
246
+ "--pattern",
247
+ "auto",
248
+ "--font-family",
249
+ "Inter",
250
+ ], io);
251
+ assert.equal(code, EXIT.INVALID_INPUT);
252
+ assert.match(io.stderr.join("\n"), /--pattern must be/);
253
+ });
254
+ test("heading-font dry-run requires no credentials and no network", async () => {
255
+ // Mirrors the dry-run-no-creds button assertion: a heading-font dry-run
256
+ // must not throw a CredentialsMissingError. Exercises AC: --apply is
257
+ // the only path that touches credentials/handshake/network.
258
+ const saved = {
259
+ WP_URL: process.env.WP_URL,
260
+ WP_USER: process.env.WP_USER,
261
+ WP_APP_PASSWORD: process.env.WP_APP_PASSWORD,
262
+ };
263
+ delete process.env.WP_URL;
264
+ delete process.env.WP_USER;
265
+ delete process.env.WP_APP_PASSWORD;
266
+ try {
267
+ const io = capture();
268
+ const code = await run([
269
+ "heading-font",
270
+ "--name",
271
+ "H1",
272
+ "--pattern",
273
+ "google",
274
+ "--font-family",
275
+ "Inter",
276
+ ], io);
277
+ assert.equal(code, EXIT.OK);
278
+ assert.equal(io.stderr.length, 0, "no error output on credential-free dry-run");
279
+ }
280
+ finally {
281
+ if (saved.WP_URL !== undefined)
282
+ process.env.WP_URL = saved.WP_URL;
283
+ if (saved.WP_USER !== undefined)
284
+ process.env.WP_USER = saved.WP_USER;
285
+ if (saved.WP_APP_PASSWORD !== undefined)
286
+ process.env.WP_APP_PASSWORD = saved.WP_APP_PASSWORD;
287
+ }
288
+ });
289
+ test("heading-font --apply without credentials exits 1 with a credentials hint", async () => {
290
+ const saved = {
291
+ WP_URL: process.env.WP_URL,
292
+ WP_USER: process.env.WP_USER,
293
+ WP_APP_PASSWORD: process.env.WP_APP_PASSWORD,
294
+ };
295
+ delete process.env.WP_URL;
296
+ delete process.env.WP_USER;
297
+ delete process.env.WP_APP_PASSWORD;
298
+ try {
299
+ const io = capture();
300
+ const code = await run([
301
+ "heading-font",
302
+ "--name",
303
+ "H1",
304
+ "--pattern",
305
+ "google",
306
+ "--font-family",
307
+ "Inter",
308
+ "--apply",
309
+ ], io);
310
+ assert.equal(code, EXIT.INVALID_INPUT);
311
+ assert.match(io.stderr.join("\n"), /requires WordPress credentials/);
312
+ }
313
+ finally {
314
+ if (saved.WP_URL !== undefined)
315
+ process.env.WP_URL = saved.WP_URL;
316
+ if (saved.WP_USER !== undefined)
317
+ process.env.WP_USER = saved.WP_USER;
318
+ if (saved.WP_APP_PASSWORD !== undefined)
319
+ process.env.WP_APP_PASSWORD = saved.WP_APP_PASSWORD;
320
+ }
321
+ });
322
+ // ------------------------------------------------------------------
323
+ // text-body-font command — CLI integration (parse → emit → dry-run JSON)
324
+ // Track 6: Pattern A only; Pattern B refused via registry-absence.
325
+ // ------------------------------------------------------------------
326
+ test("--help advertises the text-body-font command (Pattern A only)", async () => {
327
+ const io = capture();
328
+ const code = await run(["--help"], io);
329
+ assert.equal(code, EXIT.OK);
330
+ const help = io.stdout.join("\n");
331
+ assert.match(help, /text-body-font/);
332
+ assert.match(help, /Pattern A/);
333
+ });
334
+ test("text-body-font dry-run (Pattern A) emits canonical JSON, exit 0", async () => {
335
+ const io = capture();
336
+ const code = await run([
337
+ "text-body-font",
338
+ "--name",
339
+ "Body",
340
+ "--pattern",
341
+ "google",
342
+ "--font-family",
343
+ "Inter",
344
+ "--font-weight",
345
+ "400",
346
+ "--font-color",
347
+ "gcid-body-color",
348
+ "--font-size",
349
+ "16px",
350
+ ], io);
351
+ assert.equal(code, EXIT.OK);
352
+ const parsed = JSON.parse(io.stdout.join("\n"));
353
+ assert.equal(parsed.type, "group");
354
+ assert.equal(parsed.dry_run, true);
355
+ assert.equal(parsed.module_name, "divi/text");
356
+ assert.equal(parsed.group_name, "divi/font-body");
357
+ assert.equal(parsed.group_id, "designText");
358
+ const value = parsed.attrs.content.decoration.bodyFont.body.font.desktop.value;
359
+ assert.equal(value.family, "Inter");
360
+ assert.equal(value.weight, "400");
361
+ assert.equal(value.size, "16px");
362
+ assert.equal(value.color, '$variable({"type":"color","value":{"name":"gcid-body-color","settings":{}}})$');
363
+ });
364
+ test("text-body-font dry-run omits weight/size/lineHeight keys when not specified", async () => {
365
+ const io = capture();
366
+ const code = await run([
367
+ "text-body-font",
368
+ "--name",
369
+ "Body",
370
+ "--pattern",
371
+ "google",
372
+ "--font-family",
373
+ "Inter",
374
+ ], io);
375
+ assert.equal(code, EXIT.OK);
376
+ const parsed = JSON.parse(io.stdout.join("\n"));
377
+ const value = parsed.attrs.content.decoration.bodyFont.body.font.desktop.value;
378
+ assert.deepEqual(Object.keys(value), ["family"]);
379
+ });
380
+ test("text-body-font without --pattern exits 1 (invalid input)", async () => {
381
+ const io = capture();
382
+ const code = await run([
383
+ "text-body-font",
384
+ "--name",
385
+ "Body",
386
+ "--font-family",
387
+ "Inter",
388
+ "--font-weight",
389
+ "400",
390
+ ], io);
391
+ assert.equal(code, EXIT.INVALID_INPUT);
392
+ assert.match(io.stderr.join("\n"), /--pattern/);
393
+ assert.match(io.stderr.join("\n"), /google\|local/);
394
+ });
395
+ test("text-body-font without --name exits 1", async () => {
396
+ const io = capture();
397
+ const code = await run(["text-body-font", "--pattern", "google", "--font-family", "Inter"], io);
398
+ assert.equal(code, EXIT.INVALID_INPUT);
399
+ assert.match(io.stderr.join("\n"), /requires --name/);
400
+ });
401
+ test("text-body-font --pattern local is refused (registry-absence), exit 2", async () => {
402
+ // Track 6 contract: Pattern B has NO registry entry for `divi/font-body`.
403
+ // The CLI must exit non-zero. The registry-absence throw is NOT an
404
+ // EvidenceGateError (no resolution constructed) — it surfaces as a
405
+ // plain Error and the CLI's fallback branch returns INVALID_INPUT (1).
406
+ // What matters here is: (a) non-zero exit; (b) the error message names
407
+ // both family and variant so the operator can file the gap.
408
+ const io = capture();
409
+ const code = await run([
410
+ "text-body-font",
411
+ "--name",
412
+ "Body",
413
+ "--pattern",
414
+ "local",
415
+ "--font-family",
416
+ "Inter",
417
+ "--font-color",
418
+ "#666666",
419
+ ], io);
420
+ assert.notEqual(code, EXIT.OK, "refusal MUST exit non-zero");
421
+ const err = io.stderr.join("\n");
422
+ assert.match(err, /absent from verified-attrs\.json/);
423
+ assert.match(err, /divi\/font-body/);
424
+ assert.match(err, /local_hosted_pattern_b/);
425
+ });
426
+ test("text-body-font --pattern local --font-weight is ALSO refused (compound input)", async () => {
427
+ // Compound-input parity: adding --font-weight (or any other field) does
428
+ // NOT change the refusal — the gate fires on the missing variant entry
429
+ // regardless of which other fields are set.
430
+ const io = capture();
431
+ const code = await run([
432
+ "text-body-font",
433
+ "--name",
434
+ "Body",
435
+ "--pattern",
436
+ "local",
437
+ "--font-family",
438
+ "Inter",
439
+ "--font-weight",
440
+ "700",
441
+ ], io);
442
+ assert.notEqual(code, EXIT.OK);
443
+ const err = io.stderr.join("\n");
444
+ assert.match(err, /absent from verified-attrs\.json/);
445
+ assert.match(err, /local_hosted_pattern_b/);
446
+ });
447
+ test("text-body-font --pattern with an invalid value exits 1", async () => {
448
+ const io = capture();
449
+ const code = await run([
450
+ "text-body-font",
451
+ "--name",
452
+ "Body",
453
+ "--pattern",
454
+ "auto",
455
+ "--font-family",
456
+ "Inter",
457
+ ], io);
458
+ assert.equal(code, EXIT.INVALID_INPUT);
459
+ assert.match(io.stderr.join("\n"), /--pattern must be/);
460
+ });
461
+ test("text-body-font dry-run requires no credentials and no network", async () => {
462
+ const saved = {
463
+ WP_URL: process.env.WP_URL,
464
+ WP_USER: process.env.WP_USER,
465
+ WP_APP_PASSWORD: process.env.WP_APP_PASSWORD,
466
+ };
467
+ delete process.env.WP_URL;
468
+ delete process.env.WP_USER;
469
+ delete process.env.WP_APP_PASSWORD;
470
+ try {
471
+ const io = capture();
472
+ const code = await run([
473
+ "text-body-font",
474
+ "--name",
475
+ "Body",
476
+ "--pattern",
477
+ "google",
478
+ "--font-family",
479
+ "Inter",
480
+ ], io);
481
+ assert.equal(code, EXIT.OK);
482
+ assert.equal(io.stderr.length, 0, "no error output on credential-free dry-run");
483
+ }
484
+ finally {
485
+ if (saved.WP_URL !== undefined)
486
+ process.env.WP_URL = saved.WP_URL;
487
+ if (saved.WP_USER !== undefined)
488
+ process.env.WP_USER = saved.WP_USER;
489
+ if (saved.WP_APP_PASSWORD !== undefined)
490
+ process.env.WP_APP_PASSWORD = saved.WP_APP_PASSWORD;
491
+ }
492
+ });
493
+ test("text-body-font --apply without credentials exits 1 with a credentials hint", async () => {
494
+ const saved = {
495
+ WP_URL: process.env.WP_URL,
496
+ WP_USER: process.env.WP_USER,
497
+ WP_APP_PASSWORD: process.env.WP_APP_PASSWORD,
498
+ };
499
+ delete process.env.WP_URL;
500
+ delete process.env.WP_USER;
501
+ delete process.env.WP_APP_PASSWORD;
502
+ try {
503
+ const io = capture();
504
+ const code = await run([
505
+ "text-body-font",
506
+ "--name",
507
+ "Body",
508
+ "--pattern",
509
+ "google",
510
+ "--font-family",
511
+ "Inter",
512
+ "--apply",
513
+ ], io);
514
+ assert.equal(code, EXIT.INVALID_INPUT);
515
+ assert.match(io.stderr.join("\n"), /requires WordPress credentials/);
516
+ }
517
+ finally {
518
+ if (saved.WP_URL !== undefined)
519
+ process.env.WP_URL = saved.WP_URL;
520
+ if (saved.WP_USER !== undefined)
521
+ process.env.WP_USER = saved.WP_USER;
522
+ if (saved.WP_APP_PASSWORD !== undefined)
523
+ process.env.WP_APP_PASSWORD = saved.WP_APP_PASSWORD;
524
+ }
525
+ });
141
526
  test("dry-run output includes the bypass corner when requested", async () => {
142
527
  const io = capture();
143
528
  const code = await run(["button", "--name", "P", "--bg-color", "#111", "--bypass-hover-padding-gate"], io);
@@ -0,0 +1,12 @@
1
+ /**
2
+ * `divi/font` heading emitter shape + gating coverage:
3
+ * - fixture-based shape assertion against round-1a (Pattern A) and
4
+ * round-1b (Pattern B) canonical captures;
5
+ * - Pattern B no-weight discriminator: omitted `weight` produces no key;
6
+ * - Pattern B + explicit weight refused as unverified/out-of-scope;
7
+ * - variant-aware registry gating: missing applicability or under-
8
+ * verified evidence on the chosen variant throws with attr / pattern /
9
+ * effective-level / source in the message; Pattern A evidence does NOT
10
+ * vouch for Pattern B and vice versa.
11
+ */
12
+ export {};