wensity 0.1.5 → 0.1.6

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 (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +49 -11
  3. package/dist/index.js +1618 -64
  4. package/package.json +12 -8
package/dist/index.js CHANGED
@@ -1,12 +1,809 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/commands.ts
4
- import { mkdir as mkdir4, writeFile as writeFile4 } from "fs/promises";
5
- import { dirname as dirname5, join as join6 } from "path";
4
+ import { mkdir as mkdir5, readFile as readFile5, writeFile as writeFile5 } from "fs/promises";
5
+ import { dirname as dirname6, join as join6 } from "path";
6
+
7
+ // ../wensity-preset/src/constants.ts
8
+ var ICON_LIBRARIES = ["tabler", "lucide"];
9
+ var RADIUS_PRESETS = ["none", "small", "medium", "large"];
10
+ var BUILTIN_FONT_IDS = ["site", "system-sans", "system-mono"];
11
+ var RADIUS_PRESET_CSS = {
12
+ none: "0px",
13
+ small: "0.5rem",
14
+ medium: "0.875rem",
15
+ large: "1.25rem"
16
+ };
17
+ var FONT_ID_EXPANSIONS = {
18
+ "site:sans": "var(--font-sans)",
19
+ "site:display": "var(--font-display)",
20
+ "site:mono": "var(--font-mono)",
21
+ "system-sans": "ui-sans-serif, system-ui, sans-serif",
22
+ "system-mono": "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace"
23
+ };
24
+ var CONTROL_SOLID_COLOR_KEYS = [
25
+ "controlSolid",
26
+ "controlSolidHover",
27
+ "controlSolidActive",
28
+ "controlSolidForeground"
29
+ ];
30
+ var CONTROL_SOLID_CSS_VARS = {
31
+ controlSolid: "--primitive-control-solid",
32
+ controlSolidHover: "--primitive-control-solid-hover",
33
+ controlSolidActive: "--primitive-control-solid-active",
34
+ controlSolidForeground: "--primitive-control-solid-foreground"
35
+ };
36
+ var STYLE_OBJECT_KEYS = [
37
+ "iconLibrary",
38
+ "radius",
39
+ "fonts",
40
+ "colors",
41
+ "presetCode"
42
+ ];
43
+ var FONT_ROLE_KEYS = ["sans", "display", "mono"];
44
+ var COLOR_MODE_KEYS = ["light", "dark"];
45
+ var HEX_COLOR_PATTERN = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;
46
+ var CUSTOM_FONT_PREFIX = "custom:";
47
+ var CUSTOM_FONT_STACK_MAX_LENGTH = 120;
48
+ var PRESET_CODEC_PREFIX = "wsty1";
49
+ var PRESET_CODEC_MAX_DECODED_JSON_BYTES = 4 * 1024;
50
+ var PRESET_CODEC_MAX_CODE_LENGTH = 6 * 1024;
51
+ var CONFIG_SCHEMA_VERSION = 1;
52
+ function isBuiltinFontId(value) {
53
+ return BUILTIN_FONT_IDS.includes(value);
54
+ }
55
+ function isIconLibrary(value) {
56
+ return ICON_LIBRARIES.includes(value);
57
+ }
58
+ function isRadiusPreset(value) {
59
+ return RADIUS_PRESETS.includes(value);
60
+ }
61
+ function isFontRoleValue(value) {
62
+ if (isBuiltinFontId(value)) return true;
63
+ return value.startsWith(CUSTOM_FONT_PREFIX) && value.length > CUSTOM_FONT_PREFIX.length;
64
+ }
65
+
66
+ // ../wensity-preset/src/defaults.ts
67
+ var DEFAULT_STYLE_FONTS = {
68
+ sans: "site",
69
+ display: "site",
70
+ mono: "system-mono"
71
+ };
72
+ var DEFAULT_CONTROL_SOLID_LIGHT = {
73
+ controlSolid: "#24292d",
74
+ controlSolidHover: "#2c3237",
75
+ controlSolidActive: "#1e2326",
76
+ controlSolidForeground: "#ffffff"
77
+ };
78
+ var DEFAULT_CONTROL_SOLID_DARK = {
79
+ controlSolid: "#f5f3ee",
80
+ controlSolidHover: "#ffffff",
81
+ controlSolidActive: "#e8e4da",
82
+ controlSolidForeground: "#151719"
83
+ };
84
+ var DEFAULT_STYLE_CONFIG = {
85
+ iconLibrary: "tabler",
86
+ radius: "medium",
87
+ fonts: { ...DEFAULT_STYLE_FONTS },
88
+ colors: {
89
+ light: { ...DEFAULT_CONTROL_SOLID_LIGHT },
90
+ dark: { ...DEFAULT_CONTROL_SOLID_DARK }
91
+ }
92
+ };
93
+ function createDefaultStyleConfig() {
94
+ return {
95
+ iconLibrary: DEFAULT_STYLE_CONFIG.iconLibrary,
96
+ radius: DEFAULT_STYLE_CONFIG.radius,
97
+ fonts: { ...DEFAULT_STYLE_CONFIG.fonts },
98
+ colors: {
99
+ light: { ...DEFAULT_CONTROL_SOLID_LIGHT },
100
+ dark: { ...DEFAULT_CONTROL_SOLID_DARK }
101
+ }
102
+ };
103
+ }
104
+
105
+ // ../wensity-preset/src/schema.ts
106
+ var fontRoleSchema = {
107
+ oneOf: [
108
+ { type: "string", enum: [...BUILTIN_FONT_IDS] },
109
+ {
110
+ type: "string",
111
+ pattern: `^${CUSTOM_FONT_PREFIX}.+`,
112
+ minLength: CUSTOM_FONT_PREFIX.length + 1
113
+ }
114
+ ]
115
+ };
116
+ var controlSolidColorsSchema = {
117
+ type: "object",
118
+ additionalProperties: false,
119
+ properties: Object.fromEntries(
120
+ CONTROL_SOLID_COLOR_KEYS.map((key) => [
121
+ key,
122
+ { type: "string", pattern: HEX_COLOR_PATTERN.source }
123
+ ])
124
+ )
125
+ };
126
+ var WENSITY_CONFIG_SCHEMA = {
127
+ $schema: "https://json-schema.org/draft/2020-12/schema",
128
+ $id: "https://ui.wensity.com/schema/wensity.json",
129
+ title: "Wensity CLI configuration",
130
+ type: "object",
131
+ additionalProperties: false,
132
+ properties: {
133
+ $schema: { type: "string" },
134
+ version: { type: "integer", const: 1 },
135
+ registry: { type: "string", format: "uri" },
136
+ components: { type: "string", minLength: 1 },
137
+ utils: { type: "string", minLength: 1 },
138
+ tailwind: {
139
+ type: "object",
140
+ additionalProperties: false,
141
+ properties: {
142
+ config: { type: "string" },
143
+ css: { type: "string" }
144
+ }
145
+ },
146
+ aliases: {
147
+ type: "object",
148
+ additionalProperties: false,
149
+ properties: {
150
+ components: { type: "string", minLength: 1 },
151
+ utils: { type: "string", minLength: 1 }
152
+ }
153
+ },
154
+ style: {
155
+ type: "object",
156
+ additionalProperties: false,
157
+ properties: {
158
+ iconLibrary: { type: "string", enum: [...ICON_LIBRARIES] },
159
+ radius: { type: "string", enum: [...RADIUS_PRESETS] },
160
+ fonts: {
161
+ type: "object",
162
+ additionalProperties: false,
163
+ properties: {
164
+ sans: fontRoleSchema,
165
+ display: fontRoleSchema,
166
+ mono: fontRoleSchema
167
+ }
168
+ },
169
+ colors: {
170
+ type: "object",
171
+ additionalProperties: false,
172
+ properties: {
173
+ light: controlSolidColorsSchema,
174
+ dark: controlSolidColorsSchema
175
+ }
176
+ },
177
+ presetCode: {
178
+ type: "string",
179
+ minLength: 1,
180
+ maxLength: PRESET_CODEC_MAX_CODE_LENGTH
181
+ }
182
+ }
183
+ }
184
+ },
185
+ required: ["registry", "components", "utils", "aliases"]
186
+ };
187
+
188
+ // ../wensity-preset/src/fonts.ts
189
+ var ALLOWED_UNQUOTED_FONT_TOKENS = [
190
+ "ui-sans-serif",
191
+ "system-ui",
192
+ "sans-serif",
193
+ "monospace",
194
+ "ui-monospace",
195
+ "SFMono-Regular",
196
+ "Menlo",
197
+ "Monaco",
198
+ "Consolas",
199
+ "var(--font-sans)",
200
+ "var(--font-display)",
201
+ "var(--font-mono)"
202
+ ];
203
+ var ALLOWED_UNQUOTED_SET = new Set(ALLOWED_UNQUOTED_FONT_TOKENS);
204
+ var QUOTED_FAMILY_PATTERN = /^(["'])([A-Za-z0-9 -]+)\1$/;
205
+ var CONTROL_OR_BREAKOUT_PATTERN = /[\u0000-\u001f\u007f;{}\\\n\r<>]|url\s*\(|@import|expression\s*\(|<\//i;
206
+ function validateCustomFontStack(value, path) {
207
+ if (!value.startsWith(CUSTOM_FONT_PREFIX)) {
208
+ throw new PresetValidationError(`Expected custom font stack at ${path}.`, path);
209
+ }
210
+ const stack = value.slice(CUSTOM_FONT_PREFIX.length);
211
+ if (stack.length === 0) {
212
+ throw new PresetValidationError(`Empty custom font stack at ${path}.`, path);
213
+ }
214
+ if (stack.length > CUSTOM_FONT_STACK_MAX_LENGTH) {
215
+ throw new PresetValidationError(
216
+ `Custom font stack at ${path} exceeds ${CUSTOM_FONT_STACK_MAX_LENGTH} characters.`,
217
+ path
218
+ );
219
+ }
220
+ if (CONTROL_OR_BREAKOUT_PATTERN.test(stack)) {
221
+ throw new PresetValidationError(
222
+ `Custom font stack at ${path} contains forbidden characters or CSS breakout syntax.`,
223
+ path
224
+ );
225
+ }
226
+ const tokens = splitFontStackTokens(stack);
227
+ if (tokens.length === 0) {
228
+ throw new PresetValidationError(`Custom font stack at ${path} has no tokens.`, path);
229
+ }
230
+ for (const token of tokens) {
231
+ validateFontStackToken(token, path);
232
+ }
233
+ return value;
234
+ }
235
+ function splitFontStackTokens(stack) {
236
+ const tokens = [];
237
+ let current = "";
238
+ let quote = null;
239
+ for (let i = 0; i < stack.length; i += 1) {
240
+ const ch = stack[i];
241
+ if (quote) {
242
+ current += ch;
243
+ if (ch === quote) quote = null;
244
+ continue;
245
+ }
246
+ if (ch === '"' || ch === "'") {
247
+ quote = ch;
248
+ current += ch;
249
+ continue;
250
+ }
251
+ if (ch === ",") {
252
+ const trimmed2 = current.trim();
253
+ if (trimmed2) tokens.push(trimmed2);
254
+ current = "";
255
+ continue;
256
+ }
257
+ current += ch;
258
+ }
259
+ if (quote) {
260
+ throw new PresetValidationError("Unterminated quoted font family in custom stack.", "fonts");
261
+ }
262
+ const trimmed = current.trim();
263
+ if (trimmed) tokens.push(trimmed);
264
+ return tokens;
265
+ }
266
+ function validateFontStackToken(token, path) {
267
+ if (QUOTED_FAMILY_PATTERN.test(token)) return;
268
+ if (ALLOWED_UNQUOTED_SET.has(token)) return;
269
+ throw new PresetValidationError(
270
+ `Invalid font token "${token}" at ${path}. Use a quoted family or an allowlisted keyword.`,
271
+ path
272
+ );
273
+ }
274
+ function expandFontRole(value, role) {
275
+ if (value.startsWith(CUSTOM_FONT_PREFIX)) {
276
+ validateCustomFontStack(value, `fonts.${role}`);
277
+ return value.slice(CUSTOM_FONT_PREFIX.length);
278
+ }
279
+ if (value === "site") {
280
+ if (role === "sans") return FONT_ID_EXPANSIONS["site:sans"];
281
+ if (role === "display") return FONT_ID_EXPANSIONS["site:display"];
282
+ return FONT_ID_EXPANSIONS["site:mono"];
283
+ }
284
+ if (value === "system-sans") return FONT_ID_EXPANSIONS["system-sans"];
285
+ if (value === "system-mono") return FONT_ID_EXPANSIONS["system-mono"];
286
+ if (isBuiltinFontId(value)) {
287
+ throw new PresetValidationError(`Unhandled built-in font id "${value}".`, `fonts.${role}`);
288
+ }
289
+ throw new PresetValidationError(`Invalid font role value at fonts.${role}.`, `fonts.${role}`);
290
+ }
291
+
292
+ // ../wensity-preset/src/validate.ts
293
+ var FORBIDDEN_OBJECT_KEYS = ["__proto__", "constructor", "prototype"];
294
+ var PresetValidationError = class extends Error {
295
+ path;
296
+ constructor(message, path = "style") {
297
+ super(message);
298
+ this.name = "PresetValidationError";
299
+ this.path = path;
300
+ }
301
+ };
302
+ function assertNoForbiddenObjectKeys(value, path) {
303
+ for (const key of FORBIDDEN_OBJECT_KEYS) {
304
+ if (Object.prototype.hasOwnProperty.call(value, key)) {
305
+ throw new PresetValidationError(`Forbidden key at ${path}.`, path);
306
+ }
307
+ }
308
+ }
309
+ function assertPlainObject(value, path) {
310
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
311
+ throw new PresetValidationError(`Expected an object at ${path}.`, path);
312
+ }
313
+ assertNoForbiddenObjectKeys(value, path);
314
+ }
315
+ function assertAllowedKeys(value, allowed, path) {
316
+ const allowedSet = new Set(allowed);
317
+ for (const key of Object.keys(value)) {
318
+ if (!allowedSet.has(key)) {
319
+ throw new PresetValidationError(`Unknown key "${key}" at ${path}.`, `${path}.${key}`);
320
+ }
321
+ }
322
+ }
323
+ function validateHexColor(value, path) {
324
+ if (typeof value !== "string" || !HEX_COLOR_PATTERN.test(value)) {
325
+ throw new PresetValidationError(
326
+ `Invalid color at ${path}. Expected #RGB, #RRGGBB, or #RRGGBBAA.`,
327
+ path
328
+ );
329
+ }
330
+ return value;
331
+ }
332
+ function validateFontRole(value, path) {
333
+ if (typeof value !== "string" || !isFontRoleValue(value)) {
334
+ throw new PresetValidationError(
335
+ `Invalid font at ${path}. Expected site | system-sans | system-mono | custom:<stack>.`,
336
+ path
337
+ );
338
+ }
339
+ if (value.startsWith(CUSTOM_FONT_PREFIX)) {
340
+ validateCustomFontStack(value, path);
341
+ }
342
+ return value;
343
+ }
344
+ function validateControlSolidPartial(value, path) {
345
+ assertPlainObject(value, path);
346
+ assertAllowedKeys(value, CONTROL_SOLID_COLOR_KEYS, path);
347
+ const result = {};
348
+ for (const key of CONTROL_SOLID_COLOR_KEYS) {
349
+ if (key in value) {
350
+ result[key] = validateHexColor(value[key], `${path}.${key}`);
351
+ }
352
+ }
353
+ return result;
354
+ }
355
+ function validateFontsPartial(value, path) {
356
+ assertPlainObject(value, path);
357
+ assertAllowedKeys(value, FONT_ROLE_KEYS, path);
358
+ const result = {};
359
+ for (const key of FONT_ROLE_KEYS) {
360
+ if (key in value) {
361
+ result[key] = validateFontRole(value[key], `${path}.${key}`);
362
+ }
363
+ }
364
+ return result;
365
+ }
366
+ function validateColorsPartial(value, path) {
367
+ assertPlainObject(value, path);
368
+ assertAllowedKeys(value, COLOR_MODE_KEYS, path);
369
+ const result = {};
370
+ for (const mode of COLOR_MODE_KEYS) {
371
+ if (mode in value) {
372
+ result[mode] = validateControlSolidPartial(value[mode], `${path}.${mode}`);
373
+ }
374
+ }
375
+ return result;
376
+ }
377
+ function validateStyleConfigPartial(value) {
378
+ if (value === void 0 || value === null) return {};
379
+ assertPlainObject(value, "style");
380
+ assertAllowedKeys(value, STYLE_OBJECT_KEYS, "style");
381
+ const result = {};
382
+ if ("iconLibrary" in value) {
383
+ if (typeof value.iconLibrary !== "string" || !isIconLibrary(value.iconLibrary)) {
384
+ throw new PresetValidationError(
385
+ "Invalid style.iconLibrary. Expected tabler | lucide.",
386
+ "style.iconLibrary"
387
+ );
388
+ }
389
+ result.iconLibrary = value.iconLibrary;
390
+ }
391
+ if ("radius" in value) {
392
+ if (typeof value.radius !== "string" || !isRadiusPreset(value.radius)) {
393
+ throw new PresetValidationError(
394
+ "Invalid style.radius. Expected none | small | medium | large.",
395
+ "style.radius"
396
+ );
397
+ }
398
+ result.radius = value.radius;
399
+ }
400
+ if ("fonts" in value) {
401
+ result.fonts = validateFontsPartial(value.fonts, "style.fonts");
402
+ }
403
+ if ("colors" in value) {
404
+ result.colors = validateColorsPartial(value.colors, "style.colors");
405
+ }
406
+ if ("presetCode" in value) {
407
+ if (typeof value.presetCode !== "string" || value.presetCode.length === 0) {
408
+ throw new PresetValidationError(
409
+ "Invalid style.presetCode. Expected a non-empty string.",
410
+ "style.presetCode"
411
+ );
412
+ }
413
+ if (value.presetCode.length > PRESET_CODEC_MAX_CODE_LENGTH) {
414
+ throw new PresetValidationError(
415
+ `Invalid style.presetCode. Exceeds ${PRESET_CODEC_MAX_CODE_LENGTH} characters.`,
416
+ "style.presetCode"
417
+ );
418
+ }
419
+ result.presetCode = value.presetCode;
420
+ }
421
+ return result;
422
+ }
423
+
424
+ // ../wensity-preset/src/normalize.ts
425
+ function mergeControlSolid(base, partial) {
426
+ if (!partial) return { ...base };
427
+ const merged = { ...base };
428
+ for (const key of CONTROL_SOLID_COLOR_KEYS) {
429
+ const value = partial[key];
430
+ if (value !== void 0) merged[key] = value;
431
+ }
432
+ return merged;
433
+ }
434
+ function mergeFonts(base, partial) {
435
+ if (!partial) return { ...base };
436
+ const merged = { ...base };
437
+ for (const key of FONT_ROLE_KEYS) {
438
+ const value = partial[key];
439
+ if (value !== void 0) merged[key] = value;
440
+ }
441
+ return merged;
442
+ }
443
+ function mergeColors(base, partial) {
444
+ return {
445
+ light: mergeControlSolid(base.light, partial?.light),
446
+ dark: mergeControlSolid(base.dark, partial?.dark)
447
+ };
448
+ }
449
+ function normalizeStyleConfig(partial) {
450
+ const validated = validateStyleConfigPartial(partial ?? void 0);
451
+ const defaults = createDefaultStyleConfig();
452
+ const normalized = {
453
+ iconLibrary: validated.iconLibrary ?? defaults.iconLibrary,
454
+ radius: validated.radius ?? defaults.radius,
455
+ fonts: mergeFonts(defaults.fonts, validated.fonts),
456
+ colors: mergeColors(defaults.colors, validated.colors)
457
+ };
458
+ if (validated.presetCode !== void 0) {
459
+ normalized.presetCode = validated.presetCode;
460
+ }
461
+ return normalized;
462
+ }
463
+
464
+ // ../wensity-preset/src/codec.ts
465
+ var CODEC_PAYLOAD_KEYS = ["v", "iconLibrary", "radius", "fonts", "colors"];
466
+ function assertPlainObject2(value, path) {
467
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
468
+ throw new PresetValidationError(`Expected an object at ${path}.`, path);
469
+ }
470
+ assertNoForbiddenObjectKeys(value, path);
471
+ }
472
+ function assertAllowedKeys2(value, allowed, path) {
473
+ const allowedSet = new Set(allowed);
474
+ for (const key of Object.keys(value)) {
475
+ if (!allowedSet.has(key)) {
476
+ throw new PresetValidationError(`Unknown key "${key}" at ${path}.`, `${path}.${key}`);
477
+ }
478
+ }
479
+ }
480
+ function utf8ByteLength(text) {
481
+ return new TextEncoder().encode(text).byteLength;
482
+ }
483
+ var BASE64URL_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
484
+ function toBase64Url(bytes) {
485
+ let out = "";
486
+ for (let i = 0; i < bytes.length; i += 3) {
487
+ const a = bytes[i];
488
+ const b = i + 1 < bytes.length ? bytes[i + 1] : 0;
489
+ const c = i + 2 < bytes.length ? bytes[i + 2] : 0;
490
+ const triple = a << 16 | b << 8 | c;
491
+ out += BASE64URL_ALPHABET[triple >> 18 & 63];
492
+ out += BASE64URL_ALPHABET[triple >> 12 & 63];
493
+ if (i + 1 < bytes.length) out += BASE64URL_ALPHABET[triple >> 6 & 63];
494
+ if (i + 2 < bytes.length) out += BASE64URL_ALPHABET[triple & 63];
495
+ }
496
+ return out;
497
+ }
498
+ function fromBase64Url(code) {
499
+ if (code.length === 0 || /[^A-Za-z0-9_-]/.test(code)) {
500
+ throw new PresetValidationError("Preset code is not valid base64url.", "preset");
501
+ }
502
+ const lookup = /* @__PURE__ */ new Map();
503
+ for (let i = 0; i < BASE64URL_ALPHABET.length; i++) {
504
+ lookup.set(BASE64URL_ALPHABET[i], i);
505
+ }
506
+ const out = new Uint8Array(Math.floor(code.length * 3 / 4));
507
+ let outIndex = 0;
508
+ for (let i = 0; i < code.length; i += 4) {
509
+ const c0 = lookup.get(code[i]);
510
+ const c1 = lookup.get(code[i + 1]);
511
+ const c2 = i + 2 < code.length ? lookup.get(code[i + 2]) : 0;
512
+ const c3 = i + 3 < code.length ? lookup.get(code[i + 3]) : 0;
513
+ if (c0 === void 0 || c1 === void 0 || c2 === void 0 || c3 === void 0) {
514
+ throw new PresetValidationError("Preset code is not valid base64url.", "preset");
515
+ }
516
+ const triple = c0 << 18 | c1 << 12 | c2 << 6 | c3;
517
+ if (outIndex < out.length) out[outIndex++] = triple >> 16 & 255;
518
+ if (i + 2 < code.length && outIndex < out.length) out[outIndex++] = triple >> 8 & 255;
519
+ if (i + 3 < code.length && outIndex < out.length) out[outIndex++] = triple & 255;
520
+ }
521
+ return outIndex === out.length ? out : out.subarray(0, outIndex);
522
+ }
523
+ function stableStringifyCodecPayload(payload) {
524
+ const parts = [
525
+ `"v":${payload.v}`,
526
+ `"iconLibrary":${JSON.stringify(payload.iconLibrary)}`,
527
+ `"radius":${JSON.stringify(payload.radius)}`,
528
+ `"fonts":${stableStringifyFonts(payload.fonts)}`
529
+ ];
530
+ if (payload.colors !== void 0) {
531
+ parts.push(`"colors":${stableStringifyColors(payload.colors)}`);
532
+ }
533
+ return `{${parts.join(",")}}`;
534
+ }
535
+ function stableStringifyFonts(fonts) {
536
+ return `{"sans":${JSON.stringify(fonts.sans)},"display":${JSON.stringify(fonts.display)},"mono":${JSON.stringify(fonts.mono)}}`;
537
+ }
538
+ function stableStringifyColors(colors) {
539
+ const modeParts = [];
540
+ for (const mode of COLOR_MODE_KEYS) {
541
+ const partial = colors[mode];
542
+ if (!partial) continue;
543
+ const keyParts = [];
544
+ for (const key of CONTROL_SOLID_COLOR_KEYS) {
545
+ const value = partial[key];
546
+ if (value !== void 0) {
547
+ keyParts.push(`${JSON.stringify(key)}:${JSON.stringify(value)}`);
548
+ }
549
+ }
550
+ if (keyParts.length > 0) {
551
+ modeParts.push(`${JSON.stringify(mode)}:{${keyParts.join(",")}}`);
552
+ }
553
+ }
554
+ return `{${modeParts.join(",")}}`;
555
+ }
556
+ function validateCodecFonts(value, path) {
557
+ assertPlainObject2(value, path);
558
+ assertAllowedKeys2(value, FONT_ROLE_KEYS, path);
559
+ for (const key of FONT_ROLE_KEYS) {
560
+ if (!(key in value)) {
561
+ throw new PresetValidationError(`Missing required font role "${key}" at ${path}.`, `${path}.${key}`);
562
+ }
563
+ }
564
+ const fonts = {};
565
+ for (const key of FONT_ROLE_KEYS) {
566
+ const raw = value[key];
567
+ if (typeof raw !== "string" || !isFontRoleValue(raw)) {
568
+ throw new PresetValidationError(
569
+ `Invalid font at ${path}.${key}. Expected site | system-sans | system-mono | custom:<stack>.`,
570
+ `${path}.${key}`
571
+ );
572
+ }
573
+ if (raw.startsWith(CUSTOM_FONT_PREFIX)) {
574
+ validateCustomFontStack(raw, `${path}.${key}`);
575
+ }
576
+ fonts[key] = raw;
577
+ }
578
+ return fonts;
579
+ }
580
+ function validateCodecColors(value, path) {
581
+ const partial = validateStyleConfigPartial({ colors: value });
582
+ if (!partial.colors) {
583
+ throw new PresetValidationError(`Invalid colors at ${path}.`, path);
584
+ }
585
+ return partial.colors;
586
+ }
587
+ function validateCodecPayload(value) {
588
+ assertPlainObject2(value, "preset");
589
+ assertAllowedKeys2(value, CODEC_PAYLOAD_KEYS, "preset");
590
+ if (value.v !== 1) {
591
+ throw new PresetValidationError(
592
+ typeof value.v === "number" ? `Unsupported preset codec version ${value.v}. Expected v: 1.` : "Preset payload missing v: 1.",
593
+ "preset.v"
594
+ );
595
+ }
596
+ if (typeof value.iconLibrary !== "string" || !isIconLibrary(value.iconLibrary)) {
597
+ throw new PresetValidationError(
598
+ "Invalid preset.iconLibrary. Expected tabler | lucide.",
599
+ "preset.iconLibrary"
600
+ );
601
+ }
602
+ if (typeof value.radius !== "string" || !isRadiusPreset(value.radius)) {
603
+ throw new PresetValidationError(
604
+ "Invalid preset.radius. Expected none | small | medium | large.",
605
+ "preset.radius"
606
+ );
607
+ }
608
+ if (!("fonts" in value)) {
609
+ throw new PresetValidationError("Preset payload missing fonts.", "preset.fonts");
610
+ }
611
+ const payload = {
612
+ v: 1,
613
+ iconLibrary: value.iconLibrary,
614
+ radius: value.radius,
615
+ fonts: validateCodecFonts(value.fonts, "preset.fonts")
616
+ };
617
+ if ("colors" in value) {
618
+ payload.colors = validateCodecColors(value.colors, "preset.colors");
619
+ }
620
+ return payload;
621
+ }
622
+ function codecPayloadToStyleInput(payload) {
623
+ const input = {
624
+ iconLibrary: payload.iconLibrary,
625
+ radius: payload.radius,
626
+ fonts: payload.fonts
627
+ };
628
+ if (payload.colors) {
629
+ input.colors = payload.colors;
630
+ }
631
+ return input;
632
+ }
633
+ function encodePreset(style) {
634
+ const normalized = normalizeStyleConfig(style);
635
+ for (const role of FONT_ROLE_KEYS) {
636
+ const value = normalized.fonts[role];
637
+ if (value.startsWith(CUSTOM_FONT_PREFIX)) {
638
+ validateCustomFontStack(value, `style.fonts.${role}`);
639
+ }
640
+ }
641
+ const colors = normalized.colors ? {
642
+ light: pickDefinedColors(normalized.colors.light),
643
+ dark: pickDefinedColors(normalized.colors.dark)
644
+ } : void 0;
645
+ const hasColors = colors && (colors.light && Object.keys(colors.light).length > 0 || colors.dark && Object.keys(colors.dark).length > 0);
646
+ const payload = {
647
+ v: 1,
648
+ iconLibrary: normalized.iconLibrary,
649
+ radius: normalized.radius,
650
+ fonts: { ...normalized.fonts },
651
+ ...hasColors ? { colors } : {}
652
+ };
653
+ const json = stableStringifyCodecPayload(payload);
654
+ const bytes = new TextEncoder().encode(json);
655
+ if (bytes.byteLength > PRESET_CODEC_MAX_DECODED_JSON_BYTES) {
656
+ throw new PresetValidationError(
657
+ `Preset JSON exceeds ${PRESET_CODEC_MAX_DECODED_JSON_BYTES} bytes.`,
658
+ "preset"
659
+ );
660
+ }
661
+ const code = `${PRESET_CODEC_PREFIX}.${toBase64Url(bytes)}`;
662
+ if (code.length > PRESET_CODEC_MAX_CODE_LENGTH) {
663
+ throw new PresetValidationError(
664
+ `Preset code exceeds ${PRESET_CODEC_MAX_CODE_LENGTH} characters.`,
665
+ "preset"
666
+ );
667
+ }
668
+ return code;
669
+ }
670
+ function pickDefinedColors(partial) {
671
+ if (!partial) return void 0;
672
+ const out = {};
673
+ for (const key of CONTROL_SOLID_COLOR_KEYS) {
674
+ if (partial[key] !== void 0) out[key] = partial[key];
675
+ }
676
+ return Object.keys(out).length > 0 ? out : void 0;
677
+ }
678
+ function decodePreset(code) {
679
+ if (typeof code !== "string" || code.length === 0) {
680
+ throw new PresetValidationError("Preset code is required.", "preset");
681
+ }
682
+ if (code.length > PRESET_CODEC_MAX_CODE_LENGTH) {
683
+ throw new PresetValidationError(
684
+ `Preset code exceeds ${PRESET_CODEC_MAX_CODE_LENGTH} characters.`,
685
+ "preset"
686
+ );
687
+ }
688
+ const prefix = `${PRESET_CODEC_PREFIX}.`;
689
+ if (!code.startsWith(prefix)) {
690
+ throw new PresetValidationError(
691
+ `Unsupported or missing preset prefix. Expected codes starting with "${PRESET_CODEC_PREFIX}".`,
692
+ "preset"
693
+ );
694
+ }
695
+ const encoded = code.slice(prefix.length);
696
+ if (!encoded) {
697
+ throw new PresetValidationError("Preset code payload is empty.", "preset");
698
+ }
699
+ if (encoded.includes("=") || /[^A-Za-z0-9_-]/.test(encoded)) {
700
+ throw new PresetValidationError("Preset code payload is not valid unpadded base64url.", "preset");
701
+ }
702
+ const bytes = fromBase64Url(encoded);
703
+ if (bytes.byteLength === 0) {
704
+ throw new PresetValidationError("Preset code decodes to empty payload.", "preset");
705
+ }
706
+ if (bytes.byteLength > PRESET_CODEC_MAX_DECODED_JSON_BYTES) {
707
+ throw new PresetValidationError(
708
+ `Decoded preset JSON exceeds ${PRESET_CODEC_MAX_DECODED_JSON_BYTES} bytes.`,
709
+ "preset"
710
+ );
711
+ }
712
+ let jsonText;
713
+ try {
714
+ jsonText = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
715
+ } catch {
716
+ throw new PresetValidationError("Preset payload is not valid UTF-8.", "preset");
717
+ }
718
+ if (utf8ByteLength(jsonText) > PRESET_CODEC_MAX_DECODED_JSON_BYTES) {
719
+ throw new PresetValidationError(
720
+ `Decoded preset JSON exceeds ${PRESET_CODEC_MAX_DECODED_JSON_BYTES} bytes.`,
721
+ "preset"
722
+ );
723
+ }
724
+ let parsed;
725
+ try {
726
+ parsed = JSON.parse(jsonText);
727
+ } catch {
728
+ throw new PresetValidationError("Preset payload is not valid JSON.", "preset");
729
+ }
730
+ return validateCodecPayload(parsed);
731
+ }
732
+
733
+ // ../wensity-preset/src/icon-map.ts
734
+ var ICON_TRANSFORM_EXCLUSIONS = [
735
+ "IconFileTypeCsv",
736
+ "IconFileTypeDoc",
737
+ "IconFileTypePdf",
738
+ "IconFileTypePpt",
739
+ "IconFileTypeXls",
740
+ "IconPointFilled",
741
+ "IconLoader2"
742
+ ];
743
+ var TABLER_TO_LUCIDE = {
744
+ IconAlertTriangle: "AlertTriangle",
745
+ IconArrowDownRight: "ArrowDownRight",
746
+ IconArrowLeft: "ArrowLeft",
747
+ IconArrowUpRight: "ArrowUpRight",
748
+ IconCalendar: "Calendar",
749
+ IconCamera: "Camera",
750
+ IconCheck: "Check",
751
+ IconChevronDown: "ChevronDown",
752
+ IconChevronLeft: "ChevronLeft",
753
+ IconChevronRight: "ChevronRight",
754
+ IconChevronUp: "ChevronUp",
755
+ IconChevronsLeft: "ChevronsLeft",
756
+ IconChevronsRight: "ChevronsRight",
757
+ IconCircleX: "CircleX",
758
+ IconClock: "Clock",
759
+ IconCornerDownLeft: "CornerDownLeft",
760
+ IconDots: "Ellipsis",
761
+ IconDownload: "Download",
762
+ IconFile: "File",
763
+ IconFileMusic: "FileAudio",
764
+ IconFileText: "FileText",
765
+ IconFileZip: "FileArchive",
766
+ IconInfoCircle: "Info",
767
+ IconLayoutSidebarLeftCollapse: "PanelLeftClose",
768
+ IconLayoutSidebarLeftExpand: "PanelLeftOpen",
769
+ IconMinus: "Minus",
770
+ IconPhoto: "Image",
771
+ IconPlus: "Plus",
772
+ IconRefresh: "RefreshCw",
773
+ IconSearch: "Search",
774
+ IconSelector: "ChevronsUpDown",
775
+ IconSlash: "Slash",
776
+ IconUser: "User",
777
+ IconVideo: "Video",
778
+ IconX: "X"
779
+ };
780
+ var EXCLUSION_SET = new Set(ICON_TRANSFORM_EXCLUSIONS);
781
+ function isIconTransformExclusion(name) {
782
+ return EXCLUSION_SET.has(name);
783
+ }
784
+ function isMappedTablerIcon(name) {
785
+ return Object.prototype.hasOwnProperty.call(TABLER_TO_LUCIDE, name);
786
+ }
787
+ function resolveLucideExport(tablerName) {
788
+ if (isIconTransformExclusion(tablerName)) {
789
+ throw new Error(
790
+ `Icon "${tablerName}" is a Phase 0 exclusion and must not be transformed. Use internal SVG/CSS geometry instead.`
791
+ );
792
+ }
793
+ if (!isMappedTablerIcon(tablerName)) {
794
+ throw new Error(
795
+ `No approved Lucide mapping for Tabler icon "${tablerName}". Add an audited map entry or convert it to internal geometry.`
796
+ );
797
+ }
798
+ return TABLER_TO_LUCIDE[tablerName];
799
+ }
800
+ var LUCIDE_TO_TABLER = Object.fromEntries(
801
+ Object.entries(TABLER_TO_LUCIDE).map(([tabler, lucide]) => [lucide, tabler])
802
+ );
6
803
 
7
804
  // src/constants.ts
8
805
  var CLI_NAME = "wensity";
9
- var CLI_VERSION = "0.1.5";
806
+ var CLI_VERSION = "0.1.6";
10
807
  var CONFIG_FILE_NAME = "wensity.json";
11
808
  var DEFAULT_API_ORIGIN = "https://ui.wensity.com";
12
809
  var DEFAULT_REGISTRY_PATH = "/api/registry";
@@ -14,10 +811,11 @@ var DEFAULT_COMPONENTS_DIR = "src/components/wensity";
14
811
  var DEFAULT_UTILS_PATH = "src/lib/utils";
15
812
  var DEFAULT_COMPONENTS_ALIAS = "@/components/wensity";
16
813
  var DEFAULT_UTILS_ALIAS = "@/lib/utils";
814
+ var INIT_PRIMITIVE_CSS_SLUG = "button";
17
815
  var HELP_TEXT = `Wensity CLI ${CLI_VERSION}
18
816
 
19
817
  Usage:
20
- wensity init [options]
818
+ wensity init [--preset <code>] [--preset-file <path>] [options]
21
819
  wensity login [options]
22
820
  wensity logout
23
821
  wensity whoami [--json]
@@ -27,7 +825,7 @@ Usage:
27
825
  wensity version
28
826
 
29
827
  Commands:
30
- init Create wensity.json and local helper files
828
+ init Create wensity.json and local helper files (optional --preset applies style + CSS)
31
829
  login Save a Wensity API token for premium component access
32
830
  logout Remove saved CLI credentials
33
831
  whoami Verify the saved token and print the authenticated account
@@ -41,12 +839,21 @@ Options:
41
839
  --registry <url> Override the registry endpoint
42
840
  --type <type> Select component or block, defaults to component
43
841
  --token <token> Provide a token non-interactively for login
842
+ --preset <code> Apply a shareable Wensity style preset (wsty1.\u2026); merges style when config exists
843
+ --preset-file <path> Read a preset code or JSON payload from a file
44
844
  --force Overwrite generated files when adding or initializing
45
845
  --no-install Write files without running the package manager
46
846
  --dry-run Print planned writes and installs without changing files
47
847
  --json Print machine-readable output where supported
48
848
  --help Show help
49
849
  --version Print the CLI version
850
+
851
+ Preset notes:
852
+ Generate codes at ${DEFAULT_API_ORIGIN}/docs/primitives/setup
853
+ Plain init refuses an existing wensity.json unless --force
854
+ init --preset merges style + primitive CSS; does not rewrite installed source
855
+ add/update read style.iconLibrary for UI primitives only (Tabler|Lucide)
856
+ Invalid/expired/unsupported codes exit non-zero with an actionable message
50
857
  `;
51
858
 
52
859
  // src/errors.ts
@@ -108,7 +915,7 @@ function hasFlag(args, name) {
108
915
  return args.options.get(name) === true;
109
916
  }
110
917
  function expectsValue(name) {
111
- return ["api-url", "registry", "token", "type"].includes(name);
918
+ return ["api-url", "registry", "token", "type", "preset", "preset-file"].includes(name);
112
919
  }
113
920
  function normalizeOptionName(name) {
114
921
  if (name === "dry") return "dry-run";
@@ -205,7 +1012,20 @@ async function loadConfig(cwd, overrides = {}) {
205
1012
  try {
206
1013
  const parsed = JSON.parse(await readFile(foundPath, "utf8"));
207
1014
  return {
208
- config: normalizeConfig({ ...defaults, ...parsed }, overrides),
1015
+ config: normalizeConfig(
1016
+ {
1017
+ ...defaults,
1018
+ ...parsed,
1019
+ aliases: {
1020
+ ...defaults.aliases,
1021
+ ...parsed.aliases
1022
+ },
1023
+ tailwind: parsed.tailwind ? { ...defaults.tailwind, ...parsed.tailwind } : defaults.tailwind,
1024
+ style: parsed.style,
1025
+ version: parsed.version ?? defaults.version
1026
+ },
1027
+ overrides
1028
+ ),
209
1029
  configPath,
210
1030
  projectRoot,
211
1031
  exists: true
@@ -214,6 +1034,9 @@ async function loadConfig(cwd, overrides = {}) {
214
1034
  if (error instanceof SyntaxError) {
215
1035
  throw new CliError(`${CONFIG_FILE_NAME} contains invalid JSON.`);
216
1036
  }
1037
+ if (error instanceof PresetValidationError) {
1038
+ throw new CliError(`${CONFIG_FILE_NAME}: ${error.message}`);
1039
+ }
217
1040
  throw error;
218
1041
  }
219
1042
  }
@@ -221,6 +1044,7 @@ function createDefaultConfig(projectRoot, apiUrl = DEFAULT_API_ORIGIN) {
221
1044
  const apiOrigin = normalizeUrlOrigin(apiUrl);
222
1045
  return {
223
1046
  $schema: joinUrl(apiOrigin, "/schema/wensity.json"),
1047
+ version: CONFIG_SCHEMA_VERSION,
224
1048
  registry: joinUrl(apiOrigin, DEFAULT_REGISTRY_PATH),
225
1049
  components: DEFAULT_COMPONENTS_DIR,
226
1050
  utils: DEFAULT_UTILS_PATH,
@@ -231,7 +1055,8 @@ function createDefaultConfig(projectRoot, apiUrl = DEFAULT_API_ORIGIN) {
231
1055
  aliases: {
232
1056
  components: DEFAULT_COMPONENTS_ALIAS,
233
1057
  utils: DEFAULT_UTILS_ALIAS
234
- }
1058
+ },
1059
+ style: createDefaultStyleConfig()
235
1060
  };
236
1061
  }
237
1062
  async function writeConfigFile(path, config, force) {
@@ -242,6 +1067,26 @@ async function writeConfigFile(path, config, force) {
242
1067
  await writeFile(path, `${JSON.stringify(config, null, 2)}
243
1068
  `, "utf8");
244
1069
  }
1070
+ async function mergeStyleIntoConfigFile(configPath, style) {
1071
+ const raw = JSON.parse(await readFile(configPath, "utf8"));
1072
+ const merged = {
1073
+ ...raw,
1074
+ version: raw.version ?? CONFIG_SCHEMA_VERSION,
1075
+ registry: raw.registry ?? "",
1076
+ components: raw.components ?? DEFAULT_COMPONENTS_DIR,
1077
+ utils: raw.utils ?? DEFAULT_UTILS_PATH,
1078
+ aliases: {
1079
+ components: raw.aliases?.components ?? DEFAULT_COMPONENTS_ALIAS,
1080
+ utils: raw.aliases?.utils ?? DEFAULT_UTILS_ALIAS
1081
+ },
1082
+ style
1083
+ };
1084
+ if (raw.$schema) merged.$schema = raw.$schema;
1085
+ if (raw.tailwind) merged.tailwind = raw.tailwind;
1086
+ await writeFile(configPath, `${JSON.stringify(merged, null, 2)}
1087
+ `, "utf8");
1088
+ return normalizeConfig(merged, {});
1089
+ }
245
1090
  function resolveConfiguredFilePath(projectRoot, relativePath, extension = "ts") {
246
1091
  try {
247
1092
  return resolveInside(projectRoot, withExtension(relativePath, extension));
@@ -271,23 +1116,39 @@ async function findUp(start, fileName) {
271
1116
  }
272
1117
  function normalizeConfig(config, overrides) {
273
1118
  const apiOrigin = overrides.apiUrl ? normalizeUrlOrigin(overrides.apiUrl) : void 0;
274
- return {
275
- ...config,
276
- registry: overrides.registry ?? (apiOrigin ? joinUrl(apiOrigin, DEFAULT_REGISTRY_PATH) : config.registry),
277
- components: config.components || DEFAULT_COMPONENTS_DIR,
278
- utils: config.utils || DEFAULT_UTILS_PATH,
279
- aliases: {
280
- components: config.aliases?.components || DEFAULT_COMPONENTS_ALIAS,
281
- utils: config.aliases?.utils || DEFAULT_UTILS_ALIAS
1119
+ try {
1120
+ return {
1121
+ ...config,
1122
+ version: config.version ?? CONFIG_SCHEMA_VERSION,
1123
+ registry: overrides.registry ?? (apiOrigin ? joinUrl(apiOrigin, DEFAULT_REGISTRY_PATH) : config.registry),
1124
+ components: config.components || DEFAULT_COMPONENTS_DIR,
1125
+ utils: config.utils || DEFAULT_UTILS_PATH,
1126
+ aliases: {
1127
+ components: config.aliases?.components || DEFAULT_COMPONENTS_ALIAS,
1128
+ utils: config.aliases?.utils || DEFAULT_UTILS_ALIAS
1129
+ },
1130
+ // In-memory only: missing style fields resolve to defaults without rewriting disk.
1131
+ style: normalizeStyleConfig(config.style)
1132
+ };
1133
+ } catch (error) {
1134
+ if (error instanceof PresetValidationError) {
1135
+ throw new CliError(`${CONFIG_FILE_NAME}: ${error.message}`);
282
1136
  }
283
- };
1137
+ throw error;
1138
+ }
284
1139
  }
285
1140
  function detectTailwindConfig(projectRoot) {
286
1141
  const candidates = ["tailwind.config.ts", "tailwind.config.js", "tailwind.config.mjs"];
287
1142
  return candidates.find((candidate) => existsSync(join2(projectRoot, candidate))) ?? "tailwind.config.ts";
288
1143
  }
289
1144
  function detectGlobalCss(projectRoot) {
290
- const candidates = ["src/app/globals.css", "src/styles/globals.css", "app/globals.css"];
1145
+ const candidates = [
1146
+ "src/app/globals.css",
1147
+ "src/styles/globals.css",
1148
+ "app/globals.css",
1149
+ "src/index.css",
1150
+ "styles/globals.css"
1151
+ ];
291
1152
  return candidates.find((candidate) => existsSync(join2(projectRoot, candidate))) ?? "src/app/globals.css";
292
1153
  }
293
1154
  function withExtension(path, extension) {
@@ -614,6 +1475,262 @@ Upgrade here: ${pricingBlue("https://ui.wensity.com/pricing")}`;
614
1475
  return message;
615
1476
  }
616
1477
 
1478
+ // src/icon-transform.ts
1479
+ var UI_PRIMITIVES_CATEGORY = "UI Primitives";
1480
+ var TABLER_ICONS_PACKAGE = "@tabler/icons-react";
1481
+ var LUCIDE_ICONS_PACKAGE = "lucide-react";
1482
+ var EMPTY_SWAP = { remove: [], add: [] };
1483
+ var NATIVE_SVG_TAGS = /* @__PURE__ */ new Set([
1484
+ "svg",
1485
+ "path",
1486
+ "circle",
1487
+ "rect",
1488
+ "line",
1489
+ "polyline",
1490
+ "polygon",
1491
+ "g",
1492
+ "defs",
1493
+ "use",
1494
+ "symbol",
1495
+ "clipPath",
1496
+ "mask",
1497
+ "linearGradient",
1498
+ "radialGradient",
1499
+ "stop",
1500
+ "text",
1501
+ "tspan",
1502
+ "foreignObject"
1503
+ ]);
1504
+ function createTablerAdapter() {
1505
+ return {
1506
+ id: "tabler",
1507
+ packageName: TABLER_ICONS_PACKAGE,
1508
+ oppositePackageName: LUCIDE_ICONS_PACKAGE,
1509
+ resolveExportFromTabler: (tablerName) => tablerName
1510
+ };
1511
+ }
1512
+ function createLucideAdapter() {
1513
+ return {
1514
+ id: "lucide",
1515
+ packageName: LUCIDE_ICONS_PACKAGE,
1516
+ oppositePackageName: TABLER_ICONS_PACKAGE,
1517
+ resolveExportFromTabler: (tablerName, context) => {
1518
+ try {
1519
+ return resolveLucideExport(tablerName);
1520
+ } catch (error) {
1521
+ const message = error instanceof Error ? error.message : String(error);
1522
+ throw new CliError(`${context}: ${message}`);
1523
+ }
1524
+ },
1525
+ strokePropTo: "strokeWidth"
1526
+ };
1527
+ }
1528
+ var ICON_LIBRARY_ADAPTERS = {
1529
+ tabler: createTablerAdapter,
1530
+ lucide: createLucideAdapter
1531
+ };
1532
+ function getIconLibraryAdapter(library) {
1533
+ return ICON_LIBRARY_ADAPTERS[library]();
1534
+ }
1535
+ function isPrimitivePayloadCategory(category) {
1536
+ return category === UI_PRIMITIVES_CATEGORY;
1537
+ }
1538
+ function transformIconLibrary(content, options) {
1539
+ if (!options.isPrimitiveCategory) {
1540
+ return { content, dependencySwap: EMPTY_SWAP };
1541
+ }
1542
+ const adapter = getIconLibraryAdapter(options.iconLibrary);
1543
+ const context = options.slug ? `primitive "${options.slug}"` : "primitive source";
1544
+ const hasTabler = hasPackageImport(content, TABLER_ICONS_PACKAGE);
1545
+ const hasLucide = hasPackageImport(content, LUCIDE_ICONS_PACKAGE);
1546
+ if (adapter.id === "lucide") {
1547
+ if (hasTabler) {
1548
+ const transformed = rewriteTablerImportsToTarget(content, adapter, context);
1549
+ return {
1550
+ content: rewriteStrokeProps(transformed.content, transformed.localBindings),
1551
+ dependencySwap: {
1552
+ remove: [TABLER_ICONS_PACKAGE],
1553
+ add: [LUCIDE_ICONS_PACKAGE]
1554
+ }
1555
+ };
1556
+ }
1557
+ if (hasLucide) {
1558
+ return {
1559
+ content,
1560
+ dependencySwap: {
1561
+ remove: [TABLER_ICONS_PACKAGE],
1562
+ add: [LUCIDE_ICONS_PACKAGE]
1563
+ }
1564
+ };
1565
+ }
1566
+ return { content, dependencySwap: EMPTY_SWAP };
1567
+ }
1568
+ if (hasLucide && !hasTabler) {
1569
+ return {
1570
+ content,
1571
+ dependencySwap: {
1572
+ remove: [LUCIDE_ICONS_PACKAGE],
1573
+ add: [TABLER_ICONS_PACKAGE]
1574
+ }
1575
+ };
1576
+ }
1577
+ if (hasTabler) {
1578
+ assertNoUnmappedTablerImports(content, context);
1579
+ return {
1580
+ content,
1581
+ dependencySwap: {
1582
+ remove: [LUCIDE_ICONS_PACKAGE],
1583
+ add: [TABLER_ICONS_PACKAGE]
1584
+ }
1585
+ };
1586
+ }
1587
+ return { content, dependencySwap: EMPTY_SWAP };
1588
+ }
1589
+ function aggregateIconDependencySwaps(swaps) {
1590
+ const remove = /* @__PURE__ */ new Set();
1591
+ const add = /* @__PURE__ */ new Set();
1592
+ for (const swap of swaps) {
1593
+ for (const pkg of swap.remove) remove.add(pkg);
1594
+ for (const pkg of swap.add) add.add(pkg);
1595
+ }
1596
+ return {
1597
+ remove: [...remove],
1598
+ add: [...add]
1599
+ };
1600
+ }
1601
+ function applyIconDependencySwap(declared, swap) {
1602
+ const remove = new Set(swap.remove);
1603
+ const next = declared.filter((dep) => !remove.has(dep));
1604
+ for (const pkg of swap.add) {
1605
+ if (!next.includes(pkg)) next.push(pkg);
1606
+ }
1607
+ return next;
1608
+ }
1609
+ function warnOppositeIconPackageMessage(iconLibrary, installedPackages) {
1610
+ const selected = iconLibrary === "lucide" ? LUCIDE_ICONS_PACKAGE : TABLER_ICONS_PACKAGE;
1611
+ const opposite = iconLibrary === "lucide" ? TABLER_ICONS_PACKAGE : LUCIDE_ICONS_PACKAGE;
1612
+ if (installedPackages.has(opposite) && !installedPackages.has(selected)) {
1613
+ return `Project package.json still lists ${opposite} while style.iconLibrary is "${iconLibrary}". Wensity will install ${selected} for new primitive installs but will not uninstall ${opposite} (it may be used outside Wensity).`;
1614
+ }
1615
+ if (installedPackages.has(opposite) && installedPackages.has(selected)) {
1616
+ return `Project package.json lists both ${selected} and ${opposite}. Wensity primitives use only ${selected} (style.iconLibrary="${iconLibrary}"); consider removing ${opposite} if unused.`;
1617
+ }
1618
+ return void 0;
1619
+ }
1620
+ function hasPackageImport(source, packageName) {
1621
+ const escaped = packageName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1622
+ return new RegExp(`from\\s+["']${escaped}["']`).test(source);
1623
+ }
1624
+ function rewriteTablerImportsToTarget(content, adapter, context) {
1625
+ const localBindings = /* @__PURE__ */ new Set();
1626
+ const importPattern = /^(?<indent>[ \t]*)(?<typeOnly>import\s+type\s+|import\s+)\{(?<clause>[^}]*)\}\s*from\s+(?<quote>["'])@tabler\/icons-react\k<quote>;?[ \t]*$/gm;
1627
+ const next = content.replace(
1628
+ importPattern,
1629
+ (full, indent, typePrefix, clause, quote) => {
1630
+ const specs = parseNamedImportClause(clause);
1631
+ if (specs.length === 0) return full;
1632
+ const rewritten = [];
1633
+ for (const spec of specs) {
1634
+ if (spec.isTypeOnly || typePrefix.includes("type")) {
1635
+ if (spec.imported === "Icon" || isIconTransformExclusion(spec.imported)) {
1636
+ throw new CliError(
1637
+ `${context}: unexpected type-only icon import "${spec.imported}" from ${TABLER_ICONS_PACKAGE}.`
1638
+ );
1639
+ }
1640
+ }
1641
+ if (isIconTransformExclusion(spec.imported)) {
1642
+ throw new CliError(
1643
+ `${context}: "${spec.imported}" is excluded from the icon map (Phase 0 internal geometry). It must not appear as a library import.`
1644
+ );
1645
+ }
1646
+ const targetExport = adapter.resolveExportFromTabler(spec.imported, context);
1647
+ localBindings.add(spec.local);
1648
+ if (adapter.id === "lucide") {
1649
+ if (targetExport === spec.local) {
1650
+ rewritten.push(targetExport);
1651
+ } else {
1652
+ rewritten.push(`${targetExport} as ${spec.local}`);
1653
+ }
1654
+ } else {
1655
+ rewritten.push(
1656
+ spec.imported === spec.local ? spec.imported : `${spec.imported} as ${spec.local}`
1657
+ );
1658
+ }
1659
+ }
1660
+ const prefix = typePrefix.startsWith("import type") ? "import type " : "import ";
1661
+ return `${indent}${prefix}{ ${rewritten.join(", ")} } from ${quote}${adapter.packageName}${quote};`;
1662
+ }
1663
+ );
1664
+ if (hasPackageImport(next, TABLER_ICONS_PACKAGE)) {
1665
+ throw new CliError(
1666
+ `${context}: failed to rewrite all ${TABLER_ICONS_PACKAGE} imports for ${adapter.id}.`
1667
+ );
1668
+ }
1669
+ if (/(?:import|export)\s+\*\s+as\s+\w+\s+from\s+["']@tabler\/icons-react["']/.test(content)) {
1670
+ throw new CliError(
1671
+ `${context}: namespace imports from ${TABLER_ICONS_PACKAGE} are not supported. Use named imports.`
1672
+ );
1673
+ }
1674
+ return { content: next, localBindings };
1675
+ }
1676
+ function assertNoUnmappedTablerImports(content, context) {
1677
+ const importPattern = /\{([^}]*)\}\s*from\s+["']@tabler\/icons-react["']/g;
1678
+ for (const match of content.matchAll(importPattern)) {
1679
+ const specs = parseNamedImportClause(match[1] ?? "");
1680
+ for (const spec of specs) {
1681
+ if (isIconTransformExclusion(spec.imported)) {
1682
+ throw new CliError(
1683
+ `${context}: "${spec.imported}" must not appear as a library import (Phase 0 exclusion).`
1684
+ );
1685
+ }
1686
+ if (spec.imported.startsWith("Icon")) {
1687
+ resolveLucideExport(spec.imported);
1688
+ }
1689
+ }
1690
+ }
1691
+ }
1692
+ function parseNamedImportClause(clause) {
1693
+ const specs = [];
1694
+ for (const raw of clause.split(",")) {
1695
+ const part = raw.trim();
1696
+ if (!part) continue;
1697
+ const typeOnly = /^type\s+/.test(part);
1698
+ const body = typeOnly ? part.replace(/^type\s+/, "").trim() : part;
1699
+ const asMatch = body.match(/^(\w+)\s+as\s+(\w+)$/);
1700
+ if (asMatch) {
1701
+ specs.push({
1702
+ imported: asMatch[1],
1703
+ local: asMatch[2],
1704
+ isTypeOnly: typeOnly
1705
+ });
1706
+ continue;
1707
+ }
1708
+ if (/^\w+$/.test(body)) {
1709
+ specs.push({ imported: body, local: body, isTypeOnly: typeOnly });
1710
+ }
1711
+ }
1712
+ return specs;
1713
+ }
1714
+ function rewriteStrokeProps(content, localBindings) {
1715
+ if (localBindings.size === 0) return content;
1716
+ const tags = new Set(localBindings);
1717
+ if (localBindings.size > 0 && /\bFileIcon\b/.test(content) && /\bpackIconMap\b/.test(content)) {
1718
+ tags.add("FileIcon");
1719
+ }
1720
+ const tagAlternation = [...tags].map((tag) => tag.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|");
1721
+ if (!tagAlternation) return content;
1722
+ const openTagPattern = new RegExp(`<(${tagAlternation})(\\s[\\s\\S]*?)(/?)>`, "g");
1723
+ return content.replace(openTagPattern, (full, tagName, attrs, selfClose) => {
1724
+ if (NATIVE_SVG_TAGS.has(tagName)) return full;
1725
+ if (!attrs) return full;
1726
+ const nextAttrs = attrs.replace(
1727
+ /(^|[\s])stroke(\s*=\s*(?:\{[^}]*\}|"[^"]*"|'[^']*'))/g,
1728
+ "$1strokeWidth$2"
1729
+ );
1730
+ return `<${tagName}${nextAttrs}${selfClose}>`;
1731
+ });
1732
+ }
1733
+
617
1734
  // src/package-manager.ts
618
1735
  import { existsSync as existsSync2, readFileSync } from "fs";
619
1736
  import { spawnSync } from "child_process";
@@ -626,16 +1743,22 @@ function detectPackageManager(projectRoot) {
626
1743
  return "npm";
627
1744
  }
628
1745
  function missingDependencies(projectRoot, dependencies) {
1746
+ const installed = readInstalledPackageNames(projectRoot);
1747
+ if (!installed) return dependencies;
1748
+ return dependencies.filter((dependency) => !installed.has(dependency));
1749
+ }
1750
+ function readInstalledPackageNames(projectRoot) {
629
1751
  const packageJsonPath = join4(projectRoot, "package.json");
630
- if (!existsSync2(packageJsonPath)) return dependencies;
1752
+ if (!existsSync2(packageJsonPath)) return void 0;
631
1753
  const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
632
- const installed = {
633
- ...packageJson.dependencies,
634
- ...packageJson.devDependencies,
635
- ...packageJson.peerDependencies,
636
- ...packageJson.optionalDependencies
637
- };
638
- return dependencies.filter((dependency) => !installed[dependency]);
1754
+ return new Set(
1755
+ Object.keys({
1756
+ ...packageJson.dependencies,
1757
+ ...packageJson.devDependencies,
1758
+ ...packageJson.peerDependencies,
1759
+ ...packageJson.optionalDependencies
1760
+ })
1761
+ );
639
1762
  }
640
1763
  function runInstall(projectRoot, dependencies) {
641
1764
  if (dependencies.length === 0) return;
@@ -671,9 +1794,179 @@ function installArgs(packageManager, dependencies) {
671
1794
  }
672
1795
  }
673
1796
 
674
- // src/component-install.ts
1797
+ // src/preset.ts
1798
+ import { readFile as readFile3 } from "fs/promises";
1799
+ async function resolvePresetStyle(input) {
1800
+ if (input.presetCode && input.presetFile) {
1801
+ throw new CliError("Use either --preset or --preset-file, not both.");
1802
+ }
1803
+ let code = input.presetCode;
1804
+ if (input.presetFile) {
1805
+ try {
1806
+ const raw = await readFile3(input.presetFile, "utf8");
1807
+ const trimmed = raw.trim();
1808
+ if (trimmed.startsWith("{")) {
1809
+ const parsed = JSON.parse(trimmed);
1810
+ if (parsed && typeof parsed === "object" && "v" in parsed) {
1811
+ const payload = validateCodecPayload(parsed);
1812
+ const json = stableStringifyCodecPayload(payload);
1813
+ code = `${PRESET_CODEC_PREFIX}.${Buffer.from(json, "utf8").toString("base64url")}`;
1814
+ } else {
1815
+ code = encodePreset(parsed);
1816
+ }
1817
+ } else {
1818
+ code = trimmed;
1819
+ }
1820
+ } catch (error) {
1821
+ if (error instanceof PresetValidationError) {
1822
+ throw new CliError(`Invalid preset file: ${error.message}`);
1823
+ }
1824
+ if (error instanceof SyntaxError) {
1825
+ throw new CliError(`Preset file contains invalid JSON: ${error.message}`);
1826
+ }
1827
+ if (error instanceof CliError) throw error;
1828
+ const message = error instanceof Error ? error.message : String(error);
1829
+ throw new CliError(`Failed to read preset file: ${message}`);
1830
+ }
1831
+ }
1832
+ if (!code) {
1833
+ throw new CliError("Missing preset. Pass --preset <code> or --preset-file <path>.");
1834
+ }
1835
+ try {
1836
+ const trimmed = code.trim();
1837
+ const payload = decodePreset(trimmed);
1838
+ const style = normalizeStyleConfig({
1839
+ ...codecPayloadToStyleInput(payload),
1840
+ presetCode: trimmed
1841
+ });
1842
+ return { style, presetCode: trimmed };
1843
+ } catch (error) {
1844
+ if (error instanceof PresetValidationError) {
1845
+ throw new CliError(`Invalid preset: ${error.message}`);
1846
+ }
1847
+ throw error;
1848
+ }
1849
+ }
1850
+
1851
+ // src/preset-init-writes.ts
675
1852
  import { mkdir as mkdir3, writeFile as writeFile3 } from "fs/promises";
676
- import { dirname as dirname4, join as join5, relative as relative3 } from "path";
1853
+ import { dirname as dirname4 } from "path";
1854
+ async function commitPresetInitWrites(input) {
1855
+ let cssWritten = false;
1856
+ try {
1857
+ if (input.cssChanged) {
1858
+ await mkdir3(dirname4(input.cssAbsolute), { recursive: true });
1859
+ await writeFile3(input.cssAbsolute, input.nextCss, "utf8");
1860
+ cssWritten = true;
1861
+ }
1862
+ if (input.writeUtility) {
1863
+ await mkdir3(dirname4(input.utilityAbsolutePath), { recursive: true });
1864
+ await writeFile3(input.utilityAbsolutePath, input.utilityContent, "utf8");
1865
+ }
1866
+ await input.commitConfig();
1867
+ } catch (error) {
1868
+ if (cssWritten) {
1869
+ try {
1870
+ await writeFile3(input.cssAbsolute, input.existingCss, "utf8");
1871
+ } catch {
1872
+ }
1873
+ }
1874
+ throw error;
1875
+ }
1876
+ }
1877
+
1878
+ // src/style-css.ts
1879
+ var ROOT_SELECTOR = ":root";
1880
+ var DARK_SELECTOR = ".dark";
1881
+ function applyStyleToPrimitiveCss(block, styleInput) {
1882
+ const style = normalizeStyleConfig(styleInput);
1883
+ let next = block;
1884
+ const radiusValue = RADIUS_PRESET_CSS[style.radius];
1885
+ next = replaceDeclInSelector(next, ROOT_SELECTOR, "--primitive-radius", radiusValue);
1886
+ next = replaceDeclInSelector(next, DARK_SELECTOR, "--primitive-radius", radiusValue);
1887
+ for (const role of FONT_ROLE_KEYS) {
1888
+ const cssVar = `--primitive-font-${role}`;
1889
+ const cssValue = expandFontRole(style.fonts[role], role);
1890
+ next = replaceDeclInSelector(next, ROOT_SELECTOR, cssVar, cssValue);
1891
+ next = replaceDeclInSelector(next, DARK_SELECTOR, cssVar, cssValue);
1892
+ }
1893
+ if (style.colors?.light) {
1894
+ for (const key of CONTROL_SOLID_COLOR_KEYS) {
1895
+ const value = style.colors.light[key];
1896
+ if (value === void 0) continue;
1897
+ next = replaceDeclInSelector(next, ROOT_SELECTOR, CONTROL_SOLID_CSS_VARS[key], value);
1898
+ }
1899
+ }
1900
+ if (style.colors?.dark) {
1901
+ for (const key of CONTROL_SOLID_COLOR_KEYS) {
1902
+ const value = style.colors.dark[key];
1903
+ if (value === void 0) continue;
1904
+ next = replaceDeclInSelector(next, DARK_SELECTOR, CONTROL_SOLID_CSS_VARS[key], value);
1905
+ }
1906
+ }
1907
+ return next;
1908
+ }
1909
+ function replaceDeclInSelector(css, selector, property, value) {
1910
+ const block = findTopLevelRule(css, selector);
1911
+ if (!block) return css;
1912
+ const declPattern = new RegExp(
1913
+ `(${escapeRegExp(property)}\\s*:\\s*)([^;]+)(;)`
1914
+ );
1915
+ if (!declPattern.test(block.body)) {
1916
+ return css;
1917
+ }
1918
+ const updatedBody = block.body.replace(declPattern, `$1${value}$3`);
1919
+ if (updatedBody === block.body) return css;
1920
+ return `${css.slice(0, block.bodyStart)}${updatedBody}${css.slice(block.bodyEnd)}`;
1921
+ }
1922
+ function findTopLevelRule(css, selector) {
1923
+ const needle = `${selector} {`;
1924
+ let searchFrom = 0;
1925
+ while (searchFrom < css.length) {
1926
+ const start = css.indexOf(needle, searchFrom);
1927
+ if (start === -1) return void 0;
1928
+ const bodyStart = start + needle.length;
1929
+ const bodyEnd = findMatchingBrace(css, bodyStart - 1);
1930
+ if (bodyEnd === -1) return void 0;
1931
+ if (isLikelyTopLevel(css, start)) {
1932
+ return {
1933
+ bodyStart,
1934
+ bodyEnd,
1935
+ body: css.slice(bodyStart, bodyEnd)
1936
+ };
1937
+ }
1938
+ searchFrom = bodyEnd + 1;
1939
+ }
1940
+ return void 0;
1941
+ }
1942
+ function isLikelyTopLevel(css, index) {
1943
+ let depth = 0;
1944
+ for (let i = 0; i < index; i += 1) {
1945
+ const ch = css[i];
1946
+ if (ch === "{") depth += 1;
1947
+ else if (ch === "}") depth -= 1;
1948
+ }
1949
+ return depth === 0;
1950
+ }
1951
+ function findMatchingBrace(css, openIndex) {
1952
+ let depth = 0;
1953
+ for (let i = openIndex; i < css.length; i += 1) {
1954
+ const ch = css[i];
1955
+ if (ch === "{") depth += 1;
1956
+ else if (ch === "}") {
1957
+ depth -= 1;
1958
+ if (depth === 0) return i;
1959
+ }
1960
+ }
1961
+ return -1;
1962
+ }
1963
+ function escapeRegExp(value) {
1964
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1965
+ }
1966
+
1967
+ // src/component-install.ts
1968
+ import { mkdir as mkdir4, readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
1969
+ import { dirname as dirname5, join as join5, relative as relative3 } from "path";
677
1970
 
678
1971
  // src/templates.ts
679
1972
  function cnUtilityTemplate() {
@@ -830,27 +2123,46 @@ export function WensitySecondaryButton({
830
2123
 
831
2124
  // src/component-install.ts
832
2125
  var ALWAYS_PRESENT_PACKAGES = /* @__PURE__ */ new Set(["react", "react-dom", "next"]);
833
- async function prepareInstall(projectRoot, config, payload) {
2126
+ var OVERLAY_HELPER_PATH = "components/wensity/overlay.tsx";
2127
+ var PRIMITIVE_CSS_START_MARKER = "/* \u2500\u2500 Wensity Primitive Tokens";
2128
+ var PRIMITIVE_CSS_END_MARKER = "/* \u2500\u2500 End Wensity Primitive Tokens \u2500\u2500 */";
2129
+ async function prepareInstall(projectRoot, config, payload, options = {}) {
834
2130
  const fallbackFileName = componentFileName(payload.slug);
835
2131
  const files = [];
836
2132
  const componentContents = [];
837
- let needsUtility = false;
838
- let needsPrimitives = false;
839
- for (const payloadFile of payload.files) {
840
- const safePayloadPath = sanitizePayloadPath(payloadFile.path, fallbackFileName);
841
- const relativeOutputPath = toPosixPath(join5(config.components, safePayloadPath));
842
- const transformed = transformComponentSource(payloadFile.content, config);
843
- needsUtility ||= transformed.needsUtility || transformed.needsPrimitives;
844
- needsPrimitives ||= transformed.needsPrimitives;
845
- componentContents.push(transformed.content);
846
- files.push({
847
- absolutePath: resolveInside(projectRoot, relativeOutputPath),
848
- relativePath: relativeOutputPath,
849
- content: transformed.content,
850
- kind: "component"
2133
+ const iconSwaps = [];
2134
+ const style = normalizeStyleConfig(config.style);
2135
+ const isPrimitiveCategory = isPrimitivePayloadCategory(payload.category);
2136
+ const payloadHelperNeeds = normalizePayloadHelperNeeds(
2137
+ "helperNeeds" in payload ? payload.helperNeeds : void 0
2138
+ );
2139
+ const inferredHelperNeeds = { utils: false, primitives: false, overlay: false };
2140
+ const hasGraphHelperFiles = payload.files.some((file) => isGraphHelperFile(file));
2141
+ for (const [index, payloadFile] of payload.files.entries()) {
2142
+ const payloadFileKind = getPayloadFileKind(payloadFile);
2143
+ const relativeOutputPath = getPayloadOutputPath(payloadFile, fallbackFileName, config);
2144
+ const absoluteOutputPath = resolveInside(projectRoot, relativeOutputPath);
2145
+ if (!payloadHelperNeeds) mergeHelperNeeds(inferredHelperNeeds, inferHelperNeeds(payloadFile.content));
2146
+ const transformed = transformComponentSource(payloadFile.content, config, {
2147
+ isPrimitiveCategory,
2148
+ iconLibrary: style.iconLibrary,
2149
+ slug: payload.slug
851
2150
  });
2151
+ componentContents.push(transformed.content);
2152
+ iconSwaps.push(transformed.dependencySwap);
2153
+ const isPrimaryFile = index === 0 && payloadFileKind === "component";
2154
+ const shouldWriteFile = isPrimaryFile || options.overwriteExistingDependencyFiles || !await fileExists(absoluteOutputPath);
2155
+ if (shouldWriteFile) {
2156
+ files.push({
2157
+ absolutePath: absoluteOutputPath,
2158
+ relativePath: relativeOutputPath,
2159
+ content: transformed.content,
2160
+ kind: payloadFileKind
2161
+ });
2162
+ }
852
2163
  }
853
- if (needsUtility) {
2164
+ const helperNeeds = payloadHelperNeeds ?? inferredHelperNeeds;
2165
+ if (!hasGraphHelperFiles && helperNeeds.utils) {
854
2166
  const utilityPath = withSourceExtension(config.utils, "ts");
855
2167
  if (!await fileExists(resolveInside(projectRoot, utilityPath))) {
856
2168
  files.push({
@@ -861,7 +2173,7 @@ async function prepareInstall(projectRoot, config, payload) {
861
2173
  });
862
2174
  }
863
2175
  }
864
- if (needsPrimitives) {
2176
+ if (!hasGraphHelperFiles && helperNeeds.primitives) {
865
2177
  const primitivesPath = toPosixPath(join5(config.components, "primitives.tsx"));
866
2178
  if (!await fileExists(resolveInside(projectRoot, primitivesPath))) {
867
2179
  const primitives = primitivesTemplate(config.aliases.utils);
@@ -874,17 +2186,69 @@ async function prepareInstall(projectRoot, config, payload) {
874
2186
  componentContents.push(primitives);
875
2187
  }
876
2188
  }
2189
+ const iconDependencySwap = aggregateIconDependencySwaps(iconSwaps);
2190
+ const declaredAfterSwap = applyIconDependencySwap(payload.dependencies ?? [], iconDependencySwap);
877
2191
  const dependencies = collectDependencies({
878
- declared: payload.dependencies ?? [],
2192
+ declared: declaredAfterSwap,
879
2193
  source: componentContents.join("\n"),
880
- needsUtility
2194
+ needsUtility: helperNeeds.utils
881
2195
  });
882
- return { files, dependencies };
2196
+ const cssFile = await planPrimitiveCssFile(projectRoot, config, payload);
2197
+ return {
2198
+ files,
2199
+ dependencies,
2200
+ ...cssFile ? { cssFile } : {},
2201
+ ...isPrimitiveCategory ? { iconDependencySwap } : {}
2202
+ };
2203
+ }
2204
+ async function planPrimitiveCssFile(projectRoot, config, payload) {
2205
+ if (!("primitiveCss" in payload) || typeof payload.primitiveCss !== "string") {
2206
+ return void 0;
2207
+ }
2208
+ let primitiveCss = payload.primitiveCss.trim();
2209
+ if (!primitiveCss) return void 0;
2210
+ const style = normalizeStyleConfig(config.style);
2211
+ primitiveCss = applyStyleToPrimitiveCss(primitiveCss, style).trim();
2212
+ const cssRelative = config.tailwind?.css;
2213
+ if (!cssRelative) return void 0;
2214
+ const absolutePath = resolveInside(projectRoot, cssRelative);
2215
+ const existing = await fileExists(absolutePath) ? await readFile4(absolutePath, "utf8") : "";
2216
+ const next = upsertPrimitiveCssBlock(existing, primitiveCss);
2217
+ if (next === existing) return void 0;
2218
+ return {
2219
+ absolutePath,
2220
+ relativePath: toPosixPath(cssRelative),
2221
+ content: next,
2222
+ kind: "config"
2223
+ };
2224
+ }
2225
+ function upsertPrimitiveCssBlock(existingCss, block) {
2226
+ const normalizedBlock = block.trim();
2227
+ const start = existingCss.indexOf(PRIMITIVE_CSS_START_MARKER);
2228
+ const end = existingCss.indexOf(PRIMITIVE_CSS_END_MARKER);
2229
+ if (start !== -1 && end !== -1 && end > start) {
2230
+ const before = existingCss.slice(0, start).replace(/\s*$/, "\n\n");
2231
+ const after = existingCss.slice(end + PRIMITIVE_CSS_END_MARKER.length).replace(/^\s*/, "\n");
2232
+ return `${before}${normalizedBlock}${after}`.replace(/\n{3,}/g, "\n\n");
2233
+ }
2234
+ if (start !== -1 || end !== -1) {
2235
+ throw new CliError(
2236
+ "Configured Tailwind CSS has a partial Wensity Primitive Tokens block. Remove the broken markers and re-run install."
2237
+ );
2238
+ }
2239
+ const trimmed = existingCss.replace(/\s*$/, "");
2240
+ if (!trimmed) return `${normalizedBlock}
2241
+ `;
2242
+ return `${trimmed}
2243
+
2244
+ ${normalizedBlock}
2245
+ `;
883
2246
  }
884
2247
  async function assertNoConflicts(files, force) {
885
2248
  if (force) return;
886
2249
  const conflicts = [];
887
2250
  for (const file of files) {
2251
+ if (file.kind === "config") continue;
888
2252
  if (await fileExists(file.absolutePath)) conflicts.push(file.relativePath);
889
2253
  }
890
2254
  if (conflicts.length > 0) {
@@ -894,27 +2258,93 @@ ${conflicts.map((path) => ` - ${path}`).join("\n")}`);
894
2258
  }
895
2259
  async function writePlannedFiles(files) {
896
2260
  for (const file of files) {
897
- await mkdir3(dirname4(file.absolutePath), { recursive: true });
898
- await writeFile3(file.absolutePath, file.content, "utf8");
2261
+ await mkdir4(dirname5(file.absolutePath), { recursive: true });
2262
+ await writeFile4(file.absolutePath, file.content, "utf8");
899
2263
  }
900
2264
  }
901
2265
  function summarizeFiles(projectRoot, files) {
902
2266
  return files.map((file) => toPosixPath(relative3(projectRoot, file.absolutePath)));
903
2267
  }
904
- function transformComponentSource(content, config) {
905
- const needsUtility = content.includes("@wensity/ui/lib") || importsFrom(content, "@/lib/utils");
906
- const needsPrimitives = content.includes("@wensity/ui/components") || importsFrom(content, "@/components/wensity/primitives");
2268
+ function transformComponentSource(content, config, iconOptions) {
907
2269
  let transformed = content;
908
2270
  transformed = transformed.replace(/from\s+["']@wensity\/ui\/lib["']/g, `from "${config.aliases.utils}"`);
909
2271
  transformed = transformed.replace(/from\s+["']@\/lib\/utils["']/g, `from "${config.aliases.utils}"`);
910
- transformed = transformed.replace(/from\s+["']@wensity\/ui\/components["']/g, `from "${config.aliases.components}/primitives"`);
911
- transformed = transformed.replace(/from\s+["']@\/components\/wensity\/primitives["']/g, `from "${config.aliases.components}/primitives"`);
912
- return { content: transformed, needsUtility, needsPrimitives };
2272
+ transformed = transformed.replace(/from\s+["']@wensity\/ui\/components["']/g, `from "@/components/wensity/primitives"`);
2273
+ transformed = rewriteWensityComponentAlias(transformed, config.aliases.components);
2274
+ const iconResult = transformIconLibrary(transformed, {
2275
+ isPrimitiveCategory: iconOptions.isPrimitiveCategory,
2276
+ iconLibrary: iconOptions.iconLibrary,
2277
+ slug: iconOptions.slug
2278
+ });
2279
+ return { content: iconResult.content, dependencySwap: iconResult.dependencySwap };
2280
+ }
2281
+ function getPayloadFileKind(payloadFile) {
2282
+ if ("kind" in payloadFile && payloadFile.kind === "helper") return "helper";
2283
+ return isGraphHelperFile(payloadFile) ? "helper" : "component";
2284
+ }
2285
+ function getPayloadOutputPath(payloadFile, fallbackFileName, config) {
2286
+ if (isUtilsHelperFile(payloadFile)) return withSourceExtension(config.utils, "ts");
2287
+ if (isOverlayHelperFile(payloadFile)) return toPosixPath(join5(config.components, "overlay.tsx"));
2288
+ if (isPrimitivesHelperFile(payloadFile)) return toPosixPath(join5(config.components, "primitives.tsx"));
2289
+ if (payloadFile.kind === "helper") {
2290
+ throw new CliError(`Unsupported registry helper file path: ${payloadFile.path}`);
2291
+ }
2292
+ const safePayloadPath = sanitizePayloadPath(payloadFile.path, fallbackFileName);
2293
+ return toPosixPath(join5(config.components, safePayloadPath));
2294
+ }
2295
+ function isGraphHelperFile(payloadFile) {
2296
+ return isUtilsHelperFile(payloadFile) || isOverlayHelperFile(payloadFile) || isPrimitivesHelperFile(payloadFile);
2297
+ }
2298
+ function isUtilsHelperFile(payloadFile) {
2299
+ const normalized = payloadFile.path.replace(/\\/g, "/").replace(/^\/+/, "");
2300
+ return payloadFile.kind === "helper" && (normalized === "lib/utils.ts" || normalized === "src/lib/utils.ts");
2301
+ }
2302
+ function isOverlayHelperFile(payloadFile) {
2303
+ const normalized = payloadFile.path.replace(/\\/g, "/").replace(/^\/+/, "");
2304
+ return payloadFile.kind === "helper" && (normalized === OVERLAY_HELPER_PATH || normalized === `src/${OVERLAY_HELPER_PATH}`);
2305
+ }
2306
+ function isPrimitivesHelperFile(payloadFile) {
2307
+ const normalized = payloadFile.path.replace(/\\/g, "/").replace(/^\/+/, "");
2308
+ return payloadFile.kind === "helper" && (normalized === "components/wensity/primitives.tsx" || normalized === "src/components/wensity/primitives.tsx");
2309
+ }
2310
+ function rewriteWensityComponentAlias(source, componentsAlias) {
2311
+ const replacement = componentsAlias.replace(/\/$/, "");
2312
+ return source.replace(
2313
+ /(from\s+["'])@\/components\/wensity(\/[^"']*)?(["'])/g,
2314
+ (_match, prefix, suffix, quote) => `${prefix}${replacement}${suffix ?? ""}${quote}`
2315
+ ).replace(
2316
+ /(import\(\s*["'])@\/components\/wensity(\/[^"']*)?(["']\s*\))/g,
2317
+ (_match, prefix, suffix, suffixQuote) => `${prefix}${replacement}${suffix ?? ""}${suffixQuote}`
2318
+ );
913
2319
  }
914
2320
  function importsFrom(source, specifier) {
915
2321
  const escaped = specifier.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
916
2322
  return new RegExp(`from\\s+["']${escaped}["']`).test(source);
917
2323
  }
2324
+ function normalizePayloadHelperNeeds(value) {
2325
+ if (!value || typeof value !== "object") return void 0;
2326
+ const helperNeeds = value;
2327
+ if (typeof helperNeeds.utils !== "boolean" || typeof helperNeeds.primitives !== "boolean") {
2328
+ return void 0;
2329
+ }
2330
+ const overlay = helperNeeds.overlay === true;
2331
+ return {
2332
+ utils: helperNeeds.primitives || overlay || helperNeeds.utils,
2333
+ primitives: helperNeeds.primitives,
2334
+ overlay
2335
+ };
2336
+ }
2337
+ function inferHelperNeeds(source) {
2338
+ const overlay = importsFrom(source, "@/components/wensity/overlay");
2339
+ const primitives = source.includes("@wensity/ui/components") || importsFrom(source, "@/components/wensity/primitives");
2340
+ const utils = overlay || primitives || source.includes("@wensity/ui/lib") || importsFrom(source, "@/lib/utils");
2341
+ return { utils, primitives, overlay };
2342
+ }
2343
+ function mergeHelperNeeds(target, source) {
2344
+ target.overlay ||= source.overlay === true;
2345
+ target.primitives ||= source.primitives;
2346
+ target.utils ||= source.utils || source.primitives || source.overlay === true;
2347
+ }
918
2348
  function collectDependencies(input) {
919
2349
  const dependencies = /* @__PURE__ */ new Set();
920
2350
  for (const dependency of input.declared) {
@@ -1008,11 +2438,26 @@ async function initCommand(context, parsed) {
1008
2438
  const force = hasFlag(parsed, "force");
1009
2439
  const install = !hasFlag(parsed, "no-install");
1010
2440
  const apiUrl = getStringOption(parsed, "api-url") ?? context.env.WENSITY_API_URL ?? DEFAULT_API_ORIGIN;
2441
+ const presetCode = getStringOption(parsed, "preset");
2442
+ const presetFile = getStringOption(parsed, "preset-file");
2443
+ const hasPreset = Boolean(presetCode || presetFile);
2444
+ const dependencies = ["clsx", "tailwind-merge"];
2445
+ if (hasPreset) {
2446
+ await initWithPreset(context, {
2447
+ dryRun,
2448
+ force,
2449
+ install,
2450
+ apiUrl,
2451
+ presetCode,
2452
+ presetFile,
2453
+ dependencies
2454
+ });
2455
+ return;
2456
+ }
1011
2457
  const config = createDefaultConfig(context.cwd, apiUrl);
1012
2458
  const configPath = join6(context.cwd, CONFIG_FILE_NAME);
1013
2459
  const utilityPath = resolveConfiguredFilePath(context.cwd, config.utils);
1014
2460
  const utilityRelativePath = toPosixPath(config.utils.endsWith(".ts") ? config.utils : `${config.utils}.ts`);
1015
- const dependencies = ["clsx", "tailwind-merge"];
1016
2461
  if (dryRun) {
1017
2462
  printTitle("Project initialization preview");
1018
2463
  print(action("Would write:"));
@@ -1039,6 +2484,104 @@ async function initCommand(context, parsed) {
1039
2484
  "Install your chosen slug: npx wensity add <component-slug>"
1040
2485
  ]));
1041
2486
  }
2487
+ async function initWithPreset(context, options) {
2488
+ const { style, presetCode } = await resolvePresetStyle({
2489
+ presetCode: options.presetCode,
2490
+ presetFile: options.presetFile
2491
+ });
2492
+ const configPath = join6(context.cwd, CONFIG_FILE_NAME);
2493
+ const configExists = await fileExists(configPath);
2494
+ let config;
2495
+ if (configExists && !options.force) {
2496
+ const loaded = await loadConfig(context.cwd, { apiUrl: options.apiUrl });
2497
+ config = {
2498
+ ...loaded.config,
2499
+ style
2500
+ };
2501
+ } else {
2502
+ config = {
2503
+ ...createDefaultConfig(context.cwd, options.apiUrl),
2504
+ style
2505
+ };
2506
+ }
2507
+ const utilityPath = resolveConfiguredFilePath(context.cwd, config.utils);
2508
+ const utilityRelativePath = toPosixPath(config.utils.endsWith(".ts") ? config.utils : `${config.utils}.ts`);
2509
+ const registry = config.registry;
2510
+ let primitiveCss;
2511
+ try {
2512
+ const payload = await withSpinner(
2513
+ `Fetching primitive tokens (${INIT_PRIMITIVE_CSS_SLUG})`,
2514
+ () => fetchComponentPayload({ slug: INIT_PRIMITIVE_CSS_SLUG, registry }),
2515
+ "Fetched primitive tokens"
2516
+ );
2517
+ if (typeof payload.primitiveCss !== "string" || !payload.primitiveCss.trim()) {
2518
+ throw new CliError(
2519
+ `Pinned slug "${INIT_PRIMITIVE_CSS_SLUG}" did not return primitiveCss. Cannot apply preset styles.`
2520
+ );
2521
+ }
2522
+ primitiveCss = applyStyleToPrimitiveCss(payload.primitiveCss, style).trim();
2523
+ } catch (error) {
2524
+ if (error instanceof CliError) throw error;
2525
+ const message = error instanceof Error ? error.message : String(error);
2526
+ throw new CliError(`Failed to fetch primitive CSS for preset init: ${message}`);
2527
+ }
2528
+ const cssRelative = config.tailwind?.css;
2529
+ if (!cssRelative) {
2530
+ throw new CliError("No Tailwind CSS path configured. Set tailwind.css in wensity.json.");
2531
+ }
2532
+ const cssAbsolute = resolveInside(context.cwd, cssRelative);
2533
+ const existingCss = await fileExists(cssAbsolute) ? await readFile5(cssAbsolute, "utf8") : "";
2534
+ const nextCss = upsertPrimitiveCssBlock(existingCss, primitiveCss);
2535
+ const cssChanged = nextCss !== existingCss;
2536
+ if (options.dryRun) {
2537
+ printTitle("Project initialization preview (preset)");
2538
+ print(action("Would write:"));
2539
+ const planned = [CONFIG_FILE_NAME, utilityRelativePath];
2540
+ if (cssChanged) planned.push(toPosixPath(cssRelative));
2541
+ print(formatList(planned));
2542
+ print(action(`Preset: ${presetCode.slice(0, 24)}\u2026`));
2543
+ print(action(`Style: icon=${style.iconLibrary} radius=${style.radius}`));
2544
+ if (options.install) print(action(`Would install: ${options.dependencies.join(" ")}`));
2545
+ return;
2546
+ }
2547
+ const shouldWriteUtility = options.force || !await fileExists(utilityPath);
2548
+ await commitPresetInitWrites({
2549
+ cssAbsolute,
2550
+ existingCss,
2551
+ nextCss,
2552
+ cssChanged,
2553
+ writeUtility: shouldWriteUtility,
2554
+ utilityAbsolutePath: utilityPath,
2555
+ utilityContent: cnUtilityTemplate(),
2556
+ commitConfig: async () => {
2557
+ if (configExists && !options.force) {
2558
+ await mergeStyleIntoConfigFile(configPath, style);
2559
+ } else {
2560
+ await writeConfigFile(configPath, config, options.force);
2561
+ }
2562
+ }
2563
+ });
2564
+ if (options.install) {
2565
+ runInstall(context.cwd, missingDependencies(context.cwd, options.dependencies));
2566
+ }
2567
+ printTitle("Project initialized");
2568
+ const created = [
2569
+ success(CONFIG_FILE_NAME),
2570
+ success(utilityRelativePath),
2571
+ success(`registry: ${options.apiUrl}`),
2572
+ success(`preset applied (${style.iconLibrary}, radius=${style.radius})`)
2573
+ ];
2574
+ if (cssChanged) {
2575
+ created.push(success(`CSS tokens: ${toPosixPath(cssRelative)}`));
2576
+ } else {
2577
+ created.push(action(`CSS tokens unchanged: ${toPosixPath(cssRelative)}`));
2578
+ }
2579
+ print(panel("Created", created));
2580
+ print(nextSteps([
2581
+ "Browse available components: npx wensity list",
2582
+ "Install a primitive: npx wensity add button"
2583
+ ]));
2584
+ }
1042
2585
  async function loginCommand(context, parsed) {
1043
2586
  const apiUrl = getStringOption(parsed, "api-url") ?? context.env.WENSITY_API_URL ?? DEFAULT_API_ORIGIN;
1044
2587
  const apiBaseUrl = normalizeUrlOrigin(apiUrl);
@@ -1167,22 +2710,32 @@ Usage: wensity ${updateMode ? "update" : "add"} <slug>${resourceType === "block"
1167
2710
  const loaded = await loadConfig(context.cwd, { registry: registryOverride, apiUrl });
1168
2711
  const credentials = await readCredentials(context.env);
1169
2712
  const registry = registryUrlForType(loaded.config.registry, resourceType, registryOverride);
1170
- const payload = resourceType === "block" ? await fetchBlockPayload({ slug, registry, credentials }) : await fetchComponentPayload({ slug, registry, credentials });
1171
- const prepared = await prepareInstall(loaded.projectRoot, loaded.config, payload);
1172
2713
  const force = updateMode || hasFlag(parsed, "force");
2714
+ const payload = resourceType === "block" ? await fetchBlockPayload({ slug, registry, credentials }) : await fetchComponentPayload({ slug, registry, credentials });
2715
+ const prepared = await prepareInstall(loaded.projectRoot, loaded.config, payload, {
2716
+ overwriteExistingDependencyFiles: force
2717
+ });
1173
2718
  const dryRun = hasFlag(parsed, "dry-run");
1174
2719
  const install = !hasFlag(parsed, "no-install");
1175
- await assertNoConflicts(prepared.files, force || dryRun);
1176
- const plannedFiles = summarizeFiles(loaded.projectRoot, prepared.files);
2720
+ const plannedInstallFiles = prepared.cssFile ? [...prepared.files, prepared.cssFile] : prepared.files;
2721
+ await assertNoConflicts(plannedInstallFiles, force || dryRun);
2722
+ const plannedFiles = summarizeFiles(loaded.projectRoot, plannedInstallFiles);
1177
2723
  const missing = missingDependencies(loaded.projectRoot, prepared.dependencies);
2724
+ const style = normalizeStyleConfig(loaded.config.style);
2725
+ const installedPackages = readInstalledPackageNames(loaded.projectRoot);
2726
+ const oppositeIconWarning = installedPackages && prepared.iconDependencySwap ? warnOppositeIconPackageMessage(style.iconLibrary, installedPackages) : void 0;
1178
2727
  if (dryRun) {
1179
2728
  printTitle(updateMode ? "Update preview" : "Install preview");
1180
2729
  print(action("Would write:"));
1181
2730
  print(formatList(plannedFiles));
2731
+ if (prepared.cssFile) {
2732
+ print(action(`Would update CSS tokens in: ${prepared.cssFile.relativePath}`));
2733
+ }
1182
2734
  if (install && missing.length > 0) print(action(`Would install: ${missing.join(" ")}`));
2735
+ if (oppositeIconWarning) warn(oppositeIconWarning);
1183
2736
  return;
1184
2737
  }
1185
- await writePlannedFiles(prepared.files);
2738
+ await writePlannedFiles(plannedInstallFiles);
1186
2739
  if (install) {
1187
2740
  runInstall(loaded.projectRoot, missing);
1188
2741
  }
@@ -1191,6 +2744,7 @@ Usage: wensity ${updateMode ? "update" : "add"} <slug>${resourceType === "block"
1191
2744
  success(`${updateMode ? "Updated" : "Added"} ${payload.slug}`),
1192
2745
  ...plannedFiles.map((file) => success(file))
1193
2746
  ]));
2747
+ if (oppositeIconWarning) warn(oppositeIconWarning);
1194
2748
  if (!install && missing.length > 0) {
1195
2749
  warn(`Dependencies were not installed: ${missing.join(" ")}`);
1196
2750
  print(nextSteps([
@@ -1313,8 +2867,8 @@ function formatAccountPanel(whoami, savedPath) {
1313
2867
  return panel(isPro ? "Wensity Pro" : "Wensity Free", lines);
1314
2868
  }
1315
2869
  async function writeTextFile(path, content) {
1316
- await mkdir4(dirname5(path), { recursive: true });
1317
- await writeFile4(path, content, "utf8");
2870
+ await mkdir5(dirname6(path), { recursive: true });
2871
+ await writeFile5(path, content, "utf8");
1318
2872
  }
1319
2873
 
1320
2874
  // src/index.ts