@bezel-labs/bezel-kit 0.3.0 → 0.4.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 CHANGED
@@ -51,9 +51,38 @@ any of it with `--dir`, `--variables-output`, `--contexts-output`, `--fonts-outp
51
51
  `--no-contexts`, `--no-fonts`, `--color`, `--unit`. An existing `bezel.json` is never
52
52
  overwritten without `--force`.
53
53
 
54
+ Link the repo to a Bezel project with `--project <uuid>`, and pin which tokens the Bezel
55
+ MCP fetches with `--tokens-version <v>` (`latest` or a published semver like `1.4.0`;
56
+ default `latest`). These two flags update only their own key in an existing `bezel.json`,
57
+ so no `--force` is needed:
58
+
59
+ ```sh
60
+ bezel init --project 0f7a4c2e-1b3d-4e5f-8a9b-0c1d2e3f4a5b --tokens-version 1.4.0
61
+ ```
62
+
54
63
  `build` auto-loads `bezel.json` from the working directory when present. Run
55
64
  `bezel --help` for all options.
56
65
 
66
+ ### Config — `bezel.json`
67
+
68
+ Any `BezelOptions` key can be set in `bezel.json` (`variablesOutput`, `contextsOutput`,
69
+ `fontsOutput`, `colorFormat`, `dimensionUnit`, `nameExtension`, ...). Two keys describe
70
+ the project link rather than the build:
71
+
72
+ - `projectId` (optional) — the Bezel project this repo is linked to (a UUID).
73
+ - `version` (recommended, default `latest`) — the tokens the Bezel MCP fetches: `latest`
74
+ for the project's latest snapshot, or a published semver like `1.4.0`.
75
+
76
+ Both are read only by the Bezel MCP and ignored by `build`.
77
+
78
+ ```json
79
+ {
80
+ "projectId": "0f7a4c2e-1b3d-4e5f-8a9b-0c1d2e3f4a5b",
81
+ "version": "latest",
82
+ "variablesOutput": "src/bezel/variables.css"
83
+ }
84
+ ```
85
+
57
86
  Generated outputs live in their own directory because `build` overwrites them without
58
87
  asking and adds them to `.gitignore` — keeping them out of a hand-written `src/styles`
59
88
  means a generic name like `variables.css` can never clobber a file you wrote.
@@ -71,8 +71,18 @@ ${block}
71
71
  import { existsSync } from "fs";
72
72
  import { readFile, writeFile } from "fs/promises";
73
73
  import { isAbsolute as isAbsolute2, resolve as resolve2 } from "path";
74
+ 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);
75
+ var isTokensVersion = (value) => value === "latest" || /^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/.test(value);
74
76
  var joinPath = (dir, file) => dir === "" ? file : `${dir.replace(/\/+$/, "")}/${file}`;
75
77
  async function initConfig(options = {}) {
78
+ if (options.projectId !== void 0 && !isUuid(options.projectId)) {
79
+ throw new Error(`bezel: --project must be a UUID, got "${options.projectId}"`);
80
+ }
81
+ if (options.version !== void 0 && !isTokensVersion(options.version)) {
82
+ throw new Error(
83
+ `bezel: --tokens-version must be "latest" or a semver like 1.4.0, got "${options.version}"`
84
+ );
85
+ }
76
86
  const cwd = options.cwd ?? process.cwd();
77
87
  const toAbs = (p) => isAbsolute2(p) ? p : resolve2(cwd, p);
78
88
  const configPath = toAbs(DEFAULT_CONFIG_FILE);
@@ -82,9 +92,10 @@ async function initConfig(options = {}) {
82
92
  const outputDir = options.outputDir ?? (hasSrc ? DEFAULT_OUTPUT_DIR : FALLBACK_OUTPUT_DIR);
83
93
  const wantsContexts = options.contextsOutput !== void 0 || (options.contexts ?? isTypeScript);
84
94
  const wantsFonts = options.fontsOutput !== void 0 || (options.fonts ?? isTypeScript);
85
- const config = {
86
- variablesOutput: options.variablesOutput ?? joinPath(outputDir, VARIABLES_FILE)
87
- };
95
+ const config = {};
96
+ if (options.projectId !== void 0) config.projectId = options.projectId;
97
+ config.version = options.version ?? "latest";
98
+ config.variablesOutput = options.variablesOutput ?? joinPath(outputDir, VARIABLES_FILE);
88
99
  if (wantsContexts) {
89
100
  config.contextsOutput = options.contextsOutput ?? joinPath(outputDir, CONTEXTS_FILE);
90
101
  }
@@ -94,11 +105,33 @@ async function initConfig(options = {}) {
94
105
  if (options.colorFormat) config.colorFormat = options.colorFormat;
95
106
  if (options.dimensionUnit) config.dimensionUnit = options.dimensionUnit;
96
107
  if (existsSync(configPath) && !options.force) {
97
- return { configPath, config, written: false, hasTokensFile, isTypeScript };
108
+ const wantsLinkUpdate = options.projectId !== void 0 || options.version !== void 0;
109
+ if (!wantsLinkUpdate) {
110
+ return { configPath, config, written: false, updated: false, hasTokensFile, isTypeScript };
111
+ }
112
+ const existing = JSON.parse(await readFile(configPath, "utf8"));
113
+ const { projectId: previousProjectId, version: previousVersion, ...rest } = existing;
114
+ const merged = {};
115
+ const projectId = options.projectId ?? previousProjectId;
116
+ if (projectId !== void 0) merged.projectId = projectId;
117
+ merged.version = options.version ?? previousVersion ?? "latest";
118
+ Object.assign(merged, rest);
119
+ await writeFile(configPath, `${JSON.stringify(merged, null, 2)}
120
+ `, "utf8");
121
+ return {
122
+ configPath,
123
+ config: merged,
124
+ written: true,
125
+ updated: true,
126
+ previousProjectId,
127
+ previousVersion,
128
+ hasTokensFile,
129
+ isTypeScript
130
+ };
98
131
  }
99
132
  await writeFile(configPath, `${JSON.stringify(config, null, 2)}
