rafters 0.0.79 → 0.0.81

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 +1570 -1310
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -5,7 +5,13 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
6
  var __getProtoOf = Object.getPrototypeOf;
7
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
- var __commonJS = (cb, mod) => function __require() {
8
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
9
+ get: (a2, b2) => (typeof require !== "undefined" ? require : a2)[b2]
10
+ }) : x)(function(x) {
11
+ if (typeof require !== "undefined") return require.apply(this, arguments);
12
+ throw Error('Dynamic require of "' + x + '" is not supported');
13
+ });
14
+ var __commonJS = (cb, mod) => function __require2() {
9
15
  return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
10
16
  };
11
17
  var __export = (target, all) => {
@@ -80,9 +86,9 @@ var require_escape_html = __commonJS({
80
86
  import { Command } from "commander";
81
87
 
82
88
  // src/commands/add.ts
83
- import { existsSync as existsSync3 } from "fs";
84
- import { access, mkdir as mkdir2, readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
85
- import { dirname, join as join7 } from "path";
89
+ import { existsSync as existsSync4 } from "fs";
90
+ import { access, mkdir as mkdir3, readFile as readFile4, writeFile as writeFile3 } from "fs/promises";
91
+ import { dirname, join as join8 } from "path";
86
92
 
87
93
  // ../design-tokens/src/exporters/dtcg.ts
88
94
  function categoryToDTCGType(category, token) {
@@ -1253,34 +1259,59 @@ ${themeBody}`;
1253
1259
  }
1254
1260
  }
1255
1261
  function postProcessDocSheet(css) {
1262
+ const csstree3 = __require("css-tree");
1263
+ const ast = csstree3.parse(css);
1256
1264
  const parts = [];
1257
- const themeLayerRe = /@layer theme\{(.*?)\}(?=@layer|$)/s;
1258
- const themeMatch = themeLayerRe.exec(css);
1259
- if (themeMatch) {
1260
- let themeContent = themeMatch[1] ?? "";
1261
- themeContent = themeContent.replace(/:root,:host/g, ":host");
1262
- parts.push(`:host{container-type:inline-size}${themeContent}`);
1263
- }
1264
- const layerRe = /@layer (base|components|utilities)\{/g;
1265
- let match;
1266
- while ((match = layerRe.exec(css)) !== null) {
1267
- const start = match.index;
1268
- let depth = 0;
1269
- let end = start;
1270
- for (let i = start; i < css.length; i++) {
1271
- if (css[i] === "{") depth++;
1272
- if (css[i] === "}") {
1273
- depth--;
1274
- if (depth === 0) {
1275
- end = i + 1;
1276
- break;
1265
+ let hasHostContainer = false;
1266
+ const atruleNames = /* @__PURE__ */ new Set(["layer", "property", "keyframes"]);
1267
+ csstree3.walk(ast, {
1268
+ visit: "Atrule",
1269
+ enter(node) {
1270
+ if (!atruleNames.has(node.name)) return;
1271
+ if (node.name === "layer") {
1272
+ const prelude = node.prelude ? csstree3.generate(node.prelude) : "";
1273
+ if (prelude === "theme" && node.block) {
1274
+ let content = csstree3.generate(node.block);
1275
+ content = content.replace(/^\{/, "").replace(/\}$/, "");
1276
+ content = rewriteRootToHost(content);
1277
+ if (!hasHostContainer) {
1278
+ parts.push(`:host{container-type:inline-size}${content}`);
1279
+ hasHostContainer = true;
1280
+ } else {
1281
+ parts.push(content);
1282
+ }
1283
+ } else if (/^(base|components|utilities)/.test(prelude) && node.block) {
1284
+ parts.push(csstree3.generate(node));
1285
+ } else if (prelude.startsWith("properties") && node.block) {
1286
+ let block = csstree3.generate(node);
1287
+ block = rewritePropertiesSelector(block);
1288
+ parts.push(block);
1277
1289
  }
1290
+ } else {
1291
+ parts.push(csstree3.generate(node));
1278
1292
  }
1279
1293
  }
1280
- parts.push(css.slice(start, end));
1281
- }
1294
+ });
1295
+ csstree3.walk(ast, {
1296
+ visit: "Rule",
1297
+ enter(node, _item, list) {
1298
+ if (list !== ast.children) return;
1299
+ let block = csstree3.generate(node);
1300
+ block = rewriteRootToHost(block);
1301
+ parts.push(block);
1302
+ }
1303
+ });
1282
1304
  return parts.join("");
1283
1305
  }
1306
+ function rewriteRootToHost(css) {
1307
+ return css.replace(/:root\s*,\s*:host/g, ":host").replace(/:root\[data-theme/g, ":host[data-theme").replace(/:root(?=[{\s,[])/g, ":host");
1308
+ }
1309
+ function rewritePropertiesSelector(css) {
1310
+ return css.replace(
1311
+ /\*\s*,\s*:?:?before\s*,\s*:?:?after\s*,\s*:?:?backdrop/g,
1312
+ ":host,*,::before,::after"
1313
+ );
1314
+ }
1284
1315
 
1285
1316
  // ../design-tokens/src/exporters/typescript.ts
1286
1317
  function tokenValueToTS(token) {
@@ -27299,194 +27330,23 @@ function generateBaseSystem(config2 = {}) {
27299
27330
  };
27300
27331
  }
27301
27332
 
27302
- // ../design-tokens/src/graph.ts
27303
- var UserOverrideSchema = external_exports.object({
27304
- previousValue: external_exports.unknown(),
27305
- reason: external_exports.string(),
27306
- context: external_exports.string().optional(),
27307
- // Provenance. Optional by design: absent means unknown, which is what every
27308
- // override written before this field existed actually is.
27309
- kind: OverrideKindSchema.optional()
27310
- });
27311
- var BindingSchema2 = external_exports.object({
27312
- plugin: external_exports.string(),
27313
- input: external_exports.unknown()
27314
- });
27315
- var NodeSchema = external_exports.object({
27316
- name: external_exports.string(),
27317
- value: external_exports.unknown(),
27318
- binding: BindingSchema2.optional(),
27319
- userOverride: UserOverrideSchema.optional()
27320
- });
27321
- var CircularDependencyError = class extends Error {
27322
- constructor(cycle) {
27323
- super(`Circular dependency: ${cycle.join(" -> ")}`);
27324
- this.cycle = cycle;
27325
- this.name = "CircularDependencyError";
27326
- }
27327
- };
27328
- var UnknownPluginError = class extends Error {
27329
- constructor(pluginName) {
27330
- super(`Unknown plugin: ${pluginName}`);
27331
- this.pluginName = pluginName;
27332
- this.name = "UnknownPluginError";
27333
- }
27334
- };
27335
- var TokenGraph = class {
27336
- nodes = /* @__PURE__ */ new Map();
27337
- plugins = /* @__PURE__ */ new Map();
27338
- snapshot = null;
27339
- constructor(plugins = []) {
27340
- for (const plugin of plugins) this.plugins.set(plugin.name, plugin);
27341
- }
27342
- registerPlugin(plugin) {
27343
- this.plugins.set(plugin.name, plugin);
27344
- }
27345
- set(name, value, options) {
27346
- this.takeSnapshot();
27347
- const existing = this.nodes.get(name);
27348
- const next = {
27349
- name,
27350
- value,
27351
- userOverride: {
27352
- previousValue: existing?.value,
27353
- reason: options.reason,
27354
- ...options.context ? { context: options.context } : {},
27355
- ...options.kind ? { kind: options.kind } : {}
27356
- },
27357
- ...existing?.binding ? { binding: existing.binding } : {}
27358
- };
27359
- this.nodes.set(name, next);
27360
- this.cascadeFrom(name);
27361
- }
27362
- // Hydrate a node from persisted state. No cascade fires; the caller
27363
- // (registry constructor) is responsible for ordering: leaves first, then
27364
- // bindings. userOverride / binding are preserved verbatim from the persisted
27365
- // token so that the anchor + re-derivation hook both survive reload.
27366
- seed(name, value, extras) {
27367
- this.nodes.set(name, {
27368
- name,
27369
- value,
27370
- ...extras?.userOverride ? { userOverride: extras.userOverride } : {},
27371
- ...extras?.binding ? { binding: extras.binding } : {}
27372
- });
27373
- }
27374
- bind(name, pluginName, input) {
27375
- const plugin = this.requirePlugin(pluginName);
27376
- plugin.inputSchema.parse(input);
27377
- const directDeps = plugin.dependsOn(input);
27378
- this.assertNoCycle(name, directDeps);
27379
- this.takeSnapshot();
27380
- const value = plugin.transform(input, (n2) => this.get(n2));
27381
- plugin.outputSchema.parse(value);
27382
- this.nodes.set(name, { name, value, binding: { plugin: pluginName, input } });
27383
- this.cascadeFrom(name);
27384
- }
27385
- get(name) {
27386
- return this.nodes.get(name)?.value;
27387
- }
27388
- node(name) {
27389
- return this.nodes.get(name);
27390
- }
27391
- undo() {
27392
- if (!this.snapshot) return;
27393
- this.nodes = this.snapshot;
27394
- this.snapshot = null;
27395
- }
27396
- has(name) {
27397
- return this.nodes.has(name);
27398
- }
27399
- list(filter) {
27400
- const all = Array.from(this.nodes.values());
27401
- if (!filter?.namespace) return all;
27402
- const prefix = filter.namespace;
27403
- return all.filter(
27404
- (n2) => n2.name === prefix || n2.name.startsWith(`${prefix}-`) || n2.name.startsWith(`${prefix}.`)
27405
- );
27406
- }
27407
- size() {
27408
- return this.nodes.size;
27409
- }
27410
- requirePlugin(pluginName) {
27411
- const plugin = this.plugins.get(pluginName);
27412
- if (!plugin) throw new UnknownPluginError(pluginName);
27413
- return plugin;
27414
- }
27415
- takeSnapshot() {
27416
- this.snapshot = /* @__PURE__ */ new Map();
27417
- for (const [k, v] of this.nodes) this.snapshot.set(k, structuredClone(v));
27418
- }
27419
- cascadeFrom(changed) {
27420
- const dependents = this.collectDependents(changed);
27421
- if (dependents.size === 0) return;
27422
- const ordered = this.topoSort(dependents);
27423
- for (const name of ordered) {
27424
- const node = this.nodes.get(name);
27425
- if (!node || node.userOverride || !node.binding) continue;
27426
- const plugin = this.requirePlugin(node.binding.plugin);
27427
- const value = plugin.transform(node.binding.input, (n2) => this.get(n2));
27428
- plugin.outputSchema.parse(value);
27429
- this.nodes.set(name, { ...node, value });
27430
- }
27431
- }
27432
- collectDependents(changed) {
27433
- const out = /* @__PURE__ */ new Set();
27434
- const queue = [changed];
27435
- while (queue.length > 0) {
27436
- const current = queue.shift();
27437
- for (const [name, node] of this.nodes) {
27438
- if (out.has(name) || !node.binding) continue;
27439
- const plugin = this.plugins.get(node.binding.plugin);
27440
- if (!plugin) continue;
27441
- if (plugin.dependsOn(node.binding.input).includes(current)) {
27442
- out.add(name);
27443
- queue.push(name);
27444
- }
27445
- }
27446
- }
27447
- return out;
27448
- }
27449
- topoSort(names) {
27450
- const sorted = [];
27451
- const visited = /* @__PURE__ */ new Set();
27452
- const visiting = /* @__PURE__ */ new Set();
27453
- const visit = (name, path) => {
27454
- if (visited.has(name)) return;
27455
- if (visiting.has(name)) throw new CircularDependencyError([...path, name]);
27456
- visiting.add(name);
27457
- const node = this.nodes.get(name);
27458
- if (node?.binding) {
27459
- const plugin = this.plugins.get(node.binding.plugin);
27460
- if (plugin) {
27461
- for (const dep of plugin.dependsOn(node.binding.input)) {
27462
- if (names.has(dep)) visit(dep, [...path, name]);
27463
- }
27464
- }
27465
- }
27466
- visiting.delete(name);
27467
- visited.add(name);
27468
- sorted.push(name);
27469
- };
27470
- for (const name of names) visit(name, []);
27471
- return sorted;
27472
- }
27473
- assertNoCycle(name, directDeps) {
27474
- const visited = /* @__PURE__ */ new Set();
27475
- const dfs = (current, path) => {
27476
- if (current === name) throw new CircularDependencyError([...path, current]);
27477
- if (visited.has(current)) return;
27478
- visited.add(current);
27479
- const node = this.nodes.get(current);
27480
- if (!node?.binding) return;
27481
- const plugin = this.plugins.get(node.binding.plugin);
27482
- if (!plugin) return;
27483
- for (const dep of plugin.dependsOn(node.binding.input)) {
27484
- dfs(dep, [...path, current]);
27485
- }
27486
- };
27487
- for (const dep of directDeps) dfs(dep, [name]);
27333
+ // ../design-tokens/src/importers/adapter.ts
27334
+ var adapters = /* @__PURE__ */ new Map();
27335
+ function register2(adapter) {
27336
+ if (adapters.has(adapter.name)) {
27337
+ throw new Error(`DesignSystemAdapter "${adapter.name}" is already registered.`);
27488
27338
  }
27489
- };
27339
+ adapters.set(adapter.name, adapter);
27340
+ }
27341
+ function getAdapter(name) {
27342
+ const adapter = adapters.get(name);
27343
+ if (adapter !== void 0) return adapter;
27344
+ const known = getAvailableAdapters();
27345
+ throw new Error(`Unknown design system adapter "${name}". Valid values: ${known.join(", ")}.`);
27346
+ }
27347
+ function getAvailableAdapters() {
27348
+ return [...adapters.keys()].sort();
27349
+ }
27490
27350
 
27491
27351
  // ../design-tokens/src/importers/shadcn.ts
27492
27352
  import * as csstree from "css-tree";
@@ -27654,46 +27514,6 @@ function classifyDeclarations(declarations) {
27654
27514
  return { byNamespace: buckets, unclassified };
27655
27515
  }
27656
27516
 
27657
- // ../design-tokens/src/importers/color.ts
27658
- function importColorFamily(name, seedPosition, seed) {
27659
- const seedIndex = POSITION_TO_INDEX[seedPosition];
27660
- if (seedIndex === void 0) {
27661
- throw new Error(`importColorFamily: unknown scale position "${seedPosition}"`);
27662
- }
27663
- const built = buildColorValue(seed, { token: name });
27664
- if (built.scale.length !== SCALE_POSITIONS.length) {
27665
- throw new Error(
27666
- `importColorFamily: buildColorValue returned ${built.scale.length} positions, expected ${SCALE_POSITIONS.length}`
27667
- );
27668
- }
27669
- const scale2 = [...built.scale];
27670
- scale2[seedIndex] = seed;
27671
- const value = { ...built, name, scale: scale2 };
27672
- const tokens = [
27673
- {
27674
- name,
27675
- namespace: "color",
27676
- category: "color",
27677
- value,
27678
- userOverride: null,
27679
- containerQueryAware: true
27680
- }
27681
- ];
27682
- for (const [i, pos] of SCALE_POSITIONS.entries()) {
27683
- const oklch2 = value.scale[i];
27684
- if (!oklch2) continue;
27685
- tokens.push({
27686
- name: `${name}-${pos}`,
27687
- namespace: "color",
27688
- category: "color",
27689
- value: oklchToCSS(oklch2),
27690
- userOverride: null,
27691
- containerQueryAware: true
27692
- });
27693
- }
27694
- return tokens;
27695
- }
27696
-
27697
27517
  // ../design-tokens/src/importers/colors.ts
27698
27518
  function colorsFromClassification(classification) {
27699
27519
  const result = [];
@@ -27728,9 +27548,14 @@ var GENERIC_KEYWORDS = /* @__PURE__ */ new Set([
27728
27548
  "revert",
27729
27549
  "revert-layer"
27730
27550
  ]);
27551
+ var SOURCE_PRIORITY = {
27552
+ declaration: 0,
27553
+ google: 1,
27554
+ "font-face": 2
27555
+ };
27731
27556
  function detectFonts(css) {
27732
27557
  const found = /* @__PURE__ */ new Map();
27733
- const accept = (name, stack, sourceDeclName) => {
27558
+ const accept = (name, stack, source, sourceDeclName) => {
27734
27559
  if (name === null) return;
27735
27560
  const canonical = normalizeFamilyName(name);
27736
27561
  if (canonical === "" || GENERIC_KEYWORDS.has(canonical.toLowerCase())) return;
@@ -27740,30 +27565,33 @@ function detectFonts(css) {
27740
27565
  found.set(key, {
27741
27566
  name: canonical,
27742
27567
  stack,
27568
+ source,
27743
27569
  ...sourceDeclName && { sourceDeclName }
27744
27570
  });
27745
27571
  return;
27746
27572
  }
27573
+ const mergedSource = SOURCE_PRIORITY[source] > SOURCE_PRIORITY[existing.source] ? source : existing.source;
27747
27574
  const mergedStack = stack.length > existing.stack.length ? stack : existing.stack;
27748
27575
  const mergedDeclName = existing.sourceDeclName ?? sourceDeclName;
27749
27576
  found.set(key, {
27750
27577
  name: existing.name,
27751
27578
  stack: mergedStack,
27579
+ source: mergedSource,
27752
27580
  ...mergedDeclName && { sourceDeclName: mergedDeclName }
27753
27581
  });
27754
27582
  };
27755
27583
  for (const family of extractGoogleFontFamilies(css)) {
27756
- accept(family, quoteIfNeeded(family));
27584
+ accept(family, quoteIfNeeded(family), "google");
27757
27585
  }
27758
27586
  for (const family of extractFontFaceFamilies(css)) {
27759
- accept(family, quoteIfNeeded(family));
27587
+ accept(family, quoteIfNeeded(family), "font-face");
27760
27588
  }
27761
27589
  const rootDecls = extractShadcnRoot(css);
27762
27590
  const themeDecls = extractThemeBlocks2(css);
27763
27591
  for (const decl of [...rootDecls, ...themeDecls]) {
27764
27592
  if (!decl.name.startsWith("font-")) continue;
27765
27593
  const first = firstFamilyFromStack(decl.value);
27766
- accept(first, decl.value.trim(), decl.name);
27594
+ accept(first, decl.value.trim(), "declaration", decl.name);
27767
27595
  }
27768
27596
  return Array.from(found.values());
27769
27597
  }
@@ -27838,8 +27666,271 @@ function quoteIfNeeded(name) {
27838
27666
  return `"${name}"`;
27839
27667
  }
27840
27668
 
27841
- // ../design-tokens/src/importers/sense.ts
27842
- function senseShadcnCss(css) {
27669
+ // ../design-tokens/src/importers/shadcn-adapter.ts
27670
+ var sharedDetection = {
27671
+ detectFonts(css) {
27672
+ return [...detectFonts(css)];
27673
+ },
27674
+ detectColors(css) {
27675
+ const declarations = extractShadcnRoot(css);
27676
+ const classification = classifyDeclarations(declarations);
27677
+ return [...colorsFromClassification(classification)];
27678
+ },
27679
+ detectSpacing(css) {
27680
+ const value = detectSpacingBase(css);
27681
+ return value !== null ? { base: value } : {};
27682
+ },
27683
+ detectRadius(css) {
27684
+ const value = detectRadiusBase(css);
27685
+ return value !== null ? { base: value } : {};
27686
+ },
27687
+ detectFocusRing(css) {
27688
+ const value = detectFocusRingWidth(css);
27689
+ return value !== null ? { width: value } : {};
27690
+ },
27691
+ detectFontSize(css) {
27692
+ const value = detectFontSizeBase(css);
27693
+ return value !== null ? { base: value } : {};
27694
+ }
27695
+ };
27696
+ var shadcnAdapter = { name: "shadcn", ...sharedDetection };
27697
+ register2(shadcnAdapter);
27698
+
27699
+ // ../design-tokens/src/importers/tailwind-adapter.ts
27700
+ var tailwindAdapter = { name: "tailwind", ...sharedDetection };
27701
+ register2(tailwindAdapter);
27702
+
27703
+ // ../design-tokens/src/graph.ts
27704
+ var UserOverrideSchema = external_exports.object({
27705
+ previousValue: external_exports.unknown(),
27706
+ reason: external_exports.string(),
27707
+ context: external_exports.string().optional(),
27708
+ // Provenance. Optional by design: absent means unknown, which is what every
27709
+ // override written before this field existed actually is.
27710
+ kind: OverrideKindSchema.optional()
27711
+ });
27712
+ var BindingSchema2 = external_exports.object({
27713
+ plugin: external_exports.string(),
27714
+ input: external_exports.unknown()
27715
+ });
27716
+ var NodeSchema = external_exports.object({
27717
+ name: external_exports.string(),
27718
+ value: external_exports.unknown(),
27719
+ binding: BindingSchema2.optional(),
27720
+ userOverride: UserOverrideSchema.optional()
27721
+ });
27722
+ var CircularDependencyError = class extends Error {
27723
+ constructor(cycle) {
27724
+ super(`Circular dependency: ${cycle.join(" -> ")}`);
27725
+ this.cycle = cycle;
27726
+ this.name = "CircularDependencyError";
27727
+ }
27728
+ };
27729
+ var UnknownPluginError = class extends Error {
27730
+ constructor(pluginName) {
27731
+ super(`Unknown plugin: ${pluginName}`);
27732
+ this.pluginName = pluginName;
27733
+ this.name = "UnknownPluginError";
27734
+ }
27735
+ };
27736
+ var TokenGraph = class {
27737
+ nodes = /* @__PURE__ */ new Map();
27738
+ plugins = /* @__PURE__ */ new Map();
27739
+ snapshot = null;
27740
+ constructor(plugins = []) {
27741
+ for (const plugin of plugins) this.plugins.set(plugin.name, plugin);
27742
+ }
27743
+ registerPlugin(plugin) {
27744
+ this.plugins.set(plugin.name, plugin);
27745
+ }
27746
+ set(name, value, options) {
27747
+ this.takeSnapshot();
27748
+ const existing = this.nodes.get(name);
27749
+ const next = {
27750
+ name,
27751
+ value,
27752
+ userOverride: {
27753
+ previousValue: existing?.value,
27754
+ reason: options.reason,
27755
+ ...options.context ? { context: options.context } : {},
27756
+ ...options.kind ? { kind: options.kind } : {}
27757
+ },
27758
+ ...existing?.binding ? { binding: existing.binding } : {}
27759
+ };
27760
+ this.nodes.set(name, next);
27761
+ this.cascadeFrom(name);
27762
+ }
27763
+ // Hydrate a node from persisted state. No cascade fires; the caller
27764
+ // (registry constructor) is responsible for ordering: leaves first, then
27765
+ // bindings. userOverride / binding are preserved verbatim from the persisted
27766
+ // token so that the anchor + re-derivation hook both survive reload.
27767
+ seed(name, value, extras) {
27768
+ this.nodes.set(name, {
27769
+ name,
27770
+ value,
27771
+ ...extras?.userOverride ? { userOverride: extras.userOverride } : {},
27772
+ ...extras?.binding ? { binding: extras.binding } : {}
27773
+ });
27774
+ }
27775
+ bind(name, pluginName, input) {
27776
+ const plugin = this.requirePlugin(pluginName);
27777
+ plugin.inputSchema.parse(input);
27778
+ const directDeps = plugin.dependsOn(input);
27779
+ this.assertNoCycle(name, directDeps);
27780
+ this.takeSnapshot();
27781
+ const value = plugin.transform(input, (n2) => this.get(n2));
27782
+ plugin.outputSchema.parse(value);
27783
+ this.nodes.set(name, { name, value, binding: { plugin: pluginName, input } });
27784
+ this.cascadeFrom(name);
27785
+ }
27786
+ get(name) {
27787
+ return this.nodes.get(name)?.value;
27788
+ }
27789
+ node(name) {
27790
+ return this.nodes.get(name);
27791
+ }
27792
+ undo() {
27793
+ if (!this.snapshot) return;
27794
+ this.nodes = this.snapshot;
27795
+ this.snapshot = null;
27796
+ }
27797
+ has(name) {
27798
+ return this.nodes.has(name);
27799
+ }
27800
+ list(filter) {
27801
+ const all = Array.from(this.nodes.values());
27802
+ if (!filter?.namespace) return all;
27803
+ const prefix = filter.namespace;
27804
+ return all.filter(
27805
+ (n2) => n2.name === prefix || n2.name.startsWith(`${prefix}-`) || n2.name.startsWith(`${prefix}.`)
27806
+ );
27807
+ }
27808
+ size() {
27809
+ return this.nodes.size;
27810
+ }
27811
+ requirePlugin(pluginName) {
27812
+ const plugin = this.plugins.get(pluginName);
27813
+ if (!plugin) throw new UnknownPluginError(pluginName);
27814
+ return plugin;
27815
+ }
27816
+ takeSnapshot() {
27817
+ this.snapshot = /* @__PURE__ */ new Map();
27818
+ for (const [k, v] of this.nodes) this.snapshot.set(k, structuredClone(v));
27819
+ }
27820
+ cascadeFrom(changed) {
27821
+ const dependents = this.collectDependents(changed);
27822
+ if (dependents.size === 0) return;
27823
+ const ordered = this.topoSort(dependents);
27824
+ for (const name of ordered) {
27825
+ const node = this.nodes.get(name);
27826
+ if (!node || node.userOverride || !node.binding) continue;
27827
+ const plugin = this.requirePlugin(node.binding.plugin);
27828
+ const value = plugin.transform(node.binding.input, (n2) => this.get(n2));
27829
+ plugin.outputSchema.parse(value);
27830
+ this.nodes.set(name, { ...node, value });
27831
+ }
27832
+ }
27833
+ collectDependents(changed) {
27834
+ const out = /* @__PURE__ */ new Set();
27835
+ const queue = [changed];
27836
+ while (queue.length > 0) {
27837
+ const current = queue.shift();
27838
+ for (const [name, node] of this.nodes) {
27839
+ if (out.has(name) || !node.binding) continue;
27840
+ const plugin = this.plugins.get(node.binding.plugin);
27841
+ if (!plugin) continue;
27842
+ if (plugin.dependsOn(node.binding.input).includes(current)) {
27843
+ out.add(name);
27844
+ queue.push(name);
27845
+ }
27846
+ }
27847
+ }
27848
+ return out;
27849
+ }
27850
+ topoSort(names) {
27851
+ const sorted = [];
27852
+ const visited = /* @__PURE__ */ new Set();
27853
+ const visiting = /* @__PURE__ */ new Set();
27854
+ const visit = (name, path) => {
27855
+ if (visited.has(name)) return;
27856
+ if (visiting.has(name)) throw new CircularDependencyError([...path, name]);
27857
+ visiting.add(name);
27858
+ const node = this.nodes.get(name);
27859
+ if (node?.binding) {
27860
+ const plugin = this.plugins.get(node.binding.plugin);
27861
+ if (plugin) {
27862
+ for (const dep of plugin.dependsOn(node.binding.input)) {
27863
+ if (names.has(dep)) visit(dep, [...path, name]);
27864
+ }
27865
+ }
27866
+ }
27867
+ visiting.delete(name);
27868
+ visited.add(name);
27869
+ sorted.push(name);
27870
+ };
27871
+ for (const name of names) visit(name, []);
27872
+ return sorted;
27873
+ }
27874
+ assertNoCycle(name, directDeps) {
27875
+ const visited = /* @__PURE__ */ new Set();
27876
+ const dfs = (current, path) => {
27877
+ if (current === name) throw new CircularDependencyError([...path, current]);
27878
+ if (visited.has(current)) return;
27879
+ visited.add(current);
27880
+ const node = this.nodes.get(current);
27881
+ if (!node?.binding) return;
27882
+ const plugin = this.plugins.get(node.binding.plugin);
27883
+ if (!plugin) return;
27884
+ for (const dep of plugin.dependsOn(node.binding.input)) {
27885
+ dfs(dep, [...path, current]);
27886
+ }
27887
+ };
27888
+ for (const dep of directDeps) dfs(dep, [name]);
27889
+ }
27890
+ };
27891
+
27892
+ // ../design-tokens/src/importers/color.ts
27893
+ function importColorFamily(name, seedPosition, seed) {
27894
+ const seedIndex = POSITION_TO_INDEX[seedPosition];
27895
+ if (seedIndex === void 0) {
27896
+ throw new Error(`importColorFamily: unknown scale position "${seedPosition}"`);
27897
+ }
27898
+ const built = buildColorValue(seed, { token: name });
27899
+ if (built.scale.length !== SCALE_POSITIONS.length) {
27900
+ throw new Error(
27901
+ `importColorFamily: buildColorValue returned ${built.scale.length} positions, expected ${SCALE_POSITIONS.length}`
27902
+ );
27903
+ }
27904
+ const scale2 = [...built.scale];
27905
+ scale2[seedIndex] = seed;
27906
+ const value = { ...built, name, scale: scale2 };
27907
+ const tokens = [
27908
+ {
27909
+ name,
27910
+ namespace: "color",
27911
+ category: "color",
27912
+ value,
27913
+ userOverride: null,
27914
+ containerQueryAware: true
27915
+ }
27916
+ ];
27917
+ for (const [i, pos] of SCALE_POSITIONS.entries()) {
27918
+ const oklch2 = value.scale[i];
27919
+ if (!oklch2) continue;
27920
+ tokens.push({
27921
+ name: `${name}-${pos}`,
27922
+ namespace: "color",
27923
+ category: "color",
27924
+ value: oklchToCSS(oklch2),
27925
+ userOverride: null,
27926
+ containerQueryAware: true
27927
+ });
27928
+ }
27929
+ return tokens;
27930
+ }
27931
+
27932
+ // ../design-tokens/src/importers/sense.ts
27933
+ function senseShadcnCss(css) {
27843
27934
  const declarations = extractShadcnRoot(css);
27844
27935
  const classification = classifyDeclarations(declarations);
27845
27936
  const byNamespace = Object.fromEntries(
@@ -29330,912 +29421,566 @@ function isPathField(value) {
29330
29421
  return typeof value === "string" || Array.isArray(value);
29331
29422
  }
29332
29423
 
29333
- // src/commands/add.ts
29424
+ // src/commands/init.ts
29425
+ import { existsSync as existsSync3 } from "fs";
29426
+ import { copyFile, mkdir as mkdir2, readFile as readFile3, rm, writeFile as writeFile2 } from "fs/promises";
29427
+ import { createRequire } from "module";
29428
+ import { join as join7, relative as relative2 } from "path";
29429
+ import { checkbox, confirm, select } from "@inquirer/prompts";
29334
29430
  var REGISTRY_PLUGINS = [scalePlugin, contrastPlugin, statePlugin, invertPlugin];
29335
- async function regenerateAfterInstall(cwd, config2) {
29336
- const paths = getRaftersPaths(cwd);
29337
- let registry2;
29338
- try {
29339
- registry2 = loadRegistryFromDir(paths.tokens, REGISTRY_PLUGINS);
29340
- } catch {
29341
- return;
29342
- }
29343
- if (registry2.size() === 0) return;
29344
- try {
29345
- await regenerateOutputs(registry2, {
29346
- outputDir: paths.output,
29347
- exports: config2.exports,
29348
- contentSources: resolveContentSources(cwd, config2),
29349
- darkMode: config2.darkMode ?? "class",
29350
- includeImport: !config2.shadcn
29351
- });
29352
- } catch (err) {
29353
- log({
29354
- event: "add:regen-failed",
29355
- message: `Output regeneration failed: ${err instanceof Error ? err.message : String(err)}`
29356
- });
29357
- }
29431
+ async function backupCss(cssPath) {
29432
+ const backupPath = cssPath.replace(/\.css$/, ".backup.css");
29433
+ await copyFile(cssPath, backupPath);
29434
+ return backupPath;
29358
29435
  }
29359
- async function isInitialized(cwd) {
29360
- const paths = getRaftersPaths(cwd);
29361
- try {
29362
- await access(paths.root);
29363
- return true;
29364
- } catch {
29365
- return false;
29436
+ function cleanSourceCssBlocks(content) {
29437
+ function parseBlocks(src) {
29438
+ const blocks = [];
29439
+ let depth = 0;
29440
+ const headerStack = [];
29441
+ let segmentStart = 0;
29442
+ for (let i = 0; i < src.length; i++) {
29443
+ const ch = src.charAt(i);
29444
+ if (ch === "{") {
29445
+ headerStack.push(segmentStart);
29446
+ depth++;
29447
+ if (depth >= 1) {
29448
+ blocks.push({
29449
+ headerStart: segmentStart,
29450
+ bodyStart: i + 1,
29451
+ end: -1,
29452
+ header: src.slice(segmentStart, i).trim()
29453
+ });
29454
+ }
29455
+ segmentStart = i + 1;
29456
+ } else if (ch === "}") {
29457
+ depth--;
29458
+ const open = blocks.slice().reverse().find((b2) => b2.end === -1);
29459
+ if (open) open.end = i + 1;
29460
+ headerStack.pop();
29461
+ segmentStart = i + 1;
29462
+ } else if (ch === ";") {
29463
+ segmentStart = i + 1;
29464
+ }
29465
+ }
29466
+ return blocks.filter((b2) => b2.end !== -1);
29366
29467
  }
29367
- }
29368
- async function loadConfig(cwd) {
29369
- const paths = getRaftersPaths(cwd);
29370
- try {
29371
- const content = await readFile3(paths.config, "utf-8");
29372
- return JSON.parse(content);
29373
- } catch (err) {
29374
- if (existsSync3(paths.config)) {
29375
- const message = err instanceof Error ? err.message : String(err);
29376
- log({ event: "add:warning", message: `Failed to load config: ${message}` });
29468
+ function isContentEmpty(body) {
29469
+ return body.replace(/\/\*[\s\S]*?\*\//g, "").trim().length === 0;
29470
+ }
29471
+ let out = content;
29472
+ for (let pass = 0; pass < 10; pass++) {
29473
+ const blocks = parseBlocks(out);
29474
+ let edited = false;
29475
+ for (const block of blocks.sort((a2, b2) => b2.headerStart - a2.headerStart)) {
29476
+ const header = block.header;
29477
+ if (/^@theme\s+inline$/.test(header)) {
29478
+ out = out.slice(0, block.headerStart) + out.slice(block.end);
29479
+ edited = true;
29480
+ break;
29481
+ }
29482
+ if (header === ":root" || /(^|,)\s*:root\s*$/.test(header)) {
29483
+ const body = out.slice(block.bodyStart, block.end - 1);
29484
+ const cleanedBody = body.replace(/--[\w-]+\s*:[^;{}]*;?/g, "");
29485
+ if (isContentEmpty(cleanedBody)) {
29486
+ out = out.slice(0, block.headerStart) + out.slice(block.end);
29487
+ edited = true;
29488
+ break;
29489
+ }
29490
+ if (cleanedBody !== body) {
29491
+ out = out.slice(0, block.bodyStart) + cleanedBody + out.slice(block.end - 1);
29492
+ edited = true;
29493
+ break;
29494
+ }
29495
+ continue;
29496
+ }
29497
+ if (header.startsWith("@media") || header.startsWith("@supports")) {
29498
+ const body = out.slice(block.bodyStart, block.end - 1);
29499
+ if (isContentEmpty(body)) {
29500
+ out = out.slice(0, block.headerStart) + out.slice(block.end);
29501
+ edited = true;
29502
+ break;
29503
+ }
29504
+ }
29377
29505
  }
29378
- return null;
29506
+ if (!edited) break;
29379
29507
  }
29508
+ return out.replace(/[ \t]+$/gm, "").replace(/\n{3,}/g, "\n\n");
29380
29509
  }
29381
- function resolveRegistryUrl(options, config2) {
29382
- return options.registryUrl ?? config2?.registryUrl;
29510
+ async function stripImportedDeclarations(cwd, cssPath, importedNames) {
29511
+ if (importedNames.length === 0) return;
29512
+ const fullPath = join7(cwd, cssPath);
29513
+ const content = await readFile3(fullPath, "utf-8");
29514
+ const pattern = new RegExp(importedNames.map((n2) => `^\\s*--${n2}[^;]*;\\s*$`).join("|"), "gm");
29515
+ const cleaned = cleanSourceCssBlocks(content.replace(pattern, ""));
29516
+ const collapsed = cleaned.replace(/\n{3,}/g, "\n\n");
29517
+ await writeFile2(fullPath, collapsed);
29383
29518
  }
29384
- async function saveConfig(cwd, config2) {
29385
- const paths = getRaftersPaths(cwd);
29386
- await writeFile2(paths.config, JSON.stringify(config2, null, 2));
29519
+ function migrateConfig(raw) {
29520
+ if ("shadcn" in raw && !("source" in raw)) {
29521
+ if (raw.shadcn === true) {
29522
+ raw.source = "shadcn";
29523
+ }
29524
+ delete raw.shadcn;
29525
+ }
29526
+ return raw;
29387
29527
  }
29388
- function getInstalledNames(config2) {
29389
- if (!config2?.installed) return [];
29390
- const names = /* @__PURE__ */ new Set([
29391
- ...config2.installed.components,
29392
- ...config2.installed.primitives,
29393
- ...config2.installed.composites ?? [],
29394
- ...config2.installed.rules ?? [],
29395
- ...config2.installed.substrate ?? []
29396
- ]);
29397
- return [...names].sort();
29528
+ async function updateMainCss(cwd, cssPath, themePath) {
29529
+ const fullCssPath = join7(cwd, cssPath);
29530
+ const cssContent = await readFile3(fullCssPath, "utf-8");
29531
+ const cssDir = join7(cwd, cssPath, "..");
29532
+ const themeFullPath = join7(cwd, themePath);
29533
+ const relativeThemePath = relative2(cssDir, themeFullPath);
29534
+ if (cssContent.includes(".rafters/output/rafters.css")) {
29535
+ log({ event: "init:css_already_imported", cssPath });
29536
+ return;
29537
+ }
29538
+ await backupCss(fullCssPath);
29539
+ let newContent;
29540
+ if (cssContent.includes('@import "tailwindcss"')) {
29541
+ newContent = cssContent.replace('@import "tailwindcss";', `@import "${relativeThemePath}";`);
29542
+ } else if (cssContent.includes("@import 'tailwindcss'")) {
29543
+ newContent = cssContent.replace("@import 'tailwindcss';", `@import "${relativeThemePath}";`);
29544
+ } else {
29545
+ newContent = `@import "${relativeThemePath}";
29546
+
29547
+ ${cssContent}`;
29548
+ }
29549
+ await writeFile2(fullCssPath, newContent);
29550
+ log({
29551
+ event: "init:css_updated",
29552
+ cssPath,
29553
+ themePath: relativeThemePath
29554
+ });
29398
29555
  }
29399
- async function discoverUntrackedNames(cwd, config2, client, tracked) {
29400
- let index = null;
29401
- try {
29402
- index = await client.fetchIndex();
29403
- } catch (err) {
29404
- const message = err instanceof Error ? err.message : String(err);
29405
- log({
29406
- event: "add:warning",
29407
- message: `Could not read the registry index to reconcile on-disk components (${message}). Updating tracked components only.`
29556
+ function isInteractive() {
29557
+ return Boolean(process.stdin.isTTY && process.stdout.isTTY);
29558
+ }
29559
+ function resolveSourceCssPath(cwd, framework, detectedFramework, projectCssPath, shadcnCssPath, emit) {
29560
+ if (shadcnCssPath) {
29561
+ if (existsSync3(join7(cwd, shadcnCssPath))) return shadcnCssPath;
29562
+ const fallback = framework === detectedFramework ? projectCssPath : findCssPath(cwd, framework);
29563
+ emit({
29564
+ event: "init:shadcn_css_missing",
29565
+ configuredPath: shadcnCssPath,
29566
+ fallbackPath: fallback,
29567
+ message: "components.json points at a CSS file that does not exist. Falling back to the framework canonical location."
29408
29568
  });
29569
+ return fallback;
29409
29570
  }
29410
- const { untracked } = buildUpdateCandidates(tracked, index, readInstallRoots(cwd, config2));
29411
- return untracked;
29571
+ return framework === detectedFramework ? projectCssPath : findCssPath(cwd, framework);
29412
29572
  }
29413
- function getComponentTarget(config2) {
29414
- return resolveComponentTarget(config2);
29573
+ function slugifyFontName(name) {
29574
+ return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
29415
29575
  }
29416
- var SHARED_EXTENSIONS = /* @__PURE__ */ new Set([".behavior.ts", ".classes.ts"]);
29417
- function isSharedFile(path) {
29418
- for (const ext of SHARED_EXTENSIONS) {
29419
- if (path.endsWith(ext)) return true;
29420
- }
29421
- return false;
29576
+ function isFontStackValue(value) {
29577
+ if (typeof value !== "string") return false;
29578
+ if (value.startsWith("var(")) return false;
29579
+ if (/[,'"]/.test(value)) return true;
29580
+ return /\b(sans-serif|serif|monospace|cursive|fantasy|system-ui|ui-(?:sans-serif|serif|monospace|rounded))\b/.test(
29581
+ value
29582
+ );
29422
29583
  }
29423
- var REACT_FALLBACK_TARGETS = /* @__PURE__ */ new Set(["astro", "vue", "svelte"]);
29424
- function selectFilesForFramework(files, target) {
29425
- const preferredExt = targetToExtension(target);
29426
- const shared = files.filter((f) => isSharedFile(f.path));
29427
- const matched = files.filter((f) => f.path.endsWith(preferredExt));
29428
- if (matched.length > 0) {
29429
- return { files: [...matched, ...shared], fallback: false };
29430
- }
29431
- if (REACT_FALLBACK_TARGETS.has(target)) {
29432
- const fallbackFiles = files.filter((f) => f.path.endsWith(".tsx"));
29433
- if (fallbackFiles.length > 0) {
29434
- return { files: [...fallbackFiles, ...shared], fallback: true };
29584
+ async function resolveFramework(detected, flag, agentMode) {
29585
+ if (flag) {
29586
+ if (!isSelectableFramework(flag)) {
29587
+ throw new Error(
29588
+ `Unknown --framework "${flag}". Valid values: ${SELECTABLE_FRAMEWORKS.join(", ")}.`
29589
+ );
29435
29590
  }
29591
+ return flag;
29436
29592
  }
29437
- if (target !== "react" && !REACT_FALLBACK_TARGETS.has(target)) {
29438
- return { files: shared, fallback: false };
29439
- }
29440
- return { files, fallback: false };
29441
- }
29442
- var TARGET_GATED_COMPOSITE_FILES = [
29443
- { suffix: "Composite.astro", target: "astro" }
29444
- ];
29445
- function selectCompositeFiles(files, target) {
29446
- return files.filter((file2) => {
29447
- const gate = TARGET_GATED_COMPOSITE_FILES.find((g2) => file2.path.endsWith(g2.suffix));
29448
- return gate ? gate.target === target : true;
29593
+ if (detected !== "unknown") return detected;
29594
+ if (agentMode || !isInteractive()) return "unknown";
29595
+ const picked = await select({
29596
+ message: "Couldn't auto-detect your framework. Which one is this?",
29597
+ choices: SELECTABLE_FRAMEWORKS.map((value) => ({
29598
+ name: FRAMEWORK_SPECS[value].label ?? value,
29599
+ value
29600
+ }))
29449
29601
  });
29602
+ return picked;
29450
29603
  }
29451
- var FOLDER_NAMES = /* @__PURE__ */ new Set(["composites"]);
29452
- function isAlreadyInstalled(config2, item) {
29453
- if (!config2?.installed) return false;
29454
- const { components, primitives, composites: composites2, rules, substrate } = config2.installed;
29455
- const bucketByType = {
29456
- ui: components,
29457
- primitive: primitives,
29458
- composite: composites2 ?? [],
29459
- rule: rules ?? [],
29460
- substrate: substrate ?? []
29461
- };
29462
- return bucketByType[item.type].includes(item.name);
29463
- }
29464
- function isInstalledOnDisk(config2, item, cwd) {
29465
- if (!isAlreadyInstalled(config2, item)) return false;
29466
- const filesToCheck = item.type === "ui" ? selectFilesForFramework(item.files, getComponentTarget(config2)).files : item.files;
29467
- for (const file2 of filesToCheck) {
29468
- if (fileExists(cwd, transformPath(file2.path, config2, cwd))) return true;
29469
- }
29470
- return false;
29471
- }
29472
- function trackInstalled(config2, items) {
29473
- if (!config2.installed) {
29474
- config2.installed = { components: [], primitives: [], composites: [], rules: [] };
29604
+ async function promptExportFormats(existingConfig) {
29605
+ if (!isInteractive()) {
29606
+ return existingConfig ?? DEFAULT_EXPORTS;
29475
29607
  }
29476
- const installed = config2.installed;
29477
- if (!installed.composites) installed.composites = [];
29478
- if (!installed.rules) installed.rules = [];
29479
- if (!installed.substrate) installed.substrate = [];
29480
- const bucketByType = {
29481
- ui: installed.components,
29482
- primitive: installed.primitives,
29483
- composite: installed.composites,
29484
- rule: installed.rules,
29485
- substrate: installed.substrate
29486
- };
29487
- for (const item of items) {
29488
- const bucket = bucketByType[item.type];
29489
- if (!bucket.includes(item.name)) bucket.push(item.name);
29490
- }
29491
- installed.components.sort();
29492
- installed.primitives.sort();
29493
- installed.composites.sort();
29494
- installed.rules.sort();
29495
- installed.substrate.sort();
29496
- }
29497
- function rootFor(field, cwd, fallback) {
29498
- return field === void 0 ? fallback : resolveRoot(field, cwd, fallback);
29499
- }
29500
- function transformPath(registryPath, config2, cwd = process.cwd()) {
29501
- if (!config2) return registryPath;
29502
- const replacements = [
29503
- ["components/ui/", config2.componentsPath, "components/ui"],
29504
- ["lib/primitives/", config2.primitivesPath, "lib/primitives"],
29505
- ["composites/", config2.compositesPath, "composites"],
29506
- ["rules/", config2.rulesPath, "lib/rules"]
29608
+ const choices = EXPORT_CHOICES.map((choice) => ({
29609
+ name: choice.name,
29610
+ value: choice.value,
29611
+ checked: existingConfig ? existingConfig[choice.value] : choice.checked
29612
+ }));
29613
+ const allChoices = [
29614
+ ...choices,
29615
+ ...FUTURE_EXPORTS.map((choice) => ({
29616
+ name: `${choice.name} (${choice.disabled})`,
29617
+ value: choice.value,
29618
+ checked: false,
29619
+ disabled: true
29620
+ }))
29507
29621
  ];
29508
- for (const [prefix, field, fallback] of replacements) {
29509
- if (registryPath.startsWith(prefix)) {
29510
- return registryPath.replace(prefix, `${rootFor(field, cwd, fallback)}/`);
29622
+ const selections = await checkbox({
29623
+ message: "What would you like to export?",
29624
+ choices: allChoices,
29625
+ required: true
29626
+ });
29627
+ return selectionsToConfig(selections);
29628
+ }
29629
+ function isTailwindCliInstalled() {
29630
+ const require2 = createRequire(import.meta.url);
29631
+ try {
29632
+ require2.resolve("@tailwindcss/cli/package.json");
29633
+ return true;
29634
+ } catch (err) {
29635
+ if (err instanceof Error && "code" in err && err.code === "MODULE_NOT_FOUND") {
29636
+ return false;
29511
29637
  }
29638
+ throw err;
29512
29639
  }
29513
- return registryPath;
29514
- }
29515
- function substrateProjectPath(registryPath, config2, cwd = process.cwd()) {
29516
- const componentsResolved = rootFor(config2?.componentsPath, cwd, "components/ui");
29517
- const sourceRoot = componentsResolved.replace(/\/?components\/ui$/, "");
29518
- return sourceRoot ? join7(sourceRoot, registryPath) : registryPath;
29519
- }
29520
- function fileExists(cwd, relativePath) {
29521
- return existsSync3(join7(cwd, relativePath));
29522
29640
  }
29523
- function transformFileContent(content, config2, fileType = "component", cwd = process.cwd(), opts = {}) {
29524
- const { substrateKinds = [], installPath } = opts;
29525
- let transformed = content;
29526
- const componentsPath = rootFor(config2?.componentsPath, cwd, "components/ui");
29527
- const primitivesPath = rootFor(config2?.primitivesPath, cwd, "lib/primitives");
29528
- const stripSourceRoot = (p2) => p2.replace(/^(src|app)\//, "");
29529
- const aliasComponents = stripSourceRoot(componentsPath);
29530
- const aliasPrimitives = stripSourceRoot(primitivesPath);
29531
- transformed = transformed.replace(
29532
- /from\s+['"]\.\.\/\.\.\/primitives\/([^'"]+)['"]/g,
29533
- `from '@/${aliasPrimitives}/$1'`
29534
- );
29535
- transformed = transformed.replace(
29536
- /from\s+['"]\.\.\/primitives\/([^'"]+)['"]/g,
29537
- `from '@/${aliasPrimitives}/$1'`
29538
- );
29539
- const substrateOwnDir = fileType === "substrate" && installPath ? stripSourceRoot(dirname(installPath)) : null;
29540
- const aliasSibling = fileType === "primitive" ? aliasPrimitives : substrateOwnDir ?? aliasComponents;
29541
- transformed = transformed.replace(/from\s+['"]\.\/([^'"]+)['"]/g, `from '@/${aliasSibling}/$1'`);
29542
- if (substrateKinds.length > 0) {
29543
- const kindAlternation = substrateKinds.join("|");
29544
- transformed = transformed.replace(
29545
- new RegExp(`from\\s+['"](?:\\.\\./){1,2}(${kindAlternation})/([^'"]+)['"]`, "g"),
29546
- "from '@/$1/$2'"
29641
+ async function ensureTailwindCli(cwd) {
29642
+ if (!isInteractive() || isAgentMode()) {
29643
+ throw new Error(
29644
+ "Standalone CSS export requires @tailwindcss/cli. Install it as a dev dependency in your project."
29547
29645
  );
29548
29646
  }
29549
- transformed = transformed.replace(
29550
- /from\s+['"]\.\.\/([^/'"]+)\/\1([^'"]*)['"]/g,
29551
- `from '@/${aliasComponents}/$1$2'`
29552
- );
29553
- transformed = transformed.replace(
29554
- /from\s+['"]\.\.\/([^'"]+)['"]/g,
29555
- `from '@/${aliasComponents}/$1'`
29556
- );
29557
- return transformed;
29558
- }
29559
- async function installItem(cwd, item, options, config2, substrateKinds = []) {
29560
- const installedFiles = [];
29561
- let skipped = false;
29562
- let filesToInstall = item.files;
29563
- if (item.type === "ui") {
29564
- const target = getComponentTarget(config2);
29565
- const selection = selectFilesForFramework(item.files, target);
29566
- filesToInstall = selection.files;
29567
- if (selection.fallback) {
29568
- log({
29569
- event: "add:fallback",
29570
- component: item.name,
29571
- target,
29572
- message: `No ${targetToExtension(target)} version available for ${item.name}. Installing React version.`
29573
- });
29574
- }
29575
- const hasComponentFile = selection.files.some((f) => !isSharedFile(f.path));
29576
- if (!hasComponentFile) {
29577
- throw new Error(
29578
- `No ${targetToExtension(target)} version available for "${item.name}" and no compatible fallback exists.`
29579
- );
29580
- }
29581
- } else if (item.type === "composite") {
29582
- filesToInstall = selectCompositeFiles(item.files, getComponentTarget(config2));
29647
+ const shouldInstall = await confirm({
29648
+ message: "Standalone CSS requires @tailwindcss/cli. Install it now?",
29649
+ default: true
29650
+ });
29651
+ if (!shouldInstall) {
29652
+ throw new Error("Standalone CSS export requires @tailwindcss/cli.");
29583
29653
  }
29584
- for (const file2 of filesToInstall) {
29585
- const projectPath2 = item.type === "substrate" ? substrateProjectPath(file2.path, config2, cwd) : transformPath(file2.path, config2, cwd);
29586
- const targetPath = join7(cwd, projectPath2);
29587
- if (fileExists(cwd, projectPath2)) {
29588
- if (!options.overwrite) {
29589
- log({
29590
- event: "add:skip",
29591
- component: item.name,
29592
- file: projectPath2,
29593
- reason: "exists"
29594
- });
29595
- skipped = true;
29596
- continue;
29597
- }
29598
- }
29599
- await mkdir2(dirname(targetPath), { recursive: true });
29600
- const fileType = item.type === "primitive" ? "primitive" : item.type === "substrate" ? "substrate" : "component";
29601
- const transformedContent = transformFileContent(file2.content, config2, fileType, cwd, {
29602
- substrateKinds,
29603
- installPath: file2.path
29604
- });
29605
- await writeFile2(targetPath, transformedContent, "utf-8");
29606
- installedFiles.push(projectPath2);
29654
+ await updateDependencies([], ["@tailwindcss/cli"], { cwd });
29655
+ if (!isTailwindCliInstalled()) {
29656
+ throw new Error(
29657
+ "@tailwindcss/cli was installed but cannot be resolved. Try installing at the workspace root."
29658
+ );
29607
29659
  }
29608
- return {
29609
- installed: installedFiles.length > 0,
29610
- skipped,
29611
- files: installedFiles
29612
- };
29613
29660
  }
29614
- async function add(componentArgs, options) {
29615
- setAgentMode(options.agent ?? false);
29616
- let components = componentArgs;
29617
- const cwd = process.cwd();
29618
- const config2 = await loadConfig(cwd);
29619
- const client = new RegistryClient(resolveRegistryUrl(options, config2));
29620
- let folder;
29621
- const firstArg = components[0];
29622
- if (firstArg && FOLDER_NAMES.has(firstArg)) {
29623
- folder = firstArg;
29624
- components = components.slice(1);
29625
- }
29626
- if (options.list) {
29627
- const availableComponents = await client.listComponents();
29628
- const availableComposites = await client.listComposites();
29629
- if (options.agent) {
29630
- log({
29631
- event: "add:list",
29632
- components: availableComponents,
29633
- composites: availableComposites
29634
- });
29635
- } else {
29636
- console.log("Available components:\n");
29637
- for (const comp of availableComponents) {
29638
- console.log(` ${comp.name} ${comp.description ?? ""}`);
29639
- }
29640
- if (availableComposites.length > 0) {
29641
- console.log("\nAvailable composites:\n");
29642
- for (const comp of availableComposites) {
29643
- console.log(` ${comp.name} ${comp.description ?? ""}`);
29644
- }
29645
- }
29646
- }
29647
- return;
29661
+ async function generateOutputs(cwd, paths, registry2, exports, source, darkMode = "class", config2) {
29662
+ const contentSources = config2 ? resolveContentSources(cwd, config2) : [];
29663
+ if (exports.compiled && !isTailwindCliInstalled()) {
29664
+ log({ event: "init:prompting_exports" });
29665
+ await ensureTailwindCli(cwd);
29648
29666
  }
29649
- const initialized = await isInitialized(cwd);
29650
- if (!initialized) {
29651
- error46("Project not initialized. Run `rafters init` first.");
29652
- process.exitCode = 1;
29653
- return;
29667
+ if (exports.compiled) {
29668
+ log({ event: "init:compiling_css" });
29654
29669
  }
29655
- if (options.update) {
29656
- options.overwrite = true;
29670
+ return regenerateOutputs(registry2, {
29671
+ outputDir: paths.output,
29672
+ exports,
29673
+ contentSources,
29674
+ darkMode,
29675
+ includeImport: source !== "shadcn"
29676
+ });
29677
+ }
29678
+ async function regenerateFromExisting(cwd, paths, source, isAgentMode2, framework) {
29679
+ log({ event: "init:regenerate", cwd });
29680
+ let existingConfig = null;
29681
+ try {
29682
+ const configContent = await readFile3(paths.config, "utf-8");
29683
+ existingConfig = migrateConfig(
29684
+ JSON.parse(configContent)
29685
+ );
29686
+ } catch {
29657
29687
  }
29658
- let untrackedNames = [];
29659
- if (options.updateAll) {
29660
- options.overwrite = true;
29661
- if (!config2) {
29662
- error46("No rafters config found. Run 'rafters init' first.");
29663
- process.exitCode = 1;
29664
- return;
29665
- }
29666
- const trackedNames = getInstalledNames(config2);
29667
- untrackedNames = await discoverUntrackedNames(cwd, config2, client, trackedNames);
29668
- const candidates = [.../* @__PURE__ */ new Set([...trackedNames, ...untrackedNames])].sort();
29669
- if (candidates.length === 0) {
29670
- error46("No installed components found. Use 'rafters add <component>' to install first.");
29671
- process.exitCode = 1;
29672
- return;
29673
- }
29674
- if (untrackedNames.length > 0) {
29675
- log({
29676
- event: "add:untracked",
29677
- components: untrackedNames,
29678
- message: `Found ${untrackedNames.length} component(s) on disk the config never tracked: ${untrackedNames.join(", ")}. Refreshing and tracking them.`
29679
- });
29680
- }
29681
- components = candidates;
29688
+ if (framework !== "unknown" && existingConfig) {
29689
+ const frameworkPaths = FRAMEWORK_SPECS[framework].components;
29690
+ existingConfig.framework = framework;
29691
+ existingConfig.componentsPath = frameworkPaths.components;
29692
+ existingConfig.primitivesPath = frameworkPaths.primitives;
29693
+ existingConfig.compositesPath = frameworkPaths.composites;
29694
+ existingConfig.rulesPath = frameworkPaths.rules;
29682
29695
  }
29683
- if (folder === "composites" && components.length === 0) {
29684
- components = ["composites"];
29685
- }
29686
- if (components.length === 0) {
29687
- error46("No components specified. Usage: rafters add <component...>");
29688
- process.exitCode = 1;
29689
- return;
29696
+ await rm(join7(paths.tokens, "elevation.rafters.json"), { force: true });
29697
+ await rm(join7(paths.tokens, "fill.rafters.json"), { force: true });
29698
+ const registry2 = loadRegistryFromDir(paths.tokens, REGISTRY_PLUGINS);
29699
+ if (registry2.size() === 0) {
29700
+ throw new Error("No tokens found. Cannot regenerate without existing tokens.");
29690
29701
  }
29702
+ const namespaces = [...new Set(registry2.list().map((t) => t.namespace))];
29691
29703
  log({
29692
- event: "add:start",
29693
- cwd,
29694
- components,
29695
- overwrite: options.overwrite ?? false
29704
+ event: "init:loaded",
29705
+ tokenCount: registry2.size(),
29706
+ namespaces
29696
29707
  });
29697
- const allItems = [];
29698
- const seen = /* @__PURE__ */ new Set();
29699
- for (const itemName of components) {
29700
- try {
29701
- if (folder === "composites") {
29702
- if (!seen.has(itemName)) {
29703
- const composite = await client.fetchComposite(itemName);
29704
- seen.add(itemName);
29705
- allItems.push(composite);
29706
- for (const dep of composite.primitives) {
29707
- if (!seen.has(dep)) {
29708
- const depItems = await client.resolveDependencies(dep, seen);
29709
- allItems.push(...depItems);
29710
- }
29711
- }
29712
- }
29713
- } else {
29714
- const items = await client.resolveDependencies(itemName, seen);
29715
- allItems.push(...items);
29716
- }
29717
- } catch (err) {
29718
- if (err instanceof Error) {
29719
- error46(err.message);
29720
- } else {
29721
- error46(`Failed to fetch "${itemName}"`);
29722
- }
29723
- process.exitCode = 1;
29724
- return;
29708
+ let exports;
29709
+ if (isAgentMode2) {
29710
+ exports = existingConfig?.exports ?? DEFAULT_EXPORTS;
29711
+ log({ event: "init:exports_default", exports });
29712
+ } else {
29713
+ if (isInteractive()) {
29714
+ log({ event: "init:prompting_exports" });
29725
29715
  }
29716
+ exports = await promptExportFormats(existingConfig?.exports);
29717
+ log({ event: "init:exports_selected", exports });
29726
29718
  }
29727
- const substrateKinds = [
29728
- ...new Set(
29729
- allItems.filter((item) => item.type === "substrate").map((item) => item.files[0]?.path.split("/")[0]).filter((segment) => Boolean(segment))
29730
- )
29731
- ];
29732
- const written = [];
29733
- const skipped = [];
29734
- const failed = [];
29735
- const installedItems = [];
29736
- const filteredItems = [];
29737
- const target = getComponentTarget(config2);
29738
- for (const item of allItems) {
29739
- if (!options.overwrite && isAlreadyInstalled(config2, item)) {
29740
- if (isInstalledOnDisk(config2, item, cwd)) {
29741
- log({
29742
- event: "add:skip",
29743
- component: item.name,
29744
- reason: "already installed"
29745
- });
29746
- skipped.push(item.name);
29747
- continue;
29748
- }
29749
- log({
29750
- event: "add:warning",
29751
- component: item.name,
29752
- message: "Config tracks this as installed but no files found on disk -- reinstalling."
29753
- });
29754
- }
29755
- try {
29756
- const result = await installItem(cwd, item, options, config2, substrateKinds);
29757
- if (result.installed) {
29758
- written.push(item.name);
29759
- installedItems.push(item);
29760
- if (item.type === "ui") {
29761
- const selection = selectFilesForFramework(item.files, target);
29762
- filteredItems.push({ ...item, files: selection.files });
29763
- } else {
29764
- filteredItems.push(item);
29765
- }
29766
- log({
29767
- event: "add:installed",
29768
- component: item.name,
29769
- type: item.type,
29770
- files: result.files
29771
- });
29772
- }
29773
- if (result.skipped && !result.installed) {
29774
- skipped.push(item.name);
29775
- installedItems.push(item);
29776
- }
29777
- } catch (err) {
29778
- failed.push(item.name);
29779
- if (err instanceof Error) {
29780
- log({
29781
- event: "add:warning",
29782
- component: item.name,
29783
- message: err.message
29784
- });
29785
- }
29786
- }
29719
+ await mkdir2(paths.output, { recursive: true });
29720
+ const outputs = await generateOutputs(
29721
+ cwd,
29722
+ paths,
29723
+ registry2,
29724
+ exports,
29725
+ source,
29726
+ "class",
29727
+ existingConfig
29728
+ );
29729
+ if (existingConfig) {
29730
+ existingConfig.exports = exports;
29731
+ await writeFile2(paths.config, JSON.stringify(existingConfig, null, 2));
29732
+ } else {
29733
+ const frameworkPaths = FRAMEWORK_SPECS[framework].components;
29734
+ const newConfig = {
29735
+ framework,
29736
+ componentsPath: frameworkPaths.components,
29737
+ primitivesPath: frameworkPaths.primitives,
29738
+ compositesPath: frameworkPaths.composites,
29739
+ rulesPath: frameworkPaths.rules,
29740
+ cssPath: null,
29741
+ ...source ? { source } : {},
29742
+ intent: "efficient",
29743
+ fonts: { path: null, imports: [] },
29744
+ exports,
29745
+ installed: { components: [], primitives: [], composites: [], rules: [] }
29746
+ };
29747
+ await writeFile2(paths.config, JSON.stringify(newConfig, null, 2));
29787
29748
  }
29788
- const emptyDeps = {
29789
- installed: [],
29790
- skipped: [],
29791
- devInstalled: [],
29792
- failed: []
29793
- };
29794
- let depsResult = emptyDeps;
29749
+ log({
29750
+ event: "init:complete",
29751
+ outputs,
29752
+ path: paths.output
29753
+ });
29754
+ }
29755
+ async function resetToDefaults(cwd, paths, source, isAgentMode2, framework) {
29756
+ log({ event: "init:reset", cwd });
29757
+ let existingConfig = null;
29795
29758
  try {
29796
- depsResult = await installRegistryDependencies(filteredItems, cwd, { target });
29797
- } catch (err) {
29798
- const message = err instanceof Error ? err.message : String(err);
29799
- log({
29800
- event: "add:deps:install-failed",
29801
- message: `Failed to process dependencies: ${message}`,
29802
- dependencies: [],
29803
- suggestion: "Check package.json and try installing dependencies manually."
29804
- });
29759
+ const configContent = await readFile3(paths.config, "utf-8");
29760
+ existingConfig = migrateConfig(
29761
+ JSON.parse(configContent)
29762
+ );
29763
+ } catch {
29805
29764
  }
29806
- if (depsResult.installed.length > 0 || depsResult.skipped.length > 0) {
29765
+ if (framework !== "unknown" && existingConfig) {
29766
+ const frameworkPaths = FRAMEWORK_SPECS[framework].components;
29767
+ existingConfig.framework = framework;
29768
+ existingConfig.componentsPath = frameworkPaths.components;
29769
+ existingConfig.primitivesPath = frameworkPaths.primitives;
29770
+ existingConfig.compositesPath = frameworkPaths.composites;
29771
+ existingConfig.rulesPath = frameworkPaths.rules;
29772
+ }
29773
+ let existingTokens = [];
29774
+ try {
29775
+ existingTokens = loadRegistryFromDir(paths.tokens, REGISTRY_PLUGINS).list();
29776
+ } catch {
29777
+ }
29778
+ const overriddenTokens = existingTokens.filter((t) => t.userOverride);
29779
+ if (overriddenTokens.length > 0) {
29780
+ const backup = {
29781
+ resetAt: (/* @__PURE__ */ new Date()).toISOString(),
29782
+ reason: "rafters init --reset",
29783
+ overrides: overriddenTokens.map((t) => ({
29784
+ name: t.name,
29785
+ value: t.value,
29786
+ userOverride: t.userOverride,
29787
+ namespace: t.namespace
29788
+ }))
29789
+ };
29790
+ await mkdir2(paths.output, { recursive: true });
29791
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
29792
+ const backupPath = join7(paths.output, `reset-${timestamp}.json`);
29793
+ await writeFile2(backupPath, JSON.stringify(backup, null, 2));
29807
29794
  log({
29808
- event: "add:dependencies",
29809
- dependencies: depsResult.installed,
29810
- devDependencies: depsResult.devInstalled,
29811
- skipped: depsResult.skipped
29795
+ event: "init:reset_backup",
29796
+ path: backupPath,
29797
+ overrideCount: overriddenTokens.length
29812
29798
  });
29813
29799
  }
29814
- if (installedItems.length > 0 && config2) {
29815
- trackInstalled(config2, installedItems);
29816
- await saveConfig(cwd, config2);
29817
- await regenerateAfterInstall(cwd, config2);
29818
- } else if (installedItems.length > 0 && !config2) {
29800
+ let exports;
29801
+ if (isAgentMode2) {
29802
+ exports = existingConfig?.exports ?? DEFAULT_EXPORTS;
29803
+ log({ event: "init:exports_default", exports });
29804
+ } else {
29805
+ if (isInteractive()) {
29806
+ log({ event: "init:prompting_exports" });
29807
+ }
29808
+ exports = await promptExportFormats(existingConfig?.exports);
29809
+ log({ event: "init:exports_selected", exports });
29810
+ }
29811
+ const system = generateBaseSystem({});
29812
+ const registry2 = new TokenRegistry(system.allTokens, REGISTRY_PLUGINS);
29813
+ log({
29814
+ event: "init:reset_generated",
29815
+ tokenCount: registry2.size()
29816
+ });
29817
+ await rm(paths.tokens, { recursive: true, force: true });
29818
+ await mkdir2(paths.tokens, { recursive: true });
29819
+ saveRegistryToDir(paths.tokens, registry2);
29820
+ const allTokensToSave = registry2.list();
29821
+ const namespaceCount = new Set(allTokensToSave.map((t) => t.namespace)).size;
29822
+ log({
29823
+ event: "init:registry_saved",
29824
+ path: paths.tokens,
29825
+ namespaceCount
29826
+ });
29827
+ await mkdir2(paths.output, { recursive: true });
29828
+ const outputs = await generateOutputs(
29829
+ cwd,
29830
+ paths,
29831
+ registry2,
29832
+ exports,
29833
+ source,
29834
+ "class",
29835
+ existingConfig
29836
+ );
29837
+ if (existingConfig) {
29838
+ existingConfig.exports = exports;
29839
+ await writeFile2(paths.config, JSON.stringify(existingConfig, null, 2));
29840
+ } else {
29841
+ const frameworkPaths = FRAMEWORK_SPECS[framework].components;
29819
29842
  const newConfig = {
29820
- framework: "unknown",
29821
- componentsPath: "components/ui",
29822
- primitivesPath: "lib/primitives",
29823
- compositesPath: "composites",
29824
- rulesPath: "lib/rules",
29843
+ framework,
29844
+ componentsPath: frameworkPaths.components,
29845
+ primitivesPath: frameworkPaths.primitives,
29846
+ compositesPath: frameworkPaths.composites,
29847
+ rulesPath: frameworkPaths.rules,
29825
29848
  cssPath: null,
29826
- shadcn: false,
29827
- exports: DEFAULT_EXPORTS,
29849
+ ...source ? { source } : {},
29850
+ intent: "efficient",
29851
+ fonts: { path: null, imports: [] },
29852
+ exports,
29828
29853
  installed: { components: [], primitives: [], composites: [], rules: [] }
29829
29854
  };
29830
- trackInstalled(newConfig, installedItems);
29831
- await saveConfig(cwd, newConfig);
29832
- await regenerateAfterInstall(cwd, newConfig);
29855
+ await writeFile2(paths.config, JSON.stringify(newConfig, null, 2));
29833
29856
  }
29834
29857
  log({
29835
- event: "add:complete",
29836
- written: written.length,
29837
- skipped: skipped.length,
29838
- untracked: untrackedNames.length,
29839
- failed: failed.length,
29840
- components: written,
29841
- skippedComponents: skipped,
29842
- untrackedComponents: untrackedNames,
29843
- failedComponents: failed
29858
+ event: "init:complete",
29859
+ outputs,
29860
+ path: paths.output
29844
29861
  });
29845
- if (failed.length > 0) {
29846
- process.exitCode = 1;
29847
- }
29848
- if (!options.updateAll && skipped.length > 0 && written.length === 0) {
29862
+ }
29863
+ async function init(options) {
29864
+ setAgentMode(options.agent ?? false);
29865
+ const isAgentMode2 = options.agent ?? false;
29866
+ const cwd = process.cwd();
29867
+ const paths = getRaftersPaths(cwd);
29868
+ log({ event: "init:start", cwd });
29869
+ const project = await detectProject(cwd);
29870
+ const { framework: detectedFramework, shadcn, tailwindVersion, astroHasReact } = project;
29871
+ const source = shadcn ? "shadcn" : void 0;
29872
+ log({
29873
+ event: "init:detected",
29874
+ framework: detectedFramework,
29875
+ tailwindVersion,
29876
+ hasShadcn: !!shadcn
29877
+ });
29878
+ const framework = await resolveFramework(detectedFramework, options.framework, isAgentMode2);
29879
+ if (framework !== detectedFramework) {
29849
29880
  log({
29850
- event: "add:hint",
29851
- message: "Some components were skipped. Use --update to re-fetch, or --update-all to refresh everything.",
29852
- skipped
29881
+ event: "init:framework_resolved",
29882
+ detected: detectedFramework,
29883
+ resolved: framework,
29884
+ source: options.framework ? "flag" : "prompt"
29853
29885
  });
29854
- error46("Component already exists. Use --update to re-fetch from registry.");
29855
- process.exitCode = 1;
29856
29886
  }
29857
- }
29858
-
29859
- // src/commands/init.ts
29860
- import { existsSync as existsSync4 } from "fs";
29861
- import { copyFile, mkdir as mkdir3, readFile as readFile4, rm, writeFile as writeFile3 } from "fs/promises";
29862
- import { createRequire } from "module";
29863
- import { join as join8, relative as relative2 } from "path";
29864
- import { checkbox, confirm, select } from "@inquirer/prompts";
29865
- var REGISTRY_PLUGINS2 = [scalePlugin, contrastPlugin, statePlugin, invertPlugin];
29866
- async function backupCss(cssPath) {
29867
- const backupPath = cssPath.replace(/\.css$/, ".backup.css");
29868
- await copyFile(cssPath, backupPath);
29869
- return backupPath;
29870
- }
29871
- function cleanSourceCssBlocks(content) {
29872
- function parseBlocks(src) {
29873
- const blocks = [];
29874
- let depth = 0;
29875
- const headerStack = [];
29876
- let segmentStart = 0;
29877
- for (let i = 0; i < src.length; i++) {
29878
- const ch = src.charAt(i);
29879
- if (ch === "{") {
29880
- headerStack.push(segmentStart);
29881
- depth++;
29882
- if (depth >= 1) {
29883
- blocks.push({
29884
- headerStart: segmentStart,
29885
- bodyStart: i + 1,
29886
- end: -1,
29887
- header: src.slice(segmentStart, i).trim()
29888
- });
29889
- }
29890
- segmentStart = i + 1;
29891
- } else if (ch === "}") {
29892
- depth--;
29893
- const open = blocks.slice().reverse().find((b2) => b2.end === -1);
29894
- if (open) open.end = i + 1;
29895
- headerStack.pop();
29896
- segmentStart = i + 1;
29897
- } else if (ch === ";") {
29898
- segmentStart = i + 1;
29899
- }
29900
- }
29901
- return blocks.filter((b2) => b2.end !== -1);
29887
+ if (isTailwindV3(tailwindVersion)) {
29888
+ throw new Error("Tailwind v3 detected. Rafters requires Tailwind v4.");
29902
29889
  }
29903
- function isContentEmpty(body) {
29904
- return body.replace(/\/\*[\s\S]*?\*\//g, "").trim().length === 0;
29890
+ const raftersExists = existsSync3(paths.root);
29891
+ if (options.reset && !raftersExists) {
29892
+ throw new Error("Nothing to reset. No .rafters/ directory found.");
29905
29893
  }
29906
- let out = content;
29907
- for (let pass = 0; pass < 10; pass++) {
29908
- const blocks = parseBlocks(out);
29909
- let edited = false;
29910
- for (const block of blocks.sort((a2, b2) => b2.headerStart - a2.headerStart)) {
29911
- const header = block.header;
29912
- if (/^@theme\s+inline$/.test(header)) {
29913
- out = out.slice(0, block.headerStart) + out.slice(block.end);
29914
- edited = true;
29915
- break;
29916
- }
29917
- if (header === ":root" || /(^|,)\s*:root\s*$/.test(header)) {
29918
- const body = out.slice(block.bodyStart, block.end - 1);
29919
- const cleanedBody = body.replace(/--[\w-]+\s*:[^;{}]*;?/g, "");
29920
- if (isContentEmpty(cleanedBody)) {
29921
- out = out.slice(0, block.headerStart) + out.slice(block.end);
29922
- edited = true;
29923
- break;
29924
- }
29925
- if (cleanedBody !== body) {
29926
- out = out.slice(0, block.bodyStart) + cleanedBody + out.slice(block.end - 1);
29927
- edited = true;
29928
- break;
29929
- }
29930
- continue;
29931
- }
29932
- if (header.startsWith("@media") || header.startsWith("@supports")) {
29933
- const body = out.slice(block.bodyStart, block.end - 1);
29934
- if (isContentEmpty(body)) {
29935
- out = out.slice(0, block.headerStart) + out.slice(block.end);
29936
- edited = true;
29937
- break;
29938
- }
29939
- }
29940
- }
29941
- if (!edited) break;
29894
+ if (raftersExists && options.reset) {
29895
+ await resetToDefaults(cwd, paths, source, isAgentMode2, framework);
29896
+ return;
29942
29897
  }
29943
- return out.replace(/[ \t]+$/gm, "").replace(/\n{3,}/g, "\n\n");
29944
- }
29945
- async function stripImportedDeclarations(cwd, cssPath, importedNames) {
29946
- if (importedNames.length === 0) return;
29947
- const fullPath = join8(cwd, cssPath);
29948
- const content = await readFile4(fullPath, "utf-8");
29949
- const pattern = new RegExp(importedNames.map((n2) => `^\\s*--${n2}[^;]*;\\s*$`).join("|"), "gm");
29950
- const cleaned = cleanSourceCssBlocks(content.replace(pattern, ""));
29951
- const collapsed = cleaned.replace(/\n{3,}/g, "\n\n");
29952
- await writeFile3(fullPath, collapsed);
29953
- }
29954
- async function updateMainCss(cwd, cssPath, themePath) {
29955
- const fullCssPath = join8(cwd, cssPath);
29956
- const cssContent = await readFile4(fullCssPath, "utf-8");
29957
- const cssDir = join8(cwd, cssPath, "..");
29958
- const themeFullPath = join8(cwd, themePath);
29959
- const relativeThemePath = relative2(cssDir, themeFullPath);
29960
- if (cssContent.includes(".rafters/output/rafters.css")) {
29961
- log({ event: "init:css_already_imported", cssPath });
29898
+ if (raftersExists && !options.rebuild) {
29899
+ throw new Error(
29900
+ ".rafters/ directory already exists. Use --rebuild to regenerate output files, or --reset to start from defaults."
29901
+ );
29902
+ }
29903
+ if (raftersExists && options.rebuild) {
29904
+ await regenerateFromExisting(cwd, paths, source, isAgentMode2, framework);
29962
29905
  return;
29963
29906
  }
29964
- await backupCss(fullCssPath);
29965
- let newContent;
29966
- if (cssContent.includes('@import "tailwindcss"')) {
29967
- newContent = cssContent.replace('@import "tailwindcss";', `@import "${relativeThemePath}";`);
29968
- } else if (cssContent.includes("@import 'tailwindcss'")) {
29969
- newContent = cssContent.replace("@import 'tailwindcss';", `@import "${relativeThemePath}";`);
29907
+ let exports;
29908
+ if (isAgentMode2) {
29909
+ exports = DEFAULT_EXPORTS;
29910
+ log({ event: "init:exports_default", exports });
29970
29911
  } else {
29971
- newContent = `@import "${relativeThemePath}";
29972
-
29973
- ${cssContent}`;
29974
- }
29975
- await writeFile3(fullCssPath, newContent);
29976
- log({
29977
- event: "init:css_updated",
29978
- cssPath,
29979
- themePath: relativeThemePath
29980
- });
29981
- }
29982
- function isInteractive() {
29983
- return Boolean(process.stdin.isTTY && process.stdout.isTTY);
29984
- }
29985
- function resolveSourceCssPath(cwd, framework, detectedFramework, projectCssPath, shadcnCssPath, emit) {
29986
- if (shadcnCssPath) {
29987
- if (existsSync4(join8(cwd, shadcnCssPath))) return shadcnCssPath;
29988
- const fallback = framework === detectedFramework ? projectCssPath : findCssPath(cwd, framework);
29989
- emit({
29990
- event: "init:shadcn_css_missing",
29991
- configuredPath: shadcnCssPath,
29992
- fallbackPath: fallback,
29993
- message: "components.json points at a CSS file that does not exist. Falling back to the framework canonical location."
29994
- });
29995
- return fallback;
29912
+ if (isInteractive()) {
29913
+ log({ event: "init:prompting_exports" });
29914
+ }
29915
+ exports = await promptExportFormats();
29916
+ log({ event: "init:exports_selected", exports });
29996
29917
  }
29997
- return framework === detectedFramework ? projectCssPath : findCssPath(cwd, framework);
29998
- }
29999
- function slugifyFontName(name) {
30000
- return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
30001
- }
30002
- function isFontStackValue(value) {
30003
- if (typeof value !== "string") return false;
30004
- if (value.startsWith("var(")) return false;
30005
- if (/[,'"]/.test(value)) return true;
30006
- return /\b(sans-serif|serif|monospace|cursive|fantasy|system-ui|ui-(?:sans-serif|serif|monospace|rounded))\b/.test(
30007
- value
29918
+ const adapter = getAdapter(source ?? "tailwind");
29919
+ const baseConfig = {};
29920
+ const sourceCssPathForBase = resolveSourceCssPath(
29921
+ cwd,
29922
+ framework,
29923
+ detectedFramework,
29924
+ project.cssPath,
29925
+ shadcn?.tailwind?.css,
29926
+ log
30008
29927
  );
30009
- }
30010
- async function resolveFramework(detected, flag, agentMode) {
30011
- if (flag) {
30012
- if (!isSelectableFramework(flag)) {
30013
- throw new Error(
30014
- `Unknown --framework "${flag}". Valid values: ${SELECTABLE_FRAMEWORKS.join(", ")}.`
30015
- );
29928
+ if (sourceCssPathForBase !== null) {
29929
+ try {
29930
+ const cssForBase = await readFile3(join7(cwd, sourceCssPathForBase), "utf-8");
29931
+ const baseSlots = [
29932
+ {
29933
+ detect: () => adapter.detectSpacing(cssForBase).base,
29934
+ field: "baseSpacingUnit",
29935
+ event: "init:import_spacing_applied",
29936
+ eventKey: "baseSpacingUnit"
29937
+ },
29938
+ {
29939
+ detect: () => adapter.detectRadius(cssForBase).base,
29940
+ field: "baseRadiusOverride",
29941
+ event: "init:import_radius_applied",
29942
+ eventKey: "baseRadius"
29943
+ },
29944
+ {
29945
+ detect: () => adapter.detectFontSize(cssForBase).base,
29946
+ field: "baseFontSizeOverride",
29947
+ event: "init:import_font_size_applied",
29948
+ eventKey: "baseFontSize"
29949
+ },
29950
+ {
29951
+ detect: () => adapter.detectFocusRing(cssForBase).width,
29952
+ field: "focusRingWidthOverride",
29953
+ event: "init:import_focus_ring_applied",
29954
+ eventKey: "focusRingWidth"
29955
+ }
29956
+ // Motion duration is intentionally not imported -- rafters' motion
29957
+ // system is research-backed and most projects' `--duration-base`
29958
+ // values are unresearched defaults. See importers/bases.ts for the
29959
+ // policy note; designers can `rafters set motion-duration-base ...`
29960
+ // after init to override explicitly.
29961
+ ];
29962
+ for (const slot of baseSlots) {
29963
+ const value = slot.detect();
29964
+ if (value === void 0) continue;
29965
+ baseConfig[slot.field] = value;
29966
+ log({
29967
+ event: slot.event,
29968
+ cssPath: sourceCssPathForBase,
29969
+ [slot.eventKey]: value
29970
+ });
29971
+ }
29972
+ } catch (err) {
29973
+ if (!(err instanceof Error && "code" in err && err.code === "ENOENT")) throw err;
30016
29974
  }
30017
- return flag;
30018
29975
  }
30019
- if (detected !== "unknown") return detected;
30020
- if (agentMode || !isInteractive()) return "unknown";
30021
- const picked = await select({
30022
- message: "Couldn't auto-detect your framework. Which one is this?",
30023
- choices: SELECTABLE_FRAMEWORKS.map((value) => ({
30024
- name: FRAMEWORK_SPECS[value].label ?? value,
30025
- value
30026
- }))
29976
+ const system = generateBaseSystem(baseConfig);
29977
+ const registry2 = new TokenRegistry(system.allTokens, REGISTRY_PLUGINS);
29978
+ log({
29979
+ event: "init:generated",
29980
+ tokenCount: registry2.size()
30027
29981
  });
30028
- return picked;
30029
- }
30030
- async function promptExportFormats(existingConfig) {
30031
- if (!isInteractive()) {
30032
- return existingConfig ?? DEFAULT_EXPORTS;
30033
- }
30034
- const choices = EXPORT_CHOICES.map((choice) => ({
30035
- name: choice.name,
30036
- value: choice.value,
30037
- checked: existingConfig ? existingConfig[choice.value] : choice.checked
30038
- }));
30039
- const allChoices = [
30040
- ...choices,
30041
- ...FUTURE_EXPORTS.map((choice) => ({
30042
- name: `${choice.name} (${choice.disabled})`,
30043
- value: choice.value,
30044
- checked: false,
30045
- disabled: true
30046
- }))
30047
- ];
30048
- const selections = await checkbox({
30049
- message: "What would you like to export?",
30050
- choices: allChoices,
30051
- required: true
30052
- });
30053
- return selectionsToConfig(selections);
30054
- }
30055
- function isTailwindCliInstalled() {
30056
- const require2 = createRequire(import.meta.url);
30057
- try {
30058
- require2.resolve("@tailwindcss/cli/package.json");
30059
- return true;
30060
- } catch (err) {
30061
- if (err instanceof Error && "code" in err && err.code === "MODULE_NOT_FOUND") {
30062
- return false;
30063
- }
30064
- throw err;
30065
- }
30066
- }
30067
- async function ensureTailwindCli(cwd) {
30068
- if (!isInteractive() || isAgentMode()) {
30069
- throw new Error(
30070
- "Standalone CSS export requires @tailwindcss/cli. Install it as a dev dependency in your project."
30071
- );
30072
- }
30073
- const shouldInstall = await confirm({
30074
- message: "Standalone CSS requires @tailwindcss/cli. Install it now?",
30075
- default: true
30076
- });
30077
- if (!shouldInstall) {
30078
- throw new Error("Standalone CSS export requires @tailwindcss/cli.");
30079
- }
30080
- await updateDependencies([], ["@tailwindcss/cli"], { cwd });
30081
- if (!isTailwindCliInstalled()) {
30082
- throw new Error(
30083
- "@tailwindcss/cli was installed but cannot be resolved. Try installing at the workspace root."
30084
- );
30085
- }
30086
- }
30087
- async function generateOutputs(cwd, paths, registry2, exports, shadcn, darkMode = "class", config2) {
30088
- const contentSources = config2 ? resolveContentSources(cwd, config2) : [];
30089
- if (exports.compiled && !isTailwindCliInstalled()) {
30090
- log({ event: "init:prompting_exports" });
30091
- await ensureTailwindCli(cwd);
30092
- }
30093
- if (exports.compiled) {
30094
- log({ event: "init:compiling_css" });
30095
- }
30096
- return regenerateOutputs(registry2, {
30097
- outputDir: paths.output,
30098
- exports,
30099
- contentSources,
30100
- darkMode,
30101
- includeImport: !shadcn
30102
- });
30103
- }
30104
- async function regenerateFromExisting(cwd, paths, shadcn, isAgentMode2, framework) {
30105
- log({ event: "init:regenerate", cwd });
30106
- let existingConfig = null;
30107
- try {
30108
- const configContent = await readFile4(paths.config, "utf-8");
30109
- existingConfig = JSON.parse(configContent);
30110
- } catch {
30111
- }
30112
- if (framework !== "unknown" && existingConfig) {
30113
- const frameworkPaths = FRAMEWORK_SPECS[framework].components;
30114
- existingConfig.framework = framework;
30115
- existingConfig.componentsPath = frameworkPaths.components;
30116
- existingConfig.primitivesPath = frameworkPaths.primitives;
30117
- existingConfig.compositesPath = frameworkPaths.composites;
30118
- existingConfig.rulesPath = frameworkPaths.rules;
30119
- }
30120
- await rm(join8(paths.tokens, "elevation.rafters.json"), { force: true });
30121
- await rm(join8(paths.tokens, "fill.rafters.json"), { force: true });
30122
- const registry2 = loadRegistryFromDir(paths.tokens, REGISTRY_PLUGINS2);
30123
- if (registry2.size() === 0) {
30124
- throw new Error("No tokens found. Cannot regenerate without existing tokens.");
30125
- }
30126
- const namespaces = [...new Set(registry2.list().map((t) => t.namespace))];
30127
- log({
30128
- event: "init:loaded",
30129
- tokenCount: registry2.size(),
30130
- namespaces
30131
- });
30132
- let exports;
30133
- if (isAgentMode2) {
30134
- exports = existingConfig?.exports ?? DEFAULT_EXPORTS;
30135
- log({ event: "init:exports_default", exports });
30136
- } else {
30137
- if (isInteractive()) {
30138
- log({ event: "init:prompting_exports" });
30139
- }
30140
- exports = await promptExportFormats(existingConfig?.exports);
30141
- log({ event: "init:exports_selected", exports });
30142
- }
30143
- await mkdir3(paths.output, { recursive: true });
30144
- const outputs = await generateOutputs(
30145
- cwd,
30146
- paths,
30147
- registry2,
30148
- exports,
30149
- shadcn,
30150
- "class",
30151
- existingConfig
30152
- );
30153
- if (existingConfig) {
30154
- existingConfig.exports = exports;
30155
- await writeFile3(paths.config, JSON.stringify(existingConfig, null, 2));
30156
- } else {
30157
- const frameworkPaths = FRAMEWORK_SPECS[framework].components;
30158
- const newConfig = {
30159
- framework,
30160
- componentsPath: frameworkPaths.components,
30161
- primitivesPath: frameworkPaths.primitives,
30162
- compositesPath: frameworkPaths.composites,
30163
- rulesPath: frameworkPaths.rules,
30164
- cssPath: null,
30165
- shadcn: !!shadcn,
30166
- exports,
30167
- installed: { components: [], primitives: [], composites: [], rules: [] }
30168
- };
30169
- await writeFile3(paths.config, JSON.stringify(newConfig, null, 2));
30170
- }
30171
- log({
30172
- event: "init:complete",
30173
- outputs,
30174
- path: paths.output
30175
- });
30176
- }
30177
- async function resetToDefaults(cwd, paths, shadcn, isAgentMode2, framework) {
30178
- log({ event: "init:reset", cwd });
30179
- let existingConfig = null;
30180
- try {
30181
- const configContent = await readFile4(paths.config, "utf-8");
30182
- existingConfig = JSON.parse(configContent);
30183
- } catch {
30184
- }
30185
- if (framework !== "unknown" && existingConfig) {
30186
- const frameworkPaths = FRAMEWORK_SPECS[framework].components;
30187
- existingConfig.framework = framework;
30188
- existingConfig.componentsPath = frameworkPaths.components;
30189
- existingConfig.primitivesPath = frameworkPaths.primitives;
30190
- existingConfig.compositesPath = frameworkPaths.composites;
30191
- existingConfig.rulesPath = frameworkPaths.rules;
30192
- }
30193
- let existingTokens = [];
30194
- try {
30195
- existingTokens = loadRegistryFromDir(paths.tokens, REGISTRY_PLUGINS2).list();
30196
- } catch {
30197
- }
30198
- const overriddenTokens = existingTokens.filter((t) => t.userOverride);
30199
- if (overriddenTokens.length > 0) {
30200
- const backup = {
30201
- resetAt: (/* @__PURE__ */ new Date()).toISOString(),
30202
- reason: "rafters init --reset",
30203
- overrides: overriddenTokens.map((t) => ({
30204
- name: t.name,
30205
- value: t.value,
30206
- userOverride: t.userOverride,
30207
- namespace: t.namespace
30208
- }))
30209
- };
30210
- await mkdir3(paths.output, { recursive: true });
30211
- const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
30212
- const backupPath = join8(paths.output, `reset-${timestamp}.json`);
30213
- await writeFile3(backupPath, JSON.stringify(backup, null, 2));
30214
- log({
30215
- event: "init:reset_backup",
30216
- path: backupPath,
30217
- overrideCount: overriddenTokens.length
30218
- });
30219
- }
30220
- let exports;
30221
- if (isAgentMode2) {
30222
- exports = existingConfig?.exports ?? DEFAULT_EXPORTS;
30223
- log({ event: "init:exports_default", exports });
30224
- } else {
30225
- if (isInteractive()) {
30226
- log({ event: "init:prompting_exports" });
30227
- }
30228
- exports = await promptExportFormats(existingConfig?.exports);
30229
- log({ event: "init:exports_selected", exports });
30230
- }
30231
- const system = generateBaseSystem({});
30232
- const registry2 = new TokenRegistry(system.allTokens, REGISTRY_PLUGINS2);
30233
- log({
30234
- event: "init:reset_generated",
30235
- tokenCount: registry2.size()
30236
- });
30237
- await rm(paths.tokens, { recursive: true, force: true });
30238
- await mkdir3(paths.tokens, { recursive: true });
29982
+ await mkdir2(paths.tokens, { recursive: true });
29983
+ await mkdir2(paths.output, { recursive: true });
30239
29984
  saveRegistryToDir(paths.tokens, registry2);
30240
29985
  const allTokensToSave = registry2.list();
30241
29986
  const namespaceCount = new Set(allTokensToSave.map((t) => t.namespace)).size;
@@ -30244,183 +29989,26 @@ async function resetToDefaults(cwd, paths, shadcn, isAgentMode2, framework) {
30244
29989
  path: paths.tokens,
30245
29990
  namespaceCount
30246
29991
  });
30247
- await mkdir3(paths.output, { recursive: true });
30248
- const outputs = await generateOutputs(
29992
+ const outputs = await generateOutputs(cwd, paths, registry2, exports, source);
29993
+ let detectedCssPath = null;
29994
+ detectedCssPath = resolveSourceCssPath(
30249
29995
  cwd,
30250
- paths,
30251
- registry2,
30252
- exports,
30253
- shadcn,
30254
- "class",
30255
- existingConfig
29996
+ framework,
29997
+ detectedFramework,
29998
+ project.cssPath,
29999
+ shadcn?.tailwind?.css,
30000
+ log
30256
30001
  );
30257
- if (existingConfig) {
30258
- existingConfig.exports = exports;
30259
- await writeFile3(paths.config, JSON.stringify(existingConfig, null, 2));
30260
- } else {
30261
- const frameworkPaths = FRAMEWORK_SPECS[framework].components;
30262
- const newConfig = {
30263
- framework,
30264
- componentsPath: frameworkPaths.components,
30265
- primitivesPath: frameworkPaths.primitives,
30266
- compositesPath: frameworkPaths.composites,
30267
- rulesPath: frameworkPaths.rules,
30268
- cssPath: null,
30269
- shadcn: !!shadcn,
30270
- exports,
30271
- installed: { components: [], primitives: [], composites: [], rules: [] }
30272
- };
30273
- await writeFile3(paths.config, JSON.stringify(newConfig, null, 2));
30274
- }
30275
- log({
30276
- event: "init:complete",
30277
- outputs,
30278
- path: paths.output
30279
- });
30280
- }
30281
- async function init(options) {
30282
- setAgentMode(options.agent ?? false);
30283
- const isAgentMode2 = options.agent ?? false;
30284
- const cwd = process.cwd();
30285
- const paths = getRaftersPaths(cwd);
30286
- log({ event: "init:start", cwd });
30287
- const project = await detectProject(cwd);
30288
- const { framework: detectedFramework, shadcn, tailwindVersion, astroHasReact } = project;
30289
- log({
30290
- event: "init:detected",
30291
- framework: detectedFramework,
30292
- tailwindVersion,
30293
- hasShadcn: !!shadcn
30294
- });
30295
- const framework = await resolveFramework(detectedFramework, options.framework, isAgentMode2);
30296
- if (framework !== detectedFramework) {
30297
- log({
30298
- event: "init:framework_resolved",
30299
- detected: detectedFramework,
30300
- resolved: framework,
30301
- source: options.framework ? "flag" : "prompt"
30302
- });
30303
- }
30304
- if (isTailwindV3(tailwindVersion)) {
30305
- throw new Error("Tailwind v3 detected. Rafters requires Tailwind v4.");
30306
- }
30307
- const raftersExists = existsSync4(paths.root);
30308
- if (options.reset && !raftersExists) {
30309
- throw new Error("Nothing to reset. No .rafters/ directory found.");
30310
- }
30311
- if (raftersExists && options.reset) {
30312
- await resetToDefaults(cwd, paths, shadcn, isAgentMode2, framework);
30313
- return;
30314
- }
30315
- if (raftersExists && !options.rebuild) {
30316
- throw new Error(
30317
- ".rafters/ directory already exists. Use --rebuild to regenerate output files, or --reset to start from defaults."
30318
- );
30319
- }
30320
- if (raftersExists && options.rebuild) {
30321
- await regenerateFromExisting(cwd, paths, shadcn, isAgentMode2, framework);
30322
- return;
30323
- }
30324
- let exports;
30325
- if (isAgentMode2) {
30326
- exports = DEFAULT_EXPORTS;
30327
- log({ event: "init:exports_default", exports });
30328
- } else {
30329
- if (isInteractive()) {
30330
- log({ event: "init:prompting_exports" });
30331
- }
30332
- exports = await promptExportFormats();
30333
- log({ event: "init:exports_selected", exports });
30334
- }
30335
- const baseConfig = {};
30336
- const sourceCssPathForBase = resolveSourceCssPath(
30337
- cwd,
30338
- framework,
30339
- detectedFramework,
30340
- project.cssPath,
30341
- shadcn?.tailwind?.css,
30342
- log
30343
- );
30344
- if (sourceCssPathForBase !== null) {
30345
- try {
30346
- const cssForBase = await readFile4(join8(cwd, sourceCssPathForBase), "utf-8");
30347
- const baseSlots = [
30348
- {
30349
- detect: detectSpacingBase,
30350
- field: "baseSpacingUnit",
30351
- event: "init:import_spacing_applied",
30352
- eventKey: "baseSpacingUnit"
30353
- },
30354
- {
30355
- detect: detectRadiusBase,
30356
- field: "baseRadiusOverride",
30357
- event: "init:import_radius_applied",
30358
- eventKey: "baseRadius"
30359
- },
30360
- {
30361
- detect: detectFontSizeBase,
30362
- field: "baseFontSizeOverride",
30363
- event: "init:import_font_size_applied",
30364
- eventKey: "baseFontSize"
30365
- },
30366
- {
30367
- detect: detectFocusRingWidth,
30368
- field: "focusRingWidthOverride",
30369
- event: "init:import_focus_ring_applied",
30370
- eventKey: "focusRingWidth"
30371
- }
30372
- // Motion duration is intentionally not imported -- rafters' motion
30373
- // system is research-backed and most projects' `--duration-base`
30374
- // values are unresearched defaults. See importers/bases.ts for the
30375
- // policy note; designers can `rafters set motion-duration-base ...`
30376
- // after init to override explicitly.
30377
- ];
30378
- for (const slot of baseSlots) {
30379
- const value = slot.detect(cssForBase);
30380
- if (value === null) continue;
30381
- baseConfig[slot.field] = value;
30382
- log({ event: slot.event, cssPath: sourceCssPathForBase, [slot.eventKey]: value });
30383
- }
30384
- } catch (err) {
30385
- if (!(err instanceof Error && "code" in err && err.code === "ENOENT")) throw err;
30386
- }
30387
- }
30388
- const system = generateBaseSystem(baseConfig);
30389
- const registry2 = new TokenRegistry(system.allTokens, REGISTRY_PLUGINS2);
30390
- log({
30391
- event: "init:generated",
30392
- tokenCount: registry2.size()
30393
- });
30394
- await mkdir3(paths.tokens, { recursive: true });
30395
- await mkdir3(paths.output, { recursive: true });
30396
- saveRegistryToDir(paths.tokens, registry2);
30397
- const allTokensToSave = registry2.list();
30398
- const namespaceCount = new Set(allTokensToSave.map((t) => t.namespace)).size;
30399
- log({
30400
- event: "init:registry_saved",
30401
- path: paths.tokens,
30402
- namespaceCount
30403
- });
30404
- const outputs = await generateOutputs(cwd, paths, registry2, exports, shadcn);
30405
- let detectedCssPath = null;
30406
- detectedCssPath = resolveSourceCssPath(
30407
- cwd,
30408
- framework,
30409
- detectedFramework,
30410
- project.cssPath,
30411
- shadcn?.tailwind?.css,
30412
- log
30413
- );
30414
- if (!shadcn && exports.tailwind) {
30415
- if (detectedCssPath) {
30416
- await updateMainCss(cwd, detectedCssPath, ".rafters/output/rafters.css");
30417
- } else {
30418
- log({
30419
- event: "init:css_not_found",
30420
- message: 'No main CSS file found. Add @import ".rafters/output/rafters.css" manually.',
30421
- searchedLocations: FRAMEWORK_SPECS[framework].cssLocations
30422
- });
30423
- }
30002
+ if (source !== "shadcn" && exports.tailwind) {
30003
+ if (detectedCssPath) {
30004
+ await updateMainCss(cwd, detectedCssPath, ".rafters/output/rafters.css");
30005
+ } else {
30006
+ log({
30007
+ event: "init:css_not_found",
30008
+ message: 'No main CSS file found. Add @import ".rafters/output/rafters.css" manually.',
30009
+ searchedLocations: FRAMEWORK_SPECS[framework].cssLocations
30010
+ });
30011
+ }
30424
30012
  }
30425
30013
  let componentTarget = frameworkToTarget(framework);
30426
30014
  if (framework === "astro" && astroHasReact && isInteractive() && !isAgentMode2) {
@@ -30441,14 +30029,16 @@ async function init(options) {
30441
30029
  const frameworkPaths = FRAMEWORK_SPECS[framework].components;
30442
30030
  const config2 = {
30443
30031
  framework,
30032
+ ...source ? { source } : {},
30444
30033
  componentTarget,
30445
30034
  componentsPath: frameworkPaths.components,
30446
30035
  primitivesPath: frameworkPaths.primitives,
30447
30036
  compositesPath: frameworkPaths.composites,
30448
30037
  rulesPath: frameworkPaths.rules,
30449
30038
  cssPath: detectedCssPath,
30450
- shadcn: !!shadcn,
30451
30039
  exports,
30040
+ intent: "efficient",
30041
+ fonts: { path: null, imports: [] },
30452
30042
  installed: {
30453
30043
  components: [],
30454
30044
  primitives: [],
@@ -30456,11 +30046,11 @@ async function init(options) {
30456
30046
  rules: []
30457
30047
  }
30458
30048
  };
30459
- await writeFile3(paths.config, JSON.stringify(config2, null, 2));
30049
+ await writeFile2(paths.config, JSON.stringify(config2, null, 2));
30460
30050
  if (detectedCssPath) {
30461
30051
  let sourceCss = null;
30462
30052
  try {
30463
- sourceCss = await readFile4(join8(cwd, detectedCssPath), "utf-8");
30053
+ sourceCss = await readFile3(join7(cwd, detectedCssPath), "utf-8");
30464
30054
  } catch (err) {
30465
30055
  if (!(err instanceof Error && "code" in err && err.code === "ENOENT")) throw err;
30466
30056
  }
@@ -30473,7 +30063,7 @@ async function init(options) {
30473
30063
  ...summary
30474
30064
  });
30475
30065
  const classification = classifyDeclarations(extractShadcnRoot(sourceCss));
30476
- const colorDecls = colorsFromClassification(classification);
30066
+ const colorDecls = adapter.detectColors(sourceCss);
30477
30067
  const nonColorDecls = [
30478
30068
  ...classification.byNamespace.typography,
30479
30069
  ...classification.byNamespace.spacing,
@@ -30517,7 +30107,7 @@ async function init(options) {
30517
30107
  }
30518
30108
  }
30519
30109
  saveRegistryToDir(paths.tokens, registry2);
30520
- await generateOutputs(cwd, paths, registry2, exports, shadcn);
30110
+ await generateOutputs(cwd, paths, registry2, exports, source);
30521
30111
  const importedNames = [
30522
30112
  ...toImportColors.map((c4) => c4.name),
30523
30113
  ...toImportNonColors.map((d2) => d2.name)
@@ -30549,7 +30139,10 @@ async function init(options) {
30549
30139
  familySeeds.set(baseName, { seed: oklch2, seedPosition });
30550
30140
  }
30551
30141
  }
30552
- const families = Array.from(familySeeds, ([name, info]) => ({ name, ...info }));
30142
+ const families = Array.from(familySeeds, ([name, info]) => ({
30143
+ name,
30144
+ ...info
30145
+ }));
30553
30146
  if (families.length > 0) {
30554
30147
  for (const family of families) {
30555
30148
  for (const t of importColorFamily(family.name, family.seedPosition, family.seed)) {
@@ -30592,7 +30185,10 @@ async function init(options) {
30592
30185
  if (!family) continue;
30593
30186
  const position = isAgentMode2 ? family.seedPosition : await select({
30594
30187
  message: `Which position in "${familyChoice}" for "${role}"?`,
30595
- choices: SCALE_POSITIONS.map((p2) => ({ name: p2, value: p2 })),
30188
+ choices: SCALE_POSITIONS.map((p2) => ({
30189
+ name: p2,
30190
+ value: p2
30191
+ })),
30596
30192
  default: family.seedPosition
30597
30193
  });
30598
30194
  registry2.set(
@@ -30605,7 +30201,7 @@ async function init(options) {
30605
30201
  assignments.push({ role, family: familyChoice, position });
30606
30202
  }
30607
30203
  saveRegistryToDir(paths.tokens, registry2);
30608
- await generateOutputs(cwd, paths, registry2, exports, shadcn);
30204
+ await generateOutputs(cwd, paths, registry2, exports, source);
30609
30205
  const themeImportedNames = families.flatMap(
30610
30206
  (f) => SCALE_POSITIONS.map((p2) => `color-${f.name}-${p2}`).concat([`color-${f.name}`])
30611
30207
  );
@@ -30621,7 +30217,7 @@ async function init(options) {
30621
30217
  }
30622
30218
  }
30623
30219
  if (sourceCss !== null) {
30624
- const detectedFonts = detectFonts(sourceCss);
30220
+ const detectedFonts = adapter.detectFonts(sourceCss);
30625
30221
  if (detectedFonts.length > 0) {
30626
30222
  const typographyTokens = registry2.list({ namespace: "typography" });
30627
30223
  const baseFamilyNames = new Set(
@@ -30696,27 +30292,564 @@ async function init(options) {
30696
30292
  registry2.set(base, font.stack, { reason });
30697
30293
  }
30698
30294
  }
30699
- fontAssignments.push({ role, family: font.name });
30295
+ fontAssignments.push({ role, family: font.name });
30296
+ }
30297
+ if (fontAssignments.length > 0 || definedFonts.length > 0) {
30298
+ saveRegistryToDir(paths.tokens, registry2);
30299
+ await generateOutputs(cwd, paths, registry2, exports, source);
30300
+ log({
30301
+ event: "init:import_fonts_applied",
30302
+ cssPath: detectedCssPath,
30303
+ fontsDetected: detectedFonts.map((f) => f.name),
30304
+ assignments: fontAssignments,
30305
+ ...definedFonts.length > 0 ? { definedFonts } : {}
30306
+ });
30307
+ }
30308
+ }
30309
+ }
30310
+ }
30311
+ log({
30312
+ event: "init:complete",
30313
+ outputs: [...outputs, "config.rafters.json"],
30314
+ path: paths.output
30315
+ });
30316
+ }
30317
+
30318
+ // src/commands/add.ts
30319
+ var REGISTRY_PLUGINS2 = [scalePlugin, contrastPlugin, statePlugin, invertPlugin];
30320
+ async function regenerateAfterInstall(cwd, config2) {
30321
+ const paths = getRaftersPaths(cwd);
30322
+ let registry2;
30323
+ try {
30324
+ registry2 = loadRegistryFromDir(paths.tokens, REGISTRY_PLUGINS2);
30325
+ } catch {
30326
+ return;
30327
+ }
30328
+ if (registry2.size() === 0) return;
30329
+ try {
30330
+ await regenerateOutputs(registry2, {
30331
+ outputDir: paths.output,
30332
+ exports: config2.exports,
30333
+ contentSources: resolveContentSources(cwd, config2),
30334
+ darkMode: config2.darkMode ?? "class",
30335
+ includeImport: config2.source !== "shadcn"
30336
+ });
30337
+ } catch (err) {
30338
+ log({
30339
+ event: "add:regen-failed",
30340
+ message: `Output regeneration failed: ${err instanceof Error ? err.message : String(err)}`
30341
+ });
30342
+ }
30343
+ }
30344
+ async function isInitialized(cwd) {
30345
+ const paths = getRaftersPaths(cwd);
30346
+ try {
30347
+ await access(paths.root);
30348
+ return true;
30349
+ } catch {
30350
+ return false;
30351
+ }
30352
+ }
30353
+ async function loadConfig(cwd) {
30354
+ const paths = getRaftersPaths(cwd);
30355
+ try {
30356
+ const content = await readFile4(paths.config, "utf-8");
30357
+ return migrateConfig(
30358
+ JSON.parse(content)
30359
+ );
30360
+ } catch (err) {
30361
+ if (existsSync4(paths.config)) {
30362
+ const message = err instanceof Error ? err.message : String(err);
30363
+ log({
30364
+ event: "add:warning",
30365
+ message: `Failed to load config: ${message}`
30366
+ });
30367
+ }
30368
+ return null;
30369
+ }
30370
+ }
30371
+ function resolveRegistryUrl(options, config2) {
30372
+ return options.registryUrl ?? config2?.registryUrl;
30373
+ }
30374
+ async function saveConfig(cwd, config2) {
30375
+ const paths = getRaftersPaths(cwd);
30376
+ await writeFile3(paths.config, JSON.stringify(config2, null, 2));
30377
+ }
30378
+ function getInstalledNames(config2) {
30379
+ if (!config2?.installed) return [];
30380
+ const names = /* @__PURE__ */ new Set([
30381
+ ...config2.installed.components,
30382
+ ...config2.installed.primitives,
30383
+ ...config2.installed.composites ?? [],
30384
+ ...config2.installed.rules ?? [],
30385
+ ...config2.installed.substrate ?? []
30386
+ ]);
30387
+ return [...names].sort();
30388
+ }
30389
+ async function discoverUntrackedNames(cwd, config2, client, tracked) {
30390
+ let index = null;
30391
+ try {
30392
+ index = await client.fetchIndex();
30393
+ } catch (err) {
30394
+ const message = err instanceof Error ? err.message : String(err);
30395
+ log({
30396
+ event: "add:warning",
30397
+ message: `Could not read the registry index to reconcile on-disk components (${message}). Updating tracked components only.`
30398
+ });
30399
+ }
30400
+ const { untracked } = buildUpdateCandidates(tracked, index, readInstallRoots(cwd, config2));
30401
+ return untracked;
30402
+ }
30403
+ function getComponentTarget(config2) {
30404
+ return resolveComponentTarget(config2);
30405
+ }
30406
+ var SHARED_EXTENSIONS = /* @__PURE__ */ new Set([".behavior.ts", ".classes.ts"]);
30407
+ function isSharedFile(path) {
30408
+ for (const ext of SHARED_EXTENSIONS) {
30409
+ if (path.endsWith(ext)) return true;
30410
+ }
30411
+ return false;
30412
+ }
30413
+ var REACT_FALLBACK_TARGETS = /* @__PURE__ */ new Set(["astro", "vue", "svelte"]);
30414
+ function selectFilesForFramework(files, target) {
30415
+ const preferredExt = targetToExtension(target);
30416
+ const shared = files.filter((f) => isSharedFile(f.path));
30417
+ const matched = files.filter((f) => f.path.endsWith(preferredExt));
30418
+ if (matched.length > 0) {
30419
+ return { files: [...matched, ...shared], fallback: false };
30420
+ }
30421
+ if (REACT_FALLBACK_TARGETS.has(target)) {
30422
+ const fallbackFiles = files.filter((f) => f.path.endsWith(".tsx"));
30423
+ if (fallbackFiles.length > 0) {
30424
+ return { files: [...fallbackFiles, ...shared], fallback: true };
30425
+ }
30426
+ }
30427
+ if (target !== "react" && !REACT_FALLBACK_TARGETS.has(target)) {
30428
+ return { files: shared, fallback: false };
30429
+ }
30430
+ return { files, fallback: false };
30431
+ }
30432
+ var TARGET_GATED_COMPOSITE_FILES = [{ suffix: "Composite.astro", target: "astro" }];
30433
+ function selectCompositeFiles(files, target) {
30434
+ return files.filter((file2) => {
30435
+ const gate = TARGET_GATED_COMPOSITE_FILES.find((g2) => file2.path.endsWith(g2.suffix));
30436
+ return gate ? gate.target === target : true;
30437
+ });
30438
+ }
30439
+ var FOLDER_NAMES = /* @__PURE__ */ new Set(["composites"]);
30440
+ function isAlreadyInstalled(config2, item) {
30441
+ if (!config2?.installed) return false;
30442
+ const { components, primitives, composites: composites2, rules, substrate } = config2.installed;
30443
+ const bucketByType = {
30444
+ ui: components,
30445
+ primitive: primitives,
30446
+ composite: composites2 ?? [],
30447
+ rule: rules ?? [],
30448
+ substrate: substrate ?? []
30449
+ };
30450
+ return bucketByType[item.type].includes(item.name);
30451
+ }
30452
+ function isInstalledOnDisk(config2, item, cwd) {
30453
+ if (!isAlreadyInstalled(config2, item)) return false;
30454
+ const filesToCheck = item.type === "ui" ? selectFilesForFramework(item.files, getComponentTarget(config2)).files : item.files;
30455
+ for (const file2 of filesToCheck) {
30456
+ if (fileExists(cwd, transformPath(file2.path, config2, cwd))) return true;
30457
+ }
30458
+ return false;
30459
+ }
30460
+ function trackInstalled(config2, items) {
30461
+ if (!config2.installed) {
30462
+ config2.installed = {
30463
+ components: [],
30464
+ primitives: [],
30465
+ composites: [],
30466
+ rules: []
30467
+ };
30468
+ }
30469
+ const installed = config2.installed;
30470
+ if (!installed.composites) installed.composites = [];
30471
+ if (!installed.rules) installed.rules = [];
30472
+ if (!installed.substrate) installed.substrate = [];
30473
+ const bucketByType = {
30474
+ ui: installed.components,
30475
+ primitive: installed.primitives,
30476
+ composite: installed.composites,
30477
+ rule: installed.rules,
30478
+ substrate: installed.substrate
30479
+ };
30480
+ for (const item of items) {
30481
+ const bucket = bucketByType[item.type];
30482
+ if (!bucket.includes(item.name)) bucket.push(item.name);
30483
+ }
30484
+ installed.components.sort();
30485
+ installed.primitives.sort();
30486
+ installed.composites.sort();
30487
+ installed.rules.sort();
30488
+ installed.substrate.sort();
30489
+ }
30490
+ function rootFor(field, cwd, fallback) {
30491
+ return field === void 0 ? fallback : resolveRoot(field, cwd, fallback);
30492
+ }
30493
+ function transformPath(registryPath, config2, cwd = process.cwd()) {
30494
+ if (!config2) return registryPath;
30495
+ const replacements = [
30496
+ ["components/ui/", config2.componentsPath, "components/ui"],
30497
+ ["lib/primitives/", config2.primitivesPath, "lib/primitives"],
30498
+ ["composites/", config2.compositesPath, "composites"],
30499
+ ["rules/", config2.rulesPath, "lib/rules"]
30500
+ ];
30501
+ for (const [prefix, field, fallback] of replacements) {
30502
+ if (registryPath.startsWith(prefix)) {
30503
+ return registryPath.replace(prefix, `${rootFor(field, cwd, fallback)}/`);
30504
+ }
30505
+ }
30506
+ return registryPath;
30507
+ }
30508
+ function substrateProjectPath(registryPath, config2, cwd = process.cwd()) {
30509
+ const componentsResolved = rootFor(config2?.componentsPath, cwd, "components/ui");
30510
+ const sourceRoot = componentsResolved.replace(/\/?components\/ui$/, "");
30511
+ return sourceRoot ? join8(sourceRoot, registryPath) : registryPath;
30512
+ }
30513
+ function fileExists(cwd, relativePath) {
30514
+ return existsSync4(join8(cwd, relativePath));
30515
+ }
30516
+ function transformFileContent(content, config2, fileType = "component", cwd = process.cwd(), opts = {}) {
30517
+ const { substrateKinds = [], installPath } = opts;
30518
+ let transformed = content;
30519
+ const componentsPath = rootFor(config2?.componentsPath, cwd, "components/ui");
30520
+ const primitivesPath = rootFor(config2?.primitivesPath, cwd, "lib/primitives");
30521
+ const stripSourceRoot = (p2) => p2.replace(/^(src|app)\//, "");
30522
+ const aliasComponents = stripSourceRoot(componentsPath);
30523
+ const aliasPrimitives = stripSourceRoot(primitivesPath);
30524
+ transformed = transformed.replace(
30525
+ /from\s+['"]\.\.\/\.\.\/primitives\/([^'"]+)['"]/g,
30526
+ `from '@/${aliasPrimitives}/$1'`
30527
+ );
30528
+ transformed = transformed.replace(
30529
+ /from\s+['"]\.\.\/primitives\/([^'"]+)['"]/g,
30530
+ `from '@/${aliasPrimitives}/$1'`
30531
+ );
30532
+ const substrateOwnDir = fileType === "substrate" && installPath ? stripSourceRoot(dirname(installPath)) : null;
30533
+ const aliasSibling = fileType === "primitive" ? aliasPrimitives : substrateOwnDir ?? aliasComponents;
30534
+ transformed = transformed.replace(/from\s+['"]\.\/([^'"]+)['"]/g, `from '@/${aliasSibling}/$1'`);
30535
+ if (substrateKinds.length > 0) {
30536
+ const kindAlternation = substrateKinds.join("|");
30537
+ transformed = transformed.replace(
30538
+ new RegExp(`from\\s+['"](?:\\.\\./){1,2}(${kindAlternation})/([^'"]+)['"]`, "g"),
30539
+ "from '@/$1/$2'"
30540
+ );
30541
+ }
30542
+ transformed = transformed.replace(
30543
+ /from\s+['"]\.\.\/([^/'"]+)\/\1([^'"]*)['"]/g,
30544
+ `from '@/${aliasComponents}/$1$2'`
30545
+ );
30546
+ transformed = transformed.replace(
30547
+ /from\s+['"]\.\.\/([^'"]+)['"]/g,
30548
+ `from '@/${aliasComponents}/$1'`
30549
+ );
30550
+ return transformed;
30551
+ }
30552
+ async function installItem(cwd, item, options, config2, substrateKinds = []) {
30553
+ const installedFiles = [];
30554
+ let skipped = false;
30555
+ let filesToInstall = item.files;
30556
+ if (item.type === "ui") {
30557
+ const target = getComponentTarget(config2);
30558
+ const selection = selectFilesForFramework(item.files, target);
30559
+ filesToInstall = selection.files;
30560
+ if (selection.fallback) {
30561
+ log({
30562
+ event: "add:fallback",
30563
+ component: item.name,
30564
+ target,
30565
+ message: `No ${targetToExtension(target)} version available for ${item.name}. Installing React version.`
30566
+ });
30567
+ }
30568
+ const hasComponentFile = selection.files.some((f) => !isSharedFile(f.path));
30569
+ if (!hasComponentFile) {
30570
+ throw new Error(
30571
+ `No ${targetToExtension(target)} version available for "${item.name}" and no compatible fallback exists.`
30572
+ );
30573
+ }
30574
+ } else if (item.type === "composite") {
30575
+ filesToInstall = selectCompositeFiles(item.files, getComponentTarget(config2));
30576
+ }
30577
+ for (const file2 of filesToInstall) {
30578
+ const projectPath2 = item.type === "substrate" ? substrateProjectPath(file2.path, config2, cwd) : transformPath(file2.path, config2, cwd);
30579
+ const targetPath = join8(cwd, projectPath2);
30580
+ if (fileExists(cwd, projectPath2)) {
30581
+ if (!options.overwrite) {
30582
+ log({
30583
+ event: "add:skip",
30584
+ component: item.name,
30585
+ file: projectPath2,
30586
+ reason: "exists"
30587
+ });
30588
+ skipped = true;
30589
+ continue;
30590
+ }
30591
+ }
30592
+ await mkdir3(dirname(targetPath), { recursive: true });
30593
+ const fileType = item.type === "primitive" ? "primitive" : item.type === "substrate" ? "substrate" : "component";
30594
+ const transformedContent = transformFileContent(file2.content, config2, fileType, cwd, {
30595
+ substrateKinds,
30596
+ installPath: file2.path
30597
+ });
30598
+ await writeFile3(targetPath, transformedContent, "utf-8");
30599
+ installedFiles.push(projectPath2);
30600
+ }
30601
+ return {
30602
+ installed: installedFiles.length > 0,
30603
+ skipped,
30604
+ files: installedFiles
30605
+ };
30606
+ }
30607
+ async function add(componentArgs, options) {
30608
+ setAgentMode(options.agent ?? false);
30609
+ let components = componentArgs;
30610
+ const cwd = process.cwd();
30611
+ const config2 = await loadConfig(cwd);
30612
+ const client = new RegistryClient(resolveRegistryUrl(options, config2));
30613
+ let folder;
30614
+ const firstArg = components[0];
30615
+ if (firstArg && FOLDER_NAMES.has(firstArg)) {
30616
+ folder = firstArg;
30617
+ components = components.slice(1);
30618
+ }
30619
+ if (options.list) {
30620
+ const availableComponents = await client.listComponents();
30621
+ const availableComposites = await client.listComposites();
30622
+ if (options.agent) {
30623
+ log({
30624
+ event: "add:list",
30625
+ components: availableComponents,
30626
+ composites: availableComposites
30627
+ });
30628
+ } else {
30629
+ console.log("Available components:\n");
30630
+ for (const comp of availableComponents) {
30631
+ console.log(` ${comp.name} ${comp.description ?? ""}`);
30632
+ }
30633
+ if (availableComposites.length > 0) {
30634
+ console.log("\nAvailable composites:\n");
30635
+ for (const comp of availableComposites) {
30636
+ console.log(` ${comp.name} ${comp.description ?? ""}`);
30637
+ }
30638
+ }
30639
+ }
30640
+ return;
30641
+ }
30642
+ const initialized = await isInitialized(cwd);
30643
+ if (!initialized) {
30644
+ error46("Project not initialized. Run `rafters init` first.");
30645
+ process.exitCode = 1;
30646
+ return;
30647
+ }
30648
+ if (options.update) {
30649
+ options.overwrite = true;
30650
+ }
30651
+ let untrackedNames = [];
30652
+ if (options.updateAll) {
30653
+ options.overwrite = true;
30654
+ if (!config2) {
30655
+ error46("No rafters config found. Run 'rafters init' first.");
30656
+ process.exitCode = 1;
30657
+ return;
30658
+ }
30659
+ const trackedNames = getInstalledNames(config2);
30660
+ untrackedNames = await discoverUntrackedNames(cwd, config2, client, trackedNames);
30661
+ const candidates = [.../* @__PURE__ */ new Set([...trackedNames, ...untrackedNames])].sort();
30662
+ if (candidates.length === 0) {
30663
+ error46("No installed components found. Use 'rafters add <component>' to install first.");
30664
+ process.exitCode = 1;
30665
+ return;
30666
+ }
30667
+ if (untrackedNames.length > 0) {
30668
+ log({
30669
+ event: "add:untracked",
30670
+ components: untrackedNames,
30671
+ message: `Found ${untrackedNames.length} component(s) on disk the config never tracked: ${untrackedNames.join(", ")}. Refreshing and tracking them.`
30672
+ });
30673
+ }
30674
+ components = candidates;
30675
+ }
30676
+ if (folder === "composites" && components.length === 0) {
30677
+ components = ["composites"];
30678
+ }
30679
+ if (components.length === 0) {
30680
+ error46("No components specified. Usage: rafters add <component...>");
30681
+ process.exitCode = 1;
30682
+ return;
30683
+ }
30684
+ log({
30685
+ event: "add:start",
30686
+ cwd,
30687
+ components,
30688
+ overwrite: options.overwrite ?? false
30689
+ });
30690
+ const allItems = [];
30691
+ const seen = /* @__PURE__ */ new Set();
30692
+ for (const itemName of components) {
30693
+ try {
30694
+ if (folder === "composites") {
30695
+ if (!seen.has(itemName)) {
30696
+ const composite = await client.fetchComposite(itemName);
30697
+ seen.add(itemName);
30698
+ allItems.push(composite);
30699
+ for (const dep of composite.primitives) {
30700
+ if (!seen.has(dep)) {
30701
+ const depItems = await client.resolveDependencies(dep, seen);
30702
+ allItems.push(...depItems);
30703
+ }
30704
+ }
30700
30705
  }
30701
- if (fontAssignments.length > 0 || definedFonts.length > 0) {
30702
- saveRegistryToDir(paths.tokens, registry2);
30703
- await generateOutputs(cwd, paths, registry2, exports, shadcn);
30704
- log({
30705
- event: "init:import_fonts_applied",
30706
- cssPath: detectedCssPath,
30707
- fontsDetected: detectedFonts.map((f) => f.name),
30708
- assignments: fontAssignments,
30709
- ...definedFonts.length > 0 ? { definedFonts } : {}
30710
- });
30706
+ } else {
30707
+ const items = await client.resolveDependencies(itemName, seen);
30708
+ allItems.push(...items);
30709
+ }
30710
+ } catch (err) {
30711
+ if (err instanceof Error) {
30712
+ error46(err.message);
30713
+ } else {
30714
+ error46(`Failed to fetch "${itemName}"`);
30715
+ }
30716
+ process.exitCode = 1;
30717
+ return;
30718
+ }
30719
+ }
30720
+ const substrateKinds = [
30721
+ ...new Set(
30722
+ allItems.filter((item) => item.type === "substrate").map((item) => item.files[0]?.path.split("/")[0]).filter((segment) => Boolean(segment))
30723
+ )
30724
+ ];
30725
+ const written = [];
30726
+ const skipped = [];
30727
+ const failed = [];
30728
+ const installedItems = [];
30729
+ const filteredItems = [];
30730
+ const target = getComponentTarget(config2);
30731
+ for (const item of allItems) {
30732
+ if (!options.overwrite && isAlreadyInstalled(config2, item)) {
30733
+ if (isInstalledOnDisk(config2, item, cwd)) {
30734
+ log({
30735
+ event: "add:skip",
30736
+ component: item.name,
30737
+ reason: "already installed"
30738
+ });
30739
+ skipped.push(item.name);
30740
+ continue;
30741
+ }
30742
+ log({
30743
+ event: "add:warning",
30744
+ component: item.name,
30745
+ message: "Config tracks this as installed but no files found on disk -- reinstalling."
30746
+ });
30747
+ }
30748
+ try {
30749
+ const result = await installItem(cwd, item, options, config2, substrateKinds);
30750
+ if (result.installed) {
30751
+ written.push(item.name);
30752
+ installedItems.push(item);
30753
+ if (item.type === "ui") {
30754
+ const selection = selectFilesForFramework(item.files, target);
30755
+ filteredItems.push({ ...item, files: selection.files });
30756
+ } else {
30757
+ filteredItems.push(item);
30711
30758
  }
30759
+ log({
30760
+ event: "add:installed",
30761
+ component: item.name,
30762
+ type: item.type,
30763
+ files: result.files
30764
+ });
30765
+ }
30766
+ if (result.skipped && !result.installed) {
30767
+ skipped.push(item.name);
30768
+ installedItems.push(item);
30769
+ }
30770
+ } catch (err) {
30771
+ failed.push(item.name);
30772
+ if (err instanceof Error) {
30773
+ log({
30774
+ event: "add:warning",
30775
+ component: item.name,
30776
+ message: err.message
30777
+ });
30712
30778
  }
30713
30779
  }
30714
30780
  }
30781
+ const emptyDeps = {
30782
+ installed: [],
30783
+ skipped: [],
30784
+ devInstalled: [],
30785
+ failed: []
30786
+ };
30787
+ let depsResult = emptyDeps;
30788
+ try {
30789
+ depsResult = await installRegistryDependencies(filteredItems, cwd, {
30790
+ target
30791
+ });
30792
+ } catch (err) {
30793
+ const message = err instanceof Error ? err.message : String(err);
30794
+ log({
30795
+ event: "add:deps:install-failed",
30796
+ message: `Failed to process dependencies: ${message}`,
30797
+ dependencies: [],
30798
+ suggestion: "Check package.json and try installing dependencies manually."
30799
+ });
30800
+ }
30801
+ if (depsResult.installed.length > 0 || depsResult.skipped.length > 0) {
30802
+ log({
30803
+ event: "add:dependencies",
30804
+ dependencies: depsResult.installed,
30805
+ devDependencies: depsResult.devInstalled,
30806
+ skipped: depsResult.skipped
30807
+ });
30808
+ }
30809
+ if (installedItems.length > 0 && config2) {
30810
+ trackInstalled(config2, installedItems);
30811
+ await saveConfig(cwd, config2);
30812
+ await regenerateAfterInstall(cwd, config2);
30813
+ } else if (installedItems.length > 0 && !config2) {
30814
+ const newConfig = {
30815
+ framework: "unknown",
30816
+ componentsPath: "components/ui",
30817
+ primitivesPath: "lib/primitives",
30818
+ compositesPath: "composites",
30819
+ rulesPath: "lib/rules",
30820
+ cssPath: null,
30821
+ intent: "efficient",
30822
+ fonts: { path: null, imports: [] },
30823
+ exports: DEFAULT_EXPORTS,
30824
+ installed: { components: [], primitives: [], composites: [], rules: [] }
30825
+ };
30826
+ trackInstalled(newConfig, installedItems);
30827
+ await saveConfig(cwd, newConfig);
30828
+ await regenerateAfterInstall(cwd, newConfig);
30829
+ }
30715
30830
  log({
30716
- event: "init:complete",
30717
- outputs: [...outputs, "config.rafters.json"],
30718
- path: paths.output
30831
+ event: "add:complete",
30832
+ written: written.length,
30833
+ skipped: skipped.length,
30834
+ untracked: untrackedNames.length,
30835
+ failed: failed.length,
30836
+ components: written,
30837
+ skippedComponents: skipped,
30838
+ untrackedComponents: untrackedNames,
30839
+ failedComponents: failed
30719
30840
  });
30841
+ if (failed.length > 0) {
30842
+ process.exitCode = 1;
30843
+ }
30844
+ if (!options.updateAll && skipped.length > 0 && written.length === 0) {
30845
+ log({
30846
+ event: "add:hint",
30847
+ message: "Some components were skipped. Use --update to re-fetch, or --update-all to refresh everything.",
30848
+ skipped
30849
+ });
30850
+ error46("Component already exists. Use --update to re-fetch from registry.");
30851
+ process.exitCode = 1;
30852
+ }
30720
30853
  }
30721
30854
 
30722
30855
  // src/commands/mcp.ts
@@ -30891,7 +31024,7 @@ function fuzzyScore(query, target) {
30891
31024
 
30892
31025
  // ../composites/src/registry.ts
30893
31026
  var composites = /* @__PURE__ */ new Map();
30894
- function register2(composite) {
31027
+ function register3(composite) {
30895
31028
  const { id } = composite.manifest;
30896
31029
  if (composites.has(id)) {
30897
31030
  throw new Error(`Composite "${id}" is already registered`);
@@ -31233,7 +31366,10 @@ var RaftersToolHandler = class {
31233
31366
  text: JSON.stringify({
31234
31367
  error: "workspace parameter required",
31235
31368
  suggestion: "Multiple workspaces are available. Pass `workspace` with one of the names below.",
31236
- workspaces: this.workspaces.map((w) => ({ name: w.name, root: w.root }))
31369
+ workspaces: this.workspaces.map((w) => ({
31370
+ name: w.name,
31371
+ root: w.root
31372
+ }))
31237
31373
  })
31238
31374
  }
31239
31375
  ]
@@ -31270,7 +31406,7 @@ var RaftersToolHandler = class {
31270
31406
  const { registry: registry2 } = await discoverFromDirs(...dirs);
31271
31407
  for (const composite of registry2.values()) {
31272
31408
  try {
31273
- register2(composite);
31409
+ register3(composite);
31274
31410
  } catch {
31275
31411
  }
31276
31412
  }
@@ -31298,7 +31434,9 @@ var RaftersToolHandler = class {
31298
31434
  const paths = getRaftersPaths(workspaceRoot);
31299
31435
  let config2 = null;
31300
31436
  try {
31301
- config2 = JSON.parse(await readFile6(paths.config, "utf-8"));
31437
+ config2 = migrateConfig(
31438
+ JSON.parse(await readFile6(paths.config, "utf-8"))
31439
+ );
31302
31440
  } catch {
31303
31441
  }
31304
31442
  if (!config2?.compositesPath) {
@@ -31342,7 +31480,9 @@ var RaftersToolHandler = class {
31342
31480
  // are listed -- the array is empty when nothing is constrained.
31343
31481
  blockRules: c4.blocks.filter((b2) => b2.rules && b2.rules.length > 0).map((b2) => ({ id: b2.id, type: b2.type, rules: b2.rules }))
31344
31482
  }));
31345
- return { content: [{ type: "text", text: JSON.stringify({ composites: result }, null, 2) }] };
31483
+ return {
31484
+ content: [{ type: "text", text: JSON.stringify({ composites: result }, null, 2) }]
31485
+ };
31346
31486
  }
31347
31487
  async handlePattern(args) {
31348
31488
  const { solves, query, workspace } = args;
@@ -31389,7 +31529,9 @@ var RaftersToolHandler = class {
31389
31529
  cognitiveLoad: c4.manifest.cognitiveLoad,
31390
31530
  usagePatterns: c4.manifest.usagePatterns
31391
31531
  }));
31392
- return { content: [{ type: "text", text: JSON.stringify({ patterns }, null, 2) }] };
31532
+ return {
31533
+ content: [{ type: "text", text: JSON.stringify({ patterns }, null, 2) }]
31534
+ };
31393
31535
  }
31394
31536
  async handleComponent(componentName) {
31395
31537
  try {
@@ -31508,8 +31650,8 @@ import { existsSync as existsSync8 } from "fs";
31508
31650
  import { resolve as resolve6 } from "path";
31509
31651
 
31510
31652
  // ../studio/src/api/vite-plugin.ts
31511
- import { readFile as readFile7 } from "fs/promises";
31512
- import { join as join14 } from "path";
31653
+ import { readFile as readFile7, writeFile as writeFile4 } from "fs/promises";
31654
+ import { isAbsolute as isAbsolute3, join as join14 } from "path";
31513
31655
  var REGISTRY_PLUGINS3 = [scalePlugin, contrastPlugin, statePlugin, invertPlugin];
31514
31656
  var STUDIO_REASON_DEFAULT = "studio interactive edit";
31515
31657
  var TokenResponseSchema = external_exports.object({
@@ -31537,13 +31679,52 @@ var ColorBuildOptionsSchema = external_exports.object({
31537
31679
  var projectPath = process.env.RAFTERS_PROJECT_PATH || process.cwd();
31538
31680
  var outputDir = join14(projectPath, ".rafters", "output");
31539
31681
  var configPath = join14(projectPath, ".rafters", "config.rafters.json");
31540
- async function loadStudioConfig() {
31682
+ var FontsConfigSchema = external_exports.object({
31683
+ path: external_exports.union([external_exports.string(), external_exports.null()]).optional(),
31684
+ imports: external_exports.array(external_exports.string()).optional()
31685
+ });
31686
+ var RaftersConfigSchema = external_exports.object({
31687
+ intent: external_exports.string().optional(),
31688
+ fonts: FontsConfigSchema.optional(),
31689
+ source: external_exports.string().optional(),
31690
+ darkMode: external_exports.enum(["class", "media"]).optional(),
31691
+ framework: external_exports.string().optional(),
31692
+ cssPath: external_exports.union([external_exports.string(), external_exports.null()]).optional(),
31693
+ exports: external_exports.record(external_exports.string(), external_exports.boolean()).optional()
31694
+ }).passthrough();
31695
+ var KNOWN_INTENTS = ["efficient"];
31696
+ async function readRaftersConfig() {
31541
31697
  try {
31542
- return JSON.parse(await readFile7(configPath, "utf8"));
31698
+ const raw = JSON.parse(await readFile7(configPath, "utf8"));
31699
+ if ("shadcn" in raw && !("source" in raw)) {
31700
+ if (raw.shadcn === true) raw.source = "shadcn";
31701
+ delete raw.shadcn;
31702
+ }
31703
+ const result = RaftersConfigSchema.safeParse(raw);
31704
+ if (!result.success) {
31705
+ console.log(`[rafters] Config validation failed: ${result.error.message}`);
31706
+ return null;
31707
+ }
31708
+ return result.data;
31543
31709
  } catch {
31544
31710
  return null;
31545
31711
  }
31546
31712
  }
31713
+ function validateFontsPath(path) {
31714
+ if (path === null || path === void 0) return null;
31715
+ if (isAbsolute3(path)) return "fonts path must be relative";
31716
+ return null;
31717
+ }
31718
+ function validateIntent(intent) {
31719
+ if (!KNOWN_INTENTS.includes(intent)) {
31720
+ return `unknown intent "${intent}". Known: ${KNOWN_INTENTS.join(", ")}`;
31721
+ }
31722
+ return null;
31723
+ }
31724
+ async function loadStudioConfig() {
31725
+ const raw = await readRaftersConfig();
31726
+ return raw;
31727
+ }
31547
31728
  var SetTokenMessageSchema = external_exports.object({
31548
31729
  name: external_exports.string().min(1),
31549
31730
  value: external_exports.union([external_exports.string(), ColorValueSchema, ColorReferenceSchema]),
@@ -31942,7 +32123,7 @@ function studioApiPlugin() {
31942
32123
  exports,
31943
32124
  contentSources: config2 ? resolveContentSources(projectPath, config2) : [],
31944
32125
  darkMode: config2?.darkMode ?? "class",
31945
- includeImport: !config2?.shadcn
32126
+ includeImport: config2?.source !== "shadcn"
31946
32127
  },
31947
32128
  { notify: () => server.ws.send({ type: "custom", event: "rafters:css-updated" }) }
31948
32129
  );
@@ -32060,6 +32241,85 @@ function studioApiPlugin() {
32060
32241
  client.send("rafters:token-updated", { ok: false, error: String(error47) });
32061
32242
  }
32062
32243
  });
32244
+ server.ws.on("rafters:get-config", async (_rawData, client) => {
32245
+ const config2 = await readRaftersConfig();
32246
+ if (!config2) {
32247
+ client.send("rafters:config", {
32248
+ ok: false,
32249
+ error: `config not found at ${configPath}`
32250
+ });
32251
+ return;
32252
+ }
32253
+ client.send("rafters:config", { ok: true, config: config2 });
32254
+ });
32255
+ server.ws.on("rafters:set-intent", async (rawData, client) => {
32256
+ const parsed = external_exports.object({ intent: external_exports.string().min(1) }).safeParse(rawData);
32257
+ if (!parsed.success) {
32258
+ client.send("rafters:intent-updated", {
32259
+ ok: false,
32260
+ error: `Invalid message: ${parsed.error.message}`
32261
+ });
32262
+ return;
32263
+ }
32264
+ const { intent } = parsed.data;
32265
+ const intentError = validateIntent(intent);
32266
+ if (intentError) {
32267
+ client.send("rafters:intent-updated", { ok: false, error: intentError });
32268
+ return;
32269
+ }
32270
+ const config2 = await readRaftersConfig();
32271
+ if (!config2) {
32272
+ client.send("rafters:intent-updated", {
32273
+ ok: false,
32274
+ error: `config not found at ${configPath}`
32275
+ });
32276
+ return;
32277
+ }
32278
+ const updated = { ...config2, intent };
32279
+ try {
32280
+ await writeFile4(configPath, JSON.stringify(updated, null, 2));
32281
+ client.send("rafters:intent-updated", { ok: true, intent });
32282
+ } catch (error47) {
32283
+ console.log(`[rafters] Intent update failed: ${error47}`);
32284
+ client.send("rafters:intent-updated", { ok: false, error: String(error47) });
32285
+ }
32286
+ });
32287
+ server.ws.on("rafters:set-fonts", async (rawData, client) => {
32288
+ const parsed = FontsConfigSchema.safeParse(rawData);
32289
+ if (!parsed.success) {
32290
+ client.send("rafters:fonts-updated", {
32291
+ ok: false,
32292
+ error: `Invalid message: ${parsed.error.message}`
32293
+ });
32294
+ return;
32295
+ }
32296
+ const fontsData = parsed.data;
32297
+ const pathError = validateFontsPath(fontsData.path);
32298
+ if (pathError) {
32299
+ client.send("rafters:fonts-updated", { ok: false, error: pathError });
32300
+ return;
32301
+ }
32302
+ const config2 = await readRaftersConfig();
32303
+ if (!config2) {
32304
+ client.send("rafters:fonts-updated", {
32305
+ ok: false,
32306
+ error: `config not found at ${configPath}`
32307
+ });
32308
+ return;
32309
+ }
32310
+ const existing = config2.fonts ?? {};
32311
+ const fonts = { ...existing };
32312
+ if (fontsData.path !== void 0) fonts.path = fontsData.path;
32313
+ if (fontsData.imports !== void 0) fonts.imports = fontsData.imports;
32314
+ const updated = { ...config2, fonts };
32315
+ try {
32316
+ await writeFile4(configPath, JSON.stringify(updated, null, 2));
32317
+ client.send("rafters:fonts-updated", { ok: true, fonts });
32318
+ } catch (error47) {
32319
+ console.log(`[rafters] Fonts update failed: ${error47}`);
32320
+ client.send("rafters:fonts-updated", { ok: false, error: String(error47) });
32321
+ }
32322
+ });
32063
32323
  server.middlewares.use(async (req, res, next) => {
32064
32324
  let pathname;
32065
32325
  try {