@bridge-ds/mcp 0.8.0 → 0.8.2
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.
|
@@ -596,7 +596,7 @@ var DEFAULT_GOVERNANCE = {
|
|
|
596
596
|
owners: { design: [], tech: [] }
|
|
597
597
|
};
|
|
598
598
|
var FLAT_TOKEN_KEYS = ["primitives", "brands", "semantic", "base", "density", "components", "contrastChecks"];
|
|
599
|
-
var CANONICAL_TOKEN_KEYS = ["root", "include", "exclude", "tiers", "layers"];
|
|
599
|
+
var CANONICAL_TOKEN_KEYS = ["root", "include", "exclude", "tiers", "layers", "usage"];
|
|
600
600
|
var FLAT_MODE_KEYS = ["brands", "themes", "densities", "defaults"];
|
|
601
601
|
var AXIS_OF = { brands: "brand", themes: "theme", densities: "density" };
|
|
602
602
|
var ConfigError = class extends Error {
|
|
@@ -684,7 +684,7 @@ function defineConfig(input) {
|
|
|
684
684
|
const contrastFile = contrastPath ?? [root === "." ? "" : root, paths.contrastChecks].filter(Boolean).join("/");
|
|
685
685
|
return {
|
|
686
686
|
repo: parseRepoInput(input.repo, input.branch),
|
|
687
|
-
tokens: { ...paths, include: input.tokens?.include ?? ["**/*.tokens.json"], exclude: input.tokens?.exclude ?? [], tiers, layers },
|
|
687
|
+
tokens: { ...paths, include: input.tokens?.include ?? ["**/*.tokens.json"], exclude: input.tokens?.exclude ?? [], tiers, layers, ...t.usage ? { usage: trimSlashes(t.usage) } : {} },
|
|
688
688
|
axes,
|
|
689
689
|
modes,
|
|
690
690
|
contrast: { path: contrastFile, modes: input.contrast?.modes },
|
|
@@ -719,6 +719,7 @@ function defineConfig(input) {
|
|
|
719
719
|
products: input.products?.length ? input.products.map((p) => ({ ...p })) : DEFAULT_PRODUCTS.map((p) => ({ ...p })),
|
|
720
720
|
changes: input.changes === "direct" ? "direct" : DEFAULT_CHANGES_MODE,
|
|
721
721
|
...input.storybook?.url ? { storybook: { url: input.storybook.url.trim().replace(/\/+$/, "") } } : {},
|
|
722
|
+
...input.library?.public === true ? { library: { public: true } } : {},
|
|
722
723
|
...input.audit ? { audit: { ...input.audit } } : {},
|
|
723
724
|
...input.visualImpact ? { visualImpact: { mode: input.visualImpact.mode === "on-demand" ? "on-demand" : "auto" } } : {},
|
|
724
725
|
...input.integrations ? { integrations: { ...input.integrations } } : {},
|
|
@@ -763,7 +764,7 @@ function loadConfig(source) {
|
|
|
763
764
|
if (!isObj(tokens)) problems.push('"tokens" must be an object');
|
|
764
765
|
else
|
|
765
766
|
for (const [k, v] of Object.entries(tokens)) {
|
|
766
|
-
if (FLAT_TOKEN_KEYS.includes(k) || k === "root") {
|
|
767
|
+
if (FLAT_TOKEN_KEYS.includes(k) || k === "root" || k === "usage") {
|
|
767
768
|
if (!isStr(v)) problems.push(`"tokens.${k}" must be a non-empty string`);
|
|
768
769
|
} else if (k === "include" || k === "exclude") {
|
|
769
770
|
if (!isStrList(v)) problems.push(`"tokens.${k}" must be a list of globs`);
|
|
@@ -849,6 +850,14 @@ function loadConfig(source) {
|
|
|
849
850
|
else if (!/^https:\/\/[^\s/]+/.test(sb.url.trim())) problems.push('"storybook.url" must start with https://, for example https://storybook.example.com');
|
|
850
851
|
else for (const k of Object.keys(sb)) if (k !== "url") problems.push(`"storybook.${k}" is not a known key`);
|
|
851
852
|
}
|
|
853
|
+
const lib = d.library;
|
|
854
|
+
if (lib !== void 0) {
|
|
855
|
+
if (!isObj(lib)) problems.push('"library" must be an object, for example { "public": true }');
|
|
856
|
+
else {
|
|
857
|
+
if (lib.public !== void 0 && typeof lib.public !== "boolean") problems.push('"library.public" must be true or false');
|
|
858
|
+
for (const k of Object.keys(lib)) if (k !== "public") problems.push(`"library.${k}" is not a known key`);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
852
861
|
problems.push(...productProblems(d.products));
|
|
853
862
|
const audit = d.audit;
|
|
854
863
|
if (audit !== void 0) {
|
|
@@ -887,7 +896,7 @@ function toConfigJson(config, schema = "./packages/core/bridge.config.schema.jso
|
|
|
887
896
|
$schema: schema,
|
|
888
897
|
version: 1,
|
|
889
898
|
...config.repo ? { repo: config.repo } : {},
|
|
890
|
-
tokens: { root: config.tokens.root, include: config.tokens.include, ...config.tokens.exclude.length ? { exclude: config.tokens.exclude } : {}, tiers: config.tokens.tiers, layers: config.tokens.layers.map((l) => l.optional ? l : { files: l.files }) },
|
|
899
|
+
tokens: { root: config.tokens.root, include: config.tokens.include, ...config.tokens.exclude.length ? { exclude: config.tokens.exclude } : {}, tiers: config.tokens.tiers, layers: config.tokens.layers.map((l) => l.optional ? l : { files: l.files }), ...config.tokens.usage ? { usage: config.tokens.usage } : {} },
|
|
891
900
|
modes: Object.fromEntries(Object.entries(config.axes).map(([k, a]) => [k, a.parity ? a : { values: a.values, default: a.default }])),
|
|
892
901
|
...config.contrast ? { contrast: config.contrast.modes ? config.contrast : { path: config.contrast.path } } : {},
|
|
893
902
|
...config.components ? { components: Object.fromEntries(Object.entries(config.components).filter(([, v]) => v !== void 0)) } : {},
|
|
@@ -900,6 +909,7 @@ function toConfigJson(config, schema = "./packages/core/bridge.config.schema.jso
|
|
|
900
909
|
...config.visualImpact ? { visualImpact: config.visualImpact } : {},
|
|
901
910
|
changes: changesMode(config),
|
|
902
911
|
...config.storybook ? { storybook: config.storybook } : {},
|
|
912
|
+
...config.library ? { library: config.library } : {},
|
|
903
913
|
...config.products && !sameProducts(config.products, DEFAULT_PRODUCTS) ? { products: config.products } : {}
|
|
904
914
|
};
|
|
905
915
|
}
|
|
@@ -79,7 +79,7 @@ import {
|
|
|
79
79
|
validateLayered,
|
|
80
80
|
variantKeyOf,
|
|
81
81
|
whoCanShipSentence
|
|
82
|
-
} from "./chunk-
|
|
82
|
+
} from "./chunk-DPK46GA2.js";
|
|
83
83
|
|
|
84
84
|
// src/lib.ts
|
|
85
85
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
@@ -1263,6 +1263,44 @@ function classifyAuth(status, detail, opts = {}) {
|
|
|
1263
1263
|
}
|
|
1264
1264
|
return {};
|
|
1265
1265
|
}
|
|
1266
|
+
function memoryEtagStore(max = 500) {
|
|
1267
|
+
const map = /* @__PURE__ */ new Map();
|
|
1268
|
+
return {
|
|
1269
|
+
get(key) {
|
|
1270
|
+
const e = map.get(key);
|
|
1271
|
+
if (e) {
|
|
1272
|
+
map.delete(key);
|
|
1273
|
+
map.set(key, e);
|
|
1274
|
+
}
|
|
1275
|
+
return e;
|
|
1276
|
+
},
|
|
1277
|
+
set(key, entry) {
|
|
1278
|
+
map.delete(key);
|
|
1279
|
+
map.set(key, entry);
|
|
1280
|
+
while (map.size > max) map.delete(map.keys().next().value);
|
|
1281
|
+
},
|
|
1282
|
+
clear: () => map.clear()
|
|
1283
|
+
};
|
|
1284
|
+
}
|
|
1285
|
+
var sharedEtags = memoryEtagStore();
|
|
1286
|
+
function tokenFingerprint(token) {
|
|
1287
|
+
if (!token) return "anon";
|
|
1288
|
+
let a = 2166136261;
|
|
1289
|
+
let b = 16777619 ^ token.length;
|
|
1290
|
+
for (let i = 0; i < token.length; i++) {
|
|
1291
|
+
const c = token.charCodeAt(i);
|
|
1292
|
+
a = Math.imul(a ^ c, 16777619) >>> 0;
|
|
1293
|
+
b = Math.imul(b ^ c ^ i & 255, 1540483477) >>> 0;
|
|
1294
|
+
}
|
|
1295
|
+
return a.toString(16).padStart(8, "0") + b.toString(16).padStart(8, "0");
|
|
1296
|
+
}
|
|
1297
|
+
function installationFor(list, owner) {
|
|
1298
|
+
const inst = list.find((i) => i.account?.login?.toLowerCase() === owner.toLowerCase());
|
|
1299
|
+
if (!inst) return { reach: "no" };
|
|
1300
|
+
const selection = inst.repositorySelection ?? inst.repository_selection;
|
|
1301
|
+
return selection === "all" ? { reach: "yes", id: inst.id } : { reach: "check", id: inst.id };
|
|
1302
|
+
}
|
|
1303
|
+
var MAX_PAGES = 10;
|
|
1266
1304
|
function rateLimitMessage(x) {
|
|
1267
1305
|
const when = x.resetsAt ? ` It resets at ${x.resetsAt.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}.` : "";
|
|
1268
1306
|
if (x.withToken) return `GitHub's hourly request limit for this token is used up.${when} Try again after that.`;
|
|
@@ -1270,8 +1308,11 @@ function rateLimitMessage(x) {
|
|
|
1270
1308
|
return `GitHub's hourly limit for requests without a token (60 an hour) is used up.${when} To keep going now, ${hint}: a token gets 5,000 requests an hour.`;
|
|
1271
1309
|
}
|
|
1272
1310
|
var DEFAULT_RETRY = { attempts: 3, baseDelayMs: 600, sleep: (ms) => new Promise((r) => setTimeout(r, ms)) };
|
|
1311
|
+
var hourlyLimitErrors = /* @__PURE__ */ new WeakSet();
|
|
1273
1312
|
var isTransient = (status, detail) => status === 0 || status >= 500 || status === 429 || status === 403 && /secondary rate limit/i.test(detail);
|
|
1274
1313
|
var API = "https://api.github.com";
|
|
1314
|
+
var ACCEPT_JSON = "application/vnd.github+json";
|
|
1315
|
+
var ACCEPT_RAW = "application/vnd.github.raw";
|
|
1275
1316
|
var RAW = "https://raw.githubusercontent.com";
|
|
1276
1317
|
function parseRepo(repo) {
|
|
1277
1318
|
const m = /^\s*(?:https?:\/\/github\.com\/|git@github\.com:)?([A-Za-z0-9-]+)\/([A-Za-z0-9._-]+?)(?:\.git)?\/?\s*$/.exec(repo);
|
|
@@ -1290,6 +1331,7 @@ function base64ToUtf8(b64) {
|
|
|
1290
1331
|
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
|
|
1291
1332
|
return new TextDecoder("utf-8").decode(bytes);
|
|
1292
1333
|
}
|
|
1334
|
+
var copy = (value) => value === null || typeof value !== "object" ? value : typeof structuredClone === "function" ? structuredClone(value) : JSON.parse(JSON.stringify(value));
|
|
1293
1335
|
var encodeRef = (ref) => ref.split("/").map(encodeURIComponent).join("/");
|
|
1294
1336
|
var GitHubClient = class _GitHubClient {
|
|
1295
1337
|
constructor(repo, token, fetchImpl = (url, init) => fetch(url, init), retry = DEFAULT_RETRY, options = {}) {
|
|
@@ -1302,6 +1344,8 @@ var GitHubClient = class _GitHubClient {
|
|
|
1302
1344
|
this.name = r.name;
|
|
1303
1345
|
this.api = (options.apiUrl ?? API).replace(/\/$/, "");
|
|
1304
1346
|
this.protectedBranches = options.protectedBranches ?? DEFAULT_PROTECTED_BRANCHES;
|
|
1347
|
+
this.etags = options.etags === false ? void 0 : options.etags ?? sharedEtags;
|
|
1348
|
+
this.etagScope = options.etagScope ?? tokenFingerprint(token);
|
|
1305
1349
|
}
|
|
1306
1350
|
token;
|
|
1307
1351
|
fetchImpl;
|
|
@@ -1312,6 +1356,15 @@ var GitHubClient = class _GitHubClient {
|
|
|
1312
1356
|
blobCache = /* @__PURE__ */ new Map();
|
|
1313
1357
|
api;
|
|
1314
1358
|
protectedBranches;
|
|
1359
|
+
etags;
|
|
1360
|
+
etagScope;
|
|
1361
|
+
rate;
|
|
1362
|
+
/** What the conditional request cache did for this client. */
|
|
1363
|
+
etagStats = { conditional: 0, notModified: 0, stale: 0 };
|
|
1364
|
+
/** GitHub's rate-limit headers from the last API response, or undefined before any response carried them. */
|
|
1365
|
+
get rateLimit() {
|
|
1366
|
+
return this.rate;
|
|
1367
|
+
}
|
|
1315
1368
|
/** "owner/name". */
|
|
1316
1369
|
get fullName() {
|
|
1317
1370
|
return `${this.owner}/${this.name}`;
|
|
@@ -1335,45 +1388,77 @@ var GitHubClient = class _GitHubClient {
|
|
|
1335
1388
|
get repoPath() {
|
|
1336
1389
|
return `/repos/${this.owner}/${this.name}`;
|
|
1337
1390
|
}
|
|
1338
|
-
async request(method, path, body, retryable = method === "GET") {
|
|
1391
|
+
async request(method, path, body, retryable = method === "GET", opts = {}) {
|
|
1339
1392
|
const attempts = retryable ? Math.max(1, this.retry.attempts) : 1;
|
|
1340
1393
|
for (let attempt = 1; ; attempt++) {
|
|
1341
1394
|
try {
|
|
1342
|
-
return await this.once(method, path, body);
|
|
1395
|
+
return await this.once(method, path, body, opts);
|
|
1343
1396
|
} catch (err) {
|
|
1344
1397
|
const e = err;
|
|
1345
|
-
if (attempt >= attempts || !(e instanceof GitHubError) || !isTransient(e.status, e.detail ?? "")) throw err;
|
|
1398
|
+
if (attempt >= attempts || !(e instanceof GitHubError) || hourlyLimitErrors.has(e) || !isTransient(e.status, e.detail ?? "")) throw err;
|
|
1346
1399
|
await this.retry.sleep(this.retry.baseDelayMs * 2 ** (attempt - 1));
|
|
1347
1400
|
}
|
|
1348
1401
|
}
|
|
1349
1402
|
}
|
|
1350
|
-
|
|
1403
|
+
noteRateLimit(headers) {
|
|
1404
|
+
const remaining = headers?.get("x-ratelimit-remaining");
|
|
1405
|
+
if (remaining == null) return;
|
|
1406
|
+
const limit = headers?.get("x-ratelimit-limit");
|
|
1407
|
+
const reset = headers?.get("x-ratelimit-reset");
|
|
1408
|
+
this.rate = {
|
|
1409
|
+
limit: limit != null ? Number(limit) : void 0,
|
|
1410
|
+
remaining: Number(remaining),
|
|
1411
|
+
resetsAt: reset ? new Date(Number(reset) * 1e3) : void 0,
|
|
1412
|
+
resource: headers?.get("x-ratelimit-resource") ?? void 0
|
|
1413
|
+
};
|
|
1414
|
+
}
|
|
1415
|
+
async once(method, path, body, opts = {}) {
|
|
1416
|
+
const accept = opts.accept ?? ACCEPT_JSON;
|
|
1351
1417
|
const headers = {
|
|
1352
|
-
Accept:
|
|
1418
|
+
Accept: accept,
|
|
1353
1419
|
"X-GitHub-Api-Version": "2022-11-28"
|
|
1354
1420
|
};
|
|
1355
1421
|
if (this.token) headers.Authorization = `Bearer ${this.token}`;
|
|
1356
1422
|
if (body !== void 0) headers["Content-Type"] = "application/json";
|
|
1423
|
+
const key = method === "GET" && this.etags && opts.etag !== false ? `${this.etagScope} ${accept} ${this.api}${path}` : void 0;
|
|
1424
|
+
const cached = key ? this.etags.get(key) : void 0;
|
|
1425
|
+
if (cached && this.token) {
|
|
1426
|
+
headers["If-None-Match"] = cached.etag;
|
|
1427
|
+
this.etagStats.conditional++;
|
|
1428
|
+
}
|
|
1357
1429
|
let res;
|
|
1358
1430
|
try {
|
|
1359
1431
|
res = await this.fetchImpl(`${this.api}${path}`, { method, headers, ...body !== void 0 ? { body: JSON.stringify(body) } : {} });
|
|
1360
1432
|
} catch (err) {
|
|
1433
|
+
if (err instanceof GitHubError) throw err;
|
|
1361
1434
|
throw new GitHubError(0, "Could not reach GitHub. Check your connection, then try again.", err.message);
|
|
1362
1435
|
}
|
|
1436
|
+
this.noteRateLimit(res.headers);
|
|
1437
|
+
if (res.status === 304 && cached && key && this.token) {
|
|
1438
|
+
this.etagStats.notModified++;
|
|
1439
|
+
this.etags.set(key, { ...cached, at: Date.now() });
|
|
1440
|
+
return copy(cached.body);
|
|
1441
|
+
}
|
|
1363
1442
|
if (res.status === 204) return void 0;
|
|
1364
1443
|
if (!res.ok) {
|
|
1365
1444
|
let detail = "";
|
|
1366
1445
|
try {
|
|
1367
|
-
const
|
|
1368
|
-
detail = [
|
|
1446
|
+
const data2 = await res.json();
|
|
1447
|
+
detail = [data2.message, ...(data2.errors ?? []).map((e) => typeof e === "string" ? e : e.message)].filter(Boolean).join(" ");
|
|
1369
1448
|
} catch {
|
|
1370
1449
|
detail = "";
|
|
1371
1450
|
}
|
|
1372
1451
|
const remaining = res.headers?.get("x-ratelimit-remaining");
|
|
1373
1452
|
const reset = res.headers?.get("x-ratelimit-reset");
|
|
1374
1453
|
if ((res.status === 403 || res.status === 429) && remaining === "0") {
|
|
1454
|
+
if (cached && this.options.serveStaleOnRateLimit) {
|
|
1455
|
+
this.etagStats.stale++;
|
|
1456
|
+
return copy(cached.body);
|
|
1457
|
+
}
|
|
1375
1458
|
const resetsAt = reset ? new Date(Number(reset) * 1e3) : void 0;
|
|
1376
|
-
|
|
1459
|
+
const limited = new GitHubError(429, rateLimitMessage({ withToken: Boolean(this.token), resetsAt, tokenHint: this.options.tokenHint }), detail);
|
|
1460
|
+
hourlyLimitErrors.add(limited);
|
|
1461
|
+
throw limited;
|
|
1377
1462
|
}
|
|
1378
1463
|
const info = classifyAuth(res.status, detail, {
|
|
1379
1464
|
path,
|
|
@@ -1382,7 +1467,24 @@ var GitHubClient = class _GitHubClient {
|
|
|
1382
1467
|
});
|
|
1383
1468
|
throw new GitHubError(res.status, friendlyMessage(res.status, detail, info), detail, info);
|
|
1384
1469
|
}
|
|
1385
|
-
|
|
1470
|
+
const data = opts.text ? await res.text() : await res.json();
|
|
1471
|
+
const etag = key ? res.headers?.get("etag") : null;
|
|
1472
|
+
if (key && etag) this.etags.set(key, { etag, body: copy(data), at: Date.now() });
|
|
1473
|
+
return data;
|
|
1474
|
+
}
|
|
1475
|
+
/**
|
|
1476
|
+
* Every item of a paged listing (`per_page=100`), at most 10 pages. `pick` takes the items out of one page (the
|
|
1477
|
+
* installation endpoints wrap them in an object with `total_count`).
|
|
1478
|
+
*/
|
|
1479
|
+
async paged(path, pick) {
|
|
1480
|
+
const out = [];
|
|
1481
|
+
const sep2 = path.includes("?") ? "&" : "?";
|
|
1482
|
+
for (let page = 1; page <= MAX_PAGES; page++) {
|
|
1483
|
+
const { items, total } = pick(await this.request("GET", `${path}${sep2}per_page=100&page=${page}`));
|
|
1484
|
+
out.push(...items);
|
|
1485
|
+
if (items.length < 100 || total !== void 0 && out.length >= total) break;
|
|
1486
|
+
}
|
|
1487
|
+
return out;
|
|
1386
1488
|
}
|
|
1387
1489
|
async getRepo() {
|
|
1388
1490
|
const r = await this.request(
|
|
@@ -1765,6 +1867,14 @@ var GitHubClient = class _GitHubClient {
|
|
|
1765
1867
|
*/
|
|
1766
1868
|
async readFile(path, ref) {
|
|
1767
1869
|
if (this.token) return this.getFileText(path, ref);
|
|
1870
|
+
return this.readRawFile(path, ref);
|
|
1871
|
+
}
|
|
1872
|
+
/**
|
|
1873
|
+
* Text of a file from the raw host (raw.githubusercontent.com), sent without any header so the browser makes a
|
|
1874
|
+
* simple CORS request (no preflight) and nothing counts against the API limit. Works for public repositories only:
|
|
1875
|
+
* a private file answers 404 there, like a missing one. Undefined on 404.
|
|
1876
|
+
*/
|
|
1877
|
+
async readRawFile(path, ref) {
|
|
1768
1878
|
const url = `${(this.options.rawUrl ?? RAW).replace(/\/$/, "")}/${this.owner}/${this.name}/${encodeRef(ref)}/${encodeRef(path)}`;
|
|
1769
1879
|
let res;
|
|
1770
1880
|
try {
|
|
@@ -1776,6 +1886,76 @@ var GitHubClient = class _GitHubClient {
|
|
|
1776
1886
|
if (!res.ok) throw new GitHubError(res.status, friendlyMessage(res.status, ""), `raw ${path}@${ref}`);
|
|
1777
1887
|
return res.text();
|
|
1778
1888
|
}
|
|
1889
|
+
/**
|
|
1890
|
+
* Text of a file at a ref through the contents API with the raw media type: the way to read a private file (the raw
|
|
1891
|
+
* host cannot take the token without a preflight). Counts against the API limit; conditional when cached.
|
|
1892
|
+
* Undefined when the file does not exist.
|
|
1893
|
+
*/
|
|
1894
|
+
async getRawText(path, ref) {
|
|
1895
|
+
try {
|
|
1896
|
+
return await this.request("GET", `${this.repoPath}/contents/${encodeRef(path)}?ref=${encodeURIComponent(ref)}`, void 0, true, { accept: ACCEPT_RAW, text: true });
|
|
1897
|
+
} catch (err) {
|
|
1898
|
+
if (err instanceof GitHubError && err.status === 404) return void 0;
|
|
1899
|
+
throw err;
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
/**
|
|
1903
|
+
* Text of a blob by its sha through the git blobs API with the raw media type: how a signed-in reader loads a file
|
|
1904
|
+
* listed by a tree (the raw host refuses any request that carries a token). A blob sha names its content, so the
|
|
1905
|
+
* answer is cached for the client's life and never revalidated. Undefined on 404.
|
|
1906
|
+
*/
|
|
1907
|
+
async getBlobRaw(sha) {
|
|
1908
|
+
const cached = this.blobCache.get(sha);
|
|
1909
|
+
if (cached !== void 0) return cached;
|
|
1910
|
+
try {
|
|
1911
|
+
const text = await this.request("GET", `${this.repoPath}/git/blobs/${encodeURIComponent(sha)}`, void 0, true, { accept: ACCEPT_RAW, text: true, etag: false });
|
|
1912
|
+
this.blobCache.set(sha, text);
|
|
1913
|
+
return text;
|
|
1914
|
+
} catch (err) {
|
|
1915
|
+
if (err instanceof GitHubError && err.status === 404) return void 0;
|
|
1916
|
+
throw err;
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1919
|
+
// --- GitHub App installations (user-to-server tokens) ---------------------------------------
|
|
1920
|
+
/**
|
|
1921
|
+
* Installations of the signing-in GitHub App that the signed-in user can reach (GET /user/installations, every
|
|
1922
|
+
* page). Needs a user-to-server token of that same App: a personal or OAuth token is refused (403), and each App
|
|
1923
|
+
* sees only its own installations. GitHub lists only installations on accounts and repositories the user has
|
|
1924
|
+
* explicit permission on: a public repository where the user is not a collaborator is never listed, installed or
|
|
1925
|
+
* not. (GET /repos/{o}/{r}/installation would say, but it needs the App's private key, which Bridge never holds.)
|
|
1926
|
+
*/
|
|
1927
|
+
async listUserInstallations() {
|
|
1928
|
+
this.assertToken("Reading where Bridge is installed");
|
|
1929
|
+
const list = await this.paged("/user/installations", (p) => ({ items: p.installations ?? [], total: p.total_count }));
|
|
1930
|
+
return list.map((i) => ({
|
|
1931
|
+
id: i.id,
|
|
1932
|
+
account: i.account?.login ? { login: i.account.login, type: i.account.type ?? "User" } : null,
|
|
1933
|
+
repositorySelection: i.repository_selection ?? "selected",
|
|
1934
|
+
appSlug: i.app_slug
|
|
1935
|
+
}));
|
|
1936
|
+
}
|
|
1937
|
+
/** Repositories of one installation the signed-in user can reach (GET /user/installations/{id}/repositories, every page). */
|
|
1938
|
+
async listInstallationRepos(installationId) {
|
|
1939
|
+
this.assertToken("Reading where Bridge is installed");
|
|
1940
|
+
const list = await this.paged(`/user/installations/${encodeURIComponent(String(installationId))}/repositories`, (p) => ({
|
|
1941
|
+
items: p.repositories ?? [],
|
|
1942
|
+
total: p.total_count
|
|
1943
|
+
}));
|
|
1944
|
+
return list.map((r) => ({ owner: r.owner?.login ?? r.full_name.split("/")[0], name: r.name, fullName: r.full_name, private: Boolean(r.private) }));
|
|
1945
|
+
}
|
|
1946
|
+
/**
|
|
1947
|
+
* The App installation that gives the signed-in user this repository (this client's by default): its id, or null
|
|
1948
|
+
* when the App is not installed there or the user cannot reach it. One call when the installation covers all of
|
|
1949
|
+
* the owner's repositories, two (paged) when it covers selected ones.
|
|
1950
|
+
*/
|
|
1951
|
+
async findInstallation(owner = this.owner, repo = this.name) {
|
|
1952
|
+
const found = installationFor(await this.listUserInstallations(), owner);
|
|
1953
|
+
if (found.reach === "no") return null;
|
|
1954
|
+
if (found.reach === "yes") return { id: found.id, selection: "all" };
|
|
1955
|
+
const repos = await this.listInstallationRepos(found.id);
|
|
1956
|
+
const hit = repos.some((r) => r.owner.toLowerCase() === owner.toLowerCase() && r.name.toLowerCase() === repo.toLowerCase());
|
|
1957
|
+
return hit ? { id: found.id, selection: "selected" } : null;
|
|
1958
|
+
}
|
|
1779
1959
|
/** The repository's default branch. */
|
|
1780
1960
|
async getDefaultBranch() {
|
|
1781
1961
|
return (await this.getRepo()).defaultBranch;
|
|
@@ -6255,6 +6435,7 @@ function shareMessage(x) {
|
|
|
6255
6435
|
// ../core/src/try-in-product.ts
|
|
6256
6436
|
function tryInProductUrl(preview, pr, screen) {
|
|
6257
6437
|
const url = new URL(preview);
|
|
6438
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") return "";
|
|
6258
6439
|
url.pathname = "/";
|
|
6259
6440
|
url.search = "";
|
|
6260
6441
|
url.searchParams.set("bridge", String(pr));
|
|
@@ -11754,7 +11935,7 @@ async function visualImpact(ctx, a, run2) {
|
|
|
11754
11935
|
if (ctx.mode !== "local" || !ctx.root) throw new Error("Visual impact needs a local checkout (start Bridge with --root): it builds Storybook and the app at both refs.");
|
|
11755
11936
|
if (!ctx.isGit) throw new Error(`${ctx.root} is not a git repository.`);
|
|
11756
11937
|
const out = a.out ? resolve3(ctx.root, a.out) : join5(ctx.root, "visual-impact");
|
|
11757
|
-
const runImpact = run2 ?? (await import("./src-
|
|
11938
|
+
const runImpact = run2 ?? (await import("./src-UNWS2OVM.js")).runImpact;
|
|
11758
11939
|
const r = await runImpact({
|
|
11759
11940
|
root: ctx.root,
|
|
11760
11941
|
base: a.base ?? "main",
|
|
@@ -12822,7 +13003,7 @@ async function pushFigmaComponentOp(ctx, a) {
|
|
|
12822
13003
|
}
|
|
12823
13004
|
|
|
12824
13005
|
// src/server.ts
|
|
12825
|
-
var VERSION = true ? "0.8.
|
|
13006
|
+
var VERSION = true ? "0.8.2" : "dev";
|
|
12826
13007
|
var ok = (data) => ({ content: [{ type: "text", text: typeof data === "string" ? data : JSON.stringify(data, null, 2) }] });
|
|
12827
13008
|
var fail2 = (e) => ({ content: [{ type: "text", text: `Error: ${e.message ?? String(e)}` }], isError: true });
|
|
12828
13009
|
var run = (fn) => async (a) => {
|
package/dist/index.js
CHANGED
package/dist/lib.js
CHANGED
|
@@ -28,12 +28,12 @@ import {
|
|
|
28
28
|
serveStdio,
|
|
29
29
|
studio_exports,
|
|
30
30
|
suggestions_exports
|
|
31
|
-
} from "./chunk-
|
|
31
|
+
} from "./chunk-QCPYJXK4.js";
|
|
32
32
|
import {
|
|
33
33
|
CONFIG_FILE,
|
|
34
34
|
loadConfig,
|
|
35
35
|
toConfigJson
|
|
36
|
-
} from "./chunk-
|
|
36
|
+
} from "./chunk-DPK46GA2.js";
|
|
37
37
|
export {
|
|
38
38
|
BridgeContext,
|
|
39
39
|
CONFIG_FILE,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bridge-ds/mcp",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Bridge MCP server: lets Claude (Claude Code, Claude Desktop or any MCP client) read, check and propose changes to a DTCG design system repo through pull requests.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -54,8 +54,8 @@
|
|
|
54
54
|
"build:package": "node ../../scripts/build-packages.mjs mcp"
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"@bridge-ds/core": "^0.
|
|
58
|
-
"@bridge-ds/visual": "^0.3.
|
|
57
|
+
"@bridge-ds/core": "^0.15.0",
|
|
58
|
+
"@bridge-ds/visual": "^0.3.9",
|
|
59
59
|
"@modelcontextprotocol/sdk": "1.31.0",
|
|
60
60
|
"typescript": "5.9.3",
|
|
61
61
|
"zod": "3.25.76"
|