100
133
  `, "utf8");
101
- return { configPath, config, written: true, hasTokensFile, isTypeScript };
134
+ return { configPath, config, written: true, updated: false, hasTokensFile, isTypeScript };
102
135
  }
103
136
  async function readPackageScripts(cwd) {
104
137
  try {
@@ -187,4 +220,4 @@ export {
187
220
  readPackageScripts,
188
221
  generateVariablesCss
189
222
  };
190
- //# sourceMappingURL=chunk-FGX7KW2Z.js.map
223
+ //# sourceMappingURL=chunk-RLMGV52B.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/node.ts","../src/config.ts","../src/gitignore.ts","../src/init.ts"],"sourcesContent":["/**\n * bezel/node — Node entry.\n *\n * Re-exports the full isomorphic core, plus the filesystem conveniences:\n * read a DTCG tokens file and write the generated `variables.css` (and optional\n * `contexts.ts`/`fonts.ts`) to disk. This module imports `node:*` and is not\n * browser-safe; for the pure transform use `bezel`.\n */\nimport { mkdir, readFile, writeFile } from \"node:fs/promises\"\nimport { dirname, relative, sep } from \"node:path\"\nimport { resolveOptions, type BezelOptions, type ResolvedOptions } from \"./config\"\nimport { emitCss } from \"./emit\"\nimport { formatContextsModule, getContexts } from \"./contexts\"\nimport { formatFontsModule, getFonts } from \"./fonts\"\nimport { mergeGitignore } from \"./gitignore\"\nimport type { DtcgNode } from \"./types\"\n\nexport * from \"./index\"\nexport { resolveOptions } from \"./config\"\nexport type { BezelOptions, ResolvedOptions } from \"./config\"\nexport { initConfig, readPackageScripts } from \"./init\"\nexport type { InitOptions, InitResult } from \"./init\"\nexport { DEFAULT_CONFIG_FILE, DEFAULT_OUTPUT_DIR, FALLBACK_OUTPUT_DIR } from \"./defaults\"\n\n/**\n * Generate a scoped `variables.css` from a W3C DTCG tokens file.\n *\n * Reads the tokens file, resolves each configured context (with `$root` fallback\n * and reference resolution), and writes one CSS scope per context — the base\n * context in full, others as override-only blocks. Returns the generated CSS.\n *\n * The tokens file is always read from `design-tokens.json` at the project root\n * (resolved against `cwd`); the name and location are fixed and not configurable.\n *\n * @example\n * // zero-config: ./design-tokens.json → ./src/bezel/variables.css\n * await generateVariablesCss()\n *\n * @example\n * await generateVariablesCss({\n * variablesOutput: \"packages/ui/src/bezel/variables.css\",\n * })\n */\nexport async function generateVariablesCss(options: BezelOptions = {}): Promise<string> {\n const resolved = resolveOptions(options)\n\n let raw: string\n try {\n raw = await readFile(resolved.inputPath, \"utf8\")\n } catch (err) {\n throw new Error(\n `bezel: could not read tokens file at \"${resolved.inputPath}\". ` +\n `A \"design-tokens.json\" file must exist at the project root.\\n${(err as Error).message}`,\n )\n }\n\n let tokens: DtcgNode\n try {\n tokens = JSON.parse(raw) as DtcgNode\n } catch (err) {\n throw new Error(`bezel: \"${resolved.inputPath}\" is not valid JSON.\\n${(err as Error).message}`)\n }\n\n const css = emitCss(tokens, resolved)\n\n if (resolved.write) {\n await mkdir(dirname(resolved.variablesOutputPath), { recursive: true })\n await writeFile(resolved.variablesOutputPath, css, \"utf8\")\n\n if (resolved.contextsOutput) {\n const contextsModule = formatContextsModule(getContexts(tokens))\n await mkdir(dirname(resolved.contextsOutput), { recursive: true })\n await writeFile(resolved.contextsOutput, contextsModule, \"utf8\")\n }\n\n if (resolved.fontsOutput) {\n const fontsModule = formatFontsModule(getFonts(tokens))\n await mkdir(dirname(resolved.fontsOutput), { recursive: true })\n await writeFile(resolved.fontsOutput, fontsModule, \"utf8\")\n }\n\n if (resolved.gitignore) {\n await updateGitignore(resolved)\n }\n }\n\n return css\n}\n\n/**\n * Create or update the project `.gitignore` so the generated outputs are ignored.\n * Outputs are written as paths relative to the `.gitignore`, anchored with a leading\n * `/`. Outputs outside the `.gitignore`'s directory can't be anchored from it and are\n * skipped with a warning. The file is only rewritten when its contents actually change.\n */\nasync function updateGitignore(resolved: ResolvedOptions): Promise<void> {\n const dir = dirname(resolved.gitignorePath)\n const outputs = [\n resolved.variablesOutputPath,\n resolved.contextsOutput,\n resolved.fontsOutput,\n ].filter((p): p is string => p !== null)\n\n const entries: string[] = []\n for (const output of outputs) {\n const rel = relative(dir, output)\n if (rel === \"\" || rel.startsWith(\"..\")) {\n process.stderr.write(\n `bezel: skipping .gitignore entry for \"${output}\" — outside ${resolved.gitignorePath}\\n`,\n )\n continue\n }\n entries.push(`/${rel.split(sep).join(\"/\")}`)\n }\n\n if (entries.length === 0) return\n\n let existing = \"\"\n try {\n existing = await readFile(resolved.gitignorePath, \"utf8\")\n } catch {\n // No existing .gitignore — create one.\n }\n\n const next = mergeGitignore(existing, entries)\n if (next !== existing) {\n await mkdir(dir, { recursive: true })\n await writeFile(resolved.gitignorePath, next, \"utf8\")\n }\n}\n","import { isAbsolute, resolve } from \"node:path\"\nimport { DEFAULT_INPUT, DEFAULT_VARIABLES_OUTPUT } from \"./defaults\"\nimport { resolveCssOptions, type CssOptions } from \"./options\"\n\nexport {\n DEFAULT_CONFIG_FILE,\n DEFAULT_CONTEXTS,\n DEFAULT_INPUT,\n DEFAULT_OUTPUT_DIR,\n DEFAULT_VARIABLES_OUTPUT,\n DEFAULT_NAME_EXTENSION,\n FALLBACK_OUTPUT_DIR,\n} from \"./defaults\"\n\n/**\n * Options for {@link generateVariablesCss} — the Node, file-oriented surface.\n * Extends the pure {@link CssOptions} with input/output paths. All optional;\n * defaults make the zero-config case work.\n */\nexport interface BezelOptions extends CssOptions {\n /**\n * The Bezel project this repo is linked to (a UUID). Read only by the Bezel MCP to\n * know which project to pull `design-tokens.json` from — ignored by `build`.\n */\n projectId?: string\n /**\n * Which tokens the Bezel MCP fetches: `\"latest\"` (the project's latest snapshot) or a\n * published semver like `\"1.4.0\"`. Missing means `\"latest\"`. Ignored by `build`.\n */\n version?: string\n /** Path to write the generated CSS. Default: `\"src/bezel/variables.css\"` (resolved from `cwd`). */\n variablesOutput?: string\n /** Base directory paths are resolved against. Default: `process.cwd()`. */\n cwd?: string\n /**\n * When set, also write a generated TypeScript module exporting `CONTEXTS` (the list of\n * context names derived from the tokens). Path resolved against `cwd`. Default: none.\n */\n contextsOutput?: string\n /**\n * When set, also write a generated TypeScript module exporting `FONTS` (the web fonts\n * derived from the tokens). Path resolved against `cwd`. Default: none.\n */\n fontsOutput?: string\n /** When `false`, return the CSS string without touching the filesystem. Default: `true`. */\n write?: boolean\n /**\n * Create/update a project `.gitignore` so the generated outputs are ignored. The\n * entries live in a marked block, rewritten on each run so renamed outputs are\n * cleaned up. Default: `true`.\n */\n gitignore?: boolean\n /** Path to the `.gitignore` to manage. Default: `\".gitignore\"` (resolved from `cwd`). */\n gitignorePath?: string\n}\n\n/** Fully-resolved options (no `undefined`), produced by {@link resolveOptions}. */\nexport interface ResolvedOptions {\n inputPath: string\n variablesOutputPath: string\n contexts: Record<string, string>\n colorFormat: \"oklch\" | \"hex\"\n dimensionUnit: \"preserve\" | \"rem\"\n nameExtension: string\n contextsOutput: string | null\n fontsOutput: string | null\n write: boolean\n gitignore: boolean\n gitignorePath: string\n}\n\n/** Apply defaults and resolve input/output paths against `cwd`. */\nexport function resolveOptions(options: BezelOptions = {}): ResolvedOptions {\n const cwd = options.cwd ?? process.cwd()\n const toAbs = (p: string) => (isAbsolute(p) ? p : resolve(cwd, p))\n\n const css = resolveCssOptions(options)\n\n return {\n // The tokens file is always `design-tokens.json` at the project root — not configurable.\n inputPath: toAbs(DEFAULT_INPUT),\n variablesOutputPath: toAbs(options.variablesOutput ?? DEFAULT_VARIABLES_OUTPUT),\n ...css,\n contextsOutput: options.contextsOutput ? toAbs(options.contextsOutput) : null,\n fontsOutput: options.fontsOutput ? toAbs(options.fontsOutput) : null,\n write: options.write ?? true,\n gitignore: options.gitignore ?? true,\n gitignorePath: toAbs(options.gitignorePath ?? \".gitignore\"),\n }\n}\n","/**\n * bezel — `.gitignore` managed-block merging.\n *\n * Pure string transforms (no `node:fs`): given the existing `.gitignore` contents\n * and the list of generated outputs to ignore, produce the new contents. Bezel's\n * entries live inside a marked block so repeated runs rewrite just that span —\n * renamed/removed outputs drop out automatically and entries never duplicate, while\n * the user's own rules outside the markers are preserved.\n */\n\n/** Opening marker for the Bezel-managed block. */\nexport const START = \"# bezel:start (generated — do not edit)\"\n/** Closing marker for the Bezel-managed block. */\nexport const END = \"# bezel:end\"\n\n/**\n * Merge Bezel's managed block into existing `.gitignore` contents.\n *\n * - Empty `existing` → just the block.\n * - An existing block (delimited by {@link START}…{@link END}) → replaced in place,\n * preserving everything before and after.\n * - No block → appended after the existing content, separated by a blank line.\n *\n * The result always ends with a single trailing newline.\n */\nexport function mergeGitignore(existing: string, entries: string[]): string {\n const block = [START, ...entries, END].join(\"\\n\")\n\n if (existing.trim() === \"\") return `${block}\\n`\n\n const start = existing.indexOf(START)\n if (start !== -1) {\n const endMarker = existing.indexOf(END, start)\n if (endMarker !== -1) {\n const before = existing.slice(0, start).replace(/\\n+$/, \"\")\n const after = existing.slice(endMarker + END.length).replace(/^\\n+/, \"\")\n const head = before === \"\" ? \"\" : `${before}\\n\\n`\n const tail = after === \"\" ? \"\" : `\\n\\n${after.replace(/\\n+$/, \"\")}`\n return `${head}${block}${tail}\\n`\n }\n }\n\n return `${existing.replace(/\\n+$/, \"\")}\\n\\n${block}\\n`\n}\n","/**\n * bezel — `bezel.json` scaffolding.\n *\n * Writes the config file `bezel build` auto-loads, so a project doesn't have to\n * hand-author it. Defaults are derived from the project itself: outputs land in\n * `src/bezel/` when there's a `src/` (otherwise `bezel/`), and the TypeScript\n * modules are only scaffolded for a TypeScript project — `contexts.ts`/`fonts.ts`\n * are emitted as TS (`as const`, `export type`) and would be dead weight in a\n * JavaScript one.\n *\n * Non-destructive: an existing `bezel.json` is never overwritten without `force`.\n * The one exception is the project link: passing `projectId` and/or `version` to an\n * existing config updates just those keys in place, leaving everything else as-is.\n */\nimport { existsSync } from \"node:fs\"\nimport { readFile, writeFile } from \"node:fs/promises\"\nimport { isAbsolute, resolve } from \"node:path\"\nimport type { BezelOptions } from \"./config\"\nimport {\n CONTEXTS_FILE,\n DEFAULT_CONFIG_FILE,\n DEFAULT_INPUT,\n DEFAULT_OUTPUT_DIR,\n FALLBACK_OUTPUT_DIR,\n FONTS_FILE,\n VARIABLES_FILE,\n} from \"./defaults\"\n\n/** Options for {@link initConfig}. All optional — the zero-arg call is the happy path. */\nexport interface InitOptions {\n /** Project root to scaffold into. Default: `process.cwd()`. */\n cwd?: string\n /**\n * Directory for all three outputs, relative to `cwd`. Default: `\"src/bezel\"` when\n * the project has a `src/`, otherwise `\"bezel\"`. Ignored for any output whose path\n * is set explicitly.\n */\n outputDir?: string\n /** Explicit path for the generated CSS. Overrides {@link InitOptions.outputDir}. */\n variablesOutput?: string\n /** Explicit path for the generated contexts module. Implies `contexts: true`. */\n contextsOutput?: string\n /** Explicit path for the generated fonts module. Implies `fonts: true`. */\n fontsOutput?: string\n /** Include `contextsOutput`. Default: true for a TypeScript project. */\n contexts?: boolean\n /** Include `fontsOutput`. Default: true for a TypeScript project. */\n fonts?: boolean\n /** Written to the config only when set — otherwise the build default applies. */\n colorFormat?: \"oklch\" | \"hex\"\n /** Written to the config only when set — otherwise the build default applies. */\n dimensionUnit?: \"preserve\" | \"rem\"\n /** Overwrite an existing `bezel.json`. Default: `false`. */\n force?: boolean\n /**\n * The Bezel project to link this repo to (a UUID). Written as `projectId`; on an\n * existing config it updates just that key without needing `force`.\n */\n projectId?: string\n /**\n * Tokens version the Bezel MCP fetches: `\"latest\"` or a semver like `\"1.4.0\"`.\n * Always written — defaults to `\"latest\"` on a fresh init; on an existing config it\n * updates just that key without needing `force`.\n */\n version?: string\n}\n\n/** Outcome of {@link initConfig}. */\nexport interface InitResult {\n /** Absolute path of the config file. */\n configPath: string\n /** The config that was written — or the one that would have been, when `written` is false. */\n config: BezelOptions\n /** False when an existing `bezel.json` was left untouched (no `force`). */\n written: boolean\n /** Whether a `design-tokens.json` is already present at the project root. */\n hasTokensFile: boolean\n /** Whether the project looks like TypeScript (drives the contexts/fonts defaults). */\n isTypeScript: boolean\n /** True when only `projectId`/`version` were merged into an existing `bezel.json`. */\n updated: boolean\n /** The `projectId` that was in the file before an update, if any. */\n previousProjectId?: string\n /** The `version` that was in the file before an update, if any. */\n previousVersion?: string\n}\n\n/** Whether `value` is a UUID — the shape of a Bezel project id. */\nexport const isUuid = (value: string): boolean =>\n /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value)\n\n/** Whether `value` is a valid tokens version: `\"latest\"` or a semver like `1.4.0`. */\nexport const isTokensVersion = (value: string): boolean =>\n value === \"latest\" || /^\\d+\\.\\d+\\.\\d+(-[0-9A-Za-z.-]+)?$/.test(value)\n\n/**\n * Join config path segments with forward slashes.\n *\n * Config values are portable JSON, so they always use `/` — `node:path.join` would\n * emit backslashes on Windows and bake a platform into a committed file.\n */\nconst joinPath = (dir: string, file: string): string =>\n dir === \"\" ? file : `${dir.replace(/\\/+$/, \"\")}/${file}`\n\n/**\n * Create a `bezel.json` in the project root, inferring sensible defaults from the\n * project layout.\n *\n * @example\n * // src/bezel/{variables.css,contexts.ts,fonts.ts} for a TS project with a src/\n * await initConfig()\n *\n * @example\n * await initConfig({ outputDir: \"packages/ui/src/bezel\", fonts: false })\n */\nexport async function initConfig(options: InitOptions = {}): Promise<InitResult> {\n if (options.projectId !== undefined && !isUuid(options.projectId)) {\n throw new Error(`bezel: --project must be a UUID, got \"${options.projectId}\"`)\n }\n if (options.version !== undefined && !isTokensVersion(options.version)) {\n throw new Error(\n `bezel: --tokens-version must be \"latest\" or a semver like 1.4.0, got \"${options.version}\"`,\n )\n }\n\n const cwd = options.cwd ?? process.cwd()\n const toAbs = (p: string) => (isAbsolute(p) ? p : resolve(cwd, p))\n\n const configPath = toAbs(DEFAULT_CONFIG_FILE)\n const hasTokensFile = existsSync(toAbs(DEFAULT_INPUT))\n const isTypeScript = existsSync(toAbs(\"tsconfig.json\"))\n\n const hasSrc = existsSync(toAbs(\"src\"))\n const outputDir = options.outputDir ?? (hasSrc ? DEFAULT_OUTPUT_DIR : FALLBACK_OUTPUT_DIR)\n\n // An explicit path is itself an opt-in; otherwise TypeScript projects get the\n // generated modules and JavaScript ones don't.\n const wantsContexts = options.contextsOutput !== undefined || (options.contexts ?? isTypeScript)\n const wantsFonts = options.fontsOutput !== undefined || (options.fonts ?? isTypeScript)\n\n // Link keys come first so the file reads project → version → outputs.\n const config: BezelOptions = {}\n if (options.projectId !== undefined) config.projectId = options.projectId\n config.version = options.version ?? \"latest\"\n config.variablesOutput = options.variablesOutput ?? joinPath(outputDir, VARIABLES_FILE)\n if (wantsContexts) {\n config.contextsOutput = options.contextsOutput ?? joinPath(outputDir, CONTEXTS_FILE)\n }\n if (wantsFonts) {\n config.fontsOutput = options.fontsOutput ?? joinPath(outputDir, FONTS_FILE)\n }\n // Only persist the transform options that were asked for, so the file stays minimal\n // and keeps tracking the library defaults for everything else.\n if (options.colorFormat) config.colorFormat = options.colorFormat\n if (options.dimensionUnit) config.dimensionUnit = options.dimensionUnit\n\n if (existsSync(configPath) && !options.force) {\n const wantsLinkUpdate = options.projectId !== undefined || options.version !== undefined\n if (!wantsLinkUpdate) {\n return { configPath, config, written: false, updated: false, hasTokensFile, isTypeScript }\n }\n\n // Link-only update: replace just the two link keys, keep every other key in place.\n const existing = JSON.parse(await readFile(configPath, \"utf8\")) as BezelOptions\n const { projectId: previousProjectId, version: previousVersion, ...rest } = existing\n const merged: BezelOptions = {}\n const projectId = options.projectId ?? previousProjectId\n if (projectId !== undefined) merged.projectId = projectId\n merged.version = options.version ?? previousVersion ?? \"latest\"\n Object.assign(merged, rest)\n\n await writeFile(configPath, `${JSON.stringify(merged, null, 2)}\\n`, \"utf8\")\n\n return {\n configPath,\n config: merged,\n written: true,\n updated: true,\n previousProjectId,\n previousVersion,\n hasTokensFile,\n isTypeScript,\n }\n }\n\n await writeFile(configPath, `${JSON.stringify(config, null, 2)}\\n`, \"utf8\")\n\n return { configPath, config, written: true, updated: false, hasTokensFile, isTypeScript }\n}\n\n/**\n * Read the `\"scripts\"` block of the project's `package.json`, if there is one.\n * Used only to tailor the post-init hint — a missing or unparsable file is not an error.\n */\nexport async function readPackageScripts(cwd: string): Promise<Record<string, string>> {\n try {\n const raw = await readFile(resolve(cwd, \"package.json\"), \"utf8\")\n const parsed = JSON.parse(raw) as { scripts?: Record<string, string> }\n return parsed.scripts ?? {}\n } catch {\n return {}\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAQA,SAAS,OAAO,YAAAA,WAAU,aAAAC,kBAAiB;AAC3C,SAAS,SAAS,UAAU,WAAW;;;ACTvC,SAAS,YAAY,eAAe;AAwE7B,SAAS,eAAe,UAAwB,CAAC,GAAoB;AAC1E,QAAM,MAAM,QAAQ,OAAO,QAAQ,IAAI;AACvC,QAAM,QAAQ,CAAC,MAAe,WAAW,CAAC,IAAI,IAAI,QAAQ,KAAK,CAAC;AAEhE,QAAM,MAAM,kBAAkB,OAAO;AAErC,SAAO;AAAA;AAAA,IAEL,WAAW,MAAM,aAAa;AAAA,IAC9B,qBAAqB,MAAM,QAAQ,mBAAmB,wBAAwB;AAAA,IAC9E,GAAG;AAAA,IACH,gBAAgB,QAAQ,iBAAiB,MAAM,QAAQ,cAAc,IAAI;AAAA,IACzE,aAAa,QAAQ,cAAc,MAAM,QAAQ,WAAW,IAAI;AAAA,IAChE,OAAO,QAAQ,SAAS;AAAA,IACxB,WAAW,QAAQ,aAAa;AAAA,IAChC,eAAe,MAAM,QAAQ,iBAAiB,YAAY;AAAA,EAC5D;AACF;;;AC9EO,IAAM,QAAQ;AAEd,IAAM,MAAM;AAYZ,SAAS,eAAe,UAAkB,SAA2B;AAC1E,QAAM,QAAQ,CAAC,OAAO,GAAG,SAAS,GAAG,EAAE,KAAK,IAAI;AAEhD,MAAI,SAAS,KAAK,MAAM,GAAI,QAAO,GAAG,KAAK;AAAA;AAE3C,QAAM,QAAQ,SAAS,QAAQ,KAAK;AACpC,MAAI,UAAU,IAAI;AAChB,UAAM,YAAY,SAAS,QAAQ,KAAK,KAAK;AAC7C,QAAI,cAAc,IAAI;AACpB,YAAM,SAAS,SAAS,MAAM,GAAG,KAAK,EAAE,QAAQ,QAAQ,EAAE;AAC1D,YAAM,QAAQ,SAAS,MAAM,YAAY,IAAI,MAAM,EAAE,QAAQ,QAAQ,EAAE;AACvE,YAAM,OAAO,WAAW,KAAK,KAAK,GAAG,MAAM;AAAA;AAAA;AAC3C,YAAM,OAAO,UAAU,KAAK,KAAK;AAAA;AAAA,EAAO,MAAM,QAAQ,QAAQ,EAAE,CAAC;AACjE,aAAO,GAAG,IAAI,GAAG,KAAK,GAAG,IAAI;AAAA;AAAA,IAC/B;AAAA,EACF;AAEA,SAAO,GAAG,SAAS,QAAQ,QAAQ,EAAE,CAAC;AAAA;AAAA,EAAO,KAAK;AAAA;AACpD;;;AC7BA,SAAS,kBAAkB;AAC3B,SAAS,UAAU,iBAAiB;AACpC,SAAS,cAAAC,aAAY,WAAAC,gBAAe;AAwE7B,IAAM,SAAS,CAAC,UACrB,kEAAkE,KAAK,KAAK;AAGvE,IAAM,kBAAkB,CAAC,UAC9B,UAAU,YAAY,oCAAoC,KAAK,KAAK;AAQtE,IAAM,WAAW,CAAC,KAAa,SAC7B,QAAQ,KAAK,OAAO,GAAG,IAAI,QAAQ,QAAQ,EAAE,CAAC,IAAI,IAAI;AAaxD,eAAsB,WAAW,UAAuB,CAAC,GAAwB;AAC/E,MAAI,QAAQ,cAAc,UAAa,CAAC,OAAO,QAAQ,SAAS,GAAG;AACjE,UAAM,IAAI,MAAM,yCAAyC,QAAQ,SAAS,GAAG;AAAA,EAC/E;AACA,MAAI,QAAQ,YAAY,UAAa,CAAC,gBAAgB,QAAQ,OAAO,GAAG;AACtE,UAAM,IAAI;AAAA,MACR,yEAAyE,QAAQ,OAAO;AAAA,IAC1F;AAAA,EACF;AAEA,QAAM,MAAM,QAAQ,OAAO,QAAQ,IAAI;AACvC,QAAM,QAAQ,CAAC,MAAeC,YAAW,CAAC,IAAI,IAAIC,SAAQ,KAAK,CAAC;AAEhE,QAAM,aAAa,MAAM,mBAAmB;AAC5C,QAAM,gBAAgB,WAAW,MAAM,aAAa,CAAC;AACrD,QAAM,eAAe,WAAW,MAAM,eAAe,CAAC;AAEtD,QAAM,SAAS,WAAW,MAAM,KAAK,CAAC;AACtC,QAAM,YAAY,QAAQ,cAAc,SAAS,qBAAqB;AAItE,QAAM,gBAAgB,QAAQ,mBAAmB,WAAc,QAAQ,YAAY;AACnF,QAAM,aAAa,QAAQ,gBAAgB,WAAc,QAAQ,SAAS;AAG1E,QAAM,SAAuB,CAAC;AAC9B,MAAI,QAAQ,cAAc,OAAW,QAAO,YAAY,QAAQ;AAChE,SAAO,UAAU,QAAQ,WAAW;AACpC,SAAO,kBAAkB,QAAQ,mBAAmB,SAAS,WAAW,cAAc;AACtF,MAAI,eAAe;AACjB,WAAO,iBAAiB,QAAQ,kBAAkB,SAAS,WAAW,aAAa;AAAA,EACrF;AACA,MAAI,YAAY;AACd,WAAO,cAAc,QAAQ,eAAe,SAAS,WAAW,UAAU;AAAA,EAC5E;AAGA,MAAI,QAAQ,YAAa,QAAO,cAAc,QAAQ;AACtD,MAAI,QAAQ,cAAe,QAAO,gBAAgB,QAAQ;AAE1D,MAAI,WAAW,UAAU,KAAK,CAAC,QAAQ,OAAO;AAC5C,UAAM,kBAAkB,QAAQ,cAAc,UAAa,QAAQ,YAAY;AAC/E,QAAI,CAAC,iBAAiB;AACpB,aAAO,EAAE,YAAY,QAAQ,SAAS,OAAO,SAAS,OAAO,eAAe,aAAa;AAAA,IAC3F;AAGA,UAAM,WAAW,KAAK,MAAM,MAAM,SAAS,YAAY,MAAM,CAAC;AAC9D,UAAM,EAAE,WAAW,mBAAmB,SAAS,iBAAiB,GAAG,KAAK,IAAI;AAC5E,UAAM,SAAuB,CAAC;AAC9B,UAAM,YAAY,QAAQ,aAAa;AACvC,QAAI,cAAc,OAAW,QAAO,YAAY;AAChD,WAAO,UAAU,QAAQ,WAAW,mBAAmB;AACvD,WAAO,OAAO,QAAQ,IAAI;AAE1B,UAAM,UAAU,YAAY,GAAG,KAAK,UAAU,QAAQ,MAAM,CAAC,CAAC;AAAA,GAAM,MAAM;AAE1E,WAAO;AAAA,MACL;AAAA,MACA,QAAQ;AAAA,MACR,SAAS;AAAA,MACT,SAAS;AAAA,MACT;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAEA,QAAM,UAAU,YAAY,GAAG,KAAK,UAAU,QAAQ,MAAM,CAAC,CAAC;AAAA,GAAM,MAAM;AAE1E,SAAO,EAAE,YAAY,QAAQ,SAAS,MAAM,SAAS,OAAO,eAAe,aAAa;AAC1F;AAMA,eAAsB,mBAAmB,KAA8C;AACrF,MAAI;AACF,UAAM,MAAM,MAAM,SAASA,SAAQ,KAAK,cAAc,GAAG,MAAM;AAC/D,UAAM,SAAS,KAAK,MAAM,GAAG;AAC7B,WAAO,OAAO,WAAW,CAAC;AAAA,EAC5B,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AACF;;;AH/JA,eAAsB,qBAAqB,UAAwB,CAAC,GAAoB;AACtF,QAAM,WAAW,eAAe,OAAO;AAEvC,MAAI;AACJ,MAAI;AACF,UAAM,MAAMC,UAAS,SAAS,WAAW,MAAM;AAAA,EACjD,SAAS,KAAK;AACZ,UAAM,IAAI;AAAA,MACR,yCAAyC,SAAS,SAAS;AAAA,EACQ,IAAc,OAAO;AAAA,IAC1F;AAAA,EACF;AAEA,MAAI;AACJ,MAAI;AACF,aAAS,KAAK,MAAM,GAAG;AAAA,EACzB,SAAS,KAAK;AACZ,UAAM,IAAI,MAAM,WAAW,SAAS,SAAS;AAAA,EAA0B,IAAc,OAAO,EAAE;AAAA,EAChG;AAEA,QAAM,MAAM,QAAQ,QAAQ,QAAQ;AAEpC,MAAI,SAAS,OAAO;AAClB,UAAM,MAAM,QAAQ,SAAS,mBAAmB,GAAG,EAAE,WAAW,KAAK,CAAC;AACtE,UAAMC,WAAU,SAAS,qBAAqB,KAAK,MAAM;AAEzD,QAAI,SAAS,gBAAgB;AAC3B,YAAM,iBAAiB,qBAAqB,YAAY,MAAM,CAAC;AAC/D,YAAM,MAAM,QAAQ,SAAS,cAAc,GAAG,EAAE,WAAW,KAAK,CAAC;AACjE,YAAMA,WAAU,SAAS,gBAAgB,gBAAgB,MAAM;AAAA,IACjE;AAEA,QAAI,SAAS,aAAa;AACxB,YAAM,cAAc,kBAAkB,SAAS,MAAM,CAAC;AACtD,YAAM,MAAM,QAAQ,SAAS,WAAW,GAAG,EAAE,WAAW,KAAK,CAAC;AAC9D,YAAMA,WAAU,SAAS,aAAa,aAAa,MAAM;AAAA,IAC3D;AAEA,QAAI,SAAS,WAAW;AACtB,YAAM,gBAAgB,QAAQ;AAAA,IAChC;AAAA,EACF;AAEA,SAAO;AACT;AAQA,eAAe,gBAAgB,UAA0C;AACvE,QAAM,MAAM,QAAQ,SAAS,aAAa;AAC1C,QAAM,UAAU;AAAA,IACd,SAAS;AAAA,IACT,SAAS;AAAA,IACT,SAAS;AAAA,EACX,EAAE,OAAO,CAAC,MAAmB,MAAM,IAAI;AAEvC,QAAM,UAAoB,CAAC;AAC3B,aAAW,UAAU,SAAS;AAC5B,UAAM,MAAM,SAAS,KAAK,MAAM;AAChC,QAAI,QAAQ,MAAM,IAAI,WAAW,IAAI,GAAG;AACtC,cAAQ,OAAO;AAAA,QACb,yCAAyC,MAAM,oBAAe,SAAS,aAAa;AAAA;AAAA,MACtF;AACA;AAAA,IACF;AACA,YAAQ,KAAK,IAAI,IAAI,MAAM,GAAG,EAAE,KAAK,GAAG,CAAC,EAAE;AAAA,EAC7C;AAEA,MAAI,QAAQ,WAAW,EAAG;AAE1B,MAAI,WAAW;AACf,MAAI;AACF,eAAW,MAAMD,UAAS,SAAS,eAAe,MAAM;AAAA,EAC1D,QAAQ;AAAA,EAER;AAEA,QAAM,OAAO,eAAe,UAAU,OAAO;AAC7C,MAAI,SAAS,UAAU;AACrB,UAAM,MAAM,KAAK,EAAE,WAAW,KAAK,CAAC;AACpC,UAAMC,WAAU,SAAS,eAAe,MAAM,MAAM;AAAA,EACtD;AACF;","names":["readFile","writeFile","isAbsolute","resolve","isAbsolute","resolve","readFile","writeFile"]}
package/dist/cli.cjs CHANGED
@@ -422,8 +422,18 @@ ${block}
422
422
  var import_node_fs = require("fs");
