@bridge-ds/cli 0.4.0 → 0.6.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 +643 -149
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { existsSync as existsSync6 } from "node:fs";
|
|
5
|
-
import { readFile as
|
|
6
|
-
import { isAbsolute, join as join6, relative as relative4, resolve as
|
|
5
|
+
import { readFile as readFile7, writeFile as writeFile5 } from "node:fs/promises";
|
|
6
|
+
import { isAbsolute, join as join6, relative as relative4, resolve as resolve4 } from "node:path";
|
|
7
7
|
|
|
8
8
|
// ../core/src/audit/types.ts
|
|
9
9
|
var AUDIT_SCHEMA_VERSION = 1;
|
|
@@ -2158,57 +2158,6 @@ function planCssFixes(text, lang, ds) {
|
|
|
2158
2158
|
return { edits: edits.map(({ start: _s, end: _e, ...e }) => e), output };
|
|
2159
2159
|
}
|
|
2160
2160
|
|
|
2161
|
-
// ../core/src/node/git-local.ts
|
|
2162
|
-
import { execFile } from "node:child_process";
|
|
2163
|
-
|
|
2164
|
-
// ../core/src/governance.ts
|
|
2165
|
-
var DEFAULT_PROTECTED_BRANCHES = ["main", "master"];
|
|
2166
|
-
var ProtectedBranchError = class extends Error {
|
|
2167
|
-
constructor(message) {
|
|
2168
|
-
super(message);
|
|
2169
|
-
this.name = "ProtectedBranchError";
|
|
2170
|
-
}
|
|
2171
|
-
};
|
|
2172
|
-
function assertSafeBranch(branch, rules = {}) {
|
|
2173
|
-
const name = branch.replace(/^refs\/heads\//, "").trim();
|
|
2174
|
-
const blocked = /* @__PURE__ */ new Set([...rules.protectedBranches ?? DEFAULT_PROTECTED_BRANCHES, ...rules.base ? [rules.base] : []]);
|
|
2175
|
-
if (!name || blocked.has(name)) {
|
|
2176
|
-
throw new ProtectedBranchError(`Refusing to write to protected branch "${name}". Bridge only creates new branches and pull requests.`);
|
|
2177
|
-
}
|
|
2178
|
-
if (rules.branchPrefix && !name.startsWith(rules.branchPrefix)) {
|
|
2179
|
-
throw new ProtectedBranchError(`Branch "${name}" must start with "${rules.branchPrefix}".`);
|
|
2180
|
-
}
|
|
2181
|
-
}
|
|
2182
|
-
|
|
2183
|
-
// ../core/src/node/git-local.ts
|
|
2184
|
-
function git(root, args, opts = {}) {
|
|
2185
|
-
return new Promise((resolve4, reject) => {
|
|
2186
|
-
const child = execFile("git", args, { cwd: root, env: { ...process.env, ...opts.env }, maxBuffer: 32 * 1024 * 1024 }, (err, stdout, stderr) => {
|
|
2187
|
-
if (err) reject(new Error(`git ${args.join(" ")}: ${stderr || err.message}`.trim()));
|
|
2188
|
-
else resolve4(stdout);
|
|
2189
|
-
});
|
|
2190
|
-
if (opts.input !== void 0) child.stdin.end(opts.input);
|
|
2191
|
-
});
|
|
2192
|
-
}
|
|
2193
|
-
async function showFile(root, ref, path) {
|
|
2194
|
-
try {
|
|
2195
|
-
return await git(root, ["show", `${ref}:${path}`]);
|
|
2196
|
-
} catch {
|
|
2197
|
-
return null;
|
|
2198
|
-
}
|
|
2199
|
-
}
|
|
2200
|
-
async function listFilesAt(root, ref) {
|
|
2201
|
-
return (await git(root, ["ls-tree", "-r", "--name-only", ref])).split("\n").filter(Boolean);
|
|
2202
|
-
}
|
|
2203
|
-
async function originRepo(root) {
|
|
2204
|
-
try {
|
|
2205
|
-
const url = (await git(root, ["remote", "get-url", "origin"])).trim();
|
|
2206
|
-
return /github\.com[:/]([\w.-]+\/[\w.-]+?)(\.git)?$/.exec(url)?.[1];
|
|
2207
|
-
} catch {
|
|
2208
|
-
return void 0;
|
|
2209
|
-
}
|
|
2210
|
-
}
|
|
2211
|
-
|
|
2212
2161
|
// ../core/src/previews.ts
|
|
2213
2162
|
var PREVIEW_APP_LABELS = { docs: "Docs", storybook: "Storybook", admin: "Admin demo" };
|
|
2214
2163
|
|
|
@@ -2432,6 +2381,47 @@ function visualImpactProblems(raw) {
|
|
|
2432
2381
|
return out;
|
|
2433
2382
|
}
|
|
2434
2383
|
|
|
2384
|
+
// ../core/src/products.ts
|
|
2385
|
+
var DEFAULT_PRODUCTS = [
|
|
2386
|
+
{ key: "admin", name: "Admin demo", path: "apps/admin", framework: "react" },
|
|
2387
|
+
{ key: "library", name: "Library", default: true }
|
|
2388
|
+
];
|
|
2389
|
+
var KEY = /^[a-z0-9][a-z0-9-]*$/;
|
|
2390
|
+
var REPO = /^[\w.-]+\/[\w.-]+$/;
|
|
2391
|
+
var FIELDS = ["key", "name", "repo", "path", "preview", "framework", "default"];
|
|
2392
|
+
function productProblems(raw) {
|
|
2393
|
+
if (raw === void 0) return [];
|
|
2394
|
+
if (!Array.isArray(raw)) return ['"products" must be a list of { "key", "name" } objects'];
|
|
2395
|
+
if (!raw.length) return ['"products" must list at least one product, or be left out to use the defaults'];
|
|
2396
|
+
const problems = [];
|
|
2397
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2398
|
+
let defaults = 0;
|
|
2399
|
+
raw.forEach((p, i) => {
|
|
2400
|
+
const at = `"products[${i}]`;
|
|
2401
|
+
if (!p || typeof p !== "object" || Array.isArray(p)) {
|
|
2402
|
+
problems.push(`${at}" must be an object with a "key" and a "name"`);
|
|
2403
|
+
return;
|
|
2404
|
+
}
|
|
2405
|
+
const o = p;
|
|
2406
|
+
for (const k of Object.keys(o)) if (!FIELDS.includes(k)) problems.push(`${at}.${k}" is not a known field (${FIELDS.join(", ")})`);
|
|
2407
|
+
if (typeof o.key !== "string" || !KEY.test(o.key)) problems.push(`${at}.key" must be lowercase letters, digits and dashes, such as "admin"`);
|
|
2408
|
+
else if (seen.has(o.key)) problems.push(`${at}.key" ("${o.key}") is used twice: give each product its own key`);
|
|
2409
|
+
else seen.add(o.key);
|
|
2410
|
+
if (typeof o.name !== "string" || !o.name.trim()) problems.push(`${at}.name" must be the name people read, such as "Admin demo"`);
|
|
2411
|
+
if (o.repo !== void 0 && (typeof o.repo !== "string" || !REPO.test(o.repo))) problems.push(`${at}.repo" must be "owner/name"`);
|
|
2412
|
+
if (o.path !== void 0) {
|
|
2413
|
+
const bad = typeof o.path !== "string" || !o.path.trim() || o.path.startsWith("/") || o.path.split("/").includes("..");
|
|
2414
|
+
if (bad) problems.push(`${at}.path" must be a folder relative to the repository root, without a leading "/" or "..", such as "apps/admin"`);
|
|
2415
|
+
}
|
|
2416
|
+
if (o.preview !== void 0 && (typeof o.preview !== "string" || !/^https?:\/\//.test(o.preview))) problems.push(`${at}.preview" must be an http(s) URL template, such as "https://app-git-{branch}.vercel.app"`);
|
|
2417
|
+
if (o.framework !== void 0 && (typeof o.framework !== "string" || !o.framework.trim())) problems.push(`${at}.framework" must be a name such as "react", "vue" or "other"`);
|
|
2418
|
+
if (o.default !== void 0 && typeof o.default !== "boolean") problems.push(`${at}.default" must be true or false`);
|
|
2419
|
+
if (o.default === true) defaults++;
|
|
2420
|
+
});
|
|
2421
|
+
if (defaults > 1) problems.push('"products" marks more than one product as default: keep "default": true on the library only');
|
|
2422
|
+
return problems;
|
|
2423
|
+
}
|
|
2424
|
+
|
|
2435
2425
|
// ../core/src/config.ts
|
|
2436
2426
|
var CONFIG_FILE = "bridge.config.json";
|
|
2437
2427
|
var DEFAULT_OVERRIDE = { allowed: [], requireReason: true, minReasonLength: 20 };
|
|
@@ -2572,6 +2562,7 @@ function defineConfig(input) {
|
|
|
2572
2562
|
} : {},
|
|
2573
2563
|
...g.rights ? { rights: normalizeRights(g.rights) } : {}
|
|
2574
2564
|
},
|
|
2565
|
+
products: input.products?.length ? input.products.map((p) => ({ ...p })) : DEFAULT_PRODUCTS.map((p) => ({ ...p })),
|
|
2575
2566
|
...input.audit ? { audit: { ...input.audit } } : {},
|
|
2576
2567
|
...input.visualImpact ? { visualImpact: { mode: input.visualImpact.mode === "on-demand" ? "on-demand" : "auto" } } : {},
|
|
2577
2568
|
...input.integrations ? { integrations: { ...input.integrations } } : {},
|
|
@@ -2694,6 +2685,7 @@ function loadConfig(source) {
|
|
|
2694
2685
|
}
|
|
2695
2686
|
}
|
|
2696
2687
|
problems.push(...visualImpactProblems(d.visualImpact));
|
|
2688
|
+
problems.push(...productProblems(d.products));
|
|
2697
2689
|
const audit2 = d.audit;
|
|
2698
2690
|
if (audit2 !== void 0) {
|
|
2699
2691
|
if (!isObj(audit2)) problems.push('"audit" must be an object');
|
|
@@ -2723,6 +2715,57 @@ function loadConfig(source) {
|
|
|
2723
2715
|
return defineConfig(d);
|
|
2724
2716
|
}
|
|
2725
2717
|
|
|
2718
|
+
// ../core/src/node/git-local.ts
|
|
2719
|
+
import { execFile } from "node:child_process";
|
|
2720
|
+
|
|
2721
|
+
// ../core/src/governance.ts
|
|
2722
|
+
var DEFAULT_PROTECTED_BRANCHES = ["main", "master"];
|
|
2723
|
+
var ProtectedBranchError = class extends Error {
|
|
2724
|
+
constructor(message) {
|
|
2725
|
+
super(message);
|
|
2726
|
+
this.name = "ProtectedBranchError";
|
|
2727
|
+
}
|
|
2728
|
+
};
|
|
2729
|
+
function assertSafeBranch(branch, rules = {}) {
|
|
2730
|
+
const name = branch.replace(/^refs\/heads\//, "").trim();
|
|
2731
|
+
const blocked = /* @__PURE__ */ new Set([...rules.protectedBranches ?? DEFAULT_PROTECTED_BRANCHES, ...rules.base ? [rules.base] : []]);
|
|
2732
|
+
if (!name || blocked.has(name)) {
|
|
2733
|
+
throw new ProtectedBranchError(`Refusing to write to protected branch "${name}". Bridge only creates new branches and pull requests.`);
|
|
2734
|
+
}
|
|
2735
|
+
if (rules.branchPrefix && !name.startsWith(rules.branchPrefix)) {
|
|
2736
|
+
throw new ProtectedBranchError(`Branch "${name}" must start with "${rules.branchPrefix}".`);
|
|
2737
|
+
}
|
|
2738
|
+
}
|
|
2739
|
+
|
|
2740
|
+
// ../core/src/node/git-local.ts
|
|
2741
|
+
function git(root, args, opts = {}) {
|
|
2742
|
+
return new Promise((resolve5, reject) => {
|
|
2743
|
+
const child = execFile("git", args, { cwd: root, env: { ...process.env, ...opts.env }, maxBuffer: 32 * 1024 * 1024 }, (err, stdout, stderr) => {
|
|
2744
|
+
if (err) reject(new Error(`git ${args.join(" ")}: ${stderr || err.message}`.trim()));
|
|
2745
|
+
else resolve5(stdout);
|
|
2746
|
+
});
|
|
2747
|
+
if (opts.input !== void 0) child.stdin.end(opts.input);
|
|
2748
|
+
});
|
|
2749
|
+
}
|
|
2750
|
+
async function showFile(root, ref, path) {
|
|
2751
|
+
try {
|
|
2752
|
+
return await git(root, ["show", `${ref}:${path}`]);
|
|
2753
|
+
} catch {
|
|
2754
|
+
return null;
|
|
2755
|
+
}
|
|
2756
|
+
}
|
|
2757
|
+
async function listFilesAt(root, ref) {
|
|
2758
|
+
return (await git(root, ["ls-tree", "-r", "--name-only", ref])).split("\n").filter(Boolean);
|
|
2759
|
+
}
|
|
2760
|
+
async function originRepo(root) {
|
|
2761
|
+
try {
|
|
2762
|
+
const url = (await git(root, ["remote", "get-url", "origin"])).trim();
|
|
2763
|
+
return /github\.com[:/]([\w.-]+\/[\w.-]+?)(\.git)?$/.exec(url)?.[1];
|
|
2764
|
+
} catch {
|
|
2765
|
+
return void 0;
|
|
2766
|
+
}
|
|
2767
|
+
}
|
|
2768
|
+
|
|
2726
2769
|
// ../core/src/layered-review.ts
|
|
2727
2770
|
var same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
2728
2771
|
function diffLayered(before, after) {
|
|
@@ -3624,7 +3667,7 @@ var GitHubClient = class _GitHubClient {
|
|
|
3624
3667
|
}
|
|
3625
3668
|
async getPullRequest(n) {
|
|
3626
3669
|
const r = await this.request("GET", `${this.repoPath}/pulls/${n}`);
|
|
3627
|
-
return { ...toSummary(r), body: r.body ?? "", mergeable: r.mergeable ?? null };
|
|
3670
|
+
return { ...toSummary(r), body: r.body ?? "", mergeable: r.mergeable ?? null, requestedReviewers: (r.requested_reviewers ?? []).map((x) => x.login) };
|
|
3628
3671
|
}
|
|
3629
3672
|
async listPullRequestFiles(n) {
|
|
3630
3673
|
const r = await this.request("GET", `${this.repoPath}/pulls/${n}/files?per_page=100`);
|
|
@@ -3701,6 +3744,111 @@ var GitHubClient = class _GitHubClient {
|
|
|
3701
3744
|
async removeLabel(issueNumber2, label) {
|
|
3702
3745
|
await this.request("DELETE", `${this.repoPath}/issues/${issueNumber2}/labels/${encodeURIComponent(label)}`);
|
|
3703
3746
|
}
|
|
3747
|
+
// --- branch actions shared by Studio, the MCP server and the CLI (branch-actions.ts) ---
|
|
3748
|
+
/** The account the token belongs to (GET /user). Needs a token. */
|
|
3749
|
+
async getViewer() {
|
|
3750
|
+
this.assertToken("Reading who you are on GitHub");
|
|
3751
|
+
const r = await this.request("GET", "/user");
|
|
3752
|
+
return { login: r.login };
|
|
3753
|
+
}
|
|
3754
|
+
/** The repository's merge settings and the viewer's permissions (unknown settings count as allowed). */
|
|
3755
|
+
async getMergeSettings() {
|
|
3756
|
+
return this.request("GET", this.repoPath);
|
|
3757
|
+
}
|
|
3758
|
+
/**
|
|
3759
|
+
* Merges a pull request, only at `sha` (GitHub refuses when the branch moved meanwhile). The repository's branch
|
|
3760
|
+
* rules still apply: GitHub refuses a merge they block, whatever Bridge says.
|
|
3761
|
+
*/
|
|
3762
|
+
async mergePullRequest(n, input) {
|
|
3763
|
+
this.assertToken("Merging a branch");
|
|
3764
|
+
const r = await this.request("PUT", `${this.repoPath}/pulls/${n}/merge`, {
|
|
3765
|
+
merge_method: input.method,
|
|
3766
|
+
sha: input.sha,
|
|
3767
|
+
...input.commitMessage !== void 0 && input.method !== "rebase" ? { commit_message: input.commitMessage } : {}
|
|
3768
|
+
});
|
|
3769
|
+
return { sha: r?.sha ?? "", merged: r?.merged ?? true };
|
|
3770
|
+
}
|
|
3771
|
+
/** GitHub's "Update branch": merges the base into the pull request, only when its head is still `expectedHeadSha`. */
|
|
3772
|
+
async updatePullRequestBranch(n, expectedHeadSha) {
|
|
3773
|
+
this.assertToken("Updating a branch from main");
|
|
3774
|
+
await this.request("PUT", `${this.repoPath}/pulls/${n}/update-branch`, { expected_head_sha: expectedHeadSha });
|
|
3775
|
+
}
|
|
3776
|
+
/**
|
|
3777
|
+
* Merges the branch `head` into the branch `base` (GitHub's merges API): a merge commit on `base`, or nothing when
|
|
3778
|
+
* `base` already has every commit of `head`. A conflict throws a GitHubError with status 409. Refuses protected
|
|
3779
|
+
* branches and, with a branch prefix, a `base` without it.
|
|
3780
|
+
*/
|
|
3781
|
+
async mergeIntoBranch(base, head, message) {
|
|
3782
|
+
this.assertToken("Merging a branch into another");
|
|
3783
|
+
this.assertWritableBranch(base);
|
|
3784
|
+
const r = await this.request("POST", `${this.repoPath}/merges`, { base, head, commit_message: message });
|
|
3785
|
+
return r?.sha ? { status: "merged", sha: r.sha } : { status: "up-to-date", sha: null };
|
|
3786
|
+
}
|
|
3787
|
+
/** Posts a comment on an issue or pull request and returns its id and link. */
|
|
3788
|
+
async createIssueComment(n, body) {
|
|
3789
|
+
this.assertToken("Commenting");
|
|
3790
|
+
const r = await this.request("POST", `${this.repoPath}/issues/${n}/comments`, { body });
|
|
3791
|
+
return { id: r.id, url: r.html_url };
|
|
3792
|
+
}
|
|
3793
|
+
/** Replaces the text of a comment. */
|
|
3794
|
+
async updateIssueComment(id, body) {
|
|
3795
|
+
this.assertToken("Editing a comment");
|
|
3796
|
+
await this.request("PATCH", `${this.repoPath}/issues/comments/${id}`, { body });
|
|
3797
|
+
}
|
|
3798
|
+
/** Submits a review on a given commit (the head the reviewer saw); the body may be empty for an approval. */
|
|
3799
|
+
async submitReview(n, input) {
|
|
3800
|
+
this.assertToken("Reviewing a branch");
|
|
3801
|
+
const r = await this.request("POST", `${this.repoPath}/pulls/${n}/reviews`, {
|
|
3802
|
+
event: input.event,
|
|
3803
|
+
...input.body ? { body: input.body } : {},
|
|
3804
|
+
...input.commitId ? { commit_id: input.commitId } : {}
|
|
3805
|
+
});
|
|
3806
|
+
return { id: r.id, user: r.user?.login, state: r.state, body: r.body ?? "" };
|
|
3807
|
+
}
|
|
3808
|
+
/** Line comments of a pull request (review threads), oldest first. */
|
|
3809
|
+
async listReviewComments(n) {
|
|
3810
|
+
const r = await this.request(
|
|
3811
|
+
"GET",
|
|
3812
|
+
`${this.repoPath}/pulls/${n}/comments?per_page=100`
|
|
3813
|
+
);
|
|
3814
|
+
return r.map((c) => ({ id: c.id, body: c.body ?? "", author: c.user?.login ?? "unknown", path: c.path, line: c.line ?? null, inReplyTo: c.in_reply_to_id ?? null, url: c.html_url, createdAt: c.created_at }));
|
|
3815
|
+
}
|
|
3816
|
+
/** Asks these people to review a pull request. */
|
|
3817
|
+
async requestReviewers(n, logins) {
|
|
3818
|
+
this.assertToken("Asking for a review");
|
|
3819
|
+
await this.request("POST", `${this.repoPath}/pulls/${n}/requested_reviewers`, { reviewers: logins });
|
|
3820
|
+
}
|
|
3821
|
+
/** Takes these people off the requested reviewers. */
|
|
3822
|
+
async removeRequestedReviewers(n, logins) {
|
|
3823
|
+
this.assertToken("Changing the requested reviewers");
|
|
3824
|
+
await this.request("DELETE", `${this.repoPath}/pulls/${n}/requested_reviewers`, { reviewers: logins });
|
|
3825
|
+
}
|
|
3826
|
+
/** Replies in the thread of a line comment. */
|
|
3827
|
+
async replyToReviewComment(n, commentId, body) {
|
|
3828
|
+
this.assertToken("Replying to a comment");
|
|
3829
|
+
const r = await this.request("POST", `${this.repoPath}/pulls/${n}/comments/${commentId}/replies`, { body });
|
|
3830
|
+
return { id: r.id, url: r.html_url };
|
|
3831
|
+
}
|
|
3832
|
+
/**
|
|
3833
|
+
* What `head` adds on top of `base`, with what library sync needs: the shared start (merge base), the base
|
|
3834
|
+
* commit and its tree, how far behind, and the files with their previous names.
|
|
3835
|
+
*/
|
|
3836
|
+
async compareDetailed(base, head) {
|
|
3837
|
+
const r = await this.request("GET", `${this.repoPath}/compare/${encodeRef(base)}...${encodeRef(head)}`);
|
|
3838
|
+
return {
|
|
3839
|
+
behindBy: r.behind_by,
|
|
3840
|
+
aheadBy: r.ahead_by,
|
|
3841
|
+
mergeBase: r.merge_base_commit?.sha ?? null,
|
|
3842
|
+
baseSha: r.base_commit?.sha ?? null,
|
|
3843
|
+
baseTree: r.base_commit?.commit?.tree?.sha ?? null,
|
|
3844
|
+
files: (r.files ?? []).map((f) => ({ filename: f.filename, status: f.status, ...f.previous_filename ? { previous_filename: f.previous_filename } : {} }))
|
|
3845
|
+
};
|
|
3846
|
+
}
|
|
3847
|
+
/** A tree on top of `baseTree` from entries with their own file mode (blob sha, inline content, or sha null to delete). */
|
|
3848
|
+
async createTreeEntries(baseTree, entries) {
|
|
3849
|
+
const r = await this.request("POST", `${this.repoPath}/git/trees`, { base_tree: baseTree, tree: entries.map((e) => ({ ...e, type: "blob" })) }, true);
|
|
3850
|
+
return r.sha;
|
|
3851
|
+
}
|
|
3704
3852
|
};
|
|
3705
3853
|
function toCommitSummary(c) {
|
|
3706
3854
|
return {
|
|
@@ -4162,15 +4310,15 @@ async function shoot(page, r) {
|
|
|
4162
4310
|
}
|
|
4163
4311
|
async function switchStory(page, storyId2) {
|
|
4164
4312
|
const ok = await page.evaluate(
|
|
4165
|
-
(id) => new Promise((
|
|
4313
|
+
(id) => new Promise((resolve5) => {
|
|
4166
4314
|
const ch = window.__STORYBOOK_ADDONS_CHANNEL__;
|
|
4167
|
-
if (!ch) return
|
|
4315
|
+
if (!ch) return resolve5(false);
|
|
4168
4316
|
const events = ["storyRendered", "storyErrored", "storyThrewException", "storyMissing", "playFunctionThrewException"];
|
|
4169
4317
|
const timer = setTimeout(() => done(), 15e3);
|
|
4170
4318
|
function done() {
|
|
4171
4319
|
clearTimeout(timer);
|
|
4172
4320
|
for (const e of events) ch.off(e, done);
|
|
4173
|
-
|
|
4321
|
+
resolve5(true);
|
|
4174
4322
|
}
|
|
4175
4323
|
for (const e of events) ch.on(e, done);
|
|
4176
4324
|
window.scrollTo(0, 0);
|
|
@@ -4583,12 +4731,12 @@ function serveDir(dir) {
|
|
|
4583
4731
|
res.writeHead(200, { "content-type": TYPES[extname(file)] ?? "application/octet-stream", "cache-control": "no-store" });
|
|
4584
4732
|
createReadStream(file).pipe(res);
|
|
4585
4733
|
});
|
|
4586
|
-
return new Promise((
|
|
4734
|
+
return new Promise((resolve5, reject) => {
|
|
4587
4735
|
server.once("error", reject);
|
|
4588
4736
|
server.listen(0, "127.0.0.1", () => {
|
|
4589
4737
|
const addr = server.address();
|
|
4590
4738
|
const port = typeof addr === "object" && addr ? addr.port : 0;
|
|
4591
|
-
|
|
4739
|
+
resolve5({ url: `http://127.0.0.1:${port}`, close: () => new Promise((r) => server.close(() => r())) });
|
|
4592
4740
|
});
|
|
4593
4741
|
});
|
|
4594
4742
|
}
|
|
@@ -4660,7 +4808,7 @@ async function removeWorktree(repo, dir) {
|
|
|
4660
4808
|
await git(repo, ["worktree", "prune"]).catch(() => void 0);
|
|
4661
4809
|
}
|
|
4662
4810
|
function run(cmd, args, cwd, log, root = cwd, env = {}) {
|
|
4663
|
-
return new Promise((
|
|
4811
|
+
return new Promise((resolve5, reject) => {
|
|
4664
4812
|
const bin = [.../* @__PURE__ */ new Set([join3(cwd, "node_modules", ".bin"), join3(root, "node_modules", ".bin")])].join(":");
|
|
4665
4813
|
const child = spawn(cmd, args, {
|
|
4666
4814
|
cwd,
|
|
@@ -4673,7 +4821,7 @@ function run(cmd, args, cwd, log, root = cwd, env = {}) {
|
|
|
4673
4821
|
child.stderr.on("data", keep);
|
|
4674
4822
|
child.on("error", reject);
|
|
4675
4823
|
child.on("close", (code) => {
|
|
4676
|
-
if (code === 0)
|
|
4824
|
+
if (code === 0) resolve5();
|
|
4677
4825
|
else {
|
|
4678
4826
|
log(tail);
|
|
4679
4827
|
reject(new Error(`${cmd} ${args.join(" ")} failed in ${cwd} (exit ${code})`));
|
|
@@ -5084,12 +5232,12 @@ async function captureFigmaReferences(o) {
|
|
|
5084
5232
|
}
|
|
5085
5233
|
const plan = JSON.stringify(j.plan.render);
|
|
5086
5234
|
await page.evaluate(
|
|
5087
|
-
({ id, plan: plan2 }) => new Promise((
|
|
5235
|
+
({ id, plan: plan2 }) => new Promise((resolve5) => {
|
|
5088
5236
|
const ch = window.__STORYBOOK_ADDONS_CHANNEL__;
|
|
5089
5237
|
const done = () => {
|
|
5090
5238
|
clearTimeout(timer);
|
|
5091
5239
|
ch.off("storyRendered", done);
|
|
5092
|
-
|
|
5240
|
+
resolve5();
|
|
5093
5241
|
};
|
|
5094
5242
|
const timer = setTimeout(done, 1e4);
|
|
5095
5243
|
ch.on("storyRendered", done);
|
|
@@ -5233,7 +5381,7 @@ Usage:
|
|
|
5233
5381
|
--branch: implement it on that exploration, never in a pull request to main
|
|
5234
5382
|
bridge request create --component Button --kind new-variant --title "Add a danger ghost variant"
|
|
5235
5383
|
--summary "..." [--change field=before:after ...] [--figma url]
|
|
5236
|
-
[--exploration explore/<name>] [--dry-run] [--json]
|
|
5384
|
+
[--exploration explore/<name>] [--product <key>] [--dry-run] [--json]
|
|
5237
5385
|
bridge request jira-sync
|
|
5238
5386
|
GitHub Action step: mirror a request issue to Jira (see docs/REQUESTS.md)
|
|
5239
5387
|
|
|
@@ -5388,6 +5536,7 @@ Claude Code is not installed (no \`claude\` on PATH). Install it (npm install -g
|
|
|
5388
5536
|
changes: (a.flags.change ?? []).map(parseChange),
|
|
5389
5537
|
...figma ? { figmaUrl: figma } : {},
|
|
5390
5538
|
...exploration ? { exploration } : {},
|
|
5539
|
+
...flag(a, "product") ? { product: flag(a, "product") } : {},
|
|
5391
5540
|
source: "cli",
|
|
5392
5541
|
dryRun
|
|
5393
5542
|
});
|
|
@@ -5433,7 +5582,7 @@ staging branch without touching main (an explore/<name> branch with a draft pull
|
|
|
5433
5582
|
|
|
5434
5583
|
Usage:
|
|
5435
5584
|
bridge explore start <name> --purpose "..." [--use-case ${EXPLORATION_USE_CASES.map((u) => u.value).join("|")}]
|
|
5436
|
-
[--experimental "..." ...] [--dry-run] [--json]
|
|
5585
|
+
[--experimental "..." ...] [--product <key>] [--dry-run] [--json]
|
|
5437
5586
|
GitHub token: the branch and the draft pull request on GitHub. Without one, in a git repo:
|
|
5438
5587
|
a local branch only, nothing pushed
|
|
5439
5588
|
bridge explore list [--all] [--json]
|
|
@@ -5471,7 +5620,7 @@ async function runExplore(a, root, io = defaultIO()) {
|
|
|
5471
5620
|
if (!name || !purpose) throw new RequestUsageError('Usage: bridge explore start <name> --purpose "What you want to learn"');
|
|
5472
5621
|
const useCase = flag2(a, "use-case");
|
|
5473
5622
|
const ctx = await context2(root, io);
|
|
5474
|
-
const r = await explorations.startExploration(ctx, { name, purpose, ...exploreSource(io.env), ...useCase ? { useCase } : {}, ...a.flags.experimental ? { experimental: a.flags.experimental } : {}, dryRun: has2(a, "dry-run") });
|
|
5623
|
+
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: has2(a, "dry-run") });
|
|
5475
5624
|
if (json) return io.print(JSON.stringify(r, null, 2)), 0;
|
|
5476
5625
|
if (r.status === "dry-run") {
|
|
5477
5626
|
io.print(`Dry run, nothing written.
|
|
@@ -5541,6 +5690,316 @@ ${EXPLORE_HELP}`);
|
|
|
5541
5690
|
}
|
|
5542
5691
|
}
|
|
5543
5692
|
|
|
5693
|
+
// src/branch.ts
|
|
5694
|
+
import { readFile as readFile6 } from "node:fs/promises";
|
|
5695
|
+
import { resolve as resolve3 } from "node:path";
|
|
5696
|
+
import { BridgeContext as BridgeContext4, branches, studio } from "@bridge-ds/mcp";
|
|
5697
|
+
var BRANCH_HELP = `bridge branch: branches in review, the same actions as Bridge Studio
|
|
5698
|
+
|
|
5699
|
+
Usage:
|
|
5700
|
+
bridge branch list [--limit 20] [--json] what needs me: the review queue, mine first
|
|
5701
|
+
bridge branch status <n> [--json] status, approvals, tests, main, may I merge, next step
|
|
5702
|
+
bridge branch comments <n> [--json] the conversation and line threads (ids to reply to)
|
|
5703
|
+
bridge branch ready <n> send a draft for review (its author)
|
|
5704
|
+
bridge branch draft <n> back to draft (its author)
|
|
5705
|
+
bridge branch review <n> --approve [--body "..."] | --request-changes "..." | --comment "..."
|
|
5706
|
+
bridge branch ask <n> [--dry-run] ask someone else to review (who can give the missing approvals)
|
|
5707
|
+
bridge branch comment <n> "text" [--reply-to <id>]
|
|
5708
|
+
bridge branch merge <n> [--override "reason"] [--dry-run]
|
|
5709
|
+
same gate as the Merge button; --override: owner override,
|
|
5710
|
+
the reason is posted on the branch first
|
|
5711
|
+
bridge branch close <n> | bridge branch reopen <n>
|
|
5712
|
+
bridge branch update <n> [--choice <key>=mine|theirs ...] [--dry-run]
|
|
5713
|
+
update from main; token conflicts: pick per key
|
|
5714
|
+
bridge branch try <n> [--json] try it in a product, share it with a product team
|
|
5715
|
+
bridge branch combine <n> <n> [<n> <n>] [--pick <key>=<n> ...] [--json]
|
|
5716
|
+
try 2 to 4 token branches together (writes nothing)
|
|
5717
|
+
bridge products [--json] the products and the one this folder belongs to
|
|
5718
|
+
bridge settings [--json] roles, who ships, owner override, protection, products
|
|
5719
|
+
bridge coverage [Component] [--json] which components are in Figma
|
|
5720
|
+
bridge docs save <Component> --file docs.json [--pr <n>] [--title "..."] [--jira DS-42]
|
|
5721
|
+
[--product admin] [--dry-run] [--json]
|
|
5722
|
+
save usage guidance as in the Library edit mode
|
|
5723
|
+
|
|
5724
|
+
Start work for a product: bridge propose|explore start|request create ... --product <key> [--jira DS-42].
|
|
5725
|
+
GitHub token: GITHUB_TOKEN or BRIDGE_GITHUB_TOKEN, else \`gh auth token\`. Exit codes: 0 ok, 2 usage or setup error.`;
|
|
5726
|
+
var flag3 = (a, k) => a.flags[k]?.[a.flags[k].length - 1];
|
|
5727
|
+
var has3 = (a, k) => Boolean(a.flags[k]);
|
|
5728
|
+
async function context3(root, io) {
|
|
5729
|
+
const token = io.env.BRIDGE_GITHUB_TOKEN || io.env.GITHUB_TOKEN || io.ghToken();
|
|
5730
|
+
return BridgeContext4.create({ root, ...token ? { token } : {}, ...io.fetch ? { fetch: io.fetch } : {} });
|
|
5731
|
+
}
|
|
5732
|
+
function number(a, at, usage) {
|
|
5733
|
+
const raw = a._[at];
|
|
5734
|
+
if (!raw) throw new RequestUsageError(`Usage: ${usage}`);
|
|
5735
|
+
return branches.branchNumber(raw);
|
|
5736
|
+
}
|
|
5737
|
+
function pairs(list2, what) {
|
|
5738
|
+
const out = {};
|
|
5739
|
+
for (const s of list2 ?? []) {
|
|
5740
|
+
const i = s.lastIndexOf("=");
|
|
5741
|
+
if (i <= 0) throw new RequestUsageError(`--${what} needs key=value, got "${s}".`);
|
|
5742
|
+
out[s.slice(0, i)] = s.slice(i + 1);
|
|
5743
|
+
}
|
|
5744
|
+
return out;
|
|
5745
|
+
}
|
|
5746
|
+
var said = (r) => String(r.said ?? r.summary ?? r.status ?? "Done.");
|
|
5747
|
+
async function runBranch(a, root, io = defaultIO()) {
|
|
5748
|
+
const sub = a._[1];
|
|
5749
|
+
if (!sub || sub === "help" || has3(a, "help")) {
|
|
5750
|
+
io.print(BRANCH_HELP);
|
|
5751
|
+
return sub || has3(a, "help") ? 0 : 2;
|
|
5752
|
+
}
|
|
5753
|
+
const json = has3(a, "json");
|
|
5754
|
+
const ctx = await context3(root, io);
|
|
5755
|
+
const out = (r, text) => (json ? io.print(JSON.stringify(r, null, 2)) : text(), 0);
|
|
5756
|
+
switch (sub) {
|
|
5757
|
+
case "list": {
|
|
5758
|
+
const limit = flag3(a, "limit");
|
|
5759
|
+
const r = await branches.whatNeedsMe(ctx, limit ? { limit: Number(limit) } : {});
|
|
5760
|
+
return out(r, () => {
|
|
5761
|
+
io.print(r.summary);
|
|
5762
|
+
for (const b of r.branches) io.print(`#${b.pr} ${b.statusLabel.padEnd(22)} ${b.title}${b.needsYou ? ` <- ${b.needsYou}` : ""}`);
|
|
5763
|
+
});
|
|
5764
|
+
}
|
|
5765
|
+
case "status": {
|
|
5766
|
+
const r = await branches.branchStatus(ctx, { pr: number(a, 2, "bridge branch status <n>") });
|
|
5767
|
+
return out(r, () => {
|
|
5768
|
+
io.print(`#${r.pr} ${r.title} (${r.product.name})`);
|
|
5769
|
+
io.print(`${r.status.label}. ${r.approvalNeeded} needed; approved by ${r.approvals.approvedBy.map((x) => `@${x}`).join(", ") || "nobody yet"}.`);
|
|
5770
|
+
if (r.merge.blockedBy?.length) for (const x of r.merge.blockedBy) io.print(` ${x}`);
|
|
5771
|
+
io.print(`Next: ${r.next}`);
|
|
5772
|
+
io.print(r.url);
|
|
5773
|
+
});
|
|
5774
|
+
}
|
|
5775
|
+
case "comments": {
|
|
5776
|
+
const r = await branches.listComments(ctx, { pr: number(a, 2, "bridge branch comments <n>") });
|
|
5777
|
+
return out(r, () => {
|
|
5778
|
+
for (const c of r.conversation) io.print(`@${c.author}: ${c.body}`);
|
|
5779
|
+
for (const t of r.threads) io.print(`[thread ${t.id}] ${t.file}:${t.line ?? ""} @${t.author}: ${t.body}${t.replies.length ? ` (${t.replies.length} replies)` : ""}`);
|
|
5780
|
+
});
|
|
5781
|
+
}
|
|
5782
|
+
case "ready": {
|
|
5783
|
+
const r = await branches.sendForReview(ctx, { pr: number(a, 2, "bridge branch ready <n>") });
|
|
5784
|
+
return out(r, () => io.print(r.said));
|
|
5785
|
+
}
|
|
5786
|
+
case "draft": {
|
|
5787
|
+
const r = await branches.backToDraft(ctx, { pr: number(a, 2, "bridge branch draft <n>") });
|
|
5788
|
+
return out(r, () => io.print(r.said));
|
|
5789
|
+
}
|
|
5790
|
+
case "review": {
|
|
5791
|
+
const n = number(a, 2, 'bridge branch review <n> --approve | --request-changes "..." | --comment "..."');
|
|
5792
|
+
const decision = has3(a, "approve") ? "approve" : has3(a, "request-changes") ? "request_changes" : has3(a, "comment") ? "comment" : null;
|
|
5793
|
+
if (!decision) throw new RequestUsageError('Say what you decide: --approve, --request-changes "what to change" or --comment "text".');
|
|
5794
|
+
const body = decision === "approve" ? flag3(a, "body") : flag3(a, decision === "request_changes" ? "request-changes" : "comment");
|
|
5795
|
+
const r = await branches.reviewBranch(ctx, { pr: n, decision, ...body && body !== "true" ? { body } : {} });
|
|
5796
|
+
return out(r, () => io.print(r.said));
|
|
5797
|
+
}
|
|
5798
|
+
case "ask": {
|
|
5799
|
+
const r = await branches.askSomeoneElse(ctx, { pr: number(a, 2, "bridge branch ask <n>"), dryRun: has3(a, "dry-run") });
|
|
5800
|
+
return out(r, () => io.print(r.said));
|
|
5801
|
+
}
|
|
5802
|
+
case "comment": {
|
|
5803
|
+
const n = number(a, 2, 'bridge branch comment <n> "text"');
|
|
5804
|
+
const body = a._.slice(3).join(" ") || flag3(a, "body");
|
|
5805
|
+
if (!body) throw new RequestUsageError('Usage: bridge branch comment <n> "text" [--reply-to <id>]');
|
|
5806
|
+
const reply = flag3(a, "reply-to");
|
|
5807
|
+
const r = await branches.commentOnBranch(ctx, { pr: n, body, ...reply ? { replyTo: Number(reply) } : {} });
|
|
5808
|
+
return out(r, () => io.print(r.said));
|
|
5809
|
+
}
|
|
5810
|
+
case "merge": {
|
|
5811
|
+
const n = number(a, 2, 'bridge branch merge <n> [--override "reason"] [--dry-run]');
|
|
5812
|
+
const reason = flag3(a, "override");
|
|
5813
|
+
const r = await branches.mergeBranch(ctx, { pr: n, ...reason ? { overrideReason: reason } : {}, dryRun: has3(a, "dry-run") });
|
|
5814
|
+
return out(r, () => io.print(r.said));
|
|
5815
|
+
}
|
|
5816
|
+
case "close": {
|
|
5817
|
+
const r = await branches.closeBranch(ctx, { pr: number(a, 2, "bridge branch close <n>") });
|
|
5818
|
+
return out(r, () => io.print(r.said));
|
|
5819
|
+
}
|
|
5820
|
+
case "reopen": {
|
|
5821
|
+
const r = await branches.reopenBranch(ctx, { pr: number(a, 2, "bridge branch reopen <n>") });
|
|
5822
|
+
return out(r, () => io.print(r.said));
|
|
5823
|
+
}
|
|
5824
|
+
case "update": {
|
|
5825
|
+
const n = number(a, 2, "bridge branch update <n> [--choice key=mine|theirs] [--dry-run]");
|
|
5826
|
+
const choices = pairs(a.flags.choice, "choice");
|
|
5827
|
+
for (const [k, v] of Object.entries(choices)) if (v !== "mine" && v !== "theirs") throw new RequestUsageError(`--choice ${k}= takes mine or theirs, got "${v}".`);
|
|
5828
|
+
const r = await branches.updateFromMain(ctx, { pr: n, ...Object.keys(choices).length ? { choices } : {}, dryRun: has3(a, "dry-run") });
|
|
5829
|
+
return out(r, () => {
|
|
5830
|
+
io.print(said(r));
|
|
5831
|
+
for (const c of r.conflicts ?? []) io.print(` ${c.key} (${c.token}, default ${c.default ?? "none: pick one"})`);
|
|
5832
|
+
});
|
|
5833
|
+
}
|
|
5834
|
+
case "try": {
|
|
5835
|
+
const r = await branches.tryBranch(ctx, { pr: number(a, 2, "bridge branch try <n>") });
|
|
5836
|
+
return out(r, () => {
|
|
5837
|
+
for (const p of r.tryInProduct) io.print(`${p.name}: ${p.url ?? p.how}`);
|
|
5838
|
+
io.print(`
|
|
5839
|
+
Share with a product team: ${r.share.install ? r.share.install : r.share.sentence}`);
|
|
5840
|
+
});
|
|
5841
|
+
}
|
|
5842
|
+
case "combine": {
|
|
5843
|
+
const prs = a._.slice(2);
|
|
5844
|
+
const picks = Object.fromEntries(Object.entries(pairs(a.flags.pick, "pick")).map(([k, v]) => [k, branches.branchNumber(v)]));
|
|
5845
|
+
const r = await branches.combineBranches(ctx, { prs, ...Object.keys(picks).length ? { picks } : {} });
|
|
5846
|
+
return out(r, () => {
|
|
5847
|
+
io.print(`${r.tokens} tokens change together.`);
|
|
5848
|
+
for (const c of r.conflicts) io.print(` ${c.key}: pick one of ${c.options.map((o) => `#${o.use}`).join(", ")}${c.picked ? ` (picked #${c.picked})` : ""}`);
|
|
5849
|
+
for (const [k, v] of Object.entries(r.open)) io.print(`${k}: ${v}`);
|
|
5850
|
+
io.print(r.next);
|
|
5851
|
+
});
|
|
5852
|
+
}
|
|
5853
|
+
default:
|
|
5854
|
+
throw new RequestUsageError(`Unknown branch command "${sub}".
|
|
5855
|
+
|
|
5856
|
+
${BRANCH_HELP}`);
|
|
5857
|
+
}
|
|
5858
|
+
}
|
|
5859
|
+
async function runStudioRead(cmd, a, root, io = defaultIO()) {
|
|
5860
|
+
const ctx = await context3(root, io);
|
|
5861
|
+
const json = has3(a, "json");
|
|
5862
|
+
if (cmd === "products") {
|
|
5863
|
+
const r2 = await branches.listProducts(ctx);
|
|
5864
|
+
if (json) return io.print(JSON.stringify(r2, null, 2)), 0;
|
|
5865
|
+
for (const p of r2.products) io.print(`${p.key === r2.current.key ? "*" : " "} ${p.key.padEnd(12)} ${p.name}${p.default ? " (default)" : ""}${p.path ? ` ${p.path}` : ""}${p.repo ? ` ${p.repo}` : ""}`);
|
|
5866
|
+
io.print(`
|
|
5867
|
+
This folder: ${r2.current.name}. Pick another with --product <key>.`);
|
|
5868
|
+
return 0;
|
|
5869
|
+
}
|
|
5870
|
+
if (cmd === "settings") {
|
|
5871
|
+
const r2 = await studio.getSettings(ctx);
|
|
5872
|
+
if (json) return io.print(JSON.stringify(r2, null, 2)), 0;
|
|
5873
|
+
io.print(`Design approval: ${r2.roles.designApproval.join(", ") || "any reviewer"}`);
|
|
5874
|
+
io.print(`Tech approval: ${r2.roles.techApproval.join(", ") || "any reviewer"}`);
|
|
5875
|
+
io.print(`Merge: ${r2.roles.ship}`);
|
|
5876
|
+
io.print(`Owner override: ${r2.ownerOverride.on ? `${r2.ownerOverride.who.join(", ")}. ${r2.ownerOverride.reason}` : r2.ownerOverride.reason}`);
|
|
5877
|
+
io.print(`Protection: ${r2.protection}`);
|
|
5878
|
+
io.print(`Products: ${r2.products.map((p) => p.name).join(", ")}`);
|
|
5879
|
+
return 0;
|
|
5880
|
+
}
|
|
5881
|
+
const r = await studio.figmaCoverage(ctx, a._[1] ? { component: a._[1] } : {});
|
|
5882
|
+
if (json) return io.print(JSON.stringify(r, null, 2)), 0;
|
|
5883
|
+
io.print(r.summary);
|
|
5884
|
+
for (const c of r.components) io.print(` ${c.name.padEnd(20)} ${c.inFigma}${c.why ? ` (${c.why})` : ""}`);
|
|
5885
|
+
if (r.fidelity) io.print(r.fidelity);
|
|
5886
|
+
return 0;
|
|
5887
|
+
}
|
|
5888
|
+
async function runDocsSave(a, root, io = defaultIO()) {
|
|
5889
|
+
const component = a._[2];
|
|
5890
|
+
const file = flag3(a, "file");
|
|
5891
|
+
if (!component || !file) throw new RequestUsageError('Usage: bridge docs save <Component> --file docs.json [--pr <n>] [--title "..."] [--jira DS-42] [--product admin] [--dry-run]');
|
|
5892
|
+
let docs;
|
|
5893
|
+
try {
|
|
5894
|
+
docs = JSON.parse(await readFile6(resolve3(root, file), "utf8"));
|
|
5895
|
+
} catch (e) {
|
|
5896
|
+
throw new RequestUsageError(`${file} is not a readable JSON file: ${e.message}`);
|
|
5897
|
+
}
|
|
5898
|
+
const ctx = await context3(root, io);
|
|
5899
|
+
const pr = flag3(a, "pr");
|
|
5900
|
+
const r = await studio.saveDocs(ctx, {
|
|
5901
|
+
component,
|
|
5902
|
+
docs,
|
|
5903
|
+
...pr ? { pr } : {},
|
|
5904
|
+
...flag3(a, "title") ? { title: flag3(a, "title") } : {},
|
|
5905
|
+
...flag3(a, "notes") ? { notes: flag3(a, "notes") } : {},
|
|
5906
|
+
...flag3(a, "jira") ? { jira: flag3(a, "jira") } : {},
|
|
5907
|
+
...flag3(a, "product") ? { product: flag3(a, "product") } : {},
|
|
5908
|
+
dryRun: has3(a, "dry-run")
|
|
5909
|
+
});
|
|
5910
|
+
if (has3(a, "json")) io.print(JSON.stringify(r, null, 2));
|
|
5911
|
+
else io.print(r.status === "dry-run" || r.status === "preview" ? `${r.said ?? "Dry run, nothing written."}
|
|
5912
|
+
|
|
5913
|
+
${r.path}:
|
|
5914
|
+
${r.content}` : String(r.said));
|
|
5915
|
+
return 0;
|
|
5916
|
+
}
|
|
5917
|
+
|
|
5918
|
+
// src/pieces.ts
|
|
5919
|
+
import { BridgeContext as BridgeContext5, branches as branches2, pieces } from "@bridge-ds/mcp";
|
|
5920
|
+
var PIECES_HELP = `bridge pieces: a product branch and the library
|
|
5921
|
+
|
|
5922
|
+
Usage:
|
|
5923
|
+
bridge pieces <n> [--record] [--json] what the product branch adds that the library lacks (the audit
|
|
5924
|
+
on its diff against main); --record saves it in the branch,
|
|
5925
|
+
where Bridge Studio shows it
|
|
5926
|
+
bridge pieces status <n> [--json] each linked library branch: status, reviewers, preview, next step
|
|
5927
|
+
bridge pieces open <n> <piece> [--no-preview] [--dry-run] [--json]
|
|
5928
|
+
open the library side of a piece, linked both ways: a token
|
|
5929
|
+
change, a library exploration (variant) or a request (component)
|
|
5930
|
+
bridge pieces preview <n> <piece> the product branch takes the latest library branch (merged in)
|
|
5931
|
+
bridge pieces switch <n> once the library shipped: main into the product branch
|
|
5932
|
+
|
|
5933
|
+
GitHub token: GITHUB_TOKEN or BRIDGE_GITHUB_TOKEN, else \`gh auth token\`. Exit codes: 0 ok, 2 usage or setup error.`;
|
|
5934
|
+
var has4 = (a, k) => Boolean(a.flags[k]);
|
|
5935
|
+
async function context4(root, io) {
|
|
5936
|
+
const token = io.env.BRIDGE_GITHUB_TOKEN || io.env.GITHUB_TOKEN || io.ghToken();
|
|
5937
|
+
return BridgeContext5.create({ root, ...token ? { token } : {}, ...io.fetch ? { fetch: io.fetch } : {} });
|
|
5938
|
+
}
|
|
5939
|
+
function number2(a, at, usage) {
|
|
5940
|
+
const raw = a._[at];
|
|
5941
|
+
if (!raw) throw new RequestUsageError(`Usage: ${usage}`);
|
|
5942
|
+
return branches2.branchNumber(raw);
|
|
5943
|
+
}
|
|
5944
|
+
function piece(a, at, usage) {
|
|
5945
|
+
const raw = a._[at];
|
|
5946
|
+
if (!raw) throw new RequestUsageError(`Usage: ${usage}. \`bridge pieces <n>\` lists the piece ids.`);
|
|
5947
|
+
return raw;
|
|
5948
|
+
}
|
|
5949
|
+
async function runPieces(a, root, io = defaultIO()) {
|
|
5950
|
+
const sub = a._[1];
|
|
5951
|
+
if (!sub || sub === "help" || has4(a, "help")) {
|
|
5952
|
+
io.print(PIECES_HELP);
|
|
5953
|
+
return sub || has4(a, "help") ? 0 : 2;
|
|
5954
|
+
}
|
|
5955
|
+
const json = has4(a, "json");
|
|
5956
|
+
const ctx = await context4(root, io);
|
|
5957
|
+
const out = (r, text) => (json ? io.print(JSON.stringify(r, null, 2)) : text(), 0);
|
|
5958
|
+
switch (sub) {
|
|
5959
|
+
case "status": {
|
|
5960
|
+
const r = await pieces.productBranchOp(ctx, { pr: number2(a, 2, "bridge pieces status <n>") });
|
|
5961
|
+
return out(r, () => {
|
|
5962
|
+
io.print(`#${r.pr} ${r.title} (${r.product})`);
|
|
5963
|
+
io.print(r.said);
|
|
5964
|
+
for (const l of r.linked) io.print(` ${l.piece}: #${l.library.number} ${l.status}. ${l.preview}${l.action ? ` Next: ${l.action}.` : ""}`);
|
|
5965
|
+
for (const p of r.notLinked) io.print(` ${p.title} (${p.id}): no library branch yet. bridge pieces open ${r.pr} ${p.id}`);
|
|
5966
|
+
if (r.next) io.print(r.next);
|
|
5967
|
+
});
|
|
5968
|
+
}
|
|
5969
|
+
case "open": {
|
|
5970
|
+
const usage = "bridge pieces open <n> <piece>";
|
|
5971
|
+
const r = await pieces.openLibraryBranchOp(ctx, { pr: number2(a, 2, usage), piece: piece(a, 3, usage), usePreview: !has4(a, "no-preview"), dryRun: has4(a, "dry-run"), source: "cli" });
|
|
5972
|
+
return out(r, () => {
|
|
5973
|
+
io.print(`${r.status === "dry-run" ? "Would open" : r.status === "already-linked" ? "Already linked" : "Opened"} a ${r.opens}: ${r.title}, needed by ${r.neededBy}.`);
|
|
5974
|
+
if (r.library) io.print(` #${r.library.number} ${r.library.url}`);
|
|
5975
|
+
if (r.preview) io.print(` ${r.preview.said}`);
|
|
5976
|
+
for (const w of r.warnings ?? []) io.print(` ${w}`);
|
|
5977
|
+
io.print(`Next: ${r.next}`);
|
|
5978
|
+
});
|
|
5979
|
+
}
|
|
5980
|
+
case "preview": {
|
|
5981
|
+
const usage = "bridge pieces preview <n> <piece>";
|
|
5982
|
+
const r = await pieces.useLibraryPreviewOp(ctx, { pr: number2(a, 2, usage), piece: piece(a, 3, usage) });
|
|
5983
|
+
return out(r, () => io.print(r.said));
|
|
5984
|
+
}
|
|
5985
|
+
case "switch": {
|
|
5986
|
+
const r = await pieces.switchToPublishedOp(ctx, { pr: number2(a, 2, "bridge pieces switch <n>") });
|
|
5987
|
+
return out(r, () => io.print(r.said));
|
|
5988
|
+
}
|
|
5989
|
+
default: {
|
|
5990
|
+
const r = await pieces.missingPiecesOp(ctx, { pr: number2(a, 1, "bridge pieces <n> [--record]"), record: has4(a, "record") });
|
|
5991
|
+
return out(r, () => {
|
|
5992
|
+
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.`);
|
|
5993
|
+
for (const p of r.pieces) io.print(` ${p.id} ${p.title}: ${p.summary} Opens ${p.opens}.${"linked" in p ? ` Linked: #${p.linked}.` : ""}`);
|
|
5994
|
+
if (r.fixes.count) io.print(`${r.fixes.count} value${r.fixes.count === 1 ? "" : "s"} the library already has (use them instead):
|
|
5995
|
+
${r.fixes.sample.map((s) => ` ${s}`).join("\n")}`);
|
|
5996
|
+
if (r.skipped) io.print(r.skipped);
|
|
5997
|
+
io.print(`Next: ${r.next}`);
|
|
5998
|
+
});
|
|
5999
|
+
}
|
|
6000
|
+
}
|
|
6001
|
+
}
|
|
6002
|
+
|
|
5544
6003
|
// src/protection.ts
|
|
5545
6004
|
var UNKNOWN = { kind: "unknown", missing: ["review", "checks", "history", "deletion"] };
|
|
5546
6005
|
async function protectionReport(opts) {
|
|
@@ -5573,7 +6032,7 @@ async function rolesReport(opts) {
|
|
|
5573
6032
|
}
|
|
5574
6033
|
|
|
5575
6034
|
// src/index.ts
|
|
5576
|
-
import { brand, BridgeContext as
|
|
6035
|
+
import { brand, BridgeContext as BridgeContext6, CONFIG_FILE as CONFIG_FILE2, core, detectConfig, GitRefSource, history as hist, LocalSource, ops, optionsFromArgv, serveStdio, toConfigJson, VERSION } from "@bridge-ds/mcp";
|
|
5577
6036
|
var MCP_PACKAGE = "@bridge-ds/mcp";
|
|
5578
6037
|
var HELP = `bridge ${VERSION}: design system as code, for you and your Claude
|
|
5579
6038
|
|
|
@@ -5588,9 +6047,13 @@ Usage:
|
|
|
5588
6047
|
bridge tokens <query> [--json] search tokens by name, description or value
|
|
5589
6048
|
bridge diff <base> [--json|--md] designer-readable token diff: working tree vs <base> (git ref)
|
|
5590
6049
|
bridge propose --title "..." --set path=value [--set ...] [--file f | --mode theme=dark]
|
|
5591
|
-
[--edits edits.json] [--summary "..."] [--branch explore/<name>] [--
|
|
6050
|
+
[--edits edits.json] [--summary "..."] [--branch explore/<name>] [--pr <n>]
|
|
6051
|
+
[--product <key>] [--jira DS-42] [--dry-run]
|
|
5592
6052
|
validate + contrast + new local bridge/* branch (never pushes).
|
|
5593
|
-
--branch: write the edits to the checked-out exploration instead
|
|
6053
|
+
--branch: write the edits to the checked-out exploration instead.
|
|
6054
|
+
--pr: add them to that branch in review (GitHub token).
|
|
6055
|
+
--product: the product it is for (bridge products); --jira: the
|
|
6056
|
+
ticket key, first in the branch name and title
|
|
5594
6057
|
bridge brand list [--json] brands: slug, name, default, primary action color, theme overrides
|
|
5595
6058
|
bridge brand create <slug> --from bridge --primary <hex|hue> --neutral <hex|hue>
|
|
5596
6059
|
[--name "Acme"] [--accent x] [--link x] [--radius soft|balanced|crisp|i,c,co,o]
|
|
@@ -5646,6 +6109,20 @@ Usage:
|
|
|
5646
6109
|
explorations: an explore/<name> branch with a draft pull request
|
|
5647
6110
|
labeled exploration, previews and a snapshot package for a product
|
|
5648
6111
|
staging branch. Never merged as is: promote it to review or close it
|
|
6112
|
+
bridge branch list|status|comments|ready|draft|review|ask|comment|merge|close|reopen|update|try|combine
|
|
6113
|
+
branches in review, the same actions as Bridge Studio
|
|
6114
|
+
(\`bridge branch help\`): what needs me, send for review, review,
|
|
6115
|
+
merge through the same gate (owner override with a reason), update
|
|
6116
|
+
from main with token conflicts, try it in a product, try together
|
|
6117
|
+
bridge pieces <n> [--record] | status <n> | open <n> <piece> | preview <n> <piece> | switch <n>
|
|
6118
|
+
a product branch and the library (\`bridge pieces help\`): what it adds
|
|
6119
|
+
that the library lacks, the library branch each piece opens (linked
|
|
6120
|
+
both ways), its preview in the product branch, the published version
|
|
6121
|
+
bridge products | bridge settings | bridge coverage [Component]
|
|
6122
|
+
products (and this folder's), Studio settings safe to read, which
|
|
6123
|
+
components are in Figma
|
|
6124
|
+
bridge docs save <Component> --file docs.json [--pr n] [--jira DS-42] [--product key] [--dry-run]
|
|
6125
|
+
save usage guidance as in the Library edit mode (GitHub token)
|
|
5649
6126
|
bridge mcp [--root dir | --repo owner/name] start the MCP server on stdio
|
|
5650
6127
|
|
|
5651
6128
|
Options:
|
|
@@ -5662,23 +6139,23 @@ function parse(argv) {
|
|
|
5662
6139
|
const [k, inline] = a.slice(2).split(/=(.*)/s, 2);
|
|
5663
6140
|
const next = argv[i + 1];
|
|
5664
6141
|
const fileArg = k === "json" && /\.json$/i.test(next ?? "") || k === "md" && /\.(md|markdown)$/i.test(next ?? "");
|
|
5665
|
-
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));
|
|
6142
|
+
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", "approve"].includes(k));
|
|
5666
6143
|
(out.flags[k] ??= []).push(inline ?? (takesValue ? argv[++i] : "true"));
|
|
5667
6144
|
} else out._.push(a);
|
|
5668
6145
|
}
|
|
5669
6146
|
return out;
|
|
5670
6147
|
}
|
|
5671
|
-
var
|
|
5672
|
-
var
|
|
6148
|
+
var flag4 = (a, k) => a.flags[k]?.[a.flags[k].length - 1];
|
|
6149
|
+
var has5 = (a, k) => Boolean(a.flags[k]);
|
|
5673
6150
|
var print = (s) => process.stdout.write(`${s}
|
|
5674
6151
|
`);
|
|
5675
6152
|
var modeArg = (s) => s ? Object.fromEntries(s.split(",").map((kv) => kv.split("="))) : void 0;
|
|
5676
6153
|
async function init(a, root) {
|
|
5677
6154
|
const cfgPath = join6(root, CONFIG_FILE2);
|
|
5678
6155
|
let config;
|
|
5679
|
-
if (existsSync6(cfgPath) && !
|
|
6156
|
+
if (existsSync6(cfgPath) && !has5(a, "force")) {
|
|
5680
6157
|
print(`${CONFIG_FILE2} exists, keeping it (--force to regenerate).`);
|
|
5681
|
-
config = (await (await
|
|
6158
|
+
config = (await (await BridgeContext6.create({ root })).config()).config;
|
|
5682
6159
|
} else {
|
|
5683
6160
|
const src = new LocalSource(root);
|
|
5684
6161
|
const files = await src.list("");
|
|
@@ -5694,7 +6171,7 @@ async function init(a, root) {
|
|
|
5694
6171
|
const inRepo = !relServer.startsWith("..") && !isAbsolute(relServer);
|
|
5695
6172
|
const server = (dir) => inRepo ? { command: "node", args: [relServer, "--root", dir] } : { command: "npx", args: ["-y", MCP_PACKAGE, "--root", dir] };
|
|
5696
6173
|
const mcpPath = join6(root, ".mcp.json");
|
|
5697
|
-
const existing = existsSync6(mcpPath) ? JSON.parse(await
|
|
6174
|
+
const existing = existsSync6(mcpPath) ? JSON.parse(await readFile7(mcpPath, "utf8")) : {};
|
|
5698
6175
|
existing.mcpServers = { ...existing.mcpServers ?? {}, bridge: server(".") };
|
|
5699
6176
|
await writeFile5(mcpPath, `${JSON.stringify(existing, null, 2)}
|
|
5700
6177
|
`);
|
|
@@ -5706,14 +6183,14 @@ async function init(a, root) {
|
|
|
5706
6183
|
print(` claude mcp add bridge -- ${desktop.command} ${desktop.args.join(" ")}`);
|
|
5707
6184
|
}
|
|
5708
6185
|
async function check(a, root) {
|
|
5709
|
-
const ctx = await
|
|
6186
|
+
const ctx = await BridgeContext6.create({ root });
|
|
5710
6187
|
const v = await ops.validateTokens(ctx, { includeWarnings: true });
|
|
5711
6188
|
const c = await ops.checkContrast(ctx, {});
|
|
5712
6189
|
const warnings = "warnings" in v ? v.warnings ?? [] : [];
|
|
5713
|
-
const failed = !v.valid || "failing" in c && (c.failing ?? 0) > 0 ||
|
|
6190
|
+
const failed = !v.valid || "failing" in c && (c.failing ?? 0) > 0 || has5(a, "strict") && warnings.length > 0;
|
|
5714
6191
|
const protection = await protectionOf(a, root, ctx);
|
|
5715
6192
|
const roles = await rolesOf(a, root, ctx);
|
|
5716
|
-
if (
|
|
6193
|
+
if (has5(a, "json")) {
|
|
5717
6194
|
print(JSON.stringify({ ok: !failed, validation: v, contrast: c, ...protection ? { protection: protection.state } : {}, ...roles?.teams.length ? { teams: roles.teams } : {} }, null, 2));
|
|
5718
6195
|
return failed ? 1 : 0;
|
|
5719
6196
|
}
|
|
@@ -5732,7 +6209,7 @@ async function check(a, root) {
|
|
|
5732
6209
|
return failed ? 1 : 0;
|
|
5733
6210
|
}
|
|
5734
6211
|
async function protectionOf(a, root, ctx) {
|
|
5735
|
-
if (
|
|
6212
|
+
if (has5(a, "offline") || process.env.BRIDGE_OFFLINE === "1") return void 0;
|
|
5736
6213
|
const { config } = await ctx.config();
|
|
5737
6214
|
const repo = config.repo ? `${config.repo.owner}/${config.repo.name}` : await originRepo(root);
|
|
5738
6215
|
if (!repo) return void 0;
|
|
@@ -5740,7 +6217,7 @@ async function protectionOf(a, root, ctx) {
|
|
|
5740
6217
|
return protectionReport({ repo, defaultBranch: config.repo?.defaultBranch ?? "main", token });
|
|
5741
6218
|
}
|
|
5742
6219
|
async function rolesOf(a, root, ctx) {
|
|
5743
|
-
if (
|
|
6220
|
+
if (has5(a, "offline") || process.env.BRIDGE_OFFLINE === "1") return void 0;
|
|
5744
6221
|
const { config } = await ctx.config();
|
|
5745
6222
|
const repo = config.repo ? `${config.repo.owner}/${config.repo.name}` : await originRepo(root);
|
|
5746
6223
|
if (!repo) return void 0;
|
|
@@ -5750,9 +6227,9 @@ async function rolesOf(a, root, ctx) {
|
|
|
5750
6227
|
async function tokens(a, root) {
|
|
5751
6228
|
const q = a._.slice(1).join(" ");
|
|
5752
6229
|
if (!q) throw new UsageError("bridge tokens <query>");
|
|
5753
|
-
const ctx = await
|
|
5754
|
-
const r = await ops.searchTokens(ctx, { query: q, limit: Number(
|
|
5755
|
-
if (
|
|
6230
|
+
const ctx = await BridgeContext6.create({ root });
|
|
6231
|
+
const r = await ops.searchTokens(ctx, { query: q, limit: Number(flag4(a, "limit") ?? 20) });
|
|
6232
|
+
if (has5(a, "json")) return print(JSON.stringify(r, null, 2));
|
|
5756
6233
|
if (!r.results.length) return print(`No token matches "${q}".`);
|
|
5757
6234
|
const w = Math.max(...r.results.map((x) => x.path.length));
|
|
5758
6235
|
for (const t of r.results) print(`${t.path.padEnd(w)} ${String(t.value).padEnd(10)} ${(t.tier ?? "").padEnd(9)} ${t.description ?? ""}`);
|
|
@@ -5760,20 +6237,20 @@ async function tokens(a, root) {
|
|
|
5760
6237
|
async function diff(a, root) {
|
|
5761
6238
|
const base = a._[1];
|
|
5762
6239
|
if (!base) throw new UsageError("bridge diff <base> (a git ref: main, HEAD~1, a sha)");
|
|
5763
|
-
const ctx = await
|
|
6240
|
+
const ctx = await BridgeContext6.create({ root });
|
|
5764
6241
|
const { config } = await ctx.config();
|
|
5765
6242
|
const before = await core.loadTokenSet(new GitRefSource(root, base), config);
|
|
5766
6243
|
const after = await ctx.tokens();
|
|
5767
6244
|
const changes = core.diff(before, after);
|
|
5768
|
-
const
|
|
5769
|
-
const delta2 =
|
|
6245
|
+
const pairs2 = await core.readContrastPairs(ctx.source, config).catch(() => []);
|
|
6246
|
+
const delta2 = pairs2.length ? core.contrastDelta(core.runContrast(before, pairs2, config.contrast?.modes), core.runContrast(after, pairs2, config.contrast?.modes)) : [];
|
|
5770
6247
|
const tier = core.classify(changes, [], config);
|
|
5771
6248
|
const imp = core.impact(before, after, changes, 30);
|
|
5772
|
-
if (
|
|
6249
|
+
if (has5(a, "json")) {
|
|
5773
6250
|
print(JSON.stringify({ base, changes, contrast: delta2, tier, impact: imp }, null, 2));
|
|
5774
6251
|
return 0;
|
|
5775
6252
|
}
|
|
5776
|
-
if (
|
|
6253
|
+
if (has5(a, "md")) {
|
|
5777
6254
|
print(`## Token changes since \`${base}\`
|
|
5778
6255
|
|
|
5779
6256
|
${core.tokenTable(changes)}
|
|
@@ -5808,24 +6285,37 @@ ${imp.total} other token(s) change value through aliases:`);
|
|
|
5808
6285
|
return delta2.some((d) => d.passBefore && !d.passAfter) ? 1 : 0;
|
|
5809
6286
|
}
|
|
5810
6287
|
async function propose(a, root) {
|
|
5811
|
-
const title =
|
|
6288
|
+
const title = flag4(a, "title");
|
|
5812
6289
|
if (!title) throw new UsageError('bridge propose --title "Darken muted text" --set color.text.muted={palette.neutral.700} --mode theme=light');
|
|
5813
6290
|
const edits = [];
|
|
5814
|
-
const file =
|
|
5815
|
-
const mode2 = modeArg(
|
|
6291
|
+
const file = flag4(a, "file");
|
|
6292
|
+
const mode2 = modeArg(flag4(a, "mode"));
|
|
5816
6293
|
for (const s of a.flags.set ?? []) {
|
|
5817
6294
|
const i = s.indexOf("=");
|
|
5818
6295
|
if (i < 0) throw new UsageError(`--set needs path=value, got "${s}"`);
|
|
5819
6296
|
edits.push({ op: "set", path: s.slice(0, i), value: s.slice(i + 1), ...file ? { file } : {}, ...mode2 ? { mode: mode2 } : {} });
|
|
5820
6297
|
}
|
|
5821
|
-
const editsFile =
|
|
5822
|
-
if (editsFile) edits.push(...JSON.parse(await
|
|
6298
|
+
const editsFile = flag4(a, "edits");
|
|
6299
|
+
if (editsFile) edits.push(...JSON.parse(await readFile7(resolve4(editsFile), "utf8")));
|
|
5823
6300
|
if (!edits.length) throw new UsageError("Give at least one --set path=value or --edits file.json");
|
|
5824
|
-
const
|
|
5825
|
-
const
|
|
5826
|
-
const
|
|
5827
|
-
const
|
|
5828
|
-
|
|
6301
|
+
const pr = flag4(a, "pr");
|
|
6302
|
+
const token = pr ? process.env.BRIDGE_GITHUB_TOKEN || process.env.GITHUB_TOKEN || defaultIO().ghToken() : void 0;
|
|
6303
|
+
const ctx = await BridgeContext6.create({ root, ...token ? { token } : {} });
|
|
6304
|
+
const summary = flag4(a, "summary");
|
|
6305
|
+
const branch = flag4(a, "branch");
|
|
6306
|
+
const jira = flag4(a, "jira");
|
|
6307
|
+
const product = flag4(a, "product");
|
|
6308
|
+
const r = await ops.proposeTokenChange(ctx, {
|
|
6309
|
+
title,
|
|
6310
|
+
edits,
|
|
6311
|
+
...summary ? { summary } : {},
|
|
6312
|
+
...branch ? { branch } : {},
|
|
6313
|
+
...pr ? { pr } : {},
|
|
6314
|
+
...jira ? { jira } : {},
|
|
6315
|
+
...product ? { product } : {},
|
|
6316
|
+
dryRun: has5(a, "dry-run")
|
|
6317
|
+
});
|
|
6318
|
+
if (has5(a, "json")) print(JSON.stringify(r, null, 2));
|
|
5829
6319
|
else {
|
|
5830
6320
|
print(`status: ${r.status}${r.why ? ` (${r.why})` : ""}`);
|
|
5831
6321
|
if (r.tier) print(`tier ${r.tier}, ${r.bump}`);
|
|
@@ -5840,17 +6330,17 @@ ${r.prBody}`);
|
|
|
5840
6330
|
}
|
|
5841
6331
|
async function brandCmd(a, root) {
|
|
5842
6332
|
const sub = a._[1];
|
|
5843
|
-
const ctx = await
|
|
6333
|
+
const ctx = await BridgeContext6.create({ root });
|
|
5844
6334
|
if (sub === "list") {
|
|
5845
6335
|
const r2 = await brand.listBrands(ctx);
|
|
5846
|
-
if (
|
|
6336
|
+
if (has5(a, "json")) print(JSON.stringify(r2.brands, null, 2));
|
|
5847
6337
|
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(", ")}` : ""}`);
|
|
5848
6338
|
return 0;
|
|
5849
6339
|
}
|
|
5850
6340
|
if (sub !== "create") throw new UsageError('bridge brand list | bridge brand create <slug> --from bridge --primary "#1F5F3A" --neutral "#2A2F2C"');
|
|
5851
6341
|
const slug = a._[2];
|
|
5852
6342
|
if (!slug) throw new UsageError('bridge brand create <slug> --from bridge --primary "#1F5F3A" --neutral "#2A2F2C"');
|
|
5853
|
-
const opt = (k) =>
|
|
6343
|
+
const opt = (k) => flag4(a, k);
|
|
5854
6344
|
const fonts = { ...opt("heading") ? { heading: opt("heading") } : {}, ...opt("text") ? { text: opt("text") } : {} };
|
|
5855
6345
|
const r = await brand.proposeBrand(ctx, {
|
|
5856
6346
|
slug,
|
|
@@ -5863,9 +6353,9 @@ async function brandCmd(a, root) {
|
|
|
5863
6353
|
...opt("radius") ? { radius: opt("radius") } : {},
|
|
5864
6354
|
...Object.keys(fonts).length ? { fonts } : {},
|
|
5865
6355
|
...opt("notes") ? { notes: opt("notes") } : {},
|
|
5866
|
-
dryRun:
|
|
6356
|
+
dryRun: has5(a, "dry-run")
|
|
5867
6357
|
});
|
|
5868
|
-
if (
|
|
6358
|
+
if (has5(a, "json")) {
|
|
5869
6359
|
const { fileContents: _f, ...rest } = r;
|
|
5870
6360
|
print(JSON.stringify(rest, null, 2));
|
|
5871
6361
|
return r.status === "blocked" ? 1 : 0;
|
|
@@ -5886,7 +6376,7 @@ async function brandCmd(a, root) {
|
|
|
5886
6376
|
return r.status === "blocked" ? 1 : 0;
|
|
5887
6377
|
}
|
|
5888
6378
|
async function figmaReferences(a, root) {
|
|
5889
|
-
let sha =
|
|
6379
|
+
let sha = flag4(a, "sha");
|
|
5890
6380
|
if (!sha) {
|
|
5891
6381
|
try {
|
|
5892
6382
|
sha = execFileSync2("git", ["rev-parse", "HEAD"], { cwd: root, encoding: "utf8" }).trim();
|
|
@@ -5894,49 +6384,49 @@ async function figmaReferences(a, root) {
|
|
|
5894
6384
|
sha = "unknown";
|
|
5895
6385
|
}
|
|
5896
6386
|
}
|
|
5897
|
-
const only = (
|
|
6387
|
+
const only = (flag4(a, "only") ?? "").split(",").filter(Boolean);
|
|
5898
6388
|
const r = await captureFigmaReferences({
|
|
5899
|
-
storybook:
|
|
5900
|
-
specs:
|
|
5901
|
-
out:
|
|
6389
|
+
storybook: resolve4(root, flag4(a, "storybook") ?? "storybook-static"),
|
|
6390
|
+
specs: resolve4(root, flag4(a, "specs") ?? "packages/react/component-specs.json"),
|
|
6391
|
+
out: resolve4(root, flag4(a, "out") ?? "figma-references"),
|
|
5902
6392
|
sha,
|
|
5903
6393
|
...only.length ? { only } : {},
|
|
5904
|
-
log:
|
|
6394
|
+
log: has5(a, "json") ? void 0 : (line) => print(line)
|
|
5905
6395
|
});
|
|
5906
|
-
if (
|
|
6396
|
+
if (has5(a, "json")) print(JSON.stringify({ manifest: r.manifestPath, captured: r.captured, skipped: r.skipped }, null, 2));
|
|
5907
6397
|
return 0;
|
|
5908
6398
|
}
|
|
5909
6399
|
async function impact(a, root) {
|
|
5910
|
-
const only = (
|
|
6400
|
+
const only = (flag4(a, "only") ?? "").split(",").filter(Boolean);
|
|
5911
6401
|
for (const x of only) if (x !== "stories" && x !== "app") throw new UsageError(`--only takes stories or app, got "${x}"`);
|
|
5912
|
-
const num2 = (k) =>
|
|
6402
|
+
const num2 = (k) => flag4(a, k) !== void 0 ? Number(flag4(a, k)) : void 0;
|
|
5913
6403
|
const threshold = num2("threshold");
|
|
5914
6404
|
if (threshold !== void 0 && !(threshold >= 0 && threshold <= 1)) throw new UsageError("--threshold is between 0 and 1 (pixelmatch color distance)");
|
|
5915
|
-
const imageBase =
|
|
5916
|
-
const artifactUrl =
|
|
5917
|
-
const out =
|
|
5918
|
-
const cacheDir =
|
|
6405
|
+
const imageBase = flag4(a, "image-base");
|
|
6406
|
+
const artifactUrl = flag4(a, "artifact-url");
|
|
6407
|
+
const out = flag4(a, "out");
|
|
6408
|
+
const cacheDir = flag4(a, "cache");
|
|
5919
6409
|
const maxStories = num2("max-stories");
|
|
5920
6410
|
const r = await runImpact({
|
|
5921
6411
|
root,
|
|
5922
|
-
base:
|
|
5923
|
-
head:
|
|
5924
|
-
...out ? { out:
|
|
6412
|
+
base: flag4(a, "base") ?? "main",
|
|
6413
|
+
head: flag4(a, "head") ?? "HEAD",
|
|
6414
|
+
...out ? { out: resolve4(out) } : {},
|
|
5925
6415
|
...only.length ? { only } : {},
|
|
5926
6416
|
...threshold !== void 0 ? { threshold } : {},
|
|
5927
|
-
...cacheDir ? { cacheDir:
|
|
6417
|
+
...cacheDir ? { cacheDir: resolve4(cacheDir) } : {},
|
|
5928
6418
|
...maxStories ? { maxStoriesPerComponent: maxStories } : {},
|
|
5929
6419
|
...imageBase !== void 0 ? { imageBase } : {},
|
|
5930
6420
|
...artifactUrl ? { artifactUrl } : {},
|
|
5931
|
-
plan:
|
|
5932
|
-
keepUnchanged:
|
|
6421
|
+
plan: has5(a, "plan"),
|
|
6422
|
+
keepUnchanged: has5(a, "keep-unchanged")
|
|
5933
6423
|
});
|
|
5934
|
-
if (
|
|
6424
|
+
if (has5(a, "json")) {
|
|
5935
6425
|
print(JSON.stringify({ summary: r.summary, manifest: r.manifestPath, comment: r.commentPath ?? null, ...r.manifest.summary, selection: r.manifest.selection }, null, 2));
|
|
5936
6426
|
return 0;
|
|
5937
6427
|
}
|
|
5938
6428
|
const sel = r.manifest.selection;
|
|
5939
|
-
if (
|
|
6429
|
+
if (has5(a, "plan")) {
|
|
5940
6430
|
print(`Plan ${r.manifest.base.ref} (${r.manifest.base.sha.slice(0, 7)}) -> ${r.manifest.head.ref} (${r.manifest.head.sha.slice(0, 7)})`);
|
|
5941
6431
|
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(", ")})` : ""}`);
|
|
5942
6432
|
print(` components: ${sel.components.map((c) => c.id).join(", ") || "none"}`);
|
|
@@ -5952,12 +6442,12 @@ async function impact(a, root) {
|
|
|
5952
6442
|
}
|
|
5953
6443
|
var day = (iso) => iso.slice(0, 10);
|
|
5954
6444
|
async function historyCmd(a, root) {
|
|
5955
|
-
const ctx = await
|
|
6445
|
+
const ctx = await BridgeContext6.create({ root });
|
|
5956
6446
|
const token = a._[1];
|
|
5957
|
-
const limit = Number(
|
|
6447
|
+
const limit = Number(flag4(a, "limit") ?? 20);
|
|
5958
6448
|
if (token) {
|
|
5959
6449
|
const r2 = await hist.getTokenHistory(ctx, { path: token, limit });
|
|
5960
|
-
if (
|
|
6450
|
+
if (has5(a, "json")) return print(JSON.stringify(r2, null, 2)), 0;
|
|
5961
6451
|
if (!r2.total) return print(`No recorded change of ${r2.path} in ${r2.source}.`), 0;
|
|
5962
6452
|
print(`${r2.path}: ${r2.total} change(s) (${r2.source})
|
|
5963
6453
|
`);
|
|
@@ -5968,9 +6458,9 @@ async function historyCmd(a, root) {
|
|
|
5968
6458
|
}
|
|
5969
6459
|
return 0;
|
|
5970
6460
|
}
|
|
5971
|
-
const tier =
|
|
5972
|
-
const r = await hist.getHistory(ctx, { limit, ...
|
|
5973
|
-
if (
|
|
6461
|
+
const tier = flag4(a, "tier");
|
|
6462
|
+
const r = await hist.getHistory(ctx, { limit, ...flag4(a, "author") ? { author: flag4(a, "author") } : {}, ...tier ? { tier: Number(tier) } : {}, ...flag4(a, "version") ? { version: flag4(a, "version") } : {}, ...has5(a, "breaking") ? { breaking: true } : {} });
|
|
6463
|
+
if (has5(a, "json")) return print(JSON.stringify(r, null, 2)), 0;
|
|
5974
6464
|
print(`${r.source}${r.currentVersion ? `, package version ${r.currentVersion}` : ""}`);
|
|
5975
6465
|
if (!r.total) return print(r.note ?? "No token change matches."), 0;
|
|
5976
6466
|
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" : ""}`);
|
|
@@ -5988,10 +6478,10 @@ ${r.truncated}`);
|
|
|
5988
6478
|
async function revertCmd(a, root) {
|
|
5989
6479
|
const target = a._[1];
|
|
5990
6480
|
if (!target) throw new UsageError("bridge revert <pr|commit|id> [--dry-run] (ids from `bridge history`)");
|
|
5991
|
-
const ctx = await
|
|
6481
|
+
const ctx = await BridgeContext6.create({ root });
|
|
5992
6482
|
const resolutions = Object.fromEntries((a.flags["use-before"] ?? []).map((k) => [k, "use-before"]));
|
|
5993
|
-
const r = await hist.proposeRevert(ctx, { target, resolutions, restoreConflicts:
|
|
5994
|
-
if (
|
|
6483
|
+
const r = await hist.proposeRevert(ctx, { target, resolutions, restoreConflicts: has5(a, "restore-conflicts"), dryRun: has5(a, "dry-run") });
|
|
6484
|
+
if (has5(a, "json")) {
|
|
5995
6485
|
print(JSON.stringify(r, null, 2));
|
|
5996
6486
|
return r.status === "blocked" ? 1 : 0;
|
|
5997
6487
|
}
|
|
@@ -6016,25 +6506,25 @@ async function audit(a, root) {
|
|
|
6016
6506
|
const target = a._[1];
|
|
6017
6507
|
if (!target) throw new UsageError("bridge audit <path> (a product folder or file that uses the design system)");
|
|
6018
6508
|
if (!existsSync6(target)) throw new UsageError(`No such file or folder: ${target}`);
|
|
6019
|
-
const dsRoot =
|
|
6509
|
+
const dsRoot = flag4(a, "ds") ? resolve4(flag4(a, "ds")) : existsSync6(join6(root, CONFIG_FILE2)) ? root : findDesignSystemRoot(root);
|
|
6020
6510
|
if (!dsRoot) throw new UsageError(`No ${CONFIG_FILE2} found: run bridge audit from the design system repo or pass --ds <dir>.`);
|
|
6021
6511
|
const ds = loadDesignSystem(dsRoot);
|
|
6022
|
-
if (
|
|
6023
|
-
const write =
|
|
6512
|
+
if (has5(a, "fix")) {
|
|
6513
|
+
const write = has5(a, "write");
|
|
6024
6514
|
const changes = fixCss(target, ds.index, write);
|
|
6025
6515
|
const n = changes.reduce((acc, c) => acc + c.edits.length, 0);
|
|
6026
6516
|
for (const c of changes) for (const e of c.edits) print(`${c.file}:${e.line}:${e.column} ${e.from} -> ${e.to}`);
|
|
6027
6517
|
print(n ? `${write ? "Replaced" : "Would replace"} ${n} value(s) in ${changes.length} file(s) with exact token matches.${write ? "" : " Run again with --write to apply."}` : "Nothing to fix: no CSS value equals a token exactly.");
|
|
6028
6518
|
return 0;
|
|
6029
6519
|
}
|
|
6030
|
-
const baselinePath =
|
|
6520
|
+
const baselinePath = flag4(a, "baseline");
|
|
6031
6521
|
let baseline;
|
|
6032
6522
|
if (baselinePath) {
|
|
6033
6523
|
if (!existsSync6(baselinePath)) throw new UsageError(`Baseline not found: ${baselinePath}`);
|
|
6034
|
-
baseline = JSON.parse(await
|
|
6524
|
+
baseline = JSON.parse(await readFile7(baselinePath, "utf8"));
|
|
6035
6525
|
}
|
|
6036
|
-
const label =
|
|
6037
|
-
const allowlistPath =
|
|
6526
|
+
const label = flag4(a, "label");
|
|
6527
|
+
const allowlistPath = flag4(a, "allowlist");
|
|
6038
6528
|
if (allowlistPath && !existsSync6(allowlistPath)) throw new UsageError(`Allowlist not found: ${allowlistPath}`);
|
|
6039
6529
|
let allowlist;
|
|
6040
6530
|
try {
|
|
@@ -6044,8 +6534,8 @@ async function audit(a, root) {
|
|
|
6044
6534
|
}
|
|
6045
6535
|
const report = runAudit(target, { ds: dsRoot, version: VERSION, ...label ? { label } : {}, ...baseline ? { baseline } : {}, ...allowlist ? { allowlist } : {} });
|
|
6046
6536
|
const md = auditMarkdown(report);
|
|
6047
|
-
const jsonOut =
|
|
6048
|
-
const mdOut =
|
|
6537
|
+
const jsonOut = flag4(a, "json");
|
|
6538
|
+
const mdOut = flag4(a, "md");
|
|
6049
6539
|
if (jsonOut && jsonOut !== "true") await writeFile5(jsonOut, `${JSON.stringify(report, null, 2)}
|
|
6050
6540
|
`);
|
|
6051
6541
|
if (mdOut && mdOut !== "true") await writeFile5(mdOut, `${md}
|
|
@@ -6085,7 +6575,7 @@ async function audit(a, root) {
|
|
|
6085
6575
|
for (const f of report.files) for (const e of f.ignoreErrors ?? []) process.stderr.write(`${report.target.path}/${f.path}:${e.line}: ${e.message}
|
|
6086
6576
|
`);
|
|
6087
6577
|
let code = 0;
|
|
6088
|
-
const failUnder =
|
|
6578
|
+
const failUnder = flag4(a, "fail-under");
|
|
6089
6579
|
if (failUnder !== void 0) {
|
|
6090
6580
|
const min = Number(failUnder);
|
|
6091
6581
|
if (!Number.isFinite(min)) throw new UsageError("--fail-under needs a number, e.g. --fail-under 80");
|
|
@@ -6096,7 +6586,7 @@ async function audit(a, root) {
|
|
|
6096
6586
|
code = 1;
|
|
6097
6587
|
}
|
|
6098
6588
|
}
|
|
6099
|
-
if (
|
|
6589
|
+
if (has5(a, "no-regress") && report.baseline) {
|
|
6100
6590
|
const b = report.baseline;
|
|
6101
6591
|
if ((b.tokenAdoption.delta ?? 0) < 0 || (b.componentAdoption.delta ?? 0) < 0 || b.hardcoded.delta > 0) {
|
|
6102
6592
|
process.stderr.write("bridge audit: adoption regressed since the baseline\n");
|
|
@@ -6108,9 +6598,9 @@ async function audit(a, root) {
|
|
|
6108
6598
|
async function docsCmd(a, root) {
|
|
6109
6599
|
const [, sub, name] = a._;
|
|
6110
6600
|
if (sub !== "draft" || !name) throw new UsageError("Usage: bridge docs draft <Component> [--json]");
|
|
6111
|
-
const ctx = await
|
|
6601
|
+
const ctx = await BridgeContext6.create({ root });
|
|
6112
6602
|
const d = await ops.draftDocsOp(ctx, { name });
|
|
6113
|
-
if (
|
|
6603
|
+
if (has5(a, "json")) {
|
|
6114
6604
|
print(JSON.stringify({ component: d.component, path: d.path, exists: d.exists, prefill: d.prefill, prompt: d.prompt }, null, 2));
|
|
6115
6605
|
return 0;
|
|
6116
6606
|
}
|
|
@@ -6141,12 +6631,16 @@ async function main() {
|
|
|
6141
6631
|
const argv = process.argv.slice(2);
|
|
6142
6632
|
const a = parse(argv);
|
|
6143
6633
|
const cmd = a._[0];
|
|
6144
|
-
const root =
|
|
6634
|
+
const root = resolve4(flag4(a, "root") ?? process.cwd());
|
|
6145
6635
|
if (cmd === "request") return runRequest(a, root);
|
|
6146
6636
|
if (cmd === "explore") return runExplore(a, root);
|
|
6147
|
-
if (
|
|
6637
|
+
if (cmd === "branch") return runBranch(a, root);
|
|
6638
|
+
if (cmd === "pieces") return runPieces(a, root);
|
|
6639
|
+
if (cmd === "products" || cmd === "settings" || cmd === "coverage") return runStudioRead(cmd, a, root);
|
|
6640
|
+
if (cmd === "docs" && a._[1] === "save") return runDocsSave(a, root);
|
|
6641
|
+
if (!cmd || has5(a, "help") || cmd === "help") {
|
|
6148
6642
|
print(HELP);
|
|
6149
|
-
return cmd ||
|
|
6643
|
+
return cmd || has5(a, "help") ? 0 : 2;
|
|
6150
6644
|
}
|
|
6151
6645
|
if (cmd === "--version" || cmd === "version") return print(VERSION), 0;
|
|
6152
6646
|
switch (cmd) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bridge-ds/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.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.3.
|
|
46
|
+
"@bridge-ds/mcp": "^0.5.0",
|
|
47
|
+
"@bridge-ds/visual": "^0.3.3",
|
|
48
48
|
"typescript": "5.9.3"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|