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.
- package/dist/index.js +909 -119
- 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:
|
|
13030
|
-
|
|
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: "
|
|
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
|
-
|
|
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
|
|
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
|
|
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.
|
|
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:*",
|