@bezel-labs/bezel-kit 0.9.0 → 0.10.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/README.md +26 -8
- package/dist/agent-skill.cjs +5 -5
- package/dist/agent-skill.cjs.map +1 -1
- package/dist/agent-skill.js +1 -1
- package/dist/{chunk-DXZXDPQ6.js → chunk-N57FDE2K.js} +118 -1
- package/dist/chunk-N57FDE2K.js.map +1 -0
- package/dist/chunk-NCIAL4MY.js +17 -0
- package/dist/chunk-NCIAL4MY.js.map +1 -0
- package/dist/chunk-U3AGS46P.js +444 -0
- package/dist/chunk-U3AGS46P.js.map +1 -0
- package/dist/cli.cjs +430 -67
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +84 -5
- package/dist/cli.js.map +1 -1
- package/dist/{index-Bme8IB4Z.d.cts → index-CA8VoEc5.d.cts} +79 -1
- package/dist/{index-BF0AyEbO.d.ts → index-HiFqW1Ju.d.ts} +79 -1
- package/dist/index.cjs +135 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +26 -4
- package/dist/node.cjs +383 -42
- package/dist/node.cjs.map +1 -1
- package/dist/node.d.cts +53 -9
- package/dist/node.d.ts +53 -9
- package/dist/node.js +36 -6
- package/package.json +2 -1
- package/dist/chunk-ACUFL3QJ.js +0 -238
- package/dist/chunk-ACUFL3QJ.js.map +0 -1
- package/dist/chunk-DXZXDPQ6.js.map +0 -1
- package/dist/chunk-WQRF54FG.js +0 -17
- package/dist/chunk-WQRF54FG.js.map +0 -1
package/dist/cli.cjs
CHANGED
|
@@ -24,15 +24,15 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
24
24
|
));
|
|
25
25
|
|
|
26
26
|
// src/cli.ts
|
|
27
|
-
var
|
|
28
|
-
var
|
|
27
|
+
var import_node_fs3 = require("fs");
|
|
28
|
+
var import_promises6 = require("fs/promises");
|
|
29
29
|
var import_node_url = require("url");
|
|
30
30
|
var import_node_util = require("util");
|
|
31
|
-
var
|
|
31
|
+
var import_node_path7 = require("path");
|
|
32
32
|
|
|
33
33
|
// src/node.ts
|
|
34
|
-
var
|
|
35
|
-
var
|
|
34
|
+
var import_promises3 = require("fs/promises");
|
|
35
|
+
var import_node_path4 = require("path");
|
|
36
36
|
|
|
37
37
|
// src/config.ts
|
|
38
38
|
var import_node_path = require("path");
|
|
@@ -462,23 +462,314 @@ function mergeGitignore(existing, entries) {
|
|
|
462
462
|
return mergeManagedBlock(existing, entries, { start: START, end: END });
|
|
463
463
|
}
|
|
464
464
|
|
|
465
|
+
// src/lockfile.ts
|
|
466
|
+
var LOCKFILE_NAME = "design-tokens-lock.json";
|
|
467
|
+
var LOCKFILE_VERSION = 1;
|
|
468
|
+
var SYNC_STAGING_PATH = ".bezel/sync.json";
|
|
469
|
+
function serializeLockfile(lockfile) {
|
|
470
|
+
return `${JSON.stringify(lockfile, null, 2)}
|
|
471
|
+
`;
|
|
472
|
+
}
|
|
473
|
+
function serializeTokensFile(tokens) {
|
|
474
|
+
return `${JSON.stringify(tokens, null, 2)}
|
|
475
|
+
`;
|
|
476
|
+
}
|
|
477
|
+
var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
478
|
+
function parseLockfile(text, name = LOCKFILE_NAME) {
|
|
479
|
+
let parsed;
|
|
480
|
+
try {
|
|
481
|
+
parsed = JSON.parse(text);
|
|
482
|
+
} catch {
|
|
483
|
+
const merged = /^(<<<<<<<|=======|>>>>>>>)/m.test(text);
|
|
484
|
+
return {
|
|
485
|
+
ok: false,
|
|
486
|
+
reason: "invalid-json",
|
|
487
|
+
message: merged ? `${name} has git merge conflict markers. Do not resolve them by hand; sync from Bezel to rewrite it.` : `${name} is not valid JSON. Sync from Bezel to rewrite it.`
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
if (!isRecord(parsed) || parsed["lockfileVersion"] !== LOCKFILE_VERSION || typeof parsed["projectId"] !== "string" || typeof parsed["version"] !== "string" || typeof parsed["revision"] !== "number" || !isRecord(parsed["tokens"])) {
|
|
491
|
+
return {
|
|
492
|
+
ok: false,
|
|
493
|
+
reason: "invalid-shape",
|
|
494
|
+
message: `${name} is not a version ${LOCKFILE_VERSION} Bezel lockfile. Sync from Bezel to rewrite it.`
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
return { ok: true, lockfile: parsed };
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// src/patch-applied.ts
|
|
501
|
+
var MISSING = /* @__PURE__ */ Symbol("missing");
|
|
502
|
+
var decodeSegment = (segment) => segment.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
503
|
+
function valueAt(document, pointer) {
|
|
504
|
+
if (pointer === "") return document;
|
|
505
|
+
let node = document;
|
|
506
|
+
for (const raw of pointer.split("/").slice(1)) {
|
|
507
|
+
const segment = decodeSegment(raw);
|
|
508
|
+
if (Array.isArray(node)) {
|
|
509
|
+
const index = Number(segment);
|
|
510
|
+
if (!Number.isInteger(index) || index < 0 || index >= node.length) return MISSING;
|
|
511
|
+
node = node[index];
|
|
512
|
+
} else if (typeof node === "object" && node !== null && Object.prototype.hasOwnProperty.call(node, segment)) {
|
|
513
|
+
node = node[segment];
|
|
514
|
+
} else {
|
|
515
|
+
return MISSING;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
return node;
|
|
519
|
+
}
|
|
520
|
+
function jsonEqual(a, b) {
|
|
521
|
+
if (a === b) return true;
|
|
522
|
+
if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false;
|
|
523
|
+
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
|
524
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
525
|
+
return a.length === b.length && a.every((item, i) => jsonEqual(item, b[i]));
|
|
526
|
+
}
|
|
527
|
+
const aKeys = Object.keys(a);
|
|
528
|
+
const bKeys = Object.keys(b);
|
|
529
|
+
if (aKeys.length !== bKeys.length) return false;
|
|
530
|
+
return aKeys.every(
|
|
531
|
+
(key) => Object.prototype.hasOwnProperty.call(b, key) && jsonEqual(a[key], b[key])
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
function isOperationApplied(document, operation) {
|
|
535
|
+
switch (operation.op) {
|
|
536
|
+
case "test":
|
|
537
|
+
return true;
|
|
538
|
+
case "add":
|
|
539
|
+
case "replace": {
|
|
540
|
+
const current = valueAt(document, operation.path);
|
|
541
|
+
return current !== MISSING && jsonEqual(current, operation.value);
|
|
542
|
+
}
|
|
543
|
+
case "remove":
|
|
544
|
+
return valueAt(document, operation.path) === MISSING;
|
|
545
|
+
case "copy": {
|
|
546
|
+
const target = valueAt(document, operation.path);
|
|
547
|
+
return target !== MISSING && jsonEqual(target, valueAt(document, operation.from ?? ""));
|
|
548
|
+
}
|
|
549
|
+
case "move":
|
|
550
|
+
return valueAt(document, operation.path) !== MISSING && (operation.from === void 0 || valueAt(document, operation.from) === MISSING);
|
|
551
|
+
default:
|
|
552
|
+
return false;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
function unappliedOperations(document, operations) {
|
|
556
|
+
return operations.filter((operation) => operation.op !== "test" && !isOperationApplied(document, operation));
|
|
557
|
+
}
|
|
558
|
+
|
|
465
559
|
// src/agent-skill.ts
|
|
466
560
|
var AGENT_SKILL = {
|
|
467
561
|
name: "bezel",
|
|
468
562
|
description: "Use when the repo has bezel.json or design-tokens.json and the task touches design tokens, theme colors, spacing, typography, radii, CSS variables, dark mode, or files under the Bezel output folder (src/bezel by default). Edit design-tokens.json, never the generated variables.css / contexts.ts / fonts.ts, then run the tokens build. Also covers pulling the latest tokens from the Bezel app, pushing local token edits back to Bezel after a preview and confirmation, and using the Bezel MCP tools.",
|
|
469
563
|
files: {
|
|
470
|
-
"SKILL.md": '---\nname: bezel\ndescription: Use when the repo has bezel.json or design-tokens.json and the task touches design tokens, theme colors, spacing, typography, radii, CSS variables, dark mode, or files under the Bezel output folder (src/bezel by default). Edit design-tokens.json, never the generated variables.css / contexts.ts / fonts.ts, then run the tokens build. Also covers pulling the latest tokens from the Bezel app, pushing local token edits back to Bezel after a preview and confirmation, and using the Bezel MCP tools.\n---\n\n# Bezel in this repository\n\nBezel is a design-token platform. Designers maintain the tokens in the Bezel app; this repo holds an exported copy and generates CSS from it with `@bezel-labs/bezel-kit`.\n\n```\nBezel app (cloud, source of truth)\n -> design-tokens.json DTCG file at the repo root, exported from Bezel\n -> bezel build reads bezel.json for options and output paths\n -> variables.css one CSS scope per context (:root, .dark, .light, ...)\n contexts.ts, fonts.ts optional generated TypeScript modules\n```\n\nThe Bezel app stays the source of truth. Local edits to `design-tokens.json` reach Bezel only when they are pushed through the Bezel MCP, after a preview and the user\'s explicit yes (see "Pushing changes back to Bezel"); until then the next sync replaces them.\n\n## Hard rules\n\n1. Never edit a generated file. They start with `/* Generated by bezel \u2014 do not edit by hand. */` and live at the paths named in `bezel.json` (`variablesOutput`, `contextsOutput`, `fontsOutput`; default folder `src/bezel/`). `bezel build` overwrites them without asking and they are gitignored.\n2. To change a token value or add a variable, edit `design-tokens.json`, then rebuild. Never hand-write the CSS variable.\n3. After any change to `design-tokens.json` or `bezel.json`, run the tokens build: `npm run tokens` when that script exists, otherwise `npx bezel build` (use the repo\'s package manager runner). Confirm the outputs were rewritten before finishing.\n4. Only tokens with an `exportName` become CSS variables unless `bezel.json` sets `"includeAll": true`. Adding a token without an `exportName` produces no variable.\n5. Never pass `--force` to `bezel init`; it discards the existing `bezel.json`. `--project` and `--tokens-version` update their own keys without it.\n6. Local edits to `design-tokens.json` are not in Bezel until they are pushed, and a sync from Bezel overwrites them. Never silently sync over local edits: say so and offer to push them first. Pushing means `preview-design-tokens-push`, showing its `confirmationPrompt` word for word, then `push-design-tokens-file` only after the user says yes. Never push without that confirmation, and never push from a "sync" or "pull" request.\n\n## The tokens file\n\n`design-tokens.json` follows the W3C Design Tokens (DTCG) format. Every token is an object with a `$type` and one `$value` per context:\n\n```json\n"semantic": {\n "color": {\n "primary": {\n "$type": "color",\n "$root": { "$value": "{base.color.green.500}" },\n "dark": { "$value": { "colorSpace": "oklch", "components": [0.85, 0.16, 152] } },\n "$extensions": { "com.bezel.app": { "exportName": ["primary", "ring"] } }\n }\n }\n}\n```\n\n- Context keys (`$root`, `dark`, `light`, or whatever `bezel.json` `contexts` lists) sit inside the token. `$root` is the base; other contexts only need the values that differ.\n- `$value` is either a literal (DTCG color, dimension, font family, ...) or a reference like `{base.color.green.500}`. References are resolved to literal values in the generated CSS.\n- `$extensions["com.bezel.app"].exportName` is the list of CSS variable names the token emits. One token can emit several (`foreground`, `card-foreground`). An empty list means no variable.\n- Groups (`base`, `semantic`, `color`, ...) are plain nesting with no `$type`.\n\n## What to do for common requests\n\n| Request | Edit | Then |\n|---|---|---|\n| Change a color, size, radius, or font value | The token\'s `$value` in the right context key (`$root` for every mode, `dark` for dark mode only) | Rebuild |\n| A value should differ in dark mode | Add or edit the `dark` key on that token | Rebuild |\n| New CSS variable | Add a token with `$type`, `$root.$value`, and an `exportName` (or reference an existing token) | Rebuild |\n| Rename a CSS variable | Change the `exportName` entry; update every consumer of the old `--name` | Rebuild |\n| New theme or brand context | Add the context to `bezel.json` `contexts` (name -> selector) and add that key to the tokens that differ | Rebuild |\n| Change where files are generated | Edit `variablesOutput` / `contextsOutput` / `fontsOutput` in `bezel.json`; update the CSS import | Rebuild, delete the old outputs |\n| "Pull the latest tokens" / "sync from Bezel" | See the next section | Rebuild |\n| "Push my changes to Bezel" / save local token edits back | Nothing locally; follow "Pushing changes back to Bezel" | Write the returned `revision` to `bezel.json`; rebuild only if the file was re-synced |\n\nFind the token for an existing variable by searching `design-tokens.json` for its name inside an `exportName` array.\n\n## Pulling the latest tokens from Bezel\n\n`bezel.json` `projectId` and `version` (`"latest"` or a published semver) say which Bezel project and snapshot this repo tracks. `revision` records the project revision the file was last synced from or pushed to.\n\nWith the Bezel MCP connected: call `get-design-tokens-file` with that `projectId` and `version`, write the returned `content` string to `design-tokens.json` verbatim (no reformatting), set `"revision"` in `bezel.json` to the returned `revision`, then rebuild. The `sync-project` prompt walks through this, including linking a project when `projectId` is missing. Never pick a project or version yourself; ask when it is ambiguous.\n\nWithout the MCP: ask the user to download `design-tokens.json` from the project in the Bezel app and replace the file at the repo root, then rebuild.\n\nBefore overwriting a `design-tokens.json` that has local edits, stop and offer to push them first (next section) or confirm they should be discarded.\n\n## Pushing changes back to Bezel\n\nNeeds the Bezel MCP. Pushing saves this repo\'s `design-tokens.json` as the project\'s working tokens (`latest`), so the Bezel app stays the source of truth and everyone who syncs gets the change. Publishing a version still happens in the Bezel app.\n\n1. Read `projectId`, `version`, and `revision` from `bezel.json`. If `version` is a semver, tell the user the push updates the working project, not that pinned release.\n2. Call `preview-design-tokens-push` with the exact file text as `content`, `revision` as `baseRevision`, and `version` as `trackedVersion`. It saves nothing.\n3. `invalid` or `blocked`: report the errors and stop (`blocked` is a plan limit on groups, themes, or descriptions). `no-changes`: say the project already matches. Theme keys the file uses that the project does not have yet are added to the project by the push, within the plan\'s theme limit; the preview lists them as `addedContexts`.\n4. `ready`: show `confirmationPrompt` to the user word for word and wait. Do not push on silence, a question, or a different request.\n5. After an explicit yes, call `push-design-tokens-file` with the same `content` and `expectedRevision` = the preview\'s `currentRevision`. `conflict` means Bezel changed in between: go back to step 2.\n6. Write the returned `revision` to `bezel.json`. If the result says `normalized`, sync once (`get-design-tokens-file`, write verbatim, rebuild) so the local file matches Bezel byte for byte.\n\nThe `push-project` prompt walks through this.\n\n## With the Bezel MCP connected\n\nPrefer small, targeted calls over reading the whole tokens file:\n\n- `get-project-manifest` first: groups, counts, types, contexts, no values.\n- `query-tokens` to find tokens by group, type, name, or `exportName`; `get-token-by-path` for one token with every context value.\n- `get-token-bundle` for a curated set per task (`theming-setup`, `button`, `card`, `form`, ...).\n- `export-tokens` and `get-framework-overrides` to generate CSS, JS/TS objects, or Tailwind / shadcn / Mantine config from tokens.\n- `contrast-check`, `color-convert`, `color-ramp`, `unit-convert` for deterministic math instead of estimating.\n- `design-review` to check a component description against the tokens.\n\nSee `references/mcp-tools.md` for the full list and `references/bezel-json.md` for every config field.\n\n## Setting up a repo that is not on Bezel yet\n\nRun the `setup-project` tool (or the `/mcp__bezel__setup-project` prompt) and follow it; it installs `@bezel-labs/bezel-kit`, writes `bezel.json`, fetches the tokens, builds, and wires the CSS import. Do not improvise the steps.\n',
|
|
471
|
-
"references/bezel-json.md": '# bezel.json reference\n\n`bezel.json` lives at the repo root (or the working directory `bezel build` runs in). `bezel build` loads it automatically; `bezel init` writes it. The tokens input is always `design-tokens.json` in that same directory and is not configurable.\n\n| Field | Default | Used by | Meaning |\n|---|---|---|---|\n| `projectId` | none | MCP | UUID of the Bezel project this repo tracks. Set with `npx bezel init --project <uuid>`. |\n| `version` | `"latest"` | MCP | Which tokens snapshot to fetch: `"latest"` or a published semver such as `"1.4.0"`. Set with `--tokens-version`. |\n| `
|
|
472
|
-
"references/mcp-tools.md": "# Bezel MCP quick reference\n\nRemote server at `https://mcp.bezel.new/mcp` (Streamable HTTP, OAuth). The server returns data and instructions; the coding agent performs every local file edit and command. Project-scoped tools take the `projectId` from `bezel.json` or from `find-project`.\n\n## Guided workflows\n\n| Tool / prompt | Use |\n|---|---|\n| `setup-project` (tool and prompt) | Resumable setup: install bezel-kit, link a project, write `bezel.json`, fetch tokens, build, wire the CSS import, optional Crystal live preview. |\n| `sync-project` (prompt) | Refresh `design-tokens.json` from the linked project and version
|
|
564
|
+
"SKILL.md": '---\nname: bezel\ndescription: Use when the repo has bezel.json or design-tokens.json and the task touches design tokens, theme colors, spacing, typography, radii, CSS variables, dark mode, or files under the Bezel output folder (src/bezel by default). Edit design-tokens.json, never the generated variables.css / contexts.ts / fonts.ts, then run the tokens build. Also covers pulling the latest tokens from the Bezel app, pushing local token edits back to Bezel after a preview and confirmation, and using the Bezel MCP tools.\n---\n\n# Bezel in this repository\n\nBezel is a design-token platform. Designers maintain the tokens in the Bezel app; this repo holds an exported copy and generates CSS from it with `@bezel-labs/bezel-kit`.\n\n```\nBezel app (cloud, source of truth)\n -> design-tokens.json DTCG file at the repo root, exported from Bezel\n design-tokens-lock.json what was last synced or pushed (tokens + revision); commit it\n -> bezel build reads bezel.json for options and output paths\n -> variables.css one CSS scope per context (:root, .dark, .light, ...)\n contexts.ts, fonts.ts optional generated TypeScript modules\n```\n\nThe Bezel app stays the source of truth. Local edits to `design-tokens.json` reach Bezel only when they are pushed through the Bezel MCP, after a preview and the user\'s explicit yes (see "Pushing changes back to Bezel"); until then the next sync replaces them.\n\n## Hard rules\n\n1. Never edit a generated file. They start with `/* Generated by bezel \u2014 do not edit by hand. */` and live at the paths named in `bezel.json` (`variablesOutput`, `contextsOutput`, `fontsOutput`; default folder `src/bezel/`). `bezel build` overwrites them without asking and they are gitignored.\n2. To change a token value or add a variable, edit `design-tokens.json`, then rebuild. Never hand-write the CSS variable.\n3. After any change to `design-tokens.json` or `bezel.json`, run the tokens build: `npm run tokens` when that script exists, otherwise `npx bezel build` (use the repo\'s package manager runner). Confirm the outputs were rewritten before finishing.\n4. Only tokens with an `exportName` become CSS variables unless `bezel.json` sets `"includeAll": true`. Adding a token without an `exportName` produces no variable.\n5. Never pass `--force` to `bezel init`; it discards the existing `bezel.json`. `--project` and `--tokens-version` update their own keys without it.\n6. Local edits to `design-tokens.json` are not in Bezel until they are pushed. `npx bezel diff` lists them. Never sync over them silently: offer to push them first. Pushing means `npx bezel diff --json`, `preview-design-tokens-push` with its operations, showing the `confirmationPrompt` word for word, `push-design-tokens` only after the user says yes, then a sync. Never push without that confirmation, and never push from a "sync" or "pull" request.\n7. Never write or edit `design-tokens-lock.json`, and never write a synced `design-tokens.json` from MCP output yourself. Only `bezel sync` writes them, after every sync and every push, so the two files always agree. Commit both files together.\n\n## The tokens file\n\n`design-tokens.json` follows the W3C Design Tokens (DTCG) format. Every token is an object with a `$type` and one `$value` per context:\n\n```json\n"semantic": {\n "color": {\n "primary": {\n "$type": "color",\n "$root": { "$value": "{base.color.green.500}" },\n "dark": { "$value": { "colorSpace": "oklch", "components": [0.85, 0.16, 152] } },\n "$extensions": { "com.bezel.app": { "exportName": ["primary", "ring"] } }\n }\n }\n}\n```\n\n- Context keys (`$root`, `dark`, `light`, or whatever `bezel.json` `contexts` lists) sit inside the token. `$root` is the base; other contexts only need the values that differ.\n- `$value` is either a literal (DTCG color, dimension, font family, ...) or a reference like `{base.color.green.500}`. References are resolved to literal values in the generated CSS.\n- `$extensions["com.bezel.app"].exportName` is the list of CSS variable names the token emits. One token can emit several (`foreground`, `card-foreground`). An empty list means no variable.\n- Groups (`base`, `semantic`, `color`, ...) are plain nesting with no `$type`.\n\n## What to do for common requests\n\n| Request | Edit | Then |\n|---|---|---|\n| Change a color, size, radius, or font value | The token\'s `$value` in the right context key (`$root` for every mode, `dark` for dark mode only) | Rebuild |\n| A value should differ in dark mode | Add or edit the `dark` key on that token | Rebuild |\n| New CSS variable | Add a token with `$type`, `$root.$value`, and an `exportName` (or reference an existing token) | Rebuild |\n| Rename a CSS variable | Change the `exportName` entry; update every consumer of the old `--name` | Rebuild |\n| New theme or brand context | Add the context to `bezel.json` `contexts` (name -> selector) and add that key to the tokens that differ | Rebuild |\n| Change where files are generated | Edit `variablesOutput` / `contextsOutput` / `fontsOutput` in `bezel.json`; update the CSS import | Rebuild, delete the old outputs |\n| "Pull the latest tokens" / "sync from Bezel" | See the next section | `bezel sync` rebuilds |\n| "Push my changes to Bezel" / save local token edits back | Nothing by hand; follow "Pushing changes back to Bezel" | Sync, which updates the lockfile |\n\nFind the token for an existing variable by searching `design-tokens.json` for its name inside an `exportName` array.\n\n## Pulling the latest tokens from Bezel\n\n`bezel.json` `projectId` and `version` (`"latest"` or a published semver) say which Bezel project and snapshot this repo tracks. `design-tokens-lock.json` records the tokens and project revision this repo last synced or pushed, like `package-lock.json` next to `package.json`. Only `bezel sync` writes it.\n\nWith the Bezel MCP connected:\n\n1. Run `npx bezel diff`. If it lists local edits, stop and offer to push them first (next section). A lockfile problem it reports is fixed by this sync.\n2. Call `get-design-tokens-file` with that `projectId` and `version`. Write the returned `lockfileContent` exactly as returned to `.bezel/sync.json`.\n3. Run `npx bezel sync`. It checks the staged file, writes `design-tokens-lock.json` and then `design-tokens.json`, deletes the staged file, and rebuilds.\n - Exit code 2 means `design-tokens.json` has edits Bezel does not have. Show the message and offer to push them. Use `npx bezel sync --discard-local-edits` only when the user says to drop them.\n - `unknown command "sync"` means the installed `@bezel-labs/bezel-kit` is too old. Update it first.\n\nThe `sync-project` prompt walks through this, including linking a project when `projectId` is missing. Never pick a project or version yourself; ask when it is ambiguous.\n\nWithout the MCP: ask the user to download `design-tokens.json` from the project in the Bezel app and replace the file at the repo root, then rebuild. Pushing later needs a lockfile, so sync through the MCP when it becomes available.\n\n## Pushing changes back to Bezel\n\nNeeds the Bezel MCP. A push sends only your edits, as JSON Patch operations, the same way the Bezel app saves. Bezel applies them to the project\'s working tokens (`latest`) and keeps changes others made in the meantime. Publishing a version still happens in the Bezel app.\n\n1. Run `npx bezel diff --json`. `"ok": false` prints what to do (every lockfile problem is fixed by a sync). `"operations": []` means nothing to push.\n2. If `bezel.json` `version` is a semver, tell the user the push updates the working project, not that pinned release.\n3. Call `preview-design-tokens-push` with `projectId`, the printed `operations` and `baseRevision` unchanged, and `version` as `trackedVersion`. It saves nothing.\n4. `invalid` or `blocked`: report and stop (`blocked` is a plan limit). `conflict`: the listed tokens changed in Bezel too; follow `nextStep`. `no-changes` with `alreadyApplied`: an earlier push saved these edits but the sync after it did not run; sync now. Themes the edits use that the project lacks are added by the push (`addedContexts`).\n5. `ready`: show `confirmationPrompt` to the user word for word and wait. Do not push on silence, a question, or a different request.\n6. After an explicit yes, call `push-design-tokens` with the same `projectId`, `operations`, and `baseRevision`.\n7. A push is not finished until the repo is synced. Sync as in the previous section, steps 2 and 3: `get-design-tokens-file`, write `lockfileContent` to `.bezel/sync.json`, run `npx bezel sync`. Your edits are already in Bezel, so the sync keeps them, pulls in anything others changed, and moves the lockfile to the new revision. Then tell the user to commit `design-tokens.json` and `design-tokens-lock.json` together.\n\nThe `push-project` prompt walks through this.\n\n## With the Bezel MCP connected\n\nPrefer small, targeted calls over reading the whole tokens file:\n\n- `get-project-manifest` first: groups, counts, types, contexts, no values.\n- `query-tokens` to find tokens by group, type, name, or `exportName`; `get-token-by-path` for one token with every context value.\n- `get-token-bundle` for a curated set per task (`theming-setup`, `button`, `card`, `form`, ...).\n- `export-tokens` and `get-framework-overrides` to generate CSS, JS/TS objects, or Tailwind / shadcn / Mantine config from tokens.\n- `contrast-check`, `color-convert`, `color-ramp`, `unit-convert` for deterministic math instead of estimating.\n- `design-review` to check a component description against the tokens.\n\nSee `references/mcp-tools.md` for the full list and `references/bezel-json.md` for every config field.\n\n## Setting up a repo that is not on Bezel yet\n\nRun the `setup-project` tool (or the `/mcp__bezel__setup-project` prompt) and follow it; it installs `@bezel-labs/bezel-kit`, writes `bezel.json`, fetches the tokens, builds, and wires the CSS import. Do not improvise the steps.\n',
|
|
565
|
+
"references/bezel-json.md": '# bezel.json reference\n\n`bezel.json` lives at the repo root (or the working directory `bezel build` runs in). `bezel build` loads it automatically; `bezel init` writes it. The tokens input is always `design-tokens.json` in that same directory and is not configurable.\n\n| Field | Default | Used by | Meaning |\n|---|---|---|---|\n| `projectId` | none | MCP | UUID of the Bezel project this repo tracks. Set with `npx bezel init --project <uuid>`. |\n| `version` | `"latest"` | MCP | Which tokens snapshot to fetch: `"latest"` or a published semver such as `"1.4.0"`. Set with `--tokens-version`. |\n| `variablesOutput` | `"src/bezel/variables.css"` | build | Generated CSS file. Import this from the app entry. |\n| `contextsOutput` | none | build | When set, writes a TypeScript module exporting `CONTEXTS` (the context names). |\n| `fontsOutput` | none | build | When set, writes a TypeScript module exporting `FONTS` (web fonts derived from the tokens). |\n| `contexts` | `{ "$root": ":root", "dark": ".dark", "light": ".light" }` | build | Context name -> CSS selector. The first entry is the base context and is emitted in full; the rest are override-only blocks. |\n| `colorFormat` | `"oklch"` | build | `"oklch"` or `"hex"`. |\n| `dimensionUnit` | `"preserve"` | build | `"preserve"` keeps authored units; `"rem"` converts `px` to `rem`. |\n| `nameExtension` | `"com.bezel.app"` | build | The `$extensions` key that holds `exportName`. Only change for files exported before the 0.3.0 rename. |\n| `includeAll` | `false` | build | Also emit a variable for every token without an `exportName`, named by its kebab-cased path (`--base-color-primary-500`). |\n| `write` | `true` | build | `false` returns the CSS without writing files (programmatic use). |\n| `gitignore` | `true` | build | Maintain a `# bezel:start` ... `# bezel:end` block in `.gitignore` listing the generated outputs. |\n| `gitignorePath` | `".gitignore"` | build | Which `.gitignore` to manage. |\n| `cwd` | process cwd | build | Base directory paths resolve against. |\n\n`projectId` and `version` are ignored by `bezel build`; everything else is ignored by the MCP. The synced revision lives in `design-tokens-lock.json`, not here.\n\n## design-tokens-lock.json\n\nCommitted next to `design-tokens.json`, like `package-lock.json` next to `package.json`. It records the tokens and project revision this repo last synced or pushed: `lockfileVersion`, `projectId`, `version`, `revision`, and `tokens`. `bezel diff` compares `design-tokens.json` against it to find local edits. Only `bezel sync` writes it, after every sync and every push; never edit it. `bezel init --project` deletes a lockfile that belongs to a different project.\n\n## CLI\n\n```\nbezel init [--project <uuid>] [--tokens-version <v>] [--dir <path>] ... create or update bezel.json\nbezel build [--stdout] [--no-gitignore] ... generate the outputs (default command)\nbezel install-mcp [--client <name>] connect an AI coding tool to the Bezel MCP\nbezel install-skills [--client <name>] [--no-agents-md] install this skill into the repo\nbezel diff [--json] local edits vs design-tokens-lock.json (--json: JSON Patch operations for a push)\nbezel sync [--discard-local-edits] apply .bezel/sync.json: write the lockfile and design-tokens.json, then build\n```\n\n`bezel sync` exits with code 2 when `design-tokens.json` has edits Bezel does not have; other problems exit with 1.\n\n`bezel init` on an existing `bezel.json` with `--project` or `--tokens-version` updates only those keys. Any other re-run leaves the file alone unless `--force` is passed, which agents must not do.\n',
|
|
566
|
+
"references/mcp-tools.md": "# Bezel MCP quick reference\n\nRemote server at `https://mcp.bezel.new/mcp` (Streamable HTTP, OAuth). The server returns data and instructions; the coding agent performs every local file edit and command. Project-scoped tools take the `projectId` from `bezel.json` or from `find-project`.\n\n## Guided workflows\n\n| Tool / prompt | Use |\n|---|---|\n| `setup-project` (tool and prompt) | Resumable setup: install bezel-kit, link a project, write `bezel.json`, fetch tokens, build, wire the CSS import, optional Crystal live preview. |\n| `sync-project` (prompt) | Refresh `design-tokens.json` and `design-tokens-lock.json` from the linked project and version through `bezel sync`. Offers to push local edits first instead of overwriting them. |\n| `push-project` (prompt) | Diff, preview, confirm with the user, and push the repo's token edits to the linked project, then sync so `design-tokens-lock.json` moves to the new revision. |\n| `safe-usage-guide` (prompt) | Context-budget rules: manifest first, targeted queries, cite token paths. |\n| `manifest-first-workflow`, `context-resolution` (prompts) | Recommended reading order and how contexts resolve. |\n| `get-agent-guide` (tool) | Returns this skill's text when the repo has no local copy. |\n\n## Discovery\n\n| Tool | Parameters | Returns |\n|---|---|---|\n| `find-project` | `query?` | All owned projects (name + id), or one project resolved from a name or UUID. |\n| `get-project-manifest` | `projectId` | Groups, token counts, types, contexts. No values. Call this first. |\n\n## Repo sync\n\n| Tool | Parameters | Returns |\n|---|---|---|\n| `get-design-tokens-file` | `projectId`, `version?` | `lockfileContent`: the full `design-tokens-lock.json` text (tokens + `revision`). Write it to `.bezel/sync.json` and run `npx bezel sync`. Unknown version -> list of available versions. |\n| `preview-design-tokens-push` | `projectId`, `operations`, `baseRevision?`, `trackedVersion?` | Checks the edits from `npx bezel diff --json` against the working tokens: plan-limit problems, `conflict` (tokens edited here and in Bezel), themes the edits add (`addedContexts`), `behind` when Bezel moved on (those changes are kept), and a `confirmationPrompt` to show verbatim. Saves nothing. |\n| `push-design-tokens` | `projectId`, `operations`, `baseRevision?` | Saves the previewed edits as a new revision. Call only after the user confirms the preview. Returns the new `revision`. Sync right after it so the lockfile moves to that revision. |\n\n## Reading tokens\n\n| Tool | Parameters | Returns |\n|---|---|---|\n| `query-tokens` | `projectId`, `groupPath?`, `tokenType?`, `search?`, `exportName?`, `contextKey?`, `resolveAliases?` | Filtered tokens. |\n| `get-token-by-path` | `projectId`, `tokenPath`, `contextKey?`, `resolveAliases?` | One token with every context value and alias detail. |\n| `get-token-bundle` | `projectId`, `intent` | Curated set: `theming-setup`, `button`, `card`, `input`, `form`, `auth-page`, `dashboard-shell`. |\n| `get-project-contexts` | `projectId` | Context groups and values (theme modes, platforms, brands). |\n| `resolve-context` | `projectId`, `contextGroup`, `contextValue` | Canonical context value or a structured error. |\n\n## Generating code\n\n| Tool | Parameters | Returns |\n|---|---|---|\n| `export-tokens` | `projectId`, `format` (`css-variables`, `js-object`, `ts-object`), filters, `prefix?`, `includeAll?` | Generated code with aliases resolved. |\n| `get-framework-overrides` | `projectId`, `framework` (`tailwind`, `shadcn`, `mantine`), `version?`, filters | Framework config derived from the tokens. |\n\n## Color and unit math (deterministic)\n\n| Tool | Parameters |\n|---|---|\n| `contrast-check` | `foreground`, `background` -> WCAG ratio, grade, recommendations |\n| `color-convert` | `color`, `targetFormat`, `manipulation?`, `mixColor?` |\n| `color-ramp` | `baseColor`, `steps`, `endColor?`, `colorSpace?` |\n| `unit-convert` | `value`, `fromUnit`, `toUnit`, `rootFontSize?`, `parentFontSize?` |\n\n## Review and history\n\n| Tool | Parameters |\n|---|---|\n| `design-review` | `projectId`, `componentDescription`, `tokenPathsUsed?`, `contextKey?` |\n| `get-chat-history` | `projectId` |\n| `search-chat-history` | `projectId`, `query` |\n\n## Resources\n\n- `bezel://projects`: your projects with ids and names.\n- `bezel://agent-guide`: this skill's text.\n"
|
|
473
567
|
},
|
|
474
|
-
agentsMdBlock: '## Bezel design tokens\n\n- `design-tokens.json` at the repo root is the source for every design token. Edit it to change a color, size, font, or radius, or to add a CSS variable (add an `exportName` under `$extensions["com.bezel.app"]`).\n- Files under the Bezel output folder (`variablesOutput`, `contextsOutput`, `fontsOutput` in `bezel.json`; `src/bezel/` by default) are generated. Never edit them; they are overwritten on every build.\n- After changing `design-tokens.json` or `bezel.json`, run `npm run tokens` (or `npx bezel build`) and check the outputs were rewritten.\n- Dark mode and other contexts are keys inside each token (`$root`, `dark`, `light`). Add a value under the context key instead of writing a CSS override.\n- The Bezel app is the source of truth.
|
|
475
|
-
summary: 'Use when the repo has bezel.json or design-tokens.json and the task touches design tokens, theme colors, spacing, typography, radii, CSS variables, dark mode, or files under the Bezel output folder (src/bezel by default). Edit design-tokens.json, never the generated variables.css / contexts.ts / fonts.ts, then run the tokens build. Also covers pulling the latest tokens from the Bezel app, pushing local token edits back to Bezel after a preview and confirmation, and using the Bezel MCP tools.\n\n## Hard rules\n\n1. Never edit a generated file. They start with `/* Generated by bezel \u2014 do not edit by hand. */` and live at the paths named in `bezel.json` (`variablesOutput`, `contextsOutput`, `fontsOutput`; default folder `src/bezel/`). `bezel build` overwrites them without asking and they are gitignored.\n2. To change a token value or add a variable, edit `design-tokens.json`, then rebuild. Never hand-write the CSS variable.\n3. After any change to `design-tokens.json` or `bezel.json`, run the tokens build: `npm run tokens` when that script exists, otherwise `npx bezel build` (use the repo\'s package manager runner). Confirm the outputs were rewritten before finishing.\n4. Only tokens with an `exportName` become CSS variables unless `bezel.json` sets `"includeAll": true`. Adding a token without an `exportName` produces no variable.\n5. Never pass `--force` to `bezel init`; it discards the existing `bezel.json`. `--project` and `--tokens-version` update their own keys without it.\n6. Local edits to `design-tokens.json` are not in Bezel until they are pushed
|
|
568
|
+
agentsMdBlock: '## Bezel design tokens\n\n- `design-tokens.json` at the repo root is the source for every design token. Edit it to change a color, size, font, or radius, or to add a CSS variable (add an `exportName` under `$extensions["com.bezel.app"]`).\n- Files under the Bezel output folder (`variablesOutput`, `contextsOutput`, `fontsOutput` in `bezel.json`; `src/bezel/` by default) are generated. Never edit them; they are overwritten on every build.\n- After changing `design-tokens.json` or `bezel.json`, run `npm run tokens` (or `npx bezel build`) and check the outputs were rewritten.\n- Dark mode and other contexts are keys inside each token (`$root`, `dark`, `light`). Add a value under the context key instead of writing a CSS override.\n- The Bezel app is the source of truth. `design-tokens-lock.json` records what was last synced or pushed; never edit it, and commit it together with `design-tokens.json`. `npx bezel diff` lists local edits that are not in Bezel yet.\n- Sync through the Bezel MCP: write `get-design-tokens-file`\'s `lockfileContent` to `.bezel/sync.json`, then run `npx bezel sync` (it writes both files and refuses to drop unpushed edits). To push, run `npx bezel diff --json`, call `preview-design-tokens-push` with its operations, show the confirmation prompt word for word, call `push-design-tokens` only after the user says yes, then sync to update the lockfile.\n- Never run `bezel init --force`; it discards the existing `bezel.json`. Use `--project` or `--tokens-version` to update a single key.\n- Full guidance: the `bezel` skill in `.claude/skills/bezel/SKILL.md` or `.agents/skills/bezel/SKILL.md`.',
|
|
569
|
+
summary: 'Use when the repo has bezel.json or design-tokens.json and the task touches design tokens, theme colors, spacing, typography, radii, CSS variables, dark mode, or files under the Bezel output folder (src/bezel by default). Edit design-tokens.json, never the generated variables.css / contexts.ts / fonts.ts, then run the tokens build. Also covers pulling the latest tokens from the Bezel app, pushing local token edits back to Bezel after a preview and confirmation, and using the Bezel MCP tools.\n\n## Hard rules\n\n1. Never edit a generated file. They start with `/* Generated by bezel \u2014 do not edit by hand. */` and live at the paths named in `bezel.json` (`variablesOutput`, `contextsOutput`, `fontsOutput`; default folder `src/bezel/`). `bezel build` overwrites them without asking and they are gitignored.\n2. To change a token value or add a variable, edit `design-tokens.json`, then rebuild. Never hand-write the CSS variable.\n3. After any change to `design-tokens.json` or `bezel.json`, run the tokens build: `npm run tokens` when that script exists, otherwise `npx bezel build` (use the repo\'s package manager runner). Confirm the outputs were rewritten before finishing.\n4. Only tokens with an `exportName` become CSS variables unless `bezel.json` sets `"includeAll": true`. Adding a token without an `exportName` produces no variable.\n5. Never pass `--force` to `bezel init`; it discards the existing `bezel.json`. `--project` and `--tokens-version` update their own keys without it.\n6. Local edits to `design-tokens.json` are not in Bezel until they are pushed. `npx bezel diff` lists them. Never sync over them silently: offer to push them first. Pushing means `npx bezel diff --json`, `preview-design-tokens-push` with its operations, showing the `confirmationPrompt` word for word, `push-design-tokens` only after the user says yes, then a sync. Never push without that confirmation, and never push from a "sync" or "pull" request.\n7. Never write or edit `design-tokens-lock.json`, and never write a synced `design-tokens.json` from MCP output yourself. Only `bezel sync` writes them, after every sync and every push, so the two files always agree. Commit both files together.'
|
|
476
570
|
};
|
|
477
571
|
|
|
478
572
|
// src/init.ts
|
|
573
|
+
var import_node_fs2 = require("fs");
|
|
574
|
+
var import_promises2 = require("fs/promises");
|
|
575
|
+
var import_node_path3 = require("path");
|
|
576
|
+
|
|
577
|
+
// src/tokens-sync.ts
|
|
479
578
|
var import_node_fs = require("fs");
|
|
480
579
|
var import_promises = require("fs/promises");
|
|
481
580
|
var import_node_path2 = require("path");
|
|
581
|
+
var import_fast_json_patch = __toESM(require("fast-json-patch"), 1);
|
|
582
|
+
var { compare } = import_fast_json_patch.default;
|
|
583
|
+
var SYNC_HINT = 'Sync from Bezel (ask your agent to "sync my Bezel tokens").';
|
|
584
|
+
async function readLink(cwd) {
|
|
585
|
+
const path = (0, import_node_path2.resolve)(cwd, DEFAULT_CONFIG_FILE);
|
|
586
|
+
if (!(0, import_node_fs.existsSync)(path)) {
|
|
587
|
+
return { ok: false, reason: "no-config", message: `No ${DEFAULT_CONFIG_FILE} here. Run "bezel init --project <uuid>" first.` };
|
|
588
|
+
}
|
|
589
|
+
let config;
|
|
590
|
+
try {
|
|
591
|
+
config = JSON.parse(await (0, import_promises.readFile)(path, "utf8"));
|
|
592
|
+
} catch {
|
|
593
|
+
return { ok: false, reason: "no-config", message: `${DEFAULT_CONFIG_FILE} is not valid JSON.` };
|
|
594
|
+
}
|
|
595
|
+
if (typeof config.projectId !== "string" || config.projectId === "") {
|
|
596
|
+
return {
|
|
597
|
+
ok: false,
|
|
598
|
+
reason: "no-project",
|
|
599
|
+
message: `${DEFAULT_CONFIG_FILE} has no projectId. Link the repo with "bezel init --project <uuid>".`
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
const version = typeof config.version === "string" && config.version !== "" ? config.version : "latest";
|
|
603
|
+
return { ok: true, link: { projectId: config.projectId, version } };
|
|
604
|
+
}
|
|
605
|
+
async function readTokensFile(cwd) {
|
|
606
|
+
const path = (0, import_node_path2.resolve)(cwd, DEFAULT_INPUT);
|
|
607
|
+
if (!(0, import_node_fs.existsSync)(path)) {
|
|
608
|
+
return { ok: false, reason: "no-tokens-file", message: `No ${DEFAULT_INPUT} here. ${SYNC_HINT}` };
|
|
609
|
+
}
|
|
610
|
+
try {
|
|
611
|
+
const parsed = JSON.parse(await (0, import_promises.readFile)(path, "utf8"));
|
|
612
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error("not an object");
|
|
613
|
+
return { ok: true, tokens: parsed };
|
|
614
|
+
} catch {
|
|
615
|
+
return {
|
|
616
|
+
ok: false,
|
|
617
|
+
reason: "tokens-file-invalid",
|
|
618
|
+
message: `${DEFAULT_INPUT} is not a JSON object. Fix the file (or sync from Bezel to replace it).`
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
async function readCurrentLockfile(cwd) {
|
|
623
|
+
const path = (0, import_node_path2.resolve)(cwd, LOCKFILE_NAME);
|
|
624
|
+
if (!(0, import_node_fs.existsSync)(path)) return { ok: true, lockfile: null };
|
|
625
|
+
const parsed = parseLockfile(await (0, import_promises.readFile)(path, "utf8"));
|
|
626
|
+
if (parsed.ok === false) return { ok: false, reason: "lockfile-invalid", message: parsed.message };
|
|
627
|
+
return { ok: true, lockfile: parsed.lockfile };
|
|
628
|
+
}
|
|
629
|
+
function checkLockMatchesLink(lockfile, link, name) {
|
|
630
|
+
if (lockfile.projectId !== link.projectId) {
|
|
631
|
+
return {
|
|
632
|
+
ok: false,
|
|
633
|
+
reason: "project-mismatch",
|
|
634
|
+
message: `${name} is for project ${lockfile.projectId}, but ${DEFAULT_CONFIG_FILE} links ${link.projectId}. ${SYNC_HINT}`
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
if (lockfile.version !== link.version) {
|
|
638
|
+
return {
|
|
639
|
+
ok: false,
|
|
640
|
+
reason: "version-mismatch",
|
|
641
|
+
message: `${name} holds version "${lockfile.version}", but ${DEFAULT_CONFIG_FILE} tracks "${link.version}". ${SYNC_HINT}`
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
return null;
|
|
645
|
+
}
|
|
646
|
+
function changedPaths(operations) {
|
|
647
|
+
const paths = /* @__PURE__ */ new Set();
|
|
648
|
+
for (const operation of operations) {
|
|
649
|
+
if (operation.op === "test") continue;
|
|
650
|
+
const segments = [];
|
|
651
|
+
for (const raw of operation.path.split("/").slice(1)) {
|
|
652
|
+
const segment = raw.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
653
|
+
if (segment.startsWith("$")) break;
|
|
654
|
+
segments.push(segment);
|
|
655
|
+
}
|
|
656
|
+
paths.add(segments.length > 0 ? segments.join(".") : "(root)");
|
|
657
|
+
}
|
|
658
|
+
return [...paths];
|
|
659
|
+
}
|
|
660
|
+
async function diffTokens({ cwd = process.cwd() } = {}) {
|
|
661
|
+
const link = await readLink(cwd);
|
|
662
|
+
if (link.ok === false) return link;
|
|
663
|
+
const current = await readCurrentLockfile(cwd);
|
|
664
|
+
if (current.ok === false) return current;
|
|
665
|
+
if (current.lockfile === null) {
|
|
666
|
+
return {
|
|
667
|
+
ok: false,
|
|
668
|
+
reason: "no-lockfile",
|
|
669
|
+
message: `No ${LOCKFILE_NAME} yet, so local edits cannot be told apart from Bezel's tokens. ${SYNC_HINT}`
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
const mismatch = checkLockMatchesLink(current.lockfile, link.link, LOCKFILE_NAME);
|
|
673
|
+
if (mismatch) return mismatch;
|
|
674
|
+
const local = await readTokensFile(cwd);
|
|
675
|
+
if (local.ok === false) return local;
|
|
676
|
+
const operations = compare(current.lockfile.tokens, local.tokens, true);
|
|
677
|
+
return {
|
|
678
|
+
ok: true,
|
|
679
|
+
projectId: current.lockfile.projectId,
|
|
680
|
+
version: current.lockfile.version,
|
|
681
|
+
baseRevision: current.lockfile.revision,
|
|
682
|
+
operations
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
async function syncFromStaging({
|
|
686
|
+
cwd = process.cwd(),
|
|
687
|
+
discardLocalEdits = false
|
|
688
|
+
} = {}) {
|
|
689
|
+
const stagingPath = (0, import_node_path2.resolve)(cwd, SYNC_STAGING_PATH);
|
|
690
|
+
if (!(0, import_node_fs.existsSync)(stagingPath)) {
|
|
691
|
+
return {
|
|
692
|
+
ok: false,
|
|
693
|
+
reason: "no-staging",
|
|
694
|
+
message: `Nothing to sync: ${SYNC_STAGING_PATH} is missing. Write the "lockfileContent" from the Bezel MCP's get-design-tokens-file there first.`
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
const incoming = parseLockfile(await (0, import_promises.readFile)(stagingPath, "utf8"), SYNC_STAGING_PATH);
|
|
698
|
+
if (incoming.ok === false) {
|
|
699
|
+
return {
|
|
700
|
+
ok: false,
|
|
701
|
+
reason: "staging-invalid",
|
|
702
|
+
message: `${incoming.message.replace(/ Sync from Bezel to rewrite it\.$/, "")} Write the "lockfileContent" from get-design-tokens-file to ${SYNC_STAGING_PATH} exactly as returned, then run "bezel sync" again.`
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
const next = incoming.lockfile;
|
|
706
|
+
const link = await readLink(cwd);
|
|
707
|
+
if (link.ok === false) return link;
|
|
708
|
+
const mismatch = checkLockMatchesLink(next, link.link, SYNC_STAGING_PATH);
|
|
709
|
+
if (mismatch) {
|
|
710
|
+
return {
|
|
711
|
+
...mismatch,
|
|
712
|
+
message: `${SYNC_STAGING_PATH} is for project ${next.projectId} version "${next.version}", but ${DEFAULT_CONFIG_FILE} links ${link.link.projectId} version "${link.link.version}". Fetch the tokens for the linked project and version, or relink with "bezel init --project <uuid> --tokens-version <v>".`
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
const tokensPath = (0, import_node_path2.resolve)(cwd, DEFAULT_INPUT);
|
|
716
|
+
let discardedEdits = 0;
|
|
717
|
+
let tokensChanged = true;
|
|
718
|
+
if ((0, import_node_fs.existsSync)(tokensPath)) {
|
|
719
|
+
const local = await readTokensFile(cwd);
|
|
720
|
+
if (local.ok === true) {
|
|
721
|
+
tokensChanged = !jsonEqual(local.tokens, next.tokens);
|
|
722
|
+
const current = await readCurrentLockfile(cwd);
|
|
723
|
+
const base = current.ok === true && current.lockfile !== null && checkLockMatchesLink(current.lockfile, link.link, LOCKFILE_NAME) === null ? current.lockfile : null;
|
|
724
|
+
if (tokensChanged) {
|
|
725
|
+
if (base === null) {
|
|
726
|
+
if (!discardLocalEdits) {
|
|
727
|
+
return {
|
|
728
|
+
ok: false,
|
|
729
|
+
reason: "unknown-local-edits",
|
|
730
|
+
message: `${DEFAULT_INPUT} differs from Bezel's tokens and there is no usable ${LOCKFILE_NAME} to tell local edits from older Bezel tokens. If the file has no edits worth keeping, run "bezel sync --discard-local-edits" to replace it.`
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
} else {
|
|
734
|
+
const localEdits = compare(base.tokens, local.tokens, true);
|
|
735
|
+
const unpushed = unappliedOperations(next.tokens, localEdits);
|
|
736
|
+
if (unpushed.length > 0 && !discardLocalEdits) {
|
|
737
|
+
const paths = changedPaths(unpushed);
|
|
738
|
+
return {
|
|
739
|
+
ok: false,
|
|
740
|
+
reason: "unpushed-edits",
|
|
741
|
+
unpushedPaths: paths,
|
|
742
|
+
message: `${DEFAULT_INPUT} has ${unpushed.length} local ${unpushed.length === 1 ? "edit" : "edits"} Bezel does not have (${paths.slice(0, 10).join(", ")}${paths.length > 10 ? ", ..." : ""}). Push them to Bezel first, or run "bezel sync --discard-local-edits" to drop them.`
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
discardedEdits = unpushed.length;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
} else if (!discardLocalEdits) {
|
|
749
|
+
return {
|
|
750
|
+
...local,
|
|
751
|
+
message: `${local.message.replace(/ \(or sync from Bezel to replace it\)/, "")} Or run "bezel sync --discard-local-edits" to replace it with Bezel's tokens.`
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
await (0, import_promises.writeFile)((0, import_node_path2.resolve)(cwd, LOCKFILE_NAME), serializeLockfile(next), "utf8");
|
|
756
|
+
if (tokensChanged) await (0, import_promises.writeFile)(tokensPath, serializeTokensFile(next.tokens), "utf8");
|
|
757
|
+
await (0, import_promises.rm)(stagingPath, { force: true });
|
|
758
|
+
return { ok: true, projectId: next.projectId, version: next.version, revision: next.revision, tokensChanged, discardedEdits };
|
|
759
|
+
}
|
|
760
|
+
async function removeLockfileForOtherProject(cwd, projectId) {
|
|
761
|
+
const path = (0, import_node_path2.resolve)(cwd, LOCKFILE_NAME);
|
|
762
|
+
if (!(0, import_node_fs.existsSync)(path)) return false;
|
|
763
|
+
try {
|
|
764
|
+
const parsed = JSON.parse(await (0, import_promises.readFile)(path, "utf8"));
|
|
765
|
+
if (parsed.projectId === projectId) return false;
|
|
766
|
+
} catch {
|
|
767
|
+
}
|
|
768
|
+
await (0, import_promises.rm)(path, { force: true });
|
|
769
|
+
return true;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// src/init.ts
|
|
482
773
|
var isUuid = (value) => /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
|
|
483
774
|
var isTokensVersion = (value) => value === "latest" || /^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/.test(value);
|
|
484
775
|
var joinPath = (dir, file) => dir === "" ? file : `${dir.replace(/\/+$/, "")}/${file}`;
|
|
@@ -492,11 +783,11 @@ async function initConfig(options = {}) {
|
|
|
492
783
|
);
|
|
493
784
|
}
|
|
494
785
|
const cwd = options.cwd ?? process.cwd();
|
|
495
|
-
const toAbs = (p) => (0,
|
|
786
|
+
const toAbs = (p) => (0, import_node_path3.isAbsolute)(p) ? p : (0, import_node_path3.resolve)(cwd, p);
|
|
496
787
|
const configPath = toAbs(DEFAULT_CONFIG_FILE);
|
|
497
|
-
const hasTokensFile = (0,
|
|
498
|
-
const isTypeScript = (0,
|
|
499
|
-
const hasSrc = (0,
|
|
788
|
+
const hasTokensFile = (0, import_node_fs2.existsSync)(toAbs(DEFAULT_INPUT));
|
|
789
|
+
const isTypeScript = (0, import_node_fs2.existsSync)(toAbs("tsconfig.json"));
|
|
790
|
+
const hasSrc = (0, import_node_fs2.existsSync)(toAbs("src"));
|
|
500
791
|
const outputDir = options.outputDir ?? (hasSrc ? DEFAULT_OUTPUT_DIR : FALLBACK_OUTPUT_DIR);
|
|
501
792
|
const wantsContexts = options.contextsOutput !== void 0 || (options.contexts ?? isTypeScript);
|
|
502
793
|
const wantsFonts = options.fontsOutput !== void 0 || (options.fonts ?? isTypeScript);
|
|
@@ -513,29 +804,24 @@ async function initConfig(options = {}) {
|
|
|
513
804
|
if (options.colorFormat) config.colorFormat = options.colorFormat;
|
|
514
805
|
if (options.dimensionUnit) config.dimensionUnit = options.dimensionUnit;
|
|
515
806
|
if (options.includeAll) config.includeAll = true;
|
|
516
|
-
if ((0,
|
|
807
|
+
if ((0, import_node_fs2.existsSync)(configPath) && !options.force) {
|
|
517
808
|
const wantsLinkUpdate = options.projectId !== void 0 || options.version !== void 0;
|
|
518
809
|
if (!wantsLinkUpdate) {
|
|
519
810
|
return { configPath, config, written: false, updated: false, hasTokensFile, isTypeScript };
|
|
520
811
|
}
|
|
521
|
-
const existing = JSON.parse(await (0,
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
version: previousVersion,
|
|
525
|
-
revision: previousRevision,
|
|
526
|
-
...rest
|
|
527
|
-
} = existing;
|
|
812
|
+
const existing = JSON.parse(await (0, import_promises2.readFile)(configPath, "utf8"));
|
|
813
|
+
delete existing.revision;
|
|
814
|
+
const { projectId: previousProjectId, version: previousVersion, ...rest } = existing;
|
|
528
815
|
const merged = {};
|
|
529
816
|
const projectId = options.projectId ?? previousProjectId;
|
|
530
817
|
if (projectId !== void 0) merged.projectId = projectId;
|
|
531
818
|
merged.version = options.version ?? previousVersion ?? "latest";
|
|
532
|
-
if (previousRevision !== void 0 && projectId === previousProjectId) {
|
|
533
|
-
merged.revision = previousRevision;
|
|
534
|
-
}
|
|
535
819
|
Object.assign(merged, rest);
|
|
536
|
-
await (0,
|
|
820
|
+
await (0, import_promises2.writeFile)(configPath, `${JSON.stringify(merged, null, 2)}
|
|
537
821
|
`, "utf8");
|
|
822
|
+
const removedLockfile2 = projectId !== void 0 ? await removeLockfileForOtherProject(cwd, projectId) : false;
|
|
538
823
|
return {
|
|
824
|
+
removedLockfile: removedLockfile2,
|
|
539
825
|
configPath,
|
|
540
826
|
config: merged,
|
|
541
827
|
written: true,
|
|
@@ -546,13 +832,14 @@ async function initConfig(options = {}) {
|
|
|
546
832
|
isTypeScript
|
|
547
833
|
};
|
|
548
834
|
}
|
|
549
|
-
await (0,
|
|
835
|
+
await (0, import_promises2.writeFile)(configPath, `${JSON.stringify(config, null, 2)}
|
|
550
836
|
`, "utf8");
|
|
551
|
-
|
|
837
|
+
const removedLockfile = config.projectId !== void 0 ? await removeLockfileForOtherProject(cwd, config.projectId) : false;
|
|
838
|
+
return { configPath, config, written: true, updated: false, removedLockfile, hasTokensFile, isTypeScript };
|
|
552
839
|
}
|
|
553
840
|
async function readPackageScripts(cwd) {
|
|
554
841
|
try {
|
|
555
|
-
const raw = await (0,
|
|
842
|
+
const raw = await (0, import_promises2.readFile)((0, import_node_path3.resolve)(cwd, "package.json"), "utf8");
|
|
556
843
|
const parsed = JSON.parse(raw);
|
|
557
844
|
return parsed.scripts ?? {};
|
|
558
845
|
} catch {
|
|
@@ -565,7 +852,7 @@ async function generateVariablesCss(options = {}) {
|
|
|
565
852
|
const resolved = resolveOptions(options);
|
|
566
853
|
let raw;
|
|
567
854
|
try {
|
|
568
|
-
raw = await (0,
|
|
855
|
+
raw = await (0, import_promises3.readFile)(resolved.inputPath, "utf8");
|
|
569
856
|
} catch (err) {
|
|
570
857
|
throw new Error(
|
|
571
858
|
`bezel: could not read tokens file at "${resolved.inputPath}". A "design-tokens.json" file must exist at the project root.
|
|
@@ -581,17 +868,17 @@ ${err.message}`);
|
|
|
581
868
|
}
|
|
582
869
|
const css = emitCss(tokens, resolved);
|
|
583
870
|
if (resolved.write) {
|
|
584
|
-
await (0,
|
|
585
|
-
await (0,
|
|
871
|
+
await (0, import_promises3.mkdir)((0, import_node_path4.dirname)(resolved.variablesOutputPath), { recursive: true });
|
|
872
|
+
await (0, import_promises3.writeFile)(resolved.variablesOutputPath, css, "utf8");
|
|
586
873
|
if (resolved.contextsOutput) {
|
|
587
874
|
const contextsModule = formatContextsModule(getContexts(tokens));
|
|
588
|
-
await (0,
|
|
589
|
-
await (0,
|
|
875
|
+
await (0, import_promises3.mkdir)((0, import_node_path4.dirname)(resolved.contextsOutput), { recursive: true });
|
|
876
|
+
await (0, import_promises3.writeFile)(resolved.contextsOutput, contextsModule, "utf8");
|
|
590
877
|
}
|
|
591
878
|
if (resolved.fontsOutput) {
|
|
592
879
|
const fontsModule = formatFontsModule(getFonts(tokens));
|
|
593
|
-
await (0,
|
|
594
|
-
await (0,
|
|
880
|
+
await (0, import_promises3.mkdir)((0, import_node_path4.dirname)(resolved.fontsOutput), { recursive: true });
|
|
881
|
+
await (0, import_promises3.writeFile)(resolved.fontsOutput, fontsModule, "utf8");
|
|
595
882
|
}
|
|
596
883
|
if (resolved.gitignore) {
|
|
597
884
|
await updateGitignore(resolved);
|
|
@@ -600,15 +887,17 @@ ${err.message}`);
|
|
|
600
887
|
return css;
|
|
601
888
|
}
|
|
602
889
|
async function updateGitignore(resolved) {
|
|
603
|
-
const dir = (0,
|
|
890
|
+
const dir = (0, import_node_path4.dirname)(resolved.gitignorePath);
|
|
604
891
|
const outputs = [
|
|
605
892
|
resolved.variablesOutputPath,
|
|
606
893
|
resolved.contextsOutput,
|
|
607
|
-
resolved.fontsOutput
|
|
894
|
+
resolved.fontsOutput,
|
|
895
|
+
// The MCP's sync payload is a temporary hand-off file; the lockfile itself is committed.
|
|
896
|
+
(0, import_node_path4.resolve)((0, import_node_path4.dirname)(resolved.inputPath), SYNC_STAGING_PATH)
|
|
608
897
|
].filter((p) => p !== null);
|
|
609
898
|
const entries = [];
|
|
610
899
|
for (const output of outputs) {
|
|
611
|
-
const rel = (0,
|
|
900
|
+
const rel = (0, import_node_path4.relative)(dir, output);
|
|
612
901
|
if (rel === "" || rel.startsWith("..")) {
|
|
613
902
|
process.stderr.write(
|
|
614
903
|
`bezel: skipping .gitignore entry for "${output}" \u2014 outside ${resolved.gitignorePath}
|
|
@@ -616,29 +905,29 @@ async function updateGitignore(resolved) {
|
|
|
616
905
|
);
|
|
617
906
|
continue;
|
|
618
907
|
}
|
|
619
|
-
entries.push(`/${rel.split(
|
|
908
|
+
entries.push(`/${rel.split(import_node_path4.sep).join("/")}`);
|
|
620
909
|
}
|
|
621
910
|
if (entries.length === 0) return;
|
|
622
911
|
let existing = "";
|
|
623
912
|
try {
|
|
624
|
-
existing = await (0,
|
|
913
|
+
existing = await (0, import_promises3.readFile)(resolved.gitignorePath, "utf8");
|
|
625
914
|
} catch {
|
|
626
915
|
}
|
|
627
916
|
const next = mergeGitignore(existing, entries);
|
|
628
917
|
if (next !== existing) {
|
|
629
|
-
await (0,
|
|
630
|
-
await (0,
|
|
918
|
+
await (0, import_promises3.mkdir)(dir, { recursive: true });
|
|
919
|
+
await (0, import_promises3.writeFile)(resolved.gitignorePath, next, "utf8");
|
|
631
920
|
}
|
|
632
921
|
}
|
|
633
922
|
|
|
634
923
|
// src/install-mcp.ts
|
|
635
924
|
var import_node_child_process = require("child_process");
|
|
636
|
-
var
|
|
637
|
-
var
|
|
925
|
+
var import_promises5 = require("fs/promises");
|
|
926
|
+
var import_node_path6 = require("path");
|
|
638
927
|
|
|
639
928
|
// src/install-skills.ts
|
|
640
|
-
var
|
|
641
|
-
var
|
|
929
|
+
var import_promises4 = require("fs/promises");
|
|
930
|
+
var import_node_path5 = require("path");
|
|
642
931
|
var SKILL_CLIENTS = ["claude-code", "codex", "cursor", "all"];
|
|
643
932
|
var AGENTS_MD_START = "<!-- bezel:start (generated by bezel install-skills \u2014 do not edit) -->";
|
|
644
933
|
var AGENTS_MD_END = "<!-- bezel:end -->";
|
|
@@ -657,50 +946,50 @@ function mergeAgentsMd(existing) {
|
|
|
657
946
|
}
|
|
658
947
|
async function readIfExists(path) {
|
|
659
948
|
try {
|
|
660
|
-
return await (0,
|
|
949
|
+
return await (0, import_promises4.readFile)(path, "utf8");
|
|
661
950
|
} catch (error) {
|
|
662
951
|
if (error.code === "ENOENT") return void 0;
|
|
663
952
|
throw error;
|
|
664
953
|
}
|
|
665
954
|
}
|
|
666
955
|
async function writeManaged(cwd, path, content, result) {
|
|
667
|
-
const rel = (0,
|
|
956
|
+
const rel = (0, import_node_path5.relative)(cwd, path);
|
|
668
957
|
const existing = await readIfExists(path);
|
|
669
958
|
if (existing === content) {
|
|
670
959
|
result.unchanged.push(rel);
|
|
671
960
|
return;
|
|
672
961
|
}
|
|
673
|
-
await (0,
|
|
962
|
+
await (0, import_promises4.mkdir)((0, import_node_path5.dirname)(path), { recursive: true });
|
|
674
963
|
if (existing !== void 0) {
|
|
675
964
|
try {
|
|
676
|
-
await (0,
|
|
965
|
+
await (0, import_promises4.writeFile)(`${path}.bezel-backup`, existing, { flag: "wx", mode: 384 });
|
|
677
966
|
result.backedUp.push(rel);
|
|
678
967
|
} catch (error) {
|
|
679
968
|
if (error.code !== "EEXIST") throw error;
|
|
680
969
|
}
|
|
681
970
|
}
|
|
682
|
-
await (0,
|
|
971
|
+
await (0, import_promises4.writeFile)(path, content, "utf8");
|
|
683
972
|
result.written.push(rel);
|
|
684
973
|
}
|
|
685
974
|
async function installSkills(options) {
|
|
686
|
-
const cwd = (0,
|
|
975
|
+
const cwd = (0, import_node_path5.resolve)(options.cwd ?? process.cwd());
|
|
687
976
|
const result = { written: [], unchanged: [], backedUp: [], hints: [] };
|
|
688
977
|
for (const dir of skillDirectories(options.client)) {
|
|
689
978
|
for (const [file, content] of Object.entries(AGENT_SKILL.files)) {
|
|
690
|
-
await writeManaged(cwd, (0,
|
|
979
|
+
await writeManaged(cwd, (0, import_node_path5.join)(cwd, dir, AGENT_SKILL.name, file), content, result);
|
|
691
980
|
}
|
|
692
981
|
}
|
|
693
982
|
if (options.agentsMd ?? true) {
|
|
694
|
-
const agentsPath = (0,
|
|
983
|
+
const agentsPath = (0, import_node_path5.join)(cwd, "AGENTS.md");
|
|
695
984
|
const existing = await readIfExists(agentsPath) ?? "";
|
|
696
985
|
const merged = mergeAgentsMd(existing);
|
|
697
986
|
if (merged === existing) {
|
|
698
987
|
result.unchanged.push("AGENTS.md");
|
|
699
988
|
} else {
|
|
700
|
-
await (0,
|
|
989
|
+
await (0, import_promises4.writeFile)(agentsPath, merged, "utf8");
|
|
701
990
|
result.written.push("AGENTS.md");
|
|
702
991
|
}
|
|
703
|
-
const claudeMd = await readIfExists((0,
|
|
992
|
+
const claudeMd = await readIfExists((0, import_node_path5.join)(cwd, "CLAUDE.md"));
|
|
704
993
|
if (claudeMd !== void 0 && !claudeMd.includes("@AGENTS.md")) {
|
|
705
994
|
result.hints.push(
|
|
706
995
|
"CLAUDE.md exists and does not include AGENTS.md. Add a line containing `@AGENTS.md` to CLAUDE.md so Claude Code loads the Bezel rules."
|
|
@@ -862,11 +1151,11 @@ async function installMcp(clientName, options = {}) {
|
|
|
862
1151
|
);
|
|
863
1152
|
}
|
|
864
1153
|
} else if (client === "cursor" || client === "vscode") {
|
|
865
|
-
const path = (0,
|
|
1154
|
+
const path = (0, import_node_path6.resolve)(client === "cursor" ? ".cursor/mcp.json" : ".vscode/mcp.json");
|
|
866
1155
|
let raw = "{}";
|
|
867
1156
|
let exists = false;
|
|
868
1157
|
try {
|
|
869
|
-
raw = await (0,
|
|
1158
|
+
raw = await (0, import_promises5.readFile)(path, "utf8");
|
|
870
1159
|
exists = true;
|
|
871
1160
|
} catch (error) {
|
|
872
1161
|
if (error.code !== "ENOENT") throw error;
|
|
@@ -880,9 +1169,9 @@ async function installMcp(clientName, options = {}) {
|
|
|
880
1169
|
);
|
|
881
1170
|
}
|
|
882
1171
|
if (merged !== raw) {
|
|
883
|
-
await (0,
|
|
884
|
-
if (exists) await (0,
|
|
885
|
-
await (0,
|
|
1172
|
+
await (0, import_promises5.mkdir)((0, import_node_path6.dirname)(path), { recursive: true });
|
|
1173
|
+
if (exists) await (0, import_promises5.writeFile)(`${path}.bezel-backup`, raw, { flag: "wx", mode: 384 });
|
|
1174
|
+
await (0, import_promises5.writeFile)(path, merged, { flag: exists ? "w" : "wx" });
|
|
886
1175
|
}
|
|
887
1176
|
console.log(`Bezel configured in ${path}`);
|
|
888
1177
|
} else {
|
|
@@ -930,8 +1219,15 @@ Usage:
|
|
|
930
1219
|
Teach claude-code, codex, cursor, or all about Bezel
|
|
931
1220
|
bezel init [options] Create a bezel.json for this project
|
|
932
1221
|
bezel build [options] Generate the outputs (default command)
|
|
1222
|
+
bezel diff [--json] Local edits not in Bezel yet (vs ${LOCKFILE_NAME});
|
|
1223
|
+
--json prints them as JSON Patch operations for a push
|
|
1224
|
+
bezel sync [--discard-local-edits]
|
|
1225
|
+
Apply the tokens the Bezel MCP returned (${SYNC_STAGING_PATH}):
|
|
1226
|
+
writes ${LOCKFILE_NAME} and design-tokens.json, then builds.
|
|
1227
|
+
Run after every sync and every push.
|
|
933
1228
|
|
|
934
1229
|
Reads design-tokens.json from the project root (the working directory).
|
|
1230
|
+
Commit design-tokens.json and ${LOCKFILE_NAME} together; never edit the lockfile by hand.
|
|
935
1231
|
|
|
936
1232
|
install-mcp options:
|
|
937
1233
|
--skills Also run install-skills for the same client (skips the question)
|
|
@@ -976,9 +1272,9 @@ TypeScript project unless you pass the paths explicitly.
|
|
|
976
1272
|
--project and --tokens-version update only their own key in an existing bezel.json; no --force needed.
|
|
977
1273
|
`;
|
|
978
1274
|
async function loadConfig(path) {
|
|
979
|
-
const abs = (0,
|
|
1275
|
+
const abs = (0, import_node_path7.resolve)(process.cwd(), path);
|
|
980
1276
|
if (/\.json$/.test(abs)) {
|
|
981
|
-
return JSON.parse(await (0,
|
|
1277
|
+
return JSON.parse(await (0, import_promises6.readFile)(abs, "utf8"));
|
|
982
1278
|
}
|
|
983
1279
|
const mod = await import((0, import_node_url.pathToFileURL)(abs).href);
|
|
984
1280
|
return mod.default ?? mod;
|
|
@@ -1002,6 +1298,8 @@ var OPTIONS = {
|
|
|
1002
1298
|
"no-contexts": { type: "boolean" },
|
|
1003
1299
|
"no-fonts": { type: "boolean" },
|
|
1004
1300
|
force: { type: "boolean", short: "f" },
|
|
1301
|
+
json: { type: "boolean" },
|
|
1302
|
+
"discard-local-edits": { type: "boolean" },
|
|
1005
1303
|
help: { type: "boolean", short: "h" }
|
|
1006
1304
|
};
|
|
1007
1305
|
var TOKENS_FILE_HINT = "Add design-tokens.json to the project root \u2014 ask your AI agent for it via the\n Bezel MCP, or download it from your project.";
|
|
@@ -1022,7 +1320,7 @@ async function runInit(values) {
|
|
|
1022
1320
|
projectId: values.project,
|
|
1023
1321
|
version: values["tokens-version"]
|
|
1024
1322
|
});
|
|
1025
|
-
const configName = (0,
|
|
1323
|
+
const configName = (0, import_node_path7.relative)(cwd, result.configPath) || DEFAULT_CONFIG_FILE;
|
|
1026
1324
|
if (result.updated) {
|
|
1027
1325
|
if (values.project) {
|
|
1028
1326
|
const changed = result.previousProjectId && result.previousProjectId !== values.project;
|
|
@@ -1034,6 +1332,10 @@ async function runInit(values) {
|
|
|
1034
1332
|
}
|
|
1035
1333
|
if (result.config.version !== result.previousVersion) {
|
|
1036
1334
|
process.stderr.write(`bezel: set version to ${result.config.version} in ${configName}
|
|
1335
|
+
`);
|
|
1336
|
+
}
|
|
1337
|
+
if (result.removedLockfile) {
|
|
1338
|
+
process.stderr.write(`bezel: removed ${LOCKFILE_NAME} (it belonged to the previous project)
|
|
1037
1339
|
`);
|
|
1038
1340
|
}
|
|
1039
1341
|
if (!result.hasTokensFile) {
|
|
@@ -1070,7 +1372,7 @@ ${TOKENS_FILE_HINT}
|
|
|
1070
1372
|
if (!scripts["tokens"]) {
|
|
1071
1373
|
steps.push('Add "tokens": "bezel build" to your package.json scripts.');
|
|
1072
1374
|
}
|
|
1073
|
-
if (!skillDirectories("all").some((dir) => (0,
|
|
1375
|
+
if (!skillDirectories("all").some((dir) => (0, import_node_fs3.existsSync)((0, import_node_path7.resolve)(cwd, dir, "bezel")))) {
|
|
1074
1376
|
steps.push("Teach your AI coding agent about Bezel: npx bezel install-skills");
|
|
1075
1377
|
}
|
|
1076
1378
|
steps.push(`Run the build, then import ${result.config.variablesOutput} in your entry file.`);
|
|
@@ -1079,7 +1381,7 @@ ${TOKENS_FILE_HINT}
|
|
|
1079
1381
|
`));
|
|
1080
1382
|
}
|
|
1081
1383
|
async function runBuild(values) {
|
|
1082
|
-
const configPath = values.config ?? ((0,
|
|
1384
|
+
const configPath = values.config ?? ((0, import_node_fs3.existsSync)((0, import_node_path7.resolve)(process.cwd(), DEFAULT_CONFIG_FILE)) ? DEFAULT_CONFIG_FILE : void 0);
|
|
1083
1385
|
const fileConfig = configPath ? await loadConfig(configPath) : {};
|
|
1084
1386
|
const options = { ...fileConfig };
|
|
1085
1387
|
if (values["variables-output"]) options.variablesOutput = values["variables-output"];
|
|
@@ -1098,6 +1400,59 @@ async function runBuild(values) {
|
|
|
1098
1400
|
`);
|
|
1099
1401
|
}
|
|
1100
1402
|
}
|
|
1403
|
+
var EXIT_UNPUSHED_EDITS = 2;
|
|
1404
|
+
async function runDiff(values) {
|
|
1405
|
+
const result = await diffTokens({ cwd: process.cwd() });
|
|
1406
|
+
const json = values.json;
|
|
1407
|
+
if (result.ok === false) {
|
|
1408
|
+
if (json) process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
1409
|
+
`);
|
|
1410
|
+
process.stderr.write(`bezel: ${result.message}
|
|
1411
|
+
`);
|
|
1412
|
+
process.exitCode = 1;
|
|
1413
|
+
return;
|
|
1414
|
+
}
|
|
1415
|
+
if (json) {
|
|
1416
|
+
const { projectId, version, baseRevision, operations } = result;
|
|
1417
|
+
process.stdout.write(`${JSON.stringify({ ok: true, projectId, version, baseRevision, operations })}
|
|
1418
|
+
`);
|
|
1419
|
+
return;
|
|
1420
|
+
}
|
|
1421
|
+
const edits = result.operations.filter((operation) => operation.op !== "test");
|
|
1422
|
+
if (edits.length === 0) {
|
|
1423
|
+
process.stdout.write(`In sync with Bezel revision ${result.baseRevision} (${result.version}). No local edits.
|
|
1424
|
+
`);
|
|
1425
|
+
return;
|
|
1426
|
+
}
|
|
1427
|
+
const paths = changedPaths(result.operations);
|
|
1428
|
+
process.stdout.write(
|
|
1429
|
+
`${edits.length} local ${edits.length === 1 ? "edit" : "edits"} not in Bezel yet (based on revision ${result.baseRevision}):
|
|
1430
|
+
`
|
|
1431
|
+
);
|
|
1432
|
+
for (const path of paths.slice(0, 50)) process.stdout.write(` ${path}
|
|
1433
|
+
`);
|
|
1434
|
+
if (paths.length > 50) process.stdout.write(` ...and ${paths.length - 50} more
|
|
1435
|
+
`);
|
|
1436
|
+
process.stdout.write('Push them with the Bezel MCP ("push my token changes to Bezel").\n');
|
|
1437
|
+
}
|
|
1438
|
+
async function runSync(values) {
|
|
1439
|
+
const result = await syncFromStaging({ cwd: process.cwd(), discardLocalEdits: values["discard-local-edits"] });
|
|
1440
|
+
if (result.ok === false) {
|
|
1441
|
+
process.stderr.write(`bezel: ${result.message}
|
|
1442
|
+
`);
|
|
1443
|
+
process.exitCode = result.reason === "unpushed-edits" || result.reason === "unknown-local-edits" ? EXIT_UNPUSHED_EDITS : 1;
|
|
1444
|
+
return;
|
|
1445
|
+
}
|
|
1446
|
+
process.stderr.write(
|
|
1447
|
+
`bezel: synced revision ${result.revision} (${result.version}) of project ${result.projectId} into ${LOCKFILE_NAME}${result.tokensChanged ? " and design-tokens.json" : "; design-tokens.json already matched"}
|
|
1448
|
+
`
|
|
1449
|
+
);
|
|
1450
|
+
if (result.discardedEdits > 0) {
|
|
1451
|
+
process.stderr.write(`bezel: discarded ${result.discardedEdits} local ${result.discardedEdits === 1 ? "edit" : "edits"}
|
|
1452
|
+
`);
|
|
1453
|
+
}
|
|
1454
|
+
await runBuild(values);
|
|
1455
|
+
}
|
|
1101
1456
|
async function main(argv) {
|
|
1102
1457
|
const { values, positionals } = (0, import_node_util.parseArgs)({
|
|
1103
1458
|
args: argv,
|
|
@@ -1121,6 +1476,14 @@ async function main(argv) {
|
|
|
1121
1476
|
await runInit(values);
|
|
1122
1477
|
return;
|
|
1123
1478
|
}
|
|
1479
|
+
if (command === "diff") {
|
|
1480
|
+
await runDiff(values);
|
|
1481
|
+
return;
|
|
1482
|
+
}
|
|
1483
|
+
if (command === "sync") {
|
|
1484
|
+
await runSync(values);
|
|
1485
|
+
return;
|
|
1486
|
+
}
|
|
1124
1487
|
if (command !== "build") {
|
|
1125
1488
|
process.stderr.write(`bezel: unknown command "${command}".
|
|
1126
1489
|
|