@bridge-ds/cli 0.11.2 → 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 +1639 -134
- 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";
|
|
@@ -4089,10 +4518,33 @@ var REQUEST_KINDS = [
|
|
|
4089
4518
|
{ value: "other", label: "Other", hint: "Anything else." }
|
|
4090
4519
|
];
|
|
4091
4520
|
var REQUEST_LABEL = "component-change";
|
|
4521
|
+
var MARK = "bridge-request";
|
|
4092
4522
|
function requestIssueTitle(r) {
|
|
4093
4523
|
return subjectTitle(r.component, r.title);
|
|
4094
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";
|
|
4541
|
+
}
|
|
4095
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
|
+
}
|
|
4096
4548
|
function validateIntegrations(i) {
|
|
4097
4549
|
const out = [];
|
|
4098
4550
|
if (!i?.jira) return out;
|
|
@@ -4121,7 +4573,175 @@ function jiraIssueFields(r, jira, issueUrl) {
|
|
|
4121
4573
|
};
|
|
4122
4574
|
}
|
|
4123
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
|
+
|
|
4124
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"];
|
|
4125
4745
|
var SUGGESTION_STATE_LABELS = {
|
|
4126
4746
|
"needs-review": "suggestion: needs review",
|
|
4127
4747
|
"changes-requested": "suggestion: changes requested",
|
|
@@ -4141,7 +4761,328 @@ var SUGGESTION_STATUS_LABEL = {
|
|
|
4141
4761
|
declined: "Declined",
|
|
4142
4762
|
outdated: "Outdated"
|
|
4143
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
|
+
}
|
|
4144
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
|
+
}
|
|
4145
5086
|
|
|
4146
5087
|
// ../core/src/suggestion-apply.ts
|
|
4147
5088
|
var SUGGESTION_APPLY_EXIT = {
|
|
@@ -4348,7 +5289,7 @@ function importsIn(text) {
|
|
|
4348
5289
|
for (const m of text.matchAll(IMPORT_RE)) out.add(m[1] ?? m[2]);
|
|
4349
5290
|
return [...out];
|
|
4350
5291
|
}
|
|
4351
|
-
var
|
|
5292
|
+
var norm4 = (p) => p.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
4352
5293
|
var isStory = (p) => /\.stories\.[cm]?[jt]sx?$/.test(p);
|
|
4353
5294
|
var isTest = (p) => /\.(test|spec)\.[cm]?[jt]sx?$/.test(p);
|
|
4354
5295
|
var isCss = (p) => /\.(css|scss|less)$/.test(p);
|
|
@@ -4366,14 +5307,14 @@ function componentOf(from, spec, componentsDir) {
|
|
|
4366
5307
|
return abs.slice(componentsDir.length + 1).split("/")[0] || null;
|
|
4367
5308
|
}
|
|
4368
5309
|
function componentForPath(path, componentsDir) {
|
|
4369
|
-
const dir =
|
|
4370
|
-
const p =
|
|
5310
|
+
const dir = norm4(componentsDir);
|
|
5311
|
+
const p = norm4(path);
|
|
4371
5312
|
if (!p.startsWith(`${dir}/`)) return null;
|
|
4372
5313
|
const rest = p.slice(dir.length + 1).split("/");
|
|
4373
5314
|
return rest.length >= 2 ? rest[0] : null;
|
|
4374
5315
|
}
|
|
4375
5316
|
function buildUsageIndex(files, componentsDir) {
|
|
4376
|
-
const dir =
|
|
5317
|
+
const dir = norm4(componentsDir);
|
|
4377
5318
|
const modules = {};
|
|
4378
5319
|
const vars = /* @__PURE__ */ new Map();
|
|
4379
5320
|
const imports = /* @__PURE__ */ new Map();
|
|
@@ -4381,7 +5322,7 @@ function buildUsageIndex(files, componentsDir) {
|
|
|
4381
5322
|
const globalVars = {};
|
|
4382
5323
|
const get = (m, k) => m.get(k) ?? m.set(k, /* @__PURE__ */ new Set()).get(k);
|
|
4383
5324
|
for (const f of files) {
|
|
4384
|
-
const path =
|
|
5325
|
+
const path = norm4(f.path);
|
|
4385
5326
|
const id = componentForPath(path, dir);
|
|
4386
5327
|
if (!id) {
|
|
4387
5328
|
if (isCss(path)) {
|
|
@@ -4425,7 +5366,7 @@ function impactedModules(index, input) {
|
|
|
4425
5366
|
for (const v of changedVars) for (const id of index.byVar[v] ?? []) add(id, `uses ${v}`, false);
|
|
4426
5367
|
for (const f of input.files ?? []) {
|
|
4427
5368
|
const id = componentForPath(f, index.componentsDir);
|
|
4428
|
-
if (id && index.modules[id]) add(id, `file changed: ${
|
|
5369
|
+
if (id && index.modules[id]) add(id, `file changed: ${norm4(f)}`, false);
|
|
4429
5370
|
}
|
|
4430
5371
|
const renderedBy = /* @__PURE__ */ new Map();
|
|
4431
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);
|
|
@@ -4443,6 +5384,131 @@ function impactedModules(index, input) {
|
|
|
4443
5384
|
return { modules, globalHits };
|
|
4444
5385
|
}
|
|
4445
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
|
+
|
|
4446
5512
|
// ../core/src/fidelity-references.ts
|
|
4447
5513
|
var FIGMA_REFERENCES_DIR = "figma-references";
|
|
4448
5514
|
var FIGMA_REFERENCES_MANIFEST = `${FIGMA_REFERENCES_DIR}/manifest.json`;
|
|
@@ -4715,10 +5781,10 @@ function colorCss(c) {
|
|
|
4715
5781
|
if (typeof c === "string") return { css: c, hex: /^#[0-9a-f]{6}$/i.test(c) ? c.toLowerCase() : void 0 };
|
|
4716
5782
|
const v = c;
|
|
4717
5783
|
const [r = 0, g = 0, b = 0] = v.components;
|
|
4718
|
-
const
|
|
5784
|
+
const hex2 = (v.hex ?? `#${[r, g, b].map(toHex22).join("")}`).toLowerCase();
|
|
4719
5785
|
const alpha = v.alpha ?? 1;
|
|
4720
|
-
if (alpha < 1) return { css: `rgb(${[r, g, b].map((x) => Math.round(x * 255)).join(" ")} / ${alpha})`, hex };
|
|
4721
|
-
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 };
|
|
4722
5788
|
}
|
|
4723
5789
|
var dimCss = (d) => typeof d === "object" && d !== null ? `${d.value}${d.unit}` : String(d);
|
|
4724
5790
|
function shadowCss(s) {
|
|
@@ -4772,13 +5838,13 @@ function buildTokenRows(config, readText, listFiles) {
|
|
|
4772
5838
|
...componentFiles
|
|
4773
5839
|
];
|
|
4774
5840
|
const primitives = primitiveFiles.flatMap((f) => flatFile(f)).map((t) => {
|
|
4775
|
-
const { css, hex } = tokenValueCss(t.type, t.value);
|
|
5841
|
+
const { css, hex: hex2 } = tokenValueCss(t.type, t.value);
|
|
4776
5842
|
return {
|
|
4777
5843
|
path: t.path.join("."),
|
|
4778
5844
|
segments: t.path,
|
|
4779
5845
|
type: t.type,
|
|
4780
5846
|
css,
|
|
4781
|
-
hex,
|
|
5847
|
+
hex: hex2,
|
|
4782
5848
|
cssVar: tokenRowCssVar(t.path),
|
|
4783
5849
|
figma: tokenRowFigmaName(t.path),
|
|
4784
5850
|
description: t.description,
|
|
@@ -4822,8 +5888,8 @@ function buildTokenRows(config, readText, listFiles) {
|
|
|
4822
5888
|
current = hit.value;
|
|
4823
5889
|
currentType = hit.type || currentType;
|
|
4824
5890
|
}
|
|
4825
|
-
const { css, hex } = tokenValueCss(currentType, current);
|
|
4826
|
-
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 };
|
|
4827
5893
|
};
|
|
4828
5894
|
const same2 = (a, c) => a.css === c.css && a.chain.join() === c.chain.join();
|
|
4829
5895
|
const build = (t, tier, valueFor) => {
|
|
@@ -4883,8 +5949,8 @@ var channel = (c) => {
|
|
|
4883
5949
|
const s = c / 255;
|
|
4884
5950
|
return s <= 0.04045 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
|
|
4885
5951
|
};
|
|
4886
|
-
function hexLuminance(
|
|
4887
|
-
const n = parseInt(
|
|
5952
|
+
function hexLuminance(hex2) {
|
|
5953
|
+
const n = parseInt(hex2.slice(1), 16);
|
|
4888
5954
|
return 0.2126 * channel(n >> 16 & 255) + 0.7152 * channel(n >> 8 & 255) + 0.0722 * channel(n & 255);
|
|
4889
5955
|
}
|
|
4890
5956
|
function hexContrastRatio(a, b) {
|
|
@@ -5075,7 +6141,7 @@ function scanComponents(input) {
|
|
|
5075
6141
|
const docsSuffix = input.docsSuffix ?? ".docs.json";
|
|
5076
6142
|
const styleSuffix = input.styleSuffix ?? ".module.css";
|
|
5077
6143
|
const fileSet = new Set(input.files);
|
|
5078
|
-
const
|
|
6144
|
+
const has9 = (p) => fileSet.has(p);
|
|
5079
6145
|
const fam = input.indexText === void 0 ? [] : parseComponentFamilies(input.indexText, input.indexRel ?? "components");
|
|
5080
6146
|
const familyOf = (name) => fam.find((f) => f.members.includes(name))?.name;
|
|
5081
6147
|
const names = [
|
|
@@ -5088,7 +6154,7 @@ function scanComponents(input) {
|
|
|
5088
6154
|
const folder = `${dir}/${name}`;
|
|
5089
6155
|
const files = { tsx: `${folder}/${name}.tsx`, css: `${folder}/${name}${styleSuffix}`, stories: `${folder}/${name}.stories.tsx`, docs: `${folder}/${name}${docsSuffix}` };
|
|
5090
6156
|
let docs = null;
|
|
5091
|
-
if (
|
|
6157
|
+
if (has9(files.docs)) {
|
|
5092
6158
|
try {
|
|
5093
6159
|
docs = JSON.parse(readText(files.docs) ?? "");
|
|
5094
6160
|
} catch (err) {
|
|
@@ -5098,10 +6164,10 @@ function scanComponents(input) {
|
|
|
5098
6164
|
const tsxSrc = readText(files.tsx) ?? "";
|
|
5099
6165
|
const tsx = parseComponentTsx(name, tsxSrc);
|
|
5100
6166
|
const reuses = [...tsxSrc.matchAll(reuseRe)].filter((m) => m[1] === m[2] && m[1] !== name).map((m) => m[1]);
|
|
5101
|
-
const cssText =
|
|
6167
|
+
const cssText = has9(files.css) ? readText(files.css) : void 0;
|
|
5102
6168
|
const rules = cssText === void 0 ? [] : parseComponentCss(cssText, docs?.anatomy ?? {}, describeVar);
|
|
5103
6169
|
const used = [...new Set(rules.flatMap((r) => r.vars.map((v) => v.name)))].sort();
|
|
5104
|
-
const storiesText =
|
|
6170
|
+
const storiesText = has9(files.stories) ? readText(files.stories) : void 0;
|
|
5105
6171
|
return {
|
|
5106
6172
|
name,
|
|
5107
6173
|
slug: componentSlug(name),
|
|
@@ -5496,6 +6562,389 @@ function snapshotLlmsTxt(model, snapshot) {
|
|
|
5496
6562
|
`;
|
|
5497
6563
|
}
|
|
5498
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
|
+
|
|
5499
6948
|
// ../visual/src/capture.ts
|
|
5500
6949
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
5501
6950
|
import { dirname as dirname2 } from "node:path";
|
|
@@ -5896,17 +7345,17 @@ var shotKey = (...parts) => parts.map((p) => p.toLowerCase().replace(/[^a-z0-9-]
|
|
|
5896
7345
|
|
|
5897
7346
|
// ../visual/src/select.ts
|
|
5898
7347
|
var NO_VISUAL = /(\.test\.[cm]?[jt]sx?|\.spec\.[cm]?[jt]sx?|\.docs\.json|\.mdx?|README[^/]*)$/i;
|
|
5899
|
-
var
|
|
7348
|
+
var norm5 = (p) => p.replace(/^\.\//, "").replace(/\\/g, "/");
|
|
5900
7349
|
var isAllStates = (s) => /--all-states$/.test(s.id) || /^all ?states$/i.test(s.name);
|
|
5901
7350
|
function storyComponent(s, componentsDir) {
|
|
5902
|
-
return componentForPath(
|
|
7351
|
+
return componentForPath(norm5(s.importPath), componentsDir);
|
|
5903
7352
|
}
|
|
5904
7353
|
function selectImpact(input) {
|
|
5905
7354
|
const { usage, stories } = input;
|
|
5906
7355
|
const dir = usage.componentsDir;
|
|
5907
7356
|
const lib = input.librarySrc.replace(/\/$/, "");
|
|
5908
7357
|
const cap = input.maxStoriesPerComponent ?? 4;
|
|
5909
|
-
const visualFiles = input.files.map(
|
|
7358
|
+
const visualFiles = input.files.map(norm5).filter((f) => !NO_VISUAL.test(f));
|
|
5910
7359
|
const vars = [...new Set(input.tokens.map((t) => input.cssVar(t.path)))].sort();
|
|
5911
7360
|
const impact2 = impactedModules(usage, { vars, files: visualFiles });
|
|
5912
7361
|
let fallback = null;
|
|
@@ -5946,7 +7395,7 @@ function selectImpact(input) {
|
|
|
5946
7395
|
return {
|
|
5947
7396
|
tokens: input.tokens,
|
|
5948
7397
|
vars,
|
|
5949
|
-
files: input.files.map(
|
|
7398
|
+
files: input.files.map(norm5),
|
|
5950
7399
|
components: components.map((c) => ({ id: c.id, reasons: c.reasons, indirect: c.indirect })),
|
|
5951
7400
|
stories: [...new Set(picked)],
|
|
5952
7401
|
fallback,
|
|
@@ -5965,7 +7414,7 @@ function storiesFromSource(files) {
|
|
|
5965
7414
|
if (!title) continue;
|
|
5966
7415
|
for (const m of f.text.matchAll(/export\s+const\s+([A-Z]\w*)\s*[:=]/g)) {
|
|
5967
7416
|
const name = m[1];
|
|
5968
|
-
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)}` });
|
|
5969
7418
|
}
|
|
5970
7419
|
}
|
|
5971
7420
|
return out;
|
|
@@ -6593,7 +8042,7 @@ async function jiraSync(input) {
|
|
|
6593
8042
|
log(`Bridge Jira sync: ${reason}`);
|
|
6594
8043
|
return { status: "skipped", reason };
|
|
6595
8044
|
};
|
|
6596
|
-
const
|
|
8045
|
+
const fail2 = (reason) => {
|
|
6597
8046
|
log(`::error::Bridge Jira sync: ${reason}`);
|
|
6598
8047
|
return { status: "error", reason };
|
|
6599
8048
|
};
|
|
@@ -6608,7 +8057,7 @@ async function jiraSync(input) {
|
|
|
6608
8057
|
if (!n || !repo) return skip("not an issues event.");
|
|
6609
8058
|
if (!["opened", "labeled", "closed"].includes(action)) return skip(`nothing to do on "${action}".`);
|
|
6610
8059
|
if (action === "labeled" && event.label?.name !== REQUEST_LABEL) return skip(`label "${event.label?.name ?? ""}" is not ${REQUEST_LABEL}.`);
|
|
6611
|
-
if (!env.GITHUB_TOKEN) return
|
|
8060
|
+
if (!env.GITHUB_TOKEN) return fail2("GITHUB_TOKEN is missing: pass secrets.GITHUB_TOKEN to the step.");
|
|
6612
8061
|
const ghFetch = (url2, init2) => f(url2, init2);
|
|
6613
8062
|
const gh = new GitHubClient(repo, env.GITHUB_TOKEN, ghFetch);
|
|
6614
8063
|
const issue = await gh.getIssue(n);
|
|
@@ -6637,14 +8086,14 @@ async function jiraSync(input) {
|
|
|
6637
8086
|
const text = `Closed in GitHub (#${n}), ${outcome}`;
|
|
6638
8087
|
const body = { body: { type: "doc", version: 1, content: [{ type: "paragraph", content: [{ type: "text", text, marks: [{ type: "link", attrs: { href: issue.url } }] }] }] } };
|
|
6639
8088
|
const res2 = await f(`${base}/rest/api/3/issue/${encodeURIComponent(key)}/comment`, { method: "POST", headers, body: JSON.stringify(body) });
|
|
6640
|
-
if (!res2.ok) return
|
|
8089
|
+
if (!res2.ok) return fail2(await jiraError(res2));
|
|
6641
8090
|
log(`Bridge Jira sync: commented on ${key}: ${text}.`);
|
|
6642
8091
|
return { status: "commented", key };
|
|
6643
8092
|
}
|
|
6644
8093
|
if (key) return skip(`#${n} is already linked to ${key}.`);
|
|
6645
8094
|
const request = requests.requestFromIssue(issue);
|
|
6646
8095
|
const res = await f(`${base}/rest/api/3/issue`, { method: "POST", headers, body: JSON.stringify({ fields: jiraIssueFields(request, jira, issue.url) }) });
|
|
6647
|
-
if (!res.ok) return
|
|
8096
|
+
if (!res.ok) return fail2(await jiraError(res));
|
|
6648
8097
|
const created = await res.json();
|
|
6649
8098
|
const url = `${base}/browse/${created.key}`;
|
|
6650
8099
|
await gh.addComment(n, `Jira: ${created.key} ${url}`);
|
|
@@ -6674,7 +8123,7 @@ Exit codes: 0 ok, 1 not a request or Claude Code not found, 2 usage or setup err
|
|
|
6674
8123
|
var RequestUsageError = class extends Error {
|
|
6675
8124
|
};
|
|
6676
8125
|
var flag = (a, k) => a.flags[k]?.[a.flags[k].length - 1];
|
|
6677
|
-
var
|
|
8126
|
+
var has2 = (a, k) => Boolean(a.flags[k]);
|
|
6678
8127
|
var defaultIO = () => ({
|
|
6679
8128
|
print: (s) => process.stdout.write(`${s}
|
|
6680
8129
|
`),
|
|
@@ -6734,9 +8183,9 @@ async function getOrExplain(ctx, n, io, branch) {
|
|
|
6734
8183
|
}
|
|
6735
8184
|
async function runRequest(a, root, io = defaultIO()) {
|
|
6736
8185
|
const sub = a._[1];
|
|
6737
|
-
if (!sub || sub === "help" ||
|
|
8186
|
+
if (!sub || sub === "help" || has2(a, "help")) {
|
|
6738
8187
|
io.print(REQUEST_HELP);
|
|
6739
|
-
return sub ||
|
|
8188
|
+
return sub || has2(a, "help") ? 0 : 2;
|
|
6740
8189
|
}
|
|
6741
8190
|
switch (sub) {
|
|
6742
8191
|
case "list": {
|
|
@@ -6745,7 +8194,7 @@ async function runRequest(a, root, io = defaultIO()) {
|
|
|
6745
8194
|
const ctx = await context(root, io, false);
|
|
6746
8195
|
const component = flag(a, "component");
|
|
6747
8196
|
const r = await requests2.listRequests(ctx, { status, ...component ? { component } : {} });
|
|
6748
|
-
if (
|
|
8197
|
+
if (has2(a, "json")) return io.print(JSON.stringify(r, null, 2)), 0;
|
|
6749
8198
|
if (!r.requests.length) return io.print(`No ${status === "all" ? "" : `${status} `}component change request in ${r.repo}${component ? ` for ${component}` : ""}.`), 0;
|
|
6750
8199
|
const w = Math.max(...r.requests.map((x) => x.component.length));
|
|
6751
8200
|
for (const x of r.requests) {
|
|
@@ -6759,7 +8208,7 @@ async function runRequest(a, root, io = defaultIO()) {
|
|
|
6759
8208
|
const ctx = await context(root, io, false);
|
|
6760
8209
|
const r = await getOrExplain(ctx, n, io);
|
|
6761
8210
|
if (!r) return 1;
|
|
6762
|
-
if (
|
|
8211
|
+
if (has2(a, "json")) return io.print(JSON.stringify(r, null, 2)), 0;
|
|
6763
8212
|
const q = r.request;
|
|
6764
8213
|
io.print(`#${r.number} ${q.component}: ${q.title}`);
|
|
6765
8214
|
io.print(`${REQUEST_STATUS_LABEL[r.status]}, ${q.kind}, from ${q.source}${q.requestedBy ? ` by @${q.requestedBy}` : ""}`);
|
|
@@ -6789,7 +8238,7 @@ Next: bridge request implement ${r.number} --claude`);
|
|
|
6789
8238
|
const branch = flag(a, "branch");
|
|
6790
8239
|
const r = await getOrExplain(ctx, n, io, branch);
|
|
6791
8240
|
if (!r) return 1;
|
|
6792
|
-
if (!
|
|
8241
|
+
if (!has2(a, "claude")) return io.print(r.brief), 0;
|
|
6793
8242
|
if (!io.hasClaude()) {
|
|
6794
8243
|
io.print(r.brief);
|
|
6795
8244
|
io.error(`
|
|
@@ -6807,7 +8256,7 @@ Claude Code is not installed (no \`claude\` on PATH). Install it (npm install -g
|
|
|
6807
8256
|
const usage = 'bridge request create --component Button --kind new-variant --title "Add a danger ghost variant" --summary "..."';
|
|
6808
8257
|
if (!component || !kind || !title || !summary) throw new RequestUsageError(`Usage: ${usage}`);
|
|
6809
8258
|
if (!REQUEST_KINDS.some((k) => k.value === kind)) throw new RequestUsageError(`--kind takes ${REQUEST_KINDS.map((k) => k.value).join(", ")}, got "${kind}"`);
|
|
6810
|
-
const dryRun =
|
|
8259
|
+
const dryRun = has2(a, "dry-run");
|
|
6811
8260
|
const ctx = await context(root, io, !dryRun);
|
|
6812
8261
|
const figma2 = flag(a, "figma");
|
|
6813
8262
|
const exploration = flag(a, "exploration");
|
|
@@ -6823,7 +8272,7 @@ Claude Code is not installed (no \`claude\` on PATH). Install it (npm install -g
|
|
|
6823
8272
|
source: "cli",
|
|
6824
8273
|
dryRun
|
|
6825
8274
|
});
|
|
6826
|
-
if (
|
|
8275
|
+
if (has2(a, "json")) return io.print(JSON.stringify(r, null, 2)), 0;
|
|
6827
8276
|
if (r.status === "dry-run") {
|
|
6828
8277
|
io.print(`Dry run, nothing created.
|
|
6829
8278
|
|
|
@@ -6882,7 +8331,7 @@ Work on one: git switch explore/<name>, then \`bridge propose --branch explore/<
|
|
|
6882
8331
|
GitHub token: GITHUB_TOKEN or BRIDGE_GITHUB_TOKEN, else \`gh auth token\`.
|
|
6883
8332
|
Exit codes: 0 ok, 2 usage or setup error.`;
|
|
6884
8333
|
var flag2 = (a, k) => a.flags[k]?.[a.flags[k].length - 1];
|
|
6885
|
-
var
|
|
8334
|
+
var has3 = (a, k) => Boolean(a.flags[k]);
|
|
6886
8335
|
async function context2(root, io) {
|
|
6887
8336
|
const token = io.env.BRIDGE_GITHUB_TOKEN || io.env.GITHUB_TOKEN || io.ghToken();
|
|
6888
8337
|
return BridgeContext3.create({ root, ...token ? { token } : {}, ...io.fetch ? { fetch: io.fetch } : {} });
|
|
@@ -6891,11 +8340,11 @@ var exploreSource = (env) => ({ source: "cli", ...env.CLAUDECODE ? { agent: "cla
|
|
|
6891
8340
|
var date = (iso) => iso ? iso.slice(0, 10) : "";
|
|
6892
8341
|
async function runExplore(a, root, io = defaultIO()) {
|
|
6893
8342
|
const sub = a._[1];
|
|
6894
|
-
if (!sub || sub === "help" ||
|
|
8343
|
+
if (!sub || sub === "help" || has3(a, "help")) {
|
|
6895
8344
|
io.print(EXPLORE_HELP);
|
|
6896
|
-
return sub ||
|
|
8345
|
+
return sub || has3(a, "help") ? 0 : 2;
|
|
6897
8346
|
}
|
|
6898
|
-
const json =
|
|
8347
|
+
const json = has3(a, "json");
|
|
6899
8348
|
switch (sub) {
|
|
6900
8349
|
case "start": {
|
|
6901
8350
|
const name = a._.slice(2).join(" ").trim() || flag2(a, "name");
|
|
@@ -6903,7 +8352,7 @@ async function runExplore(a, root, io = defaultIO()) {
|
|
|
6903
8352
|
if (!name || !purpose) throw new RequestUsageError('Usage: bridge explore start <name> --purpose "What you want to learn"');
|
|
6904
8353
|
const useCase = flag2(a, "use-case");
|
|
6905
8354
|
const ctx = await context2(root, io);
|
|
6906
|
-
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") });
|
|
6907
8356
|
if (json) return io.print(JSON.stringify(r, null, 2)), 0;
|
|
6908
8357
|
if (r.status === "dry-run") {
|
|
6909
8358
|
io.print(`Dry run, nothing written.
|
|
@@ -6927,7 +8376,7 @@ Next: ${r.next}`);
|
|
|
6927
8376
|
}
|
|
6928
8377
|
case "list": {
|
|
6929
8378
|
const ctx = await context2(root, io);
|
|
6930
|
-
const r = await explorations.listExplorations(ctx, { state:
|
|
8379
|
+
const r = await explorations.listExplorations(ctx, { state: has3(a, "all") ? "all" : "open" });
|
|
6931
8380
|
if (json) return io.print(JSON.stringify(r, null, 2)), 0;
|
|
6932
8381
|
io.print(r.summary);
|
|
6933
8382
|
for (const x of r.explorations) {
|
|
@@ -6948,7 +8397,7 @@ Local only: ${b} (push it and open a draft pull request labeled exploration)`);
|
|
|
6948
8397
|
const ref = a._.slice(2).join(" ").trim();
|
|
6949
8398
|
if (!ref) throw new RequestUsageError("Usage: bridge explore promote <name|#pr> [--dry-run]");
|
|
6950
8399
|
const ctx = await context2(root, io);
|
|
6951
|
-
const r = await explorations.promoteExploration(ctx, { ref, dryRun:
|
|
8400
|
+
const r = await explorations.promoteExploration(ctx, { ref, dryRun: has3(a, "dry-run") });
|
|
6952
8401
|
if (json) return io.print(JSON.stringify(r, null, 2)), 0;
|
|
6953
8402
|
io.print(`${r.status === "dry-run" ? "Dry run: would promote" : "Promoted"} #${r.number} "${r.title}" to review: Tier ${r.tier}.`);
|
|
6954
8403
|
io.print(`Labels: +${r.addLabels.join(", +")}, -${r.removeLabels.join(", -")}${r.removeFiles.length ? `. Removes ${r.removeFiles.join(", ")}` : ""}.`);
|
|
@@ -7002,10 +8451,10 @@ GitHub token: GITHUB_TOKEN or BRIDGE_GITHUB_TOKEN, else \`gh auth token\`. Readi
|
|
|
7002
8451
|
Exit codes (apply): 0 applied, already applied or a clean dry run; 1 not approved, blocked by a check or refused;
|
|
7003
8452
|
2 usage or setup error; 3 conflict with today's code.`;
|
|
7004
8453
|
var flag3 = (a, k) => a.flags[k]?.[a.flags[k].length - 1];
|
|
7005
|
-
var
|
|
8454
|
+
var has4 = (a, k) => Boolean(a.flags[k]);
|
|
7006
8455
|
function changesFlag(a) {
|
|
7007
|
-
if (
|
|
7008
|
-
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;
|
|
7009
8458
|
}
|
|
7010
8459
|
function tokenFrom2(io) {
|
|
7011
8460
|
return io.env.BRIDGE_GITHUB_TOKEN || io.env.GITHUB_TOKEN || io.ghToken();
|
|
@@ -7023,7 +8472,7 @@ var STATUSES = ["needs-review", "changes-requested", "approved", "applying", "ap
|
|
|
7023
8472
|
var KINDS = ["token-change", "docs", "component-spec", "brand", "revert", "config"];
|
|
7024
8473
|
async function runSuggestions(a, root, io = defaultIO()) {
|
|
7025
8474
|
const sub = a._[1] ?? "list";
|
|
7026
|
-
if (sub === "help" ||
|
|
8475
|
+
if (sub === "help" || has4(a, "help")) return io.print(SUGGESTIONS_HELP), 0;
|
|
7027
8476
|
switch (sub) {
|
|
7028
8477
|
case "list": {
|
|
7029
8478
|
const status = flag3(a, "status") ?? "all";
|
|
@@ -7032,7 +8481,7 @@ async function runSuggestions(a, root, io = defaultIO()) {
|
|
|
7032
8481
|
if (kind && !KINDS.includes(kind)) throw new SuggestUsageError(`--kind takes ${KINDS.join(", ")}, got "${kind}"`);
|
|
7033
8482
|
const ctx = await context3(root, io);
|
|
7034
8483
|
const r = await suggestions.listSuggestionsOp(ctx, { status, ...kind ? { kind } : {} });
|
|
7035
|
-
if (
|
|
8484
|
+
if (has4(a, "json")) return io.print(JSON.stringify(r, null, 2)), 0;
|
|
7036
8485
|
if (!r.suggestions.length) return io.print(`No ${status === "all" ? "open " : `${status} `}suggestion in ${r.repo}.`), 0;
|
|
7037
8486
|
for (const x of r.suggestions) {
|
|
7038
8487
|
const prs = x.linkedPrs.length ? ` PR ${x.linkedPrs.map((p) => `#${p.number}${p.merged ? " merged" : ` ${p.state}`}`).join(", ")}` : "";
|
|
@@ -7046,7 +8495,7 @@ ${r.next}`);
|
|
|
7046
8495
|
const n = issueNumber2(a._[2], "bridge suggestions show <number>");
|
|
7047
8496
|
const ctx = await context3(root, io);
|
|
7048
8497
|
const r = await suggestions.getSuggestionOp(ctx, { number: n });
|
|
7049
|
-
if (
|
|
8498
|
+
if (has4(a, "json")) return io.print(JSON.stringify(r, null, 2)), 0;
|
|
7050
8499
|
io.print(`#${r.number} ${r.title}`);
|
|
7051
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}` : ""}`);
|
|
7052
8501
|
io.print(r.url);
|
|
@@ -7084,7 +8533,7 @@ Next: ${r.next}`);
|
|
|
7084
8533
|
if (!pr && !branch) throw new SuggestUsageError("Usage: bridge suggestions link <number> --pr <m> (or --branch <name>)");
|
|
7085
8534
|
const ctx = await context3(root, io);
|
|
7086
8535
|
const r = await suggestions.linkSuggestionOp(ctx, { number: n, ...pr ? { pr } : {}, ...branch ? { branch } : {} });
|
|
7087
|
-
if (
|
|
8536
|
+
if (has4(a, "json")) return io.print(JSON.stringify(r, null, 2)), 0;
|
|
7088
8537
|
io.print(`#${n} is being applied${pr ? ` in #${pr}` : branch ? ` on ${branch}` : ""}.`);
|
|
7089
8538
|
for (const w of r.warnings ?? []) io.error(`warning: ${w}`);
|
|
7090
8539
|
return 0;
|
|
@@ -7097,23 +8546,23 @@ ${SUGGESTIONS_HELP}`);
|
|
|
7097
8546
|
}
|
|
7098
8547
|
var fmt = (v) => v === void 0 ? "(none)" : typeof v === "string" ? v : JSON.stringify(v);
|
|
7099
8548
|
async function runApply(a, root, io = defaultIO()) {
|
|
7100
|
-
if (
|
|
8549
|
+
if (has4(a, "help") || a._[1] === "help") return io.print(SUGGESTIONS_HELP), 0;
|
|
7101
8550
|
const n = issueNumber2(a._[1], "bridge apply <number> [--write | --patch] [--dry-run]");
|
|
7102
|
-
if (
|
|
7103
|
-
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";
|
|
7104
8553
|
const ctx = await context3(root, io);
|
|
7105
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.");
|
|
7106
8555
|
const branch = flag3(a, "branch");
|
|
7107
8556
|
const r = await suggestions.applySuggestionOp(ctx, {
|
|
7108
8557
|
number: n,
|
|
7109
|
-
mode:
|
|
7110
|
-
acceptEdited:
|
|
7111
|
-
forceUnapproved:
|
|
8558
|
+
mode: has4(a, "dry-run") ? "dry-run" : mode2,
|
|
8559
|
+
acceptEdited: has4(a, "accept-edited"),
|
|
8560
|
+
forceUnapproved: has4(a, "force-unapproved"),
|
|
7112
8561
|
...branch ? { branch } : {}
|
|
7113
8562
|
});
|
|
7114
8563
|
const code = typeof r.exitCode === "number" ? r.exitCode : SUGGESTION_APPLY_EXIT[r.outcome] ?? 1;
|
|
7115
|
-
if (
|
|
7116
|
-
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")) {
|
|
7117
8566
|
io.print(r.patch.trimEnd());
|
|
7118
8567
|
io.error(r.message);
|
|
7119
8568
|
for (const w of r.warnings ?? []) io.error(`warning: ${w}`);
|
|
@@ -7137,10 +8586,10 @@ Next: ${r.next}`);
|
|
|
7137
8586
|
return code;
|
|
7138
8587
|
}
|
|
7139
8588
|
async function runReview(a, root, io = defaultIO()) {
|
|
7140
|
-
if (
|
|
8589
|
+
if (has4(a, "help") || a._[1] === "help") return io.print(SUGGESTIONS_HELP), 0;
|
|
7141
8590
|
const usage = 'bridge review <number> --approve | --request-changes --message "..." | --decline --message "..."';
|
|
7142
8591
|
const n = issueNumber2(a._[1], usage);
|
|
7143
|
-
const events = ["approve", "request-changes", "decline", "comment"].filter((e) =>
|
|
8592
|
+
const events = ["approve", "request-changes", "decline", "comment"].filter((e) => has4(a, e));
|
|
7144
8593
|
if (events.length !== 1) throw new SuggestUsageError(`Usage: ${usage}`);
|
|
7145
8594
|
const event = events[0];
|
|
7146
8595
|
const inline = flag3(a, event);
|
|
@@ -7157,7 +8606,7 @@ async function runReview(a, root, io = defaultIO()) {
|
|
|
7157
8606
|
...role ? { role } : {},
|
|
7158
8607
|
...version ? { payloadSha: version } : {}
|
|
7159
8608
|
});
|
|
7160
|
-
if (
|
|
8609
|
+
if (has4(a, "json")) return io.print(JSON.stringify(r, null, 2)), 0;
|
|
7161
8610
|
const done = event === "approve" ? "Approved" : event === "request-changes" ? "Changes requested on" : event === "decline" ? "Declined" : "Commented on";
|
|
7162
8611
|
io.print(`${done} #${n}${r.version ? ` (version ${r.version})` : ""}. Now: ${SUGGESTION_STATUS_LABEL[r.status] ?? r.status}.`);
|
|
7163
8612
|
for (const w of r.warnings ?? []) io.error(`warning: ${w}`);
|
|
@@ -7196,7 +8645,7 @@ Usage:
|
|
|
7196
8645
|
Start work for a product: bridge propose|explore start|request create ... --product <key> [--jira DS-42].
|
|
7197
8646
|
GitHub token: GITHUB_TOKEN or BRIDGE_GITHUB_TOKEN, else \`gh auth token\`. Exit codes: 0 ok, 2 usage or setup error.`;
|
|
7198
8647
|
var flag4 = (a, k) => a.flags[k]?.[a.flags[k].length - 1];
|
|
7199
|
-
var
|
|
8648
|
+
var has5 = (a, k) => Boolean(a.flags[k]);
|
|
7200
8649
|
async function context4(root, io) {
|
|
7201
8650
|
const token = io.env.BRIDGE_GITHUB_TOKEN || io.env.GITHUB_TOKEN || io.ghToken();
|
|
7202
8651
|
return BridgeContext5.create({ root, ...token ? { token } : {}, ...io.fetch ? { fetch: io.fetch } : {} });
|
|
@@ -7218,11 +8667,11 @@ function pairs2(list2, what) {
|
|
|
7218
8667
|
var said = (r) => String(r.said ?? r.summary ?? r.status ?? "Done.");
|
|
7219
8668
|
async function runBranch(a, root, io = defaultIO()) {
|
|
7220
8669
|
const sub = a._[1];
|
|
7221
|
-
if (!sub || sub === "help" ||
|
|
8670
|
+
if (!sub || sub === "help" || has5(a, "help")) {
|
|
7222
8671
|
io.print(BRANCH_HELP);
|
|
7223
|
-
return sub ||
|
|
8672
|
+
return sub || has5(a, "help") ? 0 : 2;
|
|
7224
8673
|
}
|
|
7225
|
-
const json =
|
|
8674
|
+
const json = has5(a, "json");
|
|
7226
8675
|
const ctx = await context4(root, io);
|
|
7227
8676
|
const out = (r, text) => (json ? io.print(JSON.stringify(r, null, 2)) : text(), 0);
|
|
7228
8677
|
switch (sub) {
|
|
@@ -7263,14 +8712,14 @@ async function runBranch(a, root, io = defaultIO()) {
|
|
|
7263
8712
|
}
|
|
7264
8713
|
case "review": {
|
|
7265
8714
|
const n = number(a, 2, 'bridge branch review <n> --approve | --request-changes "..." | --comment "..."');
|
|
7266
|
-
const decision =
|
|
8715
|
+
const decision = has5(a, "approve") ? "approve" : has5(a, "request-changes") ? "request_changes" : has5(a, "comment") ? "comment" : null;
|
|
7267
8716
|
if (!decision) throw new RequestUsageError('Say what you decide: --approve, --request-changes "what to change" or --comment "text".');
|
|
7268
8717
|
const body = decision === "approve" ? flag4(a, "body") : flag4(a, decision === "request_changes" ? "request-changes" : "comment");
|
|
7269
8718
|
const r = await branches.reviewBranch(ctx, { pr: n, decision, ...body && body !== "true" ? { body } : {} });
|
|
7270
8719
|
return out(r, () => io.print(r.said));
|
|
7271
8720
|
}
|
|
7272
8721
|
case "ask": {
|
|
7273
|
-
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") });
|
|
7274
8723
|
return out(r, () => io.print(r.said));
|
|
7275
8724
|
}
|
|
7276
8725
|
case "comment": {
|
|
@@ -7284,7 +8733,7 @@ async function runBranch(a, root, io = defaultIO()) {
|
|
|
7284
8733
|
case "merge": {
|
|
7285
8734
|
const n = number(a, 2, 'bridge branch merge <n> [--override "reason"] [--dry-run]');
|
|
7286
8735
|
const reason = flag4(a, "override");
|
|
7287
|
-
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") });
|
|
7288
8737
|
return out(r, () => io.print(r.said));
|
|
7289
8738
|
}
|
|
7290
8739
|
case "close": {
|
|
@@ -7299,7 +8748,7 @@ async function runBranch(a, root, io = defaultIO()) {
|
|
|
7299
8748
|
const n = number(a, 2, "bridge branch update <n> [--choice key=mine|theirs] [--dry-run]");
|
|
7300
8749
|
const choices = pairs2(a.flags.choice, "choice");
|
|
7301
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}".`);
|
|
7302
|
-
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") });
|
|
7303
8752
|
return out(r, () => {
|
|
7304
8753
|
io.print(said(r));
|
|
7305
8754
|
for (const c of r.conflicts ?? []) io.print(` ${c.key} (${c.token}, default ${c.default ?? "none: pick one"})`);
|
|
@@ -7332,7 +8781,7 @@ ${BRANCH_HELP}`);
|
|
|
7332
8781
|
}
|
|
7333
8782
|
async function runStudioRead(cmd, a, root, io = defaultIO()) {
|
|
7334
8783
|
const ctx = await context4(root, io);
|
|
7335
|
-
const json =
|
|
8784
|
+
const json = has5(a, "json");
|
|
7336
8785
|
if (cmd === "products") {
|
|
7337
8786
|
const r2 = await branches.listProducts(ctx);
|
|
7338
8787
|
if (json) return io.print(JSON.stringify(r2, null, 2)), 0;
|
|
@@ -7383,9 +8832,9 @@ async function runDocsSave(a, root, io = defaultIO()) {
|
|
|
7383
8832
|
...flag4(a, "product") ? { product: flag4(a, "product") } : {},
|
|
7384
8833
|
...changes ? { changes } : {},
|
|
7385
8834
|
source: "cli",
|
|
7386
|
-
dryRun:
|
|
8835
|
+
dryRun: has5(a, "dry-run")
|
|
7387
8836
|
});
|
|
7388
|
-
if (
|
|
8837
|
+
if (has5(a, "json")) io.print(JSON.stringify(r, null, 2));
|
|
7389
8838
|
else io.print(r.status === "dry-run" || r.status === "preview" ? `${r.said ?? "Dry run, nothing written."}
|
|
7390
8839
|
|
|
7391
8840
|
${r.path}:
|
|
@@ -7410,7 +8859,7 @@ Usage:
|
|
|
7410
8859
|
bridge pieces switch <n> once the library shipped: main into the product branch
|
|
7411
8860
|
|
|
7412
8861
|
GitHub token: GITHUB_TOKEN or BRIDGE_GITHUB_TOKEN, else \`gh auth token\`. Exit codes: 0 ok, 2 usage or setup error.`;
|
|
7413
|
-
var
|
|
8862
|
+
var has6 = (a, k) => Boolean(a.flags[k]);
|
|
7414
8863
|
async function context5(root, io) {
|
|
7415
8864
|
const token = io.env.BRIDGE_GITHUB_TOKEN || io.env.GITHUB_TOKEN || io.ghToken();
|
|
7416
8865
|
return BridgeContext6.create({ root, ...token ? { token } : {}, ...io.fetch ? { fetch: io.fetch } : {} });
|
|
@@ -7427,11 +8876,11 @@ function piece(a, at, usage) {
|
|
|
7427
8876
|
}
|
|
7428
8877
|
async function runPieces(a, root, io = defaultIO()) {
|
|
7429
8878
|
const sub = a._[1];
|
|
7430
|
-
if (!sub || sub === "help" ||
|
|
8879
|
+
if (!sub || sub === "help" || has6(a, "help")) {
|
|
7431
8880
|
io.print(PIECES_HELP);
|
|
7432
|
-
return sub ||
|
|
8881
|
+
return sub || has6(a, "help") ? 0 : 2;
|
|
7433
8882
|
}
|
|
7434
|
-
const json =
|
|
8883
|
+
const json = has6(a, "json");
|
|
7435
8884
|
const ctx = await context5(root, io);
|
|
7436
8885
|
const out = (r, text) => (json ? io.print(JSON.stringify(r, null, 2)) : text(), 0);
|
|
7437
8886
|
switch (sub) {
|
|
@@ -7448,7 +8897,7 @@ async function runPieces(a, root, io = defaultIO()) {
|
|
|
7448
8897
|
case "open": {
|
|
7449
8898
|
const usage = "bridge pieces open <n> <piece>";
|
|
7450
8899
|
const changes = changesFlag(a);
|
|
7451
|
-
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 } : {} });
|
|
7452
8901
|
return out(r, () => {
|
|
7453
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}.`);
|
|
7454
8903
|
if (r.library) io.print(` #${r.library.number} ${r.library.url}`);
|
|
@@ -7467,7 +8916,7 @@ async function runPieces(a, root, io = defaultIO()) {
|
|
|
7467
8916
|
return out(r, () => io.print(r.said));
|
|
7468
8917
|
}
|
|
7469
8918
|
default: {
|
|
7470
|
-
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") });
|
|
7471
8920
|
return out(r, () => {
|
|
7472
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.`);
|
|
7473
8922
|
for (const p of r.pieces) io.print(` ${p.id} ${p.title}: ${p.summary} Opens ${p.opens}.${"linked" in p ? ` Linked: #${p.linked}.` : ""}`);
|
|
@@ -7497,7 +8946,7 @@ Usage:
|
|
|
7497
8946
|
token change, or a branch with the spec for Claude AI
|
|
7498
8947
|
|
|
7499
8948
|
GitHub token: GITHUB_TOKEN or BRIDGE_GITHUB_TOKEN, else \`gh auth token\`. Exit codes: 0 ok, 2 usage or setup error.`;
|
|
7500
|
-
var
|
|
8949
|
+
var has7 = (a, k) => Boolean(a.flags[k]);
|
|
7501
8950
|
async function context6(root, io) {
|
|
7502
8951
|
const token = io.env.BRIDGE_GITHUB_TOKEN || io.env.GITHUB_TOKEN || io.ghToken();
|
|
7503
8952
|
return BridgeContext7.create({ root, ...token ? { token } : {}, ...io.fetch ? { fetch: io.fetch } : {} });
|
|
@@ -7509,17 +8958,17 @@ function number3(a, usage) {
|
|
|
7509
8958
|
}
|
|
7510
8959
|
async function runFigma(a, root, io = defaultIO()) {
|
|
7511
8960
|
const sub = a._[1];
|
|
7512
|
-
if (sub === "help" ||
|
|
8961
|
+
if (sub === "help" || has7(a, "help")) {
|
|
7513
8962
|
io.print(FIGMA_HELP);
|
|
7514
8963
|
return 0;
|
|
7515
8964
|
}
|
|
7516
|
-
const json =
|
|
8965
|
+
const json = has7(a, "json");
|
|
7517
8966
|
const ctx = await context6(root, io);
|
|
7518
8967
|
const out = (r, text) => (json ? io.print(JSON.stringify(r, null, 2)) : text(), 0);
|
|
7519
8968
|
switch (sub) {
|
|
7520
8969
|
case void 0:
|
|
7521
8970
|
case "list": {
|
|
7522
|
-
const r = await figma.figmaChangesOp(ctx, { state:
|
|
8971
|
+
const r = await figma.figmaChangesOp(ctx, { state: has7(a, "all") ? "all" : "open" });
|
|
7523
8972
|
return out(r, () => {
|
|
7524
8973
|
for (const c of r.changes) io.print(`#${c.pr} ${c.title} (${c.route}): ${c.status}. ${c.said}`);
|
|
7525
8974
|
io.print(`Next: ${r.next}`);
|
|
@@ -7527,7 +8976,7 @@ async function runFigma(a, root, io = defaultIO()) {
|
|
|
7527
8976
|
}
|
|
7528
8977
|
case "show": {
|
|
7529
8978
|
const r = await figma.getFigmaChangeOp(ctx, { pr: number3(a, "bridge figma show <n>") });
|
|
7530
|
-
if (
|
|
8979
|
+
if (has7(a, "draft")) {
|
|
7531
8980
|
if (!r.brief) throw new RequestUsageError(`#${r.pr} is an exact token change: there is no code to draft.`);
|
|
7532
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.");
|
|
7533
8982
|
return io.runClaude(r.brief, root);
|
|
@@ -7553,7 +9002,7 @@ ${r.brief}`);
|
|
|
7553
9002
|
throw new RequestUsageError(`${file} could not be read as JSON: ${err.message}`);
|
|
7554
9003
|
}
|
|
7555
9004
|
const changes = changesFlag(a);
|
|
7556
|
-
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 } : {} });
|
|
7557
9006
|
return out(r, () => {
|
|
7558
9007
|
io.print(`${r.component}: ${r.route}. ${r.said}`);
|
|
7559
9008
|
for (const x of r.reasons ?? []) io.print(` ${x}`);
|
|
@@ -7639,7 +9088,20 @@ async function writeSnapshot(opts) {
|
|
|
7639
9088
|
`);
|
|
7640
9089
|
written.push(summary.path);
|
|
7641
9090
|
}
|
|
7642
|
-
|
|
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 } : {} };
|
|
7643
9105
|
}
|
|
7644
9106
|
function snapshotLines(r, out) {
|
|
7645
9107
|
const s = r.snapshot;
|
|
@@ -7652,7 +9114,24 @@ function snapshotLines(r, out) {
|
|
|
7652
9114
|
` contrast ${c.checks} checks over ${c.pairs} pairs, ${c.failing} failing`,
|
|
7653
9115
|
` figma ${s.summary.figma ? `${s.summary.figma.inFigma} of ${s.summary.figma.visual} visual components` : "no component-specs.json"}`,
|
|
7654
9116
|
` audits ${s.audits.length ? s.audits.map((a) => a.path).join(", ") : "none (--audit <path> or --audit-report <file|dir>)"}`,
|
|
7655
|
-
...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}`)
|
|
7656
9135
|
];
|
|
7657
9136
|
}
|
|
7658
9137
|
|
|
@@ -7689,7 +9168,7 @@ async function rolesReport(opts) {
|
|
|
7689
9168
|
|
|
7690
9169
|
// src/index.ts
|
|
7691
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";
|
|
7692
|
-
var VERSION = true ? "0.
|
|
9171
|
+
var VERSION = true ? "0.12.0" : "dev";
|
|
7693
9172
|
var MCP_PACKAGE = "@bridge-ds/mcp";
|
|
7694
9173
|
var HELP = `bridge ${VERSION}: design system as code, for you and your Claude
|
|
7695
9174
|
|
|
@@ -7750,11 +9229,14 @@ Usage:
|
|
|
7750
9229
|
bridge audit <path> --fix [--write] safe autofix: CSS values equal to a token become var(--token).
|
|
7751
9230
|
Dry run unless --write
|
|
7752
9231
|
bridge snapshot [--out bridge-data] [--audit <path>[=label] ...] [--audit-report <file|dir> ...]
|
|
7753
|
-
[--branch main] [--json]
|
|
9232
|
+
[--branch main] [--activity [--repo owner/name]] [--json]
|
|
7754
9233
|
the repo model of this checkout for hosted Bridge (Studio and Library):
|
|
7755
9234
|
snapshot.json (token, contrast and component files, Figma coverage,
|
|
7756
9235
|
contrast results) plus audits/<id>.json. --audit runs bridge audit on a
|
|
7757
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.
|
|
7758
9240
|
The bridge-data workflow publishes the folder to the bridge-data branch
|
|
7759
9241
|
bridge docs draft <Component> [--json]
|
|
7760
9242
|
docs.json pre-fill from the code (props, tokens, anatomy, contrast,
|
|
@@ -7830,7 +9312,7 @@ function parse(argv) {
|
|
|
7830
9312
|
return out;
|
|
7831
9313
|
}
|
|
7832
9314
|
var flag5 = (a, k) => a.flags[k]?.[a.flags[k].length - 1];
|
|
7833
|
-
var
|
|
9315
|
+
var has8 = (a, k) => Boolean(a.flags[k]);
|
|
7834
9316
|
var print = (s) => process.stdout.write(`${s}
|
|
7835
9317
|
`);
|
|
7836
9318
|
var modeArg = (s) => s ? Object.fromEntries(s.split(",").map((kv) => kv.split("="))) : void 0;
|
|
@@ -7846,7 +9328,7 @@ function printSent(r) {
|
|
|
7846
9328
|
async function init(a, root) {
|
|
7847
9329
|
const cfgPath = join8(root, CONFIG_FILE2);
|
|
7848
9330
|
let config;
|
|
7849
|
-
if (existsSync8(cfgPath) && !
|
|
9331
|
+
if (existsSync8(cfgPath) && !has8(a, "force")) {
|
|
7850
9332
|
print(`${CONFIG_FILE2} exists, keeping it (--force to regenerate).`);
|
|
7851
9333
|
config = (await (await BridgeContext8.create({ root })).config()).config;
|
|
7852
9334
|
} else {
|
|
@@ -7880,10 +9362,10 @@ async function check(a, root) {
|
|
|
7880
9362
|
const v = await ops.validateTokens(ctx, { includeWarnings: true });
|
|
7881
9363
|
const c = await ops.checkContrast(ctx, {});
|
|
7882
9364
|
const warnings = "warnings" in v ? v.warnings ?? [] : [];
|
|
7883
|
-
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;
|
|
7884
9366
|
const protection = await protectionOf(a, root, ctx);
|
|
7885
9367
|
const roles = await rolesOf(a, root, ctx);
|
|
7886
|
-
if (
|
|
9368
|
+
if (has8(a, "json")) {
|
|
7887
9369
|
print(JSON.stringify({ ok: !failed, validation: v, contrast: c, ...protection ? { protection: protection.state } : {}, ...roles?.teams.length ? { teams: roles.teams } : {} }, null, 2));
|
|
7888
9370
|
return failed ? 1 : 0;
|
|
7889
9371
|
}
|
|
@@ -7902,7 +9384,7 @@ async function check(a, root) {
|
|
|
7902
9384
|
return failed ? 1 : 0;
|
|
7903
9385
|
}
|
|
7904
9386
|
async function protectionOf(a, root, ctx) {
|
|
7905
|
-
if (
|
|
9387
|
+
if (has8(a, "offline") || process.env.BRIDGE_OFFLINE === "1") return void 0;
|
|
7906
9388
|
const { config } = await ctx.config();
|
|
7907
9389
|
const repo = config.repo ? `${config.repo.owner}/${config.repo.name}` : await originRepo(root);
|
|
7908
9390
|
if (!repo) return void 0;
|
|
@@ -7910,7 +9392,7 @@ async function protectionOf(a, root, ctx) {
|
|
|
7910
9392
|
return protectionReport({ repo, defaultBranch: config.repo?.defaultBranch ?? "main", token });
|
|
7911
9393
|
}
|
|
7912
9394
|
async function rolesOf(a, root, ctx) {
|
|
7913
|
-
if (
|
|
9395
|
+
if (has8(a, "offline") || process.env.BRIDGE_OFFLINE === "1") return void 0;
|
|
7914
9396
|
const { config } = await ctx.config();
|
|
7915
9397
|
const repo = config.repo ? `${config.repo.owner}/${config.repo.name}` : await originRepo(root);
|
|
7916
9398
|
if (!repo) return void 0;
|
|
@@ -7922,7 +9404,7 @@ async function tokens(a, root) {
|
|
|
7922
9404
|
if (!q) throw new UsageError("bridge tokens <query>");
|
|
7923
9405
|
const ctx = await BridgeContext8.create({ root });
|
|
7924
9406
|
const r = await ops.searchTokens(ctx, { query: q, limit: Number(flag5(a, "limit") ?? 20) });
|
|
7925
|
-
if (
|
|
9407
|
+
if (has8(a, "json")) return print(JSON.stringify(r, null, 2));
|
|
7926
9408
|
if (!r.results.length) return print(`No token matches "${q}".`);
|
|
7927
9409
|
const w = Math.max(...r.results.map((x) => x.path.length));
|
|
7928
9410
|
for (const t of r.results) print(`${t.path.padEnd(w)} ${String(t.value).padEnd(10)} ${(t.tier ?? "").padEnd(9)} ${t.description ?? ""}`);
|
|
@@ -7939,11 +9421,11 @@ async function diff(a, root) {
|
|
|
7939
9421
|
const delta2 = pairs3.length ? core.contrastDelta(core.runContrast(before, pairs3, config.contrast?.modes), core.runContrast(after, pairs3, config.contrast?.modes)) : [];
|
|
7940
9422
|
const tier = core.classify(changes, [], config);
|
|
7941
9423
|
const imp = core.impact(before, after, changes, 30);
|
|
7942
|
-
if (
|
|
9424
|
+
if (has8(a, "json")) {
|
|
7943
9425
|
print(JSON.stringify({ base, changes, contrast: delta2, tier, impact: imp }, null, 2));
|
|
7944
9426
|
return 0;
|
|
7945
9427
|
}
|
|
7946
|
-
if (
|
|
9428
|
+
if (has8(a, "md")) {
|
|
7947
9429
|
print(`## Token changes since \`${base}\`
|
|
7948
9430
|
|
|
7949
9431
|
${core.tokenTable(changes)}
|
|
@@ -8009,9 +9491,9 @@ async function propose(a, root) {
|
|
|
8009
9491
|
...product ? { product } : {},
|
|
8010
9492
|
...changes ? { changes } : {},
|
|
8011
9493
|
source: "cli",
|
|
8012
|
-
dryRun:
|
|
9494
|
+
dryRun: has8(a, "dry-run")
|
|
8013
9495
|
});
|
|
8014
|
-
if (
|
|
9496
|
+
if (has8(a, "json")) print(JSON.stringify(r, null, 2));
|
|
8015
9497
|
else {
|
|
8016
9498
|
print(`status: ${r.status}${r.why ? ` (${r.why})` : ""}`);
|
|
8017
9499
|
if (r.tier) print(`tier ${r.tier}, ${r.bump}`);
|
|
@@ -8029,7 +9511,7 @@ async function brandCmd(a, root) {
|
|
|
8029
9511
|
const ctx = await BridgeContext8.create({ root, ...token ? { token } : {} });
|
|
8030
9512
|
if (sub === "list") {
|
|
8031
9513
|
const r2 = await brand.listBrands(ctx);
|
|
8032
|
-
if (
|
|
9514
|
+
if (has8(a, "json")) print(JSON.stringify(r2.brands, null, 2));
|
|
8033
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(", ")}` : ""}`);
|
|
8034
9516
|
return 0;
|
|
8035
9517
|
}
|
|
@@ -8051,9 +9533,9 @@ async function brandCmd(a, root) {
|
|
|
8051
9533
|
...opt("notes") ? { notes: opt("notes") } : {},
|
|
8052
9534
|
...changes ? { changes } : {},
|
|
8053
9535
|
source: "cli",
|
|
8054
|
-
dryRun:
|
|
9536
|
+
dryRun: has8(a, "dry-run")
|
|
8055
9537
|
});
|
|
8056
|
-
if (
|
|
9538
|
+
if (has8(a, "json")) {
|
|
8057
9539
|
const { fileContents: _f, ...rest } = r;
|
|
8058
9540
|
print(JSON.stringify(rest, null, 2));
|
|
8059
9541
|
return r.status === "blocked" ? 1 : 0;
|
|
@@ -8087,9 +9569,9 @@ async function figmaReferences(a, root) {
|
|
|
8087
9569
|
out: resolve6(root, flag5(a, "out") ?? "figma-references"),
|
|
8088
9570
|
sha,
|
|
8089
9571
|
...only.length ? { only } : {},
|
|
8090
|
-
log:
|
|
9572
|
+
log: has8(a, "json") ? void 0 : (line2) => print(line2)
|
|
8091
9573
|
});
|
|
8092
|
-
if (
|
|
9574
|
+
if (has8(a, "json")) print(JSON.stringify({ manifest: r.manifestPath, captured: r.captured, skipped: r.skipped }, null, 2));
|
|
8093
9575
|
return 0;
|
|
8094
9576
|
}
|
|
8095
9577
|
async function impact(a, root) {
|
|
@@ -8114,15 +9596,15 @@ async function impact(a, root) {
|
|
|
8114
9596
|
...maxStories ? { maxStoriesPerComponent: maxStories } : {},
|
|
8115
9597
|
...imageBase !== void 0 ? { imageBase } : {},
|
|
8116
9598
|
...artifactUrl ? { artifactUrl } : {},
|
|
8117
|
-
plan:
|
|
8118
|
-
keepUnchanged:
|
|
9599
|
+
plan: has8(a, "plan"),
|
|
9600
|
+
keepUnchanged: has8(a, "keep-unchanged")
|
|
8119
9601
|
});
|
|
8120
|
-
if (
|
|
9602
|
+
if (has8(a, "json")) {
|
|
8121
9603
|
print(JSON.stringify({ summary: r.summary, manifest: r.manifestPath, comment: r.commentPath ?? null, ...r.manifest.summary, selection: r.manifest.selection }, null, 2));
|
|
8122
9604
|
return 0;
|
|
8123
9605
|
}
|
|
8124
9606
|
const sel = r.manifest.selection;
|
|
8125
|
-
if (
|
|
9607
|
+
if (has8(a, "plan")) {
|
|
8126
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)})`);
|
|
8127
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(", ")})` : ""}`);
|
|
8128
9610
|
print(` components: ${sel.components.map((c) => c.id).join(", ") || "none"}`);
|
|
@@ -8143,7 +9625,7 @@ async function historyCmd(a, root) {
|
|
|
8143
9625
|
const limit = Number(flag5(a, "limit") ?? 20);
|
|
8144
9626
|
if (token) {
|
|
8145
9627
|
const r2 = await hist.getTokenHistory(ctx, { path: token, limit });
|
|
8146
|
-
if (
|
|
9628
|
+
if (has8(a, "json")) return print(JSON.stringify(r2, null, 2)), 0;
|
|
8147
9629
|
if (!r2.total) return print(`No recorded change of ${r2.path} in ${r2.source}.`), 0;
|
|
8148
9630
|
print(`${r2.path}: ${r2.total} change(s) (${r2.source})
|
|
8149
9631
|
`);
|
|
@@ -8155,8 +9637,8 @@ async function historyCmd(a, root) {
|
|
|
8155
9637
|
return 0;
|
|
8156
9638
|
}
|
|
8157
9639
|
const tier = flag5(a, "tier");
|
|
8158
|
-
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") } : {}, ...
|
|
8159
|
-
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;
|
|
8160
9642
|
print(`${r.source}${r.currentVersion ? `, package version ${r.currentVersion}` : ""}`);
|
|
8161
9643
|
if (!r.total) return print(r.note ?? "No token change matches."), 0;
|
|
8162
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" : ""}`);
|
|
@@ -8178,8 +9660,8 @@ async function revertCmd(a, root) {
|
|
|
8178
9660
|
const token = changes !== "direct" ? githubToken() : void 0;
|
|
8179
9661
|
const ctx = await BridgeContext8.create({ root, ...token ? { token } : {} });
|
|
8180
9662
|
const resolutions = Object.fromEntries((a.flags["use-before"] ?? []).map((k) => [k, "use-before"]));
|
|
8181
|
-
const r = await hist.proposeRevert(ctx, { target, resolutions, restoreConflicts:
|
|
8182
|
-
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")) {
|
|
8183
9665
|
print(JSON.stringify(r, null, 2));
|
|
8184
9666
|
return r.status === "blocked" ? 1 : 0;
|
|
8185
9667
|
}
|
|
@@ -8206,8 +9688,8 @@ async function audit(a, root) {
|
|
|
8206
9688
|
const dsRoot = flag5(a, "ds") ? resolve6(flag5(a, "ds")) : existsSync8(join8(root, CONFIG_FILE2)) ? root : findDesignSystemRoot(root);
|
|
8207
9689
|
if (!dsRoot) throw new UsageError(`No ${CONFIG_FILE2} found: run bridge audit from the design system repo or pass --ds <dir>.`);
|
|
8208
9690
|
const ds = loadDesignSystem(dsRoot);
|
|
8209
|
-
if (
|
|
8210
|
-
const write =
|
|
9691
|
+
if (has8(a, "fix")) {
|
|
9692
|
+
const write = has8(a, "write");
|
|
8211
9693
|
const changes = fixCss(target, ds.index, write);
|
|
8212
9694
|
const n = changes.reduce((acc, c) => acc + c.edits.length, 0);
|
|
8213
9695
|
for (const c of changes) for (const e of c.edits) print(`${c.file}:${e.line}:${e.column} ${e.from} -> ${e.to}`);
|
|
@@ -8283,7 +9765,7 @@ async function audit(a, root) {
|
|
|
8283
9765
|
code = 1;
|
|
8284
9766
|
}
|
|
8285
9767
|
}
|
|
8286
|
-
if (
|
|
9768
|
+
if (has8(a, "no-regress") && report.baseline) {
|
|
8287
9769
|
const b = report.baseline;
|
|
8288
9770
|
if ((b.tokenAdoption.delta ?? 0) < 0 || (b.componentAdoption.delta ?? 0) < 0 || b.hardcoded.delta > 0) {
|
|
8289
9771
|
process.stderr.write("bridge audit: adoption regressed since the baseline\n");
|
|
@@ -8295,8 +9777,31 @@ async function audit(a, root) {
|
|
|
8295
9777
|
async function snapshotCmd(a, root) {
|
|
8296
9778
|
const out = resolve6(root, flag5(a, "out") ?? "bridge-data");
|
|
8297
9779
|
const branch = flag5(a, "branch");
|
|
8298
|
-
|
|
8299
|
-
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
|
+
);
|
|
8300
9805
|
else {
|
|
8301
9806
|
const shown = relative5(process.cwd(), out);
|
|
8302
9807
|
for (const line2 of snapshotLines(r, shown.startsWith("..") || isAbsolute(shown) ? out : shown || ".")) print(line2);
|
|
@@ -8308,7 +9813,7 @@ async function docsCmd(a, root) {
|
|
|
8308
9813
|
if (sub !== "draft" || !name) throw new UsageError("Usage: bridge docs draft <Component> [--json]");
|
|
8309
9814
|
const ctx = await BridgeContext8.create({ root });
|
|
8310
9815
|
const d = await ops.draftDocsOp(ctx, { name });
|
|
8311
|
-
if (
|
|
9816
|
+
if (has8(a, "json")) {
|
|
8312
9817
|
print(JSON.stringify({ component: d.component, path: d.path, exists: d.exists, prefill: d.prefill, prompt: d.prompt }, null, 2));
|
|
8313
9818
|
return 0;
|
|
8314
9819
|
}
|
|
@@ -8350,10 +9855,10 @@ async function main() {
|
|
|
8350
9855
|
if (cmd === "review") return runReview(a, root);
|
|
8351
9856
|
if (cmd === "products" || cmd === "settings" || cmd === "coverage") return runStudioRead(cmd, a, root);
|
|
8352
9857
|
if (cmd === "docs" && a._[1] === "save") return runDocsSave(a, root);
|
|
8353
|
-
if (cmd === "version" || !cmd &&
|
|
8354
|
-
if (!cmd ||
|
|
9858
|
+
if (cmd === "version" || !cmd && has8(a, "version")) return print(VERSION), 0;
|
|
9859
|
+
if (!cmd || has8(a, "help") || cmd === "help") {
|
|
8355
9860
|
print(HELP);
|
|
8356
|
-
return cmd ||
|
|
9861
|
+
return cmd || has8(a, "help") ? 0 : 2;
|
|
8357
9862
|
}
|
|
8358
9863
|
switch (cmd) {
|
|
8359
9864
|
case "init":
|