rafters 0.0.26 → 0.0.29

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.
Files changed (2) hide show
  1. package/dist/index.js +909 -119
  2. package/package.json +2 -1
package/dist/index.js CHANGED
@@ -12825,49 +12825,6 @@ async function detectShadcn(cwd) {
12825
12825
  return null;
12826
12826
  }
12827
12827
  }
12828
- function parseCssVariables(css3) {
12829
- const light = {};
12830
- const dark = {};
12831
- const rootMatch = css3.match(/:root\s*\{([^}]+)\}/);
12832
- if (rootMatch?.[1]) {
12833
- parseVariablesIntoColors(rootMatch[1], light);
12834
- }
12835
- const darkMatch = css3.match(/\.dark\s*\{([^}]+)\}/);
12836
- if (darkMatch?.[1]) {
12837
- parseVariablesIntoColors(darkMatch[1], dark);
12838
- }
12839
- return { light, dark };
12840
- }
12841
- function parseVariablesIntoColors(block, colors) {
12842
- const varMap = {
12843
- "--background": "background",
12844
- "--foreground": "foreground",
12845
- "--card": "card",
12846
- "--card-foreground": "cardForeground",
12847
- "--popover": "popover",
12848
- "--popover-foreground": "popoverForeground",
12849
- "--primary": "primary",
12850
- "--primary-foreground": "primaryForeground",
12851
- "--secondary": "secondary",
12852
- "--secondary-foreground": "secondaryForeground",
12853
- "--muted": "muted",
12854
- "--muted-foreground": "mutedForeground",
12855
- "--accent": "accent",
12856
- "--accent-foreground": "accentForeground",
12857
- "--destructive": "destructive",
12858
- "--destructive-foreground": "destructiveForeground",
12859
- "--border": "border",
12860
- "--input": "input",
12861
- "--ring": "ring"
12862
- };
12863
- for (const [cssVar, colorKey] of Object.entries(varMap)) {
12864
- const regex = new RegExp(`${cssVar}:\\s*([^;]+);`);
12865
- const match2 = block.match(regex);
12866
- if (match2?.[1]) {
12867
- colors[colorKey] = match2[1].trim();
12868
- }
12869
- }
12870
- }
12871
12828
  function frameworkToTarget(framework) {
12872
12829
  if (framework === "astro") return "astro";
12873
12830
  return "react";
@@ -12994,14 +12951,6 @@ function log(event) {
12994
12951
  }
12995
12952
  context.spinner = ora("Generating tokens...").start();
12996
12953
  break;
12997
- case "init:shadcn_detected": {
12998
- context.spinner?.info("Found existing shadcn colors");
12999
- console.log(` Backed up: ${event.backupPath}`);
13000
- const colors = event.colorsFound;
13001
- console.log(` Colors: ${colors.light} light, ${colors.dark} dark`);
13002
- context.spinner = ora("Generating tokens...").start();
13003
- break;
13004
- }
13005
12954
  case "init:generated":
13006
12955
  context.spinner?.succeed(`Generated ${event.tokenCount} tokens`);
13007
12956
  context.spinner = ora("Saving registry...").start();
@@ -13026,8 +12975,16 @@ function log(event) {
13026
12975
  context.spinner?.succeed(`Loaded ${event.tokenCount} tokens`);
13027
12976
  context.spinner = ora("Generating outputs...").start();
13028
12977
  break;
13029
- case "init:colors_imported":
13030
- console.log(` Imported ${event.count} existing colors`);
12978
+ case "init:existing_design_detected":
12979
+ context.spinner?.info("Existing design system detected");
12980
+ console.log(` Found design decisions in ${event.cssPath}`);
12981
+ console.log("");
12982
+ console.log(" Your existing CSS has design decisions that should be");
12983
+ console.log(" migrated intentionally, not automatically.");
12984
+ console.log("");
12985
+ console.log(' Ask your AI agent: "Onboard my existing design system to Rafters"');
12986
+ console.log("");
12987
+ context.spinner = ora("Finishing up...").start();
13031
12988
  break;
13032
12989
  case "init:prompting_exports":
13033
12990
  context.spinner?.succeed("Configuration ready");
@@ -53340,6 +53297,21 @@ Color.extend(interpolation);
53340
53297
  Color.extend(contrastMethods);
53341
53298
 
53342
53299
  // ../color-utils/src/conversion.ts
53300
+ function hexToOKLCH(hex3) {
53301
+ try {
53302
+ const color = new Color(hex3);
53303
+ const oklch2 = color.to("oklch");
53304
+ return {
53305
+ l: oklch2.coords[0] ?? 0,
53306
+ c: oklch2.coords[1] ?? 0,
53307
+ h: oklch2.coords[2] ?? 0,
53308
+ // Handle undefined hue for achromatic colors
53309
+ alpha: oklch2.alpha ?? 1
53310
+ };
53311
+ } catch (_error) {
53312
+ throw new Error(`Invalid hex color: ${hex3}`);
53313
+ }
53314
+ }
53343
53315
  function roundOKLCH(oklch2) {
53344
53316
  return {
53345
53317
  l: Math.round(oklch2.l * 1e3) / 1e3,
@@ -58684,26 +58656,6 @@ async function init(options) {
58684
58656
  await regenerateFromExisting(cwd, paths, shadcn, isAgentMode2, framework);
58685
58657
  return;
58686
58658
  }
58687
- let existingColors = null;
58688
- if (shadcn?.tailwind?.css) {
58689
- const cssPath = join9(cwd, shadcn.tailwind.css);
58690
- try {
58691
- const cssContent = await readFile5(cssPath, "utf-8");
58692
- existingColors = parseCssVariables(cssContent);
58693
- const backupPath = await backupCss(cssPath);
58694
- log({
58695
- event: "init:shadcn_detected",
58696
- cssPath: shadcn.tailwind.css,
58697
- backupPath,
58698
- colorsFound: {
58699
- light: Object.keys(existingColors.light).length,
58700
- dark: Object.keys(existingColors.dark).length
58701
- }
58702
- });
58703
- } catch (err) {
58704
- log({ event: "init:shadcn_css_error", error: String(err) });
58705
- }
58706
- }
58707
58659
  let exports;
58708
58660
  if (isAgentMode2) {
58709
58661
  exports = DEFAULT_EXPORTS;
@@ -58723,39 +58675,6 @@ async function init(options) {
58723
58675
  }
58724
58676
  });
58725
58677
  const { registry: registry2 } = result;
58726
- if (existingColors) {
58727
- const tokenMap = {
58728
- background: "background",
58729
- foreground: "foreground",
58730
- card: "card",
58731
- "card-foreground": "cardForeground",
58732
- popover: "popover",
58733
- "popover-foreground": "popoverForeground",
58734
- primary: "primary",
58735
- "primary-foreground": "primaryForeground",
58736
- secondary: "secondary",
58737
- "secondary-foreground": "secondaryForeground",
58738
- muted: "muted",
58739
- "muted-foreground": "mutedForeground",
58740
- accent: "accent",
58741
- "accent-foreground": "accentForeground",
58742
- destructive: "destructive",
58743
- "destructive-foreground": "destructiveForeground",
58744
- border: "border",
58745
- input: "input",
58746
- ring: "ring"
58747
- };
58748
- for (const [tokenName, colorKey] of Object.entries(tokenMap)) {
58749
- const colorValue = existingColors.light[colorKey];
58750
- if (colorValue && registry2.has(tokenName)) {
58751
- registry2.updateToken(tokenName, colorValue);
58752
- }
58753
- }
58754
- log({
58755
- event: "init:colors_imported",
58756
- count: Object.keys(existingColors.light).length
58757
- });
58758
- }
58759
58678
  log({
58760
58679
  event: "init:generated",
58761
58680
  tokenCount: registry2.size()
@@ -58823,12 +58742,34 @@ async function init(options) {
58823
58742
  }
58824
58743
  };
58825
58744
  await writeFile3(paths.config, JSON.stringify(config3, null, 2));
58745
+ const existingCssPath = detectedCssPath ?? (shadcn?.tailwind?.css || null);
58746
+ if (existingCssPath) {
58747
+ try {
58748
+ const cssContent = await readFile5(join9(cwd, existingCssPath), "utf-8");
58749
+ if (hasExistingDesignDecisions(cssContent)) {
58750
+ log({
58751
+ event: "init:existing_design_detected",
58752
+ cssPath: existingCssPath,
58753
+ action: "run rafters_onboard analyze to begin migration"
58754
+ });
58755
+ }
58756
+ } catch {
58757
+ }
58758
+ }
58826
58759
  log({
58827
58760
  event: "init:complete",
58828
58761
  outputs: [...outputs, "config.rafters.json"],
58829
58762
  path: paths.output
58830
58763
  });
58831
58764
  }
58765
+ function hasExistingDesignDecisions(css3) {
58766
+ const stripped = css3.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/.*/g, "");
58767
+ const withoutBoilerplate = stripped.replace(/@import\s+['"][^'"]+['"];?/g, "").replace(/@tailwind\s+\w+;?/g, "").trim();
58768
+ if (withoutBoilerplate.length === 0) return false;
58769
+ const hasCustomProperties = /--[\w-]+\s*:/.test(stripped);
58770
+ const hasThemeBlock = /@theme\s*\{/.test(stripped);
58771
+ return hasCustomProperties || hasThemeBlock;
58772
+ }
58832
58773
 
58833
58774
  // src/commands/mcp.ts
58834
58775
  import { existsSync as existsSync6 } from "fs";
@@ -58841,7 +58782,7 @@ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprot
58841
58782
 
58842
58783
  // src/mcp/tools.ts
58843
58784
  import { existsSync as existsSync4 } from "fs";
58844
- import { readdir as readdir3, readFile as readFile6 } from "fs/promises";
58785
+ import { mkdir as mkdir4, readdir as readdir3, readFile as readFile6, writeFile as writeFile4 } from "fs/promises";
58845
58786
  import { basename, join as join10, relative as relative2 } from "path";
58846
58787
 
58847
58788
  // src/mcp/cognitive-load.ts
@@ -59752,6 +59693,19 @@ If no component fits your need, check @/lib/utils for official behavioral utilit
59752
59693
  COLORS ARE TAILWIND CLASSES.
59753
59694
  Write border-l-primary, bg-success, text-info-foreground. Do not create color constants, mapping objects, or reference palette names. Palette families are internal. See quickstart.colorTokens for the full list of semantic tokens and usage examples.
59754
59695
 
59696
+ ONBOARDING IS INTENTIONAL.
59697
+ When onboarding an existing project, do NOT skip the learning step.
59698
+ 1. Call rafters_vocabulary and explore token shapes first. Understand namespaces, dependency graph, the two primitives.
59699
+ 2. Call rafters_onboard analyze to see what existing design decisions are in the CSS.
59700
+ 3. Do NOT automatically map colors. Ask the designer about each ambiguous decision.
59701
+ 4. CHALLENGE SEMANTIC ASSIGNMENTS. Do not accept "this is primary" without asking why. "What makes this the primary identity color? Where is it used? What role does it play?" The designer must justify every family assignment.
59702
+ 5. Every token mutation needs a reason that captures design INTENT, not just origin.
59703
+ Bad: "imported from globals.css"
59704
+ Good: "Brand blue from --brand-primary, primary identity color used on nav and CTAs per designer"
59705
+ 6. The 11 semantic families (primary, secondary, tertiary, accent, neutral, success, warning, destructive, info, highlight, muted) MUST all be mapped. Extra colors are custom families in the color namespace.
59706
+ 7. If analyze detects color scale patterns (e.g., --color-blaze-50 through --color-blaze-950), map the family using its base color. Do NOT create 11 individual tokens.
59707
+ 8. Call rafters_onboard map to execute the migration once the designer confirms. Colors are automatically enriched with full OKLCH scales, harmonies, accessibility data, and API intelligence.
59708
+
59755
59709
  When in doubt: less code, not more. Rafters has already made the design decision.`;
59756
59710
  var CONSUMER_QUICKSTART = {
59757
59711
  rule1: "Components are pre-styled. Import and render. Do not add className for visual styling -- only for layout context (e.g. border-l-4 for accent). Input knows its focus ring. Label knows its weight. You arrange, you do not style.",
@@ -60116,13 +60070,34 @@ var TOOL_DEFINITIONS = [
60116
60070
  },
60117
60071
  {
60118
60072
  name: "rafters_token",
60119
- description: "Get full intelligence for a design token: current value, derivation rule, dependencies, dependents (cascade impact), and human override context. Shows what the system computes vs what a designer chose and why. Use when you need to understand token relationships or respect designer decisions.",
60073
+ description: "Read or write a design token. Read: returns full intelligence (value, derivation, dependencies, cascade impact, override context). Write: set/create/reset tokens with mandatory reason (why-gate). Every write cascades through the dependency graph, persists to disk, and regenerates CSS/TS/DTCG outputs.",
60120
60074
  inputSchema: {
60121
60075
  type: "object",
60122
60076
  properties: {
60123
60077
  name: {
60124
60078
  type: "string",
60125
60079
  description: 'Token name (e.g., "spacing-6", "primary-500", "spacing-base"). Use rafters_vocabulary to see available tokens.'
60080
+ },
60081
+ action: {
60082
+ type: "string",
60083
+ enum: ["get", "set", "create", "reset"],
60084
+ description: "Action to perform. get (default): read token intelligence. set: update existing token value. create: add a new token. reset: clear override, restore computed value."
60085
+ },
60086
+ value: {
60087
+ type: "string",
60088
+ description: "New token value (required for set and create actions)."
60089
+ },
60090
+ reason: {
60091
+ type: "string",
60092
+ description: "Why this change is being made (required for set, create, and reset). Every mutation needs a reason that captures design INTENT."
60093
+ },
60094
+ namespace: {
60095
+ type: "string",
60096
+ description: 'Token namespace (required for create). E.g., "color", "spacing", "typography".'
60097
+ },
60098
+ category: {
60099
+ type: "string",
60100
+ description: 'Token category (required for create). E.g., "color", "spacing", "font".'
60126
60101
  }
60127
60102
  },
60128
60103
  required: ["name"]
@@ -60147,11 +60122,64 @@ var TOOL_DEFINITIONS = [
60147
60122
  },
60148
60123
  required: ["components"]
60149
60124
  }
60125
+ },
60126
+ {
60127
+ name: "rafters_onboard",
60128
+ description: 'Analyze an existing project for design decisions and map them into Rafters tokens. Use "analyze" to surface raw findings. Use "map" to execute -- but map REQUIRES the designer to confirm every mapping first. The tool will reject unconfirmed mappings and instruct you to ask the designer. This is an intentional system, not an automatic one.',
60129
+ inputSchema: {
60130
+ type: "object",
60131
+ properties: {
60132
+ action: {
60133
+ type: "string",
60134
+ enum: ["analyze", "map", "status"],
60135
+ description: "analyze: scan CSS and return findings + family checklist. status: show which of the 11 families have designer decisions vs defaults. map: execute confirmed mappings (requires confirmed: true after designer review)."
60136
+ },
60137
+ confirmed: {
60138
+ type: "boolean",
60139
+ description: "Set to true ONLY after the designer has reviewed and approved every mapping. The reasons must come from the designer, not from you."
60140
+ },
60141
+ mappings: {
60142
+ type: "array",
60143
+ items: {
60144
+ type: "object",
60145
+ properties: {
60146
+ source: {
60147
+ type: "string",
60148
+ description: 'Original CSS variable name (e.g., "--brand-blue")'
60149
+ },
60150
+ target: {
60151
+ type: "string",
60152
+ description: 'Rafters token name to set or create (e.g., "primary")'
60153
+ },
60154
+ value: {
60155
+ type: "string",
60156
+ description: "The value to set (from the original CSS)"
60157
+ },
60158
+ reason: {
60159
+ type: "string",
60160
+ description: "The designer explains why this mapping makes sense. This must come from the human, not from you. Ask them."
60161
+ },
60162
+ namespace: {
60163
+ type: "string",
60164
+ description: "Namespace for new tokens (required if token does not exist)"
60165
+ },
60166
+ category: {
60167
+ type: "string",
60168
+ description: "Category for new tokens (required if token does not exist)"
60169
+ }
60170
+ },
60171
+ required: ["source", "target", "value", "reason"]
60172
+ },
60173
+ description: "Array of mappings to execute (required for map action)"
60174
+ }
60175
+ },
60176
+ required: ["action"]
60177
+ }
60150
60178
  }
60151
60179
  ];
60152
60180
  var NO_PROJECT_ERROR = "No .rafters/ config found. Run `pnpm dlx rafters init` in your project first. If the MCP server was launched from a different directory, pass --project-root <path>.";
60153
60181
  var PROJECT_INDEPENDENT_TOOLS = /* @__PURE__ */ new Set(["rafters_pattern"]);
60154
- var RaftersToolHandler = class {
60182
+ var RaftersToolHandler = class _RaftersToolHandler {
60155
60183
  adapter;
60156
60184
  projectRoot;
60157
60185
  cachedConfig;
@@ -60181,13 +60209,18 @@ var RaftersToolHandler = class {
60181
60209
  return this.getPattern(args.pattern);
60182
60210
  case "rafters_component":
60183
60211
  return this.getComponent(args.name);
60184
- case "rafters_token":
60185
- return this.getToken(args.name);
60212
+ case "rafters_token": {
60213
+ const action = args.action ?? "get";
60214
+ if (action === "get") return this.getToken(args.name);
60215
+ return this.writeToken(action, args);
60216
+ }
60186
60217
  case "rafters_cognitive_budget":
60187
60218
  return this.getCognitiveBudget(
60188
60219
  args.components,
60189
60220
  args.tier ?? "page"
60190
60221
  );
60222
+ case "rafters_onboard":
60223
+ return this.onboard(args);
60191
60224
  default:
60192
60225
  return {
60193
60226
  content: [{ type: "text", text: `Unknown tool: ${name2}` }],
@@ -60717,6 +60750,689 @@ var RaftersToolHandler = class {
60717
60750
  return this.handleError("getToken", error48);
60718
60751
  }
60719
60752
  }
60753
+ // ==================== Token Write Operations ====================
60754
+ /**
60755
+ * Handle set, create, and reset actions for rafters_token.
60756
+ * Loads the full registry, mutates, cascades, persists, and regenerates outputs.
60757
+ */
60758
+ async writeToken(action, args) {
60759
+ const name2 = args.name;
60760
+ const reason = args.reason;
60761
+ const value2 = args.value;
60762
+ if (!reason) {
60763
+ return {
60764
+ content: [
60765
+ {
60766
+ type: "text",
60767
+ text: JSON.stringify({
60768
+ error: "Reason is required for every token mutation.",
60769
+ hint: "Every change needs a WHY that captures design intent, not just origin.",
60770
+ bad: "imported from globals.css",
60771
+ good: "Brand blue from --brand-primary, primary identity color per designer"
60772
+ })
60773
+ }
60774
+ ],
60775
+ isError: true
60776
+ };
60777
+ }
60778
+ if (!this.adapter || !this.projectRoot) {
60779
+ return { content: [{ type: "text", text: NO_PROJECT_ERROR }], isError: true };
60780
+ }
60781
+ try {
60782
+ const allTokens = await this.adapter.load();
60783
+ const registry2 = new TokenRegistry(allTokens);
60784
+ registry2.setAdapter(this.adapter);
60785
+ switch (action) {
60786
+ case "set": {
60787
+ if (!value2) {
60788
+ return {
60789
+ content: [{ type: "text", text: '{"error": "value is required for set action"}' }],
60790
+ isError: true
60791
+ };
60792
+ }
60793
+ const existing = registry2.get(name2);
60794
+ if (!existing) {
60795
+ return {
60796
+ content: [
60797
+ {
60798
+ type: "text",
60799
+ text: JSON.stringify({
60800
+ error: `Token "${name2}" not found. Use action: "create" for new tokens.`
60801
+ })
60802
+ }
60803
+ ],
60804
+ isError: true
60805
+ };
60806
+ }
60807
+ const previousValue = existing.value;
60808
+ existing.userOverride = {
60809
+ previousValue: typeof previousValue === "string" ? previousValue : JSON.stringify(previousValue),
60810
+ reason
60811
+ };
60812
+ await registry2.set(name2, value2);
60813
+ const affected = this.getAffectedTokens(registry2, name2);
60814
+ await this.regenerateOutputs(registry2);
60815
+ return {
60816
+ content: [
60817
+ {
60818
+ type: "text",
60819
+ text: JSON.stringify({
60820
+ ok: true,
60821
+ action: "set",
60822
+ name: name2,
60823
+ reason,
60824
+ cascaded: affected
60825
+ })
60826
+ }
60827
+ ]
60828
+ };
60829
+ }
60830
+ case "create": {
60831
+ const namespace = args.namespace;
60832
+ const category = args.category;
60833
+ if (!value2) {
60834
+ return {
60835
+ content: [{ type: "text", text: '{"error": "value is required for create action"}' }],
60836
+ isError: true
60837
+ };
60838
+ }
60839
+ if (!namespace) {
60840
+ return {
60841
+ content: [
60842
+ { type: "text", text: '{"error": "namespace is required for create action"}' }
60843
+ ],
60844
+ isError: true
60845
+ };
60846
+ }
60847
+ if (!category) {
60848
+ return {
60849
+ content: [
60850
+ { type: "text", text: '{"error": "category is required for create action"}' }
60851
+ ],
60852
+ isError: true
60853
+ };
60854
+ }
60855
+ if (registry2.has(name2)) {
60856
+ return {
60857
+ content: [
60858
+ {
60859
+ type: "text",
60860
+ text: JSON.stringify({
60861
+ error: `Token "${name2}" already exists. Use action: "set" to update.`
60862
+ })
60863
+ }
60864
+ ],
60865
+ isError: true
60866
+ };
60867
+ }
60868
+ const newToken = {
60869
+ name: name2,
60870
+ namespace,
60871
+ category,
60872
+ value: value2,
60873
+ containerQueryAware: true,
60874
+ userOverride: {
60875
+ previousValue: "",
60876
+ reason
60877
+ }
60878
+ };
60879
+ registry2.add(newToken);
60880
+ await this.adapter.save(registry2.list());
60881
+ await this.regenerateOutputs(registry2);
60882
+ return {
60883
+ content: [
60884
+ {
60885
+ type: "text",
60886
+ text: JSON.stringify({
60887
+ ok: true,
60888
+ action: "create",
60889
+ name: name2,
60890
+ namespace,
60891
+ reason
60892
+ })
60893
+ }
60894
+ ]
60895
+ };
60896
+ }
60897
+ case "reset": {
60898
+ const existing = registry2.get(name2);
60899
+ if (!existing) {
60900
+ return {
60901
+ content: [
60902
+ { type: "text", text: JSON.stringify({ error: `Token "${name2}" not found.` }) }
60903
+ ],
60904
+ isError: true
60905
+ };
60906
+ }
60907
+ await registry2.set(name2, COMPUTED);
60908
+ const affected = this.getAffectedTokens(registry2, name2);
60909
+ await this.regenerateOutputs(registry2);
60910
+ return {
60911
+ content: [
60912
+ {
60913
+ type: "text",
60914
+ text: JSON.stringify({
60915
+ ok: true,
60916
+ action: "reset",
60917
+ name: name2,
60918
+ reason,
60919
+ cascaded: affected
60920
+ })
60921
+ }
60922
+ ]
60923
+ };
60924
+ }
60925
+ default:
60926
+ return {
60927
+ content: [
60928
+ {
60929
+ type: "text",
60930
+ text: JSON.stringify({
60931
+ error: `Unknown action: ${action}. Use get, set, create, or reset.`
60932
+ })
60933
+ }
60934
+ ],
60935
+ isError: true
60936
+ };
60937
+ }
60938
+ } catch (error48) {
60939
+ return this.handleError("writeToken", error48);
60940
+ }
60941
+ }
60942
+ /**
60943
+ * Get list of tokens that would be affected by changing the given token
60944
+ */
60945
+ getAffectedTokens(registry2, tokenName) {
60946
+ const affected = [];
60947
+ for (const token of registry2.list()) {
60948
+ if (token.dependsOn?.includes(tokenName)) {
60949
+ affected.push(token.name);
60950
+ }
60951
+ }
60952
+ return affected;
60953
+ }
60954
+ /**
60955
+ * Regenerate output files (CSS, TS, DTCG) from registry state
60956
+ */
60957
+ async regenerateOutputs(registry2) {
60958
+ if (!this.projectRoot) return;
60959
+ const paths = getRaftersPaths(this.projectRoot);
60960
+ const config3 = await this.loadConfig();
60961
+ const exports = config3?.exports ?? {
60962
+ tailwind: true,
60963
+ typescript: true,
60964
+ dtcg: false,
60965
+ compiled: false
60966
+ };
60967
+ const shadcn = config3?.shadcn ?? false;
60968
+ await mkdir4(paths.output, { recursive: true });
60969
+ if (exports.tailwind) {
60970
+ const css3 = registryToTailwind(registry2, { includeImport: !shadcn });
60971
+ await writeFile4(join10(paths.output, "rafters.css"), css3);
60972
+ }
60973
+ if (exports.typescript) {
60974
+ const ts = registryToTypeScript(registry2, { includeJSDoc: true });
60975
+ await writeFile4(join10(paths.output, "rafters.ts"), ts);
60976
+ }
60977
+ if (exports.dtcg) {
60978
+ const json3 = toDTCG(registry2.list());
60979
+ await writeFile4(join10(paths.output, "rafters.json"), JSON.stringify(json3, null, 2));
60980
+ }
60981
+ }
60982
+ // ==================== Tool 6: Onboard ====================
60983
+ /**
60984
+ * CSS file locations by framework (mirrors init.ts CSS_LOCATIONS)
60985
+ */
60986
+ static CSS_LOCATIONS = {
60987
+ astro: ["src/styles/global.css", "src/styles/globals.css", "src/global.css"],
60988
+ next: ["src/app/globals.css", "app/globals.css", "styles/globals.css"],
60989
+ vite: ["src/index.css", "src/main.css", "src/styles.css", "src/app.css"],
60990
+ remix: ["app/styles/global.css", "app/globals.css", "app/root.css"],
60991
+ "react-router": ["app/app.css", "app/root.css", "app/styles.css", "app/globals.css"],
60992
+ unknown: [
60993
+ "src/styles/global.css",
60994
+ "src/styles/globals.css",
60995
+ "src/index.css",
60996
+ "src/main.css",
60997
+ "src/app.css",
60998
+ "styles/globals.css",
60999
+ "app/globals.css"
61000
+ ]
61001
+ };
61002
+ /**
61003
+ * The 11 semantic color families every design system needs.
61004
+ * Defaults are not wrong. Unexamined defaults are wrong.
61005
+ */
61006
+ static SEMANTIC_FAMILIES = [
61007
+ "primary",
61008
+ "secondary",
61009
+ "tertiary",
61010
+ "accent",
61011
+ "neutral",
61012
+ "success",
61013
+ "warning",
61014
+ "destructive",
61015
+ "info",
61016
+ "highlight",
61017
+ "muted"
61018
+ ];
61019
+ /**
61020
+ * Handle rafters_onboard tool calls
61021
+ */
61022
+ async onboard(args) {
61023
+ const action = args.action;
61024
+ switch (action) {
61025
+ case "analyze":
61026
+ return this.analyzeProject();
61027
+ case "status":
61028
+ return this.getOnboardStatus();
61029
+ case "map": {
61030
+ const confirmed = args.confirmed;
61031
+ if (!confirmed) {
61032
+ return {
61033
+ content: [
61034
+ {
61035
+ type: "text",
61036
+ text: JSON.stringify({
61037
+ error: "Human confirmation required.",
61038
+ action: "STOP. Do not call this tool again until the designer has reviewed and approved every mapping.",
61039
+ instructions: [
61040
+ "Show each proposed mapping to the designer: source -> target, value, and your proposed reason.",
61041
+ 'Ask the designer: "Is this the right semantic role? Why did you choose this color for this purpose?"',
61042
+ "The designer must provide or approve the reason. Do not write reasons yourself.",
61043
+ "Once the designer has confirmed all mappings, call rafters_onboard map again with confirmed: true."
61044
+ ]
61045
+ })
61046
+ }
61047
+ ],
61048
+ isError: true
61049
+ };
61050
+ }
61051
+ return this.mapTokens(args.mappings);
61052
+ }
61053
+ default:
61054
+ return {
61055
+ content: [
61056
+ {
61057
+ type: "text",
61058
+ text: JSON.stringify({ error: `Unknown action: ${action}. Use analyze or map.` })
61059
+ }
61060
+ ],
61061
+ isError: true
61062
+ };
61063
+ }
61064
+ }
61065
+ /**
61066
+ * Scan the project for existing design decisions.
61067
+ * Returns raw findings -- the agent interprets, not the tool.
61068
+ */
61069
+ async analyzeProject() {
61070
+ if (!this.projectRoot) {
61071
+ return { content: [{ type: "text", text: NO_PROJECT_ERROR }], isError: true };
61072
+ }
61073
+ try {
61074
+ const config3 = await this.loadConfig();
61075
+ const framework = config3?.framework ?? "unknown";
61076
+ const cssFindings = [];
61077
+ const locations = /* @__PURE__ */ new Set();
61078
+ const frameworkLocations = _RaftersToolHandler.CSS_LOCATIONS[framework] ?? _RaftersToolHandler.CSS_LOCATIONS.unknown ?? [];
61079
+ for (const loc of frameworkLocations) locations.add(loc);
61080
+ if (config3?.cssPath) locations.add(config3.cssPath);
61081
+ for (const cssPath of locations) {
61082
+ const fullPath = join10(this.projectRoot, cssPath);
61083
+ if (!existsSync4(fullPath)) continue;
61084
+ try {
61085
+ const content = await readFile6(fullPath, "utf-8");
61086
+ const finding = this.parseCssFindings(cssPath, content);
61087
+ if (finding.customProperties.length > 0 || finding.themeBlocks.length > 0 || finding.imports.length > 0) {
61088
+ cssFindings.push(finding);
61089
+ }
61090
+ } catch {
61091
+ }
61092
+ }
61093
+ let shadcn = { detected: false };
61094
+ try {
61095
+ const componentsJson = await readFile6(join10(this.projectRoot, "components.json"), "utf-8");
61096
+ const parsed = JSON.parse(componentsJson);
61097
+ const cssPath = parsed.tailwind?.css;
61098
+ shadcn = cssPath ? { detected: true, cssPath } : { detected: true };
61099
+ } catch {
61100
+ }
61101
+ let designDeps = [];
61102
+ try {
61103
+ const pkgContent = await readFile6(join10(this.projectRoot, "package.json"), "utf-8");
61104
+ const pkg = JSON.parse(pkgContent);
61105
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
61106
+ const designPatterns = [
61107
+ "@radix-ui",
61108
+ "class-variance-authority",
61109
+ "tailwind-merge",
61110
+ "clsx",
61111
+ "@headlessui",
61112
+ "lucide-react",
61113
+ "@heroicons"
61114
+ ];
61115
+ designDeps = Object.keys(allDeps).filter(
61116
+ (dep) => designPatterns.some((p2) => dep.startsWith(p2))
61117
+ );
61118
+ } catch {
61119
+ }
61120
+ let existingTokenCount = 0;
61121
+ const familyStatus = {};
61122
+ if (this.adapter) {
61123
+ const tokens = await this.adapter.load();
61124
+ existingTokenCount = tokens.length;
61125
+ for (const family of _RaftersToolHandler.SEMANTIC_FAMILIES) {
61126
+ const token = tokens.find((t2) => t2.name === family);
61127
+ if (!token) {
61128
+ familyStatus[family] = { status: "unmapped" };
61129
+ } else if (token.userOverride?.reason) {
61130
+ familyStatus[family] = { status: "designer", reason: token.userOverride.reason };
61131
+ } else {
61132
+ familyStatus[family] = { status: "default" };
61133
+ }
61134
+ }
61135
+ }
61136
+ const detectedFamilies = [];
61137
+ const allProps = cssFindings.flatMap((f) => f.customProperties);
61138
+ const familyMap = /* @__PURE__ */ new Map();
61139
+ for (const prop of allProps) {
61140
+ const scaleMatch = prop.name.match(
61141
+ /^--(color-)?(.+?)-(50|100|200|300|400|500|600|700|800|900|950)$/
61142
+ );
61143
+ if (scaleMatch?.[2] && scaleMatch[3]) {
61144
+ const family = scaleMatch[2];
61145
+ if (!familyMap.has(family)) familyMap.set(family, []);
61146
+ familyMap.get(family)?.push({ position: scaleMatch[3], value: prop.value });
61147
+ }
61148
+ }
61149
+ for (const [family, positions] of familyMap) {
61150
+ if (positions.length >= 3) {
61151
+ const base = positions.find((p2) => p2.position === "500") ?? positions.find((p2) => p2.position === "600") ?? positions[0];
61152
+ if (base) {
61153
+ detectedFamilies.push({
61154
+ family,
61155
+ positions: positions.map((p2) => p2.position),
61156
+ baseValue: base.value,
61157
+ source: `--color-${family}-*`
61158
+ });
61159
+ }
61160
+ }
61161
+ }
61162
+ const designerCount = Object.values(familyStatus).filter(
61163
+ (s) => s.status === "designer"
61164
+ ).length;
61165
+ const totalFamilies = _RaftersToolHandler.SEMANTIC_FAMILIES.length;
61166
+ const result = {
61167
+ framework,
61168
+ cssFiles: cssFindings,
61169
+ colorFamilies: detectedFamilies.length > 0 ? detectedFamilies : void 0,
61170
+ familyStatus,
61171
+ familyCoverage: `${designerCount}/${totalFamilies} semantic families have designer decisions`,
61172
+ shadcn,
61173
+ designDependencies: designDeps,
61174
+ existingTokenCount,
61175
+ guidance: detectedFamilies.length > 0 ? `Found ${detectedFamilies.length} color scale pattern(s): ${detectedFamilies.map((f) => f.family).join(", ")}. Map each family using its base color (position 500/600), not individual scale positions. buildColorValue() will regenerate the full 11-step scale.` : 'Review the custom properties above. Map each to a rafters token using rafters_onboard with action: "map". For ambiguous decisions, ask the designer.'
61176
+ };
61177
+ return {
61178
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
61179
+ };
61180
+ } catch (error48) {
61181
+ return this.handleError("analyzeProject", error48);
61182
+ }
61183
+ }
61184
+ /**
61185
+ * Check onboarding completeness -- which families have designer decisions
61186
+ */
61187
+ async getOnboardStatus() {
61188
+ if (!this.adapter || !this.projectRoot) {
61189
+ return { content: [{ type: "text", text: NO_PROJECT_ERROR }], isError: true };
61190
+ }
61191
+ try {
61192
+ const tokens = await this.adapter.load();
61193
+ const mapped = [];
61194
+ const defaultsRemaining = [];
61195
+ const unmapped = [];
61196
+ for (const family of _RaftersToolHandler.SEMANTIC_FAMILIES) {
61197
+ const token = tokens.find((t2) => t2.name === family);
61198
+ if (!token) {
61199
+ unmapped.push(family);
61200
+ } else if (token.userOverride?.reason) {
61201
+ mapped.push(family);
61202
+ } else {
61203
+ defaultsRemaining.push(family);
61204
+ }
61205
+ }
61206
+ const semanticSet = new Set(_RaftersToolHandler.SEMANTIC_FAMILIES);
61207
+ const customFamilies = tokens.filter(
61208
+ (t2) => t2.namespace === "color" && !semanticSet.has(t2.name) && t2.userOverride?.reason && !t2.name.includes("-")
61209
+ ).map((t2) => t2.name);
61210
+ const complete = defaultsRemaining.length === 0 && unmapped.length === 0;
61211
+ const coverage = `${mapped.length}/${_RaftersToolHandler.SEMANTIC_FAMILIES.length}`;
61212
+ let guidance;
61213
+ if (complete) {
61214
+ guidance = "All semantic families have designer decisions. Onboarding is complete.";
61215
+ } else if (defaultsRemaining.length > 0) {
61216
+ guidance = `${defaultsRemaining.length} families still use generated defaults: ${defaultsRemaining.join(", ")}. Ask the designer: do these defaults work, or should we choose specific colors? If the defaults are intentional, confirm them with a reason.`;
61217
+ } else {
61218
+ guidance = `${unmapped.length} families are unmapped: ${unmapped.join(", ")}. These need designer decisions.`;
61219
+ }
61220
+ return {
61221
+ content: [
61222
+ {
61223
+ type: "text",
61224
+ text: JSON.stringify(
61225
+ {
61226
+ complete,
61227
+ coverage,
61228
+ families: { mapped, defaultsRemaining, unmapped, customFamilies },
61229
+ guidance
61230
+ },
61231
+ null,
61232
+ 2
61233
+ )
61234
+ }
61235
+ ]
61236
+ };
61237
+ } catch (error48) {
61238
+ return this.handleError("getOnboardStatus", error48);
61239
+ }
61240
+ }
61241
+ /**
61242
+ * Parse CSS content into structured findings
61243
+ */
61244
+ parseCssFindings(path2, content) {
61245
+ const customProperties = [];
61246
+ const themeBlocks = [];
61247
+ const imports = [];
61248
+ const importMatches = content.matchAll(/@import\s+['"]([^'"]+)['"]/g);
61249
+ for (const match2 of importMatches) {
61250
+ if (match2[1]) imports.push(match2[1]);
61251
+ }
61252
+ const themeMatches = content.matchAll(/@theme\s*\{([^}]+)\}/g);
61253
+ for (const match2 of themeMatches) {
61254
+ if (match2[0]) themeBlocks.push(match2[0].trim());
61255
+ if (match2[1]) this.extractCustomProperties(match2[1], "@theme", customProperties);
61256
+ }
61257
+ const rootMatch = content.match(/:root\s*\{([^}]+)\}/);
61258
+ if (rootMatch?.[1]) {
61259
+ this.extractCustomProperties(rootMatch[1], ":root", customProperties);
61260
+ }
61261
+ const darkMatch = content.match(/\.dark\s*\{([^}]+)\}/);
61262
+ if (darkMatch?.[1]) {
61263
+ this.extractCustomProperties(darkMatch[1], ".dark", customProperties);
61264
+ }
61265
+ const prefersMatch = content.match(
61266
+ /@media\s*\(prefers-color-scheme:\s*dark\)\s*\{[^{]*\{([^}]+)\}/
61267
+ );
61268
+ if (prefersMatch?.[1]) {
61269
+ this.extractCustomProperties(prefersMatch[1], "prefers-color-scheme: dark", customProperties);
61270
+ }
61271
+ return { path: path2, customProperties, themeBlocks, imports };
61272
+ }
61273
+ /**
61274
+ * Extract CSS custom properties from a block
61275
+ */
61276
+ extractCustomProperties(block, context2, out) {
61277
+ const propMatches = block.matchAll(/(--[\w-]+)\s*:\s*([^;]+);/g);
61278
+ for (const match2 of propMatches) {
61279
+ if (match2[1] && match2[2]) {
61280
+ out.push({ name: match2[1], value: match2[2].trim(), context: context2 });
61281
+ }
61282
+ }
61283
+ }
61284
+ /**
61285
+ * Detect if a string value is a CSS color (not a size, calc, or var reference)
61286
+ */
61287
+ static isColorValue(value2) {
61288
+ const v = value2.trim().toLowerCase();
61289
+ if (/^\d/.test(v) || v.startsWith("var(") || v.startsWith("calc(")) return false;
61290
+ if (/\d+(rem|px|em|%|vw|vh|dvh|svh|ch|ex)$/.test(v)) return false;
61291
+ return v.startsWith("#") || v.startsWith("rgb") || v.startsWith("hsl") || v.startsWith("oklch") || v.startsWith("oklab") || v.startsWith("lch") || v.startsWith("lab") || v.startsWith("color(") || v.startsWith("hwb");
61292
+ }
61293
+ /**
61294
+ * Parse any CSS color to OKLCH. hexToOKLCH uses colorjs.io internally
61295
+ * which accepts all CSS color formats, not just hex.
61296
+ */
61297
+ static parseToOKLCH(value2) {
61298
+ try {
61299
+ return hexToOKLCH(value2.trim());
61300
+ } catch {
61301
+ return null;
61302
+ }
61303
+ }
61304
+ /**
61305
+ * Fire api.rafters.studio enrichment for a color (non-blocking)
61306
+ */
61307
+ static fireEnrichment(oklch2) {
61308
+ const l = oklch2.l.toFixed(3);
61309
+ const c4 = oklch2.c.toFixed(3);
61310
+ const h = Math.round(oklch2.h);
61311
+ return fetch(`https://api.rafters.studio/color/${l}-${c4}-${h}?sync=true`).then((r) => r.ok ? r.json() : null).catch(() => null);
61312
+ }
61313
+ /**
61314
+ * Build enriched ColorValue: local math + API intelligence
61315
+ */
61316
+ static async buildEnrichedColor(oklch2, tokenName) {
61317
+ const enrichmentPromise = _RaftersToolHandler.fireEnrichment(oklch2);
61318
+ const colorValue = buildColorValue(oklch2, { token: tokenName });
61319
+ const enrichment = await enrichmentPromise;
61320
+ if (enrichment && typeof enrichment === "object" && "color" in enrichment) {
61321
+ const apiColor = enrichment.color;
61322
+ if (apiColor?.intelligence) {
61323
+ colorValue.intelligence = apiColor.intelligence;
61324
+ }
61325
+ }
61326
+ return colorValue;
61327
+ }
61328
+ /**
61329
+ * Execute a mapping plan -- enriches colors, writes tokens
61330
+ */
61331
+ async mapTokens(mappings) {
61332
+ if (!mappings || mappings.length === 0) {
61333
+ return {
61334
+ content: [
61335
+ {
61336
+ type: "text",
61337
+ text: JSON.stringify({
61338
+ error: "mappings array is required for map action",
61339
+ hint: "Run analyze first, then provide mappings with source, target, value, and reason"
61340
+ })
61341
+ }
61342
+ ],
61343
+ isError: true
61344
+ };
61345
+ }
61346
+ if (!this.adapter || !this.projectRoot) {
61347
+ return { content: [{ type: "text", text: NO_PROJECT_ERROR }], isError: true };
61348
+ }
61349
+ try {
61350
+ const allTokens = await this.adapter.load();
61351
+ const registry2 = new TokenRegistry(allTokens);
61352
+ registry2.setAdapter(this.adapter);
61353
+ const results = [];
61354
+ for (const mapping of mappings) {
61355
+ const { source, target, value: value2, reason, namespace, category } = mapping;
61356
+ if (!source || !target || !value2 || !reason) {
61357
+ results.push({
61358
+ source: source ?? "?",
61359
+ target: target ?? "?",
61360
+ action: "skipped",
61361
+ ok: false,
61362
+ error: "Missing required fields: source, target, value, reason"
61363
+ });
61364
+ continue;
61365
+ }
61366
+ let tokenValue = value2;
61367
+ let enriched = false;
61368
+ if (_RaftersToolHandler.isColorValue(value2)) {
61369
+ const oklch2 = _RaftersToolHandler.parseToOKLCH(value2);
61370
+ if (oklch2) {
61371
+ tokenValue = await _RaftersToolHandler.buildEnrichedColor(oklch2, target);
61372
+ enriched = true;
61373
+ }
61374
+ }
61375
+ const exists = registry2.has(target);
61376
+ if (exists) {
61377
+ const existing = registry2.get(target);
61378
+ if (existing) {
61379
+ const previousValue = existing.value;
61380
+ existing.userOverride = {
61381
+ previousValue: typeof previousValue === "string" ? previousValue : JSON.stringify(previousValue),
61382
+ reason: `Onboarded from ${source}: ${reason}`
61383
+ };
61384
+ await registry2.set(target, tokenValue);
61385
+ results.push({ source, target, action: "set", ok: true, enriched });
61386
+ }
61387
+ } else {
61388
+ const ns = namespace ?? (enriched ? "color" : "custom");
61389
+ const cat = category ?? ns;
61390
+ const newToken = {
61391
+ name: target,
61392
+ namespace: ns,
61393
+ category: cat,
61394
+ value: tokenValue,
61395
+ containerQueryAware: true,
61396
+ userOverride: {
61397
+ previousValue: "",
61398
+ reason: `Onboarded from ${source}: ${reason}`
61399
+ }
61400
+ };
61401
+ registry2.add(newToken);
61402
+ results.push({ source, target, action: "create", ok: true, enriched });
61403
+ }
61404
+ }
61405
+ await this.adapter.save(registry2.list());
61406
+ await this.regenerateOutputs(registry2);
61407
+ const setCount = results.filter((r) => r.action === "set" && r.ok).length;
61408
+ const createCount = results.filter((r) => r.action === "create" && r.ok).length;
61409
+ const enrichedCount = results.filter((r) => r.enriched).length;
61410
+ const failCount = results.filter((r) => !r.ok).length;
61411
+ return {
61412
+ content: [
61413
+ {
61414
+ type: "text",
61415
+ text: JSON.stringify(
61416
+ {
61417
+ ok: true,
61418
+ summary: {
61419
+ set: setCount,
61420
+ created: createCount,
61421
+ enriched: enrichedCount,
61422
+ failed: failCount
61423
+ },
61424
+ results
61425
+ },
61426
+ null,
61427
+ 2
61428
+ )
61429
+ }
61430
+ ]
61431
+ };
61432
+ } catch (error48) {
61433
+ return this.handleError("mapTokens", error48);
61434
+ }
61435
+ }
60720
61436
  // ==================== Tool 5: Cognitive Budget ====================
60721
61437
  /**
60722
61438
  * Evaluate composition-level cognitive load
@@ -60942,7 +61658,7 @@ import { existsSync as existsSync7 } from "fs";
60942
61658
  import { resolve as resolve6 } from "path";
60943
61659
 
60944
61660
  // ../studio/src/api/vite-plugin.ts
60945
- import { writeFile as writeFile4 } from "fs/promises";
61661
+ import { writeFile as writeFile5 } from "fs/promises";
60946
61662
  import { join as join13 } from "path";
60947
61663
  var TokenResponseSchema = external_exports.object({
60948
61664
  ok: external_exports.literal(true),
@@ -61156,12 +61872,6 @@ async function handleBuildColor(req, res) {
61156
61872
  }
61157
61873
  }
61158
61874
  async function handlePostToken(req, res, name2, registry2) {
61159
- const existingToken = registry2.get(name2);
61160
- if (!existingToken) {
61161
- res.statusCode = 404;
61162
- res.end(JSON.stringify({ ok: false, error: `Token "${name2}" not found` }));
61163
- return;
61164
- }
61165
61875
  let body;
61166
61876
  try {
61167
61877
  body = await readJsonBody(req);
@@ -61171,6 +61881,53 @@ async function handlePostToken(req, res, name2, registry2) {
61171
61881
  res.end(JSON.stringify({ ok: false, error: message }));
61172
61882
  return;
61173
61883
  }
61884
+ const existingToken = registry2.get(name2);
61885
+ if (!existingToken) {
61886
+ const createSchema = external_exports.object({
61887
+ namespace: external_exports.string().min(1),
61888
+ category: external_exports.string().min(1),
61889
+ value: external_exports.union([external_exports.string(), ColorValueSchema, ColorReferenceSchema]),
61890
+ userOverride: external_exports.object({
61891
+ previousValue: external_exports.union([external_exports.string(), ColorValueSchema, ColorReferenceSchema]),
61892
+ reason: external_exports.string().min(1, "Reason is required. Every token needs a why."),
61893
+ context: external_exports.string().optional()
61894
+ }),
61895
+ description: external_exports.string().optional()
61896
+ });
61897
+ const createResult = createSchema.safeParse(body);
61898
+ if (!createResult.success) {
61899
+ res.statusCode = 400;
61900
+ const issues = createResult.error.issues;
61901
+ const message = issues[0] ? `${issues[0].path.join(".") || "body"}: ${issues[0].message}` : createResult.error.message;
61902
+ res.end(JSON.stringify({ ok: false, error: message }));
61903
+ return;
61904
+ }
61905
+ const newToken = {
61906
+ name: name2,
61907
+ namespace: createResult.data.namespace,
61908
+ category: createResult.data.category,
61909
+ value: createResult.data.value,
61910
+ userOverride: createResult.data.userOverride,
61911
+ containerQueryAware: true,
61912
+ description: createResult.data.description
61913
+ };
61914
+ const tokenResult2 = TokenSchema.safeParse(newToken);
61915
+ if (!tokenResult2.success) {
61916
+ res.statusCode = 400;
61917
+ res.end(JSON.stringify({ ok: false, error: tokenResult2.error.message }));
61918
+ return;
61919
+ }
61920
+ try {
61921
+ registry2.add(tokenResult2.data);
61922
+ const response = TokenResponseSchema.parse({ ok: true, token: registry2.get(name2) });
61923
+ res.statusCode = 201;
61924
+ res.end(JSON.stringify(response));
61925
+ } catch (error48) {
61926
+ res.statusCode = 500;
61927
+ res.end(JSON.stringify({ ok: false, error: String(error48) }));
61928
+ }
61929
+ return;
61930
+ }
61174
61931
  const namespaceSchema = getNamespacePatchSchema(existingToken.namespace);
61175
61932
  const patchResult = namespaceSchema.safeParse(body);
61176
61933
  if (!patchResult.success) {
@@ -61300,7 +62057,7 @@ function studioApiPlugin() {
61300
62057
  }
61301
62058
  registry2.setChangeCallback(async () => {
61302
62059
  try {
61303
- await writeFile4(outputPath, registryToVars(registry2));
62060
+ await writeFile5(outputPath, registryToVars(registry2));
61304
62061
  server.ws.send({ type: "custom", event: "rafters:css-updated" });
61305
62062
  } catch (error48) {
61306
62063
  console.log(`[rafters] CSS regeneration failed: ${error48}`);
@@ -61317,6 +62074,19 @@ function studioApiPlugin() {
61317
62074
  }
61318
62075
  const data = parsed.data;
61319
62076
  const shouldPersist = data.persist !== false;
62077
+ const existingToken = registry2.get(data.name);
62078
+ const isColorFamily = existingToken?.namespace === "color" && typeof data.value === "object" && data.value !== null && "scale" in data.value;
62079
+ let enrichmentPromise = null;
62080
+ if (isColorFamily) {
62081
+ const colorValue = data.value;
62082
+ const base = colorValue.scale?.[5];
62083
+ if (base) {
62084
+ const l = base.l.toFixed(3);
62085
+ const c4 = base.c.toFixed(3);
62086
+ const h = Math.round(base.h);
62087
+ enrichmentPromise = fetch(`https://api.rafters.studio/color/${l}-${c4}-${h}?sync=true`).then((r) => r.ok ? r.json() : null).catch(() => null);
62088
+ }
62089
+ }
61320
62090
  try {
61321
62091
  if (shouldPersist) {
61322
62092
  await registry2.set(data.name, data.value);
@@ -61328,6 +62098,26 @@ function studioApiPlugin() {
61328
62098
  name: data.name,
61329
62099
  persisted: shouldPersist
61330
62100
  });
62101
+ if (enrichmentPromise) {
62102
+ const enrichment = await enrichmentPromise;
62103
+ if (enrichment && typeof enrichment === "object" && "color" in enrichment) {
62104
+ const apiColor = enrichment.color;
62105
+ if (apiColor?.intelligence) {
62106
+ const current = registry2.get(data.name);
62107
+ if (current && typeof current.value === "object" && current.value !== null) {
62108
+ const enrichedValue = {
62109
+ ...current.value,
62110
+ intelligence: apiColor.intelligence
62111
+ };
62112
+ await registry2.set(data.name, enrichedValue);
62113
+ client.send("rafters:color-enriched", {
62114
+ name: data.name,
62115
+ intelligence: apiColor.intelligence
62116
+ });
62117
+ }
62118
+ }
62119
+ }
62120
+ }
61331
62121
  } catch (error48) {
61332
62122
  console.log(`[rafters] Token update failed for "${data.name}": ${error48}`);
61333
62123
  client.send("rafters:token-updated", { ok: false, error: String(error48) });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rafters",
3
- "version": "0.0.26",
3
+ "version": "0.0.29",
4
4
  "description": "CLI for Rafters design system - scaffold tokens and add components",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -40,6 +40,7 @@
40
40
  },
41
41
  "devDependencies": {
42
42
  "@playwright/test": "catalog:",
43
+ "@rafters/color-utils": "workspace:*",
43
44
  "@rafters/design-tokens": "workspace:*",
44
45
  "@rafters/shared": "workspace:*",
45
46
  "@rafters/studio": "workspace:*",