@diviops/mcp-server 1.5.10 → 1.5.11

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.
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Apply-mode coverage — mocked only (no live substrate write, per #725 AC #8).
3
+ *
4
+ * Asserts: the capability gate (storage_multipath_probe_v1 present → proceed,
5
+ * absent → fail fast), the POST route + request body, and credential
6
+ * handling. The HTTP client is a stub; nothing touches the network.
7
+ */
8
+ import { test } from "node:test";
9
+ import assert from "node:assert/strict";
10
+ import { applyButtonPreset, assertStorageCapability, buildClientFromEnv, CapabilityMissingError, CredentialsMissingError, STORAGE_CAPABILITY, PRESET_CREATE_ROUTE, } from "../write-path.js";
11
+ import { emitButtonGroupPreset } from "../button-emitter.js";
12
+ import { loadRegistry } from "../registry.js";
13
+ const registry = loadRegistry();
14
+ function handshake(capabilities) {
15
+ return {
16
+ compatible: true,
17
+ plugin_version: "1.4.9",
18
+ min_server: "1.5.0",
19
+ divi: { active: true, version: "5.5.2" },
20
+ capabilities,
21
+ };
22
+ }
23
+ const TEST_SERVER_VERSION = "1.5.9";
24
+ /** A mock client recording the calls it receives. */
25
+ function mockClient(opts) {
26
+ const calls = [];
27
+ const handshakeVersions = [];
28
+ return {
29
+ calls,
30
+ handshakeVersions,
31
+ async handshake(serverVersion) {
32
+ handshakeVersions.push(serverVersion);
33
+ const hs = handshake(opts.capabilities);
34
+ if (opts.pluginVersion)
35
+ hs.plugin_version = opts.pluginVersion;
36
+ return hs;
37
+ },
38
+ async requestEnveloped(endpoint, options) {
39
+ calls.push({ endpoint, options });
40
+ return { ok: true, data: { preset_id: "mocked123" } };
41
+ },
42
+ };
43
+ }
44
+ test("assertStorageCapability proceeds when the capability is present", async () => {
45
+ const client = mockClient({ capabilities: { [STORAGE_CAPABILITY]: true } });
46
+ const hs = await assertStorageCapability(client, TEST_SERVER_VERSION);
47
+ assert.equal(hs.capabilities[STORAGE_CAPABILITY], true);
48
+ assert.deepEqual(client.handshakeVersions, [TEST_SERVER_VERSION], "the server version reaches handshake()");
49
+ assert.ok(client.handshakeVersions.every((v) => typeof v === "string" && v.length > 0), "handshake() never receives an undefined/empty server version");
50
+ });
51
+ test("assertStorageCapability fails fast when the capability is absent", async () => {
52
+ const client = mockClient({ capabilities: {}, pluginVersion: "1.4.8" });
53
+ await assert.rejects(() => assertStorageCapability(client, TEST_SERVER_VERSION), (err) => {
54
+ assert.ok(err instanceof CapabilityMissingError);
55
+ assert.equal(err.capability, STORAGE_CAPABILITY);
56
+ assert.match(err.message, /1\.4\.8/);
57
+ return true;
58
+ });
59
+ });
60
+ test("applyButtonPreset gates capability BEFORE issuing the write", async () => {
61
+ const client = mockClient({ capabilities: {} });
62
+ const entry = emitButtonGroupPreset({ name: "Primary", bg_color: "#111" }, registry);
63
+ await assert.rejects(() => applyButtonPreset(client, entry, { serverVersion: TEST_SERVER_VERSION }), CapabilityMissingError);
64
+ assert.equal(client.calls.length, 0, "no write issued when capability is absent");
65
+ });
66
+ test("applyButtonPreset posts to /preset/create with the canonical body", async () => {
67
+ const client = mockClient({ capabilities: { [STORAGE_CAPABILITY]: true } });
68
+ const entry = emitButtonGroupPreset({
69
+ name: "Primary",
70
+ bg_color: "gcid-primary-color",
71
+ bg_color_hover: "gcid-secondary-color",
72
+ radius: { topLeft: "8px", topRight: "8px", bottomLeft: "8px", bottomRight: "8px" },
73
+ font: { family: "Inter", weight: "600", color: "gcid-body-color" },
74
+ }, registry);
75
+ const result = await applyButtonPreset(client, entry, {
76
+ serverVersion: TEST_SERVER_VERSION,
77
+ });
78
+ assert.equal(client.handshakeVersions[0], TEST_SERVER_VERSION, "applyButtonPreset threads a non-empty server version into handshake()");
79
+ assert.ok((client.handshakeVersions[0] ?? "").length > 0, "handshake() server version is never empty in apply mode");
80
+ assert.equal(client.calls.length, 1, "exactly one write");
81
+ const call = client.calls[0];
82
+ assert.equal(call.endpoint, PRESET_CREATE_ROUTE, "posts to /preset/create");
83
+ const options = call.options;
84
+ assert.equal(options.method, "POST");
85
+ assert.equal(options.body.type, "group");
86
+ assert.equal(options.body.module_name, "divi/button");
87
+ assert.equal(options.body.group_name, "divi/button");
88
+ assert.equal(options.body.group_id, "button");
89
+ assert.equal(options.body.name, "Primary");
90
+ assert.deepEqual(options.body.attrs, entry.attrs);
91
+ assert.equal("dry_run" in options.body, false, "apply mode does not set dry_run");
92
+ assert.deepEqual(result, { ok: true, data: { preset_id: "mocked123" } });
93
+ });
94
+ test("applyButtonPreset threads dry_run into the body when requested", async () => {
95
+ const client = mockClient({ capabilities: { [STORAGE_CAPABILITY]: true } });
96
+ const entry = emitButtonGroupPreset({ name: "P", bg_color: "#111" }, registry);
97
+ await applyButtonPreset(client, entry, {
98
+ serverVersion: TEST_SERVER_VERSION,
99
+ dry_run: true,
100
+ });
101
+ const options = client.calls[0].options;
102
+ assert.equal(options.body.dry_run, true);
103
+ });
104
+ test("buildClientFromEnv throws CredentialsMissingError when env vars are absent", () => {
105
+ assert.throws(() => buildClientFromEnv({}), (err) => {
106
+ assert.ok(err instanceof CredentialsMissingError);
107
+ assert.match(err.message, /WP_URL/);
108
+ assert.match(err.message, /WP_USER/);
109
+ assert.match(err.message, /WP_APP_PASSWORD/);
110
+ return true;
111
+ });
112
+ });
113
+ test("buildClientFromEnv succeeds with the standard env vars", () => {
114
+ const client = buildClientFromEnv({
115
+ WP_URL: "http://divi5-ai.local",
116
+ WP_USER: "admin",
117
+ WP_APP_PASSWORD: "xxxx xxxx xxxx",
118
+ });
119
+ assert.ok(client, "WPClient constructed from WP_URL/WP_USER/WP_APP_PASSWORD");
120
+ });
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * `diviops-preset` bin entrypoint.
4
+ *
5
+ * Thin wrapper around `run()` from `cli.ts`: forwards argv, prints output,
6
+ * and translates the structured exit code into `process.exitCode`.
7
+ */
8
+ export {};
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * `diviops-preset` bin entrypoint.
4
+ *
5
+ * Thin wrapper around `run()` from `cli.ts`: forwards argv, prints output,
6
+ * and translates the structured exit code into `process.exitCode`.
7
+ */
8
+ import { readFileSync } from "fs";
9
+ import { dirname, join } from "path";
10
+ import { fileURLToPath } from "url";
11
+ import { run } from "./cli.js";
12
+ const __dirname = dirname(fileURLToPath(import.meta.url));
13
+ // Read version from package.json — same single-source-of-truth pattern as
14
+ // `src/index.ts`. Threaded into `run()` so apply-mode supplies it to the
15
+ // plugin handshake (the /handshake route requires `mcp_server_version`).
16
+ const SERVER_VERSION = (() => {
17
+ try {
18
+ const pkg = JSON.parse(readFileSync(join(__dirname, "..", "..", "package.json"), "utf-8"));
19
+ return pkg.version ?? "0.0.0";
20
+ }
21
+ catch {
22
+ return "0.0.0";
23
+ }
24
+ })();
25
+ run(process.argv.slice(2), undefined, SERVER_VERSION)
26
+ .then((code) => {
27
+ process.exitCode = code;
28
+ })
29
+ .catch((err) => {
30
+ process.stderr.write(`diviops-preset: unexpected error: ${err instanceof Error ? err.stack ?? err.message : String(err)}\n`);
31
+ process.exitCode = 4;
32
+ });
@@ -0,0 +1,117 @@
1
+ /**
2
+ * `divi/button` group-preset emitter — Track 4 vertical slice.
3
+ *
4
+ * Emits a byte-canonical Divi 5.5.x `type: "group"` button preset at
5
+ * `attrs.button.decoration.{background, border, font}`, gated by the
6
+ * verified-attrs registry. Shape canon:
7
+ * `docs/verification/canonical-preset-shapes-per-module-type-2026-05-18.md`
8
+ * (`divi/button` section), cross-checked against
9
+ * `docs/verification/evidence/canonical-shape-dumps-2026-05-18/round-3-button-canonical-complete.json`.
10
+ *
11
+ * Shape rules enforced here (brief §3):
12
+ * - Emit-on-specification only — omitted params produce NO keys.
13
+ * - Hover shape is one wrapper shallower: `background.desktop.hover.color`
14
+ * (NO `value` wrapper). Desktop is `background.desktop.value.color`.
15
+ * - Border radius vocabulary `{topLeft, topRight, bottomLeft, bottomRight,
16
+ * sync}`.
17
+ * - Font double-nesting: `font.font.desktop.value.{family, weight, color,
18
+ * size}`.
19
+ * - `$variable()` color tokens use the `{name, settings}` object shape
20
+ * with the trailing `)$`.
21
+ * - Do NOT emit `attrs.font`/`attrs.spacing` top-level, `renderAttrs`, or
22
+ * `button.decoration.button.desktop.value.*` — unless
23
+ * `bypass_hover_padding_gate: true`, which adds only
24
+ * `button.decoration.button.desktop.value.padding.top: "0px"`.
25
+ */
26
+ import { type VerifiedAttrsRegistry } from "./registry.js";
27
+ export declare const BUTTON_MODULE = "divi/button";
28
+ export declare const BUTTON_GROUP_NAME = "divi/button";
29
+ export declare const BUTTON_GROUP_ID = "button";
30
+ /** Pattern families the button emitter touches in the registry. */
31
+ export declare const BUTTON_PATTERN_FAMILIES: {
32
+ readonly background: "divi/button.background";
33
+ readonly border: "divi/button.border";
34
+ readonly font: "divi/button.font";
35
+ };
36
+ export interface ButtonRadiusInput {
37
+ topLeft?: string;
38
+ topRight?: string;
39
+ bottomLeft?: string;
40
+ bottomRight?: string;
41
+ /** Explicit override; otherwise auto-derived (see deriveRadiusSync). */
42
+ sync?: "on" | "off";
43
+ }
44
+ export interface ButtonBorderStylesInput {
45
+ width?: string;
46
+ style?: string;
47
+ color?: string;
48
+ }
49
+ export interface ButtonFontInput {
50
+ family?: string;
51
+ weight?: string;
52
+ color?: string;
53
+ size?: string;
54
+ }
55
+ export interface ButtonEmitterInput {
56
+ /** Required display name for the preset. */
57
+ name: string;
58
+ /** Desktop background color — literal hex or bare/formed variable token. */
59
+ bg_color?: string;
60
+ /** Hover background color — literal hex or bare/formed variable token. */
61
+ bg_color_hover?: string;
62
+ /** Border radius composable widget. */
63
+ radius?: ButtonRadiusInput;
64
+ /** Outline-button border styles (`styles.all`). */
65
+ border?: ButtonBorderStylesInput;
66
+ /** Button font. */
67
+ font?: ButtonFontInput;
68
+ /** Opt-in hover-padding-gate bypass corner (default false). */
69
+ bypass_hover_padding_gate?: boolean;
70
+ }
71
+ /** The composed canonical preset entry shape sent to `/preset/create`. */
72
+ export interface ButtonPresetEntry {
73
+ type: "group";
74
+ module_name: string;
75
+ group_name: string;
76
+ group_id: string;
77
+ name: string;
78
+ attrs: Record<string, unknown>;
79
+ }
80
+ /** Deep clone via structuredClone with a JSON fallback. */
81
+ export declare function deepClone<T>(value: T): T;
82
+ /**
83
+ * Compose the canonical `attrs.button.decoration.*` bag from the input.
84
+ * Emit-on-specification: only specified sub-fields produce keys.
85
+ */
86
+ export declare function composeButtonAttrs(input: ButtonEmitterInput): Record<string, unknown>;
87
+ /**
88
+ * Gate every pattern family the composed attrs touch against the registry.
89
+ * Throws `EvidenceGateError` if any touched family is below the
90
+ * `VB_PRESET_STORAGE_VERIFIED` threshold. The `divi/button` decoration
91
+ * slot (`button.decoration.button.*`) used by the bypass corner is part
92
+ * of the `divi/button.border`/baseline write surface; it is not separately
93
+ * gated because the bypass corner is an explicit opt-in workaround, not a
94
+ * styling attr — but the three styling families ARE gated.
95
+ */
96
+ export declare function gateButtonAttrs(attrs: Record<string, unknown>, registry: VerifiedAttrsRegistry): void;
97
+ /**
98
+ * Emit a canonical `divi/button` group preset.
99
+ *
100
+ * 1. Compose `attrs.button.decoration.*` (emit-on-specification).
101
+ * 2. Gate every touched styling family against the verified-attrs
102
+ * registry — throws if any is below `VB_PRESET_STORAGE_VERIFIED`.
103
+ * 3. Return the canonical preset entry. `styleAttrs` and `renderAttrs` are
104
+ * intentionally NOT part of this entry — the plugin's `/preset/create`
105
+ * route mirrors the single `attrs` bag into all three buckets, writing
106
+ * `attrs == styleAttrs == renderAttrs` to match VB save semantics
107
+ * (see `trait-preset.php` `preset_create`).
108
+ */
109
+ export declare function emitButtonGroupPreset(input: ButtonEmitterInput, registry?: VerifiedAttrsRegistry): ButtonPresetEntry;
110
+ /**
111
+ * Build the `POST /diviops/v1/preset/create` request body from a preset
112
+ * entry. Matches the body shape the `diviops_preset_create` MCP tool
113
+ * posts — the CLI reuses the existing route, it does not add one.
114
+ */
115
+ export declare function buildPresetCreateBody(entry: ButtonPresetEntry, opts?: {
116
+ dry_run?: boolean;
117
+ }): Record<string, unknown>;
@@ -0,0 +1,218 @@
1
+ /**
2
+ * `divi/button` group-preset emitter — Track 4 vertical slice.
3
+ *
4
+ * Emits a byte-canonical Divi 5.5.x `type: "group"` button preset at
5
+ * `attrs.button.decoration.{background, border, font}`, gated by the
6
+ * verified-attrs registry. Shape canon:
7
+ * `docs/verification/canonical-preset-shapes-per-module-type-2026-05-18.md`
8
+ * (`divi/button` section), cross-checked against
9
+ * `docs/verification/evidence/canonical-shape-dumps-2026-05-18/round-3-button-canonical-complete.json`.
10
+ *
11
+ * Shape rules enforced here (brief §3):
12
+ * - Emit-on-specification only — omitted params produce NO keys.
13
+ * - Hover shape is one wrapper shallower: `background.desktop.hover.color`
14
+ * (NO `value` wrapper). Desktop is `background.desktop.value.color`.
15
+ * - Border radius vocabulary `{topLeft, topRight, bottomLeft, bottomRight,
16
+ * sync}`.
17
+ * - Font double-nesting: `font.font.desktop.value.{family, weight, color,
18
+ * size}`.
19
+ * - `$variable()` color tokens use the `{name, settings}` object shape
20
+ * with the trailing `)$`.
21
+ * - Do NOT emit `attrs.font`/`attrs.spacing` top-level, `renderAttrs`, or
22
+ * `button.decoration.button.desktop.value.*` — unless
23
+ * `bypass_hover_padding_gate: true`, which adds only
24
+ * `button.decoration.button.desktop.value.padding.top: "0px"`.
25
+ */
26
+ import { loadRegistry, gateWriteAttr, } from "./registry.js";
27
+ import { normalizeColorValue } from "./variable-token.js";
28
+ export const BUTTON_MODULE = "divi/button";
29
+ export const BUTTON_GROUP_NAME = "divi/button";
30
+ export const BUTTON_GROUP_ID = "button";
31
+ /** Pattern families the button emitter touches in the registry. */
32
+ export const BUTTON_PATTERN_FAMILIES = {
33
+ background: "divi/button.background",
34
+ border: "divi/button.border",
35
+ font: "divi/button.font",
36
+ };
37
+ /**
38
+ * Derive the radius `sync` flag when the caller did not pass it explicitly.
39
+ *
40
+ * Per Rule 1a in `docs/verification/canonical-preset-shapes-per-module-type-2026-05-18.md`:
41
+ * composable-widget sync flags default to `"off"` when any sub-field is
42
+ * touched. Auto-deriving `"on"` is only correct when ALL FOUR corners are
43
+ * specified AND identical — a partial corner set (or a single corner) must
44
+ * derive `"off"`, never `"on"` (a 1-element set is trivially "all equal").
45
+ * An explicitly-passed `radius.sync` always wins.
46
+ */
47
+ function deriveRadiusSync(radius) {
48
+ if (radius.sync === "on" || radius.sync === "off")
49
+ return radius.sync;
50
+ const corners = [
51
+ radius.topLeft,
52
+ radius.topRight,
53
+ radius.bottomLeft,
54
+ radius.bottomRight,
55
+ ].filter((v) => typeof v === "string");
56
+ if (corners.length === 0)
57
+ return "off";
58
+ return corners.length === 4 && corners.every((v) => v === corners[0])
59
+ ? "on"
60
+ : "off";
61
+ }
62
+ function isPlainObject(v) {
63
+ return (!!v && typeof v === "object" && !Array.isArray(v));
64
+ }
65
+ /** Deep clone via structuredClone with a JSON fallback. */
66
+ export function deepClone(value) {
67
+ if (typeof structuredClone === "function")
68
+ return structuredClone(value);
69
+ return JSON.parse(JSON.stringify(value));
70
+ }
71
+ /**
72
+ * Compose the canonical `attrs.button.decoration.*` bag from the input.
73
+ * Emit-on-specification: only specified sub-fields produce keys.
74
+ */
75
+ export function composeButtonAttrs(input) {
76
+ const decoration = {};
77
+ // --- background -----------------------------------------------------
78
+ if (input.bg_color !== undefined || input.bg_color_hover !== undefined) {
79
+ const desktop = {};
80
+ if (input.bg_color !== undefined) {
81
+ desktop.value = { color: normalizeColorValue(input.bg_color) };
82
+ }
83
+ if (input.bg_color_hover !== undefined) {
84
+ // Hover is one wrapper shallower — NO `value` between hover and color.
85
+ desktop.hover = { color: normalizeColorValue(input.bg_color_hover) };
86
+ }
87
+ decoration.background = { desktop };
88
+ }
89
+ // --- border ---------------------------------------------------------
90
+ const borderValue = {};
91
+ if (input.radius) {
92
+ const r = input.radius;
93
+ const radius = {};
94
+ if (r.topLeft !== undefined)
95
+ radius.topLeft = r.topLeft;
96
+ if (r.topRight !== undefined)
97
+ radius.topRight = r.topRight;
98
+ if (r.bottomLeft !== undefined)
99
+ radius.bottomLeft = r.bottomLeft;
100
+ if (r.bottomRight !== undefined)
101
+ radius.bottomRight = r.bottomRight;
102
+ if (Object.keys(radius).length > 0) {
103
+ // Composable widget: `sync` emits alongside any corner.
104
+ radius.sync = deriveRadiusSync(r);
105
+ borderValue.radius = radius;
106
+ }
107
+ }
108
+ if (input.border) {
109
+ const b = input.border;
110
+ const all = {};
111
+ if (b.width !== undefined)
112
+ all.width = b.width;
113
+ if (b.style !== undefined)
114
+ all.style = b.style;
115
+ if (b.color !== undefined)
116
+ all.color = normalizeColorValue(b.color);
117
+ if (Object.keys(all).length > 0) {
118
+ borderValue.styles = { all };
119
+ }
120
+ }
121
+ if (Object.keys(borderValue).length > 0) {
122
+ decoration.border = { desktop: { value: borderValue } };
123
+ }
124
+ // --- font (double-nested font.font) --------------------------------
125
+ if (input.font) {
126
+ const f = input.font;
127
+ const value = {};
128
+ if (f.family !== undefined)
129
+ value.family = f.family;
130
+ if (f.weight !== undefined)
131
+ value.weight = f.weight;
132
+ if (f.color !== undefined)
133
+ value.color = normalizeColorValue(f.color);
134
+ if (f.size !== undefined)
135
+ value.size = f.size;
136
+ if (Object.keys(value).length > 0) {
137
+ decoration.font = { font: { desktop: { value } } };
138
+ }
139
+ }
140
+ // --- hover-padding-gate bypass (opt-in only) -----------------------
141
+ if (input.bypass_hover_padding_gate === true) {
142
+ decoration.button = { desktop: { value: { padding: { top: "0px" } } } };
143
+ }
144
+ return { button: { decoration } };
145
+ }
146
+ /**
147
+ * Gate every pattern family the composed attrs touch against the registry.
148
+ * Throws `EvidenceGateError` if any touched family is below the
149
+ * `VB_PRESET_STORAGE_VERIFIED` threshold. The `divi/button` decoration
150
+ * slot (`button.decoration.button.*`) used by the bypass corner is part
151
+ * of the `divi/button.border`/baseline write surface; it is not separately
152
+ * gated because the bypass corner is an explicit opt-in workaround, not a
153
+ * styling attr — but the three styling families ARE gated.
154
+ */
155
+ export function gateButtonAttrs(attrs, registry) {
156
+ const decoration = (isPlainObject(attrs.button) ? attrs.button.decoration : undefined);
157
+ if (!isPlainObject(decoration))
158
+ return;
159
+ if (decoration.background !== undefined) {
160
+ gateWriteAttr(registry, BUTTON_MODULE, BUTTON_PATTERN_FAMILIES.background);
161
+ }
162
+ if (decoration.border !== undefined) {
163
+ gateWriteAttr(registry, BUTTON_MODULE, BUTTON_PATTERN_FAMILIES.border);
164
+ }
165
+ if (decoration.font !== undefined) {
166
+ gateWriteAttr(registry, BUTTON_MODULE, BUTTON_PATTERN_FAMILIES.font);
167
+ }
168
+ }
169
+ /**
170
+ * Emit a canonical `divi/button` group preset.
171
+ *
172
+ * 1. Compose `attrs.button.decoration.*` (emit-on-specification).
173
+ * 2. Gate every touched styling family against the verified-attrs
174
+ * registry — throws if any is below `VB_PRESET_STORAGE_VERIFIED`.
175
+ * 3. Return the canonical preset entry. `styleAttrs` and `renderAttrs` are
176
+ * intentionally NOT part of this entry — the plugin's `/preset/create`
177
+ * route mirrors the single `attrs` bag into all three buckets, writing
178
+ * `attrs == styleAttrs == renderAttrs` to match VB save semantics
179
+ * (see `trait-preset.php` `preset_create`).
180
+ */
181
+ export function emitButtonGroupPreset(input, registry = loadRegistry()) {
182
+ if (!input.name || typeof input.name !== "string") {
183
+ throw new Error("Button emitter requires a non-empty `name`.");
184
+ }
185
+ const attrs = composeButtonAttrs(input);
186
+ const decoration = attrs.button.decoration;
187
+ if (!decoration || Object.keys(decoration).length === 0) {
188
+ throw new Error("Button emitter produced an empty preset — pass at least one of " +
189
+ "bg_color, bg_color_hover, radius, border, or font.");
190
+ }
191
+ gateButtonAttrs(attrs, registry);
192
+ return {
193
+ type: "group",
194
+ module_name: BUTTON_MODULE,
195
+ group_name: BUTTON_GROUP_NAME,
196
+ group_id: BUTTON_GROUP_ID,
197
+ name: input.name,
198
+ attrs,
199
+ };
200
+ }
201
+ /**
202
+ * Build the `POST /diviops/v1/preset/create` request body from a preset
203
+ * entry. Matches the body shape the `diviops_preset_create` MCP tool
204
+ * posts — the CLI reuses the existing route, it does not add one.
205
+ */
206
+ export function buildPresetCreateBody(entry, opts = {}) {
207
+ const body = {
208
+ module_name: entry.module_name,
209
+ name: entry.name,
210
+ attrs: entry.attrs,
211
+ type: entry.type,
212
+ group_name: entry.group_name,
213
+ group_id: entry.group_id,
214
+ };
215
+ if (opts.dry_run)
216
+ body.dry_run = true;
217
+ return body;
218
+ }
@@ -0,0 +1,59 @@
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 { type ButtonEmitterInput } from "./button-emitter.js";
26
+ export declare const EXIT: {
27
+ readonly OK: 0;
28
+ readonly INVALID_INPUT: 1;
29
+ readonly EVIDENCE_GATE: 2;
30
+ readonly CAPABILITY_MISSING: 3;
31
+ readonly WRITE_ERROR: 4;
32
+ };
33
+ export type ExitCode = (typeof EXIT)[keyof typeof EXIT];
34
+ /** Sink for CLI output — injectable so tests capture without touching real stdio. */
35
+ export interface CliIO {
36
+ out(text: string): void;
37
+ err(text: string): void;
38
+ }
39
+ interface ParsedArgs {
40
+ command: string | null;
41
+ help: boolean;
42
+ apply: boolean;
43
+ dryRun: boolean;
44
+ options: Map<string, string | true>;
45
+ }
46
+ /** Parse argv (after `node script`) into a structured shape. Throws on unknown flags. */
47
+ export declare function parseArgs(argv: string[]): ParsedArgs;
48
+ export declare class UsageError extends Error {
49
+ constructor(message: string);
50
+ }
51
+ /** Map parsed `button` options into the emitter input shape. */
52
+ export declare function buildButtonInput(parsed: ParsedArgs): ButtonEmitterInput;
53
+ /**
54
+ * Run the CLI. Returns the structured exit code (does NOT call
55
+ * `process.exit` — the thin bin wrapper does). `io` is injectable so
56
+ * tests capture output without touching real stdio.
57
+ */
58
+ export declare function run(argv: string[], io?: CliIO, serverVersion?: string): Promise<ExitCode>;
59
+ export {};