@diviops/mcp-server 1.5.10 → 1.5.12
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 +34 -0
- package/dist/preset-cli/__tests__/button-emitter.test.d.ts +8 -0
- package/dist/preset-cli/__tests__/button-emitter.test.js +188 -0
- package/dist/preset-cli/__tests__/cli.test.d.ts +9 -0
- package/dist/preset-cli/__tests__/cli.test.js +330 -0
- package/dist/preset-cli/__tests__/heading-font-emitter.test.d.ts +12 -0
- package/dist/preset-cli/__tests__/heading-font-emitter.test.js +249 -0
- package/dist/preset-cli/__tests__/preset-create-unchanged.test.d.ts +13 -0
- package/dist/preset-cli/__tests__/preset-create-unchanged.test.js +64 -0
- package/dist/preset-cli/__tests__/registry.test.d.ts +5 -0
- package/dist/preset-cli/__tests__/registry.test.js +149 -0
- package/dist/preset-cli/__tests__/write-path.test.d.ts +8 -0
- package/dist/preset-cli/__tests__/write-path.test.js +174 -0
- package/dist/preset-cli/bin.d.ts +8 -0
- package/dist/preset-cli/bin.js +32 -0
- package/dist/preset-cli/button-emitter.d.ts +117 -0
- package/dist/preset-cli/button-emitter.js +218 -0
- package/dist/preset-cli/cli.d.ts +62 -0
- package/dist/preset-cli/cli.js +429 -0
- package/dist/preset-cli/heading-font-emitter.d.ts +128 -0
- package/dist/preset-cli/heading-font-emitter.js +166 -0
- package/dist/preset-cli/registry.d.ts +121 -0
- package/dist/preset-cli/registry.js +192 -0
- package/dist/preset-cli/variable-token.d.ts +42 -0
- package/dist/preset-cli/variable-token.js +70 -0
- package/dist/preset-cli/write-path.d.ts +74 -0
- package/dist/preset-cli/write-path.js +110 -0
- package/package.json +4 -2
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `diviops-preset` — standalone preset-emitter CLI (Track 4 scaffold).
|
|
3
|
+
*
|
|
4
|
+
* Emits byte-canonical Divi 5.5.x preset JSON, gated by the verified-attrs
|
|
5
|
+
* registry and routed through the existing storage-path contract. Track 4
|
|
6
|
+
* ships one emitter: `divi/button` group presets.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* diviops-preset button [options] Emit a divi/button group preset
|
|
10
|
+
* diviops-preset --help Show help
|
|
11
|
+
*
|
|
12
|
+
* Modes:
|
|
13
|
+
* --dry-run (default) Compose and print canonical JSON. No credentials,
|
|
14
|
+
* no handshake, no network.
|
|
15
|
+
* --apply Capability-gate + POST to /preset/create. Reuses
|
|
16
|
+
* WP_URL / WP_USER / WP_APP_PASSWORD env vars.
|
|
17
|
+
*
|
|
18
|
+
* Exit codes:
|
|
19
|
+
* 0 success
|
|
20
|
+
* 1 invalid input / usage error
|
|
21
|
+
* 2 evidence-gate refusal (attr below VB_PRESET_STORAGE_VERIFIED)
|
|
22
|
+
* 3 capability-missing (plugin lacks storage_multipath_probe_v1)
|
|
23
|
+
* 4 write / network error
|
|
24
|
+
*/
|
|
25
|
+
import { emitButtonGroupPreset, buildPresetCreateBody, } from "./button-emitter.js";
|
|
26
|
+
import { emitHeadingFontGroupPreset, buildHeadingFontPresetCreateBody, UnsupportedVariantCombinationError, } from "./heading-font-emitter.js";
|
|
27
|
+
import { EvidenceGateError } from "./registry.js";
|
|
28
|
+
import { applyButtonPreset, applyHeadingFontPreset, buildClientFromEnv, CredentialsMissingError, CapabilityMissingError, } from "./write-path.js";
|
|
29
|
+
export const EXIT = {
|
|
30
|
+
OK: 0,
|
|
31
|
+
INVALID_INPUT: 1,
|
|
32
|
+
EVIDENCE_GATE: 2,
|
|
33
|
+
CAPABILITY_MISSING: 3,
|
|
34
|
+
WRITE_ERROR: 4,
|
|
35
|
+
};
|
|
36
|
+
const realIO = {
|
|
37
|
+
out: (t) => process.stdout.write(t + "\n"),
|
|
38
|
+
err: (t) => process.stderr.write(t + "\n"),
|
|
39
|
+
};
|
|
40
|
+
const HELP = `diviops-preset — Divi 5.5.x canonical preset-emitter CLI
|
|
41
|
+
|
|
42
|
+
USAGE
|
|
43
|
+
diviops-preset button [options] Emit a divi/button group preset
|
|
44
|
+
diviops-preset heading-font [options] Emit a divi/font group preset for
|
|
45
|
+
divi/heading
|
|
46
|
+
diviops-preset --help Show this help
|
|
47
|
+
|
|
48
|
+
MODE
|
|
49
|
+
--dry-run Compose + print canonical JSON only (DEFAULT).
|
|
50
|
+
No credentials, no handshake, no network.
|
|
51
|
+
--apply Capability-gate, then POST to /preset/create.
|
|
52
|
+
Requires WP_URL / WP_USER / WP_APP_PASSWORD.
|
|
53
|
+
|
|
54
|
+
button OPTIONS (all styling fields optional; emit-on-specification only)
|
|
55
|
+
--name <string> Preset display name (required).
|
|
56
|
+
--bg-color <value> Desktop background color. Hex literal, or a
|
|
57
|
+
bare gcid-*/gvid-* token, or a $variable(...)$ token.
|
|
58
|
+
--bg-color-hover <value> Hover background color (same value forms).
|
|
59
|
+
--radius-top-left <v> Border radius corner. Any subset of the four
|
|
60
|
+
--radius-top-right <v> corners; the radius widget emits a sync flag
|
|
61
|
+
--radius-bottom-left <v> alongside any corner ("on" only when all four
|
|
62
|
+
--radius-bottom-right <v> corners are given and equal, else "off";
|
|
63
|
+
override with --radius-sync).
|
|
64
|
+
--radius-sync <on|off> Explicit radius sync flag.
|
|
65
|
+
--radius <v> Shorthand: sets all four corners to <v>.
|
|
66
|
+
--border-width <v> Outline-button border width.
|
|
67
|
+
--border-style <v> Outline-button border style (solid|dashed|...).
|
|
68
|
+
--border-color <value> Outline-button border color (same value forms).
|
|
69
|
+
--font-family <string> Button font family (literal string).
|
|
70
|
+
--font-weight <string> Button font weight (e.g. "600").
|
|
71
|
+
--font-color <value> Button font color (same value forms).
|
|
72
|
+
--font-size <v> Button font size (e.g. "16px").
|
|
73
|
+
--bypass-hover-padding-gate
|
|
74
|
+
Opt-in: emit button.decoration.button.desktop
|
|
75
|
+
.value.padding.top="0px" (hover-padding-gate
|
|
76
|
+
workaround). Off by default.
|
|
77
|
+
|
|
78
|
+
heading-font OPTIONS (all styling fields optional; emit-on-specification only)
|
|
79
|
+
--name <string> Preset display name (required).
|
|
80
|
+
--pattern <google|local> Required. Selects the verified variant:
|
|
81
|
+
google — Pattern A: plain family + explicit
|
|
82
|
+
numeric weight (e.g. family "Inter",
|
|
83
|
+
weight "700"). Verified against
|
|
84
|
+
round-1a fixture.
|
|
85
|
+
local — Pattern B: weight-encoded family
|
|
86
|
+
string (e.g. family "Sora 700") with
|
|
87
|
+
NO --font-weight. Used for local-
|
|
88
|
+
hosted/EU-GDPR font flows. Verified
|
|
89
|
+
against round-1b fixture.
|
|
90
|
+
Pattern A and Pattern B are distinct registry
|
|
91
|
+
variants — there is NO default; omitting
|
|
92
|
+
--pattern is invalid input.
|
|
93
|
+
--font-family <string> Font family. Pattern A: plain name ("Inter").
|
|
94
|
+
Pattern B: weight-encoded ("Sora 700").
|
|
95
|
+
--font-weight <string> Font weight (e.g. "700"). Pattern A only —
|
|
96
|
+
passing it with --pattern local is refused.
|
|
97
|
+
--font-color <value> Font color. Hex literal, bare gcid-*/gvid-*
|
|
98
|
+
token, or already-formed $variable(...)$ token.
|
|
99
|
+
--font-size <v> Font size (e.g. "48px").
|
|
100
|
+
--font-line-height <v> Font line-height (e.g. "1.1").
|
|
101
|
+
|
|
102
|
+
EXIT CODES
|
|
103
|
+
0 success 1 invalid input 2 evidence-gate refusal
|
|
104
|
+
3 capability missing 4 write error
|
|
105
|
+
|
|
106
|
+
EXAMPLES
|
|
107
|
+
diviops-preset button --name "Primary" --bg-color gcid-primary-color \\
|
|
108
|
+
--bg-color-hover gcid-secondary-color --radius 8px \\
|
|
109
|
+
--font-family Inter --font-weight 600 --font-color gcid-body-color
|
|
110
|
+
|
|
111
|
+
diviops-preset button --name "Primary" --bg-color "#2563eb" --apply
|
|
112
|
+
|
|
113
|
+
diviops-preset heading-font --name "Heading H1" --pattern google \\
|
|
114
|
+
--font-family Inter --font-weight 700 \\
|
|
115
|
+
--font-color gcid-heading-color --font-size 48px
|
|
116
|
+
|
|
117
|
+
diviops-preset heading-font --name "Heading H1 (local)" --pattern local \\
|
|
118
|
+
--font-family "Sora 700" \\
|
|
119
|
+
--font-color gcid-heading-color --font-size 48px
|
|
120
|
+
`;
|
|
121
|
+
const VALUE_FLAGS = new Set([
|
|
122
|
+
"--name",
|
|
123
|
+
"--bg-color",
|
|
124
|
+
"--bg-color-hover",
|
|
125
|
+
"--radius",
|
|
126
|
+
"--radius-top-left",
|
|
127
|
+
"--radius-top-right",
|
|
128
|
+
"--radius-bottom-left",
|
|
129
|
+
"--radius-bottom-right",
|
|
130
|
+
"--radius-sync",
|
|
131
|
+
"--border-width",
|
|
132
|
+
"--border-style",
|
|
133
|
+
"--border-color",
|
|
134
|
+
"--font-family",
|
|
135
|
+
"--font-weight",
|
|
136
|
+
"--font-color",
|
|
137
|
+
"--font-size",
|
|
138
|
+
"--font-line-height",
|
|
139
|
+
"--pattern",
|
|
140
|
+
]);
|
|
141
|
+
/** Parse argv (after `node script`) into a structured shape. Throws on unknown flags. */
|
|
142
|
+
export function parseArgs(argv) {
|
|
143
|
+
const parsed = {
|
|
144
|
+
command: null,
|
|
145
|
+
help: false,
|
|
146
|
+
apply: false,
|
|
147
|
+
dryRun: false,
|
|
148
|
+
options: new Map(),
|
|
149
|
+
};
|
|
150
|
+
let i = 0;
|
|
151
|
+
// First non-flag token is the command.
|
|
152
|
+
if (argv.length > 0 && !argv[0].startsWith("-")) {
|
|
153
|
+
parsed.command = argv[0];
|
|
154
|
+
i = 1;
|
|
155
|
+
}
|
|
156
|
+
for (; i < argv.length; i++) {
|
|
157
|
+
const tok = argv[i];
|
|
158
|
+
if (tok === "--help" || tok === "-h") {
|
|
159
|
+
parsed.help = true;
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (tok === "--apply") {
|
|
163
|
+
parsed.apply = true;
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if (tok === "--dry-run") {
|
|
167
|
+
parsed.dryRun = true;
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
if (tok === "--bypass-hover-padding-gate") {
|
|
171
|
+
parsed.options.set(tok, true);
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
if (VALUE_FLAGS.has(tok)) {
|
|
175
|
+
const val = argv[i + 1];
|
|
176
|
+
if (val === undefined || val.startsWith("--")) {
|
|
177
|
+
throw new UsageError(`Flag ${tok} requires a value.`);
|
|
178
|
+
}
|
|
179
|
+
parsed.options.set(tok, val);
|
|
180
|
+
i++;
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
if (!tok.startsWith("-") && parsed.command === null) {
|
|
184
|
+
parsed.command = tok;
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
throw new UsageError(`Unknown flag or argument: ${tok}`);
|
|
188
|
+
}
|
|
189
|
+
if (parsed.apply && parsed.dryRun) {
|
|
190
|
+
throw new UsageError("--apply and --dry-run are mutually exclusive.");
|
|
191
|
+
}
|
|
192
|
+
// dry-run is the default-safe mode when neither is given.
|
|
193
|
+
if (!parsed.apply)
|
|
194
|
+
parsed.dryRun = true;
|
|
195
|
+
return parsed;
|
|
196
|
+
}
|
|
197
|
+
export class UsageError extends Error {
|
|
198
|
+
constructor(message) {
|
|
199
|
+
super(message);
|
|
200
|
+
this.name = "UsageError";
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
/** Known CLI commands. The CLI rejects anything else with EXIT.INVALID_INPUT. */
|
|
204
|
+
const KNOWN_COMMANDS = new Set(["button", "heading-font"]);
|
|
205
|
+
/** Map parsed `button` options into the emitter input shape. */
|
|
206
|
+
export function buildButtonInput(parsed) {
|
|
207
|
+
const opt = (k) => {
|
|
208
|
+
const v = parsed.options.get(k);
|
|
209
|
+
return typeof v === "string" ? v : undefined;
|
|
210
|
+
};
|
|
211
|
+
const name = opt("--name");
|
|
212
|
+
if (!name) {
|
|
213
|
+
throw new UsageError("button command requires --name <string>.");
|
|
214
|
+
}
|
|
215
|
+
const input = { name };
|
|
216
|
+
const bg = opt("--bg-color");
|
|
217
|
+
if (bg !== undefined)
|
|
218
|
+
input.bg_color = bg;
|
|
219
|
+
const bgh = opt("--bg-color-hover");
|
|
220
|
+
if (bgh !== undefined)
|
|
221
|
+
input.bg_color_hover = bgh;
|
|
222
|
+
// Radius — shorthand --radius sets all four corners.
|
|
223
|
+
const radius = {};
|
|
224
|
+
const radiusAll = opt("--radius");
|
|
225
|
+
if (radiusAll !== undefined) {
|
|
226
|
+
radius.topLeft = radiusAll;
|
|
227
|
+
radius.topRight = radiusAll;
|
|
228
|
+
radius.bottomLeft = radiusAll;
|
|
229
|
+
radius.bottomRight = radiusAll;
|
|
230
|
+
}
|
|
231
|
+
const rtl = opt("--radius-top-left");
|
|
232
|
+
if (rtl !== undefined)
|
|
233
|
+
radius.topLeft = rtl;
|
|
234
|
+
const rtr = opt("--radius-top-right");
|
|
235
|
+
if (rtr !== undefined)
|
|
236
|
+
radius.topRight = rtr;
|
|
237
|
+
const rbl = opt("--radius-bottom-left");
|
|
238
|
+
if (rbl !== undefined)
|
|
239
|
+
radius.bottomLeft = rbl;
|
|
240
|
+
const rbr = opt("--radius-bottom-right");
|
|
241
|
+
if (rbr !== undefined)
|
|
242
|
+
radius.bottomRight = rbr;
|
|
243
|
+
const rsync = opt("--radius-sync");
|
|
244
|
+
if (rsync !== undefined) {
|
|
245
|
+
if (rsync !== "on" && rsync !== "off") {
|
|
246
|
+
throw new UsageError('--radius-sync must be "on" or "off".');
|
|
247
|
+
}
|
|
248
|
+
radius.sync = rsync;
|
|
249
|
+
}
|
|
250
|
+
if (Object.keys(radius).length > 0)
|
|
251
|
+
input.radius = radius;
|
|
252
|
+
// Outline border styles.
|
|
253
|
+
const border = {};
|
|
254
|
+
const bw = opt("--border-width");
|
|
255
|
+
if (bw !== undefined)
|
|
256
|
+
border.width = bw;
|
|
257
|
+
const bs = opt("--border-style");
|
|
258
|
+
if (bs !== undefined)
|
|
259
|
+
border.style = bs;
|
|
260
|
+
const bc = opt("--border-color");
|
|
261
|
+
if (bc !== undefined)
|
|
262
|
+
border.color = bc;
|
|
263
|
+
if (Object.keys(border).length > 0)
|
|
264
|
+
input.border = border;
|
|
265
|
+
// Font.
|
|
266
|
+
const font = {};
|
|
267
|
+
const ff = opt("--font-family");
|
|
268
|
+
if (ff !== undefined)
|
|
269
|
+
font.family = ff;
|
|
270
|
+
const fw = opt("--font-weight");
|
|
271
|
+
if (fw !== undefined)
|
|
272
|
+
font.weight = fw;
|
|
273
|
+
const fc = opt("--font-color");
|
|
274
|
+
if (fc !== undefined)
|
|
275
|
+
font.color = fc;
|
|
276
|
+
const fs = opt("--font-size");
|
|
277
|
+
if (fs !== undefined)
|
|
278
|
+
font.size = fs;
|
|
279
|
+
if (Object.keys(font).length > 0)
|
|
280
|
+
input.font = font;
|
|
281
|
+
if (parsed.options.get("--bypass-hover-padding-gate") === true) {
|
|
282
|
+
input.bypass_hover_padding_gate = true;
|
|
283
|
+
}
|
|
284
|
+
return input;
|
|
285
|
+
}
|
|
286
|
+
/** Map parsed `heading-font` options into the heading-font emitter input shape. */
|
|
287
|
+
export function buildHeadingFontInput(parsed) {
|
|
288
|
+
const opt = (k) => {
|
|
289
|
+
const v = parsed.options.get(k);
|
|
290
|
+
return typeof v === "string" ? v : undefined;
|
|
291
|
+
};
|
|
292
|
+
const name = opt("--name");
|
|
293
|
+
if (!name) {
|
|
294
|
+
throw new UsageError("heading-font command requires --name <string>.");
|
|
295
|
+
}
|
|
296
|
+
// --pattern is REQUIRED — Pattern A vs Pattern B are distinct registry/
|
|
297
|
+
// evidence variants and there is no safe default. An omitted --pattern
|
|
298
|
+
// is invalid input; the CLI fails BEFORE emission (no guessing).
|
|
299
|
+
const patternRaw = opt("--pattern");
|
|
300
|
+
if (patternRaw === undefined) {
|
|
301
|
+
throw new UsageError("heading-font command requires --pattern <google|local>. " +
|
|
302
|
+
"There is no default — Pattern A (google) and Pattern B (local) are distinct " +
|
|
303
|
+
"registry variants and must be selected intentionally.");
|
|
304
|
+
}
|
|
305
|
+
if (patternRaw !== "google" && patternRaw !== "local") {
|
|
306
|
+
throw new UsageError(`--pattern must be "google" or "local"; got ${JSON.stringify(patternRaw)}.`);
|
|
307
|
+
}
|
|
308
|
+
const pattern = patternRaw;
|
|
309
|
+
const input = { name, pattern };
|
|
310
|
+
const family = opt("--font-family");
|
|
311
|
+
if (family !== undefined)
|
|
312
|
+
input.family = family;
|
|
313
|
+
const weight = opt("--font-weight");
|
|
314
|
+
if (weight !== undefined)
|
|
315
|
+
input.weight = weight;
|
|
316
|
+
const color = opt("--font-color");
|
|
317
|
+
if (color !== undefined)
|
|
318
|
+
input.color = color;
|
|
319
|
+
const size = opt("--font-size");
|
|
320
|
+
if (size !== undefined)
|
|
321
|
+
input.size = size;
|
|
322
|
+
const lineHeight = opt("--font-line-height");
|
|
323
|
+
if (lineHeight !== undefined)
|
|
324
|
+
input.lineHeight = lineHeight;
|
|
325
|
+
return input;
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Run the CLI. Returns the structured exit code (does NOT call
|
|
329
|
+
* `process.exit` — the thin bin wrapper does). `io` is injectable so
|
|
330
|
+
* tests capture output without touching real stdio.
|
|
331
|
+
*/
|
|
332
|
+
export async function run(argv, io = realIO, serverVersion) {
|
|
333
|
+
let parsed;
|
|
334
|
+
try {
|
|
335
|
+
parsed = parseArgs(argv);
|
|
336
|
+
}
|
|
337
|
+
catch (err) {
|
|
338
|
+
io.err(err instanceof Error ? err.message : String(err));
|
|
339
|
+
io.err("");
|
|
340
|
+
io.err("Run `diviops-preset --help` for usage.");
|
|
341
|
+
return EXIT.INVALID_INPUT;
|
|
342
|
+
}
|
|
343
|
+
if (parsed.help || (parsed.command === null && parsed.options.size === 0)) {
|
|
344
|
+
io.out(HELP);
|
|
345
|
+
return EXIT.OK;
|
|
346
|
+
}
|
|
347
|
+
if (parsed.command === null || !KNOWN_COMMANDS.has(parsed.command)) {
|
|
348
|
+
io.err(`Unknown command: ${parsed.command ?? "(none)"}. ` +
|
|
349
|
+
`Known commands: ${[...KNOWN_COMMANDS].sort().join(", ")}.`);
|
|
350
|
+
io.err("Run `diviops-preset --help` for usage.");
|
|
351
|
+
return EXIT.INVALID_INPUT;
|
|
352
|
+
}
|
|
353
|
+
// --- compose + gate -------------------------------------------------
|
|
354
|
+
// Per-command branch produces:
|
|
355
|
+
// - `dryRunBody`: the canonical JSON to print in --dry-run mode.
|
|
356
|
+
// - `applyFn`: a closure that issues the apply-mode write when the
|
|
357
|
+
// capability check passes. Both branches reuse the existing write-
|
|
358
|
+
// path plumbing (`assertStorageCapability` → `/preset/create`).
|
|
359
|
+
let dryRunBody;
|
|
360
|
+
let applyFn;
|
|
361
|
+
try {
|
|
362
|
+
if (parsed.command === "button") {
|
|
363
|
+
const input = buildButtonInput(parsed);
|
|
364
|
+
const entry = emitButtonGroupPreset(input);
|
|
365
|
+
dryRunBody = buildPresetCreateBody(entry, { dry_run: true });
|
|
366
|
+
applyFn = (client, sv) => applyButtonPreset(client, entry, { serverVersion: sv });
|
|
367
|
+
}
|
|
368
|
+
else {
|
|
369
|
+
// heading-font
|
|
370
|
+
const input = buildHeadingFontInput(parsed);
|
|
371
|
+
const entry = emitHeadingFontGroupPreset(input);
|
|
372
|
+
dryRunBody = buildHeadingFontPresetCreateBody(entry, { dry_run: true });
|
|
373
|
+
applyFn = (client, sv) => applyHeadingFontPreset(client, entry, { serverVersion: sv });
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
catch (err) {
|
|
377
|
+
if (err instanceof EvidenceGateError) {
|
|
378
|
+
io.err(err.message);
|
|
379
|
+
return EXIT.EVIDENCE_GATE;
|
|
380
|
+
}
|
|
381
|
+
if (err instanceof UsageError) {
|
|
382
|
+
io.err(err.message);
|
|
383
|
+
io.err("Run `diviops-preset --help` for usage.");
|
|
384
|
+
return EXIT.INVALID_INPUT;
|
|
385
|
+
}
|
|
386
|
+
if (err instanceof UnsupportedVariantCombinationError) {
|
|
387
|
+
// Distinct from EvidenceGateError: the registry IS complete for the
|
|
388
|
+
// verified variants, but the caller asked for a combination outside
|
|
389
|
+
// any verified variant. Surfaces as invalid input (exit 1).
|
|
390
|
+
io.err(err.message);
|
|
391
|
+
return EXIT.INVALID_INPUT;
|
|
392
|
+
}
|
|
393
|
+
io.err(err instanceof Error ? err.message : String(err));
|
|
394
|
+
return EXIT.INVALID_INPUT;
|
|
395
|
+
}
|
|
396
|
+
// --- dry-run --------------------------------------------------------
|
|
397
|
+
if (parsed.dryRun) {
|
|
398
|
+
io.out(JSON.stringify(dryRunBody, null, 2));
|
|
399
|
+
return EXIT.OK;
|
|
400
|
+
}
|
|
401
|
+
// --- apply ----------------------------------------------------------
|
|
402
|
+
try {
|
|
403
|
+
const client = buildClientFromEnv();
|
|
404
|
+
// Apply mode requires the server version for the plugin handshake; the
|
|
405
|
+
// /handshake route gates on `mcp_server_version`. The bin entrypoint
|
|
406
|
+
// always supplies it — guard here so apply mode can never reach the
|
|
407
|
+
// handshake with an undefined/empty version.
|
|
408
|
+
if (!serverVersion) {
|
|
409
|
+
io.err("Apply mode requires the server version for the plugin handshake. " +
|
|
410
|
+
"This is an internal error — invoke via the `diviops-preset` bin.");
|
|
411
|
+
return EXIT.WRITE_ERROR;
|
|
412
|
+
}
|
|
413
|
+
const result = await applyFn(client, serverVersion);
|
|
414
|
+
io.out(JSON.stringify(result, null, 2));
|
|
415
|
+
return EXIT.OK;
|
|
416
|
+
}
|
|
417
|
+
catch (err) {
|
|
418
|
+
if (err instanceof CapabilityMissingError) {
|
|
419
|
+
io.err(err.message);
|
|
420
|
+
return EXIT.CAPABILITY_MISSING;
|
|
421
|
+
}
|
|
422
|
+
if (err instanceof CredentialsMissingError) {
|
|
423
|
+
io.err(err.message);
|
|
424
|
+
return EXIT.INVALID_INPUT;
|
|
425
|
+
}
|
|
426
|
+
io.err(`Write failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
427
|
+
return EXIT.WRITE_ERROR;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `divi/font` heading group-preset emitter — Track 5 vertical slice.
|
|
3
|
+
*
|
|
4
|
+
* Emits a byte-canonical Divi 5.5.x `type: "group"` `divi/font` preset
|
|
5
|
+
* targeting `divi/heading` at `attrs.title.decoration.font.font.desktop.value.*`,
|
|
6
|
+
* gated by the verified-attrs registry. Canonical shape:
|
|
7
|
+
* `docs/verification/canonical-preset-shapes-per-module-type-2026-05-18.md`
|
|
8
|
+
* (`divi/font` section), cross-checked against
|
|
9
|
+
* `docs/verification/evidence/canonical-shape-dumps-2026-05-18/round-1a-heading-h1-pattern-a-google.json`
|
|
10
|
+
* and `…/round-1b-heading-h1-pattern-b-local.json`.
|
|
11
|
+
*
|
|
12
|
+
* Two verified Pattern variants live in the registry — they are NOT
|
|
13
|
+
* interchangeable evidence. The caller MUST select one explicitly:
|
|
14
|
+
*
|
|
15
|
+
* - Pattern A (Google Fonts CDN): plain family + explicit numeric weight.
|
|
16
|
+
* Registry entry: `divi/font` with `pattern_variant: "google_fonts_pattern_a"`.
|
|
17
|
+
* Verified evidence: round-1a fixture.
|
|
18
|
+
* - Pattern B (local-hosted, EU-GDPR): weight encoded into the family
|
|
19
|
+
* string ("Sora 700") and NO `weight` key at all in the preset.
|
|
20
|
+
* Registry entry: `divi/font` with `pattern_variant: "local_hosted_pattern_b"`.
|
|
21
|
+
* Verified evidence: round-1b fixture.
|
|
22
|
+
*
|
|
23
|
+
* Shape rules enforced here:
|
|
24
|
+
* - Emit-on-specification only — omitted params produce NO keys.
|
|
25
|
+
* - `attrs == styleAttrs == renderAttrs` at the route layer (the plugin's
|
|
26
|
+
* `/preset/create` route mirrors `attrs` into all three buckets to match
|
|
27
|
+
* VB save semantics); the CLI request body only carries `attrs`.
|
|
28
|
+
* - Double-font nesting: `title.decoration.font.font.desktop.value.*` —
|
|
29
|
+
* outer `font` is the decoration category, inner `font` is the attr
|
|
30
|
+
* name within that category.
|
|
31
|
+
* - Pattern B with an explicit `weight` is REFUSED — the verified Pattern
|
|
32
|
+
* B capture has no `weight` key and there is no registry entry
|
|
33
|
+
* authorizing local-hosted-with-explicit-weight.
|
|
34
|
+
*/
|
|
35
|
+
import { type VerifiedAttrsRegistry } from "./registry.js";
|
|
36
|
+
export declare const HEADING_FONT_MODULE = "divi/heading";
|
|
37
|
+
export declare const HEADING_FONT_GROUP_NAME = "divi/font";
|
|
38
|
+
export declare const HEADING_FONT_GROUP_ID = "designTitleText";
|
|
39
|
+
export declare const HEADING_FONT_PATTERN_FAMILY = "divi/font";
|
|
40
|
+
/** The two verified pattern variants for `divi/font` on `divi/heading`. */
|
|
41
|
+
export declare const HEADING_FONT_PATTERN_VARIANTS: {
|
|
42
|
+
readonly google: "google_fonts_pattern_a";
|
|
43
|
+
readonly local: "local_hosted_pattern_b";
|
|
44
|
+
};
|
|
45
|
+
export type HeadingFontPattern = "google" | "local";
|
|
46
|
+
export interface HeadingFontEmitterInput {
|
|
47
|
+
/** Required display name for the preset. */
|
|
48
|
+
name: string;
|
|
49
|
+
/**
|
|
50
|
+
* Required pattern selector. There is no default — Pattern A and
|
|
51
|
+
* Pattern B are distinct registry/evidence variants and the caller
|
|
52
|
+
* must select intentionally.
|
|
53
|
+
*/
|
|
54
|
+
pattern: HeadingFontPattern;
|
|
55
|
+
/**
|
|
56
|
+
* Font family.
|
|
57
|
+
* - Pattern A (google): plain family name, e.g. `"Inter"`.
|
|
58
|
+
* - Pattern B (local): weight-encoded family name, e.g. `"Sora 700"`.
|
|
59
|
+
*/
|
|
60
|
+
family?: string;
|
|
61
|
+
/**
|
|
62
|
+
* Font weight (numeric-string, e.g. `"700"`).
|
|
63
|
+
* - Pattern A: optional; emitted when specified.
|
|
64
|
+
* - Pattern B: PROHIBITED — passing a weight in `local` mode is a
|
|
65
|
+
* usage error (the verified Pattern B shape has no `weight` key,
|
|
66
|
+
* and there is no registry entry vouching for that combination).
|
|
67
|
+
*/
|
|
68
|
+
weight?: string;
|
|
69
|
+
/** Font color — literal hex or bare/formed variable token. */
|
|
70
|
+
color?: string;
|
|
71
|
+
/** Font size — literal (e.g. `"48px"`) or already-formed `$variable(...)$`. */
|
|
72
|
+
size?: string;
|
|
73
|
+
/** Line height — optional, emit-on-specification only. */
|
|
74
|
+
lineHeight?: string;
|
|
75
|
+
}
|
|
76
|
+
/** The composed canonical preset entry shape sent to `/preset/create`. */
|
|
77
|
+
export interface HeadingFontPresetEntry {
|
|
78
|
+
type: "group";
|
|
79
|
+
module_name: string;
|
|
80
|
+
group_name: string;
|
|
81
|
+
group_id: string;
|
|
82
|
+
pattern_variant: string;
|
|
83
|
+
name: string;
|
|
84
|
+
attrs: Record<string, unknown>;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Raised when the caller passes a combination the registry does not
|
|
88
|
+
* authorize for the current Track — distinct from `EvidenceGateError`
|
|
89
|
+
* (registry-evidence shortfall) and `UsageError` (CLI arg parse).
|
|
90
|
+
*/
|
|
91
|
+
export declare class UnsupportedVariantCombinationError extends Error {
|
|
92
|
+
constructor(message: string);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Compose the canonical `attrs.title.decoration.font.font.desktop.value`
|
|
96
|
+
* bag from the input. Emit-on-specification: only specified sub-fields
|
|
97
|
+
* produce keys.
|
|
98
|
+
*/
|
|
99
|
+
export declare function composeHeadingFontAttrs(input: HeadingFontEmitterInput): Record<string, unknown>;
|
|
100
|
+
/**
|
|
101
|
+
* Emit a canonical `divi/font` heading group preset.
|
|
102
|
+
*
|
|
103
|
+
* 1. Validate input shape (name, pattern, Pattern B + weight refusal).
|
|
104
|
+
* 2. Compose `attrs.title.decoration.font.font.desktop.value.*`
|
|
105
|
+
* (emit-on-specification).
|
|
106
|
+
* 3. Gate the chosen `(divi/font, pattern_variant)` cell on `divi/heading`
|
|
107
|
+
* against the verified-attrs registry — throws `EvidenceGateError`
|
|
108
|
+
* when effective evidence is below `VB_PRESET_STORAGE_VERIFIED`.
|
|
109
|
+
*
|
|
110
|
+
* `styleAttrs` and `renderAttrs` are intentionally NOT part of this
|
|
111
|
+
* entry: the plugin's `/preset/create` route mirrors the single `attrs`
|
|
112
|
+
* bag into all three buckets, writing `attrs == styleAttrs == renderAttrs`
|
|
113
|
+
* to match VB save semantics — see `trait-preset.php` `preset_create`.
|
|
114
|
+
* The Round 1a/1b fixtures capture the post-write storage shape.
|
|
115
|
+
*/
|
|
116
|
+
export declare function emitHeadingFontGroupPreset(input: HeadingFontEmitterInput, registry?: VerifiedAttrsRegistry): HeadingFontPresetEntry;
|
|
117
|
+
/**
|
|
118
|
+
* Build the `POST /diviops/v1/preset/create` request body from a heading
|
|
119
|
+
* font preset entry. Matches the body shape the `diviops_preset_create`
|
|
120
|
+
* MCP tool posts — the CLI reuses the existing route, it does not add one.
|
|
121
|
+
*
|
|
122
|
+
* `pattern_variant` is informational metadata on the in-memory entry and
|
|
123
|
+
* is NOT sent over the wire — the registry-gate decision happens
|
|
124
|
+
* client-side before the write.
|
|
125
|
+
*/
|
|
126
|
+
export declare function buildHeadingFontPresetCreateBody(entry: HeadingFontPresetEntry, opts?: {
|
|
127
|
+
dry_run?: boolean;
|
|
128
|
+
}): Record<string, unknown>;
|