@flashdsm/genome 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,3775 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ GENOME_CONFIG_FILENAME,
4
+ GenomeConfigSchema,
5
+ LEGACY_CONFIG_FILENAME,
6
+ TokenCategorySchema,
7
+ TokenSchema,
8
+ TokenTypeSchema,
9
+ configCandidates,
10
+ getDesignTokens,
11
+ getImpact,
12
+ getModule,
13
+ getRelations,
14
+ listModules,
15
+ loadTokensFromFile,
16
+ makeChangePlan,
17
+ normalizeTokenDocument,
18
+ parseCategoryInput,
19
+ parseFlashAtomicDocument,
20
+ resolveTokenFilePath,
21
+ tokenChangeToPlanEntry
22
+ } from "./chunk-6OCQZNS7.js";
23
+
24
+ // src/index.ts
25
+ import { resolve as resolve11 } from "path";
26
+ import { fileURLToPath } from "url";
27
+ import {
28
+ McpServer
29
+ } from "@modelcontextprotocol/sdk/server/mcp.js";
30
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
31
+ import { z as z2 } from "zod";
32
+ import { readFileSync, realpathSync } from "fs";
33
+ import { readFile as readFile12 } from "fs/promises";
34
+
35
+ // src/api.ts
36
+ var ApiError = class extends Error {
37
+ constructor(message, status, body) {
38
+ super(message);
39
+ this.status = status;
40
+ this.body = body;
41
+ this.name = "ApiError";
42
+ }
43
+ status;
44
+ body;
45
+ };
46
+ function createApiClient(opts) {
47
+ const baseUrl = opts.baseUrl.replace(/\/$/, "");
48
+ async function request(path, init) {
49
+ const headers = new Headers({ "Content-Type": "application/json" });
50
+ if (opts.accessToken) {
51
+ headers.set("Authorization", `Bearer ${opts.accessToken}`);
52
+ }
53
+ const res = await fetch(`${baseUrl}${path}`, { ...init, headers });
54
+ const text = await res.text();
55
+ let body;
56
+ if (text) {
57
+ try {
58
+ body = JSON.parse(text);
59
+ } catch {
60
+ body = text;
61
+ }
62
+ }
63
+ if (!res.ok) {
64
+ const msg = typeof body === "object" && body && "detail" in body && typeof body.detail === "string" ? body.detail : `HTTP ${res.status}`;
65
+ throw new ApiError(msg, res.status, body);
66
+ }
67
+ return body;
68
+ }
69
+ return {
70
+ health: () => request("/health"),
71
+ listDesignSystems: () => request("/api/v1/design-systems/"),
72
+ createFromCodeScan: (body) => request("/api/v1/design-systems/from-code-scan", {
73
+ method: "POST",
74
+ body: JSON.stringify(body)
75
+ }),
76
+ getDesignSystem: (id) => request(`/api/v1/design-systems/${id}`)
77
+ };
78
+ }
79
+
80
+ // src/load-components.ts
81
+ import { readFile } from "fs/promises";
82
+ import { dirname, isAbsolute, resolve } from "path";
83
+ function isRecord(value) {
84
+ return typeof value === "object" && value !== null && !Array.isArray(value);
85
+ }
86
+ function parseComponent(value) {
87
+ if (!isRecord(value) || typeof value.name !== "string") return null;
88
+ return value;
89
+ }
90
+ function normalizeComponentDocument(raw) {
91
+ if (Array.isArray(raw)) {
92
+ return {
93
+ components: raw.map(parseComponent).filter((c) => c !== null)
94
+ };
95
+ }
96
+ if (!isRecord(raw)) {
97
+ return { components: [] };
98
+ }
99
+ const name = typeof raw.name === "string" ? raw.name : void 0;
100
+ const list = Array.isArray(raw.components) ? raw.components : [];
101
+ return {
102
+ name,
103
+ components: list.map(parseComponent).filter((c) => c !== null)
104
+ };
105
+ }
106
+ async function readJsonObject(path) {
107
+ try {
108
+ const text = await readFile(path, "utf8");
109
+ const parsed = JSON.parse(text);
110
+ return isRecord(parsed) ? parsed : null;
111
+ } catch {
112
+ return null;
113
+ }
114
+ }
115
+ async function resolveComponentsFilePath(options) {
116
+ const cwd = options.cwd ?? process.cwd();
117
+ if (options.componentsPath) {
118
+ return isAbsolute(options.componentsPath) ? options.componentsPath : resolve(cwd, options.componentsPath);
119
+ }
120
+ for (const configPath of configCandidates(options.configPath, cwd)) {
121
+ const raw = await readJsonObject(configPath);
122
+ if (!raw) continue;
123
+ const base = dirname(configPath);
124
+ if (raw.version === 2) {
125
+ const parsed = GenomeConfigSchema.safeParse(raw);
126
+ if (!parsed.success) continue;
127
+ const dir = parsed.data.outputs.genomeDir.replace(/\/+$/, "");
128
+ const named = `${dir}/components.json`;
129
+ return isAbsolute(named) ? named : resolve(base, named);
130
+ }
131
+ if (typeof raw.componentsFile !== "string") continue;
132
+ return isAbsolute(raw.componentsFile) ? raw.componentsFile : resolve(base, raw.componentsFile);
133
+ }
134
+ if (options.tokenFile) {
135
+ return resolve(dirname(options.tokenFile), "components.json");
136
+ }
137
+ return resolve(cwd, "components.json");
138
+ }
139
+ async function loadComponentsFromFile(componentsFile) {
140
+ const text = await readFile(componentsFile, "utf8");
141
+ const raw = JSON.parse(text);
142
+ const { components, name } = normalizeComponentDocument(raw);
143
+ return { source: componentsFile, components, name };
144
+ }
145
+ function filterComponentsByCategory(components, category) {
146
+ if (!category) return components;
147
+ const needle = category.toLowerCase();
148
+ return components.filter((c) => (c.category ?? "").toLowerCase() === needle);
149
+ }
150
+ function findComponentByName(components, name) {
151
+ const needle = name.toLowerCase();
152
+ return components.find((c) => c.name.toLowerCase() === needle);
153
+ }
154
+
155
+ // ../codegen/dist/shared/colorMath.js
156
+ function normalizeHex(value) {
157
+ const m = /^#([0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i.exec(value.trim());
158
+ if (!m)
159
+ return null;
160
+ let hex = m[1].toLowerCase();
161
+ if (hex.length === 3)
162
+ hex = hex.split("").map((c) => c + c).join("");
163
+ return `#${hex.slice(0, 6)}`;
164
+ }
165
+ function srgbToLinear(c) {
166
+ return c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
167
+ }
168
+ function hexToLinearRgb(hex) {
169
+ const normalized = normalizeHex(hex);
170
+ if (!normalized)
171
+ return null;
172
+ return [
173
+ srgbToLinear(parseInt(normalized.slice(1, 3), 16) / 255),
174
+ srgbToLinear(parseInt(normalized.slice(3, 5), 16) / 255),
175
+ srgbToLinear(parseInt(normalized.slice(5, 7), 16) / 255)
176
+ ];
177
+ }
178
+ function relativeLuminance(hex) {
179
+ const rgb = hexToLinearRgb(hex);
180
+ if (!rgb)
181
+ return null;
182
+ return 0.2126 * rgb[0] + 0.7152 * rgb[1] + 0.0722 * rgb[2];
183
+ }
184
+ function contrastRatio(a, b) {
185
+ const la = relativeLuminance(a);
186
+ const lb = relativeLuminance(b);
187
+ if (la === null || lb === null)
188
+ return null;
189
+ const [hi, lo] = la >= lb ? [la, lb] : [lb, la];
190
+ return (hi + 0.05) / (lo + 0.05);
191
+ }
192
+ function hexToOklab(hex) {
193
+ const rgb = hexToLinearRgb(hex);
194
+ if (!rgb)
195
+ return null;
196
+ const [r, g, b] = rgb;
197
+ const l_ = Math.cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b);
198
+ const m_ = Math.cbrt(0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b);
199
+ const s_ = Math.cbrt(0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b);
200
+ return {
201
+ L: 0.2104542553 * l_ + 0.793617785 * m_ - 0.0040720468 * s_,
202
+ a: 1.9779984951 * l_ - 2.428592205 * m_ + 0.4505937099 * s_,
203
+ b: 0.0259040371 * l_ + 0.7827717662 * m_ - 0.808675766 * s_
204
+ };
205
+ }
206
+ function hexToOklch(hex) {
207
+ const lab = hexToOklab(hex);
208
+ if (!lab)
209
+ return null;
210
+ return {
211
+ l: lab.L,
212
+ c: Math.sqrt(lab.a * lab.a + lab.b * lab.b),
213
+ h: (Math.atan2(lab.b, lab.a) * 180 / Math.PI + 360) % 360
214
+ };
215
+ }
216
+ function hexToRgba(value) {
217
+ const raw = value.trim();
218
+ const m = /^#([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/i.exec(raw);
219
+ if (!m)
220
+ return null;
221
+ let hex = m[1];
222
+ if (hex.length <= 4) {
223
+ hex = hex.split("").map((c) => c + c).join("");
224
+ }
225
+ const n = (i) => parseInt(hex.slice(i, i + 2), 16);
226
+ return {
227
+ r: n(0),
228
+ g: n(2),
229
+ b: n(4),
230
+ a: hex.length === 8 ? Math.round(n(6) / 255 * 1e3) / 1e3 : 1
231
+ };
232
+ }
233
+ function round(n, places) {
234
+ const f = 10 ** places;
235
+ return Math.round(n * f) / f;
236
+ }
237
+ function formatColor(value, format) {
238
+ if (format === "hex")
239
+ return value;
240
+ const rgba = hexToRgba(value);
241
+ if (!rgba)
242
+ return value;
243
+ const { r, g, b, a } = rgba;
244
+ const alpha = a < 1 ? `, ${a}` : "";
245
+ if (format === "rgba") {
246
+ return a < 1 ? `rgba(${r}, ${g}, ${b}${alpha})` : `rgb(${r}, ${g}, ${b})`;
247
+ }
248
+ if (format === "hsl") {
249
+ const rn = r / 255;
250
+ const gn = g / 255;
251
+ const bn = b / 255;
252
+ const max = Math.max(rn, gn, bn);
253
+ const min = Math.min(rn, gn, bn);
254
+ const l = (max + min) / 2;
255
+ let h = 0;
256
+ let s = 0;
257
+ if (max !== min) {
258
+ const d = max - min;
259
+ s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
260
+ if (max === rn)
261
+ h = (gn - bn) / d + (gn < bn ? 6 : 0);
262
+ else if (max === gn)
263
+ h = (bn - rn) / d + 2;
264
+ else
265
+ h = (rn - gn) / d + 4;
266
+ h *= 60;
267
+ }
268
+ const body2 = `${round(h, 1)}, ${round(s * 100, 1)}%, ${round(l * 100, 1)}%`;
269
+ return a < 1 ? `hsla(${body2}${alpha})` : `hsl(${body2})`;
270
+ }
271
+ const solid = `#${[r, g, b].map((c) => c.toString(16).padStart(2, "0")).join("")}`;
272
+ const lch = hexToOklch(solid);
273
+ if (!lch)
274
+ return value;
275
+ const body = `${round(lch.l * 100, 2)}% ${round(lch.c, 4)} ${round(lch.c < 5e-4 ? 0 : lch.h, 2)}`;
276
+ return a < 1 ? `oklch(${body} / ${a})` : `oklch(${body})`;
277
+ }
278
+
279
+ // ../codegen/dist/shared/appearances.js
280
+ var CONTRAST_ENHANCED = 7;
281
+ var PAIRED_ROLES = [
282
+ "primary",
283
+ "secondary",
284
+ "accent",
285
+ "muted",
286
+ "control",
287
+ "destructive",
288
+ "success",
289
+ "warning",
290
+ "info"
291
+ ];
292
+ function semanticPairs(names) {
293
+ const pairs = [];
294
+ if (names.has("background") && names.has("foreground")) {
295
+ pairs.push(["background", "foreground"]);
296
+ }
297
+ for (const role of PAIRED_ROLES) {
298
+ if (names.has(role) && names.has(`${role}-foreground`)) {
299
+ pairs.push([role, `${role}-foreground`]);
300
+ }
301
+ }
302
+ return pairs;
303
+ }
304
+ function splitRamp(name) {
305
+ const m = /^(.+?)-(\d{1,4})$/.exec(name);
306
+ if (!m)
307
+ return null;
308
+ const step = Number(m[2]);
309
+ return Number.isFinite(step) ? { family: m[1], step } : null;
310
+ }
311
+ function resolveToken(byName, name) {
312
+ let current = byName.get(name);
313
+ const seen = /* @__PURE__ */ new Set();
314
+ for (let depth = 0; current && depth < 10; depth++) {
315
+ if (!(current.layer === "alias" && current.referencedTokenId))
316
+ return current;
317
+ if (seen.has(current.name))
318
+ return void 0;
319
+ seen.add(current.name);
320
+ current = byName.get(current.referencedTokenId);
321
+ }
322
+ return void 0;
323
+ }
324
+ function resolve2(byName, name) {
325
+ return resolveToken(byName, name)?.value ?? "";
326
+ }
327
+ function highContrastOverrides(palette) {
328
+ const byName = /* @__PURE__ */ new Map();
329
+ for (const t of palette)
330
+ if (t?.name)
331
+ byName.set(t.name, t);
332
+ const ramps = /* @__PURE__ */ new Map();
333
+ for (const t of byName.values()) {
334
+ if (t.layer === "alias" || t.referencedTokenId)
335
+ continue;
336
+ const split = splitRamp(t.name);
337
+ if (!split || !normalizeHex(t.value ?? ""))
338
+ continue;
339
+ const list = ramps.get(split.family);
340
+ if (list)
341
+ list.push(t);
342
+ else
343
+ ramps.set(split.family, [t]);
344
+ }
345
+ const overrides = [];
346
+ for (const [surfaceName, foregroundName] of semanticPairs(new Set(byName.keys()))) {
347
+ const surface = resolve2(byName, surfaceName);
348
+ const foreground = resolve2(byName, foregroundName);
349
+ const current = contrastRatio(surface, foreground);
350
+ if (current === null || current >= CONTRAST_ENHANCED)
351
+ continue;
352
+ const via = resolveToken(byName, foregroundName);
353
+ const family = via ? splitRamp(via.name)?.family : void 0;
354
+ const steps = family ? ramps.get(family) ?? [] : [];
355
+ let best = null;
356
+ for (const step of steps) {
357
+ const ratio = contrastRatio(surface, step.value);
358
+ if (ratio === null || ratio < CONTRAST_ENHANCED)
359
+ continue;
360
+ if (!best || ratio < best.ratio)
361
+ best = { value: step.value, ratio };
362
+ }
363
+ if (!best)
364
+ continue;
365
+ overrides.push({
366
+ name: foregroundName,
367
+ value: best.value,
368
+ because: `${foregroundName} on ${surfaceName}: ${current.toFixed(2)}:1 becomes ${best.ratio.toFixed(2)}:1`
369
+ });
370
+ }
371
+ return overrides;
372
+ }
373
+ function resolvedValue(palette, name) {
374
+ const byName = /* @__PURE__ */ new Map();
375
+ for (const t of palette)
376
+ if (t?.name)
377
+ byName.set(t.name, t);
378
+ const value = resolve2(byName, name);
379
+ return normalizeHex(value) ? value : null;
380
+ }
381
+ var DENSITY_SCALES = {
382
+ compact: 0.85,
383
+ comfortable: 1.15
384
+ };
385
+ var LENGTH = /^(-?\d*\.?\d+)(px|rem|em)$/;
386
+ function densityOverrides(spacing, scale) {
387
+ const out = [];
388
+ for (const t of spacing) {
389
+ if (!t?.name || t.layer === "alias" || t.referencedTokenId)
390
+ continue;
391
+ const m = LENGTH.exec((t.value ?? "").trim());
392
+ if (!m)
393
+ continue;
394
+ const scaled = Number(m[1]) * scale;
395
+ if (!Number.isFinite(scaled) || scaled === 0)
396
+ continue;
397
+ const rounded = Number(scaled.toPrecision(6));
398
+ if (rounded === Number(m[1]) || rounded === 0)
399
+ continue;
400
+ out.push({
401
+ name: t.name,
402
+ value: `${rounded}${m[2]}`,
403
+ because: `${t.value} at ${scale}x`
404
+ });
405
+ }
406
+ return out;
407
+ }
408
+
409
+ // ../codegen/dist/tokens.js
410
+ function tokensToCssVarName(name) {
411
+ return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
412
+ }
413
+ function resolveAliasLiteral(byName, t) {
414
+ let current = t;
415
+ const seen = /* @__PURE__ */ new Set();
416
+ for (let depth = 0; current && depth < 10; depth++) {
417
+ if (!(current.layer === "alias" && current.referencedTokenId)) {
418
+ return current.value;
419
+ }
420
+ if (seen.has(current.name))
421
+ return void 0;
422
+ seen.add(current.name);
423
+ current = byName.get(current.referencedTokenId);
424
+ }
425
+ return void 0;
426
+ }
427
+ function resolveTokenValue(t) {
428
+ if (t.layer === "alias" && t.referencedTokenId) {
429
+ return `var(--${tokensToCssVarName(t.referencedTokenId)})`;
430
+ }
431
+ return t.value;
432
+ }
433
+ function emitColorVars(colors, lines, elementSemantics = [], colorFormat = "hex") {
434
+ const primitives = colors.filter((t) => t.layer !== "alias");
435
+ const aliases = colors.filter((t) => t.layer === "alias");
436
+ if (primitives.length) {
437
+ lines.push(" /* Color Primitives */");
438
+ for (const t of primitives) {
439
+ lines.push(` --${tokensToCssVarName(t.name)}: ${formatColor(t.value, colorFormat)};`);
440
+ }
441
+ lines.push("");
442
+ }
443
+ if (aliases.length || elementSemantics.length) {
444
+ lines.push(" /* Semantics */");
445
+ for (const t of aliases) {
446
+ lines.push(` --${tokensToCssVarName(t.name)}: ${resolveTokenValue(t)};`);
447
+ }
448
+ for (const s of elementSemantics) {
449
+ lines.push(` --${tokensToCssVarName(s.name)}: var(--${tokensToCssVarName(s.tokenName)});`);
450
+ }
451
+ lines.push("");
452
+ }
453
+ }
454
+ function unchangedPrimitiveNames(colors, lightColors) {
455
+ const dark = /* @__PURE__ */ new Map();
456
+ for (const t of colors) {
457
+ if (t.layer !== "alias" && !t.referencedTokenId)
458
+ dark.set(t.name, t.value ?? "");
459
+ }
460
+ const same2 = /* @__PURE__ */ new Set();
461
+ for (const t of lightColors) {
462
+ if (t.layer !== "alias" && !t.referencedTokenId && dark.get(t.name) === (t.value ?? "")) {
463
+ same2.add(t.name);
464
+ }
465
+ }
466
+ return same2;
467
+ }
468
+ function sharedAliasNames(colors, lightColors) {
469
+ const darkRefs = /* @__PURE__ */ new Map();
470
+ for (const t of colors) {
471
+ if (t.layer === "alias" && t.referencedTokenId) {
472
+ darkRefs.set(t.name, t.referencedTokenId);
473
+ }
474
+ }
475
+ const shared = /* @__PURE__ */ new Set();
476
+ for (const t of lightColors) {
477
+ if (t.layer === "alias" && t.referencedTokenId && darkRefs.get(t.name) === t.referencedTokenId) {
478
+ shared.add(t.name);
479
+ }
480
+ }
481
+ return shared;
482
+ }
483
+ function singleThemePalette(tokens) {
484
+ return tokens.colors.length ? tokens.colors : tokens.lightColors;
485
+ }
486
+ function motionTokens(tokens) {
487
+ return tokens.motion ?? [];
488
+ }
489
+ var NAMED_EASINGS = {
490
+ linear: [0, 0, 1, 1],
491
+ ease: [0.25, 0.1, 0.25, 1],
492
+ "ease-in": [0.42, 0, 1, 1],
493
+ "ease-out": [0, 0, 0.58, 1],
494
+ "ease-in-out": [0.42, 0, 0.58, 1]
495
+ };
496
+ function parseDuration(css) {
497
+ const m = /^\s*(-?[\d.]+)\s*(ms|s)\s*$/i.exec(css);
498
+ if (!m)
499
+ return null;
500
+ const value = Number(m[1]);
501
+ if (!Number.isFinite(value))
502
+ return null;
503
+ return { value, unit: m[2].toLowerCase() };
504
+ }
505
+ function parseCubicBezier(css) {
506
+ const trimmed = css.trim();
507
+ const named = NAMED_EASINGS[trimmed.toLowerCase()];
508
+ if (named)
509
+ return [...named];
510
+ const m = /^cubic-bezier\(([^)]+)\)$/i.exec(trimmed);
511
+ if (!m)
512
+ return null;
513
+ const parts = m[1].split(",").map((p) => Number(p.trim()));
514
+ if (parts.length !== 4 || parts.some((n) => !Number.isFinite(n)))
515
+ return null;
516
+ return parts;
517
+ }
518
+ function motionTokenKind(t) {
519
+ if (t.type === "duration" || t.type === "cubicBezier" || t.type === "transition")
520
+ return t.type;
521
+ const value = t.value.trim();
522
+ if (parseDuration(value))
523
+ return "duration";
524
+ if (parseCubicBezier(value) || /^linear\(/i.test(value))
525
+ return "cubicBezier";
526
+ const n = t.name.toLowerCase();
527
+ if (n.startsWith("duration-"))
528
+ return "duration";
529
+ if (n.startsWith("ease-") || n.startsWith("spring-"))
530
+ return "cubicBezier";
531
+ if (/\s/.test(value))
532
+ return "transition";
533
+ return null;
534
+ }
535
+ function splitTopLevel(css) {
536
+ const out = [];
537
+ let depth = 0;
538
+ let cur = "";
539
+ for (const ch of css.trim()) {
540
+ if (ch === "(")
541
+ depth++;
542
+ else if (ch === ")")
543
+ depth--;
544
+ if (depth === 0 && /\s/.test(ch)) {
545
+ if (cur)
546
+ out.push(cur);
547
+ cur = "";
548
+ continue;
549
+ }
550
+ cur += ch;
551
+ }
552
+ if (cur)
553
+ out.push(cur);
554
+ return out;
555
+ }
556
+ function decomposeTransition(css) {
557
+ const parts = splitTopLevel(css);
558
+ if (parts.length < 2 || parts.length > 3)
559
+ return null;
560
+ const isTime = (p) => parseDuration(p) !== null || /duration|delay/i.test(p);
561
+ const isEase = (p) => parseCubicBezier(p) !== null || /linear\(|ease|spring|cubic-bezier/i.test(p);
562
+ const duration = parts.find(isTime);
563
+ const timingFunction = parts.find((p) => p !== duration && isEase(p));
564
+ if (!duration || !timingFunction)
565
+ return null;
566
+ const delay = parts.find((p) => p !== duration && p !== timingFunction && isTime(p)) ?? "0ms";
567
+ return { duration, timingFunction, delay };
568
+ }
569
+ function motionAlias(css) {
570
+ const m = /^var\(\s*--([a-z0-9-]+)\s*(?:,[^)]*)?\)$/i.exec(css.trim());
571
+ return m ? `{motion.${m[1]}}` : null;
572
+ }
573
+ var FLASH_MODES_EXTENSION = "com.flashdsm.modes";
574
+ function motionDtcgEntry(t, legacyDurationStrings = false) {
575
+ const base = t.description ? { $description: t.description } : {};
576
+ const encodeTime = (css) => {
577
+ const alias = motionAlias(css);
578
+ if (alias)
579
+ return alias;
580
+ const parsed = parseDuration(css);
581
+ if (!parsed)
582
+ return css;
583
+ return legacyDurationStrings ? `${parsed.value}${parsed.unit}` : parsed;
584
+ };
585
+ switch (motionTokenKind(t)) {
586
+ case "duration": {
587
+ const parsed = parseDuration(t.value);
588
+ if (parsed) {
589
+ return {
590
+ $type: "duration",
591
+ $value: legacyDurationStrings ? `${parsed.value}${parsed.unit}` : parsed,
592
+ ...base
593
+ };
594
+ }
595
+ break;
596
+ }
597
+ case "cubicBezier": {
598
+ const isLinearFn = /^linear\(/i.test(t.value.trim());
599
+ const curve = parseCubicBezier(isLinearFn ? t.fallbackValue ?? "" : t.value);
600
+ if (curve) {
601
+ return {
602
+ $type: "cubicBezier",
603
+ $value: curve,
604
+ ...base,
605
+ ...isLinearFn ? { $extensions: { "com.flashdsm.motion": { easing: t.value } } } : {}
606
+ };
607
+ }
608
+ break;
609
+ }
610
+ case "transition": {
611
+ const parts = decomposeTransition(t.value);
612
+ if (parts) {
613
+ return {
614
+ $type: "transition",
615
+ $value: {
616
+ duration: encodeTime(parts.duration),
617
+ delay: encodeTime(parts.delay),
618
+ timingFunction: motionAlias(parts.timingFunction) ?? parseCubicBezier(parts.timingFunction) ?? parts.timingFunction
619
+ },
620
+ ...base
621
+ };
622
+ }
623
+ break;
624
+ }
625
+ }
626
+ return { $type: t.type || "motion", $value: t.value, ...base };
627
+ }
628
+ function emitReducedMotion(motion, lines) {
629
+ const durations = motion.filter((t) => motionTokenKind(t) === "duration");
630
+ const distances = motion.filter((t) => tokensToCssVarName(t.name).startsWith("motion-distance"));
631
+ const scale = motion.find((t) => tokensToCssVarName(t.name) === "motion-scale");
632
+ if (!durations.length && !distances.length && !scale)
633
+ return;
634
+ const off = [];
635
+ const on = [];
636
+ if (scale) {
637
+ off.push(` --${tokensToCssVarName(scale.name)}: 0.001;`);
638
+ on.push(` --${tokensToCssVarName(scale.name)}: 1;`);
639
+ }
640
+ for (const t of durations) {
641
+ off.push(` --${tokensToCssVarName(t.name)}: 0.001ms;`);
642
+ on.push(` --${tokensToCssVarName(t.name)}: ${t.value};`);
643
+ }
644
+ for (const t of distances) {
645
+ off.push(` --${tokensToCssVarName(t.name)}: 0px;`);
646
+ on.push(` --${tokensToCssVarName(t.name)}: ${t.value};`);
647
+ }
648
+ lines.push("");
649
+ lines.push("/* Reduced motion - reduce, don't remove. */");
650
+ lines.push("@media (prefers-reduced-motion: reduce) {");
651
+ lines.push(' :root:not([data-motion="full"]) {');
652
+ for (const l of off)
653
+ lines.push(` ${l}`);
654
+ lines.push(" }");
655
+ lines.push("}");
656
+ lines.push("");
657
+ lines.push("/* In-app override - wins over the OS setting, both directions. */");
658
+ lines.push('[data-motion="reduced"] {');
659
+ for (const l of off)
660
+ lines.push(l);
661
+ lines.push("}");
662
+ lines.push("");
663
+ lines.push('[data-motion="full"] {');
664
+ for (const l of on)
665
+ lines.push(l);
666
+ lines.push("}");
667
+ }
668
+ function emitOverrides(overrides, selectors, indent, lines, withReasons = false) {
669
+ selectors.forEach((selector, i) => {
670
+ const last = i === selectors.length - 1;
671
+ lines.push(`${indent}${selector}${last ? " {" : ","}`);
672
+ });
673
+ for (const o of overrides) {
674
+ const reason = withReasons ? ` /* ${o.because} */` : "";
675
+ lines.push(`${indent} --${tokensToCssVarName(o.name)}: ${o.value};${reason}`);
676
+ }
677
+ lines.push(`${indent}}`);
678
+ }
679
+ function emitHighContrast(tokens, lines, colorFormat = "hex") {
680
+ const hasLight = tokens.lightColors.length > 0;
681
+ const hasDark = tokens.colors.length > 0;
682
+ const bothThemes = hasLight && hasDark;
683
+ const defaultPalette = hasDark ? tokens.colors : tokens.lightColors;
684
+ const otherPalette = bothThemes ? tokens.lightColors : [];
685
+ const defaults = highContrastOverrides(defaultPalette);
686
+ const others = bothThemes ? highContrastOverrides(otherPalette) : [];
687
+ if (!defaults.length && !others.length)
688
+ return;
689
+ const blockFor = (own, otherNames, palette) => {
690
+ const byName = new Map(own.map((o) => [o.name, o]));
691
+ for (const name of otherNames) {
692
+ if (byName.has(name))
693
+ continue;
694
+ const held = resolvedValue(palette, name);
695
+ if (!held)
696
+ continue;
697
+ byName.set(name, {
698
+ name,
699
+ value: held,
700
+ because: `unchanged here; the other theme moves it`
701
+ });
702
+ }
703
+ return [...byName.values()];
704
+ };
705
+ const spelled = (block) => block.map((o) => ({ ...o, value: formatColor(o.value, colorFormat) }));
706
+ const defaultBlock = spelled(blockFor(defaults, others.map((o) => o.name), defaultPalette));
707
+ const otherBlock = bothThemes ? spelled(blockFor(others, defaults.map((o) => o.name), otherPalette)) : [];
708
+ lines.push("");
709
+ lines.push("/* High contrast: only the foregrounds move, each to a step of its own ramp that reaches 7:1 */");
710
+ if (defaultBlock.length) {
711
+ lines.push("@media (prefers-contrast: more) {");
712
+ emitOverrides(defaultBlock, [":root"], " ", lines, true);
713
+ lines.push("}");
714
+ emitOverrides(defaultBlock, [':root[data-contrast="more"]'], "", lines);
715
+ }
716
+ if (otherBlock.length) {
717
+ lines.push("");
718
+ lines.push("@media (prefers-contrast: more) {");
719
+ emitOverrides(otherBlock, ['[data-theme="light"]', ".light"], " ", lines, true);
720
+ lines.push("}");
721
+ lines.push("@media (prefers-contrast: more) and (prefers-color-scheme: light) {");
722
+ emitOverrides(otherBlock, [':root:not([data-theme="dark"])'], " ", lines);
723
+ lines.push("}");
724
+ emitOverrides(otherBlock, [
725
+ '[data-theme="light"][data-contrast="more"]',
726
+ '.light[data-contrast="more"]',
727
+ '[data-contrast="more"] [data-theme="light"]',
728
+ '[data-contrast="more"] .light'
729
+ ], "", lines);
730
+ }
731
+ }
732
+ function emitDensity(tokens, lines) {
733
+ for (const [name, scale] of Object.entries(DENSITY_SCALES)) {
734
+ const overrides = densityOverrides(tokens.spacing, scale);
735
+ if (!overrides.length)
736
+ continue;
737
+ lines.push("");
738
+ lines.push(`/* Density: ${name}, spacing at ${scale}x */`);
739
+ emitOverrides(overrides, [`[data-density="${name}"]`], "", lines);
740
+ }
741
+ }
742
+ function generateCSS(tokens, attribution = false, appearances = {}) {
743
+ const hasLightColors = tokens.lightColors.length > 0;
744
+ const hasDarkColors = tokens.colors.length > 0;
745
+ const hasBothThemes = hasLightColors && hasDarkColors;
746
+ const colorFormat = appearances.colorFormat ?? "hex";
747
+ const lines = [];
748
+ if (attribution) {
749
+ lines.push("/*");
750
+ lines.push(" * Design tokens generated by Flash DSM - https://flashdsm.com");
751
+ lines.push(" * Upgrade to a paid plan to remove this attribution.");
752
+ lines.push(" */");
753
+ lines.push("");
754
+ }
755
+ const knownColorVars = new Set([...tokens.colors, ...tokens.lightColors].map((t) => tokensToCssVarName(t.name)));
756
+ const allDeclaredVars = new Set([
757
+ ...knownColorVars,
758
+ ...tokens.spacing,
759
+ ...tokens.typography,
760
+ ...tokens.radius,
761
+ ...tokens.shadows,
762
+ ...tokens.border,
763
+ ...tokens.effects,
764
+ ...tokens.icons,
765
+ ...motionTokens(tokens)
766
+ ].map((t) => typeof t === "string" ? t : tokensToCssVarName(t.name)));
767
+ const takenSemanticVars = /* @__PURE__ */ new Set();
768
+ const semantics = (tokens.usageSemantics ?? []).filter((s) => {
769
+ const name = tokensToCssVarName(s.name);
770
+ if (!knownColorVars.has(tokensToCssVarName(s.tokenName)))
771
+ return false;
772
+ if (allDeclaredVars.has(name) || takenSemanticVars.has(name))
773
+ return false;
774
+ takenSemanticVars.add(name);
775
+ return true;
776
+ });
777
+ if (hasBothThemes) {
778
+ lines.push("/* Dark mode (default) */");
779
+ lines.push(":root {");
780
+ emitColorVars(tokens.colors, lines, semantics, colorFormat);
781
+ } else {
782
+ lines.push(":root {");
783
+ if (hasDarkColors)
784
+ emitColorVars(tokens.colors, lines, semantics, colorFormat);
785
+ else if (hasLightColors)
786
+ emitColorVars(tokens.lightColors, lines, semantics, colorFormat);
787
+ }
788
+ const nonColor = [
789
+ ["/* Spacing */", tokens.spacing],
790
+ ["/* Typography */", tokens.typography],
791
+ ["/* Border Radius */", tokens.radius],
792
+ ["/* Shadows */", tokens.shadows],
793
+ ["/* Border */", tokens.border],
794
+ ["/* Effects */", tokens.effects],
795
+ ["/* Icons */", tokens.icons],
796
+ ["/* Motion */", motionTokens(tokens)]
797
+ ];
798
+ for (const [c, group] of nonColor) {
799
+ if (!group.length)
800
+ continue;
801
+ lines.push(` ${c}`);
802
+ for (const t of group) {
803
+ lines.push(` --${tokensToCssVarName(t.name)}: ${resolveTokenValue(t)};`);
804
+ }
805
+ lines.push("");
806
+ }
807
+ lines.push("}");
808
+ if (hasBothThemes) {
809
+ const shared = sharedAliasNames(tokens.colors, tokens.lightColors);
810
+ const unchanged = unchangedPrimitiveNames(tokens.colors, tokens.lightColors);
811
+ const lightOnly = tokens.lightColors.filter((t) => t.layer === "alias" || t.referencedTokenId ? !shared.has(t.name) : !unchanged.has(t.name));
812
+ lines.push("");
813
+ lines.push("/* Light mode - explicit opt-in */");
814
+ if (shared.size) {
815
+ lines.push("/* Semantic aliases are declared once in :root; themes swap the values under them. */");
816
+ }
817
+ lines.push('[data-theme="light"],');
818
+ lines.push(".light {");
819
+ emitColorVars(lightOnly, lines, [], colorFormat);
820
+ lines.push("}");
821
+ lines.push("");
822
+ lines.push("/* Light mode - respects OS preference unless explicitly overridden */");
823
+ lines.push("@media (prefers-color-scheme: light) {");
824
+ lines.push(' :root:not([data-theme="dark"]) {');
825
+ const lp = lightOnly.filter((t) => t.layer !== "alias");
826
+ const la = lightOnly.filter((t) => t.layer === "alias");
827
+ for (const t of lp) {
828
+ lines.push(` --${tokensToCssVarName(t.name)}: ${formatColor(t.value, colorFormat)};`);
829
+ }
830
+ for (const t of la) {
831
+ lines.push(` --${tokensToCssVarName(t.name)}: ${resolveTokenValue(t)};`);
832
+ }
833
+ lines.push(" }");
834
+ lines.push("}");
835
+ }
836
+ if (appearances.highContrast)
837
+ emitHighContrast(tokens, lines, colorFormat);
838
+ if (appearances.density)
839
+ emitDensity(tokens, lines);
840
+ emitReducedMotion(motionTokens(tokens), lines);
841
+ return lines.join("\n");
842
+ }
843
+ function generateJSON(tokens, options = {}) {
844
+ const result = {};
845
+ const palette = singleThemePalette(tokens);
846
+ if (palette.length) {
847
+ result["color"] = {};
848
+ const defaultTheme = palette === tokens.colors ? "dark" : "light";
849
+ const otherTheme = defaultTheme === "dark" ? "light" : "dark";
850
+ const other = defaultTheme === "dark" ? tokens.lightColors : tokens.colors;
851
+ const keyOf = (t) => tokensToCssVarName(t.name);
852
+ const otherByKey = new Map(other.map((t) => [keyOf(t), t]));
853
+ const valueIn = (list) => {
854
+ const byName = new Map(list.map((t) => [t.name, t]));
855
+ return (t) => {
856
+ if (!(t.layer === "alias" && t.referencedTokenId))
857
+ return t.value;
858
+ const reference = `{color.${tokensToCssVarName(t.referencedTokenId)}}`;
859
+ if (!options.resolved)
860
+ return reference;
861
+ return resolveAliasLiteral(byName, t) ?? reference;
862
+ };
863
+ };
864
+ const defaultValue = valueIn(palette);
865
+ const otherValue = valueIn(other);
866
+ const entryFor = (t, $value) => ({
867
+ $value,
868
+ $type: "color",
869
+ ...t.description ? { $description: t.description } : {}
870
+ });
871
+ const modes = (dark, light) => ({
872
+ [FLASH_MODES_EXTENSION]: {
873
+ ...dark !== null ? { dark } : {},
874
+ ...light !== null ? { light } : {}
875
+ }
876
+ });
877
+ const oneThemeOnly = (theme, value) => theme === "dark" ? modes(value, null) : modes(null, value);
878
+ for (const t of palette) {
879
+ const $value = defaultValue(t);
880
+ const entry = entryFor(t, $value);
881
+ const twin = otherByKey.get(keyOf(t));
882
+ if (twin) {
883
+ const twinValue = otherValue(twin);
884
+ if (twinValue !== $value) {
885
+ entry.$extensions = defaultTheme === "dark" ? modes($value, twinValue) : modes(twinValue, $value);
886
+ }
887
+ } else if (other.length) {
888
+ entry.$extensions = oneThemeOnly(defaultTheme, $value);
889
+ }
890
+ result["color"][keyOf(t)] = entry;
891
+ }
892
+ const defaultKeys = new Set(palette.map(keyOf));
893
+ for (const t of other) {
894
+ if (defaultKeys.has(keyOf(t)))
895
+ continue;
896
+ const value = otherValue(t);
897
+ const entry = entryFor(t, value);
898
+ entry.$extensions = oneThemeOnly(otherTheme, value);
899
+ result["color"][keyOf(t)] = entry;
900
+ }
901
+ }
902
+ const sections = [
903
+ ["spacing", tokens.spacing],
904
+ ["typography", tokens.typography],
905
+ ["borderRadius", tokens.radius],
906
+ ["shadow", tokens.shadows]
907
+ ];
908
+ for (const [group, items] of sections) {
909
+ if (!items.length)
910
+ continue;
911
+ result[group] = {};
912
+ const keyFor = (name) => tokensToCssVarName(name).replace(new RegExp(`^${group}-?`), "") || name;
913
+ for (const t of items) {
914
+ const $value = t.layer === "alias" && t.referencedTokenId ? `{${group}.${keyFor(t.referencedTokenId)}}` : t.value;
915
+ result[group][keyFor(t.name)] = {
916
+ $value,
917
+ $type: t.type || group,
918
+ ...t.description ? { $description: t.description } : {}
919
+ };
920
+ }
921
+ }
922
+ const motion = motionTokens(tokens);
923
+ if (motion.length) {
924
+ result["motion"] = {};
925
+ for (const t of motion) {
926
+ result["motion"][tokensToCssVarName(t.name)] = motionDtcgEntry(t, options.legacyDurationStrings);
927
+ }
928
+ }
929
+ return JSON.stringify(result, null, 2);
930
+ }
931
+ var SD_LENGTH_RE = /^-?\d*\.?\d+(px|rem|em|pt)$/i;
932
+ function styleDictionaryType(group, t, resolved = t.value) {
933
+ const value = (resolved ?? "").trim();
934
+ const isLength = SD_LENGTH_RE.test(value);
935
+ if (group === "color")
936
+ return "color";
937
+ if (group === "motion") {
938
+ return motionTokenKind(t) === "duration" ? "duration" : "cubicBezier";
939
+ }
940
+ if (group === "shadow")
941
+ return t.type || group;
942
+ if (group === "font") {
943
+ const name = tokensToCssVarName(t.name);
944
+ if (/famil|typeface|stack/.test(name))
945
+ return "fontFamily";
946
+ if (/weight/.test(name))
947
+ return "fontWeight";
948
+ if (/line-height|leading/.test(name))
949
+ return isLength ? "size" : "number";
950
+ }
951
+ return isLength ? "size" : t.type || group;
952
+ }
953
+ function generateStyleDictionary(tokens) {
954
+ const result = {};
955
+ const write = (group, items, keyFor) => {
956
+ if (!items.length)
957
+ return;
958
+ const byName = new Map(items.map((t) => [t.name, t]));
959
+ const resolveValue = (t) => resolveAliasLiteral(byName, t);
960
+ const bucket = {};
961
+ for (const t of items) {
962
+ const value = t.layer === "alias" && t.referencedTokenId ? `{${group}.${keyFor(t.referencedTokenId)}.value}` : t.value;
963
+ bucket[keyFor(t.name)] = {
964
+ value,
965
+ type: styleDictionaryType(group, t, resolveValue(t)),
966
+ ...t.description ? { comment: t.description } : {}
967
+ };
968
+ }
969
+ result[group] = bucket;
970
+ };
971
+ const stripped = (...prefixes) => (name) => {
972
+ let key = tokensToCssVarName(name);
973
+ for (const prefix of prefixes) {
974
+ key = key.replace(new RegExp(`^${prefix}-?`), "");
975
+ }
976
+ return key || tokensToCssVarName(name);
977
+ };
978
+ write("color", singleThemePalette(tokens), stripped());
979
+ write("size", tokens.spacing, stripped("spacing"));
980
+ write("radius", tokens.radius, stripped("radius"));
981
+ write("font", tokens.typography, stripped("typography", "font"));
982
+ write("shadow", tokens.shadows, stripped("shadow"));
983
+ write("border", tokens.border, stripped("border"));
984
+ write("motion", motionTokens(tokens), stripped());
985
+ return JSON.stringify(result, null, 2);
986
+ }
987
+ function generateStyleDictionaryConfig() {
988
+ const platform = (name, destination, format, options) => ({
989
+ transformGroup: name,
990
+ buildPath: `build/${name}/`,
991
+ files: [{ destination, format, ...options ? { options } : {} }]
992
+ });
993
+ return JSON.stringify({
994
+ source: ["tokens/style-dictionary.tokens.json"],
995
+ platforms: {
996
+ css: platform("css", "variables.css", "css/variables"),
997
+ scss: platform("scss", "_variables.scss", "scss/variables"),
998
+ "ios-swift": {
999
+ ...platform("ios-swift", "Tokens.swift", "ios-swift/class.swift", {
1000
+ className: "Tokens"
1001
+ })
1002
+ },
1003
+ android: platform("android", "tokens.xml", "android/resources"),
1004
+ compose: platform("compose", "Tokens.kt", "compose/object", {
1005
+ className: "Tokens",
1006
+ packageName: "tokens"
1007
+ })
1008
+ }
1009
+ }, null, 2);
1010
+ }
1011
+ function shortHash(input) {
1012
+ let h = 5381;
1013
+ for (let i = 0; i < input.length; i++) {
1014
+ h = (h << 5) + h + input.charCodeAt(i) | 0;
1015
+ }
1016
+ return (h >>> 0).toString(16);
1017
+ }
1018
+ function tokenHashes(tokens) {
1019
+ const out = {};
1020
+ const all = [
1021
+ ["colors", tokens.colors],
1022
+ ["lightColors", tokens.lightColors],
1023
+ ["spacing", tokens.spacing],
1024
+ ["typography", tokens.typography],
1025
+ ["radius", tokens.radius],
1026
+ ["shadows", tokens.shadows],
1027
+ ["border", tokens.border],
1028
+ ["effects", tokens.effects],
1029
+ ["icons", tokens.icons],
1030
+ ["motion", motionTokens(tokens)]
1031
+ ];
1032
+ for (const [group, items] of all) {
1033
+ for (const t of items) {
1034
+ out[`${group}.${tokensToCssVarName(t.name)}`] = shortHash(`${t.value}|${t.layer || ""}|${t.referencedTokenId || ""}`);
1035
+ }
1036
+ }
1037
+ return out;
1038
+ }
1039
+
1040
+ // ../codegen/dist/from-atomic-document.js
1041
+ function atomicTokenToDesign(t, value = t.value) {
1042
+ return {
1043
+ name: t.name,
1044
+ value,
1045
+ type: t.type,
1046
+ category: t.category,
1047
+ ...t.description ? { description: t.description } : {},
1048
+ ...t.layer ? { layer: t.layer } : {},
1049
+ ...t.referencedTokenId ? { referencedTokenId: t.referencedTokenId } : {},
1050
+ ...t.colorGroup ? { colorGroup: t.colorGroup } : {},
1051
+ ...t.fallbackValue ? { fallbackValue: t.fallbackValue } : {},
1052
+ ...t.metadata ? { metadata: t.metadata } : {}
1053
+ };
1054
+ }
1055
+ function atomicThemeValues(t) {
1056
+ const meta = t.metadata ?? {};
1057
+ const dark = meta.dark || meta.darkValue;
1058
+ const light = meta.light || meta.lightValue;
1059
+ if (!dark && !light)
1060
+ return [t.value, t.value];
1061
+ return [dark ? String(dark) : null, light ? String(light) : null];
1062
+ }
1063
+ function fromAtomicDocument(doc) {
1064
+ const f = doc.foundations;
1065
+ const colors = [];
1066
+ const lightColors = [];
1067
+ const palette = [
1068
+ ...f.colors ?? [],
1069
+ ...(doc.semantics ?? []).map((t) => ({ ...t, layer: t.layer ?? "alias" }))
1070
+ ];
1071
+ for (const t of palette) {
1072
+ const [dark, light] = atomicThemeValues(t);
1073
+ if (dark !== null)
1074
+ colors.push(atomicTokenToDesign(t, dark));
1075
+ if (light !== null)
1076
+ lightColors.push(atomicTokenToDesign(t, light));
1077
+ }
1078
+ return {
1079
+ colors,
1080
+ lightColors,
1081
+ spacing: (f.spacing ?? []).map((t) => atomicTokenToDesign(t)),
1082
+ typography: (f.typography ?? []).map((t) => atomicTokenToDesign(t)),
1083
+ radius: (f.radius ?? []).map((t) => atomicTokenToDesign(t)),
1084
+ shadows: (f.shadows ?? []).map((t) => atomicTokenToDesign(t)),
1085
+ border: (f.border ?? []).map((t) => atomicTokenToDesign(t)),
1086
+ motion: (f.motion ?? []).map((t) => atomicTokenToDesign(t)),
1087
+ effects: (f.effects ?? []).map((t) => atomicTokenToDesign(t)),
1088
+ icons: (f.icons ?? []).map((t) => atomicTokenToDesign(t))
1089
+ };
1090
+ }
1091
+ function defaultStateBindings(component) {
1092
+ const defaultBindings = component.bindings.filter((b) => (b.stateId ?? "default") === "default" && !b.variantId);
1093
+ if (defaultBindings.length)
1094
+ return defaultBindings;
1095
+ const primaryVariant = component.variants[0]?.id;
1096
+ if (!primaryVariant)
1097
+ return component.bindings;
1098
+ return component.bindings.filter((b) => (b.stateId ?? "default") === "default" && b.variantId === primaryVariant);
1099
+ }
1100
+ function slotToTailwind(slot, tokenId) {
1101
+ const cssVar = `var(--${tokensToCssVarName(tokenId)})`;
1102
+ switch (slot) {
1103
+ case "background":
1104
+ return `bg-[${cssVar}]`;
1105
+ case "foreground":
1106
+ case "color":
1107
+ return `text-[${cssVar}]`;
1108
+ case "accent":
1109
+ return `accent-[${cssVar}]`;
1110
+ case "border":
1111
+ return `border border-[${cssVar}]`;
1112
+ case "padding":
1113
+ return `p-[${cssVar}]`;
1114
+ case "margin":
1115
+ return `m-[${cssVar}]`;
1116
+ case "gap":
1117
+ return `gap-[${cssVar}]`;
1118
+ case "borderRadius":
1119
+ return `rounded-[${cssVar}]`;
1120
+ case "fontSize":
1121
+ return `text-[${cssVar}]`;
1122
+ case "fontWeight":
1123
+ return `font-[${cssVar}]`;
1124
+ case "lineHeight":
1125
+ return `leading-[${cssVar}]`;
1126
+ case "letterSpacing":
1127
+ return `tracking-[${cssVar}]`;
1128
+ case "boxShadow":
1129
+ return `shadow-[${cssVar}]`;
1130
+ case "minHeight":
1131
+ return `min-h-[${cssVar}]`;
1132
+ case "transition":
1133
+ return `transition-[${cssVar}]`;
1134
+ default:
1135
+ return null;
1136
+ }
1137
+ }
1138
+ function rootTagForComponent(id) {
1139
+ switch (id) {
1140
+ case "button":
1141
+ case "icon-button":
1142
+ return "button";
1143
+ case "input":
1144
+ case "search-input":
1145
+ case "password-input":
1146
+ return "input";
1147
+ case "textarea":
1148
+ return "textarea";
1149
+ case "select":
1150
+ return "select";
1151
+ case "checkbox":
1152
+ case "radio":
1153
+ case "switch":
1154
+ return "input";
1155
+ case "a":
1156
+ case "link":
1157
+ return "a";
1158
+ default:
1159
+ return "div";
1160
+ }
1161
+ }
1162
+ function atomicComponentsToExportSpecs(doc) {
1163
+ return doc.components.map((component) => {
1164
+ const classes = defaultStateBindings(component).map((b) => slotToTailwind(String(b.slot), b.tokenId)).filter((c) => Boolean(c));
1165
+ const tag = rootTagForComponent(component.id);
1166
+ const classAttr = classes.length > 0 ? ` className="${classes.join(" ")}"` : "";
1167
+ const selfClosing = tag === "input";
1168
+ const code = selfClosing ? `<${tag}${classAttr} />` : `<${tag}${classAttr}>${component.name}</${tag}>`;
1169
+ return {
1170
+ name: component.id,
1171
+ category: component.category,
1172
+ description: component.description,
1173
+ code
1174
+ };
1175
+ });
1176
+ }
1177
+
1178
+ // ../codegen/dist/shared/componentAliases.js
1179
+ var COMPONENT_ALIASES = {
1180
+ TextField: ["input"],
1181
+ TextInput: ["input"],
1182
+ InputField: ["input"],
1183
+ FormField: ["input"],
1184
+ Field: ["input"],
1185
+ Entry: ["input"],
1186
+ TextBox: ["textarea", "input"],
1187
+ MultilineInput: ["textarea"],
1188
+ PasswordField: ["password-input", "input"],
1189
+ PasswordInput: ["password-input", "input"],
1190
+ NumberField: ["number-input", "input"],
1191
+ NumberInput: ["number-input", "input"],
1192
+ SpinButton: ["number-input", "input"],
1193
+ SearchBar: ["search-input", "input"],
1194
+ SearchField: ["search-input", "input"],
1195
+ SearchBox: ["search-input", "input"],
1196
+ OTP: ["otp-input", "input"],
1197
+ PinInput: ["otp-input", "input"],
1198
+ VerificationCode: ["otp-input", "input"],
1199
+ Combobox: ["select", "dropdown-list"],
1200
+ Autocomplete: ["select", "dropdown-list"],
1201
+ Listbox: ["select", "dropdown-list"],
1202
+ Picker: ["select", "dropdown-list"],
1203
+ Menu: ["dropdown-list", "select"],
1204
+ MenuList: ["dropdown-list", "select"],
1205
+ ContextMenu: ["dropdown-list"],
1206
+ TokenInput: ["multi-select", "select"],
1207
+ TagsInput: ["multi-select", "select"],
1208
+ CheckboxGroup: ["checkbox"],
1209
+ Tickbox: ["checkbox"],
1210
+ RadioGroup: ["radio"],
1211
+ RadioButton: ["radio"],
1212
+ OptionButton: ["radio"],
1213
+ ToggleSwitch: ["switch", "toggle"],
1214
+ ToggleButton: ["toggle", "switch"],
1215
+ SegmentedControl: ["toggle", "tabs"],
1216
+ RangeSlider: ["slider"],
1217
+ Range: ["slider"],
1218
+ Dialog: ["modal"],
1219
+ AlertDialog: ["modal"],
1220
+ Sheet: ["modal", "drawer"],
1221
+ Popup: ["modal", "popover"],
1222
+ Lightbox: ["modal"],
1223
+ SideSheet: ["drawer", "sidebar"],
1224
+ SidePanel: ["drawer", "sidebar"],
1225
+ Offcanvas: ["drawer", "sidebar"],
1226
+ Flyout: ["popover", "dropdown-list"],
1227
+ HoverCard: ["popover", "tooltip"],
1228
+ Hint: ["tooltip"],
1229
+ Infotip: ["tooltip"],
1230
+ Popper: ["tooltip", "popover"],
1231
+ Snackbar: ["toast", "alert"],
1232
+ Notification: ["toast", "alert"],
1233
+ Notice: ["alert", "toast"],
1234
+ Banner: ["alert"],
1235
+ Callout: ["alert"],
1236
+ InlineMessage: ["alert"],
1237
+ InfoBox: ["alert"],
1238
+ Tag: ["chip", "badge"],
1239
+ Pill: ["chip", "badge"],
1240
+ StatusBadge: ["badge", "chip"],
1241
+ Counter: ["badge", "chip"],
1242
+ DataGrid: ["data-table"],
1243
+ Table: ["data-table"],
1244
+ DataTable: ["data-table"],
1245
+ Calendar: ["date-picker"],
1246
+ DateField: ["date-picker"],
1247
+ DateRangePicker: ["date-picker"],
1248
+ ListView: ["list"],
1249
+ ItemList: ["list"],
1250
+ Repeater: ["list"],
1251
+ CircularProgress: ["progress-indicator", "progress", "spinner"],
1252
+ ProgressCircle: ["progress-indicator", "progress"],
1253
+ ProgressBar: ["progress"],
1254
+ LinearProgress: ["progress"],
1255
+ Meter: ["progress"],
1256
+ Gauge: ["progress"],
1257
+ Loader: ["spinner"],
1258
+ Loading: ["spinner"],
1259
+ LoadingIndicator: ["spinner", "progress-indicator"],
1260
+ ActivityIndicator: ["spinner"],
1261
+ Throbber: ["spinner"],
1262
+ Placeholder: ["skeleton"],
1263
+ Shimmer: ["skeleton"],
1264
+ LoadingSkeleton: ["skeleton"],
1265
+ AppBar: ["navbar"],
1266
+ TopBar: ["navbar"],
1267
+ Toolbar: ["navbar"],
1268
+ NavigationBar: ["navbar"],
1269
+ Nav: ["navbar"],
1270
+ SideNav: ["sidebar", "navbar"],
1271
+ NavigationRail: ["sidebar", "navbar"],
1272
+ TabBar: ["tabs"],
1273
+ TabList: ["tabs"],
1274
+ TabPanel: ["tabs"],
1275
+ Breadcrumbs: ["breadcrumb"],
1276
+ Crumbs: ["breadcrumb"],
1277
+ Paginator: ["pagination"],
1278
+ Pager: ["pagination"],
1279
+ Stepper: ["pagination"],
1280
+ Anchor: ["link"],
1281
+ Hyperlink: ["link"],
1282
+ NavLink: ["link"],
1283
+ Panel: ["card"],
1284
+ Tile: ["card"],
1285
+ Collapse: ["accordion"],
1286
+ Collapsible: ["accordion"],
1287
+ Disclosure: ["accordion"],
1288
+ Expander: ["accordion"],
1289
+ Details: ["accordion"],
1290
+ Separator: ["divider"],
1291
+ Rule: ["divider"],
1292
+ PageTitle: ["page-header"],
1293
+ Masthead: ["page-header"],
1294
+ TitleBar: ["page-header"],
1295
+ Hero: ["page-header"],
1296
+ BlankSlate: ["empty-state"],
1297
+ NoData: ["empty-state"],
1298
+ ZeroState: ["empty-state"],
1299
+ ProfilePicture: ["avatar"],
1300
+ UserPic: ["avatar"],
1301
+ Initials: ["avatar"],
1302
+ Img: ["image"],
1303
+ Picture: ["image"],
1304
+ Thumbnail: ["image"],
1305
+ Glyph: ["icon"],
1306
+ Symbol: ["icon"],
1307
+ FAB: ["icon-button", "button"],
1308
+ FloatingActionButton: ["icon-button", "button"],
1309
+ ActionButton: ["icon-button", "button"],
1310
+ DropdownButton: ["split-button", "button"],
1311
+ ButtonGroup: ["split-button", "button"],
1312
+ Btn: ["button"],
1313
+ CTA: ["button"],
1314
+ Submit: ["button"],
1315
+ Video: ["video-frame"],
1316
+ Player: ["video-frame"],
1317
+ MediaFrame: ["video-frame"]
1318
+ };
1319
+ function aliasKey(name) {
1320
+ return (name ?? "").toLowerCase().replace(/[^a-z0-9]/g, "");
1321
+ }
1322
+ var ALIAS_INDEX = Object.fromEntries(Object.entries(COMPONENT_ALIASES).map(([alias, candidates]) => [
1323
+ aliasKey(alias),
1324
+ candidates
1325
+ ]));
1326
+ function resolveComponentName(wanted, available) {
1327
+ const lookup = /* @__PURE__ */ new Map();
1328
+ for (const name of available) {
1329
+ if (typeof name !== "string" || !name.trim())
1330
+ continue;
1331
+ const key2 = aliasKey(name);
1332
+ if (!lookup.has(key2))
1333
+ lookup.set(key2, name);
1334
+ }
1335
+ const key = aliasKey(wanted);
1336
+ if (!key)
1337
+ return null;
1338
+ const direct = lookup.get(key);
1339
+ if (direct !== void 0)
1340
+ return direct;
1341
+ for (const candidate of ALIAS_INDEX[key] ?? []) {
1342
+ const found = lookup.get(aliasKey(candidate));
1343
+ if (found !== void 0)
1344
+ return found;
1345
+ }
1346
+ return null;
1347
+ }
1348
+ function aliasesFor(canonical, available) {
1349
+ const key = aliasKey(canonical);
1350
+ const ranked = [];
1351
+ for (const [alias, candidates] of Object.entries(COMPONENT_ALIASES)) {
1352
+ const keys = candidates.map(aliasKey);
1353
+ const rank = keys.indexOf(key);
1354
+ if (rank < 0)
1355
+ continue;
1356
+ if (aliasKey(alias) === key)
1357
+ continue;
1358
+ if (aliasKey(resolveComponentName(alias, available) ?? "") === key) {
1359
+ ranked.push([rank, alias]);
1360
+ }
1361
+ }
1362
+ return ranked.sort((a, b) => a[0] - b[0] || a[1].localeCompare(b[1])).map(([, alias]) => alias);
1363
+ }
1364
+
1365
+ // ../codegen/dist/shared/tactileKit.js
1366
+ var TACTILE_CSS_PATH = "tokens/tactile.css";
1367
+ var TACTILE_COMPONENT_NAMES = /* @__PURE__ */ new Set([
1368
+ "button",
1369
+ "icon-button",
1370
+ "switch",
1371
+ "checkbox",
1372
+ "slider",
1373
+ "toggle"
1374
+ ]);
1375
+ var HEADER = `Generated by Flash DSM Maker - tactile kit. Styles live in ${TACTILE_CSS_PATH}.`;
1376
+
1377
+ // ../codegen/dist/shared/agentsMd.js
1378
+ var MAX_ALIASES_PER_COMPONENT = 8;
1379
+ var NAMED_PRIMITIVES = ["Box", "Stack", "Flex", "Grid", "Container"];
1380
+ function semanticRoles(tokens) {
1381
+ const all = [
1382
+ ...tokens.colors ?? [],
1383
+ ...tokens.lightColors ?? []
1384
+ ];
1385
+ const byId = /* @__PURE__ */ new Map();
1386
+ for (const token of all) {
1387
+ if (token.name && !byId.has(token.name))
1388
+ byId.set(token.name, token);
1389
+ }
1390
+ const seen = /* @__PURE__ */ new Set();
1391
+ const out = [];
1392
+ for (const token of all) {
1393
+ const ref = token.referencedTokenId;
1394
+ if (!ref || !token.name || seen.has(token.name))
1395
+ continue;
1396
+ seen.add(token.name);
1397
+ const target = byId.get(ref);
1398
+ out.push([
1399
+ tokensToCssVarName(token.name),
1400
+ tokensToCssVarName(target?.name ?? ref),
1401
+ target?.value ?? token.value ?? ""
1402
+ ]);
1403
+ }
1404
+ return out;
1405
+ }
1406
+ function componentLines(components) {
1407
+ const names = components.map((c) => c.name).filter(Boolean);
1408
+ const byName = new Map(components.map((c) => [c.name, c]));
1409
+ const lines = [];
1410
+ for (const name of [...names].sort()) {
1411
+ const spec = byName.get(name);
1412
+ let line = `- \`${name}\``;
1413
+ if (spec?.description?.trim())
1414
+ line += ` - ${spec.description.trim()}`;
1415
+ lines.push(line);
1416
+ const aliases = aliasesFor(name, names);
1417
+ if (aliases.length) {
1418
+ const shown = aliases.slice(0, MAX_ALIASES_PER_COMPONENT).join(", ");
1419
+ lines.push(` - Also known as: ${shown}`);
1420
+ }
1421
+ }
1422
+ return lines;
1423
+ }
1424
+ function interactionsBlock(components) {
1425
+ const kit = components.map((c) => c.name).filter((name) => TACTILE_COMPONENT_NAMES.has(name)).sort();
1426
+ const which = kit.length ? `the ${kit.map((n) => `\`${n}\``).join(", ")} components` : "the kit's controls";
1427
+ return `
1428
+ ## Interactions
1429
+
1430
+ This system uses the tactile kit. \`${TACTILE_CSS_PATH}\` gives ${which}
1431
+ their look and motion: pill surfaces, ink for selected states, a 90ms press
1432
+ and a small spring on release.
1433
+
1434
+ - Use these components as they are. Do not restyle their \`tk-*\` classes,
1435
+ add transitions to them, or rebuild them with utility classes.
1436
+ - Keep the native input inside the switch, checkbox, segmented control and
1437
+ slider. Their state lives there (\`:checked\`, \`:has()\`), so swapping it
1438
+ for a \`div\` breaks the behaviour and the accessibility together.
1439
+ - To change the motion, change the tokens (\`--ease-spring\`,
1440
+ \`--ease-bounce\`, \`--duration-press\`), not the stylesheet.
1441
+ `;
1442
+ }
1443
+ function generateAgentsMd(options) {
1444
+ const { projectName, tokens, components, library, interactionKit } = options;
1445
+ const lines = componentLines(components);
1446
+ const componentsBlock = lines.length ? `Build with these - they live in \`components/\`. Do not create a second
1447
+ version of one that already exists, and do not import a component from
1448
+ another UI library to fill a gap.
1449
+
1450
+ ${lines.join("\n")}` : `This export ships no components. Style plain elements with the tokens
1451
+ in \`tokens/tokens.css\`.`;
1452
+ const roles = semanticRoles(tokens);
1453
+ const rolesBlock = roles.length ? `Style with the role, never the primitive underneath it.
1454
+ \`var(--primary)\` survives a rebrand; \`var(--primary-500)\` and a raw hex
1455
+ do not.
1456
+
1457
+ ${roles.map(([role, ref, value]) => `- \`--${role}\` \u2192 \`var(--${ref})\`${value ? ` (${value})` : ""}`).join("\n")}` : `This export has no semantic colour layer - use the named tokens in
1458
+ \`tokens/tokens.css\`.`;
1459
+ const libraryNote = library ? `
1460
+ This project is themed for **${library}**. Its theme file under
1461
+ \`theme/\` is generated from the same tokens - change tokens, not the theme.
1462
+ ` : "";
1463
+ return `# ${projectName}
1464
+
1465
+ Instructions for AI coding agents working in this project. Its UI is built
1466
+ on a design system exported from Flash DSM.
1467
+
1468
+ Token reference: \`tokens/tokens.css\` (CSS custom properties) and
1469
+ \`tokens/tokens.json\` (DTCG).
1470
+ ${libraryNote}
1471
+ ## Rules
1472
+
1473
+ 1. Use a component from the catalogue below before writing a new one. If
1474
+ nothing fits, compose from existing components rather than inventing a
1475
+ new primitive.
1476
+ 2. Style only with the tokens this system defines. No hex codes, no
1477
+ arbitrary Tailwind values (\`bg-[#1e293b]\`), no inline pixel sizes.
1478
+ 3. Prefer the semantic colour roles over the primitives they point at.
1479
+ 4. Keep the foreground/background pairs as defined - they are tuned for
1480
+ WCAG AA contrast. Do not mix a foreground from one pair with a
1481
+ background from another.
1482
+ 5. If a value you need has no token, say so in your response instead of
1483
+ hardcoding it. A flagged gap is fixable; a hardcoded hex is invisible.
1484
+
1485
+ ## Components
1486
+
1487
+ ${componentsBlock}
1488
+ ${interactionKit === "tactile" ? interactionsBlock(components) : ""}
1489
+ ## Layout
1490
+
1491
+ ${NAMED_PRIMITIVES.map((p) => `\`${p}\``).join(", ")}
1492
+ are not components in this system - they are layout primitives from other
1493
+ libraries. Use plain elements (\`div\`, \`section\`, \`ul\`) with this
1494
+ system's spacing tokens instead of reaching for a component that does not
1495
+ exist here.
1496
+
1497
+ ## Colour roles
1498
+
1499
+ ${rolesBlock}
1500
+
1501
+ ## Do not
1502
+
1503
+ - Do not install or import another UI library (MUI, Chakra, Ant, Radix,
1504
+ Bootstrap) to satisfy a requirement. If this system cannot express it,
1505
+ say so.
1506
+ - Do not invent a component name. Every name you import must appear in the
1507
+ catalogue above.
1508
+ - Do not hardcode a colour, spacing, radius, shadow or font size that has
1509
+ a token.
1510
+ - Do not restyle a component inline to make it look like a different
1511
+ variant. Use the variant the system defines.
1512
+ `;
1513
+ }
1514
+
1515
+ // ../codegen/dist/shared/motionPrune.js
1516
+ var CORE_MOTION = /* @__PURE__ */ new Set([
1517
+ "duration-fast",
1518
+ "duration-normal",
1519
+ "duration-slow",
1520
+ "ease-in",
1521
+ "ease-out",
1522
+ "ease-in-out",
1523
+ "motion-enter",
1524
+ "motion-exit",
1525
+ "motion-transition",
1526
+ "motion-scale",
1527
+ "motion-distance-sm",
1528
+ "motion-distance-md",
1529
+ "motion-distance-lg"
1530
+ ]);
1531
+ var VAR_REF_RE = /var\(\s*--([a-z0-9-]+)/gi;
1532
+ function collectVarRefs(text, into) {
1533
+ for (const match of text.matchAll(VAR_REF_RE)) {
1534
+ into.add(match[1].toLowerCase());
1535
+ }
1536
+ }
1537
+ function pruneUnusedMotionTokens(tokens, components = []) {
1538
+ const motion = tokens.motion ?? [];
1539
+ if (!motion.length)
1540
+ return tokens;
1541
+ const motionNames = /* @__PURE__ */ new Map();
1542
+ for (const t of motion) {
1543
+ if (t?.name)
1544
+ motionNames.set(t.name.toLowerCase(), t);
1545
+ }
1546
+ const referenced = /* @__PURE__ */ new Set();
1547
+ const otherCategories = [
1548
+ tokens.colors,
1549
+ tokens.lightColors,
1550
+ tokens.spacing,
1551
+ tokens.typography,
1552
+ tokens.radius,
1553
+ tokens.shadows,
1554
+ tokens.border,
1555
+ tokens.effects,
1556
+ tokens.icons
1557
+ ];
1558
+ for (const list of otherCategories) {
1559
+ for (const t of list ?? [])
1560
+ collectVarRefs(t?.value ?? "", referenced);
1561
+ }
1562
+ for (const c of components)
1563
+ collectVarRefs(c?.code ?? "", referenced);
1564
+ const keep = /* @__PURE__ */ new Set();
1565
+ const queue = [];
1566
+ for (const name of motionNames.keys()) {
1567
+ if (CORE_MOTION.has(name) || referenced.has(name)) {
1568
+ keep.add(name);
1569
+ queue.push(name);
1570
+ }
1571
+ }
1572
+ while (queue.length) {
1573
+ const token = motionNames.get(queue.pop());
1574
+ if (!token)
1575
+ continue;
1576
+ const refs = /* @__PURE__ */ new Set();
1577
+ collectVarRefs(token.value ?? "", refs);
1578
+ if (token.referencedTokenId)
1579
+ refs.add(token.referencedTokenId.toLowerCase());
1580
+ for (const ref of refs) {
1581
+ if (motionNames.has(ref) && !keep.has(ref)) {
1582
+ keep.add(ref);
1583
+ queue.push(ref);
1584
+ }
1585
+ }
1586
+ }
1587
+ if (keep.size === motionNames.size)
1588
+ return tokens;
1589
+ return {
1590
+ ...tokens,
1591
+ motion: motion.filter((t) => keep.has((t?.name ?? "").toLowerCase()))
1592
+ };
1593
+ }
1594
+
1595
+ // ../codegen/dist/repo-token-diff.js
1596
+ var GROUP_SECTIONS = {
1597
+ spacing: "spacing",
1598
+ typography: "typography",
1599
+ borderRadius: "radius",
1600
+ shadow: "shadows"
1601
+ };
1602
+ var BUNDLE_SECTION = {
1603
+ colors: "design-tokens-dark-colors",
1604
+ lightColors: "design-tokens-light-colors",
1605
+ spacing: "design-tokens-spacing",
1606
+ typography: "design-tokens-typography",
1607
+ radius: "design-tokens-radius",
1608
+ shadows: "design-tokens-shadow"
1609
+ };
1610
+ function groupKey(group, name) {
1611
+ return tokensToCssVarName(name).replace(new RegExp(`^${group}-?`), "") || name;
1612
+ }
1613
+ var HEX = /^#[0-9a-f]{3,8}$/i;
1614
+ function same(a, b) {
1615
+ const x = a.trim();
1616
+ const y = b.trim();
1617
+ return HEX.test(x) && HEX.test(y) ? x.toLowerCase() === y.toLowerCase() : x === y;
1618
+ }
1619
+ var isReference = (value) => /^\{[^}]+\}$/.test(value.trim());
1620
+ function modesOf(entry) {
1621
+ const modes = entry.$extensions?.[FLASH_MODES_EXTENSION];
1622
+ return modes && typeof modes === "object" ? modes : null;
1623
+ }
1624
+ function themeValue(entry, theme, defaultTheme, web) {
1625
+ const value = typeof entry.$value === "string" ? entry.$value : void 0;
1626
+ if (theme === defaultTheme && web && value !== void 0 && typeof web.$value === "string" && !same(value, web.$value)) {
1627
+ return value;
1628
+ }
1629
+ const modes = modesOf(entry);
1630
+ if (modes) {
1631
+ const own = modes[theme];
1632
+ return typeof own === "string" ? own : void 0;
1633
+ }
1634
+ return value;
1635
+ }
1636
+ function parseTree(text) {
1637
+ try {
1638
+ const parsed = JSON.parse(text);
1639
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
1640
+ } catch {
1641
+ return null;
1642
+ }
1643
+ }
1644
+ function diffRepoTokens(doc, repoDtcg) {
1645
+ const tokens = fromAtomicDocument(doc);
1646
+ const web = JSON.parse(generateJSON(tokens));
1647
+ const repo = parseTree(repoDtcg);
1648
+ const edits = [];
1649
+ const findings = [];
1650
+ if (!repo) {
1651
+ findings.push({
1652
+ path: "(file)",
1653
+ code: "unreadable",
1654
+ reason: "The tokens file is not a JSON object any more. Fix it, or pull again to restore it."
1655
+ });
1656
+ return { edits, findings };
1657
+ }
1658
+ const compare = (path, section, name, before, after, theme) => {
1659
+ if (before === void 0 || after === void 0)
1660
+ return;
1661
+ if (same(before, after))
1662
+ return;
1663
+ if (isReference(before) || isReference(after)) {
1664
+ findings.push({
1665
+ path,
1666
+ code: "alias",
1667
+ before,
1668
+ after,
1669
+ reason: "This token is an alias, or became one. Re-point aliases in Flash; a push only carries literal values."
1670
+ });
1671
+ return;
1672
+ }
1673
+ edits.push({
1674
+ path,
1675
+ section,
1676
+ name,
1677
+ ...theme ? { theme } : {},
1678
+ before,
1679
+ after
1680
+ });
1681
+ };
1682
+ const presence = (group, webGroup) => {
1683
+ const repoGroup = repo[group] ?? {};
1684
+ for (const key of Object.keys(repoGroup)) {
1685
+ if (key.startsWith("$") || key in webGroup)
1686
+ continue;
1687
+ findings.push({
1688
+ path: `${group}.${key}`,
1689
+ code: "added-in-repo",
1690
+ reason: "Not in the design system. Add it in Flash, then pull; a push does not create tokens."
1691
+ });
1692
+ }
1693
+ for (const key of Object.keys(webGroup)) {
1694
+ if (key in repoGroup)
1695
+ continue;
1696
+ findings.push({
1697
+ path: `${group}.${key}`,
1698
+ code: "removed-in-repo",
1699
+ reason: "Deleted from the file but still in the design system. A push does not remove tokens; delete it in Flash if that is the intent."
1700
+ });
1701
+ }
1702
+ };
1703
+ const webColors = web.color ?? {};
1704
+ presence("color", webColors);
1705
+ const palette = singleThemePalette(tokens);
1706
+ const defaultTheme = palette === tokens.colors ? "dark" : "light";
1707
+ const names = {
1708
+ dark: new Map(tokens.colors.map((t) => [tokensToCssVarName(t.name), t])),
1709
+ light: new Map(tokens.lightColors.map((t) => [tokensToCssVarName(t.name), t]))
1710
+ };
1711
+ for (const [key, webEntry] of Object.entries(webColors)) {
1712
+ const repoEntry = repo.color?.[key];
1713
+ if (!repoEntry)
1714
+ continue;
1715
+ for (const theme of ["dark", "light"]) {
1716
+ const token = names[theme].get(key);
1717
+ if (!token)
1718
+ continue;
1719
+ compare(`color.${key}`, BUNDLE_SECTION[theme === "dark" ? "colors" : "lightColors"], token.name, themeValue(webEntry, theme, defaultTheme), themeValue(repoEntry, theme, defaultTheme, webEntry), theme);
1720
+ }
1721
+ }
1722
+ for (const [group, field] of Object.entries(GROUP_SECTIONS)) {
1723
+ const webGroup = web[group];
1724
+ if (!webGroup)
1725
+ continue;
1726
+ presence(group, webGroup);
1727
+ const byKey = new Map(tokens[field].map((t) => [groupKey(group, t.name), t]));
1728
+ for (const [key, webEntry] of Object.entries(webGroup)) {
1729
+ const repoEntry = repo[group]?.[key];
1730
+ const token = byKey.get(key);
1731
+ if (!repoEntry || !token)
1732
+ continue;
1733
+ if (typeof webEntry.$value !== "string")
1734
+ continue;
1735
+ if (typeof repoEntry.$value !== "string")
1736
+ continue;
1737
+ compare(`${group}.${key}`, BUNDLE_SECTION[field], token.name, webEntry.$value, repoEntry.$value);
1738
+ }
1739
+ }
1740
+ return { edits, findings };
1741
+ }
1742
+
1743
+ // ../codegen/dist/repo-bundle.js
1744
+ var DEFAULT_REPO_BUNDLE_PATHS = {
1745
+ tokensDtcg: "design-tokens.json",
1746
+ tokensCss: "tokens.css",
1747
+ styleDictionary: null,
1748
+ styleDictionaryConfig: null,
1749
+ agentsMd: "AGENTS.md"
1750
+ };
1751
+ function hashRepoFiles(files) {
1752
+ const sorted = [...files].sort((a, b) => a.path.localeCompare(b.path));
1753
+ const framed = sorted.map((f) => `${f.path.length}:${f.path}${f.content.length}:${f.content}`);
1754
+ return shortHash(`${framed.length}:${framed.join("")}`);
1755
+ }
1756
+ function documentToRepoBundle(doc, options = {}) {
1757
+ const paths = {
1758
+ ...DEFAULT_REPO_BUNDLE_PATHS,
1759
+ ...options.paths
1760
+ };
1761
+ const components = atomicComponentsToExportSpecs(doc);
1762
+ const tokens = pruneUnusedMotionTokens(fromAtomicDocument(doc), components);
1763
+ const projectName = options.projectName ?? doc.name;
1764
+ const files = [];
1765
+ if (paths.tokensDtcg) {
1766
+ files.push({ path: paths.tokensDtcg, content: generateJSON(tokens) });
1767
+ }
1768
+ if (paths.tokensCss) {
1769
+ files.push({
1770
+ path: paths.tokensCss,
1771
+ content: generateCSS(tokens, options.attribution === true)
1772
+ });
1773
+ }
1774
+ if (paths.styleDictionary) {
1775
+ files.push({
1776
+ path: paths.styleDictionary,
1777
+ content: generateStyleDictionary(tokens)
1778
+ });
1779
+ }
1780
+ if (paths.styleDictionaryConfig) {
1781
+ files.push({
1782
+ path: paths.styleDictionaryConfig,
1783
+ content: generateStyleDictionaryConfig()
1784
+ });
1785
+ }
1786
+ if (paths.agentsMd) {
1787
+ files.push({
1788
+ path: paths.agentsMd,
1789
+ content: generateAgentsMd({
1790
+ projectName,
1791
+ tokens,
1792
+ components,
1793
+ ...options.library ? { library: options.library } : {}
1794
+ })
1795
+ });
1796
+ }
1797
+ return {
1798
+ files,
1799
+ hashes: tokenHashes(tokens),
1800
+ contentHash: hashRepoFiles(files)
1801
+ };
1802
+ }
1803
+
1804
+ // src/document.ts
1805
+ var TYPES = new Set(TokenTypeSchema.options);
1806
+ var CATEGORIES = new Set(TokenCategorySchema.options);
1807
+ var CATEGORY_ALIASES = {
1808
+ color: "colors",
1809
+ shadows: "shadow",
1810
+ effect: "effects",
1811
+ icon: "icons",
1812
+ borders: "border",
1813
+ font: "typography",
1814
+ fonts: "typography"
1815
+ };
1816
+ var TYPE_FOR_CATEGORY = {
1817
+ colors: "color",
1818
+ spacing: "size",
1819
+ typography: "font",
1820
+ radius: "radius",
1821
+ shadow: "shadow",
1822
+ motion: "motion",
1823
+ border: "border",
1824
+ effects: "effect",
1825
+ icons: "icon"
1826
+ };
1827
+ function normaliseToken(token, fallbackCategory) {
1828
+ if (!token || typeof token !== "object" || Array.isArray(token)) return token;
1829
+ const t = { ...token };
1830
+ if (typeof t.category === "string" && !CATEGORIES.has(t.category)) {
1831
+ t.category = CATEGORY_ALIASES[t.category] ?? fallbackCategory ?? t.category;
1832
+ }
1833
+ if (typeof t.type === "string" && !TYPES.has(t.type)) {
1834
+ const category = typeof t.category === "string" && CATEGORIES.has(t.category) ? t.category : CATEGORY_ALIASES[t.type] ?? t.type;
1835
+ t.type = TYPE_FOR_CATEGORY[category] ?? t.type;
1836
+ }
1837
+ return t;
1838
+ }
1839
+ var FOUNDATION_CATEGORY = {
1840
+ colors: "colors",
1841
+ typography: "typography",
1842
+ spacing: "spacing",
1843
+ radius: "radius",
1844
+ shadows: "shadow",
1845
+ motion: "motion",
1846
+ border: "border",
1847
+ effects: "effects",
1848
+ icons: "icons"
1849
+ };
1850
+ function normaliseServerDocument(input) {
1851
+ if (!input || typeof input !== "object" || Array.isArray(input)) return input;
1852
+ const doc = { ...input };
1853
+ const foundations = doc.foundations;
1854
+ if (foundations && typeof foundations === "object" && !Array.isArray(foundations)) {
1855
+ const next = {};
1856
+ for (const [key, list] of Object.entries(foundations)) {
1857
+ next[key] = Array.isArray(list) ? list.map((t) => normaliseToken(t, FOUNDATION_CATEGORY[key])) : list;
1858
+ }
1859
+ doc.foundations = next;
1860
+ }
1861
+ if (Array.isArray(doc.semantics)) {
1862
+ doc.semantics = doc.semantics.map((t) => normaliseToken(t));
1863
+ }
1864
+ return doc;
1865
+ }
1866
+ function parseServerDocument(input) {
1867
+ return parseFlashAtomicDocument(normaliseServerDocument(input));
1868
+ }
1869
+
1870
+ // src/tools/pull-from-web.ts
1871
+ import { join as join2, resolve as resolve4 } from "path";
1872
+
1873
+ // src/lock.ts
1874
+ import { readFile as readFile2 } from "fs/promises";
1875
+ import { z } from "zod";
1876
+ var GENOME_LOCK_BASENAME = "genome.lock";
1877
+ var GenomeLockSchema = z.object({
1878
+ version: z.literal(1),
1879
+ genomeId: z.number().int().positive(),
1880
+ /** The web revision these files were projected from. */
1881
+ revision: z.union([z.string(), z.number()]),
1882
+ /** `documentToRepoBundle`'s hash over the files it wrote. */
1883
+ contentHash: z.string().min(1),
1884
+ /** ISO timestamp of the pull, for a human reading the file. */
1885
+ pulledAt: z.string().min(1),
1886
+ /** The paths the pull wrote, so a later drift check knows what to hash. */
1887
+ files: z.array(z.string().min(1)).default([]),
1888
+ /**
1889
+ * Each written file's own hash, so a drift check can say *which* files
1890
+ * moved rather than only that something did. Empty in locks written before
1891
+ * it existed; those still answer the yes-or-no question through
1892
+ * `contentHash`.
1893
+ */
1894
+ fileHashes: z.record(z.string(), z.string()).default({})
1895
+ });
1896
+ function renderLock(lock) {
1897
+ return `${JSON.stringify(lock, null, 2)}
1898
+ `;
1899
+ }
1900
+ async function readLock(path) {
1901
+ const raw = await readFile2(path, "utf8").catch(() => null);
1902
+ if (!raw) return null;
1903
+ try {
1904
+ const parsed = GenomeLockSchema.safeParse(JSON.parse(raw));
1905
+ return parsed.success ? parsed.data : null;
1906
+ } catch {
1907
+ return null;
1908
+ }
1909
+ }
1910
+
1911
+ // src/write-plan.ts
1912
+ import { mkdir, readFile as readFile3, realpath, writeFile } from "fs/promises";
1913
+ import { dirname as dirname2, isAbsolute as isAbsolute2, join, relative, resolve as resolve3 } from "path";
1914
+ var MAX_HUNKS = 20;
1915
+ function hunks(before, after) {
1916
+ const a = before.split("\n");
1917
+ const b = after.split("\n");
1918
+ let start = 0;
1919
+ while (start < a.length && start < b.length && a[start] === b[start]) start++;
1920
+ let endA = a.length;
1921
+ let endB = b.length;
1922
+ while (endA > start && endB > start && a[endA - 1] === b[endB - 1]) {
1923
+ endA--;
1924
+ endB--;
1925
+ }
1926
+ const removed = a.slice(start, endA);
1927
+ const added = b.slice(start, endB);
1928
+ const out = [];
1929
+ for (const line of removed.slice(0, MAX_HUNKS)) out.push(`- ${line}`);
1930
+ if (removed.length > MAX_HUNKS) {
1931
+ out.push(`\u2026 ${removed.length - MAX_HUNKS} more removed`);
1932
+ }
1933
+ for (const line of added.slice(0, MAX_HUNKS)) out.push(`+ ${line}`);
1934
+ if (added.length > MAX_HUNKS) {
1935
+ out.push(`\u2026 ${added.length - MAX_HUNKS} more added`);
1936
+ }
1937
+ return out.join("\n");
1938
+ }
1939
+ function safeJoin(root, path) {
1940
+ if (isAbsolute2(path)) return null;
1941
+ const full = resolve3(join(root, path));
1942
+ const rel = relative(resolve3(root), full);
1943
+ if (rel.startsWith("..") || isAbsolute2(rel)) return null;
1944
+ return full;
1945
+ }
1946
+ async function containedAfterLinks(root, full) {
1947
+ let dir = dirname2(full);
1948
+ let realDir = null;
1949
+ for (let depth = 0; depth < 64; depth++) {
1950
+ realDir = await realpath(dir).catch(() => null);
1951
+ if (realDir) break;
1952
+ const parent = dirname2(dir);
1953
+ if (parent === dir) break;
1954
+ dir = parent;
1955
+ }
1956
+ if (!realDir) return true;
1957
+ const realRoot = await realpath(root).catch(() => null) ?? resolve3(root);
1958
+ const rel = relative(realRoot, join(realDir, relative(dir, full)));
1959
+ return !rel.startsWith("..") && !isAbsolute2(rel);
1960
+ }
1961
+ async function writeFiles(files, opts) {
1962
+ const root = resolve3(opts.workspaceRoot);
1963
+ const confirm = opts.confirm === true;
1964
+ const entries = [];
1965
+ const written = [];
1966
+ for (const file of files) {
1967
+ const full = safeJoin(root, file.path);
1968
+ if (full === null) {
1969
+ entries.push({
1970
+ action: "flag",
1971
+ targetKind: "file",
1972
+ path: file.path,
1973
+ code: "outside-workspace",
1974
+ reason: "This path leaves the workspace root, so Genome will not write it. Fix the outputs entry in genome.config.json."
1975
+ });
1976
+ continue;
1977
+ }
1978
+ if (!await containedAfterLinks(root, full)) {
1979
+ entries.push({
1980
+ action: "flag",
1981
+ targetKind: "file",
1982
+ path: file.path,
1983
+ code: "outside-workspace",
1984
+ reason: "A directory on this path is a link out of the workspace, so Genome will not write through it."
1985
+ });
1986
+ continue;
1987
+ }
1988
+ const before = await readFile3(full, "utf8").catch(() => null);
1989
+ if (before !== null && file.createOnly) {
1990
+ entries.push({
1991
+ action: "skip",
1992
+ targetKind: "file",
1993
+ path: file.path,
1994
+ reason: "Written once, then yours to edit. Genome leaves it alone."
1995
+ });
1996
+ continue;
1997
+ }
1998
+ if (before === file.content) {
1999
+ entries.push({ action: "skip", targetKind: "file", path: file.path });
2000
+ continue;
2001
+ }
2002
+ entries.push({
2003
+ action: before === null ? "create" : "update",
2004
+ targetKind: "file",
2005
+ path: file.path,
2006
+ ...before === null ? {} : { reason: hunks(before, file.content) }
2007
+ });
2008
+ if (!confirm) continue;
2009
+ try {
2010
+ await mkdir(dirname2(full), { recursive: true });
2011
+ await writeFile(full, file.content, "utf8");
2012
+ written.push(file.path);
2013
+ } catch (err) {
2014
+ entries[entries.length - 1] = {
2015
+ action: "flag",
2016
+ targetKind: "file",
2017
+ path: file.path,
2018
+ code: "write-failed",
2019
+ reason: err instanceof Error ? err.message : String(err)
2020
+ };
2021
+ }
2022
+ }
2023
+ return { ...makeChangePlan(entries, !confirm), written };
2024
+ }
2025
+
2026
+ // src/tools/pull-from-web.ts
2027
+ function inDir(dir, name) {
2028
+ return `${dir.replace(/\/+$/, "")}/${name}`;
2029
+ }
2030
+ function parseIconFiles(text) {
2031
+ if (!text?.trim()) return {};
2032
+ const parsed = JSON.parse(text);
2033
+ const files = parsed?.files;
2034
+ if (!files || typeof files !== "object" || Array.isArray(files)) return {};
2035
+ const out = {};
2036
+ for (const [path, content] of Object.entries(files)) {
2037
+ if (typeof content === "string") out[path] = content;
2038
+ }
2039
+ return out;
2040
+ }
2041
+ function iconFilePath(iconsDir, serverPath) {
2042
+ const relative2 = serverPath.replace(/^icons\//, "");
2043
+ if (!relative2 || relative2.startsWith("/") || relative2.includes("\\") || relative2.split("/").some((segment) => segment === ".." || segment === "")) {
2044
+ return null;
2045
+ }
2046
+ return inDir(iconsDir, relative2);
2047
+ }
2048
+ async function pullFromWeb(opts) {
2049
+ const envelope = await opts.client.getGenome();
2050
+ if (!envelope?.document) {
2051
+ throw new Error(
2052
+ "This design system has no v2 document to project. Open it in Flash and save once, then pull again."
2053
+ );
2054
+ }
2055
+ if (envelope.id !== opts.config.genomeId) {
2056
+ throw new Error(
2057
+ `This token belongs to design system #${envelope.id}, but genome.config.json names #${opts.config.genomeId}. Run link again, or use the token for #${opts.config.genomeId}.`
2058
+ );
2059
+ }
2060
+ const doc = parseServerDocument(envelope.document);
2061
+ const { outputs } = opts.config;
2062
+ const bundle = documentToRepoBundle(doc, {
2063
+ projectName: envelope.name,
2064
+ paths: {
2065
+ tokensDtcg: outputs.tokensDtcg,
2066
+ tokensCss: outputs.tokensCss,
2067
+ // The Style Dictionary source and AGENTS.md are not part of the token
2068
+ // projection: the first is opt-in, and the second comes from the
2069
+ // server so the repo and the hosted kit say the same thing.
2070
+ styleDictionary: null,
2071
+ styleDictionaryConfig: null,
2072
+ agentsMd: null
2073
+ }
2074
+ });
2075
+ const files = bundle.files.map((f) => ({
2076
+ path: f.path,
2077
+ content: f.content
2078
+ }));
2079
+ const dir = outputs.genomeDir;
2080
+ files.push({
2081
+ path: inDir(dir, "components.json"),
2082
+ content: `${JSON.stringify(
2083
+ doc.components.map((c) => ({
2084
+ id: c.id,
2085
+ name: c.name,
2086
+ category: c.category,
2087
+ ...c.description ? { description: c.description } : {},
2088
+ variants: c.variants.map((v) => v.name),
2089
+ states: c.states
2090
+ })),
2091
+ null,
2092
+ 2
2093
+ )}
2094
+ `
2095
+ });
2096
+ const kit = await opts.client.readResource("flash://design-system/kit.md").catch(() => null);
2097
+ if (kit) files.push({ path: inDir(dir, "kit.md"), content: kit });
2098
+ const componentDocs = await opts.client.readResource("flash://design-system/components.md").catch(() => null);
2099
+ if (componentDocs && componentDocs.trim()) {
2100
+ files.push({ path: inDir(dir, "components.md"), content: componentDocs });
2101
+ }
2102
+ const iconFiles = await opts.client.readResource("flash://design-system/icon-files.json").then(parseIconFiles).catch(() => ({}));
2103
+ const iconsDir = opts.config.outputs.icons ?? inDir(dir, "icons");
2104
+ for (const [serverPath, content] of Object.entries(iconFiles)) {
2105
+ const local = iconFilePath(iconsDir, serverPath);
2106
+ if (local) files.push({ path: local, content });
2107
+ }
2108
+ const lockPath = inDir(dir, GENOME_LOCK_BASENAME);
2109
+ const previous = await readLock(join2(resolve4(opts.workspaceRoot), lockPath));
2110
+ const contentHash = hashRepoFiles(
2111
+ files.map((f) => ({ path: f.path, content: f.content }))
2112
+ );
2113
+ const result = await writeFiles(files, {
2114
+ workspaceRoot: opts.workspaceRoot,
2115
+ confirm: opts.confirm
2116
+ });
2117
+ const accepted = new Set(
2118
+ result.entries.filter((e) => e.action !== "flag").map((e) => e.path)
2119
+ );
2120
+ const sameFiles = previous !== null && previous.files.length === accepted.size && previous.files.every((f) => accepted.has(f));
2121
+ const unchanged = sameFiles && previous.contentHash === contentHash && String(previous.revision) === String(envelope.revision);
2122
+ const fileHashes = {};
2123
+ for (const f of files) {
2124
+ if (accepted.has(f.path)) {
2125
+ fileHashes[f.path] = hashRepoFiles([
2126
+ { path: f.path, content: f.content }
2127
+ ]);
2128
+ }
2129
+ }
2130
+ const lock = {
2131
+ version: 1,
2132
+ genomeId: envelope.id,
2133
+ revision: envelope.revision,
2134
+ contentHash,
2135
+ pulledAt: unchanged ? previous.pulledAt : (/* @__PURE__ */ new Date()).toISOString(),
2136
+ files: [...accepted],
2137
+ fileHashes
2138
+ };
2139
+ const lockResult = await writeFiles(
2140
+ [{ path: lockPath, content: renderLock(lock) }],
2141
+ {
2142
+ workspaceRoot: opts.workspaceRoot,
2143
+ confirm: opts.confirm
2144
+ }
2145
+ );
2146
+ let recorded = false;
2147
+ const codebase = opts.config.codebase;
2148
+ if (opts.confirm === true && codebase) {
2149
+ const tokenFilesLanded = bundle.files.some((f) => accepted.has(f.path));
2150
+ const componentsLanded = accepted.has(inDir(dir, "components.json"));
2151
+ const items = [
2152
+ ...componentsLanded ? [
2153
+ ...doc.components.map((c) => ({
2154
+ type: "component",
2155
+ id: c.id,
2156
+ name: c.name
2157
+ })),
2158
+ ...doc.modules.map((m) => ({
2159
+ type: "module",
2160
+ id: m.id,
2161
+ name: m.name
2162
+ }))
2163
+ ] : [],
2164
+ // The token projection is pulled whole; one event stands for it.
2165
+ ...tokenFilesLanded ? [{ type: "token", name: "all" }] : []
2166
+ ];
2167
+ if (items.length) {
2168
+ try {
2169
+ await opts.client.callTool("record_pull", {
2170
+ codebase_id: codebase.id,
2171
+ codebase_name: codebase.name,
2172
+ revision: envelope.revision,
2173
+ items
2174
+ });
2175
+ recorded = true;
2176
+ } catch {
2177
+ recorded = false;
2178
+ }
2179
+ }
2180
+ }
2181
+ const entries = [...result.entries, ...lockResult.entries];
2182
+ return {
2183
+ ...makeChangePlan(entries, opts.confirm !== true),
2184
+ written: [...result.written, ...lockResult.written],
2185
+ genomeId: envelope.id,
2186
+ name: envelope.name,
2187
+ revision: envelope.revision,
2188
+ contentHash,
2189
+ recorded
2190
+ };
2191
+ }
2192
+
2193
+ // src/tools/detect-drift.ts
2194
+ import { readFile as readFile5 } from "fs/promises";
2195
+ import { join as join4, resolve as resolve6 } from "path";
2196
+
2197
+ // src/tools/repo-state.ts
2198
+ import { readFile as readFile4 } from "fs/promises";
2199
+ import { join as join3, resolve as resolve5 } from "path";
2200
+ function lockPathFor(config) {
2201
+ return `${config.outputs.genomeDir.replace(/\/+$/, "")}/${GENOME_LOCK_BASENAME}`;
2202
+ }
2203
+ async function readRepoState(opts) {
2204
+ const envelope = await opts.client.getGenome();
2205
+ if (!envelope?.document) {
2206
+ throw new Error(
2207
+ "This design system has no v2 document to compare against. Open it in Flash and save once, then try again."
2208
+ );
2209
+ }
2210
+ if (envelope.id !== opts.config.genomeId) {
2211
+ throw new Error(
2212
+ `This token belongs to design system #${envelope.id}, but genome.config.json names #${opts.config.genomeId}. Run link again, or use the token for #${opts.config.genomeId}.`
2213
+ );
2214
+ }
2215
+ const root = resolve5(opts.workspaceRoot);
2216
+ const lock = await readLock(join3(root, lockPathFor(opts.config)));
2217
+ const tokensPath = opts.config.outputs.tokensDtcg;
2218
+ const tokensText = await readFile4(join3(root, tokensPath), "utf8").catch(
2219
+ () => null
2220
+ );
2221
+ return {
2222
+ envelope,
2223
+ doc: parseServerDocument(envelope.document),
2224
+ lock,
2225
+ tokensPath,
2226
+ tokensText
2227
+ };
2228
+ }
2229
+
2230
+ // src/tools/detect-drift.ts
2231
+ async function detectDrift(opts) {
2232
+ const state = await readRepoState(opts);
2233
+ const { envelope, lock } = state;
2234
+ if (!lock) {
2235
+ return {
2236
+ ...makeChangePlan([], true),
2237
+ genomeId: envelope.id,
2238
+ local: { drifted: false, files: [], missing: [] },
2239
+ remote: {
2240
+ drifted: false,
2241
+ pulledRevision: null,
2242
+ revision: envelope.revision
2243
+ },
2244
+ pushable: 0,
2245
+ summary: "This repo has never pulled, so there is nothing to drift from. Run pull_from_web first."
2246
+ };
2247
+ }
2248
+ const root = resolve6(opts.workspaceRoot);
2249
+ const current = [];
2250
+ const moved = [];
2251
+ const missing = [];
2252
+ for (const path of lock.files) {
2253
+ const content = await readFile5(join4(root, path), "utf8").catch(() => null);
2254
+ if (content === null) {
2255
+ missing.push(path);
2256
+ continue;
2257
+ }
2258
+ current.push({ path, content });
2259
+ const recorded = lock.fileHashes[path];
2260
+ if (recorded && recorded !== hashRepoFiles([{ path, content }])) {
2261
+ moved.push(path);
2262
+ }
2263
+ }
2264
+ const localDrifted = missing.length > 0 || hashRepoFiles(current) !== lock.contentHash;
2265
+ const remoteDrifted = String(lock.revision) !== String(envelope.revision);
2266
+ const entries = [];
2267
+ let pushable = 0;
2268
+ if (state.tokensText !== null) {
2269
+ const diff = diffRepoTokens(state.doc, state.tokensText);
2270
+ pushable = diff.edits.length;
2271
+ for (const edit of diff.edits) {
2272
+ entries.push({
2273
+ action: "flag",
2274
+ targetKind: "token",
2275
+ path: edit.theme ? `${edit.path} (${edit.theme})` : edit.path,
2276
+ before: edit.before,
2277
+ after: edit.after,
2278
+ code: "repo-edit",
2279
+ reason: "Changed in the repo. push_to_web proposes it for review in Flash."
2280
+ });
2281
+ }
2282
+ for (const finding of diff.findings) {
2283
+ entries.push({
2284
+ action: "flag",
2285
+ targetKind: "token",
2286
+ path: finding.path,
2287
+ before: finding.before ?? null,
2288
+ after: finding.after ?? null,
2289
+ code: finding.code,
2290
+ reason: finding.reason
2291
+ });
2292
+ }
2293
+ }
2294
+ let summary;
2295
+ if (!localDrifted && !remoteDrifted) {
2296
+ summary = "In step: the repo holds exactly what it pulled, and Flash has not moved.";
2297
+ } else if (remoteDrifted && localDrifted) {
2298
+ summary = `Both moved: Flash is at revision ${envelope.revision}, the repo pulled ${lock.revision}, and the repo has edits of its own. A push is refused while Flash is ahead, because it cannot tell your edits from Flash's. Keep a copy of the flagged values, pull, re-apply them, then push.`;
2299
+ } else if (remoteDrifted) {
2300
+ summary = `Flash moved from revision ${lock.revision} to ${envelope.revision}. Run pull_from_web to bring the repo up to date.`;
2301
+ } else if (pushable > 0) {
2302
+ summary = `The repo changed ${pushable} token value${pushable === 1 ? "" : "s"}. Run push_to_web to propose ${pushable === 1 ? "it" : "them"} for review in Flash.`;
2303
+ } else {
2304
+ summary = "The repo's files changed, but no token value a push can carry. See the flagged entries.";
2305
+ }
2306
+ return {
2307
+ ...makeChangePlan(entries, true),
2308
+ genomeId: envelope.id,
2309
+ local: { drifted: localDrifted, files: moved, missing },
2310
+ remote: {
2311
+ drifted: remoteDrifted,
2312
+ pulledRevision: lock.revision,
2313
+ revision: envelope.revision
2314
+ },
2315
+ pushable,
2316
+ summary
2317
+ };
2318
+ }
2319
+
2320
+ // src/tools/push-to-web.ts
2321
+ function findingEntry(finding) {
2322
+ return {
2323
+ action: "flag",
2324
+ targetKind: "token",
2325
+ path: finding.path,
2326
+ before: finding.before ?? null,
2327
+ after: finding.after ?? null,
2328
+ code: finding.code,
2329
+ reason: finding.reason
2330
+ };
2331
+ }
2332
+ async function pushToWeb(opts) {
2333
+ const state = await readRepoState(opts);
2334
+ const { envelope, lock } = state;
2335
+ if (!lock) {
2336
+ throw new Error(
2337
+ "This repo has never pulled, so there is no baseline to push from. Run pull_from_web first."
2338
+ );
2339
+ }
2340
+ if (String(lock.revision) !== String(envelope.revision)) {
2341
+ throw new Error(
2342
+ `Flash has moved on since this repo pulled (revision ${lock.revision}, now ${envelope.revision}). A push now could undo those changes. Keep a copy of your edits, pull, re-apply them, then push.`
2343
+ );
2344
+ }
2345
+ if (state.tokensText === null) {
2346
+ throw new Error(
2347
+ `${state.tokensPath} is missing, so there is nothing to push. Pull again to restore it.`
2348
+ );
2349
+ }
2350
+ const { edits, findings } = diffRepoTokens(state.doc, state.tokensText);
2351
+ const flagged = findings.map(findingEntry);
2352
+ const base = {
2353
+ genomeId: envelope.id,
2354
+ revision: envelope.revision,
2355
+ findings
2356
+ };
2357
+ if (!edits.length) {
2358
+ return {
2359
+ ...makeChangePlan(flagged, true),
2360
+ ...base,
2361
+ status: "no_changes",
2362
+ changeSetId: null,
2363
+ message: findings.length ? "No token value changed. The flagged entries are things a push does not carry." : "No token value changed since the last pull."
2364
+ };
2365
+ }
2366
+ const planned = edits.map((edit) => ({
2367
+ action: "update",
2368
+ targetKind: "token",
2369
+ path: edit.theme ? `${edit.path} (${edit.theme})` : edit.path,
2370
+ before: edit.before,
2371
+ after: edit.after
2372
+ }));
2373
+ if (opts.confirm !== true) {
2374
+ return {
2375
+ ...makeChangePlan([...planned, ...flagged], true),
2376
+ ...base,
2377
+ status: "dry_run",
2378
+ changeSetId: null,
2379
+ message: `${edits.length} token value${edits.length === 1 ? "" : "s"} to propose for review. Run again with confirm to open the change set in Flash.`
2380
+ };
2381
+ }
2382
+ const codebase = opts.config.codebase?.name ?? opts.config.name;
2383
+ const response = await opts.client.callTool(
2384
+ "propose_change_set",
2385
+ {
2386
+ base_revision: envelope.revision,
2387
+ source_label: `Genome \xB7 ${codebase}`,
2388
+ changes: edits.map((edit) => ({
2389
+ section: edit.section,
2390
+ name: edit.name,
2391
+ value: edit.after
2392
+ }))
2393
+ }
2394
+ );
2395
+ const skipped = (response.skipped ?? []).map((s) => ({
2396
+ action: "flag",
2397
+ targetKind: "token",
2398
+ path: `${s.section}/${s.name}`,
2399
+ code: "skipped-by-server",
2400
+ reason: s.reason
2401
+ }));
2402
+ const proposed = (response.diff ?? []).map(tokenChangeToPlanEntry);
2403
+ return {
2404
+ ...makeChangePlan([...proposed, ...skipped, ...flagged], false),
2405
+ ...base,
2406
+ status: response.no_changes ? "no_changes" : "proposed",
2407
+ changeSetId: response.change_set_id ?? null,
2408
+ message: response.message ?? (response.no_changes ? "Flash already has these values, or the same proposal is waiting for review." : "Proposed for review. Open Sync in Flash to apply or discard it.")
2409
+ };
2410
+ }
2411
+
2412
+ // src/tools/graph.ts
2413
+ async function graphDocument(client) {
2414
+ const envelope = await client.getGenome();
2415
+ if (!envelope?.document) {
2416
+ throw new Error(
2417
+ "These tools read the v2 document, and this system is not on schema v2 yet. Use get_tokens instead."
2418
+ );
2419
+ }
2420
+ return normaliseServerDocument(envelope.document);
2421
+ }
2422
+ async function runGraphTool(client, tool, args) {
2423
+ const doc = await graphDocument(client);
2424
+ switch (tool) {
2425
+ case "list_modules":
2426
+ return { modules: listModules(doc) };
2427
+ case "get_module": {
2428
+ const wanted = args.module?.trim();
2429
+ if (!wanted) throw new Error("Missing required argument: module");
2430
+ const found = getModule(doc, wanted);
2431
+ if (!found.found) {
2432
+ const shown = found.available.slice(0, 30).join(", ") || "none";
2433
+ throw new Error(`Module '${wanted}' not found. Available: ${shown}`);
2434
+ }
2435
+ return found.module;
2436
+ }
2437
+ case "get_relations": {
2438
+ const ref = args.token?.trim() || void 0;
2439
+ const relations = getRelations(doc, ref);
2440
+ if (ref !== void 0 && "found" in relations && !relations.found) {
2441
+ throw new Error(`Token '${ref}' not found in this system.`);
2442
+ }
2443
+ return relations;
2444
+ }
2445
+ case "get_impact": {
2446
+ const wanted = args.token?.trim();
2447
+ if (!wanted) throw new Error("Missing required argument: token");
2448
+ const impact = getImpact(doc, wanted);
2449
+ if (!impact.found) {
2450
+ throw new Error(`Token '${wanted}' not found in this system.`);
2451
+ }
2452
+ return impact;
2453
+ }
2454
+ }
2455
+ }
2456
+
2457
+ // src/tools/sync-context.ts
2458
+ var CREATE_ONLY = true;
2459
+ async function syncContext(opts) {
2460
+ const { outputs } = opts.config;
2461
+ const files = [];
2462
+ const agents = await opts.client.readResource("flash://design-system/AGENTS.md").catch(() => null);
2463
+ if (agents) {
2464
+ files.push({
2465
+ path: outputs.agentsMd,
2466
+ content: agents,
2467
+ createOnly: CREATE_ONLY
2468
+ });
2469
+ }
2470
+ const rules = await opts.client.callTool("get_agent_rules").catch(() => null);
2471
+ if (typeof rules === "string" && rules.trim()) {
2472
+ files.push({
2473
+ path: outputs.cursorRules,
2474
+ content: ensureTrailingNewline(rules)
2475
+ });
2476
+ }
2477
+ const skill = await opts.client.readResource("flash://design-system/SKILL.md").catch(() => null);
2478
+ if (skill) {
2479
+ files.push({
2480
+ path: `${outputs.genomeDir.replace(/\/+$/, "")}/SKILL.md`,
2481
+ content: skill
2482
+ });
2483
+ }
2484
+ if (!files.length) {
2485
+ throw new Error(
2486
+ "The server returned no agent context to write. Check the token is still valid and the design system has been saved at least once."
2487
+ );
2488
+ }
2489
+ return writeFiles(files, {
2490
+ workspaceRoot: opts.workspaceRoot,
2491
+ confirm: opts.confirm
2492
+ });
2493
+ }
2494
+ function ensureTrailingNewline(text) {
2495
+ return text.endsWith("\n") ? text : `${text}
2496
+ `;
2497
+ }
2498
+
2499
+ // src/workspace.ts
2500
+ import { readFile as readFile6 } from "fs/promises";
2501
+ import { resolve as resolve7 } from "path";
2502
+
2503
+ // src/rpc.ts
2504
+ var TOOL_ERROR = -32e3;
2505
+ var RpcError = class extends Error {
2506
+ constructor(message, code, data) {
2507
+ super(message);
2508
+ this.code = code;
2509
+ this.data = data;
2510
+ this.name = "RpcError";
2511
+ }
2512
+ code;
2513
+ data;
2514
+ };
2515
+ function unwrapToolResult(result) {
2516
+ if (typeof result !== "object" || result === null) return result;
2517
+ const envelope = result;
2518
+ const text = envelope.content?.find((c) => c.type === "text")?.text;
2519
+ if (text === void 0) return result;
2520
+ if (envelope.isError) throw new RpcError(text, TOOL_ERROR);
2521
+ try {
2522
+ return JSON.parse(text);
2523
+ } catch {
2524
+ return text;
2525
+ }
2526
+ }
2527
+ function createRpcClient(opts) {
2528
+ const baseUrl = opts.baseUrl.replace(/\/$/, "");
2529
+ const doFetch = opts.fetchImpl ?? fetch;
2530
+ let nextId = 1;
2531
+ async function rpc(method, params = {}) {
2532
+ const res = await doFetch(`${baseUrl}/api/v1/mcp`, {
2533
+ method: "POST",
2534
+ headers: {
2535
+ "Content-Type": "application/json",
2536
+ Accept: "application/json, text/event-stream",
2537
+ Authorization: `Bearer ${opts.token}`
2538
+ },
2539
+ body: JSON.stringify({
2540
+ jsonrpc: "2.0",
2541
+ id: nextId++,
2542
+ method,
2543
+ params
2544
+ })
2545
+ });
2546
+ const text = await res.text();
2547
+ if (!res.ok) {
2548
+ const hint = res.status === 401 ? "The MCP token was rejected. Mint a new one in Flash under Connect, and set FLASH_GENOME_TOKEN." : `HTTP ${res.status}`;
2549
+ throw new RpcError(hint, res.status, text.slice(0, 500));
2550
+ }
2551
+ let body;
2552
+ try {
2553
+ body = JSON.parse(text);
2554
+ } catch {
2555
+ throw new RpcError(
2556
+ "The server did not answer with JSON-RPC",
2557
+ -32700,
2558
+ text.slice(0, 500)
2559
+ );
2560
+ }
2561
+ if (body.error) {
2562
+ throw new RpcError(body.error.message, body.error.code, body.error.data);
2563
+ }
2564
+ return body.result;
2565
+ }
2566
+ async function callTool(name, args = {}) {
2567
+ const result = await rpc("tools/call", { name, arguments: args });
2568
+ return unwrapToolResult(result);
2569
+ }
2570
+ async function readResource(uri) {
2571
+ const result = await rpc("resources/read", { uri });
2572
+ const text = result?.contents?.[0]?.text;
2573
+ if (typeof text !== "string") {
2574
+ throw new RpcError(`Resource ${uri} came back with no text`, TOOL_ERROR);
2575
+ }
2576
+ return text;
2577
+ }
2578
+ async function getGenome() {
2579
+ try {
2580
+ return await callTool("get_design_system");
2581
+ } catch (err) {
2582
+ if (!(err instanceof RpcError) || err.code !== TOOL_ERROR) throw err;
2583
+ try {
2584
+ return await callTool("get_tokens");
2585
+ } catch {
2586
+ throw err;
2587
+ }
2588
+ }
2589
+ }
2590
+ return { callTool, readResource, getGenome };
2591
+ }
2592
+
2593
+ // src/workspace.ts
2594
+ function genomeToken(explicit) {
2595
+ const token = explicit || process.env.FLASH_GENOME_TOKEN || "";
2596
+ if (!token) {
2597
+ throw new Error(
2598
+ "No MCP token. Mint one in Flash under Connect and set FLASH_GENOME_TOKEN, or start Genome with --token."
2599
+ );
2600
+ }
2601
+ return token;
2602
+ }
2603
+ async function readGenomeConfig(workspaceRoot, configPath) {
2604
+ let legacy = false;
2605
+ for (const path of configCandidates(configPath, workspaceRoot)) {
2606
+ const raw = await readFile6(path, "utf8").catch(() => null);
2607
+ if (!raw) continue;
2608
+ if (path.endsWith(LEGACY_CONFIG_FILENAME)) legacy = true;
2609
+ let parsed;
2610
+ try {
2611
+ parsed = GenomeConfigSchema.safeParse(JSON.parse(raw));
2612
+ } catch {
2613
+ throw new Error(
2614
+ `${path} is not valid JSON. Fix or delete it, then run flash-genome link.`
2615
+ );
2616
+ }
2617
+ if (parsed.success) return parsed.data;
2618
+ if (JSON.parse(raw).version === 2) {
2619
+ throw new Error(
2620
+ `${path} does not match the genome.config.json schema. Run flash-genome link to rewrite it.`
2621
+ );
2622
+ }
2623
+ }
2624
+ throw new Error(
2625
+ legacy ? `${GENOME_CONFIG_FILENAME} is missing; this repo still has the older ${LEGACY_CONFIG_FILENAME}. Run flash-genome link to write one.` : `No ${GENOME_CONFIG_FILENAME} here. Run flash-genome link in this repository first.`
2626
+ );
2627
+ }
2628
+ async function workspaceContext(opts) {
2629
+ const workspaceRoot = resolve7(process.cwd(), opts.workspace ?? ".");
2630
+ const config = await readGenomeConfig(workspaceRoot, opts.configPath);
2631
+ const client = createRpcClient({
2632
+ // Already resolved by the caller in the documented order: the flag, then
2633
+ // the environment, then this repo's config, then the hosted default.
2634
+ // Preferring the config here would make the flag and the variable
2635
+ // unusable, since the config always has a url.
2636
+ baseUrl: opts.apiUrl,
2637
+ token: genomeToken(opts.token)
2638
+ });
2639
+ return { config, client, workspaceRoot };
2640
+ }
2641
+
2642
+ // src/tools/link.ts
2643
+ import { randomBytes } from "crypto";
2644
+ import { readFile as readFile7, writeFile as writeFile2 } from "fs/promises";
2645
+ import { basename, join as join5, resolve as resolve8 } from "path";
2646
+ var DEFAULT_INPUTS = {
2647
+ tokens: ["design-tokens.json", "tokens.css"],
2648
+ components: []
2649
+ };
2650
+ function codebaseIdentity(previous, workspaceRoot) {
2651
+ if (previous) return previous;
2652
+ return {
2653
+ id: `cb_${randomBytes(12).toString("hex")}`,
2654
+ name: basename(workspaceRoot) || "codebase"
2655
+ };
2656
+ }
2657
+ async function linkGenome(opts) {
2658
+ const envelope = await opts.client.getGenome();
2659
+ if (!envelope || typeof envelope.id !== "number") {
2660
+ throw new Error(
2661
+ "The server did not say which design system this token belongs to."
2662
+ );
2663
+ }
2664
+ const root = resolve8(opts.workspaceRoot);
2665
+ const path = join5(root, GENOME_CONFIG_FILENAME);
2666
+ const before = await readFile7(path, "utf8").catch(() => null);
2667
+ let previous = null;
2668
+ let unreadable = false;
2669
+ if (before) {
2670
+ try {
2671
+ const parsed = GenomeConfigSchema.safeParse(JSON.parse(before));
2672
+ previous = parsed.success ? parsed.data : null;
2673
+ unreadable = !parsed.success;
2674
+ } catch {
2675
+ unreadable = true;
2676
+ }
2677
+ }
2678
+ if (unreadable) {
2679
+ throw new Error(
2680
+ `${GENOME_CONFIG_FILENAME} exists but is not a valid config. Fix or delete it, then run link again - overwriting it would lose the paths it names.`
2681
+ );
2682
+ }
2683
+ const config = GenomeConfigSchema.parse({
2684
+ version: 2,
2685
+ genomeId: envelope.id,
2686
+ name: envelope.name,
2687
+ apiUrl: opts.apiUrl,
2688
+ revision: envelope.revision,
2689
+ codebase: codebaseIdentity(previous?.codebase, root),
2690
+ // Keep whatever the repo had configured: a re-link is usually a token
2691
+ // rotation, and rewriting someone's paths is not what they asked for.
2692
+ ...previous ? { inputs: previous.inputs, outputs: previous.outputs } : { inputs: DEFAULT_INPUTS },
2693
+ ...opts.framework ? { framework: opts.framework } : {},
2694
+ ...previous?.framework && !opts.framework ? { framework: previous.framework } : {}
2695
+ });
2696
+ const body = `${JSON.stringify(config, null, 2)}
2697
+ `;
2698
+ const unchanged = before === body;
2699
+ if (!unchanged) await writeFile2(path, body, "utf8");
2700
+ return {
2701
+ path,
2702
+ config,
2703
+ written: !unchanged,
2704
+ rebound: Boolean(previous && previous.genomeId !== config.genomeId),
2705
+ ...previous ? { previousGenomeId: previous.genomeId } : {}
2706
+ };
2707
+ }
2708
+ function describeLink(result) {
2709
+ const lines = [
2710
+ result.written ? `Wrote ${GENOME_CONFIG_FILENAME}` : `${GENOME_CONFIG_FILENAME} already matched`,
2711
+ ` design system: ${result.config.name} (#${result.config.genomeId})`,
2712
+ ` revision: ${result.config.revision ?? "unknown"}`,
2713
+ ` api: ${result.config.apiUrl}`,
2714
+ ...result.config.codebase ? [
2715
+ ` codebase: ${result.config.codebase.name} (${result.config.codebase.id})`
2716
+ ] : []
2717
+ ];
2718
+ if (result.rebound) {
2719
+ lines.push(
2720
+ ` note: this repo was linked to #${result.previousGenomeId}; it now points at #${result.config.genomeId}`
2721
+ );
2722
+ }
2723
+ lines.push(
2724
+ "",
2725
+ "The token stays out of this file. Keep it in FLASH_GENOME_TOKEN."
2726
+ );
2727
+ return lines.join("\n");
2728
+ }
2729
+
2730
+ // src/tools/apply-token-patch.ts
2731
+ import { readFile as readFile8, writeFile as writeFile3 } from "fs/promises";
2732
+ function isRecord2(value) {
2733
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2734
+ }
2735
+ async function applyTokenPatch(tokenFile, patch, options = {}) {
2736
+ const confirm = options.confirm === true;
2737
+ const text = await readFile8(tokenFile, "utf8");
2738
+ const raw = JSON.parse(text);
2739
+ const { tokens, name } = normalizeTokenDocument(raw);
2740
+ const beforeCount = tokens.length;
2741
+ const byName = new Map(tokens.map((t) => [t.name, t]));
2742
+ const changes = [];
2743
+ for (const step of patch) {
2744
+ if (step.op === "remove") {
2745
+ if (byName.delete(step.token.name)) {
2746
+ changes.push({ op: "remove", name: step.token.name });
2747
+ }
2748
+ continue;
2749
+ }
2750
+ const existing = byName.get(step.token.name);
2751
+ const candidate = {
2752
+ name: step.token.name,
2753
+ value: step.token.value ?? existing?.value ?? "",
2754
+ type: step.token.type ?? existing?.type ?? "color",
2755
+ category: step.token.category ?? existing?.category ?? "colors",
2756
+ description: step.token.description ?? existing?.description
2757
+ };
2758
+ const parsed = TokenSchema.safeParse(candidate);
2759
+ if (!parsed.success) {
2760
+ throw new Error(
2761
+ `Invalid token patch for "${step.token.name}": ${parsed.error.message}`
2762
+ );
2763
+ }
2764
+ byName.set(parsed.data.name, parsed.data);
2765
+ changes.push({
2766
+ op: existing ? "update" : "insert",
2767
+ name: parsed.data.name
2768
+ });
2769
+ }
2770
+ const next = [...byName.values()];
2771
+ const result = {
2772
+ source: tokenFile,
2773
+ dryRun: !confirm,
2774
+ applied: false,
2775
+ beforeCount,
2776
+ afterCount: next.length,
2777
+ changes
2778
+ };
2779
+ if (!confirm) {
2780
+ result.tokens = next;
2781
+ return result;
2782
+ }
2783
+ let out;
2784
+ if (Array.isArray(raw)) {
2785
+ out = next;
2786
+ } else if (isRecord2(raw) && Array.isArray(raw.tokens)) {
2787
+ out = { ...raw, name: name ?? raw.name, tokens: next };
2788
+ } else {
2789
+ out = { name: name ?? "Design System", version: "0.1.0", tokens: next };
2790
+ }
2791
+ await writeFile3(tokenFile, `${JSON.stringify(out, null, 2)}
2792
+ `, "utf8");
2793
+ result.applied = true;
2794
+ result.tokens = next;
2795
+ return result;
2796
+ }
2797
+
2798
+ // src/tools/brand-dna.ts
2799
+ import { readFile as readFile9 } from "fs/promises";
2800
+ import { dirname as dirname3, resolve as resolve9 } from "path";
2801
+ function isRecord3(value) {
2802
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2803
+ }
2804
+ async function getBrandDna(options) {
2805
+ if (options.brandDnaFile) {
2806
+ const text2 = await readFile9(options.brandDnaFile, "utf8");
2807
+ const raw2 = JSON.parse(text2);
2808
+ if (!isRecord3(raw2)) {
2809
+ throw new Error("brand-dna file must be a JSON object");
2810
+ }
2811
+ return { source: options.brandDnaFile, brandDna: raw2 };
2812
+ }
2813
+ const sibling = resolve9(dirname3(options.tokenFile), "brand-dna.json");
2814
+ try {
2815
+ const text2 = await readFile9(sibling, "utf8");
2816
+ const raw2 = JSON.parse(text2);
2817
+ if (isRecord3(raw2)) {
2818
+ return { source: sibling, brandDna: raw2 };
2819
+ }
2820
+ } catch {
2821
+ }
2822
+ const text = await readFile9(options.tokenFile, "utf8");
2823
+ const raw = JSON.parse(text);
2824
+ if (!isRecord3(raw)) {
2825
+ throw new Error(
2826
+ "Brand DNA not found (no brand-dna.json and tokens is not an object)"
2827
+ );
2828
+ }
2829
+ for (const key of ["dsm-brand-dna", "brandDna", "brand_dna"]) {
2830
+ const value = raw[key];
2831
+ if (isRecord3(value)) {
2832
+ return { source: `${options.tokenFile}#${key}`, brandDna: value };
2833
+ }
2834
+ }
2835
+ throw new Error(
2836
+ "Brand DNA not found. Add fixtures/brand-dna.json or tokens['dsm-brand-dna']."
2837
+ );
2838
+ }
2839
+
2840
+ // src/tools/components.ts
2841
+ async function listComponents(componentsFile, category) {
2842
+ const loaded = await loadComponentsFromFile(componentsFile);
2843
+ const components = filterComponentsByCategory(loaded.components, category);
2844
+ return {
2845
+ source: loaded.source,
2846
+ name: loaded.name,
2847
+ category,
2848
+ count: components.length,
2849
+ components
2850
+ };
2851
+ }
2852
+ async function getComponent(componentsFile, name) {
2853
+ const loaded = await loadComponentsFromFile(componentsFile);
2854
+ const component = findComponentByName(loaded.components, name);
2855
+ if (!component) {
2856
+ const available = loaded.components.map((c) => c.name).join(", ");
2857
+ throw new Error(
2858
+ `Component "${name}" not found. Available: ${available || "(none)"}`
2859
+ );
2860
+ }
2861
+ return { source: loaded.source, component };
2862
+ }
2863
+
2864
+ // src/tools/generate-component-prompt.ts
2865
+ async function generateComponentPrompt(componentsFile, tokenFile, name) {
2866
+ const { source: componentSource, component } = await getComponent(
2867
+ componentsFile,
2868
+ name
2869
+ );
2870
+ const tokens = await getDesignTokens(tokenFile);
2871
+ const related = (component.tokensUsed ?? []).map((tokenName) => tokens.tokens.find((t) => t.name === tokenName)).filter((t) => Boolean(t));
2872
+ const tokenBlock = related.length > 0 ? related.map((t) => `- ${t.name}: ${t.value} (${t.category})`).join("\n") : tokens.tokens.slice(0, 12).map((t) => `- ${t.name}: ${t.value} (${t.category})`).join("\n");
2873
+ const variants = component.variants?.length ? component.variants.join(", ") : "default";
2874
+ const prompt = [
2875
+ `Implement the Flash DSM "${component.name}" component.`,
2876
+ component.description ? `Purpose: ${component.description}` : null,
2877
+ `Category: ${component.category ?? "uncategorized"}`,
2878
+ `Variants: ${variants}`,
2879
+ "",
2880
+ "Use these design tokens (do not hardcode raw values when a token exists):",
2881
+ tokenBlock,
2882
+ "",
2883
+ "Constraints:",
2884
+ "- Prefer design tokens over hard-coded colors/spacing",
2885
+ "- Keep accessibility (labels, focus, contrast) intact",
2886
+ "- Match existing project component patterns"
2887
+ ].filter((line) => line !== null).join("\n");
2888
+ return {
2889
+ name: component.name,
2890
+ prompt,
2891
+ componentSource,
2892
+ tokenSource: tokens.source
2893
+ };
2894
+ }
2895
+
2896
+ // src/tools/list-violations.ts
2897
+ import { readFile as readFile10 } from "fs/promises";
2898
+ var HEX_RE = /#(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})\b/g;
2899
+ var ARBITRARY_TW_RE = /\b(?:bg|text|border|from|to|via|ring)-\[#[0-9a-fA-F]{3,8}\]/g;
2900
+ var ARBITRARY_PX_RE = /\b(?:p|px|py|pt|pb|pl|pr|m|mx|my|mt|mb|ml|mr|gap|space-[xy])-\[(\d+)px\]/g;
2901
+ function normalizeHex2(hex) {
2902
+ let h = hex.slice(1).toLowerCase();
2903
+ if (h.length === 3 || h.length === 4) {
2904
+ h = h.split("").map((c) => c + c).join("").slice(0, 6);
2905
+ }
2906
+ return `#${h.slice(0, 6)}`;
2907
+ }
2908
+ function colorTokenSet(tokens) {
2909
+ const map = /* @__PURE__ */ new Map();
2910
+ for (const t of tokens) {
2911
+ if (t.type === "color" || t.category === "colors") {
2912
+ if (t.value.startsWith("#")) {
2913
+ map.set(normalizeHex2(t.value), t.name);
2914
+ }
2915
+ }
2916
+ }
2917
+ return map;
2918
+ }
2919
+ function spacingTokenSet(tokens) {
2920
+ const map = /* @__PURE__ */ new Map();
2921
+ for (const t of tokens) {
2922
+ if (t.category === "spacing" || t.type === "size") {
2923
+ map.set(t.value.toLowerCase(), t.name);
2924
+ }
2925
+ }
2926
+ return map;
2927
+ }
2928
+ function lineCol(source, index) {
2929
+ const upto = source.slice(0, index);
2930
+ const lines = upto.split("\n");
2931
+ return {
2932
+ line: lines.length,
2933
+ column: (lines[lines.length - 1]?.length ?? 0) + 1
2934
+ };
2935
+ }
2936
+ async function listViolations(tokenFile, filePath) {
2937
+ const loaded = await loadTokensFromFile(tokenFile);
2938
+ const source = await readFile10(filePath, "utf8");
2939
+ const colors = colorTokenSet(loaded.tokens);
2940
+ const spacing = spacingTokenSet(loaded.tokens);
2941
+ const violations = [];
2942
+ for (const match of source.matchAll(HEX_RE)) {
2943
+ const hex = match[0];
2944
+ const idx = match.index ?? 0;
2945
+ const { line, column } = lineCol(source, idx);
2946
+ const tokenName = colors.get(normalizeHex2(hex));
2947
+ if (tokenName) {
2948
+ violations.push({
2949
+ rule: "prefer-token",
2950
+ message: `Hardcoded color ${hex} matches token ${tokenName} - prefer the token`,
2951
+ match: hex,
2952
+ line,
2953
+ column,
2954
+ suggestion: tokenName
2955
+ });
2956
+ } else {
2957
+ violations.push({
2958
+ rule: "unknown-color",
2959
+ message: `Hardcoded color ${hex} is not in the design token palette`,
2960
+ match: hex,
2961
+ line,
2962
+ column
2963
+ });
2964
+ }
2965
+ }
2966
+ for (const match of source.matchAll(ARBITRARY_TW_RE)) {
2967
+ const cls = match[0];
2968
+ const idx = match.index ?? 0;
2969
+ const { line, column } = lineCol(source, idx);
2970
+ violations.push({
2971
+ rule: "arbitrary-tailwind-color",
2972
+ message: `Arbitrary Tailwind color ${cls} bypasses design tokens`,
2973
+ match: cls,
2974
+ line,
2975
+ column
2976
+ });
2977
+ }
2978
+ for (const match of source.matchAll(ARBITRARY_PX_RE)) {
2979
+ const cls = match[0];
2980
+ const px = `${match[1]}px`;
2981
+ const idx = match.index ?? 0;
2982
+ const { line, column } = lineCol(source, idx);
2983
+ const tokenName = spacing.get(px.toLowerCase());
2984
+ violations.push({
2985
+ rule: "arbitrary-tailwind-spacing",
2986
+ message: tokenName ? `Arbitrary spacing ${cls} matches token ${tokenName}` : `Arbitrary spacing ${cls} is not in spacing tokens`,
2987
+ match: cls,
2988
+ line,
2989
+ column,
2990
+ suggestion: tokenName
2991
+ });
2992
+ }
2993
+ return {
2994
+ file: filePath,
2995
+ tokenSource: loaded.source,
2996
+ count: violations.length,
2997
+ violations
2998
+ };
2999
+ }
3000
+
3001
+ // src/tools/scan-workspace.ts
3002
+ import { readFile as readFile11, readdir } from "fs/promises";
3003
+ import { basename as basename2, join as join6, resolve as resolve10 } from "path";
3004
+ var MODES_EXTENSION = "com.flashdsm.modes";
3005
+ function walkDtcg(node, path, visit) {
3006
+ for (const [k, v] of Object.entries(node)) {
3007
+ if (k.startsWith("$") || typeof v !== "object" || !v) continue;
3008
+ const child = v;
3009
+ if ("$value" in child || "value" in child) {
3010
+ visit([...path, k].join("-"), child);
3011
+ } else {
3012
+ walkDtcg(child, [...path, k], visit);
3013
+ }
3014
+ }
3015
+ }
3016
+ var TOKEN_FILES = ["design-tokens.json", "tokens.json"];
3017
+ var CSS_RE = /tokens?\.css$/i;
3018
+ var TW_RE = /^tailwind\.config\.(js|cjs|mjs|ts)$/i;
3019
+ var SKIP = /node_modules|dist|build|\.git/i;
3020
+ var CSS_VAR = /--([a-z0-9-_]+)\s*:\s*([^;{}]+);/gi;
3021
+ function classify(name, value) {
3022
+ const v = value.trim();
3023
+ if (/^#[0-9a-fA-F]{3,8}$/.test(v))
3024
+ return { category: "colors", type: "color" };
3025
+ if (/^-?\d*\.?\d+(px|rem|em|%)$/.test(v)) {
3026
+ return /radius/i.test(name) ? { category: "radius", type: "radius" } : { category: "spacing", type: "size" };
3027
+ }
3028
+ return null;
3029
+ }
3030
+ async function walk(root, dir, out) {
3031
+ const entries = await readdir(dir, { withFileTypes: true }).catch(() => []);
3032
+ for (const e of entries) {
3033
+ const full = join6(dir, e.name);
3034
+ if (e.isDirectory()) {
3035
+ if (!SKIP.test(e.name)) await walk(root, full, out);
3036
+ } else out.push(full.slice(root.length + 1));
3037
+ }
3038
+ }
3039
+ async function scanWorkspace(workspaceRoot) {
3040
+ const root = resolve10(workspaceRoot);
3041
+ const paths = [];
3042
+ await walk(root, root, paths);
3043
+ const tokens = {};
3044
+ const sources = /* @__PURE__ */ new Set();
3045
+ let scanned = 0;
3046
+ for (const rel of paths) {
3047
+ const base = basename2(rel);
3048
+ if (!TOKEN_FILES.includes(base) && !CSS_RE.test(base) && !TW_RE.test(base))
3049
+ continue;
3050
+ const text = await readFile11(join6(root, rel), "utf8").catch(() => null);
3051
+ if (!text) continue;
3052
+ scanned += 1;
3053
+ if (base.endsWith(".json")) {
3054
+ try {
3055
+ const raw = JSON.parse(text);
3056
+ walkDtcg(raw, [], (name, leaf) => {
3057
+ const val = leaf.$value ?? leaf.value;
3058
+ if (typeof val !== "string") return;
3059
+ const c = classify(name, val);
3060
+ if (!c) return;
3061
+ const ext = leaf.$extensions;
3062
+ const modes = ext?.[MODES_EXTENSION];
3063
+ const token = {
3064
+ name,
3065
+ value: val,
3066
+ type: c.type,
3067
+ category: c.category
3068
+ };
3069
+ if (modes && typeof modes === "object") {
3070
+ const clean = {};
3071
+ for (const [theme, v] of Object.entries(modes)) {
3072
+ if (typeof v === "string") clean[theme] = v;
3073
+ }
3074
+ if (Object.keys(clean).length) token.modes = clean;
3075
+ }
3076
+ tokens[c.category] ??= [];
3077
+ tokens[c.category].push(token);
3078
+ sources.add(rel);
3079
+ });
3080
+ } catch {
3081
+ }
3082
+ } else if (/\.css$/i.test(rel)) {
3083
+ for (const m of text.matchAll(CSS_VAR)) {
3084
+ const c = classify(m[1], m[2]);
3085
+ if (!c) continue;
3086
+ tokens[c.category] ??= [];
3087
+ tokens[c.category].push({
3088
+ name: m[1].toLowerCase(),
3089
+ value: m[2].trim(),
3090
+ type: c.type,
3091
+ category: c.category
3092
+ });
3093
+ sources.add(rel);
3094
+ }
3095
+ }
3096
+ }
3097
+ return {
3098
+ workspaceRoot: root,
3099
+ tokens,
3100
+ components: [],
3101
+ sources: [...sources],
3102
+ stats: {
3103
+ filesScanned: scanned,
3104
+ colors: tokens.colors?.length ?? 0,
3105
+ spacing: tokens.spacing?.length ?? 0,
3106
+ typography: tokens.typography?.length ?? 0,
3107
+ radius: tokens.radius?.length ?? 0,
3108
+ shadows: tokens.shadows?.length ?? 0,
3109
+ components: 0
3110
+ }
3111
+ };
3112
+ }
3113
+
3114
+ // src/tools/suggest-token-usage.ts
3115
+ var HEX_RE2 = /#(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})\b/g;
3116
+ var PX_RE = /\b(\d+(?:\.\d+)?)px\b/g;
3117
+ function normalizeHex3(hex) {
3118
+ let h = hex.slice(1).toLowerCase();
3119
+ if (h.length === 3 || h.length === 4) {
3120
+ h = h.split("").map((c) => c + c).join("").slice(0, 6);
3121
+ }
3122
+ return `#${h.slice(0, 6)}`;
3123
+ }
3124
+ function hexDistance(a, b) {
3125
+ const parse = (h) => {
3126
+ const n = normalizeHex3(h).slice(1);
3127
+ return [
3128
+ parseInt(n.slice(0, 2), 16),
3129
+ parseInt(n.slice(2, 4), 16),
3130
+ parseInt(n.slice(4, 6), 16)
3131
+ ];
3132
+ };
3133
+ try {
3134
+ const [ar, ag, ab] = parse(a);
3135
+ const [br, bg, bb] = parse(b);
3136
+ return Math.hypot(ar - br, ag - bg, ab - bb);
3137
+ } catch {
3138
+ return Number.POSITIVE_INFINITY;
3139
+ }
3140
+ }
3141
+ function nearestColor(tokens, hex) {
3142
+ const colors = tokens.filter(
3143
+ (t) => t.category === "colors" || t.type === "color"
3144
+ );
3145
+ let best = null;
3146
+ let bestDist = Number.POSITIVE_INFINITY;
3147
+ for (const t of colors) {
3148
+ if (!t.value.startsWith("#")) continue;
3149
+ const d = hexDistance(hex, t.value);
3150
+ if (d < bestDist) {
3151
+ bestDist = d;
3152
+ best = t;
3153
+ }
3154
+ }
3155
+ return best;
3156
+ }
3157
+ function nearestSpacing(tokens, px) {
3158
+ const spacing = tokens.filter(
3159
+ (t) => t.category === "spacing" || t.type === "size"
3160
+ );
3161
+ const target = parseFloat(px);
3162
+ let best = null;
3163
+ let bestDist = Number.POSITIVE_INFINITY;
3164
+ for (const t of spacing) {
3165
+ const m = t.value.match(/^(\d+(?:\.\d+)?)px$/i);
3166
+ if (!m) continue;
3167
+ const d = Math.abs(parseFloat(m[1]) - target);
3168
+ if (d < bestDist) {
3169
+ bestDist = d;
3170
+ best = t;
3171
+ }
3172
+ }
3173
+ return best;
3174
+ }
3175
+ async function suggestTokenUsage(tokenFile, context) {
3176
+ const loaded = await loadTokensFromFile(tokenFile);
3177
+ const suggestions = [];
3178
+ const hexes = context.match(HEX_RE2) ?? [];
3179
+ for (const hex of [...new Set(hexes)]) {
3180
+ const nearest = nearestColor(loaded.tokens, hex);
3181
+ const exact = nearest && normalizeHex3(nearest.value) === normalizeHex3(hex);
3182
+ suggestions.push({
3183
+ found: hex,
3184
+ kind: "color",
3185
+ suggestion: nearest ? {
3186
+ name: nearest.name,
3187
+ value: nearest.value,
3188
+ distance: exact ? 0 : hexDistance(hex, nearest.value)
3189
+ } : null,
3190
+ note: nearest ? exact ? `Exact match - use token ${nearest.name}` : `Nearest color token is ${nearest.name} (${nearest.value})` : "No color tokens available"
3191
+ });
3192
+ }
3193
+ const pxs = [...context.matchAll(PX_RE)].map((m) => `${m[1]}px`);
3194
+ for (const px of [...new Set(pxs)]) {
3195
+ const nearest = nearestSpacing(loaded.tokens, px);
3196
+ const exact = nearest?.value.toLowerCase() === px.toLowerCase();
3197
+ suggestions.push({
3198
+ found: px,
3199
+ kind: "spacing",
3200
+ suggestion: nearest ? { name: nearest.name, value: nearest.value } : null,
3201
+ note: nearest ? exact ? `Exact match - use token ${nearest.name}` : `Nearest spacing token is ${nearest.name} (${nearest.value})` : "No spacing tokens available"
3202
+ });
3203
+ }
3204
+ return {
3205
+ source: loaded.source,
3206
+ contextPreview: context.slice(0, 200),
3207
+ suggestions
3208
+ };
3209
+ }
3210
+
3211
+ // src/index.ts
3212
+ var PACKAGE_ROOT = resolve11(fileURLToPath(new URL("..", import.meta.url)));
3213
+ function packageVersion() {
3214
+ try {
3215
+ const raw = readFileSync(resolve11(PACKAGE_ROOT, "package.json"), "utf8");
3216
+ const version = JSON.parse(raw).version;
3217
+ return typeof version === "string" ? version : "0.0.0";
3218
+ } catch {
3219
+ return "0.0.0";
3220
+ }
3221
+ }
3222
+ var TOOL_NAMES = [
3223
+ "get_tokens",
3224
+ "list_components",
3225
+ "get_component",
3226
+ "get_brand_dna",
3227
+ "suggest_token",
3228
+ "apply_token_patch",
3229
+ "generate_component_prompt",
3230
+ "check_violations",
3231
+ "scan_workspace",
3232
+ "pull_from_web",
3233
+ "detect_drift",
3234
+ "push_to_web",
3235
+ "list_modules",
3236
+ "get_module",
3237
+ "get_relations",
3238
+ "get_impact",
3239
+ "sync_context",
3240
+ "api_health",
3241
+ "list_design_systems"
3242
+ ];
3243
+ function parseCliArgs(argv) {
3244
+ let tokensPath;
3245
+ let componentsPath;
3246
+ let configPath;
3247
+ let apiUrl;
3248
+ let accessToken;
3249
+ let token;
3250
+ const VALUE_FLAGS = /* @__PURE__ */ new Set([
3251
+ "--tokens",
3252
+ "-t",
3253
+ "--components",
3254
+ "--config",
3255
+ "-c",
3256
+ "--api-url",
3257
+ "--access-token",
3258
+ "--token"
3259
+ ]);
3260
+ let command;
3261
+ for (let i = 0; i < argv.length; i++) {
3262
+ const arg = argv[i];
3263
+ if (VALUE_FLAGS.has(arg)) {
3264
+ i++;
3265
+ continue;
3266
+ }
3267
+ if (arg.startsWith("-")) continue;
3268
+ command = arg;
3269
+ break;
3270
+ }
3271
+ for (let i = 0; i < argv.length; i++) {
3272
+ const arg = argv[i];
3273
+ if (arg === command) continue;
3274
+ if (arg === "--tokens" || arg === "-t") {
3275
+ tokensPath = argv[++i];
3276
+ continue;
3277
+ }
3278
+ if (arg === "--components") {
3279
+ componentsPath = argv[++i];
3280
+ continue;
3281
+ }
3282
+ if (arg === "--config" || arg === "-c") {
3283
+ configPath = argv[++i];
3284
+ continue;
3285
+ }
3286
+ if (arg === "--api-url") {
3287
+ apiUrl = argv[++i];
3288
+ continue;
3289
+ }
3290
+ if (arg === "--access-token") {
3291
+ accessToken = argv[++i];
3292
+ continue;
3293
+ }
3294
+ if (arg === "--token") {
3295
+ token = argv[++i];
3296
+ continue;
3297
+ }
3298
+ if (arg === "--help" || arg === "-h") {
3299
+ console.error(`Usage: flash-genome [command] [options]
3300
+
3301
+ Flash Genome: the local MCP for a Flash design system (stdio).
3302
+
3303
+ Commands:
3304
+ (none) Start the MCP server on stdio
3305
+ link Bind this repo to the design system the token belongs to
3306
+
3307
+ Credentials:
3308
+ FLASH_GENOME_TOKEN Flash MCP token, flash_mcp_\u2026, minted in Flash under
3309
+ Connect. Used by the link command.
3310
+ --token The same token as an argument. Visible in shell history
3311
+ and in process listings, so prefer the variable.
3312
+
3313
+ Options:
3314
+ --tokens, -t Path to design-tokens.json
3315
+ --components Path to components.json
3316
+ --config, -c Path to genome.config.json (or the older flash-ds.config.json)
3317
+ --api-url Flash base URL. Without it: FLASH_GENOME_API_URL, then
3318
+ FLASH_DS_API_URL, then this repo's genome.config.json,
3319
+ then https://flashdsm.com
3320
+ --access-token Deprecated web JWT, for api_health and list_design_systems
3321
+ --help, -h Show this help
3322
+ `);
3323
+ process.exit(0);
3324
+ }
3325
+ }
3326
+ return {
3327
+ command,
3328
+ tokensPath,
3329
+ componentsPath,
3330
+ configPath,
3331
+ apiUrl,
3332
+ accessToken,
3333
+ token
3334
+ };
3335
+ }
3336
+ function textResult(data, isError = false) {
3337
+ return {
3338
+ content: [
3339
+ {
3340
+ type: "text",
3341
+ text: typeof data === "string" ? data : JSON.stringify(data, null, 2)
3342
+ }
3343
+ ],
3344
+ ...isError ? { isError: true } : {}
3345
+ };
3346
+ }
3347
+ function errorResult(err) {
3348
+ const message = err instanceof Error ? err.message : String(err);
3349
+ return textResult(`Error: ${message}`, true);
3350
+ }
3351
+ async function createServer(opts) {
3352
+ const cli = { configPath: opts.configPath };
3353
+ const server = new McpServer({
3354
+ name: "flash-genome",
3355
+ version: packageVersion()
3356
+ });
3357
+ const api = createApiClient({
3358
+ baseUrl: opts.apiUrl,
3359
+ accessToken: opts.accessToken
3360
+ });
3361
+ const categoryEnum = z2.enum(
3362
+ TokenCategorySchema.options
3363
+ );
3364
+ const alias = (names, description, schema, handler) => {
3365
+ for (const name of names) server.tool(name, description, schema, handler);
3366
+ };
3367
+ alias(
3368
+ ["get_tokens", "get_design_tokens"],
3369
+ "Read design tokens from the local Flash token file. Optionally filter by category. Read-only.",
3370
+ {
3371
+ category: categoryEnum.optional().describe("Optional token category (e.g. colors, spacing)")
3372
+ },
3373
+ async (args) => {
3374
+ try {
3375
+ const category = parseCategoryInput(args.category);
3376
+ const result = await getDesignTokens(opts.tokenFile, { category });
3377
+ return textResult(result);
3378
+ } catch (err) {
3379
+ return errorResult(err);
3380
+ }
3381
+ }
3382
+ );
3383
+ server.tool(
3384
+ "list_components",
3385
+ "List components from the local components.json (optionally filter by category).",
3386
+ {
3387
+ category: z2.string().optional().describe("Optional component category (e.g. actions, forms)")
3388
+ },
3389
+ async (args) => {
3390
+ try {
3391
+ const result = await listComponents(opts.componentsFile, args.category);
3392
+ return textResult(result);
3393
+ } catch (err) {
3394
+ return errorResult(err);
3395
+ }
3396
+ }
3397
+ );
3398
+ server.tool(
3399
+ "get_component",
3400
+ "Get a single component spec by name from the local components file.",
3401
+ {
3402
+ name: z2.string().describe("Component name (e.g. Button)")
3403
+ },
3404
+ async (args) => {
3405
+ try {
3406
+ const result = await getComponent(opts.componentsFile, args.name);
3407
+ return textResult(result);
3408
+ } catch (err) {
3409
+ return errorResult(err);
3410
+ }
3411
+ }
3412
+ );
3413
+ server.tool(
3414
+ "get_brand_dna",
3415
+ "Read brand DNA from brand-dna.json (sibling of tokens) or tokens['dsm-brand-dna'].",
3416
+ {},
3417
+ async () => {
3418
+ try {
3419
+ const result = await getBrandDna({ tokenFile: opts.tokenFile });
3420
+ return textResult(result);
3421
+ } catch (err) {
3422
+ return errorResult(err);
3423
+ }
3424
+ }
3425
+ );
3426
+ alias(
3427
+ ["suggest_token", "suggest_token_usage"],
3428
+ "Suggest design tokens for hardcoded colors/spacing found in a context snippet (deterministic, no LLM).",
3429
+ {
3430
+ context: z2.string().describe("Code or design context containing colors/px values")
3431
+ },
3432
+ async (args) => {
3433
+ try {
3434
+ const result = await suggestTokenUsage(opts.tokenFile, args.context);
3435
+ return textResult(result);
3436
+ } catch (err) {
3437
+ return errorResult(err);
3438
+ }
3439
+ }
3440
+ );
3441
+ server.tool(
3442
+ "apply_token_patch",
3443
+ "Upsert/remove tokens in the local design-tokens.json. Dry-run by default; set confirm=true to write.",
3444
+ {
3445
+ patch: z2.array(
3446
+ z2.object({
3447
+ op: z2.enum(["upsert", "remove"]),
3448
+ token: z2.object({
3449
+ name: z2.string(),
3450
+ value: z2.string().optional(),
3451
+ type: z2.string().optional(),
3452
+ category: z2.string().optional(),
3453
+ description: z2.string().optional()
3454
+ })
3455
+ })
3456
+ ).describe("List of upsert/remove operations"),
3457
+ confirm: z2.boolean().optional().describe("Must be true to write the file (default: dry-run)")
3458
+ },
3459
+ async (args) => {
3460
+ try {
3461
+ const result = await applyTokenPatch(opts.tokenFile, args.patch, {
3462
+ confirm: args.confirm
3463
+ });
3464
+ return textResult(result);
3465
+ } catch (err) {
3466
+ return errorResult(err);
3467
+ }
3468
+ }
3469
+ );
3470
+ server.tool(
3471
+ "generate_component_prompt",
3472
+ "Generate an AI handoff prompt for implementing a named component with Flash tokens.",
3473
+ {
3474
+ name: z2.string().describe("Component name")
3475
+ },
3476
+ async (args) => {
3477
+ try {
3478
+ const result = await generateComponentPrompt(
3479
+ opts.componentsFile,
3480
+ opts.tokenFile,
3481
+ args.name
3482
+ );
3483
+ return textResult(result);
3484
+ } catch (err) {
3485
+ return errorResult(err);
3486
+ }
3487
+ }
3488
+ );
3489
+ alias(
3490
+ ["check_violations", "list_violations"],
3491
+ "Lint a source file for hardcoded colors / arbitrary Tailwind values vs design tokens.",
3492
+ {
3493
+ file: z2.string().describe("Absolute or cwd-relative path to a source file")
3494
+ },
3495
+ async (args) => {
3496
+ try {
3497
+ const filePath = resolve11(process.cwd(), args.file);
3498
+ const result = await listViolations(opts.tokenFile, filePath);
3499
+ return textResult(result);
3500
+ } catch (err) {
3501
+ return errorResult(err);
3502
+ }
3503
+ }
3504
+ );
3505
+ server.tool(
3506
+ "scan_workspace",
3507
+ "Scan the workspace for DTCG JSON, CSS variables, and Tailwind colors; POST to FastAPI to create a v2 design system. Requires --access-token.",
3508
+ {
3509
+ workspace: z2.string().optional().describe("Workspace root (default: cwd)"),
3510
+ name: z2.string().optional().describe("Name for the new design system")
3511
+ },
3512
+ async (args) => {
3513
+ try {
3514
+ if (!opts.accessToken) {
3515
+ throw new Error(
3516
+ "scan_workspace requires --access-token or FLASH_DS_ACCESS_TOKEN"
3517
+ );
3518
+ }
3519
+ const root = resolve11(process.cwd(), args.workspace ?? ".");
3520
+ const scan = await scanWorkspace(root);
3521
+ const created = await api.createFromCodeScan({
3522
+ name: args.name,
3523
+ scan
3524
+ });
3525
+ return textResult({ scan: scan.stats, ...created });
3526
+ } catch (err) {
3527
+ return errorResult(err);
3528
+ }
3529
+ }
3530
+ );
3531
+ const workspaceArgs = {
3532
+ workspace: z2.string().optional().describe("Repo root (default: cwd)"),
3533
+ confirm: z2.boolean().optional().describe("Must be true to write files. Without it, nothing is written")
3534
+ };
3535
+ const pullTool = async (args) => {
3536
+ try {
3537
+ const ctx = await workspaceContext({
3538
+ workspace: args.workspace,
3539
+ configPath: cli.configPath,
3540
+ apiUrl: opts.apiUrl,
3541
+ token: opts.token
3542
+ });
3543
+ return textResult(await pullFromWeb({ ...ctx, confirm: args.confirm }));
3544
+ } catch (err) {
3545
+ return errorResult(err);
3546
+ }
3547
+ };
3548
+ alias(
3549
+ ["pull_from_web", "sync_to_repo"],
3550
+ "Write this design system into the repository: the token files and mirrors named by genome.config.json. Returns the plan; writes only with confirm. Run sync_context as well for the files an agent reads before writing UI.",
3551
+ workspaceArgs,
3552
+ pullTool
3553
+ );
3554
+ server.tool(
3555
+ "detect_drift",
3556
+ "Has the repo moved since the last pull, has Flash, and which token values changed. Read-only; every finding is flagged.",
3557
+ { workspace: workspaceArgs.workspace },
3558
+ async (args) => {
3559
+ try {
3560
+ const ctx = await workspaceContext({
3561
+ workspace: args.workspace,
3562
+ configPath: cli.configPath,
3563
+ apiUrl: opts.apiUrl,
3564
+ token: opts.token
3565
+ });
3566
+ return textResult(await detectDrift(ctx));
3567
+ } catch (err) {
3568
+ return errorResult(err);
3569
+ }
3570
+ }
3571
+ );
3572
+ server.tool(
3573
+ "push_to_web",
3574
+ "Propose the token values this repo changed to Flash, as a change set a person reviews on the Sync page. Nothing is applied. Returns the plan; proposes only with confirm. Refused while Flash is ahead of the last pull.",
3575
+ workspaceArgs,
3576
+ async (args) => {
3577
+ try {
3578
+ const ctx = await workspaceContext({
3579
+ workspace: args.workspace,
3580
+ configPath: cli.configPath,
3581
+ apiUrl: opts.apiUrl,
3582
+ token: opts.token
3583
+ });
3584
+ return textResult(await pushToWeb({ ...ctx, confirm: args.confirm }));
3585
+ } catch (err) {
3586
+ return errorResult(err);
3587
+ }
3588
+ }
3589
+ );
3590
+ const graphClient = () => createRpcClient({ baseUrl: opts.apiUrl, token: genomeToken(opts.token) });
3591
+ const graphTool = (name) => async (args) => {
3592
+ try {
3593
+ return textResult(await runGraphTool(graphClient(), name, args));
3594
+ } catch (err) {
3595
+ return errorResult(err);
3596
+ }
3597
+ };
3598
+ server.tool(
3599
+ "list_modules",
3600
+ "Every module, with its part count, the components it is built from, and how many token bindings it carries.",
3601
+ {},
3602
+ graphTool("list_modules")
3603
+ );
3604
+ server.tool(
3605
+ "get_module",
3606
+ "One module by id or name: its parts in order, each resolved to its component, and its bindings.",
3607
+ { module: z2.string().describe("Module id or name") },
3608
+ graphTool("get_module")
3609
+ );
3610
+ server.tool(
3611
+ "get_relations",
3612
+ "With a token: what it aliases, what aliases it, and what binds to it directly. Without one: every alias edge, binding and module part.",
3613
+ { token: z2.string().optional().describe("Token id or name") },
3614
+ graphTool("get_relations")
3615
+ );
3616
+ server.tool(
3617
+ "get_impact",
3618
+ "What breaks if a token changes: every token that resolves through it, and the components and modules that end up using it. Call before changing a token.",
3619
+ { token: z2.string().describe("Token id or name") },
3620
+ graphTool("get_impact")
3621
+ );
3622
+ server.tool(
3623
+ "sync_context",
3624
+ "Refresh the files an agent reads before writing UI: AGENTS.md (once), the Cursor rule, and the Agent Skill. Returns the plan; writes only with confirm.",
3625
+ workspaceArgs,
3626
+ async (args) => {
3627
+ try {
3628
+ const ctx = await workspaceContext({
3629
+ workspace: args.workspace,
3630
+ configPath: cli.configPath,
3631
+ apiUrl: opts.apiUrl,
3632
+ token: opts.token
3633
+ });
3634
+ return textResult(await syncContext({ ...ctx, confirm: args.confirm }));
3635
+ } catch (err) {
3636
+ return errorResult(err);
3637
+ }
3638
+ }
3639
+ );
3640
+ server.tool(
3641
+ "api_health",
3642
+ "Check Flash DSM FastAPI /health endpoint.",
3643
+ {},
3644
+ async () => {
3645
+ try {
3646
+ const result = await api.health();
3647
+ return textResult(result);
3648
+ } catch (err) {
3649
+ return errorResult(err);
3650
+ }
3651
+ }
3652
+ );
3653
+ server.tool(
3654
+ "list_design_systems",
3655
+ "List design systems from FastAPI (requires --access-token / FLASH_DS_ACCESS_TOKEN).",
3656
+ {},
3657
+ async () => {
3658
+ try {
3659
+ const result = await api.listDesignSystems();
3660
+ return textResult(result);
3661
+ } catch (err) {
3662
+ return errorResult(err);
3663
+ }
3664
+ }
3665
+ );
3666
+ return server;
3667
+ }
3668
+ async function resolveApiUrl(cli, cwd) {
3669
+ if (cli.apiUrl) return cli.apiUrl;
3670
+ const fromEnv = process.env.FLASH_GENOME_API_URL || process.env.FLASH_DS_API_URL;
3671
+ if (fromEnv) return fromEnv;
3672
+ for (const path of configCandidates(cli.configPath, cwd)) {
3673
+ const raw = await readFile12(path, "utf8").catch(() => null);
3674
+ if (!raw) continue;
3675
+ try {
3676
+ const parsed = GenomeConfigSchema.safeParse(JSON.parse(raw));
3677
+ if (parsed.success) return parsed.data.apiUrl;
3678
+ } catch {
3679
+ continue;
3680
+ }
3681
+ }
3682
+ return "https://flashdsm.com";
3683
+ }
3684
+ async function runLink(cli) {
3685
+ const token = cli.token || process.env.FLASH_GENOME_TOKEN || "";
3686
+ if (!token) {
3687
+ console.error(
3688
+ "flash-genome link needs an MCP token. Mint one in Flash under Connect, then run it again with --token flash_mcp_\u2026 or FLASH_GENOME_TOKEN set."
3689
+ );
3690
+ process.exit(1);
3691
+ }
3692
+ const cwd = process.cwd();
3693
+ const apiUrl = await resolveApiUrl(cli, cwd);
3694
+ const client = createRpcClient({ baseUrl: apiUrl, token });
3695
+ try {
3696
+ const result = await linkGenome({ client, workspaceRoot: cwd, apiUrl });
3697
+ console.log(describeLink(result));
3698
+ } catch (err) {
3699
+ console.error(err instanceof Error ? err.message : String(err));
3700
+ process.exit(1);
3701
+ }
3702
+ }
3703
+ async function main() {
3704
+ const cli = parseCliArgs(process.argv.slice(2));
3705
+ if (cli.command === "link") return runLink(cli);
3706
+ if (cli.command) {
3707
+ console.error(
3708
+ `Unknown command "${cli.command}". Run flash-genome --help for the list.`
3709
+ );
3710
+ process.exit(1);
3711
+ }
3712
+ const apiUrl = await resolveApiUrl(cli, process.cwd());
3713
+ const accessToken = cli.accessToken || process.env.FLASH_DS_ACCESS_TOKEN || null;
3714
+ let tokenFile;
3715
+ try {
3716
+ tokenFile = await resolveTokenFilePath({
3717
+ tokensPath: cli.tokensPath,
3718
+ configPath: cli.configPath,
3719
+ cwd: process.cwd()
3720
+ });
3721
+ } catch (err) {
3722
+ tokenFile = resolve11(PACKAGE_ROOT, "fixtures", "design-tokens.json");
3723
+ console.error(
3724
+ `[flash-genome] resolve failed (${err instanceof Error ? err.message : err}); using ${tokenFile}`
3725
+ );
3726
+ }
3727
+ let componentsFile;
3728
+ try {
3729
+ componentsFile = await resolveComponentsFilePath({
3730
+ componentsPath: cli.componentsPath,
3731
+ configPath: cli.configPath,
3732
+ tokenFile,
3733
+ cwd: process.cwd()
3734
+ });
3735
+ } catch {
3736
+ componentsFile = resolve11(PACKAGE_ROOT, "fixtures", "components.json");
3737
+ }
3738
+ console.error(`[flash-genome] tokens: ${tokenFile}`);
3739
+ console.error(`[flash-genome] components: ${componentsFile}`);
3740
+ console.error(`[flash-genome] api: ${apiUrl}`);
3741
+ console.error(`[flash-genome] tools: ${TOOL_NAMES.join(", ")}`);
3742
+ const server = await createServer({
3743
+ tokenFile,
3744
+ componentsFile,
3745
+ apiUrl,
3746
+ accessToken,
3747
+ token: cli.token || process.env.FLASH_GENOME_TOKEN || null,
3748
+ configPath: cli.configPath
3749
+ });
3750
+ const transport = new StdioServerTransport();
3751
+ await server.connect(transport);
3752
+ }
3753
+ function isEntryPoint(argv1, moduleUrl) {
3754
+ if (argv1 === void 0) return false;
3755
+ const real = (p) => {
3756
+ try {
3757
+ return realpathSync(p);
3758
+ } catch {
3759
+ return resolve11(p);
3760
+ }
3761
+ };
3762
+ return real(fileURLToPath(moduleUrl)) === real(argv1);
3763
+ }
3764
+ var invokedDirectly = isEntryPoint(process.argv[1], import.meta.url);
3765
+ if (invokedDirectly) {
3766
+ main().catch((err) => {
3767
+ console.error("[flash-genome] fatal:", err);
3768
+ process.exit(1);
3769
+ });
3770
+ }
3771
+ export {
3772
+ TOOL_NAMES,
3773
+ createServer,
3774
+ isEntryPoint
3775
+ };