@diviops/mcp-server 1.5.12 → 1.5.14
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 +9 -0
- package/data/verified-attrs-backlog.json +5 -5
- package/data/verified-attrs.json +30 -16
- package/dist/preset-cli/__tests__/cli.test.js +461 -0
- package/dist/preset-cli/__tests__/registry.test.js +26 -0
- package/dist/preset-cli/__tests__/spacing-emitter.test.d.ts +20 -0
- package/dist/preset-cli/__tests__/spacing-emitter.test.js +409 -0
- package/dist/preset-cli/__tests__/text-body-font-emitter.test.d.ts +14 -0
- package/dist/preset-cli/__tests__/text-body-font-emitter.test.js +191 -0
- package/dist/preset-cli/__tests__/write-path.test.js +114 -1
- package/dist/preset-cli/cli.d.ts +6 -0
- package/dist/preset-cli/cli.js +217 -8
- package/dist/preset-cli/spacing-emitter.d.ts +132 -0
- package/dist/preset-cli/spacing-emitter.js +276 -0
- package/dist/preset-cli/text-body-font-emitter.d.ts +127 -0
- package/dist/preset-cli/text-body-font-emitter.js +169 -0
- package/dist/preset-cli/write-path.d.ts +32 -0
- package/dist/preset-cli/write-path.js +44 -0
- package/package.json +1 -1
|
@@ -7,9 +7,11 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { test } from "node:test";
|
|
9
9
|
import assert from "node:assert/strict";
|
|
10
|
-
import { applyButtonPreset, applyHeadingFontPreset, assertStorageCapability, buildClientFromEnv, CapabilityMissingError, CredentialsMissingError, STORAGE_CAPABILITY, PRESET_CREATE_ROUTE, } from "../write-path.js";
|
|
10
|
+
import { applyButtonPreset, applyHeadingFontPreset, applyTextBodyFontPreset, applySpacingPreset, assertStorageCapability, buildClientFromEnv, CapabilityMissingError, CredentialsMissingError, STORAGE_CAPABILITY, PRESET_CREATE_ROUTE, } from "../write-path.js";
|
|
11
11
|
import { emitButtonGroupPreset } from "../button-emitter.js";
|
|
12
12
|
import { emitHeadingFontGroupPreset } from "../heading-font-emitter.js";
|
|
13
|
+
import { emitTextBodyFontGroupPreset } from "../text-body-font-emitter.js";
|
|
14
|
+
import { emitSpacingGroupPreset } from "../spacing-emitter.js";
|
|
13
15
|
import { loadRegistry } from "../registry.js";
|
|
14
16
|
const registry = loadRegistry();
|
|
15
17
|
function handshake(capabilities) {
|
|
@@ -155,6 +157,117 @@ test("applyHeadingFontPreset threads dry_run into the body when requested", asyn
|
|
|
155
157
|
const options = client.calls[0].options;
|
|
156
158
|
assert.equal(options.body.dry_run, true);
|
|
157
159
|
});
|
|
160
|
+
// ------------------------------------------------------------------
|
|
161
|
+
// text-body-font apply-mode — mocked only (Track 6).
|
|
162
|
+
// Mirrors the heading-font apply-mode coverage. pattern_variant is
|
|
163
|
+
// in-memory metadata and must NOT appear in the wire body. Pattern B is
|
|
164
|
+
// refused at the emitter level (registry-absence) — apply-mode is never
|
|
165
|
+
// reached for `--pattern local`, so it has no mocked test here.
|
|
166
|
+
// ------------------------------------------------------------------
|
|
167
|
+
test("applyTextBodyFontPreset gates capability BEFORE issuing the write", async () => {
|
|
168
|
+
const client = mockClient({ capabilities: {} });
|
|
169
|
+
const entry = emitTextBodyFontGroupPreset({ name: "Body", pattern: "google", family: "Inter", weight: "400" }, registry);
|
|
170
|
+
await assert.rejects(() => applyTextBodyFontPreset(client, entry, {
|
|
171
|
+
serverVersion: TEST_SERVER_VERSION,
|
|
172
|
+
}), CapabilityMissingError);
|
|
173
|
+
assert.equal(client.calls.length, 0, "no write issued when capability is absent");
|
|
174
|
+
});
|
|
175
|
+
test("applyTextBodyFontPreset posts to /preset/create with the canonical body", async () => {
|
|
176
|
+
const client = mockClient({ capabilities: { [STORAGE_CAPABILITY]: true } });
|
|
177
|
+
const entry = emitTextBodyFontGroupPreset({
|
|
178
|
+
name: "Body",
|
|
179
|
+
pattern: "google",
|
|
180
|
+
family: "Inter",
|
|
181
|
+
weight: "400",
|
|
182
|
+
color: "gcid-body-color",
|
|
183
|
+
size: "16px",
|
|
184
|
+
}, registry);
|
|
185
|
+
const result = await applyTextBodyFontPreset(client, entry, {
|
|
186
|
+
serverVersion: TEST_SERVER_VERSION,
|
|
187
|
+
});
|
|
188
|
+
assert.equal(client.handshakeVersions[0], TEST_SERVER_VERSION);
|
|
189
|
+
assert.equal(client.calls.length, 1);
|
|
190
|
+
const call = client.calls[0];
|
|
191
|
+
assert.equal(call.endpoint, PRESET_CREATE_ROUTE);
|
|
192
|
+
const options = call.options;
|
|
193
|
+
assert.equal(options.method, "POST");
|
|
194
|
+
assert.equal(options.body.type, "group");
|
|
195
|
+
assert.equal(options.body.module_name, "divi/text");
|
|
196
|
+
assert.equal(options.body.group_name, "divi/font-body");
|
|
197
|
+
assert.equal(options.body.group_id, "designText");
|
|
198
|
+
assert.equal(options.body.name, "Body");
|
|
199
|
+
assert.deepEqual(options.body.attrs, entry.attrs);
|
|
200
|
+
assert.equal("pattern_variant" in options.body, false, "pattern_variant is client-side gating metadata; it must not be on the wire");
|
|
201
|
+
assert.equal("dry_run" in options.body, false);
|
|
202
|
+
assert.deepEqual(result, { ok: true, data: { preset_id: "mocked123" } });
|
|
203
|
+
});
|
|
204
|
+
test("applyTextBodyFontPreset threads dry_run into the body when requested", async () => {
|
|
205
|
+
const client = mockClient({ capabilities: { [STORAGE_CAPABILITY]: true } });
|
|
206
|
+
const entry = emitTextBodyFontGroupPreset({ name: "Body", pattern: "google", family: "Inter" }, registry);
|
|
207
|
+
await applyTextBodyFontPreset(client, entry, {
|
|
208
|
+
serverVersion: TEST_SERVER_VERSION,
|
|
209
|
+
dry_run: true,
|
|
210
|
+
});
|
|
211
|
+
const options = client.calls[0].options;
|
|
212
|
+
assert.equal(options.body.dry_run, true);
|
|
213
|
+
});
|
|
214
|
+
// ------------------------------------------------------------------
|
|
215
|
+
// applySpacingPreset — Track 7b mocked apply-mode coverage.
|
|
216
|
+
// Mirrors the text-body-font apply-mode coverage. Notably the spacing
|
|
217
|
+
// body DOES carry primary_attr_name on the wire (the section cell needs
|
|
218
|
+
// it per Track 7a load-bearing finding); the other emitters omit it.
|
|
219
|
+
// ------------------------------------------------------------------
|
|
220
|
+
test("applySpacingPreset gates capability BEFORE issuing the write", async () => {
|
|
221
|
+
const client = mockClient({ capabilities: {} });
|
|
222
|
+
const entry = emitSpacingGroupPreset({
|
|
223
|
+
name: "Sec",
|
|
224
|
+
module: "divi/section",
|
|
225
|
+
padding: { top: "40px" },
|
|
226
|
+
}, registry);
|
|
227
|
+
await assert.rejects(() => applySpacingPreset(client, entry, { serverVersion: TEST_SERVER_VERSION }), CapabilityMissingError);
|
|
228
|
+
assert.equal(client.calls.length, 0, "no write issued when capability is absent");
|
|
229
|
+
});
|
|
230
|
+
test("applySpacingPreset posts to /preset/create with the canonical body", async () => {
|
|
231
|
+
const client = mockClient({ capabilities: { [STORAGE_CAPABILITY]: true } });
|
|
232
|
+
const entry = emitSpacingGroupPreset({
|
|
233
|
+
name: "Section Rhythm",
|
|
234
|
+
module: "divi/section",
|
|
235
|
+
padding: { top: "80px", bottom: "80px" },
|
|
236
|
+
margin: { bottom: "40px" },
|
|
237
|
+
}, registry);
|
|
238
|
+
await applySpacingPreset(client, entry, {
|
|
239
|
+
serverVersion: TEST_SERVER_VERSION,
|
|
240
|
+
});
|
|
241
|
+
assert.equal(client.handshakeVersions[0], TEST_SERVER_VERSION);
|
|
242
|
+
assert.equal(client.calls.length, 1);
|
|
243
|
+
const call = client.calls[0];
|
|
244
|
+
assert.equal(call.endpoint, PRESET_CREATE_ROUTE);
|
|
245
|
+
const options = call.options;
|
|
246
|
+
assert.equal(options.method, "POST");
|
|
247
|
+
assert.equal(options.body.type, "group");
|
|
248
|
+
assert.equal(options.body.module_name, "divi/section");
|
|
249
|
+
assert.equal(options.body.group_name, "divi/spacing");
|
|
250
|
+
assert.equal(options.body.group_id, "designSpacing");
|
|
251
|
+
assert.equal(options.body.primary_attr_name, "module");
|
|
252
|
+
assert.deepEqual(options.body.attrs, entry.attrs);
|
|
253
|
+
assert.equal("styleAttrs" in options.body, false, "styleAttrs is mirrored at the route layer; CLI body carries attrs only");
|
|
254
|
+
assert.equal("renderAttrs" in options.body, false, "renderAttrs is mirrored at the route layer; CLI body carries attrs only");
|
|
255
|
+
assert.equal("dry_run" in options.body, false);
|
|
256
|
+
});
|
|
257
|
+
test("applySpacingPreset threads dry_run into the body when requested", async () => {
|
|
258
|
+
const client = mockClient({ capabilities: { [STORAGE_CAPABILITY]: true } });
|
|
259
|
+
const entry = emitSpacingGroupPreset({
|
|
260
|
+
name: "Sec",
|
|
261
|
+
module: "divi/section",
|
|
262
|
+
padding: { top: "40px" },
|
|
263
|
+
}, registry);
|
|
264
|
+
await applySpacingPreset(client, entry, {
|
|
265
|
+
serverVersion: TEST_SERVER_VERSION,
|
|
266
|
+
dry_run: true,
|
|
267
|
+
});
|
|
268
|
+
const options = client.calls[0].options;
|
|
269
|
+
assert.equal(options.body.dry_run, true);
|
|
270
|
+
});
|
|
158
271
|
test("buildClientFromEnv throws CredentialsMissingError when env vars are absent", () => {
|
|
159
272
|
assert.throws(() => buildClientFromEnv({}), (err) => {
|
|
160
273
|
assert.ok(err instanceof CredentialsMissingError);
|
package/dist/preset-cli/cli.d.ts
CHANGED
|
@@ -24,6 +24,8 @@
|
|
|
24
24
|
*/
|
|
25
25
|
import { type ButtonEmitterInput } from "./button-emitter.js";
|
|
26
26
|
import { type HeadingFontEmitterInput } from "./heading-font-emitter.js";
|
|
27
|
+
import { type TextBodyFontEmitterInput } from "./text-body-font-emitter.js";
|
|
28
|
+
import { type SpacingEmitterInput } from "./spacing-emitter.js";
|
|
27
29
|
export declare const EXIT: {
|
|
28
30
|
readonly OK: 0;
|
|
29
31
|
readonly INVALID_INPUT: 1;
|
|
@@ -53,6 +55,10 @@ export declare class UsageError extends Error {
|
|
|
53
55
|
export declare function buildButtonInput(parsed: ParsedArgs): ButtonEmitterInput;
|
|
54
56
|
/** Map parsed `heading-font` options into the heading-font emitter input shape. */
|
|
55
57
|
export declare function buildHeadingFontInput(parsed: ParsedArgs): HeadingFontEmitterInput;
|
|
58
|
+
/** Map parsed `text-body-font` options into the text-body-font emitter input shape. */
|
|
59
|
+
export declare function buildTextBodyFontInput(parsed: ParsedArgs): TextBodyFontEmitterInput;
|
|
60
|
+
/** Map parsed `spacing` options into the spacing emitter input shape. */
|
|
61
|
+
export declare function buildSpacingInput(parsed: ParsedArgs): SpacingEmitterInput;
|
|
56
62
|
/**
|
|
57
63
|
* Run the CLI. Returns the structured exit code (does NOT call
|
|
58
64
|
* `process.exit` — the thin bin wrapper does). `io` is injectable so
|
package/dist/preset-cli/cli.js
CHANGED
|
@@ -24,8 +24,10 @@
|
|
|
24
24
|
*/
|
|
25
25
|
import { emitButtonGroupPreset, buildPresetCreateBody, } from "./button-emitter.js";
|
|
26
26
|
import { emitHeadingFontGroupPreset, buildHeadingFontPresetCreateBody, UnsupportedVariantCombinationError, } from "./heading-font-emitter.js";
|
|
27
|
+
import { emitTextBodyFontGroupPreset, buildTextBodyFontPresetCreateBody, } from "./text-body-font-emitter.js";
|
|
28
|
+
import { emitSpacingGroupPreset, buildSpacingPresetCreateBody, } from "./spacing-emitter.js";
|
|
27
29
|
import { EvidenceGateError } from "./registry.js";
|
|
28
|
-
import { applyButtonPreset, applyHeadingFontPreset, buildClientFromEnv, CredentialsMissingError, CapabilityMissingError, } from "./write-path.js";
|
|
30
|
+
import { applyButtonPreset, applyHeadingFontPreset, applyTextBodyFontPreset, applySpacingPreset, buildClientFromEnv, CredentialsMissingError, CapabilityMissingError, } from "./write-path.js";
|
|
29
31
|
export const EXIT = {
|
|
30
32
|
OK: 0,
|
|
31
33
|
INVALID_INPUT: 1,
|
|
@@ -40,10 +42,14 @@ const realIO = {
|
|
|
40
42
|
const HELP = `diviops-preset — Divi 5.5.x canonical preset-emitter CLI
|
|
41
43
|
|
|
42
44
|
USAGE
|
|
43
|
-
diviops-preset button [options]
|
|
44
|
-
diviops-preset heading-font [options]
|
|
45
|
-
|
|
46
|
-
diviops-preset
|
|
45
|
+
diviops-preset button [options] Emit a divi/button group preset
|
|
46
|
+
diviops-preset heading-font [options] Emit a divi/font group preset for
|
|
47
|
+
divi/heading
|
|
48
|
+
diviops-preset text-body-font [options] Emit a divi/font-body group preset
|
|
49
|
+
for divi/text (Pattern A only)
|
|
50
|
+
diviops-preset spacing [options] Emit a divi/spacing group preset
|
|
51
|
+
(currently divi/section only)
|
|
52
|
+
diviops-preset --help Show this help
|
|
47
53
|
|
|
48
54
|
MODE
|
|
49
55
|
--dry-run Compose + print canonical JSON only (DEFAULT).
|
|
@@ -99,6 +105,59 @@ heading-font OPTIONS (all styling fields optional; emit-on-specification only)
|
|
|
99
105
|
--font-size <v> Font size (e.g. "48px").
|
|
100
106
|
--font-line-height <v> Font line-height (e.g. "1.1").
|
|
101
107
|
|
|
108
|
+
text-body-font OPTIONS (all styling fields optional; emit-on-specification only)
|
|
109
|
+
--name <string> Preset display name (required).
|
|
110
|
+
--pattern <google|local> Required. Example: --pattern google.
|
|
111
|
+
Pattern A is the only supported variant for now:
|
|
112
|
+
google — Pattern A: plain family + optional
|
|
113
|
+
numeric weight (e.g. family "Inter",
|
|
114
|
+
weight "400"). Verified against
|
|
115
|
+
round-2 body-text fixture.
|
|
116
|
+
local — Pattern B for divi/font-body is NOT
|
|
117
|
+
registered (no canonical-shape
|
|
118
|
+
capture exists yet). Selecting it
|
|
119
|
+
lands on a registry-absence refusal.
|
|
120
|
+
There is NO default; omitting --pattern is
|
|
121
|
+
invalid input.
|
|
122
|
+
--font-family <string> Font family (plain name, e.g. "Inter").
|
|
123
|
+
--font-weight <string> Font weight (e.g. "400").
|
|
124
|
+
--font-color <value> Font color. Hex literal, bare gcid-*/gvid-*
|
|
125
|
+
token, or already-formed $variable(...)$ token.
|
|
126
|
+
--font-size <v> Font size (e.g. "16px").
|
|
127
|
+
--font-line-height <v> Font line-height (e.g. "1.5").
|
|
128
|
+
|
|
129
|
+
spacing OPTIONS (sparse-emit per axis; paired sync flags per axis)
|
|
130
|
+
--name <string> Preset display name (required).
|
|
131
|
+
--module <name> Required. Currently only divi/section is wired
|
|
132
|
+
(Track 7a verified only the divi/section cell).
|
|
133
|
+
Other modules (divi/heading, divi/text,
|
|
134
|
+
divi/button, etc.) resolve to the registry gate
|
|
135
|
+
and are refused with EvidenceGateError —
|
|
136
|
+
promoting them requires a Track-7a-style
|
|
137
|
+
canonical capture PR plus a follow-up
|
|
138
|
+
implementation/docs PR (NOT a free dispatch-
|
|
139
|
+
clear via the gate alone).
|
|
140
|
+
--padding-top <v> Desktop padding corners. Pass any subset; only
|
|
141
|
+
--padding-right <v> passed corners emit (sparse-emit per axis). v1
|
|
142
|
+
--padding-bottom <v> accepts literal CSS lengths only (px / rem /
|
|
143
|
+
--padding-left <v> em / % / vw / vh) — $variable(...) / gvid-*
|
|
144
|
+
tokens are refused (deferred until canonical
|
|
145
|
+
variable-token capture lands).
|
|
146
|
+
--margin-top <v> Desktop margin corners. Same shape rules as
|
|
147
|
+
--margin-right <v> padding; padding and margin are independent —
|
|
148
|
+
--margin-bottom <v> passing only padding flags omits the margin bag
|
|
149
|
+
--margin-left <v> from the output, and vice versa.
|
|
150
|
+
--padding-sync-vertical <on|off>
|
|
151
|
+
Explicit padding sync flag (default "off").
|
|
152
|
+
Both syncVertical AND syncHorizontal always
|
|
153
|
+
--padding-sync-horizontal <on|off>
|
|
154
|
+
emit as paired siblings when the padding axis
|
|
155
|
+
has any touched corner.
|
|
156
|
+
--margin-sync-vertical <on|off>
|
|
157
|
+
Explicit margin sync flag (default "off").
|
|
158
|
+
--margin-sync-horizontal <on|off>
|
|
159
|
+
Same paired-siblings rule as padding.
|
|
160
|
+
|
|
102
161
|
EXIT CODES
|
|
103
162
|
0 success 1 invalid input 2 evidence-gate refusal
|
|
104
163
|
3 capability missing 4 write error
|
|
@@ -117,6 +176,13 @@ EXAMPLES
|
|
|
117
176
|
diviops-preset heading-font --name "Heading H1 (local)" --pattern local \\
|
|
118
177
|
--font-family "Sora 700" \\
|
|
119
178
|
--font-color gcid-heading-color --font-size 48px
|
|
179
|
+
|
|
180
|
+
diviops-preset text-body-font --name "Body Text" --pattern google \\
|
|
181
|
+
--font-family Inter --font-weight 400 \\
|
|
182
|
+
--font-color gcid-body-color --font-size 16px
|
|
183
|
+
|
|
184
|
+
diviops-preset spacing --name "Section Rhythm" --module divi/section \\
|
|
185
|
+
--padding-top 80px --padding-bottom 80px --margin-bottom 40px
|
|
120
186
|
`;
|
|
121
187
|
const VALUE_FLAGS = new Set([
|
|
122
188
|
"--name",
|
|
@@ -137,6 +203,19 @@ const VALUE_FLAGS = new Set([
|
|
|
137
203
|
"--font-size",
|
|
138
204
|
"--font-line-height",
|
|
139
205
|
"--pattern",
|
|
206
|
+
"--module",
|
|
207
|
+
"--padding-top",
|
|
208
|
+
"--padding-right",
|
|
209
|
+
"--padding-bottom",
|
|
210
|
+
"--padding-left",
|
|
211
|
+
"--margin-top",
|
|
212
|
+
"--margin-right",
|
|
213
|
+
"--margin-bottom",
|
|
214
|
+
"--margin-left",
|
|
215
|
+
"--padding-sync-vertical",
|
|
216
|
+
"--padding-sync-horizontal",
|
|
217
|
+
"--margin-sync-vertical",
|
|
218
|
+
"--margin-sync-horizontal",
|
|
140
219
|
]);
|
|
141
220
|
/** Parse argv (after `node script`) into a structured shape. Throws on unknown flags. */
|
|
142
221
|
export function parseArgs(argv) {
|
|
@@ -201,7 +280,12 @@ export class UsageError extends Error {
|
|
|
201
280
|
}
|
|
202
281
|
}
|
|
203
282
|
/** Known CLI commands. The CLI rejects anything else with EXIT.INVALID_INPUT. */
|
|
204
|
-
const KNOWN_COMMANDS = new Set([
|
|
283
|
+
const KNOWN_COMMANDS = new Set([
|
|
284
|
+
"button",
|
|
285
|
+
"heading-font",
|
|
286
|
+
"text-body-font",
|
|
287
|
+
"spacing",
|
|
288
|
+
]);
|
|
205
289
|
/** Map parsed `button` options into the emitter input shape. */
|
|
206
290
|
export function buildButtonInput(parsed) {
|
|
207
291
|
const opt = (k) => {
|
|
@@ -324,6 +408,109 @@ export function buildHeadingFontInput(parsed) {
|
|
|
324
408
|
input.lineHeight = lineHeight;
|
|
325
409
|
return input;
|
|
326
410
|
}
|
|
411
|
+
/** Map parsed `text-body-font` options into the text-body-font emitter input shape. */
|
|
412
|
+
export function buildTextBodyFontInput(parsed) {
|
|
413
|
+
const opt = (k) => {
|
|
414
|
+
const v = parsed.options.get(k);
|
|
415
|
+
return typeof v === "string" ? v : undefined;
|
|
416
|
+
};
|
|
417
|
+
const name = opt("--name");
|
|
418
|
+
if (!name) {
|
|
419
|
+
throw new UsageError("text-body-font command requires --name <string>.");
|
|
420
|
+
}
|
|
421
|
+
// --pattern is REQUIRED — there is no safe default. Pattern A only;
|
|
422
|
+
// passing "local" lands on the registry-absence refusal downstream
|
|
423
|
+
// (no Pattern B entry exists for `divi/font-body`).
|
|
424
|
+
const patternRaw = opt("--pattern");
|
|
425
|
+
if (patternRaw === undefined) {
|
|
426
|
+
throw new UsageError("text-body-font command requires --pattern <google|local>. " +
|
|
427
|
+
"Example: --pattern google. Pattern A (google) is the only " +
|
|
428
|
+
"supported variant for now; Pattern B (local) has no registry " +
|
|
429
|
+
"entry for `divi/font-body` and will be refused.");
|
|
430
|
+
}
|
|
431
|
+
if (patternRaw !== "google" && patternRaw !== "local") {
|
|
432
|
+
throw new UsageError(`--pattern must be "google" or "local"; got ${JSON.stringify(patternRaw)}.`);
|
|
433
|
+
}
|
|
434
|
+
const pattern = patternRaw;
|
|
435
|
+
const input = { name, pattern };
|
|
436
|
+
const family = opt("--font-family");
|
|
437
|
+
if (family !== undefined)
|
|
438
|
+
input.family = family;
|
|
439
|
+
const weight = opt("--font-weight");
|
|
440
|
+
if (weight !== undefined)
|
|
441
|
+
input.weight = weight;
|
|
442
|
+
const color = opt("--font-color");
|
|
443
|
+
if (color !== undefined)
|
|
444
|
+
input.color = color;
|
|
445
|
+
const size = opt("--font-size");
|
|
446
|
+
if (size !== undefined)
|
|
447
|
+
input.size = size;
|
|
448
|
+
const lineHeight = opt("--font-line-height");
|
|
449
|
+
if (lineHeight !== undefined)
|
|
450
|
+
input.lineHeight = lineHeight;
|
|
451
|
+
return input;
|
|
452
|
+
}
|
|
453
|
+
/** Parse an optional `on|off` flag value; throw a usage error otherwise. */
|
|
454
|
+
function parseOnOff(raw, flag) {
|
|
455
|
+
if (raw === undefined)
|
|
456
|
+
return undefined;
|
|
457
|
+
if (raw !== "on" && raw !== "off") {
|
|
458
|
+
throw new UsageError(`${flag} must be "on" or "off"; got ${JSON.stringify(raw)}.`);
|
|
459
|
+
}
|
|
460
|
+
return raw;
|
|
461
|
+
}
|
|
462
|
+
/** Map parsed `spacing` options into the spacing emitter input shape. */
|
|
463
|
+
export function buildSpacingInput(parsed) {
|
|
464
|
+
const opt = (k) => {
|
|
465
|
+
const v = parsed.options.get(k);
|
|
466
|
+
return typeof v === "string" ? v : undefined;
|
|
467
|
+
};
|
|
468
|
+
const name = opt("--name");
|
|
469
|
+
if (!name) {
|
|
470
|
+
throw new UsageError("spacing command requires --name <string>.");
|
|
471
|
+
}
|
|
472
|
+
const module = opt("--module");
|
|
473
|
+
if (!module) {
|
|
474
|
+
throw new UsageError("spacing command requires --module <name>. Currently only " +
|
|
475
|
+
"divi/section is wired; other modules are refused by the registry " +
|
|
476
|
+
"gate (heading/text/button cells are SCHEMA_OBSERVED).");
|
|
477
|
+
}
|
|
478
|
+
const input = { name, module };
|
|
479
|
+
// Sparse-emit at parse time too: only attach an axis bag when at least
|
|
480
|
+
// one corner OR a sync flag was passed. (Sync-flag-only input lands on
|
|
481
|
+
// the emitter's per-axis sync-without-corner refusal.) Padding and
|
|
482
|
+
// margin follow the identical shape rule, so the per-axis collection
|
|
483
|
+
// is hoisted into a single helper.
|
|
484
|
+
const buildAxis = (prefix) => {
|
|
485
|
+
const bag = {};
|
|
486
|
+
const t = opt(`--${prefix}-top`);
|
|
487
|
+
if (t !== undefined)
|
|
488
|
+
bag.top = t;
|
|
489
|
+
const r = opt(`--${prefix}-right`);
|
|
490
|
+
if (r !== undefined)
|
|
491
|
+
bag.right = r;
|
|
492
|
+
const b = opt(`--${prefix}-bottom`);
|
|
493
|
+
if (b !== undefined)
|
|
494
|
+
bag.bottom = b;
|
|
495
|
+
const l = opt(`--${prefix}-left`);
|
|
496
|
+
if (l !== undefined)
|
|
497
|
+
bag.left = l;
|
|
498
|
+
const sv = parseOnOff(opt(`--${prefix}-sync-vertical`), `--${prefix}-sync-vertical`);
|
|
499
|
+
if (sv !== undefined)
|
|
500
|
+
bag.syncVertical = sv;
|
|
501
|
+
const sh = parseOnOff(opt(`--${prefix}-sync-horizontal`), `--${prefix}-sync-horizontal`);
|
|
502
|
+
if (sh !== undefined)
|
|
503
|
+
bag.syncHorizontal = sh;
|
|
504
|
+
return Object.keys(bag).length > 0 ? bag : undefined;
|
|
505
|
+
};
|
|
506
|
+
const padding = buildAxis("padding");
|
|
507
|
+
if (padding)
|
|
508
|
+
input.padding = padding;
|
|
509
|
+
const margin = buildAxis("margin");
|
|
510
|
+
if (margin)
|
|
511
|
+
input.margin = margin;
|
|
512
|
+
return input;
|
|
513
|
+
}
|
|
327
514
|
/**
|
|
328
515
|
* Run the CLI. Returns the structured exit code (does NOT call
|
|
329
516
|
* `process.exit` — the thin bin wrapper does). `io` is injectable so
|
|
@@ -365,13 +552,35 @@ export async function run(argv, io = realIO, serverVersion) {
|
|
|
365
552
|
dryRunBody = buildPresetCreateBody(entry, { dry_run: true });
|
|
366
553
|
applyFn = (client, sv) => applyButtonPreset(client, entry, { serverVersion: sv });
|
|
367
554
|
}
|
|
368
|
-
else {
|
|
369
|
-
// heading-font
|
|
555
|
+
else if (parsed.command === "heading-font") {
|
|
370
556
|
const input = buildHeadingFontInput(parsed);
|
|
371
557
|
const entry = emitHeadingFontGroupPreset(input);
|
|
372
558
|
dryRunBody = buildHeadingFontPresetCreateBody(entry, { dry_run: true });
|
|
373
559
|
applyFn = (client, sv) => applyHeadingFontPreset(client, entry, { serverVersion: sv });
|
|
374
560
|
}
|
|
561
|
+
else if (parsed.command === "text-body-font") {
|
|
562
|
+
// text-body-font (Pattern A only; Pattern B lands on the
|
|
563
|
+
// registry-absence refusal in emitTextBodyFontGroupPreset).
|
|
564
|
+
const input = buildTextBodyFontInput(parsed);
|
|
565
|
+
const entry = emitTextBodyFontGroupPreset(input);
|
|
566
|
+
dryRunBody = buildTextBodyFontPresetCreateBody(entry, { dry_run: true });
|
|
567
|
+
applyFn = (client, sv) => applyTextBodyFontPreset(client, entry, { serverVersion: sv });
|
|
568
|
+
}
|
|
569
|
+
else if (parsed.command === "spacing") {
|
|
570
|
+
// spacing (divi/section only — other modules land on the registry
|
|
571
|
+
// gate in emitSpacingGroupPreset and surface as EvidenceGateError).
|
|
572
|
+
const input = buildSpacingInput(parsed);
|
|
573
|
+
const entry = emitSpacingGroupPreset(input);
|
|
574
|
+
dryRunBody = buildSpacingPresetCreateBody(entry, { dry_run: true });
|
|
575
|
+
applyFn = (client, sv) => applySpacingPreset(client, entry, { serverVersion: sv });
|
|
576
|
+
}
|
|
577
|
+
else {
|
|
578
|
+
// Defensive: a new entry in KNOWN_COMMANDS without a dispatch
|
|
579
|
+
// branch here would silently break dry-run/apply. parseArgs
|
|
580
|
+
// already gates on KNOWN_COMMANDS upstream, so reaching this is a
|
|
581
|
+
// programmer error in the dispatch wiring.
|
|
582
|
+
throw new UsageError(`Unhandled command: ${parsed.command}`);
|
|
583
|
+
}
|
|
375
584
|
}
|
|
376
585
|
catch (err) {
|
|
377
586
|
if (err instanceof EvidenceGateError) {
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `divi/spacing` section group-preset emitter — Track 7b vertical slice.
|
|
3
|
+
*
|
|
4
|
+
* Emits a byte-canonical Divi 5.6.0 `type: "group"` `divi/spacing` preset
|
|
5
|
+
* targeting `divi/section` at
|
|
6
|
+
* `attrs.module.decoration.spacing.desktop.value.{padding|margin}.*`,
|
|
7
|
+
* gated by the verified-attrs registry. Canonical shape:
|
|
8
|
+
* `docs/verification/evidence/canonical-shape-dumps-2026-05-23/round-5-spacing-section.json`.
|
|
9
|
+
*
|
|
10
|
+
* Scope: `divi/section` ONLY. Heading / text / button spacing cells remain
|
|
11
|
+
* `SCHEMA_OBSERVED` and resolve to `EvidenceGateError` here per
|
|
12
|
+
* `feedback_preset_map_per_module` (no cross-module shape carry-over).
|
|
13
|
+
*
|
|
14
|
+
* Shape rules enforced here (brief §4):
|
|
15
|
+
* - Sparse-emit per axis — only user-touched corners appear under
|
|
16
|
+
* `padding`/`margin`. Untouched corners are absent (not present-as-empty,
|
|
17
|
+
* not present-as-null).
|
|
18
|
+
* - Paired sync flags per axis — any spacing touch on an axis emits BOTH
|
|
19
|
+
* `syncVertical` AND `syncHorizontal` as siblings. Default `"off"` on
|
|
20
|
+
* both unless the caller explicitly toggled them.
|
|
21
|
+
* - Padding and margin are INDEPENDENT bags — passing only padding flags
|
|
22
|
+
* omits the margin bag entirely, and vice versa.
|
|
23
|
+
* - `attrs == styleAttrs == renderAttrs` at the route layer (the plugin's
|
|
24
|
+
* `/preset/create` route mirrors `attrs` into all three buckets to match
|
|
25
|
+
* VB save semantics); the CLI request body only carries `attrs`. The
|
|
26
|
+
* Track 7a fixture captures the post-write storage shape — do NOT add
|
|
27
|
+
* `styleAttrs` / `renderAttrs` keys to the emitter output.
|
|
28
|
+
* - `groupId: "designSpacing"` — the Composable Settings panel id, NOT a
|
|
29
|
+
* dotted attr path (the prior schema-inferred `module.decoration.spacing`
|
|
30
|
+
* note was a misread, corrected in PR #751).
|
|
31
|
+
* - `primaryAttrName: "module"` for section spacing.
|
|
32
|
+
* - Variable tokens (`$variable(...)` / bare `gvid-*`) in length flags are
|
|
33
|
+
* REFUSED — Track 7a capture exercised literal CSS lengths only;
|
|
34
|
+
* variable-token shape needs its own capture before this emitter writes
|
|
35
|
+
* it.
|
|
36
|
+
*/
|
|
37
|
+
import { type VerifiedAttrsRegistry } from "./registry.js";
|
|
38
|
+
export declare const SPACING_GROUP_NAME = "divi/spacing";
|
|
39
|
+
export declare const SPACING_GROUP_ID = "designSpacing";
|
|
40
|
+
export declare const SPACING_PRIMARY_ATTR_NAME = "module";
|
|
41
|
+
export declare const SPACING_PATTERN_FAMILY = "divi/spacing";
|
|
42
|
+
/**
|
|
43
|
+
* The single currently-supported module cell. The `--module` flag accepts
|
|
44
|
+
* any string and routes through the registry gate (so the surface stays
|
|
45
|
+
* stable when heading/text/button cells eventually promote), but only
|
|
46
|
+
* `divi/section` is wired with fixtures + tests + canonical shape today.
|
|
47
|
+
*
|
|
48
|
+
* Promoting another module is NOT a free dispatch-clear via the registry
|
|
49
|
+
* gate — each new cell needs a Track-7a-style canonical capture PR landing
|
|
50
|
+
* first AND a follow-up implementation/docs PR.
|
|
51
|
+
*/
|
|
52
|
+
export declare const SPACING_SUPPORTED_MODULES: readonly ["divi/section"];
|
|
53
|
+
export type SpacingCornerInput = {
|
|
54
|
+
top?: string;
|
|
55
|
+
right?: string;
|
|
56
|
+
bottom?: string;
|
|
57
|
+
left?: string;
|
|
58
|
+
syncVertical?: "on" | "off";
|
|
59
|
+
syncHorizontal?: "on" | "off";
|
|
60
|
+
};
|
|
61
|
+
export interface SpacingEmitterInput {
|
|
62
|
+
/** Required display name for the preset. */
|
|
63
|
+
name: string;
|
|
64
|
+
/**
|
|
65
|
+
* Required module selector. Forward-compat-shaped; only `divi/section` is
|
|
66
|
+
* wired today, everything else lands on `EvidenceGateError` via the
|
|
67
|
+
* registry gate (or a SCHEMA_OBSERVED cell evidence-gate refusal).
|
|
68
|
+
*/
|
|
69
|
+
module: string;
|
|
70
|
+
/** Desktop padding corner-and-sync bag. */
|
|
71
|
+
padding?: SpacingCornerInput;
|
|
72
|
+
/** Desktop margin corner-and-sync bag. */
|
|
73
|
+
margin?: SpacingCornerInput;
|
|
74
|
+
}
|
|
75
|
+
/** The composed canonical preset entry shape sent to `/preset/create`. */
|
|
76
|
+
export interface SpacingPresetEntry {
|
|
77
|
+
type: "group";
|
|
78
|
+
module_name: string;
|
|
79
|
+
group_name: string;
|
|
80
|
+
group_id: string;
|
|
81
|
+
primary_attr_name: string;
|
|
82
|
+
name: string;
|
|
83
|
+
attrs: Record<string, unknown>;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Compose the canonical
|
|
87
|
+
* `attrs.module.decoration.spacing.desktop.value.{padding|margin}` bag.
|
|
88
|
+
* Sparse-emit per axis; padding and margin are independent.
|
|
89
|
+
*
|
|
90
|
+
* Returns BOTH the full `attrs` tree (what the emitter ships) AND the
|
|
91
|
+
* inner `value` bag — the caller does empty-preset validation against
|
|
92
|
+
* `value` without re-walking the nested attrs tree (no `as any` deep
|
|
93
|
+
* casts).
|
|
94
|
+
*/
|
|
95
|
+
export declare function composeSpacingAttrs(input: SpacingEmitterInput): {
|
|
96
|
+
attrs: Record<string, unknown>;
|
|
97
|
+
value: Record<string, unknown>;
|
|
98
|
+
};
|
|
99
|
+
/**
|
|
100
|
+
* Emit a canonical `divi/spacing` section group preset.
|
|
101
|
+
*
|
|
102
|
+
* 1. Validate input shape (name, module, at least one corner across either
|
|
103
|
+
* axis).
|
|
104
|
+
* 2. Reject variable-token values in any length flag (deferred until a
|
|
105
|
+
* canonical capture lands).
|
|
106
|
+
* 3. Compose sparse-emit `attrs.module.decoration.spacing.desktop.value.*`
|
|
107
|
+
* with paired sync flags.
|
|
108
|
+
* 4. Gate `(divi/spacing, <module>)` against the verified-attrs registry —
|
|
109
|
+
* throws `EvidenceGateError` when effective evidence is below
|
|
110
|
+
* `VB_PRESET_STORAGE_VERIFIED`.
|
|
111
|
+
*
|
|
112
|
+
* `styleAttrs` and `renderAttrs` are intentionally NOT part of this entry:
|
|
113
|
+
* the plugin's `/preset/create` route mirrors the single `attrs` bag into
|
|
114
|
+
* all three buckets at write time. The Track 7a fixture captures the
|
|
115
|
+
* post-write storage shape (which is why both `attrs` and `styleAttrs`
|
|
116
|
+
* appear there byte-identical); the CLI emits the request shape only.
|
|
117
|
+
*/
|
|
118
|
+
export declare function emitSpacingGroupPreset(input: SpacingEmitterInput, registry?: VerifiedAttrsRegistry): SpacingPresetEntry;
|
|
119
|
+
/**
|
|
120
|
+
* Build the `POST /diviops/v1/preset/create` request body from a spacing
|
|
121
|
+
* preset entry. Matches the body shape the `diviops_preset_create` MCP
|
|
122
|
+
* tool posts — the CLI reuses the existing route, it does not add one.
|
|
123
|
+
*
|
|
124
|
+
* `primary_attr_name` IS sent on the wire (the plugin's `/preset/create`
|
|
125
|
+
* route accepts it as an optional snake_case param and stores it as
|
|
126
|
+
* `primaryAttrName` in the preset). Tracks 4/5/6 emitters omit it because
|
|
127
|
+
* their preset types do not carry it; the divi/spacing cell does, per the
|
|
128
|
+
* Track 7a capture's load-bearing finding #2.
|
|
129
|
+
*/
|
|
130
|
+
export declare function buildSpacingPresetCreateBody(entry: SpacingPresetEntry, opts?: {
|
|
131
|
+
dry_run?: boolean;
|
|
132
|
+
}): Record<string, unknown>;
|