rafters 0.0.26 → 0.0.28

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 +685 -119
  2. package/package.json +1 -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");
@@ -58684,26 +58641,6 @@ async function init(options) {
58684
58641
  await regenerateFromExisting(cwd, paths, shadcn, isAgentMode2, framework);
58685
58642
  return;
58686
58643
  }
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
58644
  let exports;
58708
58645
  if (isAgentMode2) {
58709
58646
  exports = DEFAULT_EXPORTS;
@@ -58723,39 +58660,6 @@ async function init(options) {
58723
58660
  }
58724
58661
  });
58725
58662
  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
58663
  log({
58760
58664
  event: "init:generated",
58761
58665
  tokenCount: registry2.size()
@@ -58823,12 +58727,34 @@ async function init(options) {
58823
58727
  }
58824
58728
  };
58825
58729
  await writeFile3(paths.config, JSON.stringify(config3, null, 2));
58730
+ const existingCssPath = detectedCssPath ?? (shadcn?.tailwind?.css || null);
58731
+ if (existingCssPath) {
58732
+ try {
58733
+ const cssContent = await readFile5(join9(cwd, existingCssPath), "utf-8");
58734
+ if (hasExistingDesignDecisions(cssContent)) {
58735
+ log({
58736
+ event: "init:existing_design_detected",
58737
+ cssPath: existingCssPath,
58738
+ action: "run rafters_onboard analyze to begin migration"
58739
+ });
58740
+ }
58741
+ } catch {
58742
+ }
58743
+ }
58826
58744
  log({
58827
58745
  event: "init:complete",
58828
58746
  outputs: [...outputs, "config.rafters.json"],
58829
58747
  path: paths.output
58830
58748
  });
58831
58749
  }
58750
+ function hasExistingDesignDecisions(css3) {
58751
+ const stripped = css3.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/.*/g, "");
58752
+ const withoutBoilerplate = stripped.replace(/@import\s+['"][^'"]+['"];?/g, "").replace(/@tailwind\s+\w+;?/g, "").trim();
58753
+ if (withoutBoilerplate.length === 0) return false;
58754
+ const hasCustomProperties = /--[\w-]+\s*:/.test(stripped);
58755
+ const hasThemeBlock = /@theme\s*\{/.test(stripped);
58756
+ return hasCustomProperties || hasThemeBlock;
58757
+ }
58832
58758
 
58833
58759
  // src/commands/mcp.ts
58834
58760
  import { existsSync as existsSync6 } from "fs";
@@ -58841,7 +58767,7 @@ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprot
58841
58767
 
58842
58768
  // src/mcp/tools.ts
58843
58769
  import { existsSync as existsSync4 } from "fs";
58844
- import { readdir as readdir3, readFile as readFile6 } from "fs/promises";
58770
+ import { mkdir as mkdir4, readdir as readdir3, readFile as readFile6, writeFile as writeFile4 } from "fs/promises";
58845
58771
  import { basename, join as join10, relative as relative2 } from "path";
58846
58772
 
58847
58773
  // src/mcp/cognitive-load.ts
@@ -59752,6 +59678,16 @@ If no component fits your need, check @/lib/utils for official behavioral utilit
59752
59678
  COLORS ARE TAILWIND CLASSES.
59753
59679
  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
59680
 
