@bridge-ds/cli 0.3.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +291 -24
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { existsSync as
|
|
5
|
-
import { readFile as
|
|
6
|
-
import { isAbsolute, join as
|
|
4
|
+
import { existsSync as existsSync6 } from "node:fs";
|
|
5
|
+
import { readFile as readFile6, writeFile as writeFile5 } from "node:fs/promises";
|
|
6
|
+
import { isAbsolute, join as join6, relative as relative4, resolve as resolve3 } from "node:path";
|
|
7
7
|
|
|
8
8
|
// ../core/src/audit/types.ts
|
|
9
9
|
var AUDIT_SCHEMA_VERSION = 1;
|
|
@@ -1212,7 +1212,8 @@ function classifyTag(t, imports, ds) {
|
|
|
1212
1212
|
if (name === "a" && !t.hasHref) return {};
|
|
1213
1213
|
if (t.inRenderProp) return {};
|
|
1214
1214
|
const suggestion = nativeEquivalent(name, t.type, ds.components);
|
|
1215
|
-
|
|
1215
|
+
if (!suggestion) return {};
|
|
1216
|
+
return { component: { ...at, name, kind: "native", library: "native", suggestion } };
|
|
1216
1217
|
}
|
|
1217
1218
|
const root = name.split(".")[0];
|
|
1218
1219
|
const imp = imports.get(root);
|
|
@@ -2898,6 +2899,22 @@ function fixCss(target, ds, write) {
|
|
|
2898
2899
|
return out;
|
|
2899
2900
|
}
|
|
2900
2901
|
|
|
2902
|
+
// ../core/src/component-spec.ts
|
|
2903
|
+
var COMPONENT_SPECS_VERSION = 1;
|
|
2904
|
+
var variantKeyOf = (props) => Object.keys(props).sort().map((k) => `${k}=${props[k]}`).join(", ") || "Default";
|
|
2905
|
+
function parseComponentSpecs(text) {
|
|
2906
|
+
let data;
|
|
2907
|
+
try {
|
|
2908
|
+
data = JSON.parse(text);
|
|
2909
|
+
} catch {
|
|
2910
|
+
throw new Error("component-specs.json is not valid JSON.");
|
|
2911
|
+
}
|
|
2912
|
+
const f = data;
|
|
2913
|
+
if (!f || typeof f !== "object" || !Array.isArray(f.components)) throw new Error('component-specs.json has no "components" list.');
|
|
2914
|
+
if (f.version !== COMPONENT_SPECS_VERSION) throw new Error(`component-specs.json is version ${String(f.version)}; this tool reads version ${COMPONENT_SPECS_VERSION}.`);
|
|
2915
|
+
return f;
|
|
2916
|
+
}
|
|
2917
|
+
|
|
2901
2918
|
// src/index.ts
|
|
2902
2919
|
import { fileURLToPath } from "node:url";
|
|
2903
2920
|
|
|
@@ -3922,6 +3939,132 @@ function jiraIssueFields(r, jira, issueUrl) {
|
|
|
3922
3939
|
};
|
|
3923
3940
|
}
|
|
3924
3941
|
|
|
3942
|
+
// ../core/src/fidelity-references.ts
|
|
3943
|
+
var FIGMA_REFERENCES_DIR = "figma-references";
|
|
3944
|
+
var FIGMA_REFERENCES_MANIFEST = `${FIGMA_REFERENCES_DIR}/manifest.json`;
|
|
3945
|
+
var FIGMA_REFERENCE_STORY = "internal-figma-reference--variant";
|
|
3946
|
+
var FIGMA_FILL_WIDTH_TOKEN = "size.80";
|
|
3947
|
+
var STATE_PSEUDO = {
|
|
3948
|
+
default: "rest",
|
|
3949
|
+
rest: "rest",
|
|
3950
|
+
enabled: "rest",
|
|
3951
|
+
hover: "hover",
|
|
3952
|
+
hovered: "hover",
|
|
3953
|
+
pressed: "active",
|
|
3954
|
+
active: "active",
|
|
3955
|
+
focus: "focus-visible",
|
|
3956
|
+
focused: "focus-visible",
|
|
3957
|
+
"focus visible": "focus-visible",
|
|
3958
|
+
disabled: "disabled"
|
|
3959
|
+
};
|
|
3960
|
+
var iconName = (component) => component.replace(/^Icon\//, "");
|
|
3961
|
+
var exportName = (name) => name.replace(/[^A-Za-z0-9]+/g, "");
|
|
3962
|
+
function planReference(spec, variant, file) {
|
|
3963
|
+
if (spec.view === "popup") return { ok: false, reason: "drawn open in Figma; the code image shows it closed" };
|
|
3964
|
+
const props = { ...spec.fixed ?? {} };
|
|
3965
|
+
let pseudo;
|
|
3966
|
+
for (const axis of spec.axes) {
|
|
3967
|
+
const value = variant.props[axis.name];
|
|
3968
|
+
if (value === void 0) return { ok: false, reason: `the variant has no ${axis.name} value` };
|
|
3969
|
+
if (axis.prop) {
|
|
3970
|
+
props[axis.prop] = value === "true" ? true : value === "false" ? false : value;
|
|
3971
|
+
continue;
|
|
3972
|
+
}
|
|
3973
|
+
if (axis.name === "Checked") {
|
|
3974
|
+
props.checked = value === "true";
|
|
3975
|
+
if (value === "mixed") props.indeterminate = true;
|
|
3976
|
+
continue;
|
|
3977
|
+
}
|
|
3978
|
+
if (axis.name.toLowerCase() !== "state") return { ok: false, reason: `${axis.name} has no matching option in code` };
|
|
3979
|
+
const s = STATE_PSEUDO[value.toLowerCase()];
|
|
3980
|
+
if (!s) return { ok: false, reason: `the ${value} state has no code equivalent to render` };
|
|
3981
|
+
if (s === "disabled") props.disabled = true;
|
|
3982
|
+
else if (s !== "rest") pseudo = s;
|
|
3983
|
+
}
|
|
3984
|
+
const root = spec.parts.find((p) => !p.parent);
|
|
3985
|
+
if (!root) return { ok: false, reason: "the spec has no root layer" };
|
|
3986
|
+
const children = (id) => spec.parts.filter((p) => p.parent === id);
|
|
3987
|
+
const textOf = (part) => {
|
|
3988
|
+
const swapTo = variant.slots?.[part.id] ?? part.swap?.default;
|
|
3989
|
+
if (part.swap && swapTo) {
|
|
3990
|
+
const slot = file?.slots?.find((s) => s.name === swapTo);
|
|
3991
|
+
if (!slot) return { reason: `${swapTo} is not in the specs` };
|
|
3992
|
+
if (slot.kind !== "text" || !slot.text) return { reason: `Figma shows a placeholder (${swapTo}) that code has no example of` };
|
|
3993
|
+
return slot.text.default;
|
|
3994
|
+
}
|
|
3995
|
+
const prop = spec.properties.find((p) => p.name === part.text && p.type === "TEXT");
|
|
3996
|
+
return typeof prop?.default === "string" ? prop.default : part.name;
|
|
3997
|
+
};
|
|
3998
|
+
let text;
|
|
3999
|
+
const icons = {};
|
|
4000
|
+
for (const prop of spec.properties) {
|
|
4001
|
+
if (prop.type === "TEXT") {
|
|
4002
|
+
const hidden = spec.properties.some((b) => b.type === "BOOLEAN" && b.prop === prop.prop && b.default === false);
|
|
4003
|
+
if (hidden && prop.prop) continue;
|
|
4004
|
+
if (prop.prop === "children") text = String(prop.default);
|
|
4005
|
+
else if (prop.prop) props[prop.prop] = String(prop.default);
|
|
4006
|
+
continue;
|
|
4007
|
+
}
|
|
4008
|
+
const iconPart = spec.parts.find((p) => p.kind === "icon" && p.visible === prop.name);
|
|
4009
|
+
if (prop.default === true && !iconPart && prop.prop && props[prop.prop] === void 0 && spec.example?.p?.[prop.prop] === void 0) props[prop.prop] = true;
|
|
4010
|
+
}
|
|
4011
|
+
for (const part of spec.parts) {
|
|
4012
|
+
if (part.kind !== "icon" || !part.swap) continue;
|
|
4013
|
+
const shown = part.visible ? spec.properties.find((p) => p.name === part.visible)?.default === true : true;
|
|
4014
|
+
if (!shown) continue;
|
|
4015
|
+
if (!part.swap.prop) return { ok: false, reason: `the ${part.name} icon has no matching option in code` };
|
|
4016
|
+
icons[part.swap.prop] = iconName(variant.slots?.[part.id] ?? part.swap.default);
|
|
4017
|
+
}
|
|
4018
|
+
let parts;
|
|
4019
|
+
const loose = spec.properties.filter((p) => p.type === "TEXT" && !p.prop);
|
|
4020
|
+
const frames = children(root.id).filter((p) => p.kind === "frame");
|
|
4021
|
+
const compound = text === void 0 && frames.length > 0 && frames.every((f) => spec.parts.some((p) => p.parent === f.id && p.kind === "text" && (p.swap || p.text === f.name)));
|
|
4022
|
+
if (compound) {
|
|
4023
|
+
parts = [];
|
|
4024
|
+
for (const f of frames) {
|
|
4025
|
+
const t = spec.parts.find((p) => p.parent === f.id && p.kind === "text");
|
|
4026
|
+
const value = textOf(t);
|
|
4027
|
+
if (typeof value !== "string") return { ok: false, reason: value.reason };
|
|
4028
|
+
parts.push({ component: `${exportName(spec.name)}${exportName(f.name)}`, text: value });
|
|
4029
|
+
}
|
|
4030
|
+
}
|
|
4031
|
+
for (const p of loose) {
|
|
4032
|
+
if (p.name === "Value") continue;
|
|
4033
|
+
const part = spec.parts.find((x) => x.text === p.name);
|
|
4034
|
+
if (compound && part && part.parent !== root.id) continue;
|
|
4035
|
+
if (part && /placeholder$/.test(part.id)) {
|
|
4036
|
+
props.placeholder = String(p.default);
|
|
4037
|
+
continue;
|
|
4038
|
+
}
|
|
4039
|
+
return { ok: false, reason: `the ${p.name} text has no matching option in code` };
|
|
4040
|
+
}
|
|
4041
|
+
const example = spec.example;
|
|
4042
|
+
const exampleChildren = example?.children?.length ? example.children : void 0;
|
|
4043
|
+
return {
|
|
4044
|
+
ok: true,
|
|
4045
|
+
render: {
|
|
4046
|
+
component: exportName(spec.view || spec.fixed || example ? example?.c ?? spec.component ?? spec.name : spec.name),
|
|
4047
|
+
props: { ...example?.p ?? {}, ...props },
|
|
4048
|
+
...exampleChildren ? { children: exampleChildren } : {},
|
|
4049
|
+
...text !== void 0 && !exampleChildren ? { text } : {},
|
|
4050
|
+
...parts && !exampleChildren ? { parts } : {},
|
|
4051
|
+
...Object.keys(icons).length ? { icons } : {},
|
|
4052
|
+
...pseudo ? { pseudo } : {},
|
|
4053
|
+
...root.fill ? { width: FIGMA_FILL_WIDTH_TOKEN } : {}
|
|
4054
|
+
}
|
|
4055
|
+
};
|
|
4056
|
+
}
|
|
4057
|
+
function referenceImagePath(component, key) {
|
|
4058
|
+
const slug = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
4059
|
+
const file = key.split(",").map((kv) => slug(kv)).filter(Boolean).join("__");
|
|
4060
|
+
return `${slug(component)}/${file || "default"}.png`;
|
|
4061
|
+
}
|
|
4062
|
+
function planReferences(file) {
|
|
4063
|
+
return file.components.flatMap(
|
|
4064
|
+
(spec) => spec.variants.map((v) => ({ component: spec.name, specHash: spec.hash, key: variantKeyOf(v.props), plan: planReference(spec, v, file) }))
|
|
4065
|
+
);
|
|
4066
|
+
}
|
|
4067
|
+
|
|
3925
4068
|
// ../visual/src/capture.ts
|
|
3926
4069
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
3927
4070
|
import { dirname as dirname2 } from "node:path";
|
|
@@ -3945,8 +4088,8 @@ async function launch(o = {}) {
|
|
|
3945
4088
|
args: ["--lang=en-US", "--disable-gpu", "--font-render-hinting=none", "--disable-lcd-text", "--force-color-profile=srgb", "--hide-scrollbars", "--disable-skia-runtime-opts", "--disable-renderer-backgrounding"]
|
|
3946
4089
|
});
|
|
3947
4090
|
}
|
|
3948
|
-
async function newContext(browser, viewport) {
|
|
3949
|
-
const ctx = await browser.newContext({ viewport, deviceScaleFactor
|
|
4091
|
+
async function newContext(browser, viewport, deviceScaleFactor = 1) {
|
|
4092
|
+
const ctx = await browser.newContext({ viewport, deviceScaleFactor, reducedMotion: "reduce", colorScheme: "light", locale: "en-US", timezoneId: "UTC", serviceWorkers: "block" });
|
|
3950
4093
|
await ctx.addInitScript(INIT_SCRIPT);
|
|
3951
4094
|
await ctx.route(
|
|
3952
4095
|
/^https?:\/\/(?!127\.0\.0\.1[:/]|localhost[:/])/,
|
|
@@ -4379,9 +4522,9 @@ function selectImpact(input) {
|
|
|
4379
4522
|
app
|
|
4380
4523
|
};
|
|
4381
4524
|
}
|
|
4382
|
-
function storyId(title,
|
|
4525
|
+
function storyId(title, exportName2) {
|
|
4383
4526
|
const sanitize = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/-+/g, "-").replace(/^-+/, "").replace(/-+$/, "");
|
|
4384
|
-
const startCase =
|
|
4527
|
+
const startCase = exportName2.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/([A-Z])([A-Z][a-z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
|
|
4385
4528
|
return `${sanitize(title)}--${sanitize(startCase)}`;
|
|
4386
4529
|
}
|
|
4387
4530
|
function storiesFromSource(files) {
|
|
@@ -4896,9 +5039,105 @@ async function runImpact(o) {
|
|
|
4896
5039
|
return result;
|
|
4897
5040
|
}
|
|
4898
5041
|
|
|
5042
|
+
// ../visual/src/references.ts
|
|
5043
|
+
import { existsSync as existsSync5 } from "node:fs";
|
|
5044
|
+
import { mkdir as mkdir4, readFile as readFile4, writeFile as writeFile4 } from "node:fs/promises";
|
|
5045
|
+
import { dirname as dirname4, join as join5 } from "node:path";
|
|
5046
|
+
async function captureFigmaReferences(o) {
|
|
5047
|
+
const log = o.log ?? (() => void 0);
|
|
5048
|
+
const scale = o.scale ?? 2;
|
|
5049
|
+
const brand2 = o.brand ?? "bridge";
|
|
5050
|
+
const theme = o.theme ?? "light";
|
|
5051
|
+
if (!existsSync5(join5(o.storybook, "index.json"))) throw new Error(`No built Storybook in ${o.storybook}: run npm run build-storybook first.`);
|
|
5052
|
+
const index = JSON.parse(await readFile4(join5(o.storybook, "index.json"), "utf8"));
|
|
5053
|
+
if (!index.entries[FIGMA_REFERENCE_STORY]) throw new Error(`The Storybook build has no ${FIGMA_REFERENCE_STORY} story (packages/react/src/stories/FigmaReference.stories.tsx).`);
|
|
5054
|
+
const specs = parseComponentSpecs(await readFile4(o.specs, "utf8"));
|
|
5055
|
+
const jobs = planReferences(specs).filter((j) => !o.only?.length || o.only.includes(j.component));
|
|
5056
|
+
const manifest = {
|
|
5057
|
+
version: 1,
|
|
5058
|
+
tool: "@bridge-ds/visual",
|
|
5059
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5060
|
+
sha: o.sha,
|
|
5061
|
+
scale,
|
|
5062
|
+
mode: { brand: brand2, theme, density: "comfortable" },
|
|
5063
|
+
storyId: FIGMA_REFERENCE_STORY,
|
|
5064
|
+
components: {}
|
|
5065
|
+
};
|
|
5066
|
+
for (const j of jobs) manifest.components[j.component] ??= { specHash: j.specHash, variants: {} };
|
|
5067
|
+
let captured = 0;
|
|
5068
|
+
let skipped = 0;
|
|
5069
|
+
const served = await serveDir(o.storybook);
|
|
5070
|
+
const browser = await launch();
|
|
5071
|
+
try {
|
|
5072
|
+
const ctx = await newContext(browser, { width: 1200, height: 800 }, scale);
|
|
5073
|
+
const page = await newPage(ctx);
|
|
5074
|
+
await page.goto(storyUrl(served.url, FIGMA_REFERENCE_STORY, brand2, theme), { waitUntil: "load", timeout: 3e4 });
|
|
5075
|
+
await page.waitForSelector("[data-fidelity-root], [data-fidelity-error]", { state: "attached", timeout: 2e4 });
|
|
5076
|
+
await settle(page, { busy: false });
|
|
5077
|
+
log(`Capturing ${jobs.length} variants at ${scale}x (${brand2} ${theme})`);
|
|
5078
|
+
for (const j of jobs) {
|
|
5079
|
+
const variants = manifest.components[j.component].variants;
|
|
5080
|
+
if (!j.plan.ok) {
|
|
5081
|
+
variants[j.key] = { skipped: j.plan.reason };
|
|
5082
|
+
skipped++;
|
|
5083
|
+
continue;
|
|
5084
|
+
}
|
|
5085
|
+
const plan = JSON.stringify(j.plan.render);
|
|
5086
|
+
await page.evaluate(
|
|
5087
|
+
({ id, plan: plan2 }) => new Promise((resolve4) => {
|
|
5088
|
+
const ch = window.__STORYBOOK_ADDONS_CHANNEL__;
|
|
5089
|
+
const done = () => {
|
|
5090
|
+
clearTimeout(timer);
|
|
5091
|
+
ch.off("storyRendered", done);
|
|
5092
|
+
resolve4();
|
|
5093
|
+
};
|
|
5094
|
+
const timer = setTimeout(done, 1e4);
|
|
5095
|
+
ch.on("storyRendered", done);
|
|
5096
|
+
ch.emit("updateStoryArgs", { storyId: id, updatedArgs: { plan: plan2 } });
|
|
5097
|
+
}),
|
|
5098
|
+
{ id: FIGMA_REFERENCE_STORY, plan }
|
|
5099
|
+
);
|
|
5100
|
+
await settle(page, { network: false, quiet: 2, interval: 40, busy: false });
|
|
5101
|
+
const error = await page.locator("[data-fidelity-error]").first().textContent({ timeout: 200 }).catch(() => null);
|
|
5102
|
+
let target = page.locator("[data-fidelity-root] > *").first();
|
|
5103
|
+
if (!error && !await target.count()) {
|
|
5104
|
+
const marked = page.locator("[data-fidelity-target]").first();
|
|
5105
|
+
await marked.waitFor({ state: "visible", timeout: 3e3 }).catch(() => void 0);
|
|
5106
|
+
await settle(page, { network: false, quiet: 2, interval: 40, busy: false });
|
|
5107
|
+
target = marked;
|
|
5108
|
+
}
|
|
5109
|
+
if (error || !await target.count()) {
|
|
5110
|
+
variants[j.key] = { skipped: error ?? "the code example did not render" };
|
|
5111
|
+
skipped++;
|
|
5112
|
+
continue;
|
|
5113
|
+
}
|
|
5114
|
+
const file = referenceImagePath(j.component, j.key);
|
|
5115
|
+
const path = join5(o.out, file);
|
|
5116
|
+
await mkdir4(dirname4(path), { recursive: true });
|
|
5117
|
+
await target.screenshot({ path, omitBackground: true, animations: "disabled", caret: "hide", scale: "device" });
|
|
5118
|
+
const box = await target.boundingBox();
|
|
5119
|
+
variants[j.key] = { file, width: Math.round(box?.width ?? 0), height: Math.round(box?.height ?? 0) };
|
|
5120
|
+
captured++;
|
|
5121
|
+
}
|
|
5122
|
+
await ctx.close();
|
|
5123
|
+
} finally {
|
|
5124
|
+
await browser.close().catch(() => void 0);
|
|
5125
|
+
await served.close();
|
|
5126
|
+
}
|
|
5127
|
+
await mkdir4(o.out, { recursive: true });
|
|
5128
|
+
const manifestPath = join5(o.out, "manifest.json");
|
|
5129
|
+
await writeFile4(manifestPath, `${JSON.stringify(manifest, null, 2)}
|
|
5130
|
+
`);
|
|
5131
|
+
log(`${captured} images, ${skipped} variants skipped. ${manifestPath}`);
|
|
5132
|
+
return { manifest, manifestPath, captured, skipped };
|
|
5133
|
+
}
|
|
5134
|
+
|
|
5135
|
+
// src/index.ts
|
|
5136
|
+
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
5137
|
+
|
|
4899
5138
|
// src/request.ts
|
|
4900
5139
|
import { execFileSync, spawnSync } from "node:child_process";
|
|
4901
|
-
import { readFile as
|
|
5140
|
+
import { readFile as readFile5 } from "node:fs/promises";
|
|
4902
5141
|
import { BridgeContext as BridgeContext2, requests as requests2 } from "@bridge-ds/mcp";
|
|
4903
5142
|
|
|
4904
5143
|
// src/jira-sync.ts
|
|
@@ -5172,7 +5411,7 @@ ${r.issue.body}`);
|
|
|
5172
5411
|
let event = {};
|
|
5173
5412
|
if (eventPath) {
|
|
5174
5413
|
try {
|
|
5175
|
-
event = JSON.parse(await
|
|
5414
|
+
event = JSON.parse(await readFile5(eventPath, "utf8"));
|
|
5176
5415
|
} catch {
|
|
5177
5416
|
throw new RequestUsageError(`Could not read the GitHub event at ${eventPath}.`);
|
|
5178
5417
|
}
|
|
@@ -5364,6 +5603,11 @@ Usage:
|
|
|
5364
5603
|
visual impact: build Storybook and the app at both refs, screenshot
|
|
5365
5604
|
what the change touches, pixel-diff, write manifest.json + images.
|
|
5366
5605
|
Exit 0 even with diffs (2 on setup errors). Needs Chrome installed.
|
|
5606
|
+
bridge figma-references [--storybook storybook-static] [--specs packages/react/component-specs.json]
|
|
5607
|
+
[--out figma-references] [--sha <sha>] [--only Button,Card] [--json]
|
|
5608
|
+
code images for the Figma fidelity check: one PNG per generated
|
|
5609
|
+
Figma variant from a built Storybook, plus manifest.json.
|
|
5610
|
+
Needs Chrome installed
|
|
5367
5611
|
bridge history [token] [--json] [--limit n] [--author a] [--tier n] [--version v]
|
|
5368
5612
|
token history: versions, merged changes, who, when, why
|
|
5369
5613
|
bridge revert <pr|commit|id> [--dry-run] [--restore-conflicts] [--use-before path@file] [--json]
|
|
@@ -5430,9 +5674,9 @@ var print = (s) => process.stdout.write(`${s}
|
|
|
5430
5674
|
`);
|
|
5431
5675
|
var modeArg = (s) => s ? Object.fromEntries(s.split(",").map((kv) => kv.split("="))) : void 0;
|
|
5432
5676
|
async function init(a, root) {
|
|
5433
|
-
const cfgPath =
|
|
5677
|
+
const cfgPath = join6(root, CONFIG_FILE2);
|
|
5434
5678
|
let config;
|
|
5435
|
-
if (
|
|
5679
|
+
if (existsSync6(cfgPath) && !has3(a, "force")) {
|
|
5436
5680
|
print(`${CONFIG_FILE2} exists, keeping it (--force to regenerate).`);
|
|
5437
5681
|
config = (await (await BridgeContext4.create({ root })).config()).config;
|
|
5438
5682
|
} else {
|
|
@@ -5440,7 +5684,7 @@ async function init(a, root) {
|
|
|
5440
5684
|
const files = await src.list("");
|
|
5441
5685
|
const repo = await originRepo(root);
|
|
5442
5686
|
config = await detectConfig(files, (p) => src.read(p), repo ? { repo } : {});
|
|
5443
|
-
await
|
|
5687
|
+
await writeFile5(cfgPath, `${JSON.stringify(toConfigJson(config, "https://raw.githubusercontent.com/peterbono/ai-design-system-demo/main/packages/core/bridge.config.schema.json"), null, 2)}
|
|
5444
5688
|
`);
|
|
5445
5689
|
print(`Wrote ${CONFIG_FILE2}: tokens in ${config.tokens.root}, ${config.tokens.layers.length} layers, modes ${Object.keys(config.axes).join(", ") || "none"}.`);
|
|
5446
5690
|
print("Check the layers order (lowest first) and the tiers; detection is a starting point.");
|
|
@@ -5449,10 +5693,10 @@ async function init(a, root) {
|
|
|
5449
5693
|
const relServer = relative4(root, serverJs);
|
|
5450
5694
|
const inRepo = !relServer.startsWith("..") && !isAbsolute(relServer);
|
|
5451
5695
|
const server = (dir) => inRepo ? { command: "node", args: [relServer, "--root", dir] } : { command: "npx", args: ["-y", MCP_PACKAGE, "--root", dir] };
|
|
5452
|
-
const mcpPath =
|
|
5453
|
-
const existing =
|
|
5696
|
+
const mcpPath = join6(root, ".mcp.json");
|
|
5697
|
+
const existing = existsSync6(mcpPath) ? JSON.parse(await readFile6(mcpPath, "utf8")) : {};
|
|
5454
5698
|
existing.mcpServers = { ...existing.mcpServers ?? {}, bridge: server(".") };
|
|
5455
|
-
await
|
|
5699
|
+
await writeFile5(mcpPath, `${JSON.stringify(existing, null, 2)}
|
|
5456
5700
|
`);
|
|
5457
5701
|
print(`Wrote .mcp.json: Claude Code picks up the "bridge" server next time it starts in ${root}.`);
|
|
5458
5702
|
const desktop = inRepo ? { command: "node", args: [serverJs, "--root", root] } : server(root);
|
|
@@ -5575,7 +5819,7 @@ async function propose(a, root) {
|
|
|
5575
5819
|
edits.push({ op: "set", path: s.slice(0, i), value: s.slice(i + 1), ...file ? { file } : {}, ...mode2 ? { mode: mode2 } : {} });
|
|
5576
5820
|
}
|
|
5577
5821
|
const editsFile = flag3(a, "edits");
|
|
5578
|
-
if (editsFile) edits.push(...JSON.parse(await
|
|
5822
|
+
if (editsFile) edits.push(...JSON.parse(await readFile6(resolve3(editsFile), "utf8")));
|
|
5579
5823
|
if (!edits.length) throw new UsageError("Give at least one --set path=value or --edits file.json");
|
|
5580
5824
|
const ctx = await BridgeContext4.create({ root });
|
|
5581
5825
|
const summary = flag3(a, "summary");
|
|
@@ -5641,6 +5885,27 @@ async function brandCmd(a, root) {
|
|
|
5641
5885
|
if (r.hint) print(String(r.hint));
|
|
5642
5886
|
return r.status === "blocked" ? 1 : 0;
|
|
5643
5887
|
}
|
|
5888
|
+
async function figmaReferences(a, root) {
|
|
5889
|
+
let sha = flag3(a, "sha");
|
|
5890
|
+
if (!sha) {
|
|
5891
|
+
try {
|
|
5892
|
+
sha = execFileSync2("git", ["rev-parse", "HEAD"], { cwd: root, encoding: "utf8" }).trim();
|
|
5893
|
+
} catch {
|
|
5894
|
+
sha = "unknown";
|
|
5895
|
+
}
|
|
5896
|
+
}
|
|
5897
|
+
const only = (flag3(a, "only") ?? "").split(",").filter(Boolean);
|
|
5898
|
+
const r = await captureFigmaReferences({
|
|
5899
|
+
storybook: resolve3(root, flag3(a, "storybook") ?? "storybook-static"),
|
|
5900
|
+
specs: resolve3(root, flag3(a, "specs") ?? "packages/react/component-specs.json"),
|
|
5901
|
+
out: resolve3(root, flag3(a, "out") ?? "figma-references"),
|
|
5902
|
+
sha,
|
|
5903
|
+
...only.length ? { only } : {},
|
|
5904
|
+
log: has3(a, "json") ? void 0 : (line) => print(line)
|
|
5905
|
+
});
|
|
5906
|
+
if (has3(a, "json")) print(JSON.stringify({ manifest: r.manifestPath, captured: r.captured, skipped: r.skipped }, null, 2));
|
|
5907
|
+
return 0;
|
|
5908
|
+
}
|
|
5644
5909
|
async function impact(a, root) {
|
|
5645
5910
|
const only = (flag3(a, "only") ?? "").split(",").filter(Boolean);
|
|
5646
5911
|
for (const x of only) if (x !== "stories" && x !== "app") throw new UsageError(`--only takes stories or app, got "${x}"`);
|
|
@@ -5750,8 +6015,8 @@ ${r.prBody}`);
|
|
|
5750
6015
|
async function audit(a, root) {
|
|
5751
6016
|
const target = a._[1];
|
|
5752
6017
|
if (!target) throw new UsageError("bridge audit <path> (a product folder or file that uses the design system)");
|
|
5753
|
-
if (!
|
|
5754
|
-
const dsRoot = flag3(a, "ds") ? resolve3(flag3(a, "ds")) :
|
|
6018
|
+
if (!existsSync6(target)) throw new UsageError(`No such file or folder: ${target}`);
|
|
6019
|
+
const dsRoot = flag3(a, "ds") ? resolve3(flag3(a, "ds")) : existsSync6(join6(root, CONFIG_FILE2)) ? root : findDesignSystemRoot(root);
|
|
5755
6020
|
if (!dsRoot) throw new UsageError(`No ${CONFIG_FILE2} found: run bridge audit from the design system repo or pass --ds <dir>.`);
|
|
5756
6021
|
const ds = loadDesignSystem(dsRoot);
|
|
5757
6022
|
if (has3(a, "fix")) {
|
|
@@ -5765,12 +6030,12 @@ async function audit(a, root) {
|
|
|
5765
6030
|
const baselinePath = flag3(a, "baseline");
|
|
5766
6031
|
let baseline;
|
|
5767
6032
|
if (baselinePath) {
|
|
5768
|
-
if (!
|
|
5769
|
-
baseline = JSON.parse(await
|
|
6033
|
+
if (!existsSync6(baselinePath)) throw new UsageError(`Baseline not found: ${baselinePath}`);
|
|
6034
|
+
baseline = JSON.parse(await readFile6(baselinePath, "utf8"));
|
|
5770
6035
|
}
|
|
5771
6036
|
const label = flag3(a, "label");
|
|
5772
6037
|
const allowlistPath = flag3(a, "allowlist");
|
|
5773
|
-
if (allowlistPath && !
|
|
6038
|
+
if (allowlistPath && !existsSync6(allowlistPath)) throw new UsageError(`Allowlist not found: ${allowlistPath}`);
|
|
5774
6039
|
let allowlist;
|
|
5775
6040
|
try {
|
|
5776
6041
|
allowlist = allowlistPath ? loadAllowlist(allowlistPath) : void 0;
|
|
@@ -5781,9 +6046,9 @@ async function audit(a, root) {
|
|
|
5781
6046
|
const md = auditMarkdown(report);
|
|
5782
6047
|
const jsonOut = flag3(a, "json");
|
|
5783
6048
|
const mdOut = flag3(a, "md");
|
|
5784
|
-
if (jsonOut && jsonOut !== "true") await
|
|
6049
|
+
if (jsonOut && jsonOut !== "true") await writeFile5(jsonOut, `${JSON.stringify(report, null, 2)}
|
|
5785
6050
|
`);
|
|
5786
|
-
if (mdOut && mdOut !== "true") await
|
|
6051
|
+
if (mdOut && mdOut !== "true") await writeFile5(mdOut, `${md}
|
|
5787
6052
|
`);
|
|
5788
6053
|
if (jsonOut === "true") print(JSON.stringify(report, null, 2));
|
|
5789
6054
|
else if (mdOut === "true") print(md);
|
|
@@ -5901,6 +6166,8 @@ async function main() {
|
|
|
5901
6166
|
return brandCmd(a, root);
|
|
5902
6167
|
case "impact":
|
|
5903
6168
|
return impact(a, root);
|
|
6169
|
+
case "figma-references":
|
|
6170
|
+
return figmaReferences(a, root);
|
|
5904
6171
|
case "history":
|
|
5905
6172
|
return historyCmd(a, root);
|
|
5906
6173
|
case "revert":
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bridge-ds/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "bridge CLI: set up Bridge in a design system repo, check tokens in CI, search and diff tokens, propose changes, start the MCP server.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -43,8 +43,8 @@
|
|
|
43
43
|
"build:package": "node ../../scripts/build-packages.mjs cli"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"@bridge-ds/mcp": "^0.3.
|
|
47
|
-
"@bridge-ds/visual": "^0.
|
|
46
|
+
"@bridge-ds/mcp": "^0.3.3",
|
|
47
|
+
"@bridge-ds/visual": "^0.3.0",
|
|
48
48
|
"typescript": "5.9.3"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|