@bridge-ds/cli 0.11.1 → 0.12.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 +1679 -136
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -13,8 +13,9 @@ var FINDING_CATEGORIES = ["color", "spacing", "size", "radius", "border-width",
|
|
|
13
13
|
var ALIAS_RE = /^\{([^{}]+)\}$/;
|
|
14
14
|
var isAlias = (v) => typeof v === "string" && ALIAS_RE.test(v);
|
|
15
15
|
var aliasTarget = (v) => ALIAS_RE.exec(v)?.[1] ?? v;
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
var clamp01 = (n) => Math.min(1, Math.max(0, n));
|
|
17
|
+
function hexToRgba(hex2) {
|
|
18
|
+
const m = /^#?([0-9a-f]{3,8})$/i.exec(hex2.trim());
|
|
18
19
|
if (!m) return void 0;
|
|
19
20
|
let h = m[1];
|
|
20
21
|
if (h.length === 3 || h.length === 4) h = [...h].map((c) => c + c).join("");
|
|
@@ -22,6 +23,65 @@ function hexToRgba(hex) {
|
|
|
22
23
|
const n = (idx) => parseInt(h.slice(idx, idx + 2), 16) / 255;
|
|
23
24
|
return { r: n(0), g: n(2), b: n(4), a: h.length === 8 ? n(6) : 1 };
|
|
24
25
|
}
|
|
26
|
+
var to255 = (n) => Math.round(clamp01(n) * 255);
|
|
27
|
+
var rgbaToHex = (c) => "#" + [c.r, c.g, c.b].map((x) => to255(x).toString(16).padStart(2, "0")).join("");
|
|
28
|
+
function parseColor(value) {
|
|
29
|
+
if (typeof value === "string") return isAlias(value) ? void 0 : hexToRgba(value);
|
|
30
|
+
if (!value || typeof value !== "object") return void 0;
|
|
31
|
+
const v = value;
|
|
32
|
+
const alpha = typeof v.alpha === "number" ? v.alpha : 1;
|
|
33
|
+
if (v.colorSpace === "srgb" && Array.isArray(v.components) && v.components.length === 3) {
|
|
34
|
+
const [r, g, b] = v.components;
|
|
35
|
+
if ([r, g, b].every((x) => typeof x === "number")) return { r, g, b, a: alpha };
|
|
36
|
+
}
|
|
37
|
+
if (typeof v.hex === "string") {
|
|
38
|
+
const c = hexToRgba(v.hex);
|
|
39
|
+
if (c) return { ...c, a: alpha };
|
|
40
|
+
}
|
|
41
|
+
return void 0;
|
|
42
|
+
}
|
|
43
|
+
function colorsEqual(a, b) {
|
|
44
|
+
return rgbaToHex(a) === rgbaToHex(b) && Math.abs(a.a - b.a) < 2e-3;
|
|
45
|
+
}
|
|
46
|
+
function parseDimension(value) {
|
|
47
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
48
|
+
const v = value;
|
|
49
|
+
if (typeof v.value === "number" && typeof v.unit === "string") return { value: v.value, unit: v.unit };
|
|
50
|
+
}
|
|
51
|
+
if (typeof value === "string") {
|
|
52
|
+
const m = /^(-?\d+(?:\.\d+)?)(px|rem)$/.exec(value.trim());
|
|
53
|
+
if (m) return { value: Number(m[1]), unit: m[2] };
|
|
54
|
+
}
|
|
55
|
+
if (typeof value === "number") return { value, unit: "px" };
|
|
56
|
+
return void 0;
|
|
57
|
+
}
|
|
58
|
+
var numbersEqual = (a, b) => Math.abs(a - b) < 1e-4;
|
|
59
|
+
function deepEqual(a, b) {
|
|
60
|
+
if (a === b) return true;
|
|
61
|
+
if (typeof a === "number" && typeof b === "number") return numbersEqual(a, b);
|
|
62
|
+
if (typeof a !== typeof b || !a || !b || typeof a !== "object") return false;
|
|
63
|
+
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
|
64
|
+
if (Array.isArray(a) && Array.isArray(b)) return a.length === b.length && a.every((v, idx) => deepEqual(v, b[idx]));
|
|
65
|
+
const ka = Object.keys(a).filter((k) => a[k] !== void 0);
|
|
66
|
+
const kb = Object.keys(b).filter((k) => b[k] !== void 0);
|
|
67
|
+
if (ka.length !== kb.length) return false;
|
|
68
|
+
return ka.every((k) => deepEqual(a[k], b[k]));
|
|
69
|
+
}
|
|
70
|
+
function valuesEqual(type, a, b) {
|
|
71
|
+
if (a === void 0 || b === void 0) return a === b;
|
|
72
|
+
if (isAlias(a) || isAlias(b)) return isAlias(a) && isAlias(b) && aliasTarget(a) === aliasTarget(b);
|
|
73
|
+
if (type === "color") {
|
|
74
|
+
const ca = parseColor(a);
|
|
75
|
+
const cb = parseColor(b);
|
|
76
|
+
if (ca && cb) return colorsEqual(ca, cb);
|
|
77
|
+
}
|
|
78
|
+
if (type === "dimension") {
|
|
79
|
+
const da = parseDimension(a);
|
|
80
|
+
const db = parseDimension(b);
|
|
81
|
+
if (da && db) return da.unit === db.unit && numbersEqual(da.value, db.value);
|
|
82
|
+
}
|
|
83
|
+
return deepEqual(a, b);
|
|
84
|
+
}
|
|
25
85
|
|
|
26
86
|
// ../core/src/audit/color.ts
|
|
27
87
|
var NAMED_COLORS = {
|
|
@@ -1119,9 +1179,9 @@ var DesignSystemIndex = class {
|
|
|
1119
1179
|
return generic ? this.make(generic, 1, 0.3) : void 0;
|
|
1120
1180
|
}
|
|
1121
1181
|
case "shadow": {
|
|
1122
|
-
const
|
|
1182
|
+
const norm6 = (s) => s.replace(/\s+/g, " ").replace(/\s*,\s*/g, ", ").trim().toLowerCase();
|
|
1123
1183
|
const cands = this.entries(["shadow"]);
|
|
1124
|
-
const exact = cands.find((e) =>
|
|
1184
|
+
const exact = cands.find((e) => norm6(e.css) === norm6(value));
|
|
1125
1185
|
if (exact) return this.make(exact, 0, 1);
|
|
1126
1186
|
const t = shadowNums(value);
|
|
1127
1187
|
const scored = cands.map((e) => {
|
|
@@ -1572,16 +1632,16 @@ function applyAllowlist(entries, findings, components) {
|
|
|
1572
1632
|
return { findings: keep(findings), components: keep(components), ignored, used };
|
|
1573
1633
|
}
|
|
1574
1634
|
function parseAllowlist(json) {
|
|
1575
|
-
const
|
|
1635
|
+
const fail2 = (msg) => {
|
|
1576
1636
|
throw new Error(`allowlist: ${msg}`);
|
|
1577
1637
|
};
|
|
1578
|
-
if (!json || typeof json !== "object" || !Array.isArray(json.entries))
|
|
1638
|
+
if (!json || typeof json !== "object" || !Array.isArray(json.entries)) fail2('expected { "entries": [...] }');
|
|
1579
1639
|
return json.entries.map((e, i) => {
|
|
1580
1640
|
const x = e;
|
|
1581
|
-
if (typeof x.file !== "string" || !x.file)
|
|
1582
|
-
if (typeof x.line !== "number" || !Number.isInteger(x.line) || x.line < 1)
|
|
1583
|
-
if (typeof x.reason !== "string" || !x.reason.trim())
|
|
1584
|
-
if (x.what !== void 0 && typeof x.what !== "string")
|
|
1641
|
+
if (typeof x.file !== "string" || !x.file) fail2(`entry ${i} needs "file"`);
|
|
1642
|
+
if (typeof x.line !== "number" || !Number.isInteger(x.line) || x.line < 1) fail2(`entry ${i} (${x.file}) needs a "line" number`);
|
|
1643
|
+
if (typeof x.reason !== "string" || !x.reason.trim()) fail2(`entry ${i} (${x.file}:${x.line}) needs a "reason"`);
|
|
1644
|
+
if (x.what !== void 0 && typeof x.what !== "string") fail2(`entry ${i} (${x.file}:${x.line}): "what" must be a string`);
|
|
1585
1645
|
return { file: x.file, line: x.line, ...x.what !== void 0 ? { what: x.what } : {}, reason: x.reason.trim() };
|
|
1586
1646
|
});
|
|
1587
1647
|
}
|
|
@@ -2176,8 +2236,8 @@ function parseOwnerEntry(raw) {
|
|
|
2176
2236
|
var teamKey = (org, slug) => `@${org.toLowerCase()}/${slug.toLowerCase()}`;
|
|
2177
2237
|
function ownerEntryProblems(raw) {
|
|
2178
2238
|
if (raw === void 0) return [];
|
|
2179
|
-
const
|
|
2180
|
-
if (!
|
|
2239
|
+
const isObj4 = (x) => Boolean(x) && typeof x === "object" && !Array.isArray(x);
|
|
2240
|
+
if (!isObj4(raw)) return ['"governance.owners" must be { "design": [people or teams], "tech": [people or teams] }'];
|
|
2181
2241
|
const out = [];
|
|
2182
2242
|
for (const [k, v] of Object.entries(raw)) {
|
|
2183
2243
|
if (k !== "design" && k !== "tech") {
|
|
@@ -2220,6 +2280,31 @@ function teamsIn(owners) {
|
|
|
2220
2280
|
return [...out.values()];
|
|
2221
2281
|
}
|
|
2222
2282
|
var norm = (x) => x.trim().toLowerCase().replace(/^@/, "");
|
|
2283
|
+
function expandEntries(entries, teams) {
|
|
2284
|
+
const out = [];
|
|
2285
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2286
|
+
const add = (x) => {
|
|
2287
|
+
const k = norm(x);
|
|
2288
|
+
if (k && !seen.has(k)) {
|
|
2289
|
+
seen.add(k);
|
|
2290
|
+
out.push(x.replace(/^@(?=[^/]+$)/, ""));
|
|
2291
|
+
}
|
|
2292
|
+
};
|
|
2293
|
+
for (const raw of entries) {
|
|
2294
|
+
const e = parseOwnerEntry(raw);
|
|
2295
|
+
if (!e) continue;
|
|
2296
|
+
if (e.kind === "person") add(e.login);
|
|
2297
|
+
else {
|
|
2298
|
+
const state = teams?.[teamKey(e.org, e.slug)];
|
|
2299
|
+
if (state?.status === "ok") state.members.forEach(add);
|
|
2300
|
+
else add(teamKey(e.org, e.slug));
|
|
2301
|
+
}
|
|
2302
|
+
}
|
|
2303
|
+
return out;
|
|
2304
|
+
}
|
|
2305
|
+
function expandOwners(owners, teams) {
|
|
2306
|
+
return { design: expandEntries(owners.design, teams), tech: expandEntries(owners.tech, teams) };
|
|
2307
|
+
}
|
|
2223
2308
|
var people = (n) => `${n} ${n === 1 ? "person" : "people"}`;
|
|
2224
2309
|
function entryLabel(raw, teams) {
|
|
2225
2310
|
const e = parseOwnerEntry(raw);
|
|
@@ -2229,6 +2314,9 @@ function entryLabel(raw, teams) {
|
|
|
2229
2314
|
const name = `@${e.org}/${e.slug}`;
|
|
2230
2315
|
return state?.status === "ok" ? `${name} (${people(state.members.length)})` : name;
|
|
2231
2316
|
}
|
|
2317
|
+
function unreadableTeams(teams) {
|
|
2318
|
+
return Object.values(teams ?? {}).filter((t) => t.status === "unreadable");
|
|
2319
|
+
}
|
|
2232
2320
|
var TeamDirectory = class {
|
|
2233
2321
|
constructor(reader, opts = {}) {
|
|
2234
2322
|
this.reader = reader;
|
|
@@ -2351,6 +2439,8 @@ var TeamDirectory = class {
|
|
|
2351
2439
|
var OWNER_GROUPS = ["design", "tech"];
|
|
2352
2440
|
var RIGHTS = ["approveDesign", "approveTech", "ship"];
|
|
2353
2441
|
var DEFAULT_RIGHTS = { approveDesign: ["design"], approveTech: ["tech"], ship: ["tech"] };
|
|
2442
|
+
var norm2 = (x) => x.trim().toLowerCase().replace(/^@/, "");
|
|
2443
|
+
var uniq = (xs) => [...new Set(xs)];
|
|
2354
2444
|
function normalizeRights(input) {
|
|
2355
2445
|
const pick = (r) => {
|
|
2356
2446
|
const v = input?.[r];
|
|
@@ -2370,6 +2460,43 @@ function rightsProblems(raw) {
|
|
|
2370
2460
|
}
|
|
2371
2461
|
return out;
|
|
2372
2462
|
}
|
|
2463
|
+
function holdersOf(right, owners, rights) {
|
|
2464
|
+
const groups = (rights ?? DEFAULT_RIGHTS)[right];
|
|
2465
|
+
return uniq(groups.flatMap((g) => owners[g] ?? []));
|
|
2466
|
+
}
|
|
2467
|
+
function approverLists(owners, rights, teams) {
|
|
2468
|
+
const entries = approverEntries(owners, rights);
|
|
2469
|
+
return teams === void 0 ? entries : expandOwners(entries, teams);
|
|
2470
|
+
}
|
|
2471
|
+
function approverEntries(owners, rights) {
|
|
2472
|
+
if (!rights) return { design: [...owners.design], tech: [...owners.tech] };
|
|
2473
|
+
return { design: holdersOf("approveDesign", owners, rights), tech: holdersOf("approveTech", owners, rights) };
|
|
2474
|
+
}
|
|
2475
|
+
function soloApprover(owners) {
|
|
2476
|
+
const d = [...new Set(owners.design.map(norm2))];
|
|
2477
|
+
const t = [...new Set(owners.tech.map(norm2))];
|
|
2478
|
+
if (d.length !== 1 || t.length !== 1 || d[0] !== t[0] || !d[0] || d[0].includes("/")) return null;
|
|
2479
|
+
return d[0];
|
|
2480
|
+
}
|
|
2481
|
+
function unreadableShipTeams(policy) {
|
|
2482
|
+
if (!policy?.teams) return [];
|
|
2483
|
+
const keys = new Set((policy.entries ?? []).map((e) => e.trim().toLowerCase().replace(/^@?/, "@")));
|
|
2484
|
+
return unreadableTeams(policy.teams).filter((t) => keys.has(t.key));
|
|
2485
|
+
}
|
|
2486
|
+
function canShip(login, policy) {
|
|
2487
|
+
if (!policy) return true;
|
|
2488
|
+
if (!login || login.includes("/")) return false;
|
|
2489
|
+
return policy.logins.map(norm2).includes(norm2(login));
|
|
2490
|
+
}
|
|
2491
|
+
function groupsPhrase(groups) {
|
|
2492
|
+
const g = OWNER_GROUPS.filter((x) => groups.includes(x));
|
|
2493
|
+
if (g.length === 1) return `the ${g[0]} owner`;
|
|
2494
|
+
return `a ${g.join(" or ")} owner`;
|
|
2495
|
+
}
|
|
2496
|
+
function whoCanShipSentence(policy) {
|
|
2497
|
+
const names = (policy.entries ?? policy.logins).map((l) => entryLabel(l, policy.teams)).join(", ");
|
|
2498
|
+
return `Only ${groupsPhrase(policy.groups)} can merge this${names ? ` (${names})` : ""}.`;
|
|
2499
|
+
}
|
|
2373
2500
|
function visualImpactProblems(raw) {
|
|
2374
2501
|
if (raw === void 0) return [];
|
|
2375
2502
|
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return ['"visualImpact" must be { "mode": "auto" | "on-demand" }'];
|
|
@@ -2740,6 +2867,116 @@ function loadConfig(source) {
|
|
|
2740
2867
|
}
|
|
2741
2868
|
var tokensPath = (config, rel = "") => [config.tokens.root === "." ? "" : config.tokens.root, rel].filter(Boolean).join("/");
|
|
2742
2869
|
|
|
2870
|
+
// ../core/src/scopes.ts
|
|
2871
|
+
var has = (segments, ...names) => names.some((n) => segments.includes(n));
|
|
2872
|
+
function defaultScopes(path, type, tier) {
|
|
2873
|
+
const s = path.split(".");
|
|
2874
|
+
switch (type) {
|
|
2875
|
+
case "color":
|
|
2876
|
+
if (tier === "primitive" || tier === "brand" && s[0] === "palette") return [];
|
|
2877
|
+
if (has(s, "text")) return ["TEXT_FILL"];
|
|
2878
|
+
if (has(s, "border", "ring", "outline")) return ["STROKE_COLOR"];
|
|
2879
|
+
if (has(s, "bg", "surface", "background")) return ["FRAME_FILL", "SHAPE_FILL"];
|
|
2880
|
+
if (has(s, "shadow")) return ["EFFECT_COLOR"];
|
|
2881
|
+
return ["FRAME_FILL", "SHAPE_FILL", "TEXT_FILL", "STROKE_COLOR"];
|
|
2882
|
+
case "dimension":
|
|
2883
|
+
if (has(s, "radius")) return ["CORNER_RADIUS"];
|
|
2884
|
+
if (has(s, "border", "width", "stroke")) return ["STROKE_FLOAT"];
|
|
2885
|
+
if (has(s, "space", "spacing", "gap")) return ["GAP", "WIDTH_HEIGHT"];
|
|
2886
|
+
if (has(s, "size") && has(s, "font")) return ["FONT_SIZE"];
|
|
2887
|
+
if (has(s, "letter-spacing")) return ["LETTER_SPACING"];
|
|
2888
|
+
return ["WIDTH_HEIGHT", "GAP"];
|
|
2889
|
+
case "fontWeight":
|
|
2890
|
+
return ["FONT_WEIGHT"];
|
|
2891
|
+
case "fontFamily":
|
|
2892
|
+
return ["FONT_FAMILY"];
|
|
2893
|
+
case "number":
|
|
2894
|
+
if (has(s, "line-height", "lineHeight")) return ["LINE_HEIGHT"];
|
|
2895
|
+
if (has(s, "opacity")) return ["OPACITY"];
|
|
2896
|
+
return ["WIDTH_HEIGHT", "GAP"];
|
|
2897
|
+
case "string":
|
|
2898
|
+
return ["TEXT_CONTENT"];
|
|
2899
|
+
default:
|
|
2900
|
+
return [];
|
|
2901
|
+
}
|
|
2902
|
+
}
|
|
2903
|
+
var sameScopes = (a, b) => {
|
|
2904
|
+
const sa = [...new Set(a)].sort();
|
|
2905
|
+
const sb = [...new Set(b)].sort();
|
|
2906
|
+
return sa.length === sb.length && sa.every((x, i) => x === sb[i]);
|
|
2907
|
+
};
|
|
2908
|
+
var effectiveScopes = (t) => t.scopes ?? defaultScopes(t.path, t.type, t.tier);
|
|
2909
|
+
|
|
2910
|
+
// ../core/src/diff-engine.ts
|
|
2911
|
+
var normDesc = (d) => (d ?? "").trim();
|
|
2912
|
+
function fieldChanges(a, b) {
|
|
2913
|
+
const out = [];
|
|
2914
|
+
if (a.path !== b.path) out.push({ field: "path", before: a.path, after: b.path });
|
|
2915
|
+
if (a.type !== b.type) out.push({ field: "type", before: a.type, after: b.type });
|
|
2916
|
+
const modes = [.../* @__PURE__ */ new Set([...Object.keys(a.values), ...Object.keys(b.values)])];
|
|
2917
|
+
for (const mode2 of modes) {
|
|
2918
|
+
if (!valuesEqual(b.type, a.values[mode2], b.values[mode2])) out.push({ field: "value", mode: mode2, before: a.values[mode2], after: b.values[mode2] });
|
|
2919
|
+
}
|
|
2920
|
+
if (normDesc(a.description) !== normDesc(b.description)) out.push({ field: "description", before: a.description, after: b.description });
|
|
2921
|
+
const sa = effectiveScopes(a);
|
|
2922
|
+
const sb = effectiveScopes(b);
|
|
2923
|
+
if (!sameScopes(sa, sb)) out.push({ field: "scopes", before: sa, after: sb });
|
|
2924
|
+
return out;
|
|
2925
|
+
}
|
|
2926
|
+
|
|
2927
|
+
// ../core/src/risk.ts
|
|
2928
|
+
var RISK_LABELS = {
|
|
2929
|
+
1: "Tier 1: low risk, one code owner",
|
|
2930
|
+
2: "Tier 2: medium risk, design review then design + tech approval",
|
|
2931
|
+
3: "Tier 3: token change, needs design + tech approval"
|
|
2932
|
+
};
|
|
2933
|
+
var BUMP_RANK = { patch: 0, minor: 1, major: 2 };
|
|
2934
|
+
var LOW = [/\.md$/, /^docs\//, /\.stories\.tsx?$/, /\.docs\.json$/, /\.test\.tsx?$/, /^\.changeset\//];
|
|
2935
|
+
var isSummary = (c) => "kind" in c;
|
|
2936
|
+
function summarizeTokenChange(c) {
|
|
2937
|
+
if (isSummary(c)) return c;
|
|
2938
|
+
const path = (c.target ?? c.current).path;
|
|
2939
|
+
if (!c.target) return { kind: "removed", path };
|
|
2940
|
+
if (!c.current) return { kind: "added", path };
|
|
2941
|
+
if (c.current.path !== c.target.path) return { kind: "renamed", path };
|
|
2942
|
+
const fields = fieldChanges(c.current, c.target);
|
|
2943
|
+
return { kind: "changed", path, descriptionOnly: fields.length > 0 && fields.every((f) => f.field === "description") };
|
|
2944
|
+
}
|
|
2945
|
+
function tokenBump(changes) {
|
|
2946
|
+
let bump;
|
|
2947
|
+
for (const c of changes.map(summarizeTokenChange)) {
|
|
2948
|
+
const b = c.kind === "removed" || c.kind === "renamed" ? "major" : c.kind === "changed" && c.descriptionOnly ? "patch" : "minor";
|
|
2949
|
+
if (!bump || BUMP_RANK[b] > BUMP_RANK[bump]) bump = b;
|
|
2950
|
+
}
|
|
2951
|
+
return bump;
|
|
2952
|
+
}
|
|
2953
|
+
function isGuardrail(path, config) {
|
|
2954
|
+
const list2 = config ? [...config.governance.guardrails, CONFIG_FILE, ...config.contrast ? [config.contrast.path] : []] : DEFAULT_GOVERNANCE.guardrails;
|
|
2955
|
+
return list2.some((g) => g.endsWith("/") ? path.startsWith(g) : path === g) || path.startsWith(".github/") || path.endsWith("/contrast.checks.json") || path === "contrast.checks.json";
|
|
2956
|
+
}
|
|
2957
|
+
function classifyRisk(input) {
|
|
2958
|
+
const reasons = [];
|
|
2959
|
+
const tokenChanges = (input.tokenChanges ?? []).map(summarizeTokenChange);
|
|
2960
|
+
for (const c of tokenChanges) {
|
|
2961
|
+
if (c.kind === "changed" && c.descriptionOnly) reasons.push({ tier: 1, text: `Token description changed: ${c.path}` });
|
|
2962
|
+
else reasons.push({ tier: 3, text: `Token ${c.kind}: ${c.path}` });
|
|
2963
|
+
}
|
|
2964
|
+
const bump = tokenBump(tokenChanges);
|
|
2965
|
+
const componentsDir = input.config?.components?.dir;
|
|
2966
|
+
for (const f of input.files ?? []) {
|
|
2967
|
+
const p = f.path.replace(/^\/+/, "");
|
|
2968
|
+
if (p.endsWith(".tokens.json")) {
|
|
2969
|
+
if (!input.tokenChanges) reasons.push({ tier: 3, text: `Token file ${f.status ?? "changed"}: ${p}` });
|
|
2970
|
+
} else if (isGuardrail(p, input.config)) reasons.push({ tier: 3, text: `Guardrail file changed: ${p}` });
|
|
2971
|
+
else if (LOW.some((r) => r.test(p))) reasons.push({ tier: 1, text: `Docs, story or test: ${p}` });
|
|
2972
|
+
else if (componentsDir && p.startsWith(`${componentsDir}/`)) reasons.push({ tier: 2, text: `Component source ${f.status ?? "changed"}: ${p}` });
|
|
2973
|
+
else reasons.push({ tier: 2, text: `Source file changed: ${p}` });
|
|
2974
|
+
}
|
|
2975
|
+
reasons.sort((a, b) => b.tier - a.tier);
|
|
2976
|
+
const tier = reasons[0]?.tier ?? 1;
|
|
2977
|
+
return { tier, label: RISK_LABELS[tier], reasons: reasons.map((r) => r.text), bump };
|
|
2978
|
+
}
|
|
2979
|
+
|
|
2743
2980
|
// ../core/src/node/git-local.ts
|
|
2744
2981
|
import { execFile } from "node:child_process";
|
|
2745
2982
|
|
|
@@ -2843,6 +3080,198 @@ function diffLayered(before, after) {
|
|
|
2843
3080
|
for (const d of added) changes.push({ kind: "added", path: d.path, file: d.file, ...d.type ? { type: d.type } : {}, fields: [], after: d.value, afterCss: css(after, d), ...d.description ? { descriptionAfter: d.description } : {}, ...tierOf(after, d.file) });
|
|
2844
3081
|
return changes.sort((x, y) => x.path.localeCompare(y.path) || x.file.localeCompare(y.file));
|
|
2845
3082
|
}
|
|
3083
|
+
var toChangeSummary = (c) => ({
|
|
3084
|
+
kind: c.kind,
|
|
3085
|
+
path: c.path,
|
|
3086
|
+
...c.kind === "changed" ? { descriptionOnly: c.fields.length > 0 && c.fields.every((f) => f === "description") } : {}
|
|
3087
|
+
});
|
|
3088
|
+
function classifyLayered(changes, otherFiles = [], config) {
|
|
3089
|
+
const r = classifyRisk({ tokenChanges: changes.map(toChangeSummary), files: otherFiles.map((path) => ({ path })), config });
|
|
3090
|
+
return { tier: r.tier, bump: r.bump ?? "patch", reasons: r.reasons.length ? r.reasons : ["No token or guardrail change"], label: r.label };
|
|
3091
|
+
}
|
|
3092
|
+
|
|
3093
|
+
// ../core/src/history.ts
|
|
3094
|
+
var BULK_LIMIT = 60;
|
|
3095
|
+
var UNRELEASED = "Unreleased";
|
|
3096
|
+
var SUBJECT_PR = [/^Merge pull request #(\d+)/, /\(#(\d+)\)\s*$/];
|
|
3097
|
+
function prNumberFromSubject(subject) {
|
|
3098
|
+
for (const re of SUBJECT_PR) {
|
|
3099
|
+
const m = re.exec(subject);
|
|
3100
|
+
if (m) return Number(m[1]);
|
|
3101
|
+
}
|
|
3102
|
+
return void 0;
|
|
3103
|
+
}
|
|
3104
|
+
function parseChangesetText(path, text) {
|
|
3105
|
+
const m = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/.exec(text);
|
|
3106
|
+
if (!m) return null;
|
|
3107
|
+
const bumps = [...m[1].matchAll(/["']?([^"'\s:]+)["']?\s*:\s*(major|minor|patch)/g)].map((x) => ({ pkg: x[1], bump: x[2] }));
|
|
3108
|
+
const order = ["major", "minor", "patch"];
|
|
3109
|
+
const bump = order.find((b) => bumps.some((x) => x.bump === b));
|
|
3110
|
+
const summary = m[2].trim().split(/\r?\n\r?\n/)[0].replace(/\s+/g, " ").trim();
|
|
3111
|
+
return { path, ...bump ? { bump } : {}, packages: bumps.map((b) => b.pkg), summary };
|
|
3112
|
+
}
|
|
3113
|
+
function firstParagraph(text) {
|
|
3114
|
+
if (!text) return "";
|
|
3115
|
+
const cleaned = text.replace(/<!--[\s\S]*?-->/g, "");
|
|
3116
|
+
for (const para of cleaned.split(/\r?\n\s*\r?\n/)) {
|
|
3117
|
+
const lines = para.split(/\r?\n/).filter((l) => l.trim() && !/^\s*#{1,6}\s/.test(l) && !/^\s*[-*]\s*\[[ xX]\]/.test(l) && !/^\s*(Co-Authored-By|Claude-Session|Signed-off-by):/i.test(l));
|
|
3118
|
+
const s = lines.join(" ").replace(/\s+/g, " ").trim();
|
|
3119
|
+
if (s && !/^\|/.test(s)) return s.length > 400 ? `${s.slice(0, 397)}...` : s;
|
|
3120
|
+
}
|
|
3121
|
+
return "";
|
|
3122
|
+
}
|
|
3123
|
+
function countChanges(changes) {
|
|
3124
|
+
const c = { added: 0, changed: 0, removed: 0, renamed: 0, total: changes.length };
|
|
3125
|
+
for (const x of changes) c[x.kind]++;
|
|
3126
|
+
return c;
|
|
3127
|
+
}
|
|
3128
|
+
var shortSha = (sha) => sha.slice(0, 7);
|
|
3129
|
+
function titleOf(commit) {
|
|
3130
|
+
if (commit.pr?.title) return commit.pr.title;
|
|
3131
|
+
if (/^Merge pull request #\d+/.test(commit.subject)) {
|
|
3132
|
+
const first = (commit.body ?? "").split(/\r?\n/).find((l) => l.trim());
|
|
3133
|
+
if (first) return first.trim();
|
|
3134
|
+
}
|
|
3135
|
+
return commit.subject;
|
|
3136
|
+
}
|
|
3137
|
+
function buildHistoryEntry(input) {
|
|
3138
|
+
const { config, commit } = input;
|
|
3139
|
+
const beforeSet = new LayeredTokenSet(config, input.before);
|
|
3140
|
+
const afterSet = new LayeredTokenSet(config, input.after);
|
|
3141
|
+
const changes = diffLayered(beforeSet, afterSet);
|
|
3142
|
+
if (!changes.length) return null;
|
|
3143
|
+
const tier = classifyLayered(changes, [], config);
|
|
3144
|
+
const changesets = (input.changesets ?? []).map((c) => parseChangesetText(c.path, c.text)).filter((c) => c !== null);
|
|
3145
|
+
const pkg = config.changesets?.package;
|
|
3146
|
+
const relevant = changesets.filter((c) => !pkg || !c.packages.length || c.packages.includes(pkg));
|
|
3147
|
+
const title = titleOf(commit);
|
|
3148
|
+
const why = relevant.map((c) => c.summary).find(Boolean) || firstParagraph(commit.pr?.body) || firstParagraph(commit.body) || title;
|
|
3149
|
+
const prNumber = commit.pr?.number ?? prNumberFromSubject(commit.subject);
|
|
3150
|
+
const pr = commit.pr ?? (prNumber ? { number: prNumber, title, ...config.repo ? { url: `https://github.com/${config.repo.owner}/${config.repo.name}/pull/${prNumber}` } : {} } : void 0);
|
|
3151
|
+
const changesetBump = relevant.map((c) => c.bump).find((b) => b === "major");
|
|
3152
|
+
const bump = changesetBump ?? tier.bump;
|
|
3153
|
+
return {
|
|
3154
|
+
id: pr ? `pr-${pr.number}` : shortSha(commit.sha),
|
|
3155
|
+
sha: commit.sha,
|
|
3156
|
+
...commit.parents[0] ? { baseSha: commit.parents[0] } : {},
|
|
3157
|
+
date: commit.date,
|
|
3158
|
+
author: commit.pr?.author ?? commit.author,
|
|
3159
|
+
title,
|
|
3160
|
+
why,
|
|
3161
|
+
...pr ? { pr } : {},
|
|
3162
|
+
changes,
|
|
3163
|
+
counts: countChanges(changes),
|
|
3164
|
+
tier: tier.tier,
|
|
3165
|
+
bump,
|
|
3166
|
+
breaking: bump === "major",
|
|
3167
|
+
reasons: tier.reasons,
|
|
3168
|
+
changesets: relevant,
|
|
3169
|
+
...changes.length > BULK_LIMIT ? { bulk: true } : {},
|
|
3170
|
+
...input.demo ? { demo: true } : {}
|
|
3171
|
+
};
|
|
3172
|
+
}
|
|
3173
|
+
async function collectHistory(config, backend, opts) {
|
|
3174
|
+
const root = config.tokens.root.replace(/\/$/, "");
|
|
3175
|
+
const commits = await backend.listCommits({ ref: opts.ref, paths: [root === "." ? "" : root].filter(Boolean), limit: opts.limit ?? 40 });
|
|
3176
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3177
|
+
const picked = [];
|
|
3178
|
+
for (const c of commits) {
|
|
3179
|
+
const n = c.pr?.number;
|
|
3180
|
+
if (n !== void 0) {
|
|
3181
|
+
if (seen.has(n)) continue;
|
|
3182
|
+
seen.add(n);
|
|
3183
|
+
}
|
|
3184
|
+
picked.push(c);
|
|
3185
|
+
}
|
|
3186
|
+
const entries = [];
|
|
3187
|
+
let done = 0;
|
|
3188
|
+
for (const c of picked) {
|
|
3189
|
+
const parent = c.parents[0];
|
|
3190
|
+
const [before, after] = await Promise.all([parent ? backend.tokenFiles(parent) : Promise.resolve(/* @__PURE__ */ new Map()), backend.tokenFiles(c.sha)]);
|
|
3191
|
+
let changesets = [];
|
|
3192
|
+
if (backend.changesetPaths && backend.readFile && config.changesets) {
|
|
3193
|
+
const [was, now] = await Promise.all([parent ? backend.changesetPaths(parent) : Promise.resolve([]), backend.changesetPaths(c.sha)]);
|
|
3194
|
+
const added = now.filter((p) => !was.includes(p) && p.endsWith(".md") && !/README\.md$/i.test(p));
|
|
3195
|
+
changesets = (await Promise.all(added.map(async (p) => ({ path: p, text: await backend.readFile(c.sha, p) ?? "" })))).filter((x) => x.text);
|
|
3196
|
+
}
|
|
3197
|
+
const e = buildHistoryEntry({ config, commit: c, before, after, changesets });
|
|
3198
|
+
if (e) entries.push(e);
|
|
3199
|
+
opts.onProgress?.(++done, picked.length);
|
|
3200
|
+
}
|
|
3201
|
+
return entries;
|
|
3202
|
+
}
|
|
3203
|
+
function parseChangelog(text) {
|
|
3204
|
+
const out = [];
|
|
3205
|
+
const parts = text.split(/^##\s+/m).slice(1);
|
|
3206
|
+
for (const p of parts) {
|
|
3207
|
+
const [head, ...rest] = p.split(/\r?\n/);
|
|
3208
|
+
const version = /v?(\d+\.\d+\.\d+(?:-[\w.]+)?)/.exec(head ?? "")?.[1];
|
|
3209
|
+
if (!version) continue;
|
|
3210
|
+
const body = rest.join("\n");
|
|
3211
|
+
const shas = [...new Set([...body.matchAll(/(?:^|[\s[`(])([0-9a-f]{7,40})(?=[\s`\]):]|$)/gm)].map((m) => m[1]))];
|
|
3212
|
+
const prs = [...new Set([...body.matchAll(/(?:#|\/pull\/)(\d+)/g)].map((m) => Number(m[1])))];
|
|
3213
|
+
const date2 = /(\d{4}-\d{2}-\d{2})/.exec(head ?? "")?.[1];
|
|
3214
|
+
out.push({ version, ...date2 ? { date: date2 } : {}, shas, prs });
|
|
3215
|
+
}
|
|
3216
|
+
return out;
|
|
3217
|
+
}
|
|
3218
|
+
function releasesFromTags(tags, pkg) {
|
|
3219
|
+
const out = [];
|
|
3220
|
+
for (const t of tags) {
|
|
3221
|
+
const m = /^(?:(@?[^@\s]+)@)?v?(\d+\.\d+\.\d+(?:-[\w.]+)?)$/.exec(t.name);
|
|
3222
|
+
if (!m) continue;
|
|
3223
|
+
if (pkg && m[1] && m[1] !== pkg) continue;
|
|
3224
|
+
out.push({ version: m[2], ...t.sha ? { sha: t.sha } : {}, ...t.date ? { date: t.date } : {} });
|
|
3225
|
+
}
|
|
3226
|
+
return out;
|
|
3227
|
+
}
|
|
3228
|
+
function assignVersions(entries, releases) {
|
|
3229
|
+
const dated = releases.filter((r) => r.date).sort((a, b) => Date.parse(a.date) - Date.parse(b.date));
|
|
3230
|
+
return entries.map((e) => {
|
|
3231
|
+
const cited = releases.find((r) => r.shas?.some((s) => e.sha.startsWith(s)) || e.pr && r.prs?.includes(e.pr.number));
|
|
3232
|
+
const byTag = cited ? void 0 : dated.find((r) => r.sha === e.sha || Date.parse(r.date) >= Date.parse(e.date));
|
|
3233
|
+
return { ...e, version: (cited ?? byTag)?.version ?? UNRELEASED };
|
|
3234
|
+
});
|
|
3235
|
+
}
|
|
3236
|
+
var BUMP_RANK2 = { patch: 0, minor: 1, major: 2 };
|
|
3237
|
+
function versionTimeline(entries, releases = []) {
|
|
3238
|
+
const groups = /* @__PURE__ */ new Map();
|
|
3239
|
+
for (const e of entries) {
|
|
3240
|
+
const v = e.version ?? UNRELEASED;
|
|
3241
|
+
let g = groups.get(v);
|
|
3242
|
+
if (!g) {
|
|
3243
|
+
const rel = releases.find((r) => r.version === v);
|
|
3244
|
+
g = { version: v, pending: v === UNRELEASED, date: rel?.date ?? e.date, entries: [], prs: [], tokenChanges: 0, breaking: false, bump: "patch", authors: [] };
|
|
3245
|
+
groups.set(v, g);
|
|
3246
|
+
}
|
|
3247
|
+
g.entries.push(e.id);
|
|
3248
|
+
if (e.pr && !g.prs.includes(e.pr.number)) g.prs.push(e.pr.number);
|
|
3249
|
+
g.tokenChanges += e.counts.total;
|
|
3250
|
+
g.breaking ||= e.breaking;
|
|
3251
|
+
if (BUMP_RANK2[e.bump] > BUMP_RANK2[g.bump]) g.bump = e.bump;
|
|
3252
|
+
if (!g.authors.includes(e.author)) g.authors.push(e.author);
|
|
3253
|
+
if (!releases.some((r) => r.version === v && r.date) && Date.parse(e.date) > Date.parse(g.date)) g.date = e.date;
|
|
3254
|
+
}
|
|
3255
|
+
return [...groups.values()].sort((a, b) => a.pending ? -1 : b.pending ? 1 : Date.parse(b.date) - Date.parse(a.date));
|
|
3256
|
+
}
|
|
3257
|
+
function packageDirCandidates(config) {
|
|
3258
|
+
const parts = config.tokens.root.replace(/\/$/, "").split("/").filter((p) => p && p !== ".");
|
|
3259
|
+
const out = [];
|
|
3260
|
+
for (let i = parts.length; i >= 0; i--) out.push(parts.slice(0, i).join("/"));
|
|
3261
|
+
return out;
|
|
3262
|
+
}
|
|
3263
|
+
function mergeReleases(changelog, tags) {
|
|
3264
|
+
const byVersion = /* @__PURE__ */ new Map();
|
|
3265
|
+
for (const r of [...changelog, ...tags]) {
|
|
3266
|
+
const prev = byVersion.get(r.version);
|
|
3267
|
+
byVersion.set(r.version, prev ? { ...r, ...prev, ...prev.date ? {} : r.date ? { date: r.date } : {}, ...prev.sha ? {} : r.sha ? { sha: r.sha } : {} } : r);
|
|
3268
|
+
}
|
|
3269
|
+
return [...byVersion.values()];
|
|
3270
|
+
}
|
|
3271
|
+
function buildReport(args) {
|
|
3272
|
+
const entries = assignVersions(args.entries, args.releases).sort((a, b) => Date.parse(b.date) - Date.parse(a.date));
|
|
3273
|
+
return { ...args, entries, timeline: versionTimeline(entries, args.releases) };
|
|
3274
|
+
}
|
|
2846
3275
|
|
|
2847
3276
|
// ../core/src/node/audit.ts
|
|
2848
3277
|
import { existsSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
@@ -4050,6 +4479,33 @@ var EXPLORATION_USE_CASES = [
|
|
|
4050
4479
|
];
|
|
4051
4480
|
var EXPLORATION_STATUS_LABEL = { active: "Running", promoted: "Promoted to review", closed: "Closed" };
|
|
4052
4481
|
|
|
4482
|
+
// ../core/src/titles.ts
|
|
4483
|
+
var escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
4484
|
+
function properWord(word, names) {
|
|
4485
|
+
const bare = word.replace(/[^\p{L}\p{N}]+$/u, "");
|
|
4486
|
+
if (!bare) return false;
|
|
4487
|
+
if (names.some((n) => n === bare)) return true;
|
|
4488
|
+
return new RegExp("\\p{Lu}", "u").test(bare.slice(1));
|
|
4489
|
+
}
|
|
4490
|
+
function lowerFirstWord(text, names = []) {
|
|
4491
|
+
const first = text.split(/\s+/)[0] ?? "";
|
|
4492
|
+
if (!first || properWord(first, names)) return text;
|
|
4493
|
+
return `${text.charAt(0).toLowerCase()}${text.slice(1)}`;
|
|
4494
|
+
}
|
|
4495
|
+
function subjectTitle(subject, title, names = []) {
|
|
4496
|
+
const t = title.trim().replace(/\.$/, "");
|
|
4497
|
+
const keep = [subject, ...names];
|
|
4498
|
+
const s = subject.trim();
|
|
4499
|
+
if (!s) return t;
|
|
4500
|
+
const m = new RegExp(`^${escape(s)}(?=$|[^\\p{L}\\p{N}])\\s*(:)?\\s*`, "iu").exec(t);
|
|
4501
|
+
if (m) {
|
|
4502
|
+
const rest = t.slice(m[0].length);
|
|
4503
|
+
if (!rest) return s;
|
|
4504
|
+
return m[1] ? `${s}: ${lowerFirstWord(rest, keep)}` : `${s} ${rest}`;
|
|
4505
|
+
}
|
|
4506
|
+
return `${s}: ${lowerFirstWord(t, keep)}`;
|
|
4507
|
+
}
|
|
4508
|
+
|
|
4053
4509
|
// ../core/src/requests.ts
|
|
4054
4510
|
var REQUEST_KINDS = [
|
|
4055
4511
|
{ value: "new-component", label: "New component", hint: "A component the design system does not have yet." },
|
|
@@ -4062,11 +4518,33 @@ var REQUEST_KINDS = [
|
|
|
4062
4518
|
{ value: "other", label: "Other", hint: "Anything else." }
|
|
4063
4519
|
];
|
|
4064
4520
|
var REQUEST_LABEL = "component-change";
|
|
4521
|
+
var MARK = "bridge-request";
|
|
4065
4522
|
function requestIssueTitle(r) {
|
|
4066
|
-
|
|
4067
|
-
|
|
4523
|
+
return subjectTitle(r.component, r.title);
|
|
4524
|
+
}
|
|
4525
|
+
function parseRequestIssue(body) {
|
|
4526
|
+
if (!body) return void 0;
|
|
4527
|
+
const m = new RegExp(`<!-- ${MARK} (\\{[\\s\\S]*?\\}) -->`).exec(body);
|
|
4528
|
+
if (!m) return void 0;
|
|
4529
|
+
try {
|
|
4530
|
+
const r = JSON.parse(m[1]);
|
|
4531
|
+
return r && typeof r.component === "string" && typeof r.title === "string" ? { ...r, changes: Array.isArray(r.changes) ? r.changes : [] } : void 0;
|
|
4532
|
+
} catch {
|
|
4533
|
+
return void 0;
|
|
4534
|
+
}
|
|
4535
|
+
}
|
|
4536
|
+
function requestStatus(issue) {
|
|
4537
|
+
const prs = issue.linkedPrs ?? [];
|
|
4538
|
+
if (issue.state === "open") return prs.some((p) => p.state === "open") ? "in-progress" : "requested";
|
|
4539
|
+
if (prs.some((p) => p.merged) || issue.stateReason === "completed") return "done";
|
|
4540
|
+
return "declined";
|
|
4068
4541
|
}
|
|
4069
4542
|
var REQUEST_STATUS_LABEL = { requested: "Requested", "in-progress": "In progress", done: "Done", declined: "Declined" };
|
|
4543
|
+
function closingIssueNumbers(body) {
|
|
4544
|
+
const out = /* @__PURE__ */ new Set();
|
|
4545
|
+
for (const m of (body ?? "").matchAll(/\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s+#(\d+)/gi)) out.add(Number(m[1]));
|
|
4546
|
+
return [...out];
|
|
4547
|
+
}
|
|
4070
4548
|
function validateIntegrations(i) {
|
|
4071
4549
|
const out = [];
|
|
4072
4550
|
if (!i?.jira) return out;
|
|
@@ -4095,7 +4573,185 @@ function jiraIssueFields(r, jira, issueUrl) {
|
|
|
4095
4573
|
};
|
|
4096
4574
|
}
|
|
4097
4575
|
|
|
4576
|
+
// ../core/src/review-gate.ts
|
|
4577
|
+
function parseTier(labels, body) {
|
|
4578
|
+
const found = [];
|
|
4579
|
+
for (const l of labels) {
|
|
4580
|
+
const m = l.name.match(/^(?:risk[\s:-]*)?tier[\s:_-]*([123])\b/i) ?? l.name.match(/^risk[\s:_-]*([123])$/i);
|
|
4581
|
+
if (m) found.push(Number(m[1]));
|
|
4582
|
+
}
|
|
4583
|
+
for (const m of (body ?? "").matchAll(/-\s*\[[xX]\]\s*\**\s*Tier\s*([123])\b/g)) found.push(Number(m[1]));
|
|
4584
|
+
for (const m of (body ?? "").matchAll(/risk tier\W{0,6}(?:tier\s*)?([123])\b/gi)) found.push(Number(m[1]));
|
|
4585
|
+
return found.length ? Math.max(...found) : null;
|
|
4586
|
+
}
|
|
4587
|
+
function pullState(p) {
|
|
4588
|
+
if (p.merged_at || p.merged) return "merged";
|
|
4589
|
+
if (p.state && p.state !== "open") return "closed";
|
|
4590
|
+
if (p.draft) return "draft";
|
|
4591
|
+
return "open";
|
|
4592
|
+
}
|
|
4593
|
+
function summarizeApprovals(reviews, owners, author, rules = {}) {
|
|
4594
|
+
const latest = /* @__PURE__ */ new Map();
|
|
4595
|
+
const sorted = [...reviews].sort((a2, b) => String(a2.submitted_at ?? "").localeCompare(String(b.submitted_at ?? "")));
|
|
4596
|
+
for (const r of sorted) {
|
|
4597
|
+
const login = r.user?.login;
|
|
4598
|
+
if (!login || login === author) continue;
|
|
4599
|
+
if (r.state === "APPROVED" || r.state === "CHANGES_REQUESTED" || r.state === "DISMISSED") latest.set(login, r.state);
|
|
4600
|
+
}
|
|
4601
|
+
const approvers = [...latest].filter(([, s]) => s === "APPROVED").map(([l]) => l);
|
|
4602
|
+
const changesRequested = [...latest].filter(([, s]) => s === "CHANGES_REQUESTED").map(([l]) => l);
|
|
4603
|
+
const lower = (xs) => xs.map((x) => x.toLowerCase().replace(/^@/, ""));
|
|
4604
|
+
const d = lower(owners.design);
|
|
4605
|
+
const t = lower(owners.tech);
|
|
4606
|
+
const a = lower(approvers);
|
|
4607
|
+
let design;
|
|
4608
|
+
let tech;
|
|
4609
|
+
if (d.length || t.length) {
|
|
4610
|
+
design = d.length ? a.some((x) => d.includes(x)) : a.length > 0;
|
|
4611
|
+
const solo = rules.soloOwnerCanApproveBoth ? soloApprover(owners) : null;
|
|
4612
|
+
const soloBoth = (x) => a.length === 1 && d.includes(x) && x !== solo;
|
|
4613
|
+
tech = t.length ? a.some((x) => t.includes(x) && !soloBoth(x)) : a.length > 1;
|
|
4614
|
+
} else {
|
|
4615
|
+
design = a.length >= 1;
|
|
4616
|
+
tech = a.length >= 2;
|
|
4617
|
+
}
|
|
4618
|
+
return { approvers, changesRequested, design, tech };
|
|
4619
|
+
}
|
|
4620
|
+
var normLogin = (x) => x.trim().toLowerCase().replace(/^@/, "");
|
|
4621
|
+
function canOverride(login, policy) {
|
|
4622
|
+
if (!login || !policy?.allowed.length) return false;
|
|
4623
|
+
return policy.allowed.map(normLogin).includes(normLogin(login));
|
|
4624
|
+
}
|
|
4625
|
+
function mergeGate(input) {
|
|
4626
|
+
const blocking = [];
|
|
4627
|
+
const missing = [];
|
|
4628
|
+
const approvalReasons = [];
|
|
4629
|
+
const lifecycle = pullState(input);
|
|
4630
|
+
if (lifecycle === "merged") blocking.push("This branch is already merged.");
|
|
4631
|
+
else if (lifecycle === "closed") blocking.push("This branch is closed.");
|
|
4632
|
+
if (input.exploration && lifecycle !== "merged" && lifecycle !== "closed") blocking.push("It is an exploration: promote it to review first.");
|
|
4633
|
+
if (input.draft) blocking.push("It is a draft. Mark it ready for review on GitHub first.");
|
|
4634
|
+
if (input.ci.state === "fail") blocking.push(`Tests fail: ${input.ci.failed.map((f) => f.name).join(", ")}.`);
|
|
4635
|
+
if (input.ci.state === "pending") blocking.push("Tests are still running.");
|
|
4636
|
+
if (input.approvals.changesRequested.length) blocking.push(`Changes requested by @${input.approvals.changesRequested.join(", @")}.`);
|
|
4637
|
+
const tier = input.tier ?? 3;
|
|
4638
|
+
if (tier >= 2) {
|
|
4639
|
+
if (!input.approvals.design) {
|
|
4640
|
+
approvalReasons.push("It needs a design owner approval.");
|
|
4641
|
+
missing.push("design approval");
|
|
4642
|
+
}
|
|
4643
|
+
if (!input.approvals.tech) {
|
|
4644
|
+
approvalReasons.push("It needs a tech owner approval.");
|
|
4645
|
+
missing.push("tech approval");
|
|
4646
|
+
}
|
|
4647
|
+
} else if (!input.approvals.approvers.length) {
|
|
4648
|
+
approvalReasons.push("It needs one approval.");
|
|
4649
|
+
missing.push("approval");
|
|
4650
|
+
}
|
|
4651
|
+
const conflict = input.mergeable === false ? [CONFLICT_REASON] : [];
|
|
4652
|
+
const shipper = !input.viewer || canShip(input.viewer, input.ship);
|
|
4653
|
+
const ship = !shipper && lifecycle !== "merged" && lifecycle !== "closed" ? [whoCanShipSentence(input.ship), ...unreadableShipTeams(input.ship).map((t) => t.status === "unreadable" ? t.message : "")] : [];
|
|
4654
|
+
const reasons = [...blocking, ...approvalReasons, ...ship, ...conflict];
|
|
4655
|
+
const allowed = reasons.length === 0;
|
|
4656
|
+
const overridable = !allowed && blocking.length + conflict.length + ship.length === 0 && missing.length > 0 && canOverride(input.viewer, input.override);
|
|
4657
|
+
return { allowed, reasons, overridable, missing, canShip: shipper };
|
|
4658
|
+
}
|
|
4659
|
+
var CONFLICT_REASON = "It conflicts with main: resolve it with Update from main on the branch.";
|
|
4660
|
+
function effectiveTier(labels, body, files) {
|
|
4661
|
+
const labelled = parseTier(labels, body);
|
|
4662
|
+
if (!files) return labelled;
|
|
4663
|
+
const computed = classifyRisk({ files: files.map((f) => ({ path: f.filename, status: f.status })) }).tier;
|
|
4664
|
+
return Math.max(labelled ?? 0, computed);
|
|
4665
|
+
}
|
|
4666
|
+
var NO_CI = { state: "pass", total: 0, passed: 0, failed: [], pending: [] };
|
|
4667
|
+
var ROLE_OF_MISSING = { "design approval": "design", "tech approval": "tech", approval: "approval" };
|
|
4668
|
+
function pendingApprovals(tier, approvals) {
|
|
4669
|
+
if (tier == null || !approvals) return [];
|
|
4670
|
+
return mergeGate({ tier, ci: NO_CI, approvals, draft: false, state: "open" }).missing.map((m) => ROLE_OF_MISSING[m]);
|
|
4671
|
+
}
|
|
4672
|
+
|
|
4673
|
+
// ../core/src/suggestion-review.ts
|
|
4674
|
+
var SUGGESTION_REVIEW_MARK = "bridge-suggestion-review";
|
|
4675
|
+
var SUGGESTION_APPLIED_MARK = "bridge-suggestion-applied";
|
|
4676
|
+
var EVENTS = ["approve", "request-changes", "comment"];
|
|
4677
|
+
function parseSuggestionReviewComment(body) {
|
|
4678
|
+
const m = new RegExp(`<!-- ${SUGGESTION_REVIEW_MARK} (\\{[^\\n]*?\\}) -->`).exec(body ?? "");
|
|
4679
|
+
if (!m) return null;
|
|
4680
|
+
try {
|
|
4681
|
+
const r = JSON.parse(m[1]);
|
|
4682
|
+
if (!r || !EVENTS.includes(r.event) || typeof r.payloadSha !== "string" || !/^[0-9a-f]{64}$/.test(r.payloadSha)) return null;
|
|
4683
|
+
return {
|
|
4684
|
+
event: r.event,
|
|
4685
|
+
...r.role === "design" || r.role === "tech" || r.role === "approval" ? { role: r.role } : {},
|
|
4686
|
+
payloadSha: r.payloadSha,
|
|
4687
|
+
at: typeof r.at === "string" ? r.at : ""
|
|
4688
|
+
};
|
|
4689
|
+
} catch {
|
|
4690
|
+
return null;
|
|
4691
|
+
}
|
|
4692
|
+
}
|
|
4693
|
+
function parseSuggestionAppliedComment(body) {
|
|
4694
|
+
const m = new RegExp(`<!-- ${SUGGESTION_APPLIED_MARK} (\\{[^\\n]*?\\}) -->`).exec(body ?? "");
|
|
4695
|
+
if (!m) return null;
|
|
4696
|
+
try {
|
|
4697
|
+
const r = JSON.parse(m[1]);
|
|
4698
|
+
return r && typeof r.payloadSha === "string" ? { ...r, payloadSha: r.payloadSha, at: typeof r.at === "string" ? r.at : "" } : null;
|
|
4699
|
+
} catch {
|
|
4700
|
+
return null;
|
|
4701
|
+
}
|
|
4702
|
+
}
|
|
4703
|
+
var norm3 = (l) => l.trim().replace(/^@/, "").toLowerCase();
|
|
4704
|
+
var COLLABORATOR = ["OWNER", "MEMBER", "COLLABORATOR"];
|
|
4705
|
+
function summarizeSuggestionApprovals(comments, gov, opts) {
|
|
4706
|
+
const lists = approverLists(gov.owners, gov.rights ?? null, gov.teams);
|
|
4707
|
+
const owners = new Set([...lists.design, ...lists.tech].map(norm3));
|
|
4708
|
+
const anyOwner = owners.size > 0;
|
|
4709
|
+
const reviews = [];
|
|
4710
|
+
const latestOld = /* @__PURE__ */ new Map();
|
|
4711
|
+
const ignored = /* @__PURE__ */ new Set();
|
|
4712
|
+
const ordered = comments.map((c, i) => ({ c, i })).sort((a, b) => String(a.c.createdAt ?? "").localeCompare(String(b.c.createdAt ?? "")) || a.i - b.i);
|
|
4713
|
+
ordered.forEach(({ c }, i) => {
|
|
4714
|
+
const r = parseSuggestionReviewComment(c.body);
|
|
4715
|
+
if (!r || r.event === "comment" || !c.author) return;
|
|
4716
|
+
const login = norm3(c.author);
|
|
4717
|
+
if (opts.author && login === norm3(opts.author)) return;
|
|
4718
|
+
const counts = anyOwner ? owners.has(login) : COLLABORATOR.includes((c.authorAssociation ?? "").toUpperCase());
|
|
4719
|
+
if (!counts) {
|
|
4720
|
+
ignored.add(c.author);
|
|
4721
|
+
return;
|
|
4722
|
+
}
|
|
4723
|
+
const at = String(i).padStart(6, "0");
|
|
4724
|
+
if (r.payloadSha !== opts.payloadSha) {
|
|
4725
|
+
latestOld.set(c.author, r.event);
|
|
4726
|
+
reviews.push({ user: { login: c.author }, state: "DISMISSED", submitted_at: at });
|
|
4727
|
+
return;
|
|
4728
|
+
}
|
|
4729
|
+
latestOld.delete(c.author);
|
|
4730
|
+
reviews.push({ user: { login: c.author }, state: r.event === "approve" ? "APPROVED" : "CHANGES_REQUESTED", submitted_at: at });
|
|
4731
|
+
});
|
|
4732
|
+
const summary = summarizeApprovals(reviews, lists, opts.author ?? void 0, { soloOwnerCanApproveBoth: gov.soloOwnerCanApproveBoth });
|
|
4733
|
+
const stale = [...latestOld.keys()].filter((l) => !summary.approvers.includes(l) && !summary.changesRequested.includes(l));
|
|
4734
|
+
return { ...summary, stale, ignored: [...ignored].filter((l) => !summary.approvers.includes(l)) };
|
|
4735
|
+
}
|
|
4736
|
+
function suggestionReviewState(tier, approvals) {
|
|
4737
|
+
if (approvals.changesRequested.length) return "changes-requested";
|
|
4738
|
+
return pendingApprovals(tier, approvals).length ? "needs-review" : "approved";
|
|
4739
|
+
}
|
|
4740
|
+
|
|
4098
4741
|
// ../core/src/suggestions.ts
|
|
4742
|
+
var SUGGESTION_SCHEMA = "bridge.suggestion/1";
|
|
4743
|
+
var SUGGESTION_LABEL = "bridge-suggestion";
|
|
4744
|
+
var SUGGESTION_KINDS = ["token-change", "docs", "component-spec", "brand", "revert", "config"];
|
|
4745
|
+
var SUGGESTION_STATE_LABELS = {
|
|
4746
|
+
"needs-review": "suggestion: needs review",
|
|
4747
|
+
"changes-requested": "suggestion: changes requested",
|
|
4748
|
+
approved: "suggestion: approved",
|
|
4749
|
+
applying: "suggestion: applying"
|
|
4750
|
+
};
|
|
4751
|
+
var SUGGESTION_FINAL_LABELS = {
|
|
4752
|
+
applied: "suggestion: applied",
|
|
4753
|
+
declined: "suggestion: declined"
|
|
4754
|
+
};
|
|
4099
4755
|
var SUGGESTION_STATUS_LABEL = {
|
|
4100
4756
|
"needs-review": "Needs review",
|
|
4101
4757
|
"changes-requested": "Changes requested",
|
|
@@ -4105,6 +4761,328 @@ var SUGGESTION_STATUS_LABEL = {
|
|
|
4105
4761
|
declined: "Declined",
|
|
4106
4762
|
outdated: "Outdated"
|
|
4107
4763
|
};
|
|
4764
|
+
var SUGGESTION_LIMITS = {
|
|
4765
|
+
body: 65536,
|
|
4766
|
+
/** The readable part of the body (tables are cut with "and N more"). */
|
|
4767
|
+
human: 12e3,
|
|
4768
|
+
/** The JSON block in the body. */
|
|
4769
|
+
inline: 48e3,
|
|
4770
|
+
/** The JSON block of one continuation comment. */
|
|
4771
|
+
part: 6e4,
|
|
4772
|
+
/** Continuation comments at most (about 480 KB of file contents). */
|
|
4773
|
+
maxParts: 8
|
|
4774
|
+
};
|
|
4775
|
+
var utf8 = (text) => new TextEncoder().encode(text);
|
|
4776
|
+
var hex = (words) => words.map((w) => (w >>> 0).toString(16).padStart(8, "0")).join("");
|
|
4777
|
+
function padded(bytes) {
|
|
4778
|
+
const len = bytes.length;
|
|
4779
|
+
const total = Math.ceil((len + 9) / 64) * 64;
|
|
4780
|
+
const buf = new Uint8Array(total);
|
|
4781
|
+
buf.set(bytes);
|
|
4782
|
+
buf[len] = 128;
|
|
4783
|
+
const view = new DataView(buf.buffer);
|
|
4784
|
+
view.setUint32(total - 8, Math.floor(len / 536870912), false);
|
|
4785
|
+
view.setUint32(total - 4, len * 8 >>> 0, false);
|
|
4786
|
+
return view;
|
|
4787
|
+
}
|
|
4788
|
+
var K256 = [
|
|
4789
|
+
1116352408,
|
|
4790
|
+
1899447441,
|
|
4791
|
+
3049323471,
|
|
4792
|
+
3921009573,
|
|
4793
|
+
961987163,
|
|
4794
|
+
1508970993,
|
|
4795
|
+
2453635748,
|
|
4796
|
+
2870763221,
|
|
4797
|
+
3624381080,
|
|
4798
|
+
310598401,
|
|
4799
|
+
607225278,
|
|
4800
|
+
1426881987,
|
|
4801
|
+
1925078388,
|
|
4802
|
+
2162078206,
|
|
4803
|
+
2614888103,
|
|
4804
|
+
3248222580,
|
|
4805
|
+
3835390401,
|
|
4806
|
+
4022224774,
|
|
4807
|
+
264347078,
|
|
4808
|
+
604807628,
|
|
4809
|
+
770255983,
|
|
4810
|
+
1249150122,
|
|
4811
|
+
1555081692,
|
|
4812
|
+
1996064986,
|
|
4813
|
+
2554220882,
|
|
4814
|
+
2821834349,
|
|
4815
|
+
2952996808,
|
|
4816
|
+
3210313671,
|
|
4817
|
+
3336571891,
|
|
4818
|
+
3584528711,
|
|
4819
|
+
113926993,
|
|
4820
|
+
338241895,
|
|
4821
|
+
666307205,
|
|
4822
|
+
773529912,
|
|
4823
|
+
1294757372,
|
|
4824
|
+
1396182291,
|
|
4825
|
+
1695183700,
|
|
4826
|
+
1986661051,
|
|
4827
|
+
2177026350,
|
|
4828
|
+
2456956037,
|
|
4829
|
+
2730485921,
|
|
4830
|
+
2820302411,
|
|
4831
|
+
3259730800,
|
|
4832
|
+
3345764771,
|
|
4833
|
+
3516065817,
|
|
4834
|
+
3600352804,
|
|
4835
|
+
4094571909,
|
|
4836
|
+
275423344,
|
|
4837
|
+
430227734,
|
|
4838
|
+
506948616,
|
|
4839
|
+
659060556,
|
|
4840
|
+
883997877,
|
|
4841
|
+
958139571,
|
|
4842
|
+
1322822218,
|
|
4843
|
+
1537002063,
|
|
4844
|
+
1747873779,
|
|
4845
|
+
1955562222,
|
|
4846
|
+
2024104815,
|
|
4847
|
+
2227730452,
|
|
4848
|
+
2361852424,
|
|
4849
|
+
2428436474,
|
|
4850
|
+
2756734187,
|
|
4851
|
+
3204031479,
|
|
4852
|
+
3329325298
|
|
4853
|
+
];
|
|
4854
|
+
function sha256Hex(text) {
|
|
4855
|
+
const view = padded(utf8(text));
|
|
4856
|
+
const h = [1779033703, 3144134277, 1013904242, 2773480762, 1359893119, 2600822924, 528734635, 1541459225];
|
|
4857
|
+
const w = new Array(64);
|
|
4858
|
+
const rotr = (x, n) => x >>> n | x << 32 - n;
|
|
4859
|
+
for (let off = 0; off < view.byteLength; off += 64) {
|
|
4860
|
+
for (let i = 0; i < 16; i++) w[i] = view.getUint32(off + i * 4, false);
|
|
4861
|
+
for (let i = 16; i < 64; i++) {
|
|
4862
|
+
const a2 = w[i - 15];
|
|
4863
|
+
const b2 = w[i - 2];
|
|
4864
|
+
const s0 = rotr(a2, 7) ^ rotr(a2, 18) ^ a2 >>> 3;
|
|
4865
|
+
const s1 = rotr(b2, 17) ^ rotr(b2, 19) ^ b2 >>> 10;
|
|
4866
|
+
w[i] = w[i - 16] + s0 + w[i - 7] + s1 | 0;
|
|
4867
|
+
}
|
|
4868
|
+
let [a, b, c, d, e, f, g, hh] = h;
|
|
4869
|
+
for (let i = 0; i < 64; i++) {
|
|
4870
|
+
const t1 = hh + (rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25)) + (e & f ^ ~e & g) + K256[i] + w[i] | 0;
|
|
4871
|
+
const t2 = (rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22)) + (a & b ^ a & c ^ b & c) | 0;
|
|
4872
|
+
hh = g;
|
|
4873
|
+
g = f;
|
|
4874
|
+
f = e;
|
|
4875
|
+
e = d + t1 | 0;
|
|
4876
|
+
d = c;
|
|
4877
|
+
c = b;
|
|
4878
|
+
b = a;
|
|
4879
|
+
a = t1 + t2 | 0;
|
|
4880
|
+
}
|
|
4881
|
+
h[0] = h[0] + a | 0;
|
|
4882
|
+
h[1] = h[1] + b | 0;
|
|
4883
|
+
h[2] = h[2] + c | 0;
|
|
4884
|
+
h[3] = h[3] + d | 0;
|
|
4885
|
+
h[4] = h[4] + e | 0;
|
|
4886
|
+
h[5] = h[5] + f | 0;
|
|
4887
|
+
h[6] = h[6] + g | 0;
|
|
4888
|
+
h[7] = h[7] + hh | 0;
|
|
4889
|
+
}
|
|
4890
|
+
return hex(h);
|
|
4891
|
+
}
|
|
4892
|
+
function canonicalJson(value) {
|
|
4893
|
+
return JSON.stringify(sortKeys(value));
|
|
4894
|
+
}
|
|
4895
|
+
function sortKeys(v) {
|
|
4896
|
+
if (Array.isArray(v)) return v.map((x) => x === void 0 ? null : sortKeys(x));
|
|
4897
|
+
if (v && typeof v === "object" && !(v.toJSON instanceof Function)) {
|
|
4898
|
+
const out = {};
|
|
4899
|
+
for (const k of Object.keys(v).sort()) {
|
|
4900
|
+
const x = v[k];
|
|
4901
|
+
if (x !== void 0) out[k] = sortKeys(x);
|
|
4902
|
+
}
|
|
4903
|
+
return out;
|
|
4904
|
+
}
|
|
4905
|
+
return v;
|
|
4906
|
+
}
|
|
4907
|
+
function shaForm(p) {
|
|
4908
|
+
if (!p.files) return p;
|
|
4909
|
+
return { ...p, files: p.files.map(({ part: _part, ...f }) => f) };
|
|
4910
|
+
}
|
|
4911
|
+
function suggestionPayloadSha(p) {
|
|
4912
|
+
return sha256Hex(canonicalJson(shaForm(p)));
|
|
4913
|
+
}
|
|
4914
|
+
function suggestionKindFromLabels(labels) {
|
|
4915
|
+
for (const l of labels) {
|
|
4916
|
+
const m = /^suggestion: (.+)$/.exec(l);
|
|
4917
|
+
if (m && SUGGESTION_KINDS.includes(m[1])) return m[1];
|
|
4918
|
+
}
|
|
4919
|
+
return void 0;
|
|
4920
|
+
}
|
|
4921
|
+
function suggestionStateFromLabels(labels) {
|
|
4922
|
+
return Object.entries(SUGGESTION_STATE_LABELS).find(([, l]) => labels.includes(l))?.[0];
|
|
4923
|
+
}
|
|
4924
|
+
function suggestionStatus(i) {
|
|
4925
|
+
const prs = i.linkedPrs ?? [];
|
|
4926
|
+
if (i.state === "closed") return prs.some((p) => p.merged) || i.stateReason === "completed" ? "applied" : "declined";
|
|
4927
|
+
if (prs.some((p) => p.state === "open") || i.applied) return "applying";
|
|
4928
|
+
if (i.outdated) return "outdated";
|
|
4929
|
+
return i.review ?? "needs-review";
|
|
4930
|
+
}
|
|
4931
|
+
var SUGGESTION_STATUS_LABELS = [...Object.values(SUGGESTION_STATE_LABELS), ...Object.values(SUGGESTION_FINAL_LABELS)];
|
|
4932
|
+
var MARK2 = "bridge-suggestion";
|
|
4933
|
+
var PART_SCHEMA = "bridge.suggestion-part/1";
|
|
4934
|
+
function fencedAfter(text, from) {
|
|
4935
|
+
const m = /```json\r?\n([\s\S]*?)\r?\n```/.exec(text.slice(from));
|
|
4936
|
+
return m?.[1];
|
|
4937
|
+
}
|
|
4938
|
+
var fail = (reason, message) => ({ ok: false, reason, message });
|
|
4939
|
+
function parseSuggestionIssue(body, opts = {}) {
|
|
4940
|
+
const text = body ?? "";
|
|
4941
|
+
const markers = [...text.matchAll(new RegExp(`^<!-- ${MARK2}:v(\\d+) sha256=([0-9a-f]{64})(?: parts=(\\d+))? -->$`, "gm"))];
|
|
4942
|
+
const marker = markers[markers.length - 1];
|
|
4943
|
+
if (!marker) return fail("no-block", "This issue has no Bridge suggestion block.");
|
|
4944
|
+
if (marker[1] !== "1") return fail("unsupported-version", `This suggestion uses version ${marker[1]} of the format. Update Bridge to read it.`);
|
|
4945
|
+
const json = fencedAfter(text, marker.index + marker[0].length);
|
|
4946
|
+
if (json === void 0) return fail("invalid", "The suggestion block has no JSON. Someone may have edited it by hand: ask the author to send it again.");
|
|
4947
|
+
let raw;
|
|
4948
|
+
try {
|
|
4949
|
+
raw = JSON.parse(json);
|
|
4950
|
+
} catch {
|
|
4951
|
+
return fail("invalid", "The suggestion JSON does not parse. Someone may have edited it by hand: ask the author to send it again.");
|
|
4952
|
+
}
|
|
4953
|
+
const schema = raw?.schema;
|
|
4954
|
+
if (typeof schema === "string" && schema !== SUGGESTION_SCHEMA) {
|
|
4955
|
+
const v = /^bridge\.suggestion\/(\d+)$/.exec(schema);
|
|
4956
|
+
return v ? fail("unsupported-version", `This suggestion uses ${schema}. Update Bridge to read it.`) : fail("invalid", `"${schema}" is not a Bridge suggestion schema.`);
|
|
4957
|
+
}
|
|
4958
|
+
const problems = suggestionShapeProblems(raw);
|
|
4959
|
+
if (problems.length) return fail("invalid", `The suggestion block is not valid: ${problems.join(" ")}`);
|
|
4960
|
+
const payload = raw;
|
|
4961
|
+
const markedSha = marker[2];
|
|
4962
|
+
const expected = marker[3] ? Number(marker[3]) : 0;
|
|
4963
|
+
let edited = false;
|
|
4964
|
+
if (expected > 0 || payload.files?.some((f) => f.part !== void 0)) {
|
|
4965
|
+
const want = Math.max(expected, ...(payload.files ?? []).map((f) => f.part ?? 0));
|
|
4966
|
+
const found = /* @__PURE__ */ new Map();
|
|
4967
|
+
const author = opts.author?.toLowerCase();
|
|
4968
|
+
for (const c of opts.comments ?? []) {
|
|
4969
|
+
if (author && c.author && c.author.toLowerCase() !== author) continue;
|
|
4970
|
+
const m = new RegExp(`^<!-- ${MARK2}:v1 part (\\d+)/(\\d+) sha256=([0-9a-f]{64}) -->$`, "m").exec(c.body);
|
|
4971
|
+
if (!m) continue;
|
|
4972
|
+
const i = Number(m[1]);
|
|
4973
|
+
if (found.has(i)) continue;
|
|
4974
|
+
const pj = fencedAfter(c.body, m.index + m[0].length);
|
|
4975
|
+
if (pj === void 0) continue;
|
|
4976
|
+
if (sha256Hex(pj) !== m[3]) edited = true;
|
|
4977
|
+
try {
|
|
4978
|
+
const part = JSON.parse(pj);
|
|
4979
|
+
if (part.schema !== PART_SCHEMA || !Array.isArray(part.files)) continue;
|
|
4980
|
+
found.set(i, part.files.filter((x) => x && typeof x.i === "number" && typeof x.text === "string"));
|
|
4981
|
+
} catch {
|
|
4982
|
+
continue;
|
|
4983
|
+
}
|
|
4984
|
+
}
|
|
4985
|
+
for (let i = 1; i <= want; i++) {
|
|
4986
|
+
if (!found.has(i)) return fail("missing-part", `This suggestion is incomplete: part ${i} of ${want} is missing. It is still being written, or a comment was deleted: ask the author to send it again.`);
|
|
4987
|
+
}
|
|
4988
|
+
const files = (payload.files ?? []).map((f, idx) => {
|
|
4989
|
+
if (f.part === void 0) return f;
|
|
4990
|
+
let content = "";
|
|
4991
|
+
for (let i = 1; i <= want; i++) for (const ch of found.get(i)) if (ch.i === idx && ch.path === f.path) content += ch.text;
|
|
4992
|
+
const { part: _part, ...rest } = f;
|
|
4993
|
+
return { ...rest, content };
|
|
4994
|
+
});
|
|
4995
|
+
payload.files = files;
|
|
4996
|
+
}
|
|
4997
|
+
const sha = suggestionPayloadSha(payload);
|
|
4998
|
+
return { ok: true, payload, sha, markedSha, edited: edited || sha !== markedSha, parts: expected };
|
|
4999
|
+
}
|
|
5000
|
+
function suggestionShapeProblems(raw) {
|
|
5001
|
+
const out = [];
|
|
5002
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return ["The payload must be a JSON object."];
|
|
5003
|
+
const p = raw;
|
|
5004
|
+
if (p.schema !== SUGGESTION_SCHEMA) out.push(`"schema" must be "${SUGGESTION_SCHEMA}".`);
|
|
5005
|
+
if (!SUGGESTION_KINDS.includes(p.kind)) out.push(`"kind" must be one of ${SUGGESTION_KINDS.join(", ")}.`);
|
|
5006
|
+
if (typeof p.title !== "string" || !p.title.trim()) out.push('"title" must be a sentence.');
|
|
5007
|
+
if (typeof p.summary !== "string") out.push('"summary" must be text.');
|
|
5008
|
+
if (!["figma", "studio", "cli", "mcp"].includes(p.source)) out.push('"source" must be figma, studio, cli or mcp.');
|
|
5009
|
+
if (typeof p.createdAt !== "string") out.push('"createdAt" must be an ISO date.');
|
|
5010
|
+
const base = p.base;
|
|
5011
|
+
if (!base || typeof base.ref !== "string" || typeof base.sha !== "string" || !/^[0-9a-f]{7,64}$/.test(base.sha)) out.push('"base" must be { "ref", "sha" } with the commit the change was planned on.');
|
|
5012
|
+
const tier = p.tier;
|
|
5013
|
+
if (!tier || ![1, 2, 3].includes(tier.tier) || !["patch", "minor", "major"].includes(tier.bump)) out.push('"tier" must be { "tier": 1, 2 or 3, "bump": patch, minor or major }.');
|
|
5014
|
+
const checks = p.checks;
|
|
5015
|
+
if (!checks || typeof checks.newErrors !== "number" || !Array.isArray(checks.contrast) || typeof checks.impact !== "number" || !(checks.blocked === null || typeof checks.blocked === "string"))
|
|
5016
|
+
out.push('"checks" must be { "newErrors", "contrast", "blocked", "impact" }.');
|
|
5017
|
+
const tokens2 = p.tokens;
|
|
5018
|
+
if (tokens2 !== void 0 && (!tokens2 || !Array.isArray(tokens2.edits) || !Array.isArray(tokens2.changes))) out.push('"tokens" must be { "edits", "changes" }.');
|
|
5019
|
+
if (p.docs !== void 0 && !(Array.isArray(p.docs) && p.docs.every((d) => d && typeof d.component === "string" && typeof d.path === "string"))) out.push('"docs" must be a list of { "component", "path", "after" }.');
|
|
5020
|
+
const spec = p.componentSpec;
|
|
5021
|
+
if (spec !== void 0 && (!spec || typeof spec.component !== "string" || typeof spec.specPath !== "string" || typeof spec.brief !== "string")) out.push('"componentSpec" must be { "component", "specPath", "spec", "brief" }.');
|
|
5022
|
+
const nodes = p.links?.figma?.nodes;
|
|
5023
|
+
if (nodes !== void 0 && !(Array.isArray(nodes) && nodes.every((n) => n && typeof n.nodeId === "string"))) out.push('"links.figma.nodes" must be a list of { "nodeId", "url"?, "name"?, "state"? }.');
|
|
5024
|
+
if (p.files !== void 0) {
|
|
5025
|
+
if (!Array.isArray(p.files)) out.push('"files" must be a list.');
|
|
5026
|
+
else {
|
|
5027
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5028
|
+
for (const f of p.files) {
|
|
5029
|
+
const ok = f && typeof f.path === "string" && (f.beforeSha === null || typeof f.beforeSha === "string") && (f.afterSha === null || typeof f.afterSha === "string") && (f.content === void 0 || f.content === null || typeof f.content === "string") && (f.part === void 0 || Number.isInteger(f.part) && f.part >= 1 && f.part <= SUGGESTION_LIMITS.maxParts);
|
|
5030
|
+
if (!ok) {
|
|
5031
|
+
out.push('Each of "files" must be { "path", "beforeSha", "afterSha", "content"? }.');
|
|
5032
|
+
break;
|
|
5033
|
+
}
|
|
5034
|
+
if (seen.has(f.path)) out.push(`"${f.path}" is listed twice in "files".`);
|
|
5035
|
+
seen.add(f.path);
|
|
5036
|
+
}
|
|
5037
|
+
}
|
|
5038
|
+
}
|
|
5039
|
+
return out;
|
|
5040
|
+
}
|
|
5041
|
+
|
|
5042
|
+
// ../core/src/suggestions-github.ts
|
|
5043
|
+
function linkedPullsOf(n, pulls) {
|
|
5044
|
+
return pulls.filter((p) => closingIssueNumbers(p.body).includes(n)).map((p) => ({ number: p.number, url: p.url, state: p.state === "open" ? "open" : "closed", merged: p.state === "merged" }));
|
|
5045
|
+
}
|
|
5046
|
+
async function listSuggestionsOnGitHub(gh, opts = {}) {
|
|
5047
|
+
const issues = await gh.listIssues({ label: SUGGESTION_LABEL, state: opts.state ?? "open", perPage: opts.perPage ?? 50 });
|
|
5048
|
+
const pulls = opts.pulls ?? (issues.length ? await gh.listPullRequests("all").catch(() => []) : []);
|
|
5049
|
+
return issues.map((i) => listItem(i, pulls));
|
|
5050
|
+
}
|
|
5051
|
+
function listItem(i, pulls) {
|
|
5052
|
+
const linkedPrs = linkedPullsOf(i.number, pulls);
|
|
5053
|
+
const cached = suggestionStateFromLabels(i.labels);
|
|
5054
|
+
const t = i.labels.map((l) => /^tier: ([123])$/.exec(l)?.[1]).find(Boolean);
|
|
5055
|
+
return {
|
|
5056
|
+
number: i.number,
|
|
5057
|
+
title: i.title,
|
|
5058
|
+
url: i.url,
|
|
5059
|
+
author: i.author,
|
|
5060
|
+
createdAt: i.createdAt,
|
|
5061
|
+
state: i.state,
|
|
5062
|
+
stateReason: i.stateReason,
|
|
5063
|
+
labels: i.labels,
|
|
5064
|
+
kind: suggestionKindFromLabels(i.labels),
|
|
5065
|
+
tier: t ? Number(t) : void 0,
|
|
5066
|
+
linkedPrs,
|
|
5067
|
+
status: suggestionStatus({ state: i.state, stateReason: i.stateReason, linkedPrs, applied: cached === "applying", review: cached === "applying" ? void 0 : cached })
|
|
5068
|
+
};
|
|
5069
|
+
}
|
|
5070
|
+
async function getSuggestionOnGitHub(gh, n, gov, opts = {}) {
|
|
5071
|
+
const [issue, comments, pulls] = await Promise.all([gh.getIssue(n), gh.listIssueComments(n), opts.pulls ? Promise.resolve(opts.pulls) : gh.listPullRequests("all").catch(() => [])]);
|
|
5072
|
+
const parsed = parseSuggestionIssue(issue.body, { comments, author: issue.author || void 0 });
|
|
5073
|
+
const linkedPrs = linkedPullsOf(n, pulls);
|
|
5074
|
+
const applied = comments.flatMap((c) => parseSuggestionAppliedComment(c.body) ?? []);
|
|
5075
|
+
const approvals = parsed.ok ? summarizeSuggestionApprovals(comments, gov, { payloadSha: parsed.sha, author: issue.author }) : void 0;
|
|
5076
|
+
const status = suggestionStatus({
|
|
5077
|
+
state: issue.state,
|
|
5078
|
+
stateReason: issue.stateReason,
|
|
5079
|
+
linkedPrs,
|
|
5080
|
+
applied: parsed.ok && applied.some((a) => a.payloadSha === parsed.sha),
|
|
5081
|
+
outdated: opts.outdated,
|
|
5082
|
+
review: parsed.ok && approvals ? suggestionReviewState(parsed.payload.tier.tier, approvals) : void 0
|
|
5083
|
+
});
|
|
5084
|
+
return { issue, parsed, comments, approvals, linkedPrs, applied, status };
|
|
5085
|
+
}
|
|
4108
5086
|
|
|
4109
5087
|
// ../core/src/suggestion-apply.ts
|
|
4110
5088
|
var SUGGESTION_APPLY_EXIT = {
|
|
@@ -4311,7 +5289,7 @@ function importsIn(text) {
|
|
|
4311
5289
|
for (const m of text.matchAll(IMPORT_RE)) out.add(m[1] ?? m[2]);
|
|
4312
5290
|
return [...out];
|
|
4313
5291
|
}
|
|
4314
|
-
var
|
|
5292
|
+
var norm4 = (p) => p.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
4315
5293
|
var isStory = (p) => /\.stories\.[cm]?[jt]sx?$/.test(p);
|
|
4316
5294
|
var isTest = (p) => /\.(test|spec)\.[cm]?[jt]sx?$/.test(p);
|
|
4317
5295
|
var isCss = (p) => /\.(css|scss|less)$/.test(p);
|
|
@@ -4329,14 +5307,14 @@ function componentOf(from, spec, componentsDir) {
|
|
|
4329
5307
|
return abs.slice(componentsDir.length + 1).split("/")[0] || null;
|
|
4330
5308
|
}
|
|
4331
5309
|
function componentForPath(path, componentsDir) {
|
|
4332
|
-
const dir =
|
|
4333
|
-
const p =
|
|
5310
|
+
const dir = norm4(componentsDir);
|
|
5311
|
+
const p = norm4(path);
|
|
4334
5312
|
if (!p.startsWith(`${dir}/`)) return null;
|
|
4335
5313
|
const rest = p.slice(dir.length + 1).split("/");
|
|
4336
5314
|
return rest.length >= 2 ? rest[0] : null;
|
|
4337
5315
|
}
|
|
4338
5316
|
function buildUsageIndex(files, componentsDir) {
|
|
4339
|
-
const dir =
|
|
5317
|
+
const dir = norm4(componentsDir);
|
|
4340
5318
|
const modules = {};
|
|
4341
5319
|
const vars = /* @__PURE__ */ new Map();
|
|
4342
5320
|
const imports = /* @__PURE__ */ new Map();
|
|
@@ -4344,7 +5322,7 @@ function buildUsageIndex(files, componentsDir) {
|
|
|
4344
5322
|
const globalVars = {};
|
|
4345
5323
|
const get = (m, k) => m.get(k) ?? m.set(k, /* @__PURE__ */ new Set()).get(k);
|
|
4346
5324
|
for (const f of files) {
|
|
4347
|
-
const path =
|
|
5325
|
+
const path = norm4(f.path);
|
|
4348
5326
|
const id = componentForPath(path, dir);
|
|
4349
5327
|
if (!id) {
|
|
4350
5328
|
if (isCss(path)) {
|
|
@@ -4388,7 +5366,7 @@ function impactedModules(index, input) {
|
|
|
4388
5366
|
for (const v of changedVars) for (const id of index.byVar[v] ?? []) add(id, `uses ${v}`, false);
|
|
4389
5367
|
for (const f of input.files ?? []) {
|
|
4390
5368
|
const id = componentForPath(f, index.componentsDir);
|
|
4391
|
-
if (id && index.modules[id]) add(id, `file changed: ${
|
|
5369
|
+
if (id && index.modules[id]) add(id, `file changed: ${norm4(f)}`, false);
|
|
4392
5370
|
}
|
|
4393
5371
|
const renderedBy = /* @__PURE__ */ new Map();
|
|
4394
5372
|
for (const m of Object.values(index.modules)) for (const d of m.imports) (renderedBy.get(d) ?? renderedBy.set(d, []).get(d)).push(m.id);
|
|
@@ -4406,6 +5384,131 @@ function impactedModules(index, input) {
|
|
|
4406
5384
|
return { modules, globalHits };
|
|
4407
5385
|
}
|
|
4408
5386
|
|
|
5387
|
+
// ../core/src/history-github.ts
|
|
5388
|
+
function gitHubHistoryBackend(client, config, opts = {}) {
|
|
5389
|
+
const trees = /* @__PURE__ */ new Map();
|
|
5390
|
+
const tree = (sha) => {
|
|
5391
|
+
let t = trees.get(sha);
|
|
5392
|
+
if (!t) {
|
|
5393
|
+
t = client.getCommitTree(sha).then((ts2) => client.getTree(ts2, true)).then((r) => r.entries);
|
|
5394
|
+
trees.set(sha, t);
|
|
5395
|
+
}
|
|
5396
|
+
return t;
|
|
5397
|
+
};
|
|
5398
|
+
const body = async (sha, e) => (!client.canWrite ? await client.readRawFile(e.path, sha).catch(() => void 0) : void 0) ?? client.getBlobText(e.sha);
|
|
5399
|
+
const root = config.tokens.root.replace(/\/$/, "");
|
|
5400
|
+
const prefix = root === "." || !root ? "" : `${root}/`;
|
|
5401
|
+
const limit = Math.max(1, opts.concurrency ?? 4);
|
|
5402
|
+
const pool = async (items, fn) => {
|
|
5403
|
+
const out = new Array(items.length);
|
|
5404
|
+
let i = 0;
|
|
5405
|
+
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, async () => {
|
|
5406
|
+
for (let k = i++; k < items.length; k = i++) out[k] = await fn(items[k]);
|
|
5407
|
+
}));
|
|
5408
|
+
return out;
|
|
5409
|
+
};
|
|
5410
|
+
return {
|
|
5411
|
+
tree,
|
|
5412
|
+
async listCommits({ ref, paths, limit: max }) {
|
|
5413
|
+
const commits = [];
|
|
5414
|
+
for (let page = 1; commits.length < max && page <= 10; page++) {
|
|
5415
|
+
const batch = await client.listCommits({ sha: ref, path: paths[0], perPage: Math.min(100, max), page });
|
|
5416
|
+
for (const c of batch) {
|
|
5417
|
+
const [subject, ...rest] = c.message.split("\n");
|
|
5418
|
+
commits.push({ sha: c.sha, parents: c.parents, date: c.date, author: c.author, subject: subject ?? "", body: rest.join("\n").trim() });
|
|
5419
|
+
}
|
|
5420
|
+
if (batch.length < Math.min(100, max)) break;
|
|
5421
|
+
}
|
|
5422
|
+
const list2 = commits.slice(0, max);
|
|
5423
|
+
const merges = /* @__PURE__ */ new Map();
|
|
5424
|
+
const enriched = await pool(list2, async (c) => {
|
|
5425
|
+
const prs = await client.listPullRequestsForCommit(c.sha).catch(() => []);
|
|
5426
|
+
const pr = prs.find((p) => p.mergedAt && p.baseRef === ref) ?? prs.find((p) => p.mergedAt);
|
|
5427
|
+
if (!pr) return c;
|
|
5428
|
+
const prInfo = { number: pr.number, title: pr.title, url: pr.url, author: pr.author, ...pr.mergedAt ? { mergedAt: pr.mergedAt } : {}, body: pr.body };
|
|
5429
|
+
if (pr.mergeCommitSha && pr.mergeCommitSha !== c.sha) {
|
|
5430
|
+
let m = merges.get(pr.mergeCommitSha);
|
|
5431
|
+
if (!m) {
|
|
5432
|
+
m = client.getCommitInfo(pr.mergeCommitSha).then(
|
|
5433
|
+
(info) => {
|
|
5434
|
+
const [subject, ...rest] = info.message.split("\n");
|
|
5435
|
+
return { sha: info.sha, parents: info.parents, date: pr.mergedAt ?? info.date, author: info.author, subject: subject ?? "", body: rest.join("\n").trim() };
|
|
5436
|
+
},
|
|
5437
|
+
() => void 0
|
|
5438
|
+
);
|
|
5439
|
+
merges.set(pr.mergeCommitSha, m);
|
|
5440
|
+
}
|
|
5441
|
+
const merge = await m;
|
|
5442
|
+
if (merge) return { ...merge, pr: prInfo };
|
|
5443
|
+
}
|
|
5444
|
+
return { ...c, ...pr.mergedAt ? { date: pr.mergedAt } : {}, pr: prInfo };
|
|
5445
|
+
});
|
|
5446
|
+
return enriched;
|
|
5447
|
+
},
|
|
5448
|
+
async tokenFiles(sha) {
|
|
5449
|
+
const entries = await tree(sha);
|
|
5450
|
+
const blobs = entries.filter((e) => e.type === "blob" && e.path.startsWith(prefix));
|
|
5451
|
+
const picked = new Set(selectTokenFiles(config, blobs.map((e) => e.path)));
|
|
5452
|
+
const wanted = blobs.filter((e) => picked.has(prefix ? e.path.slice(prefix.length) : e.path));
|
|
5453
|
+
const texts = await pool(wanted, async (e) => [prefix ? e.path.slice(prefix.length) : e.path, await body(sha, e)]);
|
|
5454
|
+
return new Map(texts);
|
|
5455
|
+
},
|
|
5456
|
+
async changesetPaths(sha) {
|
|
5457
|
+
const dir = config.changesets?.dir.replace(/\/$/, "");
|
|
5458
|
+
if (!dir) return [];
|
|
5459
|
+
return (await tree(sha)).filter((e) => e.type === "blob" && e.path.startsWith(`${dir}/`)).map((e) => e.path);
|
|
5460
|
+
},
|
|
5461
|
+
async readFile(sha, path) {
|
|
5462
|
+
const e = (await tree(sha)).find((x) => x.type === "blob" && x.path === path);
|
|
5463
|
+
return e ? body(sha, e) : null;
|
|
5464
|
+
}
|
|
5465
|
+
};
|
|
5466
|
+
}
|
|
5467
|
+
async function loadGitHubHistory(client, config, opts = {}) {
|
|
5468
|
+
const ref = opts.ref ?? config.repo?.defaultBranch ?? await client.getDefaultBranch();
|
|
5469
|
+
const limit = opts.limit ?? 30;
|
|
5470
|
+
const backend = gitHubHistoryBackend(client, config);
|
|
5471
|
+
let listed = 0;
|
|
5472
|
+
const counted = { ...backend, listCommits: async (o) => {
|
|
5473
|
+
const r = await backend.listCommits(o);
|
|
5474
|
+
listed = r.length;
|
|
5475
|
+
return r;
|
|
5476
|
+
} };
|
|
5477
|
+
const entries = await collectHistory(config, counted, { ref, limit, ...opts.onProgress ? { onProgress: opts.onProgress } : {} });
|
|
5478
|
+
const head = await client.getBranchSha(ref).catch(() => void 0);
|
|
5479
|
+
let changelog = [];
|
|
5480
|
+
let currentVersion;
|
|
5481
|
+
if (head) {
|
|
5482
|
+
const files = await backend.tree(head).catch(() => []);
|
|
5483
|
+
for (const dir of packageDirCandidates(config)) {
|
|
5484
|
+
const p = (f) => dir ? `${dir}/${f}` : f;
|
|
5485
|
+
const cl = files.find((e) => e.path === p("CHANGELOG.md"));
|
|
5486
|
+
const pkg = files.find((e) => e.path === p("package.json"));
|
|
5487
|
+
if (!cl && !pkg) continue;
|
|
5488
|
+
if (cl) changelog = parseChangelog(await client.getBlobText(cl.sha));
|
|
5489
|
+
if (pkg) {
|
|
5490
|
+
try {
|
|
5491
|
+
const json = JSON.parse(await client.getBlobText(pkg.sha));
|
|
5492
|
+
if (!config.changesets || json.name === config.changesets.package) currentVersion = json.version;
|
|
5493
|
+
} catch {
|
|
5494
|
+
}
|
|
5495
|
+
}
|
|
5496
|
+
break;
|
|
5497
|
+
}
|
|
5498
|
+
}
|
|
5499
|
+
const tagList = await client.listTags(30).catch(() => []);
|
|
5500
|
+
const tagReleases = releasesFromTags(tagList, config.changesets?.package);
|
|
5501
|
+
await Promise.all(
|
|
5502
|
+
tagReleases.slice(0, 10).map(async (r) => {
|
|
5503
|
+
if (!r.sha) return;
|
|
5504
|
+
const info = await client.getCommitInfo(r.sha).catch(() => void 0);
|
|
5505
|
+
if (info?.date) r.date = info.date;
|
|
5506
|
+
})
|
|
5507
|
+
);
|
|
5508
|
+
const releases = mergeReleases(changelog, tagReleases);
|
|
5509
|
+
return buildReport({ source: "github", label: `${client.fullName}@${ref}`, ref, entries, releases, ...currentVersion ? { currentVersion } : {}, truncated: listed >= limit });
|
|
5510
|
+
}
|
|
5511
|
+
|
|
4409
5512
|
// ../core/src/fidelity-references.ts
|
|
4410
5513
|
var FIGMA_REFERENCES_DIR = "figma-references";
|
|
4411
5514
|
var FIGMA_REFERENCES_MANIFEST = `${FIGMA_REFERENCES_DIR}/manifest.json`;
|
|
@@ -4678,10 +5781,10 @@ function colorCss(c) {
|
|
|
4678
5781
|
if (typeof c === "string") return { css: c, hex: /^#[0-9a-f]{6}$/i.test(c) ? c.toLowerCase() : void 0 };
|
|
4679
5782
|
const v = c;
|
|
4680
5783
|
const [r = 0, g = 0, b = 0] = v.components;
|
|
4681
|
-
const
|
|
5784
|
+
const hex2 = (v.hex ?? `#${[r, g, b].map(toHex22).join("")}`).toLowerCase();
|
|
4682
5785
|
const alpha = v.alpha ?? 1;
|
|
4683
|
-
if (alpha < 1) return { css: `rgb(${[r, g, b].map((x) => Math.round(x * 255)).join(" ")} / ${alpha})`, hex };
|
|
4684
|
-
return { css:
|
|
5786
|
+
if (alpha < 1) return { css: `rgb(${[r, g, b].map((x) => Math.round(x * 255)).join(" ")} / ${alpha})`, hex: hex2 };
|
|
5787
|
+
return { css: hex2, hex: hex2 };
|
|
4685
5788
|
}
|
|
4686
5789
|
var dimCss = (d) => typeof d === "object" && d !== null ? `${d.value}${d.unit}` : String(d);
|
|
4687
5790
|
function shadowCss(s) {
|
|
@@ -4735,13 +5838,13 @@ function buildTokenRows(config, readText, listFiles) {
|
|
|
4735
5838
|
...componentFiles
|
|
4736
5839
|
];
|
|
4737
5840
|
const primitives = primitiveFiles.flatMap((f) => flatFile(f)).map((t) => {
|
|
4738
|
-
const { css, hex } = tokenValueCss(t.type, t.value);
|
|
5841
|
+
const { css, hex: hex2 } = tokenValueCss(t.type, t.value);
|
|
4739
5842
|
return {
|
|
4740
5843
|
path: t.path.join("."),
|
|
4741
5844
|
segments: t.path,
|
|
4742
5845
|
type: t.type,
|
|
4743
5846
|
css,
|
|
4744
|
-
hex,
|
|
5847
|
+
hex: hex2,
|
|
4745
5848
|
cssVar: tokenRowCssVar(t.path),
|
|
4746
5849
|
figma: tokenRowFigmaName(t.path),
|
|
4747
5850
|
description: t.description,
|
|
@@ -4785,8 +5888,8 @@ function buildTokenRows(config, readText, listFiles) {
|
|
|
4785
5888
|
current = hit.value;
|
|
4786
5889
|
currentType = hit.type || currentType;
|
|
4787
5890
|
}
|
|
4788
|
-
const { css, hex } = tokenValueCss(currentType, current);
|
|
4789
|
-
return { alias: chain[0] ?? null, chain, css, hex };
|
|
5891
|
+
const { css, hex: hex2 } = tokenValueCss(currentType, current);
|
|
5892
|
+
return { alias: chain[0] ?? null, chain, css, hex: hex2 };
|
|
4790
5893
|
};
|
|
4791
5894
|
const same2 = (a, c) => a.css === c.css && a.chain.join() === c.chain.join();
|
|
4792
5895
|
const build = (t, tier, valueFor) => {
|
|
@@ -4846,8 +5949,8 @@ var channel = (c) => {
|
|
|
4846
5949
|
const s = c / 255;
|
|
4847
5950
|
return s <= 0.04045 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
|
|
4848
5951
|
};
|
|
4849
|
-
function hexLuminance(
|
|
4850
|
-
const n = parseInt(
|
|
5952
|
+
function hexLuminance(hex2) {
|
|
5953
|
+
const n = parseInt(hex2.slice(1), 16);
|
|
4851
5954
|
return 0.2126 * channel(n >> 16 & 255) + 0.7152 * channel(n >> 8 & 255) + 0.0722 * channel(n & 255);
|
|
4852
5955
|
}
|
|
4853
5956
|
function hexContrastRatio(a, b) {
|
|
@@ -5038,7 +6141,7 @@ function scanComponents(input) {
|
|
|
5038
6141
|
const docsSuffix = input.docsSuffix ?? ".docs.json";
|
|
5039
6142
|
const styleSuffix = input.styleSuffix ?? ".module.css";
|
|
5040
6143
|
const fileSet = new Set(input.files);
|
|
5041
|
-
const
|
|
6144
|
+
const has9 = (p) => fileSet.has(p);
|
|
5042
6145
|
const fam = input.indexText === void 0 ? [] : parseComponentFamilies(input.indexText, input.indexRel ?? "components");
|
|
5043
6146
|
const familyOf = (name) => fam.find((f) => f.members.includes(name))?.name;
|
|
5044
6147
|
const names = [
|
|
@@ -5051,7 +6154,7 @@ function scanComponents(input) {
|
|
|
5051
6154
|
const folder = `${dir}/${name}`;
|
|
5052
6155
|
const files = { tsx: `${folder}/${name}.tsx`, css: `${folder}/${name}${styleSuffix}`, stories: `${folder}/${name}.stories.tsx`, docs: `${folder}/${name}${docsSuffix}` };
|
|
5053
6156
|
let docs = null;
|
|
5054
|
-
if (
|
|
6157
|
+
if (has9(files.docs)) {
|
|
5055
6158
|
try {
|
|
5056
6159
|
docs = JSON.parse(readText(files.docs) ?? "");
|
|
5057
6160
|
} catch (err) {
|
|
@@ -5061,10 +6164,10 @@ function scanComponents(input) {
|
|
|
5061
6164
|
const tsxSrc = readText(files.tsx) ?? "";
|
|
5062
6165
|
const tsx = parseComponentTsx(name, tsxSrc);
|
|
5063
6166
|
const reuses = [...tsxSrc.matchAll(reuseRe)].filter((m) => m[1] === m[2] && m[1] !== name).map((m) => m[1]);
|
|
5064
|
-
const cssText =
|
|
6167
|
+
const cssText = has9(files.css) ? readText(files.css) : void 0;
|
|
5065
6168
|
const rules = cssText === void 0 ? [] : parseComponentCss(cssText, docs?.anatomy ?? {}, describeVar);
|
|
5066
6169
|
const used = [...new Set(rules.flatMap((r) => r.vars.map((v) => v.name)))].sort();
|
|
5067
|
-
const storiesText =
|
|
6170
|
+
const storiesText = has9(files.stories) ? readText(files.stories) : void 0;
|
|
5068
6171
|
return {
|
|
5069
6172
|
name,
|
|
5070
6173
|
slug: componentSlug(name),
|
|
@@ -5459,6 +6562,389 @@ function snapshotLlmsTxt(model, snapshot) {
|
|
|
5459
6562
|
`;
|
|
5460
6563
|
}
|
|
5461
6564
|
|
|
6565
|
+
// ../core/src/activity.ts
|
|
6566
|
+
var ACTIVITY_SCHEMA_VERSION = 1;
|
|
6567
|
+
var ACTIVITY_FILE = "activity.json";
|
|
6568
|
+
var ACTIVITY_MAX_BYTES = 3e6;
|
|
6569
|
+
var ACTIVITY_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
6570
|
+
var ACTIVITY_LIMITS = {
|
|
6571
|
+
/** Open pull requests recorded in full (the page of each; the list carries every open one). */
|
|
6572
|
+
branches: 30,
|
|
6573
|
+
/** Open suggestions read with their comments (parsed payload, counted approvals). */
|
|
6574
|
+
suggestions: 30,
|
|
6575
|
+
/** Request issues listed (open and closed); open ones are read with their comments. */
|
|
6576
|
+
requests: 30,
|
|
6577
|
+
/** Merged pull requests summarized. */
|
|
6578
|
+
merged: 20,
|
|
6579
|
+
/** Token history entries (core loadGitHubHistory limit). */
|
|
6580
|
+
history: 20,
|
|
6581
|
+
/** History entries whose two commits' file lists are recorded, so their before/after opens without the API. */
|
|
6582
|
+
historyTrees: 10
|
|
6583
|
+
};
|
|
6584
|
+
var ActivityError = class extends Error {
|
|
6585
|
+
};
|
|
6586
|
+
var isObj3 = (x) => !!x && typeof x === "object" && !Array.isArray(x);
|
|
6587
|
+
var API2 = "https://api.github.com";
|
|
6588
|
+
function activityKey(url) {
|
|
6589
|
+
let u;
|
|
6590
|
+
try {
|
|
6591
|
+
u = new URL(url, API2);
|
|
6592
|
+
} catch {
|
|
6593
|
+
return null;
|
|
6594
|
+
}
|
|
6595
|
+
if (u.origin !== API2) return null;
|
|
6596
|
+
const parts = u.pathname.split("/");
|
|
6597
|
+
if (parts[1] === "repos" && parts.length > 3) {
|
|
6598
|
+
parts[2] = parts[2].toLowerCase();
|
|
6599
|
+
parts[3] = parts[3].toLowerCase();
|
|
6600
|
+
} else if (parts[1] === "users" && parts.length > 2) parts[2] = parts[2].toLowerCase();
|
|
6601
|
+
const q = [...u.searchParams].sort(([a, av], [b, bv]) => a === b ? av < bv ? -1 : 1 : a < b ? -1 : 1);
|
|
6602
|
+
const search = q.length ? `?${new URLSearchParams(q).toString()}` : "";
|
|
6603
|
+
return parts.join("/") + search;
|
|
6604
|
+
}
|
|
6605
|
+
var KEPT_URLS = /* @__PURE__ */ new Set(["html_url", "avatar_url", "target_url", "details_url", "environment_url", "raw_url"]);
|
|
6606
|
+
var DROPPED = /* @__PURE__ */ new Set(["_links", "patch", "body_html", "body_text", "reactions", "performed_via_github_app", "node_id", "gravatar_id", "user_view_type", "site_admin", "event", "sub_issues_summary", "issue_dependencies_summary", "pull_request_review_id"]);
|
|
6607
|
+
var LONG_TEXT = 6e4;
|
|
6608
|
+
var trimUser = (u) => ({
|
|
6609
|
+
login: u.login,
|
|
6610
|
+
...u.id !== void 0 ? { id: u.id } : {},
|
|
6611
|
+
...u.avatar_url ? { avatar_url: u.avatar_url } : {},
|
|
6612
|
+
...u.html_url ? { html_url: u.html_url } : {},
|
|
6613
|
+
...u.type ? { type: u.type } : {}
|
|
6614
|
+
});
|
|
6615
|
+
var trimRepo = (r) => ({
|
|
6616
|
+
...r.id !== void 0 ? { id: r.id } : {},
|
|
6617
|
+
name: r.name,
|
|
6618
|
+
full_name: r.full_name,
|
|
6619
|
+
...r.private !== void 0 ? { private: r.private } : {},
|
|
6620
|
+
...r.fork !== void 0 ? { fork: r.fork } : {},
|
|
6621
|
+
...r.default_branch ? { default_branch: r.default_branch } : {},
|
|
6622
|
+
...isObj3(r.owner) ? { owner: trimUser(r.owner) } : {},
|
|
6623
|
+
...r.html_url ? { html_url: r.html_url } : {}
|
|
6624
|
+
});
|
|
6625
|
+
function trimGitHub(value, key = "") {
|
|
6626
|
+
if (typeof value === "string") return value.length > LONG_TEXT ? value.slice(0, LONG_TEXT) : value;
|
|
6627
|
+
if (Array.isArray(value)) return value.map((v) => trimGitHub(v, key));
|
|
6628
|
+
if (!isObj3(value)) return value;
|
|
6629
|
+
if (key === "tree" && typeof value.path === "string" && typeof value.type === "string") return { path: value.path, type: value.type, sha: value.sha, ...value.mode ? { mode: value.mode } : {} };
|
|
6630
|
+
if ((key === "repo" || key === "repository" || key === "head_repository") && typeof value.full_name === "string") return trimRepo(value);
|
|
6631
|
+
if (typeof value.login === "string" && ("avatar_url" in value || "type" in value)) return trimUser(value);
|
|
6632
|
+
const out = {};
|
|
6633
|
+
for (const [k, v] of Object.entries(value)) {
|
|
6634
|
+
if (DROPPED.has(k)) continue;
|
|
6635
|
+
if ((k === "url" || k.endsWith("_url")) && !KEPT_URLS.has(k)) continue;
|
|
6636
|
+
if (k === "output" && isObj3(v)) {
|
|
6637
|
+
out[k] = { title: v.title ?? null, summary: typeof v.summary === "string" ? v.summary.slice(0, 2e3) : null, annotations_count: v.annotations_count ?? 0 };
|
|
6638
|
+
continue;
|
|
6639
|
+
}
|
|
6640
|
+
if (k === "app" && isObj3(v)) {
|
|
6641
|
+
out[k] = { id: v.id, slug: v.slug, name: v.name };
|
|
6642
|
+
continue;
|
|
6643
|
+
}
|
|
6644
|
+
out[k] = trimGitHub(v, k);
|
|
6645
|
+
}
|
|
6646
|
+
return out;
|
|
6647
|
+
}
|
|
6648
|
+
var JSON_ACCEPT = /application\/(vnd\.github\+)?json/;
|
|
6649
|
+
var SKIP2 = /* @__PURE__ */ Symbol("skip");
|
|
6650
|
+
function activityTreeFilter(config) {
|
|
6651
|
+
const under = (dir) => {
|
|
6652
|
+
const d = (dir ?? "").replace(/^\.\/|\/$/g, "");
|
|
6653
|
+
return d && d !== "." ? `${d}/` : "";
|
|
6654
|
+
};
|
|
6655
|
+
const tokens2 = under(config.tokens.root);
|
|
6656
|
+
const changesets = config.changesets ? under(config.changesets.dir) : null;
|
|
6657
|
+
const pkg = new Set(packageDirCandidates(config).flatMap((d) => [d ? `${d}/CHANGELOG.md` : "CHANGELOG.md", d ? `${d}/package.json` : "package.json"]));
|
|
6658
|
+
return (p) => p === "bridge.config.json" || pkg.has(p) || (tokens2 ? p.startsWith(tokens2) : p.endsWith(".json")) || changesets !== null && changesets !== "" && p.startsWith(changesets);
|
|
6659
|
+
}
|
|
6660
|
+
var UNREAD = [/\/git\/blobs\//, /\/commits\/[^/]+\/pulls$/, /\/commits\?/, /\/tags\?/, /\/contents\//];
|
|
6661
|
+
function activityShaper(config) {
|
|
6662
|
+
const keep = activityTreeFilter(config);
|
|
6663
|
+
return (key, body) => {
|
|
6664
|
+
if (UNREAD.some((r) => r.test(key))) return SKIP2;
|
|
6665
|
+
if (key.includes("/git/trees/") && isObj3(body) && Array.isArray(body.tree)) return { sha: body.sha, tree: body.tree.filter((e) => keep(e.path)), truncated: Boolean(body.truncated) };
|
|
6666
|
+
if (key.includes("/compare/") && isObj3(body)) {
|
|
6667
|
+
const c = body;
|
|
6668
|
+
return {
|
|
6669
|
+
status: c.status,
|
|
6670
|
+
ahead_by: c.ahead_by,
|
|
6671
|
+
behind_by: c.behind_by,
|
|
6672
|
+
total_commits: c.total_commits,
|
|
6673
|
+
merge_base_commit: c.merge_base_commit ? { sha: c.merge_base_commit.sha } : null,
|
|
6674
|
+
base_commit: c.base_commit ? { sha: c.base_commit.sha, commit: { tree: { sha: c.base_commit.commit?.tree?.sha } } } : null,
|
|
6675
|
+
files: Array.isArray(c.files) ? c.files.map((f) => ({ filename: f.filename, status: f.status, ...f.previous_filename ? { previous_filename: f.previous_filename } : {}, sha: f.sha, additions: f.additions, deletions: f.deletions, changes: f.changes })) : []
|
|
6676
|
+
};
|
|
6677
|
+
}
|
|
6678
|
+
return body;
|
|
6679
|
+
};
|
|
6680
|
+
}
|
|
6681
|
+
var HISTORY_MAX_CHANGES = 100;
|
|
6682
|
+
function capHistory(report, max = HISTORY_MAX_CHANGES) {
|
|
6683
|
+
return {
|
|
6684
|
+
...report,
|
|
6685
|
+
entries: report.entries.map((e) => e.changes.length > max || (e.reasons?.length ?? 0) > max ? { ...e, changes: e.changes.slice(0, max), ...e.reasons ? { reasons: e.reasons.slice(0, max) } : {} } : e)
|
|
6686
|
+
};
|
|
6687
|
+
}
|
|
6688
|
+
function recordingFetch(fetchImpl, into, shape = (_k, b) => b) {
|
|
6689
|
+
return async (url, init2) => {
|
|
6690
|
+
const res = await fetchImpl(url, init2);
|
|
6691
|
+
const method = (init2?.method ?? "GET").toUpperCase();
|
|
6692
|
+
const accept = init2?.headers?.Accept ?? init2?.headers?.accept ?? "application/vnd.github+json";
|
|
6693
|
+
const key = activityKey(url);
|
|
6694
|
+
if (method !== "GET" || !key || !JSON_ACCEPT.test(accept) || res.status !== 200 && res.status !== 404) return res;
|
|
6695
|
+
const text = await res.text();
|
|
6696
|
+
let body;
|
|
6697
|
+
try {
|
|
6698
|
+
body = JSON.parse(text);
|
|
6699
|
+
} catch {
|
|
6700
|
+
return { ok: res.ok, status: res.status, headers: res.headers, json: async () => JSON.parse(text), text: async () => text };
|
|
6701
|
+
}
|
|
6702
|
+
const shaped = shape(key, trimGitHub(body));
|
|
6703
|
+
if (shaped !== SKIP2) into.set(key, { status: res.status, body: shaped });
|
|
6704
|
+
return { ok: res.ok, status: res.status, ...res.headers ? { headers: res.headers } : {}, json: async () => JSON.parse(text), text: async () => text };
|
|
6705
|
+
};
|
|
6706
|
+
}
|
|
6707
|
+
var labelObjs = (labels) => labelNames(labels).map((name) => ({ name }));
|
|
6708
|
+
function labelNames(labels) {
|
|
6709
|
+
return (labels ?? []).map((l) => typeof l === "string" ? l : l.name ?? "").filter(Boolean);
|
|
6710
|
+
}
|
|
6711
|
+
var byUpdated = (a, b) => a.updated_at < b.updated_at ? 1 : a.updated_at > b.updated_at ? -1 : 0;
|
|
6712
|
+
async function collectActivity(input) {
|
|
6713
|
+
const limits = { ...ACTIVITY_LIMITS, ...input.limits };
|
|
6714
|
+
const responses = /* @__PURE__ */ new Map();
|
|
6715
|
+
const rec = recordingFetch(input.fetch, responses, activityShaper(input.config));
|
|
6716
|
+
const client = new GitHubClient(`${input.owner}/${input.repo}`, input.token, rec, { attempts: 2, baseDelayMs: 500, sleep: (ms) => new Promise((r) => setTimeout(r, ms)) }, { etags: false });
|
|
6717
|
+
const R = `/repos/${input.owner}/${input.repo}`;
|
|
6718
|
+
const problems = [];
|
|
6719
|
+
const truncated = [];
|
|
6720
|
+
let limited = false;
|
|
6721
|
+
const headers = { Accept: "application/vnd.github+json", "X-GitHub-Api-Version": "2022-11-28", ...input.token ? { Authorization: `Bearer ${input.token}` } : {} };
|
|
6722
|
+
const get = async (path) => {
|
|
6723
|
+
if (limited) return void 0;
|
|
6724
|
+
try {
|
|
6725
|
+
const res = await rec(`${API2}${path}`, { headers });
|
|
6726
|
+
if (res.status === 404) return void 0;
|
|
6727
|
+
if (!res.ok) {
|
|
6728
|
+
if ((res.status === 403 || res.status === 429) && res.headers?.get("x-ratelimit-remaining") === "0") {
|
|
6729
|
+
limited = true;
|
|
6730
|
+
problems.push("GitHub hourly limit reached while reading activity: the rest was left out.");
|
|
6731
|
+
} else problems.push(`GitHub answered ${res.status} for ${path}`);
|
|
6732
|
+
return void 0;
|
|
6733
|
+
}
|
|
6734
|
+
return await res.json();
|
|
6735
|
+
} catch (err) {
|
|
6736
|
+
problems.push(`Could not read ${path}: ${err.message}`);
|
|
6737
|
+
return void 0;
|
|
6738
|
+
}
|
|
6739
|
+
};
|
|
6740
|
+
const safely = async (what, fn, fallback) => {
|
|
6741
|
+
if (limited) return fallback;
|
|
6742
|
+
try {
|
|
6743
|
+
return await fn();
|
|
6744
|
+
} catch (err) {
|
|
6745
|
+
const msg = err.message;
|
|
6746
|
+
if (/limit/i.test(msg) && err.status === 429) limited = true;
|
|
6747
|
+
problems.push(`${what}: ${msg}`);
|
|
6748
|
+
return fallback;
|
|
6749
|
+
}
|
|
6750
|
+
};
|
|
6751
|
+
const info = await safely("Repository", () => client.getRepo(), null);
|
|
6752
|
+
const branch = info?.defaultBranch ?? input.config.repo?.defaultBranch ?? "main";
|
|
6753
|
+
const headSha = await safely("Default branch", () => client.getBranchSha(branch), void 0) ?? "";
|
|
6754
|
+
const open = await get(`${R}/pulls?state=open&per_page=100`) ?? [];
|
|
6755
|
+
const closed = await get(`${R}/pulls?state=closed&sort=updated&direction=desc&per_page=30`) ?? [];
|
|
6756
|
+
await get(`${R}/pulls?state=closed&per_page=50&sort=updated&direction=desc`);
|
|
6757
|
+
await get(`${R}/rules/branches/${encodeURIComponent(branch)}`);
|
|
6758
|
+
await get(`${R}/git/matching-refs/heads/bridge-visual`);
|
|
6759
|
+
await safely("Owner account", () => client.getAccountType(input.owner), null);
|
|
6760
|
+
const prev = input.previous ?? null;
|
|
6761
|
+
const sameRepoPrev = Boolean(prev && sameRepo(prev, input));
|
|
6762
|
+
const prevOk = sameRepoPrev && Boolean(headSha) && prev.headSha === headSha;
|
|
6763
|
+
const kept = async (path, reuse) => {
|
|
6764
|
+
const key = reuse ? activityKey(`${API2}${path}`) : null;
|
|
6765
|
+
const hit = key ? prev.responses[key] : void 0;
|
|
6766
|
+
if (key && hit) {
|
|
6767
|
+
responses.set(key, hit);
|
|
6768
|
+
return hit.status === 200 ? hit.body : void 0;
|
|
6769
|
+
}
|
|
6770
|
+
return get(path);
|
|
6771
|
+
};
|
|
6772
|
+
const branches4 = [];
|
|
6773
|
+
const sorted = [...open].sort(byUpdated);
|
|
6774
|
+
if (sorted.length > limits.branches) truncated.push(`${sorted.length - limits.branches} open branches beyond the ${limits.branches} most recently updated are listed without their pages.`);
|
|
6775
|
+
for (const [i, p] of sorted.entries()) {
|
|
6776
|
+
const full = i < limits.branches;
|
|
6777
|
+
const base = {
|
|
6778
|
+
number: p.number,
|
|
6779
|
+
title: p.title,
|
|
6780
|
+
author: p.user?.login ?? "",
|
|
6781
|
+
url: p.html_url,
|
|
6782
|
+
branch: p.head.ref,
|
|
6783
|
+
base: p.base.ref,
|
|
6784
|
+
headSha: p.head.sha,
|
|
6785
|
+
baseSha: p.base.sha,
|
|
6786
|
+
state: p.draft ? "draft" : "open",
|
|
6787
|
+
labels: labelNames(p.labels),
|
|
6788
|
+
tier: effectiveTier(labelObjs(p.labels), p.body ?? null, null),
|
|
6789
|
+
createdAt: p.created_at,
|
|
6790
|
+
updatedAt: p.updated_at,
|
|
6791
|
+
comments: null,
|
|
6792
|
+
files: null,
|
|
6793
|
+
previews: [],
|
|
6794
|
+
checks: null
|
|
6795
|
+
};
|
|
6796
|
+
if (!full || limited) {
|
|
6797
|
+
branches4.push(base);
|
|
6798
|
+
continue;
|
|
6799
|
+
}
|
|
6800
|
+
const n = p.number;
|
|
6801
|
+
const was = prevOk ? prev.branches.find((b) => b.number === n) : void 0;
|
|
6802
|
+
const same2 = Boolean(was && was.updatedAt === p.updated_at && was.headSha === p.head.sha && was.comments !== null);
|
|
6803
|
+
const [pull, files, , , , , status] = await Promise.all([
|
|
6804
|
+
kept(`${R}/pulls/${n}`, same2),
|
|
6805
|
+
kept(`${R}/pulls/${n}/files?per_page=100`, same2),
|
|
6806
|
+
kept(`${R}/pulls/${n}/reviews?per_page=100`, same2),
|
|
6807
|
+
kept(`${R}/issues/${n}/comments?per_page=100`, same2),
|
|
6808
|
+
kept(`${R}/pulls/${n}/comments?per_page=100`, same2),
|
|
6809
|
+
get(`${R}/commits/${p.head.sha}/check-runs?per_page=100`),
|
|
6810
|
+
get(`${R}/commits/${p.head.sha}/status`),
|
|
6811
|
+
kept(`${R}/compare/${encodeURIComponent(p.base.ref)}...${p.head.sha}`, same2),
|
|
6812
|
+
kept(`${R}/git/trees/${p.base.sha}?recursive=1`, sameRepoPrev),
|
|
6813
|
+
kept(`${R}/git/trees/${p.head.sha}?recursive=1`, sameRepoPrev),
|
|
6814
|
+
kept(`${R}/issues/${n}/labels`, same2)
|
|
6815
|
+
]);
|
|
6816
|
+
const root = input.config.tokens.root.replace(/\/$/, "");
|
|
6817
|
+
branches4.push({
|
|
6818
|
+
...base,
|
|
6819
|
+
tier: effectiveTier(labelObjs((pull ?? p).labels), (pull ?? p).body ?? null, files ?? null),
|
|
6820
|
+
comments: pull ? (pull.comments ?? 0) + (pull.review_comments ?? 0) : null,
|
|
6821
|
+
files: files ? { changed: files.length, tokens: files.map((f) => f.filename).filter((f) => f.startsWith(`${root}/`) && f.endsWith(".json")) } : null,
|
|
6822
|
+
previews: (status?.statuses ?? []).filter((s) => s.state === "success" && s.target_url && /^https:\/\//.test(s.target_url)).map((s) => ({ context: s.context, url: s.target_url })),
|
|
6823
|
+
checks: status ? status.state === "success" || status.state === "failure" || status.state === "pending" ? status.state : "none" : null
|
|
6824
|
+
});
|
|
6825
|
+
}
|
|
6826
|
+
const gov = { owners: input.config.governance.owners, rights: input.config.governance.rights, soloOwnerCanApproveBoth: input.config.governance.soloOwnerCanApproveBoth };
|
|
6827
|
+
const items = await safely("Suggestions", () => listSuggestionsOnGitHub(client, { state: "open" }), []);
|
|
6828
|
+
const pullsAll = items.length ? await safely("Pull requests", () => client.listPullRequests("all"), []) : [];
|
|
6829
|
+
if (items.length > limits.suggestions) truncated.push(`${items.length - limits.suggestions} open suggestions beyond the ${limits.suggestions} newest are listed without their pages.`);
|
|
6830
|
+
const suggestions2 = [];
|
|
6831
|
+
for (const [i, s] of items.entries()) {
|
|
6832
|
+
const d = i < limits.suggestions ? await safely(`Suggestion #${s.number}`, () => getSuggestionOnGitHub(client, s.number, gov, { pulls: pullsAll }), null) : null;
|
|
6833
|
+
suggestions2.push({
|
|
6834
|
+
number: s.number,
|
|
6835
|
+
title: s.title,
|
|
6836
|
+
author: s.author,
|
|
6837
|
+
url: s.url,
|
|
6838
|
+
createdAt: s.createdAt,
|
|
6839
|
+
kind: s.kind ?? null,
|
|
6840
|
+
status: d?.status ?? s.status,
|
|
6841
|
+
labels: s.labels,
|
|
6842
|
+
comments: d ? d.comments.length : null,
|
|
6843
|
+
payload: d?.parsed.ok ? d.parsed.payload : null,
|
|
6844
|
+
approvers: d?.approvals?.approvers ?? []
|
|
6845
|
+
});
|
|
6846
|
+
}
|
|
6847
|
+
const issues = await safely("Requests", () => client.listIssues({ label: REQUEST_LABEL, state: "all", perPage: 50 }), []);
|
|
6848
|
+
const requests3 = [];
|
|
6849
|
+
const openIssues = issues.filter((i) => i.state === "open");
|
|
6850
|
+
if (openIssues.length > limits.requests) truncated.push(`${openIssues.length - limits.requests} open requests beyond the ${limits.requests} newest are listed without their pages.`);
|
|
6851
|
+
for (const [i, r] of issues.slice(0, Math.max(limits.requests, 0)).entries()) {
|
|
6852
|
+
if (r.state === "open" && i < limits.requests) {
|
|
6853
|
+
await get(`${R}/issues/${r.number}`);
|
|
6854
|
+
await get(`${R}/issues/${r.number}/comments?per_page=100`);
|
|
6855
|
+
}
|
|
6856
|
+
const parsed = parseRequestIssue(r.body);
|
|
6857
|
+
const linked = [...open, ...closed].filter((p) => new RegExp(`(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\\s+#${r.number}\\b`, "i").test(p.body ?? ""));
|
|
6858
|
+
requests3.push({
|
|
6859
|
+
number: r.number,
|
|
6860
|
+
title: r.title,
|
|
6861
|
+
author: r.author,
|
|
6862
|
+
url: r.url,
|
|
6863
|
+
state: r.state,
|
|
6864
|
+
status: requestStatus({ state: r.state, stateReason: r.stateReason, linkedPrs: linked.map((p) => ({ state: p.state === "open" ? "open" : "closed", merged: Boolean(p.merged_at) })) }),
|
|
6865
|
+
kind: parsed?.kind ?? null,
|
|
6866
|
+
component: parsed?.component ?? null,
|
|
6867
|
+
labels: r.labels,
|
|
6868
|
+
createdAt: r.createdAt
|
|
6869
|
+
});
|
|
6870
|
+
}
|
|
6871
|
+
const merged = closed.filter((p) => p.merged_at).slice(0, limits.merged).map((p) => ({ number: p.number, title: p.title, author: p.user?.login ?? "", url: p.html_url, mergedAt: p.merged_at, labels: labelNames(p.labels), tier: effectiveTier(labelObjs(p.labels), p.body ?? null, null) }));
|
|
6872
|
+
let history = null;
|
|
6873
|
+
if (prevOk && prev.history) {
|
|
6874
|
+
history = prev.history;
|
|
6875
|
+
for (const [k, v] of Object.entries(prev.responses)) if (isHistoryKey(k) && !responses.has(k)) responses.set(k, v);
|
|
6876
|
+
} else if (!limited && headSha) {
|
|
6877
|
+
const read = await safely("Token history", () => loadGitHubHistory(client, input.config, { ref: branch, limit: limits.history }), null);
|
|
6878
|
+
history = read && capHistory(read);
|
|
6879
|
+
if (history) {
|
|
6880
|
+
const backend = gitHubHistoryBackend(client, input.config, { concurrency: 2 });
|
|
6881
|
+
const shas = [...new Set(history.entries.filter((e) => !e.bulk).slice(0, limits.historyTrees).flatMap((e) => [e.sha, ...e.baseSha ? [e.baseSha] : []]))];
|
|
6882
|
+
for (const sha of shas) await safely(`Files at ${sha.slice(0, 7)}`, () => backend.tree(sha), []);
|
|
6883
|
+
}
|
|
6884
|
+
}
|
|
6885
|
+
const activity = {
|
|
6886
|
+
kind: "bridge-activity",
|
|
6887
|
+
schemaVersion: ACTIVITY_SCHEMA_VERSION,
|
|
6888
|
+
tool: { name: "bridge", version: input.version },
|
|
6889
|
+
generatedAt: input.generatedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
6890
|
+
repo: { owner: input.owner, name: input.repo, branch },
|
|
6891
|
+
headSha,
|
|
6892
|
+
branches: branches4,
|
|
6893
|
+
suggestions: suggestions2,
|
|
6894
|
+
requests: requests3,
|
|
6895
|
+
merged,
|
|
6896
|
+
history,
|
|
6897
|
+
responses: Object.fromEntries([...responses].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)),
|
|
6898
|
+
truncated,
|
|
6899
|
+
problems
|
|
6900
|
+
};
|
|
6901
|
+
return fitActivity(activity);
|
|
6902
|
+
}
|
|
6903
|
+
var sameRepo = (a, b) => a.repo.owner.toLowerCase() === b.owner.toLowerCase() && a.repo.name.toLowerCase() === b.repo.toLowerCase();
|
|
6904
|
+
var isHistoryKey = (k) => /\/git\/commits\/[0-9a-f]+$/.test(k) || /\/git\/trees\/[0-9a-f]+\?recursive=1$/.test(k);
|
|
6905
|
+
var activityBytes = (a) => new TextEncoder().encode(`${JSON.stringify(a)}
|
|
6906
|
+
`).length;
|
|
6907
|
+
function fitActivity(activity, max = ACTIVITY_MAX_BYTES) {
|
|
6908
|
+
if (activityBytes(activity) <= max) return activity;
|
|
6909
|
+
const out = { ...activity, responses: { ...activity.responses }, truncated: [...activity.truncated] };
|
|
6910
|
+
const drop = (test) => {
|
|
6911
|
+
for (const k of Object.keys(out.responses)) if (test(k)) delete out.responses[k];
|
|
6912
|
+
};
|
|
6913
|
+
drop((k) => k.includes("/git/trees/"));
|
|
6914
|
+
out.truncated.push("File lists were left out to keep activity.json small: a branch or history change reads them from GitHub.");
|
|
6915
|
+
if (activityBytes(out) <= max) return out;
|
|
6916
|
+
const old = [...out.branches].sort((a, b) => a.updatedAt < b.updatedAt ? -1 : 1);
|
|
6917
|
+
for (const b of old) {
|
|
6918
|
+
drop((k) => new RegExp(`/(pulls|issues)/${b.number}(/|$|\\?)`).test(k) || k.includes(`/commits/${b.headSha}/`) || k.includes(`...${b.headSha}`));
|
|
6919
|
+
if (activityBytes(out) <= max) {
|
|
6920
|
+
out.truncated.push("The pages of the least recently updated branches were left out: they read from GitHub.");
|
|
6921
|
+
return out;
|
|
6922
|
+
}
|
|
6923
|
+
}
|
|
6924
|
+
out.responses = {};
|
|
6925
|
+
out.truncated.push("Every recorded read was left out: pages read GitHub live.");
|
|
6926
|
+
return out;
|
|
6927
|
+
}
|
|
6928
|
+
function parseActivity(input) {
|
|
6929
|
+
let data = input;
|
|
6930
|
+
if (typeof input === "string") {
|
|
6931
|
+
try {
|
|
6932
|
+
data = JSON.parse(input);
|
|
6933
|
+
} catch (err) {
|
|
6934
|
+
throw new ActivityError(`${ACTIVITY_FILE} is not valid JSON (${err.message})`);
|
|
6935
|
+
}
|
|
6936
|
+
}
|
|
6937
|
+
if (!isObj3(data) || data.kind !== "bridge-activity") throw new ActivityError(`${ACTIVITY_FILE} is not a Bridge activity file`);
|
|
6938
|
+
if (data.schemaVersion !== ACTIVITY_SCHEMA_VERSION) throw new ActivityError(`${ACTIVITY_FILE} is schema version ${String(data.schemaVersion)}; this version of Bridge reads ${ACTIVITY_SCHEMA_VERSION}`);
|
|
6939
|
+
const repo = data.repo;
|
|
6940
|
+
if (!isObj3(repo) || typeof repo.owner !== "string" || typeof repo.name !== "string" || typeof repo.branch !== "string") throw new ActivityError(`${ACTIVITY_FILE}: repo needs owner, name and branch`);
|
|
6941
|
+
if (typeof data.generatedAt !== "string" || Number.isNaN(Date.parse(data.generatedAt))) throw new ActivityError(`${ACTIVITY_FILE}: generatedAt is missing`);
|
|
6942
|
+
for (const k of ["branches", "suggestions", "requests", "merged", "truncated", "problems"]) if (!Array.isArray(data[k])) throw new ActivityError(`${ACTIVITY_FILE}: ${k} must be a list`);
|
|
6943
|
+
if (!isObj3(data.responses) || !Object.values(data.responses).every((r) => isObj3(r) && typeof r.status === "number")) throw new ActivityError(`${ACTIVITY_FILE}: responses must map paths to { status, body }`);
|
|
6944
|
+
if (data.history !== null && !(isObj3(data.history) && Array.isArray(data.history.entries))) throw new ActivityError(`${ACTIVITY_FILE}: history must be null or a history report`);
|
|
6945
|
+
return data;
|
|
6946
|
+
}
|
|
6947
|
+
|
|
5462
6948
|
// ../visual/src/capture.ts
|
|
5463
6949
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
5464
6950
|
import { dirname as dirname2 } from "node:path";
|
|
@@ -5859,17 +7345,17 @@ var shotKey = (...parts) => parts.map((p) => p.toLowerCase().replace(/[^a-z0-9-]
|
|
|
5859
7345
|
|
|
5860
7346
|
// ../visual/src/select.ts
|
|
5861
7347
|
var NO_VISUAL = /(\.test\.[cm]?[jt]sx?|\.spec\.[cm]?[jt]sx?|\.docs\.json|\.mdx?|README[^/]*)$/i;
|
|
5862
|
-
var
|
|
7348
|
+
var norm5 = (p) => p.replace(/^\.\//, "").replace(/\\/g, "/");
|
|
5863
7349
|
var isAllStates = (s) => /--all-states$/.test(s.id) || /^all ?states$/i.test(s.name);
|
|
5864
7350
|
function storyComponent(s, componentsDir) {
|
|
5865
|
-
return componentForPath(
|
|
7351
|
+
return componentForPath(norm5(s.importPath), componentsDir);
|
|
5866
7352
|
}
|
|
5867
7353
|
function selectImpact(input) {
|
|
5868
7354
|
const { usage, stories } = input;
|
|
5869
7355
|
const dir = usage.componentsDir;
|
|
5870
7356
|
const lib = input.librarySrc.replace(/\/$/, "");
|
|
5871
7357
|
const cap = input.maxStoriesPerComponent ?? 4;
|
|
5872
|
-
const visualFiles = input.files.map(
|
|
7358
|
+
const visualFiles = input.files.map(norm5).filter((f) => !NO_VISUAL.test(f));
|
|
5873
7359
|
const vars = [...new Set(input.tokens.map((t) => input.cssVar(t.path)))].sort();
|
|
5874
7360
|
const impact2 = impactedModules(usage, { vars, files: visualFiles });
|
|
5875
7361
|
let fallback = null;
|
|
@@ -5909,7 +7395,7 @@ function selectImpact(input) {
|
|
|
5909
7395
|
return {
|
|
5910
7396
|
tokens: input.tokens,
|
|
5911
7397
|
vars,
|
|
5912
|
-
files: input.files.map(
|
|
7398
|
+
files: input.files.map(norm5),
|
|
5913
7399
|
components: components.map((c) => ({ id: c.id, reasons: c.reasons, indirect: c.indirect })),
|
|
5914
7400
|
stories: [...new Set(picked)],
|
|
5915
7401
|
fallback,
|
|
@@ -5928,7 +7414,7 @@ function storiesFromSource(files) {
|
|
|
5928
7414
|
if (!title) continue;
|
|
5929
7415
|
for (const m of f.text.matchAll(/export\s+const\s+([A-Z]\w*)\s*[:=]/g)) {
|
|
5930
7416
|
const name = m[1];
|
|
5931
|
-
out.push({ id: storyId(title, name), title, name: name.replace(/([a-z0-9])([A-Z])/g, "$1 $2"), importPath: `./${
|
|
7417
|
+
out.push({ id: storyId(title, name), title, name: name.replace(/([a-z0-9])([A-Z])/g, "$1 $2"), importPath: `./${norm5(f.path)}` });
|
|
5932
7418
|
}
|
|
5933
7419
|
}
|
|
5934
7420
|
return out;
|
|
@@ -6556,7 +8042,7 @@ async function jiraSync(input) {
|
|
|
6556
8042
|
log(`Bridge Jira sync: ${reason}`);
|
|
6557
8043
|
return { status: "skipped", reason };
|
|
6558
8044
|
};
|
|
6559
|
-
const
|
|
8045
|
+
const fail2 = (reason) => {
|
|
6560
8046
|
log(`::error::Bridge Jira sync: ${reason}`);
|
|
6561
8047
|
return { status: "error", reason };
|
|
6562
8048
|
};
|
|
@@ -6571,7 +8057,7 @@ async function jiraSync(input) {
|
|
|
6571
8057
|
if (!n || !repo) return skip("not an issues event.");
|
|
6572
8058
|
if (!["opened", "labeled", "closed"].includes(action)) return skip(`nothing to do on "${action}".`);
|
|
6573
8059
|
if (action === "labeled" && event.label?.name !== REQUEST_LABEL) return skip(`label "${event.label?.name ?? ""}" is not ${REQUEST_LABEL}.`);
|
|
6574
|
-
if (!env.GITHUB_TOKEN) return
|
|
8060
|
+
if (!env.GITHUB_TOKEN) return fail2("GITHUB_TOKEN is missing: pass secrets.GITHUB_TOKEN to the step.");
|
|
6575
8061
|
const ghFetch = (url2, init2) => f(url2, init2);
|
|
6576
8062
|
const gh = new GitHubClient(repo, env.GITHUB_TOKEN, ghFetch);
|
|
6577
8063
|
const issue = await gh.getIssue(n);
|
|
@@ -6600,14 +8086,14 @@ async function jiraSync(input) {
|
|
|
6600
8086
|
const text = `Closed in GitHub (#${n}), ${outcome}`;
|
|
6601
8087
|
const body = { body: { type: "doc", version: 1, content: [{ type: "paragraph", content: [{ type: "text", text, marks: [{ type: "link", attrs: { href: issue.url } }] }] }] } };
|
|
6602
8088
|
const res2 = await f(`${base}/rest/api/3/issue/${encodeURIComponent(key)}/comment`, { method: "POST", headers, body: JSON.stringify(body) });
|
|
6603
|
-
if (!res2.ok) return
|
|
8089
|
+
if (!res2.ok) return fail2(await jiraError(res2));
|
|
6604
8090
|
log(`Bridge Jira sync: commented on ${key}: ${text}.`);
|
|
6605
8091
|
return { status: "commented", key };
|
|
6606
8092
|
}
|
|
6607
8093
|
if (key) return skip(`#${n} is already linked to ${key}.`);
|
|
6608
8094
|
const request = requests.requestFromIssue(issue);
|
|
6609
8095
|
const res = await f(`${base}/rest/api/3/issue`, { method: "POST", headers, body: JSON.stringify({ fields: jiraIssueFields(request, jira, issue.url) }) });
|
|
6610
|
-
if (!res.ok) return
|
|
8096
|
+
if (!res.ok) return fail2(await jiraError(res));
|
|
6611
8097
|
const created = await res.json();
|
|
6612
8098
|
const url = `${base}/browse/${created.key}`;
|
|
6613
8099
|
await gh.addComment(n, `Jira: ${created.key} ${url}`);
|
|
@@ -6637,7 +8123,7 @@ Exit codes: 0 ok, 1 not a request or Claude Code not found, 2 usage or setup err
|
|
|
6637
8123
|
var RequestUsageError = class extends Error {
|
|
6638
8124
|
};
|
|
6639
8125
|
var flag = (a, k) => a.flags[k]?.[a.flags[k].length - 1];
|
|
6640
|
-
var
|
|
8126
|
+
var has2 = (a, k) => Boolean(a.flags[k]);
|
|
6641
8127
|
var defaultIO = () => ({
|
|
6642
8128
|
print: (s) => process.stdout.write(`${s}
|
|
6643
8129
|
`),
|
|
@@ -6697,9 +8183,9 @@ async function getOrExplain(ctx, n, io, branch) {
|
|
|
6697
8183
|
}
|
|
6698
8184
|
async function runRequest(a, root, io = defaultIO()) {
|
|
6699
8185
|
const sub = a._[1];
|
|
6700
|
-
if (!sub || sub === "help" ||
|
|
8186
|
+
if (!sub || sub === "help" || has2(a, "help")) {
|
|
6701
8187
|
io.print(REQUEST_HELP);
|
|
6702
|
-
return sub ||
|
|
8188
|
+
return sub || has2(a, "help") ? 0 : 2;
|
|
6703
8189
|
}
|
|
6704
8190
|
switch (sub) {
|
|
6705
8191
|
case "list": {
|
|
@@ -6708,7 +8194,7 @@ async function runRequest(a, root, io = defaultIO()) {
|
|
|
6708
8194
|
const ctx = await context(root, io, false);
|
|
6709
8195
|
const component = flag(a, "component");
|
|
6710
8196
|
const r = await requests2.listRequests(ctx, { status, ...component ? { component } : {} });
|
|
6711
|
-
if (
|
|
8197
|
+
if (has2(a, "json")) return io.print(JSON.stringify(r, null, 2)), 0;
|
|
6712
8198
|
if (!r.requests.length) return io.print(`No ${status === "all" ? "" : `${status} `}component change request in ${r.repo}${component ? ` for ${component}` : ""}.`), 0;
|
|
6713
8199
|
const w = Math.max(...r.requests.map((x) => x.component.length));
|
|
6714
8200
|
for (const x of r.requests) {
|
|
@@ -6722,7 +8208,7 @@ async function runRequest(a, root, io = defaultIO()) {
|
|
|
6722
8208
|
const ctx = await context(root, io, false);
|
|
6723
8209
|
const r = await getOrExplain(ctx, n, io);
|
|
6724
8210
|
if (!r) return 1;
|
|
6725
|
-
if (
|
|
8211
|
+
if (has2(a, "json")) return io.print(JSON.stringify(r, null, 2)), 0;
|
|
6726
8212
|
const q = r.request;
|
|
6727
8213
|
io.print(`#${r.number} ${q.component}: ${q.title}`);
|
|
6728
8214
|
io.print(`${REQUEST_STATUS_LABEL[r.status]}, ${q.kind}, from ${q.source}${q.requestedBy ? ` by @${q.requestedBy}` : ""}`);
|
|
@@ -6752,7 +8238,7 @@ Next: bridge request implement ${r.number} --claude`);
|
|
|
6752
8238
|
const branch = flag(a, "branch");
|
|
6753
8239
|
const r = await getOrExplain(ctx, n, io, branch);
|
|
6754
8240
|
if (!r) return 1;
|
|
6755
|
-
if (!
|
|
8241
|
+
if (!has2(a, "claude")) return io.print(r.brief), 0;
|
|
6756
8242
|
if (!io.hasClaude()) {
|
|
6757
8243
|
io.print(r.brief);
|
|
6758
8244
|
io.error(`
|
|
@@ -6770,7 +8256,7 @@ Claude Code is not installed (no \`claude\` on PATH). Install it (npm install -g
|
|
|
6770
8256
|
const usage = 'bridge request create --component Button --kind new-variant --title "Add a danger ghost variant" --summary "..."';
|
|
6771
8257
|
if (!component || !kind || !title || !summary) throw new RequestUsageError(`Usage: ${usage}`);
|
|
6772
8258
|
if (!REQUEST_KINDS.some((k) => k.value === kind)) throw new RequestUsageError(`--kind takes ${REQUEST_KINDS.map((k) => k.value).join(", ")}, got "${kind}"`);
|
|
6773
|
-
const dryRun =
|
|
8259
|
+
const dryRun = has2(a, "dry-run");
|
|
6774
8260
|
const ctx = await context(root, io, !dryRun);
|
|
6775
8261
|
const figma2 = flag(a, "figma");
|
|
6776
8262
|
const exploration = flag(a, "exploration");
|
|
@@ -6786,7 +8272,7 @@ Claude Code is not installed (no \`claude\` on PATH). Install it (npm install -g
|
|
|
6786
8272
|
source: "cli",
|
|
6787
8273
|
dryRun
|
|
6788
8274
|
});
|
|
6789
|
-
if (
|
|
8275
|
+
if (has2(a, "json")) return io.print(JSON.stringify(r, null, 2)), 0;
|
|
6790
8276
|
if (r.status === "dry-run") {
|
|
6791
8277
|
io.print(`Dry run, nothing created.
|
|
6792
8278
|
|
|
@@ -6845,7 +8331,7 @@ Work on one: git switch explore/<name>, then \`bridge propose --branch explore/<
|
|
|
6845
8331
|
GitHub token: GITHUB_TOKEN or BRIDGE_GITHUB_TOKEN, else \`gh auth token\`.
|
|
6846
8332
|
Exit codes: 0 ok, 2 usage or setup error.`;
|
|
6847
8333
|
var flag2 = (a, k) => a.flags[k]?.[a.flags[k].length - 1];
|
|
6848
|
-
var
|
|
8334
|
+
var has3 = (a, k) => Boolean(a.flags[k]);
|
|
6849
8335
|
async function context2(root, io) {
|
|
6850
8336
|
const token = io.env.BRIDGE_GITHUB_TOKEN || io.env.GITHUB_TOKEN || io.ghToken();
|
|
6851
8337
|
return BridgeContext3.create({ root, ...token ? { token } : {}, ...io.fetch ? { fetch: io.fetch } : {} });
|
|
@@ -6854,11 +8340,11 @@ var exploreSource = (env) => ({ source: "cli", ...env.CLAUDECODE ? { agent: "cla
|
|
|
6854
8340
|
var date = (iso) => iso ? iso.slice(0, 10) : "";
|
|
6855
8341
|
async function runExplore(a, root, io = defaultIO()) {
|
|
6856
8342
|
const sub = a._[1];
|
|
6857
|
-
if (!sub || sub === "help" ||
|
|
8343
|
+
if (!sub || sub === "help" || has3(a, "help")) {
|
|
6858
8344
|
io.print(EXPLORE_HELP);
|
|
6859
|
-
return sub ||
|
|
8345
|
+
return sub || has3(a, "help") ? 0 : 2;
|
|
6860
8346
|
}
|
|
6861
|
-
const json =
|
|
8347
|
+
const json = has3(a, "json");
|
|
6862
8348
|
switch (sub) {
|
|
6863
8349
|
case "start": {
|
|
6864
8350
|
const name = a._.slice(2).join(" ").trim() || flag2(a, "name");
|
|
@@ -6866,7 +8352,7 @@ async function runExplore(a, root, io = defaultIO()) {
|
|
|
6866
8352
|
if (!name || !purpose) throw new RequestUsageError('Usage: bridge explore start <name> --purpose "What you want to learn"');
|
|
6867
8353
|
const useCase = flag2(a, "use-case");
|
|
6868
8354
|
const ctx = await context2(root, io);
|
|
6869
|
-
const r = await explorations.startExploration(ctx, { name, purpose, ...exploreSource(io.env), ...useCase ? { useCase } : {}, ...a.flags.experimental ? { experimental: a.flags.experimental } : {}, ...flag2(a, "product") ? { product: flag2(a, "product") } : {}, dryRun:
|
|
8355
|
+
const r = await explorations.startExploration(ctx, { name, purpose, ...exploreSource(io.env), ...useCase ? { useCase } : {}, ...a.flags.experimental ? { experimental: a.flags.experimental } : {}, ...flag2(a, "product") ? { product: flag2(a, "product") } : {}, dryRun: has3(a, "dry-run") });
|
|
6870
8356
|
if (json) return io.print(JSON.stringify(r, null, 2)), 0;
|
|
6871
8357
|
if (r.status === "dry-run") {
|
|
6872
8358
|
io.print(`Dry run, nothing written.
|
|
@@ -6890,7 +8376,7 @@ Next: ${r.next}`);
|
|
|
6890
8376
|
}
|
|
6891
8377
|
case "list": {
|
|
6892
8378
|
const ctx = await context2(root, io);
|
|
6893
|
-
const r = await explorations.listExplorations(ctx, { state:
|
|
8379
|
+
const r = await explorations.listExplorations(ctx, { state: has3(a, "all") ? "all" : "open" });
|
|
6894
8380
|
if (json) return io.print(JSON.stringify(r, null, 2)), 0;
|
|
6895
8381
|
io.print(r.summary);
|
|
6896
8382
|
for (const x of r.explorations) {
|
|
@@ -6911,7 +8397,7 @@ Local only: ${b} (push it and open a draft pull request labeled exploration)`);
|
|
|
6911
8397
|
const ref = a._.slice(2).join(" ").trim();
|
|
6912
8398
|
if (!ref) throw new RequestUsageError("Usage: bridge explore promote <name|#pr> [--dry-run]");
|
|
6913
8399
|
const ctx = await context2(root, io);
|
|
6914
|
-
const r = await explorations.promoteExploration(ctx, { ref, dryRun:
|
|
8400
|
+
const r = await explorations.promoteExploration(ctx, { ref, dryRun: has3(a, "dry-run") });
|
|
6915
8401
|
if (json) return io.print(JSON.stringify(r, null, 2)), 0;
|
|
6916
8402
|
io.print(`${r.status === "dry-run" ? "Dry run: would promote" : "Promoted"} #${r.number} "${r.title}" to review: Tier ${r.tier}.`);
|
|
6917
8403
|
io.print(`Labels: +${r.addLabels.join(", +")}, -${r.removeLabels.join(", -")}${r.removeFiles.length ? `. Removes ${r.removeFiles.join(", ")}` : ""}.`);
|
|
@@ -6965,10 +8451,10 @@ GitHub token: GITHUB_TOKEN or BRIDGE_GITHUB_TOKEN, else \`gh auth token\`. Readi
|
|
|
6965
8451
|
Exit codes (apply): 0 applied, already applied or a clean dry run; 1 not approved, blocked by a check or refused;
|
|
6966
8452
|
2 usage or setup error; 3 conflict with today's code.`;
|
|
6967
8453
|
var flag3 = (a, k) => a.flags[k]?.[a.flags[k].length - 1];
|
|
6968
|
-
var
|
|
8454
|
+
var has4 = (a, k) => Boolean(a.flags[k]);
|
|
6969
8455
|
function changesFlag(a) {
|
|
6970
|
-
if (
|
|
6971
|
-
return
|
|
8456
|
+
if (has4(a, "suggest") && has4(a, "direct")) throw new SuggestUsageError("Pass --suggest or --direct, not both.");
|
|
8457
|
+
return has4(a, "direct") ? "direct" : has4(a, "suggest") ? "suggest" : void 0;
|
|
6972
8458
|
}
|
|
6973
8459
|
function tokenFrom2(io) {
|
|
6974
8460
|
return io.env.BRIDGE_GITHUB_TOKEN || io.env.GITHUB_TOKEN || io.ghToken();
|
|
@@ -6986,7 +8472,7 @@ var STATUSES = ["needs-review", "changes-requested", "approved", "applying", "ap
|
|
|
6986
8472
|
var KINDS = ["token-change", "docs", "component-spec", "brand", "revert", "config"];
|
|
6987
8473
|
async function runSuggestions(a, root, io = defaultIO()) {
|
|
6988
8474
|
const sub = a._[1] ?? "list";
|
|
6989
|
-
if (sub === "help" ||
|
|
8475
|
+
if (sub === "help" || has4(a, "help")) return io.print(SUGGESTIONS_HELP), 0;
|
|
6990
8476
|
switch (sub) {
|
|
6991
8477
|
case "list": {
|
|
6992
8478
|
const status = flag3(a, "status") ?? "all";
|
|
@@ -6995,7 +8481,7 @@ async function runSuggestions(a, root, io = defaultIO()) {
|
|
|
6995
8481
|
if (kind && !KINDS.includes(kind)) throw new SuggestUsageError(`--kind takes ${KINDS.join(", ")}, got "${kind}"`);
|
|
6996
8482
|
const ctx = await context3(root, io);
|
|
6997
8483
|
const r = await suggestions.listSuggestionsOp(ctx, { status, ...kind ? { kind } : {} });
|
|
6998
|
-
if (
|
|
8484
|
+
if (has4(a, "json")) return io.print(JSON.stringify(r, null, 2)), 0;
|
|
6999
8485
|
if (!r.suggestions.length) return io.print(`No ${status === "all" ? "open " : `${status} `}suggestion in ${r.repo}.`), 0;
|
|
7000
8486
|
for (const x of r.suggestions) {
|
|
7001
8487
|
const prs = x.linkedPrs.length ? ` PR ${x.linkedPrs.map((p) => `#${p.number}${p.merged ? " merged" : ` ${p.state}`}`).join(", ")}` : "";
|
|
@@ -7009,7 +8495,7 @@ ${r.next}`);
|
|
|
7009
8495
|
const n = issueNumber2(a._[2], "bridge suggestions show <number>");
|
|
7010
8496
|
const ctx = await context3(root, io);
|
|
7011
8497
|
const r = await suggestions.getSuggestionOp(ctx, { number: n });
|
|
7012
|
-
if (
|
|
8498
|
+
if (has4(a, "json")) return io.print(JSON.stringify(r, null, 2)), 0;
|
|
7013
8499
|
io.print(`#${r.number} ${r.title}`);
|
|
7014
8500
|
io.print(`${r.said}${r.kind ? `, ${r.kind}` : ""}${r.tier ? `, tier ${r.tier} (${r.bump})` : ""}${r.source ? `, from ${r.source}` : ""}${r.suggestedBy ? ` by @${r.suggestedBy}` : ""}`);
|
|
7015
8501
|
io.print(r.url);
|
|
@@ -7047,7 +8533,7 @@ Next: ${r.next}`);
|
|
|
7047
8533
|
if (!pr && !branch) throw new SuggestUsageError("Usage: bridge suggestions link <number> --pr <m> (or --branch <name>)");
|
|
7048
8534
|
const ctx = await context3(root, io);
|
|
7049
8535
|
const r = await suggestions.linkSuggestionOp(ctx, { number: n, ...pr ? { pr } : {}, ...branch ? { branch } : {} });
|
|
7050
|
-
if (
|
|
8536
|
+
if (has4(a, "json")) return io.print(JSON.stringify(r, null, 2)), 0;
|
|
7051
8537
|
io.print(`#${n} is being applied${pr ? ` in #${pr}` : branch ? ` on ${branch}` : ""}.`);
|
|
7052
8538
|
for (const w of r.warnings ?? []) io.error(`warning: ${w}`);
|
|
7053
8539
|
return 0;
|
|
@@ -7060,23 +8546,23 @@ ${SUGGESTIONS_HELP}`);
|
|
|
7060
8546
|
}
|
|
7061
8547
|
var fmt = (v) => v === void 0 ? "(none)" : typeof v === "string" ? v : JSON.stringify(v);
|
|
7062
8548
|
async function runApply(a, root, io = defaultIO()) {
|
|
7063
|
-
if (
|
|
8549
|
+
if (has4(a, "help") || a._[1] === "help") return io.print(SUGGESTIONS_HELP), 0;
|
|
7064
8550
|
const n = issueNumber2(a._[1], "bridge apply <number> [--write | --patch] [--dry-run]");
|
|
7065
|
-
if (
|
|
7066
|
-
const mode2 =
|
|
8551
|
+
if (has4(a, "write") && has4(a, "patch")) throw new SuggestUsageError("Pass --write or --patch, not both.");
|
|
8552
|
+
const mode2 = has4(a, "write") ? "write" : has4(a, "patch") ? "patch" : "branch";
|
|
7067
8553
|
const ctx = await context3(root, io);
|
|
7068
8554
|
if (ctx.mode !== "local" || !ctx.isGit) throw new SuggestUsageError("bridge apply runs in a git checkout of the design system: cd into it, or pass --root.");
|
|
7069
8555
|
const branch = flag3(a, "branch");
|
|
7070
8556
|
const r = await suggestions.applySuggestionOp(ctx, {
|
|
7071
8557
|
number: n,
|
|
7072
|
-
mode:
|
|
7073
|
-
acceptEdited:
|
|
7074
|
-
forceUnapproved:
|
|
8558
|
+
mode: has4(a, "dry-run") ? "dry-run" : mode2,
|
|
8559
|
+
acceptEdited: has4(a, "accept-edited"),
|
|
8560
|
+
forceUnapproved: has4(a, "force-unapproved"),
|
|
7075
8561
|
...branch ? { branch } : {}
|
|
7076
8562
|
});
|
|
7077
8563
|
const code = typeof r.exitCode === "number" ? r.exitCode : SUGGESTION_APPLY_EXIT[r.outcome] ?? 1;
|
|
7078
|
-
if (
|
|
7079
|
-
if (mode2 === "patch" && r.patch && !
|
|
8564
|
+
if (has4(a, "json")) return io.print(JSON.stringify(r, null, 2)), code;
|
|
8565
|
+
if (mode2 === "patch" && r.patch && !has4(a, "dry-run")) {
|
|
7080
8566
|
io.print(r.patch.trimEnd());
|
|
7081
8567
|
io.error(r.message);
|
|
7082
8568
|
for (const w of r.warnings ?? []) io.error(`warning: ${w}`);
|
|
@@ -7100,10 +8586,10 @@ Next: ${r.next}`);
|
|
|
7100
8586
|
return code;
|
|
7101
8587
|
}
|
|
7102
8588
|
async function runReview(a, root, io = defaultIO()) {
|
|
7103
|
-
if (
|
|
8589
|
+
if (has4(a, "help") || a._[1] === "help") return io.print(SUGGESTIONS_HELP), 0;
|
|
7104
8590
|
const usage = 'bridge review <number> --approve | --request-changes --message "..." | --decline --message "..."';
|
|
7105
8591
|
const n = issueNumber2(a._[1], usage);
|
|
7106
|
-
const events = ["approve", "request-changes", "decline", "comment"].filter((e) =>
|
|
8592
|
+
const events = ["approve", "request-changes", "decline", "comment"].filter((e) => has4(a, e));
|
|
7107
8593
|
if (events.length !== 1) throw new SuggestUsageError(`Usage: ${usage}`);
|
|
7108
8594
|
const event = events[0];
|
|
7109
8595
|
const inline = flag3(a, event);
|
|
@@ -7120,7 +8606,7 @@ async function runReview(a, root, io = defaultIO()) {
|
|
|
7120
8606
|
...role ? { role } : {},
|
|
7121
8607
|
...version ? { payloadSha: version } : {}
|
|
7122
8608
|
});
|
|
7123
|
-
if (
|
|
8609
|
+
if (has4(a, "json")) return io.print(JSON.stringify(r, null, 2)), 0;
|
|
7124
8610
|
const done = event === "approve" ? "Approved" : event === "request-changes" ? "Changes requested on" : event === "decline" ? "Declined" : "Commented on";
|
|
7125
8611
|
io.print(`${done} #${n}${r.version ? ` (version ${r.version})` : ""}. Now: ${SUGGESTION_STATUS_LABEL[r.status] ?? r.status}.`);
|
|
7126
8612
|
for (const w of r.warnings ?? []) io.error(`warning: ${w}`);
|
|
@@ -7159,7 +8645,7 @@ Usage:
|
|
|
7159
8645
|
Start work for a product: bridge propose|explore start|request create ... --product <key> [--jira DS-42].
|
|
7160
8646
|
GitHub token: GITHUB_TOKEN or BRIDGE_GITHUB_TOKEN, else \`gh auth token\`. Exit codes: 0 ok, 2 usage or setup error.`;
|
|
7161
8647
|
var flag4 = (a, k) => a.flags[k]?.[a.flags[k].length - 1];
|
|
7162
|
-
var
|
|
8648
|
+
var has5 = (a, k) => Boolean(a.flags[k]);
|
|
7163
8649
|
async function context4(root, io) {
|
|
7164
8650
|
const token = io.env.BRIDGE_GITHUB_TOKEN || io.env.GITHUB_TOKEN || io.ghToken();
|
|
7165
8651
|
return BridgeContext5.create({ root, ...token ? { token } : {}, ...io.fetch ? { fetch: io.fetch } : {} });
|
|
@@ -7181,11 +8667,11 @@ function pairs2(list2, what) {
|
|
|
7181
8667
|
var said = (r) => String(r.said ?? r.summary ?? r.status ?? "Done.");
|
|
7182
8668
|
async function runBranch(a, root, io = defaultIO()) {
|
|
7183
8669
|
const sub = a._[1];
|
|
7184
|
-
if (!sub || sub === "help" ||
|
|
8670
|
+
if (!sub || sub === "help" || has5(a, "help")) {
|
|
7185
8671
|
io.print(BRANCH_HELP);
|
|
7186
|
-
return sub ||
|
|
8672
|
+
return sub || has5(a, "help") ? 0 : 2;
|
|
7187
8673
|
}
|
|
7188
|
-
const json =
|
|
8674
|
+
const json = has5(a, "json");
|
|
7189
8675
|
const ctx = await context4(root, io);
|
|
7190
8676
|
const out = (r, text) => (json ? io.print(JSON.stringify(r, null, 2)) : text(), 0);
|
|
7191
8677
|
switch (sub) {
|
|
@@ -7203,6 +8689,7 @@ async function runBranch(a, root, io = defaultIO()) {
|
|
|
7203
8689
|
io.print(`#${r.pr} ${r.title} (${r.product.name})`);
|
|
7204
8690
|
io.print(`${r.status.label}. ${r.approvalNeeded} needed; approved by ${r.approvals.approvedBy.map((x) => `@${x}`).join(", ") || "nobody yet"}.`);
|
|
7205
8691
|
if (r.approvals.soloOwner) io.print(r.approvals.soloOwner);
|
|
8692
|
+
io.print(`${r.tests.label}.`);
|
|
7206
8693
|
if (r.merge.blockedBy?.length) for (const x of r.merge.blockedBy) io.print(` ${x}`);
|
|
7207
8694
|
io.print(`Next: ${r.next}`);
|
|
7208
8695
|
io.print(r.url);
|
|
@@ -7225,14 +8712,14 @@ async function runBranch(a, root, io = defaultIO()) {
|
|
|
7225
8712
|
}
|
|
7226
8713
|
case "review": {
|
|
7227
8714
|
const n = number(a, 2, 'bridge branch review <n> --approve | --request-changes "..." | --comment "..."');
|
|
7228
|
-
const decision =
|
|
8715
|
+
const decision = has5(a, "approve") ? "approve" : has5(a, "request-changes") ? "request_changes" : has5(a, "comment") ? "comment" : null;
|
|
7229
8716
|
if (!decision) throw new RequestUsageError('Say what you decide: --approve, --request-changes "what to change" or --comment "text".');
|
|
7230
8717
|
const body = decision === "approve" ? flag4(a, "body") : flag4(a, decision === "request_changes" ? "request-changes" : "comment");
|
|
7231
8718
|
const r = await branches.reviewBranch(ctx, { pr: n, decision, ...body && body !== "true" ? { body } : {} });
|
|
7232
8719
|
return out(r, () => io.print(r.said));
|
|
7233
8720
|
}
|
|
7234
8721
|
case "ask": {
|
|
7235
|
-
const r = await branches.askSomeoneElse(ctx, { pr: number(a, 2, "bridge branch ask <n>"), dryRun:
|
|
8722
|
+
const r = await branches.askSomeoneElse(ctx, { pr: number(a, 2, "bridge branch ask <n>"), dryRun: has5(a, "dry-run") });
|
|
7236
8723
|
return out(r, () => io.print(r.said));
|
|
7237
8724
|
}
|
|
7238
8725
|
case "comment": {
|
|
@@ -7246,7 +8733,7 @@ async function runBranch(a, root, io = defaultIO()) {
|
|
|
7246
8733
|
case "merge": {
|
|
7247
8734
|
const n = number(a, 2, 'bridge branch merge <n> [--override "reason"] [--dry-run]');
|
|
7248
8735
|
const reason = flag4(a, "override");
|
|
7249
|
-
const r = await branches.mergeBranch(ctx, { pr: n, ...reason ? { overrideReason: reason } : {}, dryRun:
|
|
8736
|
+
const r = await branches.mergeBranch(ctx, { pr: n, ...reason ? { overrideReason: reason } : {}, dryRun: has5(a, "dry-run") });
|
|
7250
8737
|
return out(r, () => io.print(r.said));
|
|
7251
8738
|
}
|
|
7252
8739
|
case "close": {
|
|
@@ -7261,7 +8748,7 @@ async function runBranch(a, root, io = defaultIO()) {
|
|
|
7261
8748
|
const n = number(a, 2, "bridge branch update <n> [--choice key=mine|theirs] [--dry-run]");
|
|
7262
8749
|
const choices = pairs2(a.flags.choice, "choice");
|
|
7263
8750
|
for (const [k, v] of Object.entries(choices)) if (v !== "mine" && v !== "theirs") throw new RequestUsageError(`--choice ${k}= takes mine or theirs, got "${v}".`);
|
|
7264
|
-
const r = await branches.updateFromMain(ctx, { pr: n, ...Object.keys(choices).length ? { choices } : {}, dryRun:
|
|
8751
|
+
const r = await branches.updateFromMain(ctx, { pr: n, ...Object.keys(choices).length ? { choices } : {}, dryRun: has5(a, "dry-run") });
|
|
7265
8752
|
return out(r, () => {
|
|
7266
8753
|
io.print(said(r));
|
|
7267
8754
|
for (const c of r.conflicts ?? []) io.print(` ${c.key} (${c.token}, default ${c.default ?? "none: pick one"})`);
|
|
@@ -7294,7 +8781,7 @@ ${BRANCH_HELP}`);
|
|
|
7294
8781
|
}
|
|
7295
8782
|
async function runStudioRead(cmd, a, root, io = defaultIO()) {
|
|
7296
8783
|
const ctx = await context4(root, io);
|
|
7297
|
-
const json =
|
|
8784
|
+
const json = has5(a, "json");
|
|
7298
8785
|
if (cmd === "products") {
|
|
7299
8786
|
const r2 = await branches.listProducts(ctx);
|
|
7300
8787
|
if (json) return io.print(JSON.stringify(r2, null, 2)), 0;
|
|
@@ -7345,9 +8832,9 @@ async function runDocsSave(a, root, io = defaultIO()) {
|
|
|
7345
8832
|
...flag4(a, "product") ? { product: flag4(a, "product") } : {},
|
|
7346
8833
|
...changes ? { changes } : {},
|
|
7347
8834
|
source: "cli",
|
|
7348
|
-
dryRun:
|
|
8835
|
+
dryRun: has5(a, "dry-run")
|
|
7349
8836
|
});
|
|
7350
|
-
if (
|
|
8837
|
+
if (has5(a, "json")) io.print(JSON.stringify(r, null, 2));
|
|
7351
8838
|
else io.print(r.status === "dry-run" || r.status === "preview" ? `${r.said ?? "Dry run, nothing written."}
|
|
7352
8839
|
|
|
7353
8840
|
${r.path}:
|
|
@@ -7372,7 +8859,7 @@ Usage:
|
|
|
7372
8859
|
bridge pieces switch <n> once the library shipped: main into the product branch
|
|
7373
8860
|
|
|
7374
8861
|
GitHub token: GITHUB_TOKEN or BRIDGE_GITHUB_TOKEN, else \`gh auth token\`. Exit codes: 0 ok, 2 usage or setup error.`;
|
|
7375
|
-
var
|
|
8862
|
+
var has6 = (a, k) => Boolean(a.flags[k]);
|
|
7376
8863
|
async function context5(root, io) {
|
|
7377
8864
|
const token = io.env.BRIDGE_GITHUB_TOKEN || io.env.GITHUB_TOKEN || io.ghToken();
|
|
7378
8865
|
return BridgeContext6.create({ root, ...token ? { token } : {}, ...io.fetch ? { fetch: io.fetch } : {} });
|
|
@@ -7389,11 +8876,11 @@ function piece(a, at, usage) {
|
|
|
7389
8876
|
}
|
|
7390
8877
|
async function runPieces(a, root, io = defaultIO()) {
|
|
7391
8878
|
const sub = a._[1];
|
|
7392
|
-
if (!sub || sub === "help" ||
|
|
8879
|
+
if (!sub || sub === "help" || has6(a, "help")) {
|
|
7393
8880
|
io.print(PIECES_HELP);
|
|
7394
|
-
return sub ||
|
|
8881
|
+
return sub || has6(a, "help") ? 0 : 2;
|
|
7395
8882
|
}
|
|
7396
|
-
const json =
|
|
8883
|
+
const json = has6(a, "json");
|
|
7397
8884
|
const ctx = await context5(root, io);
|
|
7398
8885
|
const out = (r, text) => (json ? io.print(JSON.stringify(r, null, 2)) : text(), 0);
|
|
7399
8886
|
switch (sub) {
|
|
@@ -7410,7 +8897,7 @@ async function runPieces(a, root, io = defaultIO()) {
|
|
|
7410
8897
|
case "open": {
|
|
7411
8898
|
const usage = "bridge pieces open <n> <piece>";
|
|
7412
8899
|
const changes = changesFlag(a);
|
|
7413
|
-
const r = await pieces.openLibraryBranchOp(ctx, { pr: number2(a, 2, usage), piece: piece(a, 3, usage), usePreview: !
|
|
8900
|
+
const r = await pieces.openLibraryBranchOp(ctx, { pr: number2(a, 2, usage), piece: piece(a, 3, usage), usePreview: !has6(a, "no-preview"), dryRun: has6(a, "dry-run"), source: "cli", ...changes ? { changes } : {} });
|
|
7414
8901
|
return out(r, () => {
|
|
7415
8902
|
io.print(`${r.status === "dry-run" ? "Would open" : r.status === "already-linked" ? "Already linked" : r.status === "suggested" ? "Suggested" : "Opened"} a ${r.opens}: ${r.title}, needed by ${r.neededBy}.`);
|
|
7416
8903
|
if (r.library) io.print(` #${r.library.number} ${r.library.url}`);
|
|
@@ -7429,7 +8916,7 @@ async function runPieces(a, root, io = defaultIO()) {
|
|
|
7429
8916
|
return out(r, () => io.print(r.said));
|
|
7430
8917
|
}
|
|
7431
8918
|
default: {
|
|
7432
|
-
const r = await pieces.missingPiecesOp(ctx, { pr: number2(a, 1, "bridge pieces <n> [--record]"), record:
|
|
8919
|
+
const r = await pieces.missingPiecesOp(ctx, { pr: number2(a, 1, "bridge pieces <n> [--record]"), record: has6(a, "record") });
|
|
7433
8920
|
return out(r, () => {
|
|
7434
8921
|
io.print(`#${r.pr} ${r.branch} (${r.product}): ${r.pieces.length ? `${r.pieces.length} ${r.pieces.length === 1 ? "piece is" : "pieces are"} missing from the library` : "nothing is missing from the library"}, ${r.files} files read.`);
|
|
7435
8922
|
for (const p of r.pieces) io.print(` ${p.id} ${p.title}: ${p.summary} Opens ${p.opens}.${"linked" in p ? ` Linked: #${p.linked}.` : ""}`);
|
|
@@ -7459,7 +8946,7 @@ Usage:
|
|
|
7459
8946
|
token change, or a branch with the spec for Claude AI
|
|
7460
8947
|
|
|
7461
8948
|
GitHub token: GITHUB_TOKEN or BRIDGE_GITHUB_TOKEN, else \`gh auth token\`. Exit codes: 0 ok, 2 usage or setup error.`;
|
|
7462
|
-
var
|
|
8949
|
+
var has7 = (a, k) => Boolean(a.flags[k]);
|
|
7463
8950
|
async function context6(root, io) {
|
|
7464
8951
|
const token = io.env.BRIDGE_GITHUB_TOKEN || io.env.GITHUB_TOKEN || io.ghToken();
|
|
7465
8952
|
return BridgeContext7.create({ root, ...token ? { token } : {}, ...io.fetch ? { fetch: io.fetch } : {} });
|
|
@@ -7471,17 +8958,17 @@ function number3(a, usage) {
|
|
|
7471
8958
|
}
|
|
7472
8959
|
async function runFigma(a, root, io = defaultIO()) {
|
|
7473
8960
|
const sub = a._[1];
|
|
7474
|
-
if (sub === "help" ||
|
|
8961
|
+
if (sub === "help" || has7(a, "help")) {
|
|
7475
8962
|
io.print(FIGMA_HELP);
|
|
7476
8963
|
return 0;
|
|
7477
8964
|
}
|
|
7478
|
-
const json =
|
|
8965
|
+
const json = has7(a, "json");
|
|
7479
8966
|
const ctx = await context6(root, io);
|
|
7480
8967
|
const out = (r, text) => (json ? io.print(JSON.stringify(r, null, 2)) : text(), 0);
|
|
7481
8968
|
switch (sub) {
|
|
7482
8969
|
case void 0:
|
|
7483
8970
|
case "list": {
|
|
7484
|
-
const r = await figma.figmaChangesOp(ctx, { state:
|
|
8971
|
+
const r = await figma.figmaChangesOp(ctx, { state: has7(a, "all") ? "all" : "open" });
|
|
7485
8972
|
return out(r, () => {
|
|
7486
8973
|
for (const c of r.changes) io.print(`#${c.pr} ${c.title} (${c.route}): ${c.status}. ${c.said}`);
|
|
7487
8974
|
io.print(`Next: ${r.next}`);
|
|
@@ -7489,7 +8976,7 @@ async function runFigma(a, root, io = defaultIO()) {
|
|
|
7489
8976
|
}
|
|
7490
8977
|
case "show": {
|
|
7491
8978
|
const r = await figma.getFigmaChangeOp(ctx, { pr: number3(a, "bridge figma show <n>") });
|
|
7492
|
-
if (
|
|
8979
|
+
if (has7(a, "draft")) {
|
|
7493
8980
|
if (!r.brief) throw new RequestUsageError(`#${r.pr} is an exact token change: there is no code to draft.`);
|
|
7494
8981
|
if (!io.hasClaude()) throw new RequestUsageError("`claude` is not on PATH: install Claude Code, or paste the brief from `bridge figma show <n>` into Claude.");
|
|
7495
8982
|
return io.runClaude(r.brief, root);
|
|
@@ -7515,7 +9002,7 @@ ${r.brief}`);
|
|
|
7515
9002
|
throw new RequestUsageError(`${file} could not be read as JSON: ${err.message}`);
|
|
7516
9003
|
}
|
|
7517
9004
|
const changes = changesFlag(a);
|
|
7518
|
-
const r = await figma.pushFigmaComponentOp(ctx, { diff: diff2, dryRun:
|
|
9005
|
+
const r = await figma.pushFigmaComponentOp(ctx, { diff: diff2, dryRun: has7(a, "dry-run"), source: "cli", ...changes ? { changes } : {} });
|
|
7519
9006
|
return out(r, () => {
|
|
7520
9007
|
io.print(`${r.component}: ${r.route}. ${r.said}`);
|
|
7521
9008
|
for (const x of r.reasons ?? []) io.print(` ${x}`);
|
|
@@ -7601,7 +9088,20 @@ async function writeSnapshot(opts) {
|
|
|
7601
9088
|
`);
|
|
7602
9089
|
written.push(summary.path);
|
|
7603
9090
|
}
|
|
7604
|
-
|
|
9091
|
+
let activity;
|
|
9092
|
+
if (opts.activity) {
|
|
9093
|
+
const [owner, name] = (opts.activity.repo ?? `${snapshot.repo.owner}/${snapshot.repo.name}`).split("/");
|
|
9094
|
+
if (!owner || !name) throw new Error('bridge snapshot --activity needs the repository: --repo owner/name, GITHUB_REPOSITORY, or "repo" in bridge.config.json.');
|
|
9095
|
+
const fetchImpl = opts.activity.fetch ?? ((url, init2) => fetch(url, init2));
|
|
9096
|
+
const previous = await new GitHubClient(`${owner}/${name}`, opts.activity.token, fetchImpl, { attempts: 1, baseDelayMs: 0, sleep: async () => {
|
|
9097
|
+
} }, { etags: false }).getRawText(ACTIVITY_FILE, SNAPSHOT_BRANCH).then((t) => t ? parseActivity(t) : null).catch(() => null);
|
|
9098
|
+
const a = await collectActivity({ fetch: fetchImpl, token: opts.activity.token, owner, repo: name, config: model.config, version: opts.version, ...opts.generatedAt ? { generatedAt: opts.generatedAt } : {}, previous });
|
|
9099
|
+
writeFileSync2(join7(opts.out, ACTIVITY_FILE), `${JSON.stringify(a)}
|
|
9100
|
+
`);
|
|
9101
|
+
written.push(ACTIVITY_FILE);
|
|
9102
|
+
activity = { activity: a, bytes: activityBytes(a), reused: Boolean(previous && a.history && previous.history === a.history) };
|
|
9103
|
+
}
|
|
9104
|
+
return { snapshot, written, bytes, ...activity ? { activity } : {} };
|
|
7605
9105
|
}
|
|
7606
9106
|
function snapshotLines(r, out) {
|
|
7607
9107
|
const s = r.snapshot;
|
|
@@ -7614,7 +9114,24 @@ function snapshotLines(r, out) {
|
|
|
7614
9114
|
` contrast ${c.checks} checks over ${c.pairs} pairs, ${c.failing} failing`,
|
|
7615
9115
|
` figma ${s.summary.figma ? `${s.summary.figma.inFigma} of ${s.summary.figma.visual} visual components` : "no component-specs.json"}`,
|
|
7616
9116
|
` audits ${s.audits.length ? s.audits.map((a) => a.path).join(", ") : "none (--audit <path> or --audit-report <file|dir>)"}`,
|
|
7617
|
-
...s.problems.map((p) => ` problem ${p}`)
|
|
9117
|
+
...s.problems.map((p) => ` problem ${p}`),
|
|
9118
|
+
...activityLines(r, out)
|
|
9119
|
+
];
|
|
9120
|
+
}
|
|
9121
|
+
function activityLines(r, out) {
|
|
9122
|
+
if (!r.activity) return [];
|
|
9123
|
+
const a = r.activity.activity;
|
|
9124
|
+
const kb = Math.max(1, Math.round(r.activity.bytes / 1024));
|
|
9125
|
+
const reads = Object.keys(a.responses).length;
|
|
9126
|
+
return [
|
|
9127
|
+
`Wrote ${join7(out, ACTIVITY_FILE)} (${kb} KB): ${reads} GitHub reads recorded for signed-out visitors.`,
|
|
9128
|
+
` branches ${a.branches.length} open`,
|
|
9129
|
+
` suggestions ${a.suggestions.length} open`,
|
|
9130
|
+
` requests ${a.requests.length}`,
|
|
9131
|
+
` merged ${a.merged.length} recent`,
|
|
9132
|
+
` history ${a.history ? `${a.history.entries.length} token changes${r.activity.reused ? " (kept: the default branch did not move)" : ""}` : "not read"}`,
|
|
9133
|
+
...a.truncated.map((t) => ` left out ${t}`),
|
|
9134
|
+
...a.problems.map((p) => ` problem ${p}`)
|
|
7618
9135
|
];
|
|
7619
9136
|
}
|
|
7620
9137
|
|
|
@@ -7651,7 +9168,7 @@ async function rolesReport(opts) {
|
|
|
7651
9168
|
|
|
7652
9169
|
// src/index.ts
|
|
7653
9170
|
import { brand, BridgeContext as BridgeContext8, CONFIG_FILE as CONFIG_FILE2, core, detectConfig, GitRefSource, history as hist, LocalSource, ops, optionsFromArgv, serveStdio, toConfigJson } from "@bridge-ds/mcp";
|
|
7654
|
-
var VERSION = true ? "0.
|
|
9171
|
+
var VERSION = true ? "0.12.0" : "dev";
|
|
7655
9172
|
var MCP_PACKAGE = "@bridge-ds/mcp";
|
|
7656
9173
|
var HELP = `bridge ${VERSION}: design system as code, for you and your Claude
|
|
7657
9174
|
|
|
@@ -7712,11 +9229,14 @@ Usage:
|
|
|
7712
9229
|
bridge audit <path> --fix [--write] safe autofix: CSS values equal to a token become var(--token).
|
|
7713
9230
|
Dry run unless --write
|
|
7714
9231
|
bridge snapshot [--out bridge-data] [--audit <path>[=label] ...] [--audit-report <file|dir> ...]
|
|
7715
|
-
[--branch main] [--json]
|
|
9232
|
+
[--branch main] [--activity [--repo owner/name]] [--json]
|
|
7716
9233
|
the repo model of this checkout for hosted Bridge (Studio and Library):
|
|
7717
9234
|
snapshot.json (token, contrast and component files, Figma coverage,
|
|
7718
9235
|
contrast results) plus audits/<id>.json. --audit runs bridge audit on a
|
|
7719
9236
|
product folder; --audit-report carries reports already written.
|
|
9237
|
+
--activity also writes activity.json (open branches, suggestions,
|
|
9238
|
+
requests, recent merges, token history) read with GITHUB_TOKEN, so
|
|
9239
|
+
signed-out visitors of hosted Studio spend no GitHub API request.
|
|
7720
9240
|
The bridge-data workflow publishes the folder to the bridge-data branch
|
|
7721
9241
|
bridge docs draft <Component> [--json]
|
|
7722
9242
|
docs.json pre-fill from the code (props, tokens, anatomy, contrast,
|
|
@@ -7792,7 +9312,7 @@ function parse(argv) {
|
|
|
7792
9312
|
return out;
|
|
7793
9313
|
}
|
|
7794
9314
|
var flag5 = (a, k) => a.flags[k]?.[a.flags[k].length - 1];
|
|
7795
|
-
var
|
|
9315
|
+
var has8 = (a, k) => Boolean(a.flags[k]);
|
|
7796
9316
|
var print = (s) => process.stdout.write(`${s}
|
|
7797
9317
|
`);
|
|
7798
9318
|
var modeArg = (s) => s ? Object.fromEntries(s.split(",").map((kv) => kv.split("="))) : void 0;
|
|
@@ -7808,7 +9328,7 @@ function printSent(r) {
|
|
|
7808
9328
|
async function init(a, root) {
|
|
7809
9329
|
const cfgPath = join8(root, CONFIG_FILE2);
|
|
7810
9330
|
let config;
|
|
7811
|
-
if (existsSync8(cfgPath) && !
|
|
9331
|
+
if (existsSync8(cfgPath) && !has8(a, "force")) {
|
|
7812
9332
|
print(`${CONFIG_FILE2} exists, keeping it (--force to regenerate).`);
|
|
7813
9333
|
config = (await (await BridgeContext8.create({ root })).config()).config;
|
|
7814
9334
|
} else {
|
|
@@ -7842,10 +9362,10 @@ async function check(a, root) {
|
|
|
7842
9362
|
const v = await ops.validateTokens(ctx, { includeWarnings: true });
|
|
7843
9363
|
const c = await ops.checkContrast(ctx, {});
|
|
7844
9364
|
const warnings = "warnings" in v ? v.warnings ?? [] : [];
|
|
7845
|
-
const failed = !v.valid || "failing" in c && (c.failing ?? 0) > 0 ||
|
|
9365
|
+
const failed = !v.valid || "failing" in c && (c.failing ?? 0) > 0 || has8(a, "strict") && warnings.length > 0;
|
|
7846
9366
|
const protection = await protectionOf(a, root, ctx);
|
|
7847
9367
|
const roles = await rolesOf(a, root, ctx);
|
|
7848
|
-
if (
|
|
9368
|
+
if (has8(a, "json")) {
|
|
7849
9369
|
print(JSON.stringify({ ok: !failed, validation: v, contrast: c, ...protection ? { protection: protection.state } : {}, ...roles?.teams.length ? { teams: roles.teams } : {} }, null, 2));
|
|
7850
9370
|
return failed ? 1 : 0;
|
|
7851
9371
|
}
|
|
@@ -7864,7 +9384,7 @@ async function check(a, root) {
|
|
|
7864
9384
|
return failed ? 1 : 0;
|
|
7865
9385
|
}
|
|
7866
9386
|
async function protectionOf(a, root, ctx) {
|
|
7867
|
-
if (
|
|
9387
|
+
if (has8(a, "offline") || process.env.BRIDGE_OFFLINE === "1") return void 0;
|
|
7868
9388
|
const { config } = await ctx.config();
|
|
7869
9389
|
const repo = config.repo ? `${config.repo.owner}/${config.repo.name}` : await originRepo(root);
|
|
7870
9390
|
if (!repo) return void 0;
|
|
@@ -7872,7 +9392,7 @@ async function protectionOf(a, root, ctx) {
|
|
|
7872
9392
|
return protectionReport({ repo, defaultBranch: config.repo?.defaultBranch ?? "main", token });
|
|
7873
9393
|
}
|
|
7874
9394
|
async function rolesOf(a, root, ctx) {
|
|
7875
|
-
if (
|
|
9395
|
+
if (has8(a, "offline") || process.env.BRIDGE_OFFLINE === "1") return void 0;
|
|
7876
9396
|
const { config } = await ctx.config();
|
|
7877
9397
|
const repo = config.repo ? `${config.repo.owner}/${config.repo.name}` : await originRepo(root);
|
|
7878
9398
|
if (!repo) return void 0;
|
|
@@ -7884,7 +9404,7 @@ async function tokens(a, root) {
|
|
|
7884
9404
|
if (!q) throw new UsageError("bridge tokens <query>");
|
|
7885
9405
|
const ctx = await BridgeContext8.create({ root });
|
|
7886
9406
|
const r = await ops.searchTokens(ctx, { query: q, limit: Number(flag5(a, "limit") ?? 20) });
|
|
7887
|
-
if (
|
|
9407
|
+
if (has8(a, "json")) return print(JSON.stringify(r, null, 2));
|
|
7888
9408
|
if (!r.results.length) return print(`No token matches "${q}".`);
|
|
7889
9409
|
const w = Math.max(...r.results.map((x) => x.path.length));
|
|
7890
9410
|
for (const t of r.results) print(`${t.path.padEnd(w)} ${String(t.value).padEnd(10)} ${(t.tier ?? "").padEnd(9)} ${t.description ?? ""}`);
|
|
@@ -7901,11 +9421,11 @@ async function diff(a, root) {
|
|
|
7901
9421
|
const delta2 = pairs3.length ? core.contrastDelta(core.runContrast(before, pairs3, config.contrast?.modes), core.runContrast(after, pairs3, config.contrast?.modes)) : [];
|
|
7902
9422
|
const tier = core.classify(changes, [], config);
|
|
7903
9423
|
const imp = core.impact(before, after, changes, 30);
|
|
7904
|
-
if (
|
|
9424
|
+
if (has8(a, "json")) {
|
|
7905
9425
|
print(JSON.stringify({ base, changes, contrast: delta2, tier, impact: imp }, null, 2));
|
|
7906
9426
|
return 0;
|
|
7907
9427
|
}
|
|
7908
|
-
if (
|
|
9428
|
+
if (has8(a, "md")) {
|
|
7909
9429
|
print(`## Token changes since \`${base}\`
|
|
7910
9430
|
|
|
7911
9431
|
${core.tokenTable(changes)}
|
|
@@ -7971,9 +9491,9 @@ async function propose(a, root) {
|
|
|
7971
9491
|
...product ? { product } : {},
|
|
7972
9492
|
...changes ? { changes } : {},
|
|
7973
9493
|
source: "cli",
|
|
7974
|
-
dryRun:
|
|
9494
|
+
dryRun: has8(a, "dry-run")
|
|
7975
9495
|
});
|
|
7976
|
-
if (
|
|
9496
|
+
if (has8(a, "json")) print(JSON.stringify(r, null, 2));
|
|
7977
9497
|
else {
|
|
7978
9498
|
print(`status: ${r.status}${r.why ? ` (${r.why})` : ""}`);
|
|
7979
9499
|
if (r.tier) print(`tier ${r.tier}, ${r.bump}`);
|
|
@@ -7991,7 +9511,7 @@ async function brandCmd(a, root) {
|
|
|
7991
9511
|
const ctx = await BridgeContext8.create({ root, ...token ? { token } : {} });
|
|
7992
9512
|
if (sub === "list") {
|
|
7993
9513
|
const r2 = await brand.listBrands(ctx);
|
|
7994
|
-
if (
|
|
9514
|
+
if (has8(a, "json")) print(JSON.stringify(r2.brands, null, 2));
|
|
7995
9515
|
else for (const b of r2.brands) print(`${b.default ? "*" : " "} ${b.slug.padEnd(14)} ${b.name.padEnd(18)} ${b.primary ?? ""}${b.overrides.length ? ` overrides: ${b.overrides.join(", ")}` : ""}`);
|
|
7996
9516
|
return 0;
|
|
7997
9517
|
}
|
|
@@ -8013,9 +9533,9 @@ async function brandCmd(a, root) {
|
|
|
8013
9533
|
...opt("notes") ? { notes: opt("notes") } : {},
|
|
8014
9534
|
...changes ? { changes } : {},
|
|
8015
9535
|
source: "cli",
|
|
8016
|
-
dryRun:
|
|
9536
|
+
dryRun: has8(a, "dry-run")
|
|
8017
9537
|
});
|
|
8018
|
-
if (
|
|
9538
|
+
if (has8(a, "json")) {
|
|
8019
9539
|
const { fileContents: _f, ...rest } = r;
|
|
8020
9540
|
print(JSON.stringify(rest, null, 2));
|
|
8021
9541
|
return r.status === "blocked" ? 1 : 0;
|
|
@@ -8049,9 +9569,9 @@ async function figmaReferences(a, root) {
|
|
|
8049
9569
|
out: resolve6(root, flag5(a, "out") ?? "figma-references"),
|
|
8050
9570
|
sha,
|
|
8051
9571
|
...only.length ? { only } : {},
|
|
8052
|
-
log:
|
|
9572
|
+
log: has8(a, "json") ? void 0 : (line2) => print(line2)
|
|
8053
9573
|
});
|
|
8054
|
-
if (
|
|
9574
|
+
if (has8(a, "json")) print(JSON.stringify({ manifest: r.manifestPath, captured: r.captured, skipped: r.skipped }, null, 2));
|
|
8055
9575
|
return 0;
|
|
8056
9576
|
}
|
|
8057
9577
|
async function impact(a, root) {
|
|
@@ -8076,15 +9596,15 @@ async function impact(a, root) {
|
|
|
8076
9596
|
...maxStories ? { maxStoriesPerComponent: maxStories } : {},
|
|
8077
9597
|
...imageBase !== void 0 ? { imageBase } : {},
|
|
8078
9598
|
...artifactUrl ? { artifactUrl } : {},
|
|
8079
|
-
plan:
|
|
8080
|
-
keepUnchanged:
|
|
9599
|
+
plan: has8(a, "plan"),
|
|
9600
|
+
keepUnchanged: has8(a, "keep-unchanged")
|
|
8081
9601
|
});
|
|
8082
|
-
if (
|
|
9602
|
+
if (has8(a, "json")) {
|
|
8083
9603
|
print(JSON.stringify({ summary: r.summary, manifest: r.manifestPath, comment: r.commentPath ?? null, ...r.manifest.summary, selection: r.manifest.selection }, null, 2));
|
|
8084
9604
|
return 0;
|
|
8085
9605
|
}
|
|
8086
9606
|
const sel = r.manifest.selection;
|
|
8087
|
-
if (
|
|
9607
|
+
if (has8(a, "plan")) {
|
|
8088
9608
|
print(`Plan ${r.manifest.base.ref} (${r.manifest.base.sha.slice(0, 7)}) -> ${r.manifest.head.ref} (${r.manifest.head.sha.slice(0, 7)})`);
|
|
8089
9609
|
print(` tokens changing value: ${sel.tokens.length}${sel.tokens.length ? ` (${sel.tokens.filter((t) => t.kind === "direct").map((t) => t.path).slice(0, 6).join(", ")})` : ""}`);
|
|
8090
9610
|
print(` components: ${sel.components.map((c) => c.id).join(", ") || "none"}`);
|
|
@@ -8105,7 +9625,7 @@ async function historyCmd(a, root) {
|
|
|
8105
9625
|
const limit = Number(flag5(a, "limit") ?? 20);
|
|
8106
9626
|
if (token) {
|
|
8107
9627
|
const r2 = await hist.getTokenHistory(ctx, { path: token, limit });
|
|
8108
|
-
if (
|
|
9628
|
+
if (has8(a, "json")) return print(JSON.stringify(r2, null, 2)), 0;
|
|
8109
9629
|
if (!r2.total) return print(`No recorded change of ${r2.path} in ${r2.source}.`), 0;
|
|
8110
9630
|
print(`${r2.path}: ${r2.total} change(s) (${r2.source})
|
|
8111
9631
|
`);
|
|
@@ -8117,8 +9637,8 @@ async function historyCmd(a, root) {
|
|
|
8117
9637
|
return 0;
|
|
8118
9638
|
}
|
|
8119
9639
|
const tier = flag5(a, "tier");
|
|
8120
|
-
const r = await hist.getHistory(ctx, { limit, ...flag5(a, "author") ? { author: flag5(a, "author") } : {}, ...tier ? { tier: Number(tier) } : {}, ...flag5(a, "version") ? { version: flag5(a, "version") } : {}, ...
|
|
8121
|
-
if (
|
|
9640
|
+
const r = await hist.getHistory(ctx, { limit, ...flag5(a, "author") ? { author: flag5(a, "author") } : {}, ...tier ? { tier: Number(tier) } : {}, ...flag5(a, "version") ? { version: flag5(a, "version") } : {}, ...has8(a, "breaking") ? { breaking: true } : {} });
|
|
9641
|
+
if (has8(a, "json")) return print(JSON.stringify(r, null, 2)), 0;
|
|
8122
9642
|
print(`${r.source}${r.currentVersion ? `, package version ${r.currentVersion}` : ""}`);
|
|
8123
9643
|
if (!r.total) return print(r.note ?? "No token change matches."), 0;
|
|
8124
9644
|
for (const v of r.versions) print(` ${v.version.padEnd(11)} ${day(v.date)} ${v.entries} change(s), ${v.tokenChanges} token edit(s)${v.breaking ? ", breaking" : ""}`);
|
|
@@ -8140,8 +9660,8 @@ async function revertCmd(a, root) {
|
|
|
8140
9660
|
const token = changes !== "direct" ? githubToken() : void 0;
|
|
8141
9661
|
const ctx = await BridgeContext8.create({ root, ...token ? { token } : {} });
|
|
8142
9662
|
const resolutions = Object.fromEntries((a.flags["use-before"] ?? []).map((k) => [k, "use-before"]));
|
|
8143
|
-
const r = await hist.proposeRevert(ctx, { target, resolutions, restoreConflicts:
|
|
8144
|
-
if (
|
|
9663
|
+
const r = await hist.proposeRevert(ctx, { target, resolutions, restoreConflicts: has8(a, "restore-conflicts"), dryRun: has8(a, "dry-run"), ...changes ? { changes } : {}, source: "cli" });
|
|
9664
|
+
if (has8(a, "json")) {
|
|
8145
9665
|
print(JSON.stringify(r, null, 2));
|
|
8146
9666
|
return r.status === "blocked" ? 1 : 0;
|
|
8147
9667
|
}
|
|
@@ -8168,8 +9688,8 @@ async function audit(a, root) {
|
|
|
8168
9688
|
const dsRoot = flag5(a, "ds") ? resolve6(flag5(a, "ds")) : existsSync8(join8(root, CONFIG_FILE2)) ? root : findDesignSystemRoot(root);
|
|
8169
9689
|
if (!dsRoot) throw new UsageError(`No ${CONFIG_FILE2} found: run bridge audit from the design system repo or pass --ds <dir>.`);
|
|
8170
9690
|
const ds = loadDesignSystem(dsRoot);
|
|
8171
|
-
if (
|
|
8172
|
-
const write =
|
|
9691
|
+
if (has8(a, "fix")) {
|
|
9692
|
+
const write = has8(a, "write");
|
|
8173
9693
|
const changes = fixCss(target, ds.index, write);
|
|
8174
9694
|
const n = changes.reduce((acc, c) => acc + c.edits.length, 0);
|
|
8175
9695
|
for (const c of changes) for (const e of c.edits) print(`${c.file}:${e.line}:${e.column} ${e.from} -> ${e.to}`);
|
|
@@ -8245,7 +9765,7 @@ async function audit(a, root) {
|
|
|
8245
9765
|
code = 1;
|
|
8246
9766
|
}
|
|
8247
9767
|
}
|
|
8248
|
-
if (
|
|
9768
|
+
if (has8(a, "no-regress") && report.baseline) {
|
|
8249
9769
|
const b = report.baseline;
|
|
8250
9770
|
if ((b.tokenAdoption.delta ?? 0) < 0 || (b.componentAdoption.delta ?? 0) < 0 || b.hardcoded.delta > 0) {
|
|
8251
9771
|
process.stderr.write("bridge audit: adoption regressed since the baseline\n");
|
|
@@ -8257,8 +9777,31 @@ async function audit(a, root) {
|
|
|
8257
9777
|
async function snapshotCmd(a, root) {
|
|
8258
9778
|
const out = resolve6(root, flag5(a, "out") ?? "bridge-data");
|
|
8259
9779
|
const branch = flag5(a, "branch");
|
|
8260
|
-
|
|
8261
|
-
if (
|
|
9780
|
+
let activity;
|
|
9781
|
+
if (has8(a, "activity")) {
|
|
9782
|
+
const token = githubToken();
|
|
9783
|
+
if (!token) throw new UsageError("bridge snapshot --activity reads GitHub: set GITHUB_TOKEN (in a workflow, the run token is enough).");
|
|
9784
|
+
const repo = flag5(a, "repo") || process.env.GITHUB_REPOSITORY || void 0;
|
|
9785
|
+
activity = { token, ...repo ? { repo } : {} };
|
|
9786
|
+
}
|
|
9787
|
+
const r = await writeSnapshot({ root, out, version: VERSION, audit: a.flags.audit ?? [], auditReports: a.flags["audit-report"] ?? [], ...branch ? { branch } : {}, ...activity ? { activity } : {} });
|
|
9788
|
+
if (has8(a, "json"))
|
|
9789
|
+
print(
|
|
9790
|
+
JSON.stringify(
|
|
9791
|
+
{
|
|
9792
|
+
out,
|
|
9793
|
+
written: r.written,
|
|
9794
|
+
bytes: r.bytes,
|
|
9795
|
+
sourceSha: r.snapshot.sourceSha,
|
|
9796
|
+
summary: r.snapshot.summary,
|
|
9797
|
+
audits: r.snapshot.audits.map((x) => x.path),
|
|
9798
|
+
problems: r.snapshot.problems,
|
|
9799
|
+
...r.activity ? { activity: { bytes: r.activity.bytes, reads: Object.keys(r.activity.activity.responses).length, branches: r.activity.activity.branches.length, suggestions: r.activity.activity.suggestions.length, requests: r.activity.activity.requests.length, merged: r.activity.activity.merged.length, history: r.activity.activity.history?.entries.length ?? null, truncated: r.activity.activity.truncated, problems: r.activity.activity.problems } } : {}
|
|
9800
|
+
},
|
|
9801
|
+
null,
|
|
9802
|
+
2
|
|
9803
|
+
)
|
|
9804
|
+
);
|
|
8262
9805
|
else {
|
|
8263
9806
|
const shown = relative5(process.cwd(), out);
|
|
8264
9807
|
for (const line2 of snapshotLines(r, shown.startsWith("..") || isAbsolute(shown) ? out : shown || ".")) print(line2);
|
|
@@ -8270,7 +9813,7 @@ async function docsCmd(a, root) {
|
|
|
8270
9813
|
if (sub !== "draft" || !name) throw new UsageError("Usage: bridge docs draft <Component> [--json]");
|
|
8271
9814
|
const ctx = await BridgeContext8.create({ root });
|
|
8272
9815
|
const d = await ops.draftDocsOp(ctx, { name });
|
|
8273
|
-
if (
|
|
9816
|
+
if (has8(a, "json")) {
|
|
8274
9817
|
print(JSON.stringify({ component: d.component, path: d.path, exists: d.exists, prefill: d.prefill, prompt: d.prompt }, null, 2));
|
|
8275
9818
|
return 0;
|
|
8276
9819
|
}
|
|
@@ -8312,10 +9855,10 @@ async function main() {
|
|
|
8312
9855
|
if (cmd === "review") return runReview(a, root);
|
|
8313
9856
|
if (cmd === "products" || cmd === "settings" || cmd === "coverage") return runStudioRead(cmd, a, root);
|
|
8314
9857
|
if (cmd === "docs" && a._[1] === "save") return runDocsSave(a, root);
|
|
8315
|
-
if (cmd === "version" || !cmd &&
|
|
8316
|
-
if (!cmd ||
|
|
9858
|
+
if (cmd === "version" || !cmd && has8(a, "version")) return print(VERSION), 0;
|
|
9859
|
+
if (!cmd || has8(a, "help") || cmd === "help") {
|
|
8317
9860
|
print(HELP);
|
|
8318
|
-
return cmd ||
|
|
9861
|
+
return cmd || has8(a, "help") ? 0 : 2;
|
|
8319
9862
|
}
|
|
8320
9863
|
switch (cmd) {
|
|
8321
9864
|
case "init":
|