@bridge-ds/cli 0.2.0 → 0.3.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 +689 -136
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1115,9 +1115,9 @@ var DesignSystemIndex = class {
|
|
|
1115
1115
|
return generic ? this.make(generic, 1, 0.3) : void 0;
|
|
1116
1116
|
}
|
|
1117
1117
|
case "shadow": {
|
|
1118
|
-
const
|
|
1118
|
+
const norm4 = (s) => s.replace(/\s+/g, " ").replace(/\s*,\s*/g, ", ").trim().toLowerCase();
|
|
1119
1119
|
const cands = this.entries(["shadow"]);
|
|
1120
|
-
const exact = cands.find((e) =>
|
|
1120
|
+
const exact = cands.find((e) => norm4(e.css) === norm4(value));
|
|
1121
1121
|
if (exact) return this.make(exact, 0, 1);
|
|
1122
1122
|
const t = shadowNums(value);
|
|
1123
1123
|
const scored = cands.map((e) => {
|
|
@@ -1766,6 +1766,226 @@ async function originRepo(root) {
|
|
|
1766
1766
|
// ../core/src/previews.ts
|
|
1767
1767
|
var PREVIEW_APP_LABELS = { docs: "Docs", storybook: "Storybook", admin: "Admin demo" };
|
|
1768
1768
|
|
|
1769
|
+
// ../core/src/teams.ts
|
|
1770
|
+
var LOGIN = /^[A-Za-z0-9](?:[A-Za-z0-9]|-(?=[A-Za-z0-9])){0,38}$/;
|
|
1771
|
+
var SLUG = /^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/;
|
|
1772
|
+
function parseOwnerEntry(raw) {
|
|
1773
|
+
if (typeof raw !== "string") return null;
|
|
1774
|
+
const s = raw.trim().replace(/^@/, "");
|
|
1775
|
+
const slash = s.indexOf("/");
|
|
1776
|
+
if (slash < 0) return LOGIN.test(s) ? { kind: "person", login: s } : null;
|
|
1777
|
+
const org = s.slice(0, slash);
|
|
1778
|
+
const slug = s.slice(slash + 1);
|
|
1779
|
+
return LOGIN.test(org) && SLUG.test(slug) ? { kind: "team", org, slug } : null;
|
|
1780
|
+
}
|
|
1781
|
+
var teamKey = (org, slug) => `@${org.toLowerCase()}/${slug.toLowerCase()}`;
|
|
1782
|
+
function ownerEntryProblems(raw) {
|
|
1783
|
+
if (raw === void 0) return [];
|
|
1784
|
+
const isObj2 = (x) => Boolean(x) && typeof x === "object" && !Array.isArray(x);
|
|
1785
|
+
if (!isObj2(raw)) return ['"governance.owners" must be { "design": [people or teams], "tech": [people or teams] }'];
|
|
1786
|
+
const out = [];
|
|
1787
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
1788
|
+
if (k !== "design" && k !== "tech") {
|
|
1789
|
+
out.push(`"governance.owners.${k}" is not an owner group (design, tech)`);
|
|
1790
|
+
continue;
|
|
1791
|
+
}
|
|
1792
|
+
if (!Array.isArray(v)) {
|
|
1793
|
+
out.push(`"governance.owners.${k}" must be a list of people or teams`);
|
|
1794
|
+
continue;
|
|
1795
|
+
}
|
|
1796
|
+
v.forEach((x, i) => {
|
|
1797
|
+
if (typeof x !== "string" || !parseOwnerEntry(x))
|
|
1798
|
+
out.push(`"governance.owners.${k}[${i}]" (${JSON.stringify(x)}) is not a GitHub login or team. Write a person as "peterbono" or "@peterbono", a team as "@org/team-slug".`);
|
|
1799
|
+
});
|
|
1800
|
+
}
|
|
1801
|
+
return out;
|
|
1802
|
+
}
|
|
1803
|
+
function teamUnreadableMessage(team, reason) {
|
|
1804
|
+
const name = `@${team.org}/${team.slug}`;
|
|
1805
|
+
switch (reason) {
|
|
1806
|
+
case "personal-account":
|
|
1807
|
+
return `${name} cannot be used: ${team.org} is a personal GitHub account, and teams only exist in organizations. List people instead, or move the repository to an organization.`;
|
|
1808
|
+
case "missing-permission":
|
|
1809
|
+
return `The members of ${name} cannot be read: the GitHub token needs read:org (classic token) or Members: Read on the ${team.org} organization (fine-grained token).`;
|
|
1810
|
+
case "no-token":
|
|
1811
|
+
return `The members of ${name} cannot be read without a working GitHub token. Connect GitHub with a token that has read:org.`;
|
|
1812
|
+
case "not-found":
|
|
1813
|
+
return `${name} was not found. Check the team slug on GitHub, or give the token read:org so it can see the team.`;
|
|
1814
|
+
case "unreachable":
|
|
1815
|
+
return `The members of ${name} cannot be read right now: GitHub did not answer. Try again in a moment.`;
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
function teamsIn(owners) {
|
|
1819
|
+
const all = Array.isArray(owners) ? owners : [...owners.design ?? [], ...owners.tech ?? []];
|
|
1820
|
+
const out = /* @__PURE__ */ new Map();
|
|
1821
|
+
for (const raw of all) {
|
|
1822
|
+
const e = parseOwnerEntry(raw);
|
|
1823
|
+
if (e?.kind === "team") out.set(teamKey(e.org, e.slug), { key: teamKey(e.org, e.slug), org: e.org, slug: e.slug });
|
|
1824
|
+
}
|
|
1825
|
+
return [...out.values()];
|
|
1826
|
+
}
|
|
1827
|
+
var norm = (x) => x.trim().toLowerCase().replace(/^@/, "");
|
|
1828
|
+
var people = (n) => `${n} ${n === 1 ? "person" : "people"}`;
|
|
1829
|
+
function entryLabel(raw, teams) {
|
|
1830
|
+
const e = parseOwnerEntry(raw);
|
|
1831
|
+
if (!e) return raw;
|
|
1832
|
+
if (e.kind === "person") return `@${e.login}`;
|
|
1833
|
+
const state = teams?.[teamKey(e.org, e.slug)];
|
|
1834
|
+
const name = `@${e.org}/${e.slug}`;
|
|
1835
|
+
return state?.status === "ok" ? `${name} (${people(state.members.length)})` : name;
|
|
1836
|
+
}
|
|
1837
|
+
var TeamDirectory = class {
|
|
1838
|
+
constructor(reader, opts = {}) {
|
|
1839
|
+
this.reader = reader;
|
|
1840
|
+
this.ttl = opts.ttlMs ?? 5 * 6e4;
|
|
1841
|
+
this.now = opts.now ?? Date.now;
|
|
1842
|
+
}
|
|
1843
|
+
reader;
|
|
1844
|
+
teams = /* @__PURE__ */ new Map();
|
|
1845
|
+
accounts = /* @__PURE__ */ new Map();
|
|
1846
|
+
memberships = /* @__PURE__ */ new Map();
|
|
1847
|
+
ttl;
|
|
1848
|
+
now;
|
|
1849
|
+
accountType(org) {
|
|
1850
|
+
const k = org.toLowerCase();
|
|
1851
|
+
let p = this.accounts.get(k);
|
|
1852
|
+
if (!p) {
|
|
1853
|
+
p = this.reader.getAccountType(org).catch(() => null);
|
|
1854
|
+
this.accounts.set(k, p);
|
|
1855
|
+
}
|
|
1856
|
+
return p;
|
|
1857
|
+
}
|
|
1858
|
+
/** The reason a read of `org` failed, as a state. A 404 on a personal account says so; a 403 is a missing permission. */
|
|
1859
|
+
async failure(ref, err) {
|
|
1860
|
+
const e = err ?? {};
|
|
1861
|
+
let reason;
|
|
1862
|
+
if (!this.reader.hasToken) reason = await this.accountType(ref.org) === "User" ? "personal-account" : "no-token";
|
|
1863
|
+
else if (e.status === 401) reason = "no-token";
|
|
1864
|
+
else if (e.status === 403 || e.auth === "missing-permission") reason = "missing-permission";
|
|
1865
|
+
else if (e.status === 404) reason = await this.accountType(ref.org) === "User" ? "personal-account" : "not-found";
|
|
1866
|
+
else reason = "unreachable";
|
|
1867
|
+
return { ...ref, status: "unreadable", reason, message: teamUnreadableMessage(ref, reason) };
|
|
1868
|
+
}
|
|
1869
|
+
/** Members of one team, or why they cannot be read. Cached. */
|
|
1870
|
+
team(org, slug) {
|
|
1871
|
+
const ref = { key: teamKey(org, slug), org, slug };
|
|
1872
|
+
const hit = this.teams.get(ref.key);
|
|
1873
|
+
if (hit && this.now() - hit.at < this.ttl) return hit.value;
|
|
1874
|
+
const value = (async () => {
|
|
1875
|
+
if (!this.reader.hasToken) return this.failure(ref, void 0);
|
|
1876
|
+
try {
|
|
1877
|
+
const members = await this.reader.listTeamMembers(org, slug);
|
|
1878
|
+
return { ...ref, status: "ok", members };
|
|
1879
|
+
} catch (err) {
|
|
1880
|
+
return this.failure(ref, err);
|
|
1881
|
+
}
|
|
1882
|
+
})();
|
|
1883
|
+
this.teams.set(ref.key, { at: this.now(), value });
|
|
1884
|
+
void value.then((s) => {
|
|
1885
|
+
if (s.status === "unreadable" && s.reason === "unreachable") this.teams.delete(ref.key);
|
|
1886
|
+
});
|
|
1887
|
+
return value;
|
|
1888
|
+
}
|
|
1889
|
+
/** Every team named in the owner lists, read in parallel. Empty object when no team is named (no request). */
|
|
1890
|
+
async resolve(owners) {
|
|
1891
|
+
const refs = teamsIn(owners);
|
|
1892
|
+
const states = await Promise.all(refs.map((r) => this.team(r.org, r.slug)));
|
|
1893
|
+
return Object.fromEntries(states.map((s) => [s.key, s]));
|
|
1894
|
+
}
|
|
1895
|
+
/**
|
|
1896
|
+
* Whether `login` is an active member of one team: from the cached member list when there is one, else one
|
|
1897
|
+
* membership read (GET /orgs/{org}/teams/{slug}/memberships/{user}). An unreadable team returns its state.
|
|
1898
|
+
*/
|
|
1899
|
+
async isMember(org, slug, login) {
|
|
1900
|
+
const ref = { key: teamKey(org, slug), org, slug };
|
|
1901
|
+
const cached = this.teams.get(ref.key);
|
|
1902
|
+
if (cached && this.now() - cached.at < this.ttl) {
|
|
1903
|
+
const s = await cached.value;
|
|
1904
|
+
if (s.status === "ok") return s.members.some((m) => norm(m) === norm(login));
|
|
1905
|
+
}
|
|
1906
|
+
const k = `${ref.key}#${norm(login)}`;
|
|
1907
|
+
const hit = this.memberships.get(k);
|
|
1908
|
+
if (hit && this.now() - hit.at < this.ttl) return hit.value;
|
|
1909
|
+
const value = (async () => {
|
|
1910
|
+
if (!this.reader.hasToken) return this.failure(ref, void 0);
|
|
1911
|
+
try {
|
|
1912
|
+
return await this.reader.getTeamMembership(org, slug, login) === "active";
|
|
1913
|
+
} catch (err) {
|
|
1914
|
+
return this.failure(ref, err);
|
|
1915
|
+
}
|
|
1916
|
+
})();
|
|
1917
|
+
this.memberships.set(k, { at: this.now(), value });
|
|
1918
|
+
return value;
|
|
1919
|
+
}
|
|
1920
|
+
/**
|
|
1921
|
+
* Whether `login` holds a right given to `entries` (people and teams): a person entry matches directly, a team
|
|
1922
|
+
* through its membership. Teams are only read when no person entry matches.
|
|
1923
|
+
*/
|
|
1924
|
+
async memberOfAny(entries, login) {
|
|
1925
|
+
const unreadable = [];
|
|
1926
|
+
const parsed = entries.map(parseOwnerEntry).filter((e) => Boolean(e));
|
|
1927
|
+
if (parsed.some((e) => e.kind === "person" && norm(e.login) === norm(login))) return { member: true, unreadable };
|
|
1928
|
+
for (const e of parsed) {
|
|
1929
|
+
if (e.kind !== "team") continue;
|
|
1930
|
+
const r = await this.isMember(e.org, e.slug, login);
|
|
1931
|
+
if (r === true) return { member: true, unreadable };
|
|
1932
|
+
if (typeof r === "object" && r.status === "unreadable") unreadable.push(r);
|
|
1933
|
+
}
|
|
1934
|
+
return { member: false, unreadable };
|
|
1935
|
+
}
|
|
1936
|
+
/** The organization's teams for a picker, or why they cannot be listed (a personal account has none). */
|
|
1937
|
+
async orgTeams(org) {
|
|
1938
|
+
if (await this.accountType(org) === "User") return { status: "unreadable", reason: "personal-account" };
|
|
1939
|
+
if (!this.reader.hasToken) return { status: "unreadable", reason: "no-token" };
|
|
1940
|
+
try {
|
|
1941
|
+
return { status: "ok", teams: await this.reader.listOrgTeams(org) };
|
|
1942
|
+
} catch (err) {
|
|
1943
|
+
const s = await this.failure({ key: teamKey(org, "*"), org, slug: "*" }, err);
|
|
1944
|
+
return { status: "unreadable", reason: s.status === "unreadable" ? s.reason : "unreachable" };
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
/** Drops every cached read (after the token changes). */
|
|
1948
|
+
clear() {
|
|
1949
|
+
this.teams.clear();
|
|
1950
|
+
this.accounts.clear();
|
|
1951
|
+
this.memberships.clear();
|
|
1952
|
+
}
|
|
1953
|
+
};
|
|
1954
|
+
|
|
1955
|
+
// ../core/src/rights.ts
|
|
1956
|
+
var OWNER_GROUPS = ["design", "tech"];
|
|
1957
|
+
var RIGHTS = ["approveDesign", "approveTech", "ship"];
|
|
1958
|
+
var DEFAULT_RIGHTS = { approveDesign: ["design"], approveTech: ["tech"], ship: ["tech"] };
|
|
1959
|
+
function normalizeRights(input) {
|
|
1960
|
+
const pick = (r) => {
|
|
1961
|
+
const v = input?.[r];
|
|
1962
|
+
if (!Array.isArray(v)) return [...DEFAULT_RIGHTS[r]];
|
|
1963
|
+
return OWNER_GROUPS.filter((g) => v.includes(g));
|
|
1964
|
+
};
|
|
1965
|
+
return { approveDesign: pick("approveDesign"), approveTech: pick("approveTech"), ship: pick("ship") };
|
|
1966
|
+
}
|
|
1967
|
+
function rightsProblems(raw) {
|
|
1968
|
+
if (raw === void 0) return [];
|
|
1969
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return ['"governance.rights" must be { "approveDesign": [groups], "approveTech": [groups], "ship": [groups] }'];
|
|
1970
|
+
const out = [];
|
|
1971
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
1972
|
+
if (!RIGHTS.includes(k)) out.push(`"governance.rights.${k}" is not a right (${RIGHTS.join(", ")})`);
|
|
1973
|
+
else if (!Array.isArray(v) || v.length === 0 || !v.every((g) => OWNER_GROUPS.includes(g)))
|
|
1974
|
+
out.push(`"governance.rights.${k}" must list one or more of "design", "tech"`);
|
|
1975
|
+
}
|
|
1976
|
+
return out;
|
|
1977
|
+
}
|
|
1978
|
+
function visualImpactProblems(raw) {
|
|
1979
|
+
if (raw === void 0) return [];
|
|
1980
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return ['"visualImpact" must be { "mode": "auto" | "on-demand" }'];
|
|
1981
|
+
const out = [];
|
|
1982
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
1983
|
+
if (k !== "mode") out.push(`"visualImpact.${k}" is not a setting (mode)`);
|
|
1984
|
+
else if (v !== "auto" && v !== "on-demand") out.push('"visualImpact.mode" must be "auto" or "on-demand"');
|
|
1985
|
+
}
|
|
1986
|
+
return out;
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1769
1989
|
// ../core/src/config.ts
|
|
1770
1990
|
var CONFIG_FILE = "bridge.config.json";
|
|
1771
1991
|
var DEFAULT_OVERRIDE = { allowed: [], requireReason: true, minReasonLength: 20 };
|
|
@@ -1903,9 +2123,11 @@ function defineConfig(input) {
|
|
|
1903
2123
|
requireReason: g.override.requireReason ?? DEFAULT_OVERRIDE.requireReason,
|
|
1904
2124
|
minReasonLength: g.override.minReasonLength ?? DEFAULT_OVERRIDE.minReasonLength
|
|
1905
2125
|
}
|
|
1906
|
-
} : {}
|
|
2126
|
+
} : {},
|
|
2127
|
+
...g.rights ? { rights: normalizeRights(g.rights) } : {}
|
|
1907
2128
|
},
|
|
1908
2129
|
...input.audit ? { audit: { ...input.audit } } : {},
|
|
2130
|
+
...input.visualImpact ? { visualImpact: { mode: input.visualImpact.mode === "on-demand" ? "on-demand" : "auto" } } : {},
|
|
1909
2131
|
...input.integrations ? { integrations: { ...input.integrations } } : {},
|
|
1910
2132
|
...input.previews ? {
|
|
1911
2133
|
previews: {
|
|
@@ -2012,9 +2234,7 @@ function loadConfig(source) {
|
|
|
2012
2234
|
else {
|
|
2013
2235
|
for (const k of ["protectedBranches", "labels", "guardrails"]) if (gov[k] !== void 0 && !(Array.isArray(gov[k]) && gov[k].every((x) => typeof x === "string"))) problems.push(`"governance.${k}" must be a list of strings`);
|
|
2014
2236
|
if (gov.branchPrefix !== void 0 && !isStr(gov.branchPrefix)) problems.push('"governance.branchPrefix" must be a non-empty string');
|
|
2015
|
-
|
|
2016
|
-
if (o !== void 0 && (!isObj(o) || ["design", "tech"].some((k) => o[k] !== void 0 && !(Array.isArray(o[k]) && o[k].every((x) => typeof x === "string")))))
|
|
2017
|
-
problems.push('"governance.owners" must be { "design": [logins], "tech": [logins] }');
|
|
2237
|
+
problems.push(...ownerEntryProblems(gov.owners));
|
|
2018
2238
|
const ov = gov.override;
|
|
2019
2239
|
if (ov !== void 0) {
|
|
2020
2240
|
if (!isObj(ov)) problems.push('"governance.override" must be an object');
|
|
@@ -2024,8 +2244,10 @@ function loadConfig(source) {
|
|
|
2024
2244
|
if (ov.minReasonLength !== void 0 && !(Number.isInteger(ov.minReasonLength) && ov.minReasonLength >= 0)) problems.push('"governance.override.minReasonLength" must be a whole number, 0 or more');
|
|
2025
2245
|
}
|
|
2026
2246
|
}
|
|
2247
|
+
problems.push(...rightsProblems(gov.rights));
|
|
2027
2248
|
}
|
|
2028
2249
|
}
|
|
2250
|
+
problems.push(...visualImpactProblems(d.visualImpact));
|
|
2029
2251
|
const audit2 = d.audit;
|
|
2030
2252
|
if (audit2 !== void 0) {
|
|
2031
2253
|
if (!isObj(audit2)) problems.push('"audit" must be an object');
|
|
@@ -2237,125 +2459,147 @@ import { mkdir as mkdir3, readFile as readFile3, rm as rm2, unlink, writeFile as
|
|
|
2237
2459
|
import { tmpdir } from "node:os";
|
|
2238
2460
|
import { join as join4, relative as relative3, resolve as resolve2 } from "node:path";
|
|
2239
2461
|
|
|
2240
|
-
// ../core/src/
|
|
2241
|
-
var
|
|
2242
|
-
var
|
|
2243
|
-
function
|
|
2244
|
-
const
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
const
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
var
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
if (
|
|
2260
|
-
const
|
|
2261
|
-
|
|
2262
|
-
if (
|
|
2263
|
-
if (
|
|
2264
|
-
|
|
2265
|
-
}
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
if (!p.
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
function
|
|
2278
|
-
const
|
|
2279
|
-
const
|
|
2280
|
-
const
|
|
2281
|
-
|
|
2282
|
-
const
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
for (const v of mod.vars) (byVar[v] ??= []).push(mod.id);
|
|
2313
|
-
}
|
|
2314
|
-
for (const k of Object.keys(byVar)) byVar[k].sort();
|
|
2315
|
-
return { componentsDir: dir, modules, byVar, globalVars };
|
|
2316
|
-
}
|
|
2317
|
-
function impactedModules(index, input) {
|
|
2318
|
-
const reasons = /* @__PURE__ */ new Map();
|
|
2319
|
-
const add = (id, reason, indirect) => {
|
|
2320
|
-
const r = reasons.get(id);
|
|
2321
|
-
if (r) {
|
|
2322
|
-
if (!r.reasons.includes(reason)) r.reasons.push(reason);
|
|
2323
|
-
r.indirect &&= indirect;
|
|
2324
|
-
} else reasons.set(id, { reasons: [reason], indirect });
|
|
2325
|
-
};
|
|
2326
|
-
const changedVars = new Set(input.vars ?? []);
|
|
2327
|
-
for (const v of changedVars) for (const id of index.byVar[v] ?? []) add(id, `uses ${v}`, false);
|
|
2328
|
-
for (const f of input.files ?? []) {
|
|
2329
|
-
const id = componentForPath(f, index.componentsDir);
|
|
2330
|
-
if (id && index.modules[id]) add(id, `file changed: ${norm(f)}`, false);
|
|
2331
|
-
}
|
|
2332
|
-
const renderedBy = /* @__PURE__ */ new Map();
|
|
2333
|
-
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);
|
|
2334
|
-
const queue = [...reasons.keys()];
|
|
2335
|
-
while (queue.length) {
|
|
2336
|
-
const id = queue.shift();
|
|
2337
|
-
for (const parent of renderedBy.get(id) ?? []) {
|
|
2338
|
-
const known = reasons.has(parent);
|
|
2339
|
-
add(parent, `renders ${id}`, true);
|
|
2340
|
-
if (!known) queue.push(parent);
|
|
2462
|
+
// ../core/src/protection.ts
|
|
2463
|
+
var PROTECTION_PARTS = ["review", "checks", "history", "deletion"];
|
|
2464
|
+
var protectionRulesetName = (defaultBranch) => `Protect the library (${defaultBranch})`;
|
|
2465
|
+
function refPattern(pattern) {
|
|
2466
|
+
const src = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "\0").replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]").replace(/\u0000/g, ".*");
|
|
2467
|
+
return new RegExp(`^${src}$`);
|
|
2468
|
+
}
|
|
2469
|
+
function rulesetTargetsDefault(ruleset, defaultBranch) {
|
|
2470
|
+
if (ruleset.target && ruleset.target !== "branch") return false;
|
|
2471
|
+
const ref = `refs/heads/${defaultBranch}`;
|
|
2472
|
+
const hit = (p) => p === "~ALL" || p === "~DEFAULT_BRANCH" || p === ref || p === defaultBranch || refPattern(p.startsWith("refs/") ? p : `refs/heads/${p}`).test(ref);
|
|
2473
|
+
const cond = ruleset.conditions?.ref_name;
|
|
2474
|
+
if (!cond) return false;
|
|
2475
|
+
if ((cond.exclude ?? []).some(hit)) return false;
|
|
2476
|
+
return (cond.include ?? []).some(hit);
|
|
2477
|
+
}
|
|
2478
|
+
var count = (v) => typeof v === "number" ? v : 0;
|
|
2479
|
+
function rulesetParts(ruleset) {
|
|
2480
|
+
const parts = /* @__PURE__ */ new Set();
|
|
2481
|
+
if (ruleset.enforcement !== "active") return parts;
|
|
2482
|
+
for (const rule of ruleset.rules ?? []) {
|
|
2483
|
+
if (rule.type === "deletion") parts.add("deletion");
|
|
2484
|
+
if (rule.type === "non_fast_forward") parts.add("history");
|
|
2485
|
+
if (rule.type === "pull_request" && count(rule.parameters?.required_approving_review_count) >= 1) parts.add("review");
|
|
2486
|
+
if (rule.type === "required_status_checks" && Array.isArray(rule.parameters?.required_status_checks) && rule.parameters.required_status_checks.length > 0) parts.add("checks");
|
|
2487
|
+
}
|
|
2488
|
+
return parts;
|
|
2489
|
+
}
|
|
2490
|
+
function classicParts(p) {
|
|
2491
|
+
const parts = /* @__PURE__ */ new Set();
|
|
2492
|
+
if (count(p.required_pull_request_reviews?.required_approving_review_count) >= 1) parts.add("review");
|
|
2493
|
+
if (((p.required_status_checks?.contexts?.length ?? 0) || (p.required_status_checks?.checks?.length ?? 0)) > 0) parts.add("checks");
|
|
2494
|
+
if (!p.allow_force_pushes?.enabled) parts.add("history");
|
|
2495
|
+
if (!p.allow_deletions?.enabled) parts.add("deletion");
|
|
2496
|
+
return parts;
|
|
2497
|
+
}
|
|
2498
|
+
var rulesetUrl = (r) => r.html_url ?? r._links?.html?.href;
|
|
2499
|
+
function protectionState(rulesets, branchProtection, defaultBranch) {
|
|
2500
|
+
const bridge = rulesets?.find((r) => r.name === protectionRulesetName(defaultBranch));
|
|
2501
|
+
const extra = bridge?.id !== void 0 ? { bridgeRulesetId: bridge.id } : {};
|
|
2502
|
+
const covered = /* @__PURE__ */ new Set();
|
|
2503
|
+
let url;
|
|
2504
|
+
for (const r of rulesets ?? []) {
|
|
2505
|
+
if (!rulesetTargetsDefault(r, defaultBranch)) continue;
|
|
2506
|
+
const parts = rulesetParts(r);
|
|
2507
|
+
if (parts.size && !url) url = rulesetUrl(r);
|
|
2508
|
+
parts.forEach((p) => covered.add(p));
|
|
2509
|
+
}
|
|
2510
|
+
if (branchProtection) classicParts(branchProtection).forEach((p) => covered.add(p));
|
|
2511
|
+
const missing = PROTECTION_PARTS.filter((p) => !covered.has(p));
|
|
2512
|
+
const withUrl = url ? { url } : {};
|
|
2513
|
+
if (!missing.length) return { kind: "protected", missing, ...extra, ...withUrl };
|
|
2514
|
+
const unreadable = rulesets == null || branchProtection === void 0;
|
|
2515
|
+
if (covered.size === 0) return { kind: unreadable ? "unknown" : "unprotected", missing, ...extra };
|
|
2516
|
+
return { kind: "partial", missing, ...extra, ...withUrl };
|
|
2517
|
+
}
|
|
2518
|
+
var PROTECTION_PART_PHRASE = {
|
|
2519
|
+
review: "changes can skip review",
|
|
2520
|
+
checks: "checks do not have to pass",
|
|
2521
|
+
history: "past changes can be rewritten",
|
|
2522
|
+
deletion: "the library can be deleted"
|
|
2523
|
+
};
|
|
2524
|
+
var joinAnd = (xs) => xs.length <= 1 ? xs[0] ?? "" : `${xs.slice(0, -1).join(", ")} and ${xs[xs.length - 1]}`;
|
|
2525
|
+
function protectionSentence(state) {
|
|
2526
|
+
switch (state.kind) {
|
|
2527
|
+
case "protected":
|
|
2528
|
+
return "Protected: every change goes through review.";
|
|
2529
|
+
case "unprotected":
|
|
2530
|
+
return "Your library can be changed without review.";
|
|
2531
|
+
case "partial": {
|
|
2532
|
+
const s = joinAnd(state.missing.map((p) => PROTECTION_PART_PHRASE[p]));
|
|
2533
|
+
return `Partly protected: ${s}.`;
|
|
2341
2534
|
}
|
|
2535
|
+
default:
|
|
2536
|
+
return "Protection status unknown: GitHub did not let this token read it.";
|
|
2342
2537
|
}
|
|
2343
|
-
const globalHits = Object.entries(index.globalVars).map(([file, vs]) => ({ file, vars: vs.filter((v) => changedVars.has(v)) })).filter((g) => g.vars.length > 0);
|
|
2344
|
-
const modules = [...reasons.entries()].map(([id, r]) => ({ id, reasons: r.reasons, indirect: r.indirect })).sort((a, b) => Number(a.indirect) - Number(b.indirect) || a.id.localeCompare(b.id));
|
|
2345
|
-
return { modules, globalHits };
|
|
2346
2538
|
}
|
|
2347
2539
|
|
|
2348
2540
|
// ../core/src/github-client.ts
|
|
2349
2541
|
var GitHubError = class extends Error {
|
|
2350
|
-
constructor(status, message, detail) {
|
|
2542
|
+
constructor(status, message, detail, info = {}) {
|
|
2351
2543
|
super(message);
|
|
2352
2544
|
this.status = status;
|
|
2353
2545
|
this.detail = detail;
|
|
2354
2546
|
this.name = "GitHubError";
|
|
2547
|
+
this.auth = info.auth;
|
|
2548
|
+
this.permission = info.permission;
|
|
2355
2549
|
}
|
|
2356
2550
|
status;
|
|
2357
2551
|
detail;
|
|
2552
|
+
/** Set when the failure is about the token: rejected (401) or missing a permission (403). */
|
|
2553
|
+
auth;
|
|
2554
|
+
/** With auth "missing-permission": the permission the call needed, when known. */
|
|
2555
|
+
permission;
|
|
2358
2556
|
};
|
|
2557
|
+
var PERMISSION_LABELS = {
|
|
2558
|
+
issues: "Issues",
|
|
2559
|
+
pull_requests: "Pull requests",
|
|
2560
|
+
contents: "Contents",
|
|
2561
|
+
checks: "Checks",
|
|
2562
|
+
statuses: "Commit statuses",
|
|
2563
|
+
metadata: "Metadata",
|
|
2564
|
+
actions: "Actions",
|
|
2565
|
+
deployments: "Deployments",
|
|
2566
|
+
workflows: "Workflows",
|
|
2567
|
+
administration: "Administration"
|
|
2568
|
+
};
|
|
2569
|
+
function permissionLabel(permission) {
|
|
2570
|
+
return PERMISSION_LABELS[permission] ?? permission.replace(/_/g, " ").replace(/^./, (c) => c.toUpperCase());
|
|
2571
|
+
}
|
|
2572
|
+
var TOKEN_REJECTED_MESSAGE = "Your GitHub token stopped working. It was revoked, regenerated or has expired: replace it with a new one.";
|
|
2573
|
+
function missingPermissionMessage(permission, action) {
|
|
2574
|
+
const what = action ? `This token cannot ${action}.` : "This token is missing a permission for this.";
|
|
2575
|
+
if (!permission) return `${what} Check its repository permissions on GitHub (Contents and Pull requests need Read and write).`;
|
|
2576
|
+
return `${what} Add ${permissionLabel(permission)}: Read and write on GitHub.`;
|
|
2577
|
+
}
|
|
2578
|
+
function requiredPermission(path, acceptedHeader) {
|
|
2579
|
+
if (acceptedHeader) {
|
|
2580
|
+
const first = acceptedHeader.split(";")[0].split(",").map((p2) => p2.trim().split("=")[0].trim()).filter((p2) => p2 && p2 !== "metadata");
|
|
2581
|
+
if (first.length) return first[0];
|
|
2582
|
+
}
|
|
2583
|
+
const p = path.replace(/[?#].*$/, "");
|
|
2584
|
+
if (/\/repos\/[^/]+\/[^/]+\/(issues|labels|milestones)(\/|$)/.test(p)) return "issues";
|
|
2585
|
+
if (/\/repos\/[^/]+\/[^/]+\/pulls(\/|$)/.test(p)) return "pull_requests";
|
|
2586
|
+
if (/\/repos\/[^/]+\/[^/]+\/(rulesets(\/|$)|branches\/.+\/protection$)/.test(p)) return "administration";
|
|
2587
|
+
if (/\/repos\/[^/]+\/[^/]+\/(contents|git|commits|branches|merges)(\/|$)/.test(p)) return "contents";
|
|
2588
|
+
if (/\/repos\/[^/]+\/[^/]+\/(check-runs|check-suites)(\/|$)/.test(p)) return "checks";
|
|
2589
|
+
if (/\/repos\/[^/]+\/[^/]+\/statuses(\/|$)/.test(p)) return "statuses";
|
|
2590
|
+
if (/\/repos\/[^/]+\/[^/]+\/deployments(\/|$)/.test(p)) return "deployments";
|
|
2591
|
+
if (/\/repos\/[^/]+\/[^/]+\/actions(\/|$)/.test(p)) return "actions";
|
|
2592
|
+
return void 0;
|
|
2593
|
+
}
|
|
2594
|
+
var PERMISSION_DENIED = /not accessible by (personal access token|integration)|must have (admin|push|write) (rights|access)|write access to repository not granted|permission to .* denied/i;
|
|
2595
|
+
function classifyAuth(status, detail, opts = {}) {
|
|
2596
|
+
if (status === 401 && opts.withToken !== false) return { auth: "rejected" };
|
|
2597
|
+
if (status !== 403 || /rate limit|archived/i.test(detail)) return {};
|
|
2598
|
+
if (opts.acceptedPermissions || PERMISSION_DENIED.test(detail)) {
|
|
2599
|
+
return { auth: "missing-permission", permission: requiredPermission(opts.path ?? "", opts.acceptedPermissions) };
|
|
2600
|
+
}
|
|
2601
|
+
return {};
|
|
2602
|
+
}
|
|
2359
2603
|
var DEFAULT_RETRY = { attempts: 3, baseDelayMs: 600, sleep: (ms) => new Promise((r) => setTimeout(r, ms)) };
|
|
2360
2604
|
var isTransient = (status, detail) => status === 0 || status >= 500 || status === 429 || status === 403 && /secondary rate limit/i.test(detail);
|
|
2361
2605
|
var API = "https://api.github.com";
|
|
@@ -2417,7 +2661,7 @@ var GitHubClient = class _GitHubClient {
|
|
|
2417
2661
|
}
|
|
2418
2662
|
}
|
|
2419
2663
|
assertToken(what) {
|
|
2420
|
-
if (!this.token) throw new GitHubError(401, `${what} needs a GitHub token (contents and pull requests, read and write, on this repository only). Without one Bridge is read-only
|
|
2664
|
+
if (!this.token) throw new GitHubError(401, `${what} needs a GitHub token (contents and pull requests, read and write, on this repository only). Without one Bridge is read-only.`, "no-token");
|
|
2421
2665
|
}
|
|
2422
2666
|
get repoPath() {
|
|
2423
2667
|
return `/repos/${this.owner}/${this.name}`;
|
|
@@ -2466,7 +2710,12 @@ var GitHubClient = class _GitHubClient {
|
|
|
2466
2710
|
detail
|
|
2467
2711
|
);
|
|
2468
2712
|
}
|
|
2469
|
-
|
|
2713
|
+
const info = classifyAuth(res.status, detail, {
|
|
2714
|
+
path,
|
|
2715
|
+
acceptedPermissions: res.headers?.get("x-accepted-github-permissions"),
|
|
2716
|
+
withToken: Boolean(this.token)
|
|
2717
|
+
});
|
|
2718
|
+
throw new GitHubError(res.status, friendlyMessage(res.status, detail, info), detail, info);
|
|
2470
2719
|
}
|
|
2471
2720
|
return await res.json();
|
|
2472
2721
|
}
|
|
@@ -2645,6 +2894,11 @@ var GitHubClient = class _GitHubClient {
|
|
|
2645
2894
|
async addComment(issueNumber2, body) {
|
|
2646
2895
|
await this.request("POST", `${this.repoPath}/issues/${issueNumber2}/comments`, { body });
|
|
2647
2896
|
}
|
|
2897
|
+
/** Comments on an issue or pull request, oldest first (first 100: the sticky preview comment is posted early). */
|
|
2898
|
+
async listIssueComments(issueNumber2) {
|
|
2899
|
+
const r = await this.request("GET", `${this.repoPath}/issues/${issueNumber2}/comments?per_page=100`);
|
|
2900
|
+
return r.map((c) => ({ id: c.id, body: c.body ?? "", author: c.user?.login ?? "unknown", updatedAt: c.updated_at }));
|
|
2901
|
+
}
|
|
2648
2902
|
// --- Issues (component change requests) ---------------------------------------------------
|
|
2649
2903
|
async createIssue(input) {
|
|
2650
2904
|
const r = await this.request("POST", `${this.repoPath}/issues`, {
|
|
@@ -2665,6 +2919,67 @@ var GitHubClient = class _GitHubClient {
|
|
|
2665
2919
|
async getIssue(n) {
|
|
2666
2920
|
return toIssue(await this.request("GET", `${this.repoPath}/issues/${n}`));
|
|
2667
2921
|
}
|
|
2922
|
+
/** Edits an issue: its body, or closes it with a reason ("not_planned" for a declined request). */
|
|
2923
|
+
async updateIssue(n, patch) {
|
|
2924
|
+
this.assertToken("Editing an issue");
|
|
2925
|
+
return toIssue(await this.request("PATCH", `${this.repoPath}/issues/${n}`, patch));
|
|
2926
|
+
}
|
|
2927
|
+
/** Adds people to an issue's assignees (GitHub ignores logins that cannot be assigned: the result says who is). */
|
|
2928
|
+
async addAssignees(n, logins) {
|
|
2929
|
+
this.assertToken("Assigning an issue");
|
|
2930
|
+
const r = await this.request("POST", `${this.repoPath}/issues/${n}/assignees`, { assignees: logins });
|
|
2931
|
+
return (r.assignees ?? []).map((a) => a.login);
|
|
2932
|
+
}
|
|
2933
|
+
/** People with at least `permission` on the repository (reading the list needs push access). First 100. */
|
|
2934
|
+
async listCollaborators(permission = "push") {
|
|
2935
|
+
const r = await this.request("GET", `${this.repoPath}/collaborators?permission=${permission}&per_page=100`);
|
|
2936
|
+
return r.map((c) => ({ login: c.login, avatarUrl: c.avatar_url ?? "" }));
|
|
2937
|
+
}
|
|
2938
|
+
/** Whether a token is sent with each request (TeamReader). */
|
|
2939
|
+
get hasToken() {
|
|
2940
|
+
return Boolean(this.token);
|
|
2941
|
+
}
|
|
2942
|
+
/** Every page of a list endpoint (per_page 100), up to `maxPages`. */
|
|
2943
|
+
async paginate(path, maxPages = 20) {
|
|
2944
|
+
const out = [];
|
|
2945
|
+
const sep3 = path.includes("?") ? "&" : "?";
|
|
2946
|
+
for (let page = 1; page <= maxPages; page++) {
|
|
2947
|
+
const r = await this.request("GET", `${path}${sep3}per_page=100&page=${page}`);
|
|
2948
|
+
out.push(...r);
|
|
2949
|
+
if (r.length < 100) break;
|
|
2950
|
+
}
|
|
2951
|
+
return out;
|
|
2952
|
+
}
|
|
2953
|
+
/** Every member login of an organization team (needs read:org, or Members: Read on a fine-grained token). */
|
|
2954
|
+
async listTeamMembers(org, slug) {
|
|
2955
|
+
const r = await this.paginate(`/orgs/${encodeURIComponent(org)}/teams/${encodeURIComponent(slug)}/members`);
|
|
2956
|
+
return r.map((m) => m.login);
|
|
2957
|
+
}
|
|
2958
|
+
/** A user's membership of a team: "active", "pending", or null when GitHub says they are not a member (404). */
|
|
2959
|
+
async getTeamMembership(org, slug, login) {
|
|
2960
|
+
try {
|
|
2961
|
+
const r = await this.request("GET", `/orgs/${encodeURIComponent(org)}/teams/${encodeURIComponent(slug)}/memberships/${encodeURIComponent(login)}`);
|
|
2962
|
+
return r.state === "active" ? "active" : "pending";
|
|
2963
|
+
} catch (err) {
|
|
2964
|
+
if (err instanceof GitHubError && err.status === 404) return null;
|
|
2965
|
+
throw err;
|
|
2966
|
+
}
|
|
2967
|
+
}
|
|
2968
|
+
/** "User" or "Organization" for a login, null when GitHub does not know it. */
|
|
2969
|
+
async getAccountType(login) {
|
|
2970
|
+
try {
|
|
2971
|
+
const r = await this.request("GET", `/users/${encodeURIComponent(login)}`);
|
|
2972
|
+
return r.type === "Organization" ? "Organization" : r.type === "User" ? "User" : null;
|
|
2973
|
+
} catch (err) {
|
|
2974
|
+
if (err instanceof GitHubError && err.status === 404) return null;
|
|
2975
|
+
throw err;
|
|
2976
|
+
}
|
|
2977
|
+
}
|
|
2978
|
+
/** The organization's teams this token can see. */
|
|
2979
|
+
async listOrgTeams(org) {
|
|
2980
|
+
const r = await this.paginate(`/orgs/${encodeURIComponent(org)}/teams`);
|
|
2981
|
+
return r.map((t) => ({ slug: t.slug, name: t.name }));
|
|
2982
|
+
}
|
|
2668
2983
|
/** Create or update a binary file (base64 content) on a branch, e.g. a Figma export attached to a request. */
|
|
2669
2984
|
async putBase64File(input) {
|
|
2670
2985
|
this.assertWritableBranch(input.branch);
|
|
@@ -2800,6 +3115,27 @@ var GitHubClient = class _GitHubClient {
|
|
|
2800
3115
|
const pending = checks.some((c) => c.status !== "completed");
|
|
2801
3116
|
return { state: !checks.length ? "none" : failing ? "failure" : pending ? "pending" : "success", checks };
|
|
2802
3117
|
}
|
|
3118
|
+
/** Commit statuses of a commit, newest first (Vercel reports each project as "Vercel – <project>"). */
|
|
3119
|
+
async listCommitStatuses(sha) {
|
|
3120
|
+
const r = await this.request("GET", `${this.repoPath}/commits/${encodeRef(sha)}/statuses?per_page=100`);
|
|
3121
|
+
return r.map((s) => ({ context: s.context, state: s.state, target_url: s.target_url ?? null, description: s.description ?? null }));
|
|
3122
|
+
}
|
|
3123
|
+
/**
|
|
3124
|
+
* GitHub deployments of a commit with their latest status (environment "Preview – <project>", environment_url the
|
|
3125
|
+
* preview). Latest per environment only, so it costs one read plus one per environment.
|
|
3126
|
+
*/
|
|
3127
|
+
async listDeployments(sha) {
|
|
3128
|
+
const list2 = await this.request("GET", `${this.repoPath}/deployments?sha=${encodeURIComponent(sha)}&per_page=30`);
|
|
3129
|
+
const latest = /* @__PURE__ */ new Map();
|
|
3130
|
+
for (const d of list2) if (!latest.has(d.environment)) latest.set(d.environment, d);
|
|
3131
|
+
return Promise.all(
|
|
3132
|
+
[...latest.values()].map(async (d) => {
|
|
3133
|
+
const st = await this.request("GET", `${this.repoPath}/deployments/${d.id}/statuses?per_page=1`).catch(() => []);
|
|
3134
|
+
const s = st[0];
|
|
3135
|
+
return { id: d.id, environment: d.environment, state: s?.state ?? null, environmentUrl: s?.environment_url || null };
|
|
3136
|
+
})
|
|
3137
|
+
);
|
|
3138
|
+
}
|
|
2803
3139
|
async getPullRequest(n) {
|
|
2804
3140
|
const r = await this.request("GET", `${this.repoPath}/pulls/${n}`);
|
|
2805
3141
|
return { ...toSummary(r), body: r.body ?? "", mergeable: r.mergeable ?? null };
|
|
@@ -2826,7 +3162,44 @@ var GitHubClient = class _GitHubClient {
|
|
|
2826
3162
|
"GET",
|
|
2827
3163
|
`${this.repoPath}/commits/${encodeRef(ref)}/check-runs`
|
|
2828
3164
|
);
|
|
2829
|
-
return r.check_runs.map((c) => ({ id: c.id, name: c.name, status: c.status, conclusion: c.conclusion, url: c.html_url }));
|
|
3165
|
+
return r.check_runs.map((c) => ({ id: c.id, name: c.name, status: c.status, conclusion: c.conclusion, url: c.html_url, ...c.app?.slug ? { app: c.app.slug } : {} }));
|
|
3166
|
+
}
|
|
3167
|
+
/** Rulesets that apply to the repository, without their rules (GET /rulesets; read access is enough). */
|
|
3168
|
+
async listRulesets() {
|
|
3169
|
+
const r = await this.request("GET", `${this.repoPath}/rulesets?includes_parents=true&per_page=100`);
|
|
3170
|
+
return r.map(withRulesetUrl);
|
|
3171
|
+
}
|
|
3172
|
+
/** One ruleset with its rules. */
|
|
3173
|
+
async getRuleset(id) {
|
|
3174
|
+
return withRulesetUrl(await this.request("GET", `${this.repoPath}/rulesets/${id}`));
|
|
3175
|
+
}
|
|
3176
|
+
/** Creates a repository ruleset. Needs admin on the repo (fine-grained token: Administration read and write). */
|
|
3177
|
+
async createRuleset(payload) {
|
|
3178
|
+
this.assertToken("Protecting the library");
|
|
3179
|
+
return withRulesetUrl(await this.request("POST", `${this.repoPath}/rulesets`, payload));
|
|
3180
|
+
}
|
|
3181
|
+
/** Replaces a repository ruleset (same permissions as createRuleset). */
|
|
3182
|
+
async updateRuleset(id, payload) {
|
|
3183
|
+
this.assertToken("Protecting the library");
|
|
3184
|
+
return withRulesetUrl(await this.request("PUT", `${this.repoPath}/rulesets/${id}`, payload));
|
|
3185
|
+
}
|
|
3186
|
+
/**
|
|
3187
|
+
* Classic branch protection of `branch`: the settings, null when the branch has none (404), undefined when this
|
|
3188
|
+
* token cannot read them (403: the endpoint needs admin). On a 403 the public branch summary still tells
|
|
3189
|
+
* whether the branch is protected at all: `protected: false` means there is nothing to read, so null.
|
|
3190
|
+
*/
|
|
3191
|
+
async getBranchProtection(branch) {
|
|
3192
|
+
try {
|
|
3193
|
+
return await this.request("GET", `${this.repoPath}/branches/${encodeRef(branch)}/protection`);
|
|
3194
|
+
} catch (err) {
|
|
3195
|
+
if (!(err instanceof GitHubError)) throw err;
|
|
3196
|
+
if (err.status === 404) return null;
|
|
3197
|
+
if (err.status === 403 || err.status === 401) {
|
|
3198
|
+
const b = await this.request("GET", `${this.repoPath}/branches/${encodeRef(branch)}`).catch(() => void 0);
|
|
3199
|
+
return b?.protected === false ? null : void 0;
|
|
3200
|
+
}
|
|
3201
|
+
throw err;
|
|
3202
|
+
}
|
|
2830
3203
|
}
|
|
2831
3204
|
/** Create a check run (needs a GitHub App token; personal tokens are refused by GitHub). */
|
|
2832
3205
|
async createCheckRun(input) {
|
|
@@ -2868,7 +3241,9 @@ function toSummary(r) {
|
|
|
2868
3241
|
...typeof r.body === "string" ? { body: r.body } : {}
|
|
2869
3242
|
};
|
|
2870
3243
|
}
|
|
2871
|
-
function friendlyMessage(status, detail) {
|
|
3244
|
+
function friendlyMessage(status, detail, info = {}) {
|
|
3245
|
+
if (info.auth === "rejected") return TOKEN_REJECTED_MESSAGE;
|
|
3246
|
+
if (info.auth === "missing-permission") return missingPermissionMessage(info.permission);
|
|
2872
3247
|
switch (status) {
|
|
2873
3248
|
case 401:
|
|
2874
3249
|
return "GitHub rejected the token. It may be expired or mistyped: create a new one and save it in Settings.";
|
|
@@ -2888,6 +3263,10 @@ function friendlyMessage(status, detail) {
|
|
|
2888
3263
|
return `GitHub error ${status}${detail ? `: ${detail}` : ""}`;
|
|
2889
3264
|
}
|
|
2890
3265
|
}
|
|
3266
|
+
function withRulesetUrl(r) {
|
|
3267
|
+
const href = r._links?.html?.href;
|
|
3268
|
+
return href && !r.html_url ? { ...r, html_url: href } : r;
|
|
3269
|
+
}
|
|
2891
3270
|
function toIssue(r) {
|
|
2892
3271
|
return {
|
|
2893
3272
|
number: r.number,
|
|
@@ -2903,6 +3282,122 @@ function toIssue(r) {
|
|
|
2903
3282
|
};
|
|
2904
3283
|
}
|
|
2905
3284
|
|
|
3285
|
+
// ../core/src/protection-github.ts
|
|
3286
|
+
async function loadProtection(client, defaultBranch) {
|
|
3287
|
+
const rulesets = await client.listRulesets().then((list2) => Promise.all(list2.map((r) => r.enforcement === "disabled" || r.id === void 0 ? r : client.getRuleset(r.id)))).catch(() => null);
|
|
3288
|
+
const classic = await client.getBranchProtection(defaultBranch).catch(() => void 0);
|
|
3289
|
+
return protectionState(rulesets, classic, defaultBranch);
|
|
3290
|
+
}
|
|
3291
|
+
var rulesSettingsUrl = (owner, repo) => `https://github.com/${owner}/${repo}/settings/rules`;
|
|
3292
|
+
|
|
3293
|
+
// ../core/src/usage.ts
|
|
3294
|
+
var VAR_RE = /var\(\s*(--[A-Za-z0-9_-]+)/g;
|
|
3295
|
+
var IMPORT_RE = /(?:import|export)\s[^'"]*?from\s*['"]([^'"]+)['"]|import\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
3296
|
+
function cssVarsIn(text) {
|
|
3297
|
+
const out = /* @__PURE__ */ new Set();
|
|
3298
|
+
for (const m of text.matchAll(VAR_RE)) out.add(m[1]);
|
|
3299
|
+
return [...out];
|
|
3300
|
+
}
|
|
3301
|
+
function importsIn(text) {
|
|
3302
|
+
const out = /* @__PURE__ */ new Set();
|
|
3303
|
+
for (const m of text.matchAll(IMPORT_RE)) out.add(m[1] ?? m[2]);
|
|
3304
|
+
return [...out];
|
|
3305
|
+
}
|
|
3306
|
+
var norm2 = (p) => p.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
3307
|
+
var isStory = (p) => /\.stories\.[cm]?[jt]sx?$/.test(p);
|
|
3308
|
+
var isTest = (p) => /\.(test|spec)\.[cm]?[jt]sx?$/.test(p);
|
|
3309
|
+
var isCss = (p) => /\.(css|scss|less)$/.test(p);
|
|
3310
|
+
var isScript = (p) => /\.[cm]?[jt]sx?$/.test(p);
|
|
3311
|
+
function componentOf(from, spec, componentsDir) {
|
|
3312
|
+
if (!spec.startsWith(".")) return null;
|
|
3313
|
+
const parts = from.split("/").slice(0, -1);
|
|
3314
|
+
for (const seg of spec.split("/")) {
|
|
3315
|
+
if (seg === "." || seg === "") continue;
|
|
3316
|
+
if (seg === "..") parts.pop();
|
|
3317
|
+
else parts.push(seg);
|
|
3318
|
+
}
|
|
3319
|
+
const abs = parts.join("/");
|
|
3320
|
+
if (!abs.startsWith(`${componentsDir}/`)) return null;
|
|
3321
|
+
return abs.slice(componentsDir.length + 1).split("/")[0] || null;
|
|
3322
|
+
}
|
|
3323
|
+
function componentForPath(path, componentsDir) {
|
|
3324
|
+
const dir = norm2(componentsDir);
|
|
3325
|
+
const p = norm2(path);
|
|
3326
|
+
if (!p.startsWith(`${dir}/`)) return null;
|
|
3327
|
+
const rest = p.slice(dir.length + 1).split("/");
|
|
3328
|
+
return rest.length >= 2 ? rest[0] : null;
|
|
3329
|
+
}
|
|
3330
|
+
function buildUsageIndex(files, componentsDir) {
|
|
3331
|
+
const dir = norm2(componentsDir);
|
|
3332
|
+
const modules = {};
|
|
3333
|
+
const vars = /* @__PURE__ */ new Map();
|
|
3334
|
+
const imports = /* @__PURE__ */ new Map();
|
|
3335
|
+
const storyImports = /* @__PURE__ */ new Map();
|
|
3336
|
+
const globalVars = {};
|
|
3337
|
+
const get = (m, k) => m.get(k) ?? m.set(k, /* @__PURE__ */ new Set()).get(k);
|
|
3338
|
+
for (const f of files) {
|
|
3339
|
+
const path = norm2(f.path);
|
|
3340
|
+
const id = componentForPath(path, dir);
|
|
3341
|
+
if (!id) {
|
|
3342
|
+
if (isCss(path)) {
|
|
3343
|
+
const v = cssVarsIn(f.text);
|
|
3344
|
+
if (v.length) globalVars[path] = v.sort();
|
|
3345
|
+
}
|
|
3346
|
+
continue;
|
|
3347
|
+
}
|
|
3348
|
+
const mod = modules[id] ??= { id, dir: `${dir}/${id}`, files: [], vars: [], imports: [], storyImports: [] };
|
|
3349
|
+
mod.files.push(path);
|
|
3350
|
+
if (isTest(path)) continue;
|
|
3351
|
+
for (const v of cssVarsIn(f.text)) get(vars, id).add(v);
|
|
3352
|
+
if (!isScript(path)) continue;
|
|
3353
|
+
for (const spec of importsIn(f.text)) {
|
|
3354
|
+
const dep = componentOf(path, spec, dir);
|
|
3355
|
+
if (!dep || dep === id) continue;
|
|
3356
|
+
get(isStory(path) ? storyImports : imports, id).add(dep);
|
|
3357
|
+
}
|
|
3358
|
+
}
|
|
3359
|
+
const byVar = {};
|
|
3360
|
+
for (const mod of Object.values(modules)) {
|
|
3361
|
+
mod.files.sort();
|
|
3362
|
+
mod.vars = [...vars.get(mod.id) ?? []].sort();
|
|
3363
|
+
mod.imports = [...imports.get(mod.id) ?? []].sort();
|
|
3364
|
+
mod.storyImports = [...storyImports.get(mod.id) ?? []].filter((d) => !mod.imports.includes(d)).sort();
|
|
3365
|
+
for (const v of mod.vars) (byVar[v] ??= []).push(mod.id);
|
|
3366
|
+
}
|
|
3367
|
+
for (const k of Object.keys(byVar)) byVar[k].sort();
|
|
3368
|
+
return { componentsDir: dir, modules, byVar, globalVars };
|
|
3369
|
+
}
|
|
3370
|
+
function impactedModules(index, input) {
|
|
3371
|
+
const reasons = /* @__PURE__ */ new Map();
|
|
3372
|
+
const add = (id, reason, indirect) => {
|
|
3373
|
+
const r = reasons.get(id);
|
|
3374
|
+
if (r) {
|
|
3375
|
+
if (!r.reasons.includes(reason)) r.reasons.push(reason);
|
|
3376
|
+
r.indirect &&= indirect;
|
|
3377
|
+
} else reasons.set(id, { reasons: [reason], indirect });
|
|
3378
|
+
};
|
|
3379
|
+
const changedVars = new Set(input.vars ?? []);
|
|
3380
|
+
for (const v of changedVars) for (const id of index.byVar[v] ?? []) add(id, `uses ${v}`, false);
|
|
3381
|
+
for (const f of input.files ?? []) {
|
|
3382
|
+
const id = componentForPath(f, index.componentsDir);
|
|
3383
|
+
if (id && index.modules[id]) add(id, `file changed: ${norm2(f)}`, false);
|
|
3384
|
+
}
|
|
3385
|
+
const renderedBy = /* @__PURE__ */ new Map();
|
|
3386
|
+
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);
|
|
3387
|
+
const queue = [...reasons.keys()];
|
|
3388
|
+
while (queue.length) {
|
|
3389
|
+
const id = queue.shift();
|
|
3390
|
+
for (const parent of renderedBy.get(id) ?? []) {
|
|
3391
|
+
const known = reasons.has(parent);
|
|
3392
|
+
add(parent, `renders ${id}`, true);
|
|
3393
|
+
if (!known) queue.push(parent);
|
|
3394
|
+
}
|
|
3395
|
+
}
|
|
3396
|
+
const globalHits = Object.entries(index.globalVars).map(([file, vs]) => ({ file, vars: vs.filter((v) => changedVars.has(v)) })).filter((g) => g.vars.length > 0);
|
|
3397
|
+
const modules = [...reasons.entries()].map(([id, r]) => ({ id, reasons: r.reasons, indirect: r.indirect })).sort((a, b) => Number(a.indirect) - Number(b.indirect) || a.id.localeCompare(b.id));
|
|
3398
|
+
return { modules, globalHits };
|
|
3399
|
+
}
|
|
3400
|
+
|
|
2906
3401
|
// ../core/src/explorations.ts
|
|
2907
3402
|
var EXPLORATION_USE_CASES = [
|
|
2908
3403
|
{ value: "user-test", label: "Prototype for a user test", hint: "A coded prototype on a product staging branch, with the real components and tokens." },
|
|
@@ -2917,7 +3412,8 @@ var EXPLORATION_STATUS_LABEL = { active: "Running", promoted: "Promoted to revie
|
|
|
2917
3412
|
var REQUEST_KINDS = [
|
|
2918
3413
|
{ value: "new-component", label: "New component", hint: "A component the design system does not have yet." },
|
|
2919
3414
|
{ value: "new-variant", label: "New variant", hint: "A size, tone or state the component does not have yet." },
|
|
2920
|
-
{ value: "prop-change", label: "
|
|
3415
|
+
{ value: "prop-change", label: "Option change (prop)", hint: "Add, rename or change an option (a prop in code) or its default." },
|
|
3416
|
+
{ value: "structure", label: "Structure change", hint: "A part was added, removed or moved." },
|
|
2921
3417
|
{ value: "visual", label: "Visual change", hint: "Spacing, radius, layout or icon placement." },
|
|
2922
3418
|
{ value: "token-binding", label: "Token change", hint: "The component should paint with another token." },
|
|
2923
3419
|
{ value: "bug", label: "Bug", hint: "It looks or behaves differently from the guidance." },
|
|
@@ -2949,6 +3445,7 @@ function jiraIssueFields(r, jira, issueUrl) {
|
|
|
2949
3445
|
version: 1,
|
|
2950
3446
|
content: [
|
|
2951
3447
|
para(text(r.summary.trim() || r.title)),
|
|
3448
|
+
...r.line?.trim() ? [para(text(r.line.trim()))] : [],
|
|
2952
3449
|
...r.changes.map((c) => para(text(`${c.field}: ${c.before ?? "(none)"} -> ${c.after ?? "(removed)"}`))),
|
|
2953
3450
|
para({ type: "text", text: "GitHub request", marks: [{ type: "link", attrs: { href: issueUrl } }] })
|
|
2954
3451
|
]
|
|
@@ -3213,14 +3710,14 @@ function union(a, b) {
|
|
|
3213
3710
|
// ../visual/src/manifest.ts
|
|
3214
3711
|
var COMMENT_MARKER = "<!-- bridge-visual-impact -->";
|
|
3215
3712
|
function summarize(shots) {
|
|
3216
|
-
const
|
|
3713
|
+
const count2 = (s) => shots.filter((x) => x.status === s).length;
|
|
3217
3714
|
return {
|
|
3218
3715
|
shots: shots.length,
|
|
3219
|
-
changed:
|
|
3220
|
-
unchanged:
|
|
3221
|
-
added:
|
|
3222
|
-
removed:
|
|
3223
|
-
errors:
|
|
3716
|
+
changed: count2("changed"),
|
|
3717
|
+
unchanged: count2("unchanged"),
|
|
3718
|
+
added: count2("added"),
|
|
3719
|
+
removed: count2("removed"),
|
|
3720
|
+
errors: count2("error"),
|
|
3224
3721
|
maxPercent: shots.reduce((m, s) => Math.max(m, s.percent), 0)
|
|
3225
3722
|
};
|
|
3226
3723
|
}
|
|
@@ -3356,17 +3853,17 @@ var shotKey = (...parts) => parts.map((p) => p.toLowerCase().replace(/[^a-z0-9-]
|
|
|
3356
3853
|
|
|
3357
3854
|
// ../visual/src/select.ts
|
|
3358
3855
|
var NO_VISUAL = /(\.test\.[cm]?[jt]sx?|\.spec\.[cm]?[jt]sx?|\.docs\.json|\.mdx?|README[^/]*)$/i;
|
|
3359
|
-
var
|
|
3856
|
+
var norm3 = (p) => p.replace(/^\.\//, "").replace(/\\/g, "/");
|
|
3360
3857
|
var isAllStates = (s) => /--all-states$/.test(s.id) || /^all ?states$/i.test(s.name);
|
|
3361
3858
|
function storyComponent(s, componentsDir) {
|
|
3362
|
-
return componentForPath(
|
|
3859
|
+
return componentForPath(norm3(s.importPath), componentsDir);
|
|
3363
3860
|
}
|
|
3364
3861
|
function selectImpact(input) {
|
|
3365
3862
|
const { usage, stories } = input;
|
|
3366
3863
|
const dir = usage.componentsDir;
|
|
3367
3864
|
const lib = input.librarySrc.replace(/\/$/, "");
|
|
3368
3865
|
const cap = input.maxStoriesPerComponent ?? 4;
|
|
3369
|
-
const visualFiles = input.files.map(
|
|
3866
|
+
const visualFiles = input.files.map(norm3).filter((f) => !NO_VISUAL.test(f));
|
|
3370
3867
|
const vars = [...new Set(input.tokens.map((t) => input.cssVar(t.path)))].sort();
|
|
3371
3868
|
const impact2 = impactedModules(usage, { vars, files: visualFiles });
|
|
3372
3869
|
let fallback = null;
|
|
@@ -3406,7 +3903,7 @@ function selectImpact(input) {
|
|
|
3406
3903
|
return {
|
|
3407
3904
|
tokens: input.tokens,
|
|
3408
3905
|
vars,
|
|
3409
|
-
files: input.files.map(
|
|
3906
|
+
files: input.files.map(norm3),
|
|
3410
3907
|
components: components.map((c) => ({ id: c.id, reasons: c.reasons, indirect: c.indirect })),
|
|
3411
3908
|
stories: [...new Set(picked)],
|
|
3412
3909
|
fallback,
|
|
@@ -3425,7 +3922,7 @@ function storiesFromSource(files) {
|
|
|
3425
3922
|
if (!title) continue;
|
|
3426
3923
|
for (const m of f.text.matchAll(/export\s+const\s+([A-Z]\w*)\s*[:=]/g)) {
|
|
3427
3924
|
const name = m[1];
|
|
3428
|
-
out.push({ id: storyId(title, name), title, name: name.replace(/([a-z0-9])([A-Z])/g, "$1 $2"), importPath: `./${
|
|
3925
|
+
out.push({ id: storyId(title, name), title, name: name.replace(/([a-z0-9])([A-Z])/g, "$1 $2"), importPath: `./${norm3(f.path)}` });
|
|
3429
3926
|
}
|
|
3430
3927
|
}
|
|
3431
3928
|
return out;
|
|
@@ -4232,8 +4729,9 @@ Usage:
|
|
|
4232
4729
|
GitHub token: the branch and the draft pull request on GitHub. Without one, in a git repo:
|
|
4233
4730
|
a local branch only, nothing pushed
|
|
4234
4731
|
bridge explore list [--all] [--json]
|
|
4235
|
-
running and promoted explorations, with
|
|
4236
|
-
|
|
4732
|
+
running and promoted explorations, with the preview links Vercel reports, the install
|
|
4733
|
+
command from the pull request (once the snapshot is built) and the requests implemented
|
|
4734
|
+
on each (--all: closed ones too)
|
|
4237
4735
|
bridge explore promote <name|#pr> [--dry-run] [--json]
|
|
4238
4736
|
ready for review, tier labels, exploration label and note removed (never merges)
|
|
4239
4737
|
bridge explore close <name|#pr> [--json]
|
|
@@ -4279,8 +4777,7 @@ ${r.prBody}`);
|
|
|
4279
4777
|
if (r.status === "started") {
|
|
4280
4778
|
io.print(`Started ${r.branch}: draft pull request #${r.pr} ${r.url}`);
|
|
4281
4779
|
io.print(`
|
|
4282
|
-
|
|
4283
|
-
${r.install.command}`);
|
|
4780
|
+
${r.install.note}`);
|
|
4284
4781
|
for (const p of r.previews ?? []) io.print(`${p.label}: ${p.url}`);
|
|
4285
4782
|
} else io.print(`Created the local branch ${r.branch} (${String(r.commit).slice(0, 7)}).`);
|
|
4286
4783
|
io.print(`
|
|
@@ -4297,8 +4794,8 @@ Next: ${r.next}`);
|
|
|
4297
4794
|
#${x.number} ${x.name} ${EXPLORATION_STATUS_LABEL[x.status]} ${x.branch} @${x.author} ${date(x.updatedAt)}`);
|
|
4298
4795
|
if (x.purpose) io.print(` ${x.purpose}`);
|
|
4299
4796
|
io.print(` ${x.url}`);
|
|
4300
|
-
for (const p of x.previews) io.print(` ${p.label}: ${p.url}`);
|
|
4301
|
-
if (x.install
|
|
4797
|
+
for (const p of x.previews) io.print(` ${p.label}: ${p.url ? `${p.url}${p.state === "building" ? " (building)" : p.state === "failed" ? " (build failed)" : ""}` : "link not reported by Vercel yet, see the pull request"}`);
|
|
4798
|
+
if (x.install) io.print(x.install.command ? ` Install: ${x.install.command}` : ` Install: ${x.install.note}`);
|
|
4302
4799
|
if (x.requests.length) io.print(` Requests: ${x.requests.map((q) => `#${q.number}`).join(", ")}`);
|
|
4303
4800
|
}
|
|
4304
4801
|
for (const b of r.localBranches ?? []) io.print(`
|
|
@@ -4335,13 +4832,49 @@ ${EXPLORE_HELP}`);
|
|
|
4335
4832
|
}
|
|
4336
4833
|
}
|
|
4337
4834
|
|
|
4835
|
+
// src/protection.ts
|
|
4836
|
+
var UNKNOWN = { kind: "unknown", missing: ["review", "checks", "history", "deletion"] };
|
|
4837
|
+
async function protectionReport(opts) {
|
|
4838
|
+
let state = UNKNOWN;
|
|
4839
|
+
try {
|
|
4840
|
+
const client = new GitHubClient(opts.repo, opts.token ?? "", opts.fetch, { attempts: 1, baseDelayMs: 0, sleep: async () => {
|
|
4841
|
+
} });
|
|
4842
|
+
state = await loadProtection(client, opts.defaultBranch);
|
|
4843
|
+
} catch {
|
|
4844
|
+
state = UNKNOWN;
|
|
4845
|
+
}
|
|
4846
|
+
const [owner, name] = opts.repo.split("/");
|
|
4847
|
+
const lines = [`protection: ${protectionSentence(state)}`];
|
|
4848
|
+
if (state.kind === "protected" && state.url) lines.push(` ${state.url}`);
|
|
4849
|
+
if (state.kind === "unprotected" || state.kind === "partial") lines.push(` Protect it in Studio > Settings, or GitHub settings > Rules: ${rulesSettingsUrl(owner, name)}`);
|
|
4850
|
+
return { state, lines };
|
|
4851
|
+
}
|
|
4852
|
+
|
|
4853
|
+
// src/roles.ts
|
|
4854
|
+
async function rolesReport(opts) {
|
|
4855
|
+
if (!teamsIn(opts.owners).length) return { teams: [], lines: [] };
|
|
4856
|
+
const client = new GitHubClient(opts.repo, opts.token ?? "", opts.fetch, { attempts: 1, baseDelayMs: 0, sleep: async () => {
|
|
4857
|
+
} });
|
|
4858
|
+
const states = await new TeamDirectory(client).resolve(opts.owners);
|
|
4859
|
+
const teams = Object.values(states);
|
|
4860
|
+
const group = (entries) => entries.length ? entries.map((e) => entryLabel(e, states)).join(", ") : "nobody listed";
|
|
4861
|
+
const lines = [`roles: design owners ${group(opts.owners.design)}; tech owners ${group(opts.owners.tech)}`];
|
|
4862
|
+
for (const t of teams) if (t.status === "unreadable") lines.push(` warn team-unreadable ${t.message}`);
|
|
4863
|
+
return { teams, lines };
|
|
4864
|
+
}
|
|
4865
|
+
|
|
4338
4866
|
// src/index.ts
|
|
4339
4867
|
import { brand, BridgeContext as BridgeContext4, CONFIG_FILE as CONFIG_FILE2, core, detectConfig, GitRefSource, history as hist, LocalSource, ops, optionsFromArgv, serveStdio, toConfigJson, VERSION } from "@bridge-ds/mcp";
|
|
4340
4868
|
var HELP = `bridge ${VERSION}: design system as code, for you and your Claude
|
|
4341
4869
|
|
|
4342
4870
|
Usage:
|
|
4343
4871
|
bridge init [--force] write bridge.config.json (detected) and .mcp.json for Claude Code
|
|
4344
|
-
bridge check [--json] [--strict]
|
|
4872
|
+
bridge check [--json] [--strict] [--offline]
|
|
4873
|
+
validate tokens + contrast gate, and say whether the library is
|
|
4874
|
+
protected (reviewed pull requests only) and who is in the GitHub
|
|
4875
|
+
teams named as owners, reporting teams it cannot read (neither
|
|
4876
|
+
changes the exit code; --offline skips both). Exit 0 ok, 1 failures,
|
|
4877
|
+
2 setup error
|
|
4345
4878
|
bridge tokens <query> [--json] search tokens by name, description or value
|
|
4346
4879
|
bridge diff <base> [--json|--md] designer-readable token diff: working tree vs <base> (git ref)
|
|
4347
4880
|
bridge propose --title "..." --set path=value [--set ...] [--file f | --mode theme=dark]
|
|
@@ -4408,7 +4941,7 @@ function parse(argv) {
|
|
|
4408
4941
|
const [k, inline] = a.slice(2).split(/=(.*)/s, 2);
|
|
4409
4942
|
const next = argv[i + 1];
|
|
4410
4943
|
const fileArg = k === "json" && /\.json$/i.test(next ?? "") || k === "md" && /\.(md|markdown)$/i.test(next ?? "");
|
|
4411
|
-
const takesValue = inline === void 0 && next !== void 0 && !next.startsWith("--") && (fileArg || !["force", "json", "md", "strict", "dry-run", "help", "fix", "write", "no-regress", "plan", "keep-unchanged", "restore-conflicts", "breaking", "claude", "all"].includes(k));
|
|
4944
|
+
const takesValue = inline === void 0 && next !== void 0 && !next.startsWith("--") && (fileArg || !["force", "json", "md", "strict", "dry-run", "help", "fix", "write", "no-regress", "plan", "keep-unchanged", "restore-conflicts", "breaking", "claude", "all", "offline"].includes(k));
|
|
4412
4945
|
(out.flags[k] ??= []).push(inline ?? (takesValue ? argv[++i] : "true"));
|
|
4413
4946
|
} else out._.push(a);
|
|
4414
4947
|
}
|
|
@@ -4456,8 +4989,10 @@ async function check(a, root) {
|
|
|
4456
4989
|
const c = await ops.checkContrast(ctx, {});
|
|
4457
4990
|
const warnings = "warnings" in v ? v.warnings ?? [] : [];
|
|
4458
4991
|
const failed = !v.valid || "failing" in c && (c.failing ?? 0) > 0 || has3(a, "strict") && warnings.length > 0;
|
|
4992
|
+
const protection = await protectionOf(a, root, ctx);
|
|
4993
|
+
const roles = await rolesOf(a, root, ctx);
|
|
4459
4994
|
if (has3(a, "json")) {
|
|
4460
|
-
print(JSON.stringify({ ok: !failed, validation: v, contrast: c }, null, 2));
|
|
4995
|
+
print(JSON.stringify({ ok: !failed, validation: v, contrast: c, ...protection ? { protection: protection.state } : {}, ...roles?.teams.length ? { teams: roles.teams } : {} }, null, 2));
|
|
4461
4996
|
return failed ? 1 : 0;
|
|
4462
4997
|
}
|
|
4463
4998
|
print(`tokens: ${v.tokens} in ${v.files} files, ${v.modeCombinations} mode combinations`);
|
|
@@ -4469,9 +5004,27 @@ async function check(a, root) {
|
|
|
4469
5004
|
const tight = c.tightest?.[0];
|
|
4470
5005
|
if (tight) print(` tightest: ${tight.pair} [${tight.modes}] ${tight.ratio}:1 (min ${tight.min})`);
|
|
4471
5006
|
} else print(`contrast: ${c.message}`);
|
|
5007
|
+
for (const line of protection?.lines ?? []) print(line);
|
|
5008
|
+
for (const line of roles?.lines ?? []) print(line);
|
|
4472
5009
|
print(failed ? "FAIL" : "OK");
|
|
4473
5010
|
return failed ? 1 : 0;
|
|
4474
5011
|
}
|
|
5012
|
+
async function protectionOf(a, root, ctx) {
|
|
5013
|
+
if (has3(a, "offline") || process.env.BRIDGE_OFFLINE === "1") return void 0;
|
|
5014
|
+
const { config } = await ctx.config();
|
|
5015
|
+
const repo = config.repo ? `${config.repo.owner}/${config.repo.name}` : await originRepo(root);
|
|
5016
|
+
if (!repo) return void 0;
|
|
5017
|
+
const token = process.env.BRIDGE_GITHUB_TOKEN || process.env.GITHUB_TOKEN || void 0;
|
|
5018
|
+
return protectionReport({ repo, defaultBranch: config.repo?.defaultBranch ?? "main", token });
|
|
5019
|
+
}
|
|
5020
|
+
async function rolesOf(a, root, ctx) {
|
|
5021
|
+
if (has3(a, "offline") || process.env.BRIDGE_OFFLINE === "1") return void 0;
|
|
5022
|
+
const { config } = await ctx.config();
|
|
5023
|
+
const repo = config.repo ? `${config.repo.owner}/${config.repo.name}` : await originRepo(root);
|
|
5024
|
+
if (!repo) return void 0;
|
|
5025
|
+
const token = process.env.BRIDGE_GITHUB_TOKEN || process.env.GITHUB_TOKEN || void 0;
|
|
5026
|
+
return rolesReport({ repo, owners: config.governance.owners, token });
|
|
5027
|
+
}
|
|
4475
5028
|
async function tokens(a, root) {
|
|
4476
5029
|
const q = a._.slice(1).join(" ");
|
|
4477
5030
|
if (!q) throw new UsageError("bridge tokens <query>");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bridge-ds/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "bridge CLI: set up Bridge in a design system repo, check tokens in CI, search and diff tokens, propose changes, start the MCP server.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -43,8 +43,8 @@
|
|
|
43
43
|
"build:package": "node ../../scripts/build-packages.mjs cli"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"@bridge-ds/mcp": "^0.
|
|
47
|
-
"@bridge-ds/visual": "^0.2.
|
|
46
|
+
"@bridge-ds/mcp": "^0.3.0",
|
|
47
|
+
"@bridge-ds/visual": "^0.2.1",
|
|
48
48
|
"typescript": "5.9.3"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|