423
423
  var import_promises = require("fs/promises");
424
424
  var import_node_path2 = require("path");
425
+ 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);
426
+ var isTokensVersion = (value) => value === "latest" || /^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/.test(value);
425
427
  var joinPath = (dir, file) => dir === "" ? file : `${dir.replace(/\/+$/, "")}/${file}`;
426
428
  async function initConfig(options = {}) {
429
+ if (options.projectId !== void 0 && !isUuid(options.projectId)) {
430
+ throw new Error(`bezel: --project must be a UUID, got "${options.projectId}"`);
431
+ }
432
+ if (options.version !== void 0 && !isTokensVersion(options.version)) {
433
+ throw new Error(
434
+ `bezel: --tokens-version must be "latest" or a semver like 1.4.0, got "${options.version}"`
435
+ );
436
+ }
427
437
  const cwd = options.cwd ?? process.cwd();
428
438
  const toAbs = (p) => (0, import_node_path2.isAbsolute)(p) ? p : (0, import_node_path2.resolve)(cwd, p);
429
439
  const configPath = toAbs(DEFAULT_CONFIG_FILE);
@@ -433,9 +443,10 @@ async function initConfig(options = {}) {
433
443
  const outputDir = options.outputDir ?? (hasSrc ? DEFAULT_OUTPUT_DIR : FALLBACK_OUTPUT_DIR);
434
444
  const wantsContexts = options.contextsOutput !== void 0 || (options.contexts ?? isTypeScript);
435
445
  const wantsFonts = options.fontsOutput !== void 0 || (options.fonts ?? isTypeScript);
436
- const config = {
437
- variablesOutput: options.variablesOutput ?? joinPath(outputDir, VARIABLES_FILE)
438
- };
446
+ const config = {};
447
+ if (options.projectId !== void 0) config.projectId = options.projectId;
448
+ config.version = options.version ?? "latest";
449
+ config.variablesOutput = options.variablesOutput ?? joinPath(outputDir, VARIABLES_FILE);
439
450
  if (wantsContexts) {
440
451
  config.contextsOutput = options.contextsOutput ?? joinPath(outputDir, CONTEXTS_FILE);
441
452
  }
@@ -445,11 +456,33 @@ async function initConfig(options = {}) {
445
456
  if (options.colorFormat) config.colorFormat = options.colorFormat;
446
457
  if (options.dimensionUnit) config.dimensionUnit = options.dimensionUnit;
447
458
  if ((0, import_node_fs.existsSync)(configPath) && !options.force) {
448
- return { configPath, config, written: false, hasTokensFile, isTypeScript };
459
+ const wantsLinkUpdate = options.projectId !== void 0 || options.version !== void 0;
460
+ if (!wantsLinkUpdate) {
461
+ return { configPath, config, written: false, updated: false, hasTokensFile, isTypeScript };
462
+ }
463
+ const existing = JSON.parse(await (0, import_promises.readFile)(configPath, "utf8"));
464
+ const { projectId: previousProjectId, version: previousVersion, ...rest } = existing;
465
+ const merged = {};
466
+ const projectId = options.projectId ?? previousProjectId;
467
+ if (projectId !== void 0) merged.projectId = projectId;
468
+ merged.version = options.version ?? previousVersion ?? "latest";
469
+ Object.assign(merged, rest);
470
+ await (0, import_promises.writeFile)(configPath, `${JSON.stringify(merged, null, 2)}
471
+ `, "utf8");
472
+ return {
473
+ configPath,
474
+ config: merged,
475
+ written: true,
476
+ updated: true,
477
+ previousProjectId,
478
+ previousVersion,
479
+ hasTokensFile,
480
+ isTypeScript
481
+ };
449
482
  }
450
483
  await (0, import_promises.writeFile)(configPath, `${JSON.stringify(config, null, 2)}
451
484
  `, "utf8");
452
- return { configPath, config, written: true, hasTokensFile, isTypeScript };
485
+ return { configPath, config, written: true, updated: false, hasTokensFile, isTypeScript };
453
486
  }
454
487
  async function readPackageScripts(cwd) {
455
488
  try {
@@ -551,6 +584,8 @@ init options:
551
584
  --no-fonts Skip the generated FONTS module
552
585
  --color <format> Color output: oklch | hex (default: oklch)
553
586
  --unit <mode> Dimension unit: preserve | rem (default: preserve)
587
+ --project <uuid> Link this repo to a Bezel project (writes projectId)
588
+ --tokens-version <v> Tokens version the Bezel MCP fetches: latest | semver (default: latest)
554
589
  -f, --force Overwrite an existing bezel.json
555
590
 
556
591
  build options:
@@ -568,6 +603,7 @@ build options:
568
603
  A ${DEFAULT_CONFIG_FILE} file in the working directory is loaded automatically when -c is omitted.
569
604
  The contexts/fonts modules are TypeScript, so init only scaffolds them for a
570
605
  TypeScript project unless you pass the paths explicitly.
606
+ --project and --tokens-version update only their own key in an existing bezel.json; no --force needed.
571
607
  `;
572
608
  async function loadConfig(path) {
573
609
  const abs = (0, import_node_path4.resolve)(process.cwd(), path);
@@ -585,6 +621,8 @@ var OPTIONS = {
585
621
  "fonts-output": { type: "string" },
586
622
  color: { type: "string" },
587
623
  unit: { type: "string" },
624
+ project: { type: "string" },
625
+ "tokens-version": { type: "string" },
588
626
  stdout: { type: "boolean" },
589
627
  "no-gitignore": { type: "boolean" },
590
628
  "no-contexts": { type: "boolean" },
@@ -592,6 +630,7 @@ var OPTIONS = {
592
630
  force: { type: "boolean", short: "f" },
593
631
  help: { type: "boolean", short: "h" }
594
632
  };
633
+ 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.";
595
634
  async function runInit(values) {
596
635
  const cwd = process.cwd();
597
636
  const result = await initConfig({
@@ -604,12 +643,34 @@ async function runInit(values) {
604
643
  fonts: values["no-fonts"] ? false : void 0,
605
644
  colorFormat: values.color,
606
645
  dimensionUnit: values.unit,
607
- force: values.force
646
+ force: values.force,
647
+ projectId: values.project,
648
+ version: values["tokens-version"]
608
649
  });
609
650
  const configName = (0, import_node_path4.relative)(cwd, result.configPath) || DEFAULT_CONFIG_FILE;
651
+ if (result.updated) {
652
+ if (values.project) {
653
+ const changed = result.previousProjectId && result.previousProjectId !== values.project;
654
+ process.stderr.write(
655
+ changed ? `bezel: updated projectId in ${configName} (was ${result.previousProjectId})
656
+ ` : `bezel: set projectId in ${configName}
657
+ `
658
+ );
659
+ }
660
+ if (result.config.version !== result.previousVersion) {
661
+ process.stderr.write(`bezel: set version to ${result.config.version} in ${configName}
662
+ `);
663
+ }
664
+ if (!result.hasTokensFile) {
665
+ process.stderr.write(`
666
+ ${TOKENS_FILE_HINT}
667
+ `);
668
+ }
669
+ return;
670
+ }
610
671
  if (!result.written) {
611
672
  process.stderr.write(
612
- `bezel: ${configName} already exists \u2014 leaving it alone. Re-run with --force to overwrite.
673
+ `bezel: ${configName} already exists \u2014 leaving it alone. Re-run with --force to overwrite, or pass --project <uuid> / --tokens-version <v> to just update the project link.
613
674
  `
614
675
  );
615
676
  process.exitCode = 1;
@@ -629,9 +690,7 @@ async function runInit(values) {
629
690
  const scripts = await readPackageScripts(cwd);
630
691
  const steps = [];
631
692
  if (!result.hasTokensFile) {
632
- steps.push(
633
- "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."
634
- );
693
+ steps.push(TOKENS_FILE_HINT);
635
694
  }
636
695
  if (!scripts["tokens"]) {
637
696
  steps.push('Add "tokens": "bezel build" to your package.json scripts.');