59681
+ ONBOARDING IS INTENTIONAL.
59682
+ When onboarding an existing project, do NOT skip the learning step.
59683
+ 1. Call rafters_vocabulary and explore token shapes first. Understand namespaces, dependency graph, the two primitives.
59684
+ 2. Call rafters_onboard analyze to see what existing design decisions are in the CSS.
59685
+ 3. Do NOT automatically map colors. Ask the designer about each ambiguous decision.
59686
+ 4. Every token mutation needs a reason that captures design INTENT, not just origin.
59687
+ Bad: "imported from globals.css"
59688
+ Good: "Brand blue from --brand-primary, primary identity color used on nav and CTAs per designer"
59689
+ 5. Call rafters_onboard map to execute the migration once the designer confirms.
59690
+
59755
59691
  When in doubt: less code, not more. Rafters has already made the design decision.`;
59756
59692
  var CONSUMER_QUICKSTART = {
59757
59693
  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 +60052,34 @@ var TOOL_DEFINITIONS = [
60116
60052
  },
60117
60053
  {
60118
60054
  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.",
60055
+ 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
60056
  inputSchema: {
60121
60057
  type: "object",
60122
60058
  properties: {
60123
60059
  name: {
60124
60060
  type: "string",
60125
60061
  description: 'Token name (e.g., "spacing-6", "primary-500", "spacing-base"). Use rafters_vocabulary to see available tokens.'
60062
+ },
60063
+ action: {
60064
+ type: "string",
60065
+ enum: ["get", "set", "create", "reset"],
60066
+ 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."
60067
+ },
60068
+ value: {
60069
+ type: "string",
60070
+ description: "New token value (required for set and create actions)."
60071
+ },
60072
+ reason: {
60073
+ type: "string",
60074
+ description: "Why this change is being made (required for set, create, and reset). Every mutation needs a reason that captures design INTENT."
60075
+ },
60076
+ namespace: {
60077
+ type: "string",
60078
+ description: 'Token namespace (required for create). E.g., "color", "spacing", "typography".'
60079
+ },
60080
+ category: {
60081
+ type: "string",
60082
+ description: 'Token category (required for create). E.g., "color", "spacing", "font".'
60126
60083
  }
60127
60084
  },
60128
60085
  required: ["name"]
@@ -60147,11 +60104,60 @@ var TOOL_DEFINITIONS = [
60147
60104
  },
60148
60105
  required: ["components"]
60149
60106
  }
60107
+ },
60108
+ {
60109
+ name: "rafters_onboard",
60110
+ description: 'Analyze an existing project for design decisions (CSS custom properties, @theme blocks, shadcn colors) and map them into Rafters tokens. Use "analyze" to surface raw findings. Use "map" to execute token writes from a mapping plan. The agent interprets the findings, asks the designer about ambiguous decisions, then maps with intent.',
60111
+ inputSchema: {
60112
+ type: "object",
60113
+ properties: {
60114
+ action: {
60115
+ type: "string",
60116
+ enum: ["analyze", "map"],
60117
+ description: "analyze: scan project CSS and return structured findings. map: execute a mapping plan (array of source->target->reason)."
60118
+ },
60119
+ mappings: {
60120
+ type: "array",
60121
+ items: {
60122
+ type: "object",
60123
+ properties: {
60124
+ source: {
60125
+ type: "string",
60126
+ description: 'Original CSS variable name (e.g., "--brand-blue")'
60127
+ },
60128
+ target: {
60129
+ type: "string",
60130
+ description: 'Rafters token name to set or create (e.g., "primary")'
60131
+ },
60132
+ value: {
60133
+ type: "string",
60134
+ description: "The value to set (from the original CSS)"
60135
+ },
60136
+ reason: {
60137
+ type: "string",
60138
+ description: "Why this mapping makes sense (design intent)"
60139
+ },
60140
+ namespace: {
60141
+ type: "string",
60142
+ description: "Namespace for new tokens (required if token does not exist)"
60143
+ },
60144
+ category: {
60145
+ type: "string",
60146
+ description: "Category for new tokens (required if token does not exist)"
60147
+ }
60148
+ },
60149
+ required: ["source", "target", "value", "reason"]
60150
+ },
60151
+ description: "Array of mappings to execute (required for map action)"
60152
+ }
60153
+ },
60154
+ required: ["action"]
60155
+ }
60150
60156
  }
60151
60157
  ];
60152
60158
  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
60159
  var PROJECT_INDEPENDENT_TOOLS = /* @__PURE__ */ new Set(["rafters_pattern"]);
60154
- var RaftersToolHandler = class {
60160
+ var RaftersToolHandler = class _RaftersToolHandler {
60155
60161
  adapter;
60156
60162
  projectRoot;
60157
60163
  cachedConfig;
@@ -60181,13 +60187,18 @@ var RaftersToolHandler = class {
60181
60187
  return this.getPattern(args.pattern);
60182
60188
  case "rafters_component":
60183
60189
  return this.getComponent(args.name);
60184
- case "rafters_token":
60185
- return this.getToken(args.name);
60190
+ case "rafters_token": {
60191
+ const action = args.action ?? "get";
60192
+ if (action === "get") return this.getToken(args.name);
60193
+ return this.writeToken(action, args);
60194
+ }
60186
60195
  case "rafters_cognitive_budget":
60187
60196
  return this.getCognitiveBudget(
60188
60197
  args.components,
60189
60198
  args.tier ?? "page"
60190
60199
  );
60200
+ case "rafters_onboard":
60201
+ return this.onboard(args);
60191
60202
  default:
60192
60203
  return {
60193
60204
  content: [{ type: "text", text: `Unknown tool: ${name2}` }],
@@ -60717,6 +60728,487 @@ var RaftersToolHandler = class {
60717
60728
  return this.handleError("getToken", error48);
60718
60729
  }
60719
60730
  }
60731
+ // ==================== Token Write Operations ====================
60732
+ /**
60733
+ * Handle set, create, and reset actions for rafters_token.
60734
+ * Loads the full registry, mutates, cascades, persists, and regenerates outputs.
60735
+ */
60736
+ async writeToken(action, args) {
60737
+ const name2 = args.name;
60738
+ const reason = args.reason;
60739
+ const value2 = args.value;
60740
+ if (!reason) {
60741
+ return {
60742
+ content: [
60743
+ {
60744
+ type: "text",
60745
+ text: JSON.stringify({
60746
+ error: "Reason is required for every token mutation.",
60747
+ hint: "Every change needs a WHY that captures design intent, not just origin.",
60748
+ bad: "imported from globals.css",
60749
+ good: "Brand blue from --brand-primary, primary identity color per designer"
60750
+ })
60751
+ }
60752
+ ],
60753
+ isError: true
60754
+ };
60755
+ }
60756
+ if (!this.adapter || !this.projectRoot) {
60757
+ return { content: [{ type: "text", text: NO_PROJECT_ERROR }], isError: true };
60758
+ }
60759
+ try {
60760
+ const allTokens = await this.adapter.load();
60761
+ const registry2 = new TokenRegistry(allTokens);
60762
+ registry2.setAdapter(this.adapter);
60763
+ switch (action) {
60764
+ case "set": {
60765
+ if (!value2) {
60766
+ return {
60767
+ content: [{ type: "text", text: '{"error": "value is required for set action"}' }],
60768
+ isError: true
60769
+ };
60770
+ }
60771
+ const existing = registry2.get(name2);
60772
+ if (!existing) {
60773
+ return {
60774
+ content: [
60775
+ {
60776
+ type: "text",
60777
+ text: JSON.stringify({
60778
+ error: `Token "${name2}" not found. Use action: "create" for new tokens.`
60779
+ })
60780
+ }
60781
+ ],
60782
+ isError: true
60783
+ };
60784
+ }
60785
+ const previousValue = existing.value;
60786
+ existing.userOverride = {
60787
+ previousValue: typeof previousValue === "string" ? previousValue : JSON.stringify(previousValue),
60788
+ reason
60789
+ };
60790
+ await registry2.set(name2, value2);
60791
+ const affected = this.getAffectedTokens(registry2, name2);
60792
+ await this.regenerateOutputs(registry2);
60793
+ return {
60794
+ content: [
60795
+ {
60796
+ type: "text",
60797
+ text: JSON.stringify({
60798
+ ok: true,
60799
+ action: "set",
60800
+ name: name2,
60801
+ reason,
60802
+ cascaded: affected
60803
+ })
60804
+ }
60805
+ ]
60806
+ };
60807
+ }
60808
+ case "create": {
60809
+ const namespace = args.namespace;
60810
+ const category = args.category;
60811
+ if (!value2) {
60812
+ return {
60813
+ content: [{ type: "text", text: '{"error": "value is required for create action"}' }],
60814
+ isError: true
60815
+ };
60816
+ }
60817
+ if (!namespace) {
60818
+ return {
60819
+ content: [
60820
+ { type: "text", text: '{"error": "namespace is required for create action"}' }
60821
+ ],
60822
+ isError: true
60823
+ };
60824
+ }
60825
+ if (!category) {
60826
+ return {
60827
+ content: [
60828
+ { type: "text", text: '{"error": "category is required for create action"}' }
60829
+ ],
60830
+ isError: true
60831
+ };
60832
+ }
60833
+ if (registry2.has(name2)) {
60834
+ return {
60835
+ content: [
60836
+ {
60837
+ type: "text",
60838
+ text: JSON.stringify({
60839
+ error: `Token "${name2}" already exists. Use action: "set" to update.`
60840
+ })
60841
+ }
60842
+ ],
60843
+ isError: true
60844
+ };
60845
+ }
60846
+ const newToken = {
60847
+ name: name2,
60848
+ namespace,
60849
+ category,
60850
+ value: value2,
60851
+ containerQueryAware: true,
60852
+ userOverride: {
60853
+ previousValue: "",
60854
+ reason
60855
+ }
60856
+ };
60857
+ registry2.add(newToken);
60858
+ await this.adapter.save(registry2.list());
60859
+ await this.regenerateOutputs(registry2);
60860
+ return {
60861
+ content: [
60862
+ {
60863
+ type: "text",
60864
+ text: JSON.stringify({
60865
+ ok: true,
60866
+ action: "create",
60867
+ name: name2,
60868
+ namespace,
60869
+ reason
60870
+ })
60871
+ }
60872
+ ]
60873
+ };
60874
+ }
60875
+ case "reset": {
60876
+ const existing = registry2.get(name2);
60877
+ if (!existing) {
60878
+ return {
60879
+ content: [
60880
+ { type: "text", text: JSON.stringify({ error: `Token "${name2}" not found.` }) }
60881
+ ],
60882
+ isError: true
60883
+ };
60884
+ }
60885
+ await registry2.set(name2, COMPUTED);
60886
+ const affected = this.getAffectedTokens(registry2, name2);
60887
+ await this.regenerateOutputs(registry2);
60888
+ return {
60889
+ content: [
60890
+ {
60891
+ type: "text",
60892
+ text: JSON.stringify({
60893
+ ok: true,
60894
+ action: "reset",
60895
+ name: name2,
60896
+ reason,
60897
+ cascaded: affected
60898
+ })
60899
+ }
60900
+ ]
60901
+ };
60902
+ }
60903
+ default:
60904
+ return {
60905
+ content: [
60906
+ {
60907
+ type: "text",
60908
+ text: JSON.stringify({
60909
+ error: `Unknown action: ${action}. Use get, set, create, or reset.`
60910
+ })
60911
+ }
60912
+ ],
60913
+ isError: true
60914
+ };
60915
+ }
60916
+ } catch (error48) {
60917
+ return this.handleError("writeToken", error48);
60918
+ }
60919
+ }
60920
+ /**
60921
+ * Get list of tokens that would be affected by changing the given token
60922
+ */
60923
+ getAffectedTokens(registry2, tokenName) {
60924
+ const affected = [];
60925
+ for (const token of registry2.list()) {
60926
+ if (token.dependsOn?.includes(tokenName)) {
60927
+ affected.push(token.name);
60928
+ }
60929
+ }
60930
+ return affected;
60931
+ }
60932
+ /**
60933
+ * Regenerate output files (CSS, TS, DTCG) from registry state
60934
+ */
60935
+ async regenerateOutputs(registry2) {
60936
+ if (!this.projectRoot) return;
60937
+ const paths = getRaftersPaths(this.projectRoot);
60938
+ const config3 = await this.loadConfig();
60939
+ const exports = config3?.exports ?? {
60940
+ tailwind: true,
60941
+ typescript: true,
60942
+ dtcg: false,
60943
+ compiled: false
60944
+ };
60945
+ const shadcn = config3?.shadcn ?? false;
60946
+ await mkdir4(paths.output, { recursive: true });
60947
+ if (exports.tailwind) {
60948
+ const css3 = registryToTailwind(registry2, { includeImport: !shadcn });
60949
+ await writeFile4(join10(paths.output, "rafters.css"), css3);
60950
+ }
60951
+ if (exports.typescript) {
60952
+ const ts = registryToTypeScript(registry2, { includeJSDoc: true });
60953
+ await writeFile4(join10(paths.output, "rafters.ts"), ts);
60954
+ }
60955
+ if (exports.dtcg) {
60956
+ const json3 = toDTCG(registry2.list());
60957
+ await writeFile4(join10(paths.output, "rafters.json"), JSON.stringify(json3, null, 2));
60958
+ }
60959
+ }
60960
+ // ==================== Tool 6: Onboard ====================
60961
+ /**
60962
+ * CSS file locations by framework (mirrors init.ts CSS_LOCATIONS)
60963
+ */
60964
+ static CSS_LOCATIONS = {
60965
+ astro: ["src/styles/global.css", "src/styles/globals.css", "src/global.css"],
60966
+ next: ["src/app/globals.css", "app/globals.css", "styles/globals.css"],
60967
+ vite: ["src/index.css", "src/main.css", "src/styles.css", "src/app.css"],
60968
+ remix: ["app/styles/global.css", "app/globals.css", "app/root.css"],
60969
+ "react-router": ["app/app.css", "app/root.css", "app/styles.css", "app/globals.css"],
60970
+ unknown: [
60971
+ "src/styles/global.css",
60972
+ "src/styles/globals.css",
60973
+ "src/index.css",
60974
+ "src/main.css",
60975
+ "src/app.css",
60976
+ "styles/globals.css",
60977
+ "app/globals.css"
60978
+ ]
60979
+ };
60980
+ /**
60981
+ * Handle rafters_onboard tool calls
60982
+ */
60983
+ async onboard(args) {
60984
+ const action = args.action;
60985
+ switch (action) {
60986
+ case "analyze":
60987
+ return this.analyzeProject();
60988
+ case "map":
60989
+ return this.mapTokens(args.mappings);
60990
+ default:
60991
+ return {
60992
+ content: [
60993
+ {
60994
+ type: "text",
60995
+ text: JSON.stringify({ error: `Unknown action: ${action}. Use analyze or map.` })
60996
+ }
60997
+ ],
60998
+ isError: true
60999
+ };
61000
+ }
61001
+ }
61002
+ /**
61003
+ * Scan the project for existing design decisions.
61004
+ * Returns raw findings -- the agent interprets, not the tool.
61005
+ */
61006
+ async analyzeProject() {
61007
+ if (!this.projectRoot) {
61008
+ return { content: [{ type: "text", text: NO_PROJECT_ERROR }], isError: true };
61009
+ }
61010
+ try {
61011
+ const config3 = await this.loadConfig();
61012
+ const framework = config3?.framework ?? "unknown";
61013
+ const cssFindings = [];
61014
+ const locations = /* @__PURE__ */ new Set();
61015
+ const frameworkLocations = _RaftersToolHandler.CSS_LOCATIONS[framework] ?? _RaftersToolHandler.CSS_LOCATIONS.unknown ?? [];
61016
+ for (const loc of frameworkLocations) locations.add(loc);
61017
+ if (config3?.cssPath) locations.add(config3.cssPath);
61018
+ for (const cssPath of locations) {
61019
+ const fullPath = join10(this.projectRoot, cssPath);
61020
+ if (!existsSync4(fullPath)) continue;
61021
+ try {
61022
+ const content = await readFile6(fullPath, "utf-8");
61023
+ const finding = this.parseCssFindings(cssPath, content);
61024
+ if (finding.customProperties.length > 0 || finding.themeBlocks.length > 0 || finding.imports.length > 0) {
61025
+ cssFindings.push(finding);
61026
+ }
61027
+ } catch {
61028
+ }
61029
+ }
61030
+ let shadcn = { detected: false };
61031
+ try {
61032
+ const componentsJson = await readFile6(join10(this.projectRoot, "components.json"), "utf-8");
61033
+ const parsed = JSON.parse(componentsJson);
61034
+ const cssPath = parsed.tailwind?.css;
61035
+ shadcn = cssPath ? { detected: true, cssPath } : { detected: true };
61036
+ } catch {
61037
+ }
61038
+ let designDeps = [];
61039
+ try {
61040
+ const pkgContent = await readFile6(join10(this.projectRoot, "package.json"), "utf-8");
61041
+ const pkg = JSON.parse(pkgContent);
61042
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
61043
+ const designPatterns = [
61044
+ "@radix-ui",
61045
+ "class-variance-authority",
61046
+ "tailwind-merge",
61047
+ "clsx",
61048
+ "@headlessui",
61049
+ "lucide-react",
61050
+ "@heroicons"
61051
+ ];
61052
+ designDeps = Object.keys(allDeps).filter(
61053
+ (dep) => designPatterns.some((p2) => dep.startsWith(p2))
61054
+ );
61055
+ } catch {
61056
+ }
61057
+ let existingTokenCount = 0;
61058
+ if (this.adapter) {
61059
+ const tokens = await this.adapter.load();
61060
+ existingTokenCount = tokens.length;
61061
+ }
61062
+ const result = {
61063
+ framework,
61064
+ cssFiles: cssFindings,
61065
+ shadcn,
61066
+ designDependencies: designDeps,
61067
+ existingTokenCount,
61068
+ guidance: 'Review the custom properties above. Map each to a rafters token using rafters_onboard with action: "map". For ambiguous decisions, ask the designer.'
61069
+ };
61070
+ return {
61071
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
61072
+ };
61073
+ } catch (error48) {
61074
+ return this.handleError("analyzeProject", error48);
61075
+ }
61076
+ }
61077
+ /**
61078
+ * Parse CSS content into structured findings
61079
+ */
61080
+ parseCssFindings(path2, content) {
61081
+ const customProperties = [];
61082
+ const themeBlocks = [];
61083
+ const imports = [];
61084
+ const importMatches = content.matchAll(/@import\s+['"]([^'"]+)['"]/g);
61085
+ for (const match2 of importMatches) {
61086
+ if (match2[1]) imports.push(match2[1]);
61087
+ }
61088
+ const themeMatches = content.matchAll(/@theme\s*\{([^}]+)\}/g);
61089
+ for (const match2 of themeMatches) {
61090
+ if (match2[0]) themeBlocks.push(match2[0].trim());
61091
+ }
61092
+ const rootMatch = content.match(/:root\s*\{([^}]+)\}/);
61093
+ if (rootMatch?.[1]) {
61094
+ this.extractCustomProperties(rootMatch[1], ":root", customProperties);
61095
+ }
61096
+ const darkMatch = content.match(/\.dark\s*\{([^}]+)\}/);
61097
+ if (darkMatch?.[1]) {
61098
+ this.extractCustomProperties(darkMatch[1], ".dark", customProperties);
61099
+ }
61100
+ const prefersMatch = content.match(
61101
+ /@media\s*\(prefers-color-scheme:\s*dark\)\s*\{[^{]*\{([^}]+)\}/
61102
+ );
61103
+ if (prefersMatch?.[1]) {
61104
+ this.extractCustomProperties(prefersMatch[1], "prefers-color-scheme: dark", customProperties);
61105
+ }
61106
+ return { path: path2, customProperties, themeBlocks, imports };
61107
+ }
61108
+ /**
61109
+ * Extract CSS custom properties from a block
61110
+ */
61111
+ extractCustomProperties(block, context2, out) {
61112
+ const propMatches = block.matchAll(/(--[\w-]+)\s*:\s*([^;]+);/g);
61113
+ for (const match2 of propMatches) {
61114
+ if (match2[1] && match2[2]) {
61115
+ out.push({ name: match2[1], value: match2[2].trim(), context: context2 });
61116
+ }
61117
+ }
61118
+ }
61119
+ /**
61120
+ * Execute a mapping plan -- calls writeToken for each mapping
61121
+ */
61122
+ async mapTokens(mappings) {
61123
+ if (!mappings || mappings.length === 0) {
61124
+ return {
61125
+ content: [
61126
+ {
61127
+ type: "text",
61128
+ text: JSON.stringify({
61129
+ error: "mappings array is required for map action",
61130
+ hint: "Run analyze first, then provide mappings with source, target, value, and reason"
61131
+ })
61132
+ }
61133
+ ],
61134
+ isError: true
61135
+ };
61136
+ }
61137
+ if (!this.adapter || !this.projectRoot) {
61138
+ return { content: [{ type: "text", text: NO_PROJECT_ERROR }], isError: true };
61139
+ }
61140
+ try {
61141
+ const allTokens = await this.adapter.load();
61142
+ const registry2 = new TokenRegistry(allTokens);
61143
+ registry2.setAdapter(this.adapter);
61144
+ const results = [];
61145
+ for (const mapping of mappings) {
61146
+ const { source, target, value: value2, reason, namespace, category } = mapping;
61147
+ if (!source || !target || !value2 || !reason) {
61148
+ results.push({
61149
+ source: source ?? "?",
61150
+ target: target ?? "?",
61151
+ action: "skipped",
61152
+ ok: false,
61153
+ error: "Missing required fields: source, target, value, reason"
61154
+ });
61155
+ continue;
61156
+ }
61157
+ const exists = registry2.has(target);
61158
+ if (exists) {
61159
+ const existing = registry2.get(target);
61160
+ if (existing) {
61161
+ const previousValue = existing.value;
61162
+ existing.userOverride = {
61163
+ previousValue: typeof previousValue === "string" ? previousValue : JSON.stringify(previousValue),
61164
+ reason: `Onboarded from ${source}: ${reason}`
61165
+ };
61166
+ await registry2.set(target, value2);
61167
+ results.push({ source, target, action: "set", ok: true });
61168
+ }
61169
+ } else {
61170
+ const ns = namespace ?? "color";
61171
+ const cat = category ?? ns;
61172
+ const newToken = {
61173
+ name: target,
61174
+ namespace: ns,
61175
+ category: cat,
61176
+ value: value2,
61177
+ containerQueryAware: true,
61178
+ userOverride: {
61179
+ previousValue: "",
61180
+ reason: `Onboarded from ${source}: ${reason}`
61181
+ }
61182
+ };
61183
+ registry2.add(newToken);
61184
+ results.push({ source, target, action: "create", ok: true });
61185
+ }
61186
+ }
61187
+ await this.adapter.save(registry2.list());
61188
+ await this.regenerateOutputs(registry2);
61189
+ const setCount = results.filter((r) => r.action === "set" && r.ok).length;
61190
+ const createCount = results.filter((r) => r.action === "create" && r.ok).length;
61191
+ const failCount = results.filter((r) => !r.ok).length;
61192
+ return {
61193
+ content: [
61194
+ {
61195
+ type: "text",
61196
+ text: JSON.stringify(
61197
+ {
61198
+ ok: true,
61199
+ summary: { set: setCount, created: createCount, failed: failCount },
61200
+ results
61201
+ },
61202
+ null,
61203
+ 2
61204
+ )
61205
+ }
61206
+ ]
61207
+ };
61208
+ } catch (error48) {
61209
+ return this.handleError("mapTokens", error48);
61210
+ }
61211
+ }
60720
61212
  // ==================== Tool 5: Cognitive Budget ====================
60721
61213
  /**
60722
61214
  * Evaluate composition-level cognitive load
@@ -60942,7 +61434,7 @@ import { existsSync as existsSync7 } from "fs";
60942
61434
  import { resolve as resolve6 } from "path";
60943
61435
 
60944
61436
  // ../studio/src/api/vite-plugin.ts
60945
- import { writeFile as writeFile4 } from "fs/promises";
61437
+ import { writeFile as writeFile5 } from "fs/promises";
60946
61438
  import { join as join13 } from "path";
60947
61439
  var TokenResponseSchema = external_exports.object({
60948
61440
  ok: external_exports.literal(true),
@@ -61156,12 +61648,6 @@ async function handleBuildColor(req, res) {
61156
61648
  }
61157
61649
  }
61158
61650
  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
61651
  let body;
61166
61652
  try {
61167
61653
  body = await readJsonBody(req);
@@ -61171,6 +61657,53 @@ async function handlePostToken(req, res, name2, registry2) {
61171
61657
  res.end(JSON.stringify({ ok: false, error: message }));
61172
61658
  return;
61173
61659
  }
61660
+ const existingToken = registry2.get(name2);
61661
+ if (!existingToken) {
61662
+ const createSchema = external_exports.object({
61663
+ namespace: external_exports.string().min(1),
61664
+ category: external_exports.string().min(1),
61665
+ value: external_exports.union([external_exports.string(), ColorValueSchema, ColorReferenceSchema]),
61666
+ userOverride: external_exports.object({
61667
+ previousValue: external_exports.union([external_exports.string(), ColorValueSchema, ColorReferenceSchema]),
61668
+ reason: external_exports.string().min(1, "Reason is required. Every token needs a why."),
61669
+ context: external_exports.string().optional()
61670
+ }),
61671
+ description: external_exports.string().optional()
61672
+ });
61673
+ const createResult = createSchema.safeParse(body);
61674
+ if (!createResult.success) {
61675
+ res.statusCode = 400;
61676
+ const issues = createResult.error.issues;
61677
+ const message = issues[0] ? `${issues[0].path.join(".") || "body"}: ${issues[0].message}` : createResult.error.message;
61678
+ res.end(JSON.stringify({ ok: false, error: message }));
61679
+ return;
61680
+ }
61681
+ const newToken = {
61682
+ name: name2,
61683
+ namespace: createResult.data.namespace,
61684
+ category: createResult.data.category,
61685
+ value: createResult.data.value,
61686
+ userOverride: createResult.data.userOverride,
61687
+ containerQueryAware: true,
61688
+ description: createResult.data.description
61689
+ };
61690
+ const tokenResult2 = TokenSchema.safeParse(newToken);
61691
+ if (!tokenResult2.success) {
61692
+ res.statusCode = 400;
61693
+ res.end(JSON.stringify({ ok: false, error: tokenResult2.error.message }));
61694
+ return;
61695
+ }
61696
+ try {
61697
+ registry2.add(tokenResult2.data);
61698
+ const response = TokenResponseSchema.parse({ ok: true, token: registry2.get(name2) });
61699
+ res.statusCode = 201;
61700
+ res.end(JSON.stringify(response));
61701
+ } catch (error48) {
61702
+ res.statusCode = 500;
61703
+ res.end(JSON.stringify({ ok: false, error: String(error48) }));
61704
+ }
61705
+ return;
61706
+ }
61174
61707
  const namespaceSchema = getNamespacePatchSchema(existingToken.namespace);
61175
61708
  const patchResult = namespaceSchema.safeParse(body);
61176
61709
  if (!patchResult.success) {
@@ -61300,7 +61833,7 @@ function studioApiPlugin() {
61300
61833
  }
61301
61834
  registry2.setChangeCallback(async () => {
61302
61835
  try {
61303
- await writeFile4(outputPath, registryToVars(registry2));
61836
+ await writeFile5(outputPath, registryToVars(registry2));
61304
61837
  server.ws.send({ type: "custom", event: "rafters:css-updated" });
61305
61838
  } catch (error48) {
61306
61839
  console.log(`[rafters] CSS regeneration failed: ${error48}`);
@@ -61317,6 +61850,19 @@ function studioApiPlugin() {
61317
61850
  }
61318
61851
  const data = parsed.data;
61319
61852
  const shouldPersist = data.persist !== false;
61853
+ const existingToken = registry2.get(data.name);
61854
+ const isColorFamily = existingToken?.namespace === "color" && typeof data.value === "object" && data.value !== null && "scale" in data.value;
61855
+ let enrichmentPromise = null;
61856
+ if (isColorFamily) {
61857
+ const colorValue = data.value;
61858
+ const base = colorValue.scale?.[5];
61859
+ if (base) {
61860
+ const l = base.l.toFixed(3);
61861
+ const c4 = base.c.toFixed(3);
61862
+ const h = Math.round(base.h);
61863
+ enrichmentPromise = fetch(`https://api.rafters.studio/color/${l}-${c4}-${h}?sync=true`).then((r) => r.ok ? r.json() : null).catch(() => null);
61864
+ }
61865
+ }
61320
61866
  try {
61321
61867
  if (shouldPersist) {
61322
61868
  await registry2.set(data.name, data.value);
@@ -61328,6 +61874,26 @@ function studioApiPlugin() {
61328
61874
  name: data.name,
61329
61875
  persisted: shouldPersist
61330
61876
  });
61877
+ if (enrichmentPromise) {
61878
+ const enrichment = await enrichmentPromise;
61879
+ if (enrichment && typeof enrichment === "object" && "color" in enrichment) {
61880
+ const apiColor = enrichment.color;
61881
+ if (apiColor?.intelligence) {
61882
+ const current = registry2.get(data.name);
61883
+ if (current && typeof current.value === "object" && current.value !== null) {
61884
+ const enrichedValue = {
61885
+ ...current.value,
61886
+ intelligence: apiColor.intelligence
61887
+ };
61888
+ await registry2.set(data.name, enrichedValue);
61889
+ client.send("rafters:color-enriched", {
61890
+ name: data.name,
61891
+ intelligence: apiColor.intelligence
61892
+ });
61893
+ }
61894
+ }
61895
+ }
61896
+ }
61331
61897
  } catch (error48) {
61332
61898
  console.log(`[rafters] Token update failed for "${data.name}": ${error48}`);
61333
61899
  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.28",
4
4
  "description": "CLI for Rafters design system - scaffold tokens and add components",
5
5
  "license": "MIT",
6
6
  "type": "module",