wensity 0.1.0

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 (3) hide show
  1. package/README.md +14 -0
  2. package/dist/index.js +1031 -0
  3. package/package.json +32 -0
package/README.md ADDED
@@ -0,0 +1,14 @@
1
+ # Wensity CLI
2
+
3
+ The `wensity` CLI installs source-owned components from the Wensity registry.
4
+
5
+ ```bash
6
+ pnpm dlx wensity init
7
+ pnpm dlx wensity login
8
+ pnpm dlx wensity list
9
+ pnpm dlx wensity add liquid-multimodal-input
10
+ pnpm dlx wensity whoami
11
+ ```
12
+
13
+ Premium components require an API token from the Wensity dashboard. Tokens are
14
+ stored at `~/.config/wensity/credentials.json` with `0600` permissions.
package/dist/index.js ADDED
@@ -0,0 +1,1031 @@
1
+ #!/usr/bin/env node
2
+
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";
6
+
7
+ // src/constants.ts
8
+ var CLI_NAME = "wensity";
9
+ var CLI_VERSION = "0.1.0";
10
+ var CONFIG_FILE_NAME = "wensity.json";
11
+ var DEFAULT_API_ORIGIN = "https://wensity.com";
12
+ var DEFAULT_REGISTRY_PATH = "/api/registry";
13
+ var DEFAULT_COMPONENTS_DIR = "src/components/wensity";
14
+ var DEFAULT_UTILS_PATH = "src/lib/utils";
15
+ var DEFAULT_COMPONENTS_ALIAS = "@/components/wensity";
16
+ var DEFAULT_UTILS_ALIAS = "@/lib/utils";
17
+ var HELP_TEXT = `Wensity CLI ${CLI_VERSION}
18
+
19
+ Usage:
20
+ wensity init [options]
21
+ wensity login [options]
22
+ wensity logout
23
+ wensity whoami [--json]
24
+ wensity list [options]
25
+ wensity add <slug> [options]
26
+ wensity update <slug> [options]
27
+ wensity version
28
+
29
+ Commands:
30
+ init Create wensity.json and local helper files
31
+ login Save a Wensity API token for premium component access
32
+ logout Remove saved CLI credentials
33
+ whoami Verify the saved token and print the authenticated account
34
+ list List registry components
35
+ add Download a component source file into this project
36
+ update Re-download a component and overwrite the local copy
37
+ version Print the CLI version
38
+
39
+ Options:
40
+ --api-url <url> Override the Wensity app origin, defaults to ${DEFAULT_API_ORIGIN}
41
+ --registry <url> Override the registry endpoint
42
+ --token <token> Provide a token non-interactively for login
43
+ --force Overwrite generated files when adding or initializing
44
+ --no-install Write files without running the package manager
45
+ --dry-run Print planned writes and installs without changing files
46
+ --json Print machine-readable output where supported
47
+ --help Show help
48
+ --version Print the CLI version
49
+ `;
50
+
51
+ // src/errors.ts
52
+ var CliError = class extends Error {
53
+ exitCode;
54
+ constructor(message, exitCode = 1) {
55
+ super(message);
56
+ this.name = "CliError";
57
+ this.exitCode = exitCode;
58
+ }
59
+ };
60
+ function isNodeError(error) {
61
+ return error instanceof Error && "code" in error;
62
+ }
63
+
64
+ // src/args.ts
65
+ function parseArgs(argv) {
66
+ const positionals = [];
67
+ const options = /* @__PURE__ */ new Map();
68
+ for (let index = 0; index < argv.length; index += 1) {
69
+ const token = argv[index];
70
+ if (!token.startsWith("--")) {
71
+ positionals.push(token);
72
+ continue;
73
+ }
74
+ if (token === "--") {
75
+ positionals.push(...argv.slice(index + 1));
76
+ break;
77
+ }
78
+ const inlineValueIndex = token.indexOf("=");
79
+ const rawName = inlineValueIndex > -1 ? token.slice(2, inlineValueIndex) : token.slice(2);
80
+ const name = normalizeOptionName(rawName);
81
+ if (!name) {
82
+ throw new CliError(`Invalid option: ${token}`);
83
+ }
84
+ if (inlineValueIndex > -1) {
85
+ options.set(name, token.slice(inlineValueIndex + 1));
86
+ continue;
87
+ }
88
+ const nextToken = argv[index + 1];
89
+ if (nextToken && !nextToken.startsWith("--") && expectsValue(name)) {
90
+ options.set(name, nextToken);
91
+ index += 1;
92
+ continue;
93
+ }
94
+ options.set(name, true);
95
+ }
96
+ return { positionals, options };
97
+ }
98
+ function getStringOption(args, name) {
99
+ const value = args.options.get(name);
100
+ if (value === void 0) return void 0;
101
+ if (typeof value === "boolean") {
102
+ throw new CliError(`--${name} expects a value.`);
103
+ }
104
+ return value;
105
+ }
106
+ function hasFlag(args, name) {
107
+ return args.options.get(name) === true;
108
+ }
109
+ function expectsValue(name) {
110
+ return ["api-url", "registry", "token"].includes(name);
111
+ }
112
+ function normalizeOptionName(name) {
113
+ if (name === "dry") return "dry-run";
114
+ return name;
115
+ }
116
+
117
+ // src/config.ts
118
+ import { existsSync } from "fs";
119
+ import { access, mkdir, readFile, writeFile } from "fs/promises";
120
+ import { dirname as dirname2, join as join2, relative as relative2 } from "path";
121
+
122
+ // src/paths.ts
123
+ import { dirname, isAbsolute, relative, resolve, sep } from "path";
124
+ function normalizeUrlOrigin(input) {
125
+ const trimmed = input.trim().replace(/\/+$/, "");
126
+ const withProtocol = /^https?:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`;
127
+ const url = new URL(withProtocol);
128
+ if (url.protocol !== "https:" && !isLocalHttpUrl(url)) {
129
+ throw new CliError("API URL must use HTTPS, except for localhost development URLs.");
130
+ }
131
+ return url.origin;
132
+ }
133
+ function isLocalHttpUrl(url) {
134
+ if (url.protocol !== "http:") return false;
135
+ return url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "::1";
136
+ }
137
+ function joinUrl(originOrUrl, path) {
138
+ const base = originOrUrl.endsWith("/") ? originOrUrl : `${originOrUrl}/`;
139
+ const normalizedPath = path.startsWith("/") ? path.slice(1) : path;
140
+ return new URL(normalizedPath, base).toString();
141
+ }
142
+ function resolveInside(root, candidate) {
143
+ if (isAbsolute(candidate)) {
144
+ throw new Error(`Path must be project-relative: ${candidate}`);
145
+ }
146
+ const absolutePath = resolve(root, candidate);
147
+ const relativePath = relative(root, absolutePath);
148
+ if (relativePath.startsWith("..") || isAbsolute(relativePath)) {
149
+ throw new Error(`Path escapes the project: ${candidate}`);
150
+ }
151
+ return absolutePath;
152
+ }
153
+ function toPosixPath(input) {
154
+ return input.split(sep).join("/");
155
+ }
156
+ function parentDirectory(input) {
157
+ return dirname(input);
158
+ }
159
+ function stripKnownComponentPrefix(path) {
160
+ const normalized = path.replace(/\\/g, "/").replace(/^\/+/, "");
161
+ const prefixes = ["components/wensity/", "src/components/wensity/"];
162
+ const prefix = prefixes.find((candidate) => normalized.startsWith(candidate));
163
+ return prefix ? normalized.slice(prefix.length) : normalized;
164
+ }
165
+ function sanitizePayloadPath(path, fallbackFileName) {
166
+ const normalized = stripKnownComponentPrefix(path);
167
+ const parts = normalized.split("/").filter(Boolean);
168
+ if (parts.length === 0) return fallbackFileName;
169
+ for (const part of parts) {
170
+ if (part === "." || part === ".." || part.includes("\0")) {
171
+ throw new Error(`Unsafe registry file path: ${path}`);
172
+ }
173
+ }
174
+ const last = parts.at(-1) ?? fallbackFileName;
175
+ if (!/\.(tsx|ts|jsx|js)$/.test(last)) {
176
+ parts[parts.length - 1] = fallbackFileName;
177
+ }
178
+ return parts.join("/");
179
+ }
180
+ function componentFileName(slug) {
181
+ return `${slugToPascalCase(slug)}.tsx`;
182
+ }
183
+ function slugToPascalCase(slug) {
184
+ return slug.split(/[^a-zA-Z0-9]+/).filter(Boolean).map((part) => `${part.charAt(0).toUpperCase()}${part.slice(1)}`).join("");
185
+ }
186
+ function extensionlessPath(path) {
187
+ return path.replace(/\.(tsx|ts|jsx|js)$/, "");
188
+ }
189
+
190
+ // src/config.ts
191
+ async function loadConfig(cwd, overrides = {}) {
192
+ const foundPath = await findUp(cwd, CONFIG_FILE_NAME);
193
+ const projectRoot = foundPath ? dirname2(foundPath) : cwd;
194
+ const configPath = foundPath ?? join2(projectRoot, CONFIG_FILE_NAME);
195
+ const defaults = createDefaultConfig(projectRoot, overrides.apiUrl ?? DEFAULT_API_ORIGIN);
196
+ if (!foundPath) {
197
+ return {
198
+ config: normalizeConfig(defaults, overrides),
199
+ configPath,
200
+ projectRoot,
201
+ exists: false
202
+ };
203
+ }
204
+ try {
205
+ const parsed = JSON.parse(await readFile(foundPath, "utf8"));
206
+ return {
207
+ config: normalizeConfig({ ...defaults, ...parsed }, overrides),
208
+ configPath,
209
+ projectRoot,
210
+ exists: true
211
+ };
212
+ } catch (error) {
213
+ if (error instanceof SyntaxError) {
214
+ throw new CliError(`${CONFIG_FILE_NAME} contains invalid JSON.`);
215
+ }
216
+ throw error;
217
+ }
218
+ }
219
+ function createDefaultConfig(projectRoot, apiUrl = DEFAULT_API_ORIGIN) {
220
+ const apiOrigin = normalizeUrlOrigin(apiUrl);
221
+ return {
222
+ $schema: joinUrl(apiOrigin, "/schema/wensity.json"),
223
+ registry: joinUrl(apiOrigin, DEFAULT_REGISTRY_PATH),
224
+ components: DEFAULT_COMPONENTS_DIR,
225
+ utils: DEFAULT_UTILS_PATH,
226
+ tailwind: {
227
+ config: detectTailwindConfig(projectRoot),
228
+ css: detectGlobalCss(projectRoot)
229
+ },
230
+ aliases: {
231
+ components: DEFAULT_COMPONENTS_ALIAS,
232
+ utils: DEFAULT_UTILS_ALIAS
233
+ }
234
+ };
235
+ }
236
+ async function writeConfigFile(path, config, force) {
237
+ if (!force && await fileExists(path)) {
238
+ throw new CliError(`${toPosixPath(relative2(process.cwd(), path))} already exists. Use --force to overwrite it.`);
239
+ }
240
+ await mkdir(dirname2(path), { recursive: true });
241
+ await writeFile(path, `${JSON.stringify(config, null, 2)}
242
+ `, "utf8");
243
+ }
244
+ function resolveConfiguredFilePath(projectRoot, relativePath, extension = "ts") {
245
+ try {
246
+ return resolveInside(projectRoot, withExtension(relativePath, extension));
247
+ } catch (error) {
248
+ const message = error instanceof Error ? error.message : String(error);
249
+ throw new CliError(message);
250
+ }
251
+ }
252
+ async function fileExists(path) {
253
+ try {
254
+ await access(path);
255
+ return true;
256
+ } catch (error) {
257
+ if (isNodeError(error) && error.code === "ENOENT") return false;
258
+ throw error;
259
+ }
260
+ }
261
+ async function findUp(start, fileName) {
262
+ let current = start;
263
+ while (true) {
264
+ const candidate = join2(current, fileName);
265
+ if (await fileExists(candidate)) return candidate;
266
+ const parent = dirname2(current);
267
+ if (parent === current) return void 0;
268
+ current = parent;
269
+ }
270
+ }
271
+ function normalizeConfig(config, overrides) {
272
+ const apiOrigin = overrides.apiUrl ? normalizeUrlOrigin(overrides.apiUrl) : void 0;
273
+ return {
274
+ ...config,
275
+ registry: overrides.registry ?? (apiOrigin ? joinUrl(apiOrigin, DEFAULT_REGISTRY_PATH) : config.registry),
276
+ components: config.components || DEFAULT_COMPONENTS_DIR,
277
+ utils: config.utils || DEFAULT_UTILS_PATH,
278
+ aliases: {
279
+ components: config.aliases?.components || DEFAULT_COMPONENTS_ALIAS,
280
+ utils: config.aliases?.utils || DEFAULT_UTILS_ALIAS
281
+ }
282
+ };
283
+ }
284
+ function detectTailwindConfig(projectRoot) {
285
+ const candidates = ["tailwind.config.ts", "tailwind.config.js", "tailwind.config.mjs"];
286
+ return candidates.find((candidate) => existsSync(join2(projectRoot, candidate))) ?? "tailwind.config.ts";
287
+ }
288
+ function detectGlobalCss(projectRoot) {
289
+ const candidates = ["src/app/globals.css", "src/styles/globals.css", "app/globals.css"];
290
+ return candidates.find((candidate) => existsSync(join2(projectRoot, candidate))) ?? "src/app/globals.css";
291
+ }
292
+ function withExtension(path, extension) {
293
+ if (/\.(ts|tsx|js|jsx)$/.test(path)) return path;
294
+ return `${path}.${extension}`;
295
+ }
296
+
297
+ // src/credentials.ts
298
+ import { mkdir as mkdir2, readFile as readFile2, rename, rm, writeFile as writeFile2, chmod } from "fs/promises";
299
+ import { dirname as dirname3, join as join3 } from "path";
300
+ import { randomUUID } from "crypto";
301
+ import { stdin, stdout } from "process";
302
+ function credentialsPath(env) {
303
+ if (env.WENSITY_CREDENTIALS_FILE) return env.WENSITY_CREDENTIALS_FILE;
304
+ const configHome = env.WENSITY_CONFIG_HOME ?? env.XDG_CONFIG_HOME ?? join3(requiredHome(env), ".config");
305
+ return join3(configHome, "wensity", "credentials.json");
306
+ }
307
+ async function readCredentials(env) {
308
+ const path = credentialsPath(env);
309
+ try {
310
+ const content = await readFile2(path, "utf8");
311
+ const parsed = JSON.parse(content);
312
+ if (parsed.version !== 1 || !parsed.token || !parsed.apiBaseUrl) {
313
+ throw new CliError(`Credentials at ${path} are not valid. Run wensity login again.`);
314
+ }
315
+ return parsed;
316
+ } catch (error) {
317
+ if (isNodeError(error) && error.code === "ENOENT") return void 0;
318
+ if (error instanceof SyntaxError) {
319
+ throw new CliError(`Credentials at ${path} are not valid JSON. Run wensity login again.`);
320
+ }
321
+ throw error;
322
+ }
323
+ }
324
+ async function writeCredentials(env, credentials) {
325
+ const path = credentialsPath(env);
326
+ const directory = dirname3(path);
327
+ const temporaryPath = join3(directory, `.credentials.${randomUUID()}.tmp`);
328
+ await mkdir2(directory, { recursive: true, mode: 448 });
329
+ await writeFile2(temporaryPath, `${JSON.stringify(credentials, null, 2)}
330
+ `, {
331
+ encoding: "utf8",
332
+ mode: 384
333
+ });
334
+ await rename(temporaryPath, path);
335
+ await chmod(path, 384);
336
+ return path;
337
+ }
338
+ async function deleteCredentials(env) {
339
+ const path = credentialsPath(env);
340
+ try {
341
+ await rm(path, { force: false });
342
+ return true;
343
+ } catch (error) {
344
+ if (isNodeError(error) && error.code === "ENOENT") return false;
345
+ throw error;
346
+ }
347
+ }
348
+ async function readTokenInteractively(prompt = "Wensity API token: ") {
349
+ if (!stdin.isTTY) {
350
+ const chunks = [];
351
+ for await (const chunk of stdin) {
352
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
353
+ }
354
+ return Buffer.concat(chunks).toString("utf8").trim();
355
+ }
356
+ return new Promise((resolveToken, rejectToken) => {
357
+ const input = stdin;
358
+ const previousRawMode = input.isRaw;
359
+ let token = "";
360
+ function cleanup() {
361
+ input.off("data", onData);
362
+ if (typeof previousRawMode === "boolean") {
363
+ input.setRawMode(previousRawMode);
364
+ }
365
+ input.pause();
366
+ }
367
+ function onData(chunk) {
368
+ const value = chunk.toString("utf8");
369
+ if (value === "") {
370
+ cleanup();
371
+ stdout.write("\n");
372
+ rejectToken(new CliError("Login cancelled.", 130));
373
+ return;
374
+ }
375
+ if (value === "\r" || value === "\n" || value === "\r\n") {
376
+ cleanup();
377
+ stdout.write("\n");
378
+ resolveToken(token.trim());
379
+ return;
380
+ }
381
+ if (value === "\x7F") {
382
+ token = token.slice(0, -1);
383
+ return;
384
+ }
385
+ token += value;
386
+ }
387
+ stdout.write(prompt);
388
+ input.setRawMode(true);
389
+ input.resume();
390
+ input.on("data", onData);
391
+ });
392
+ }
393
+ function requiredHome(env) {
394
+ if (env.HOME) return env.HOME;
395
+ throw new CliError("HOME is not set, so the CLI cannot locate ~/.config/wensity.");
396
+ }
397
+
398
+ // src/http.ts
399
+ async function requestJson(url, options = {}) {
400
+ const headers = new Headers(options.headers);
401
+ headers.set("accept", "application/json");
402
+ headers.set("user-agent", `${CLI_NAME}/${CLI_VERSION}`);
403
+ if (options.body && !headers.has("content-type")) {
404
+ headers.set("content-type", "application/json");
405
+ }
406
+ if (options.token) {
407
+ headers.set("authorization", `Bearer ${options.token}`);
408
+ }
409
+ let response;
410
+ try {
411
+ response = await fetch(url, { ...options, headers });
412
+ } catch (error) {
413
+ const message = error instanceof Error ? error.message : "Unknown network error";
414
+ throw new CliError(`Could not reach ${url}: ${message}`);
415
+ }
416
+ const text = await response.text();
417
+ const payload = parseJson(text);
418
+ if (!response.ok) {
419
+ const message = formatErrorMessage(payload) ?? response.statusText;
420
+ throw new CliError(`${response.status} ${message}`);
421
+ }
422
+ return payload;
423
+ }
424
+ function parseJson(text) {
425
+ if (!text.trim()) return null;
426
+ try {
427
+ return JSON.parse(text);
428
+ } catch {
429
+ throw new CliError("Registry returned invalid JSON.");
430
+ }
431
+ }
432
+ function formatErrorMessage(payload) {
433
+ if (!payload || typeof payload !== "object") return void 0;
434
+ const record = payload;
435
+ const code = typeof record.error === "string" ? record.error : void 0;
436
+ const message = typeof record.message === "string" ? record.message : code;
437
+ if (code === "pro_license_required") {
438
+ const base = message ?? "This component requires Wensity Pro.";
439
+ return `${base}
440
+ Upgrade here: https://wensity.com/pricing`;
441
+ }
442
+ return message;
443
+ }
444
+
445
+ // src/package-manager.ts
446
+ import { existsSync as existsSync2, readFileSync } from "fs";
447
+ import { spawnSync } from "child_process";
448
+ import { join as join4 } from "path";
449
+
450
+ // src/output.ts
451
+ function print(message = "") {
452
+ process.stdout.write(`${message}
453
+ `);
454
+ }
455
+ function warn(message) {
456
+ process.stderr.write(`Warning: ${message}
457
+ `);
458
+ }
459
+ function info(message) {
460
+ process.stderr.write(`${message}
461
+ `);
462
+ }
463
+ function printJson(value) {
464
+ print(JSON.stringify(value, null, 2));
465
+ }
466
+ function formatList(items) {
467
+ return items.map((item) => ` - ${item}`).join("\n");
468
+ }
469
+
470
+ // src/package-manager.ts
471
+ function detectPackageManager(projectRoot) {
472
+ if (existsSync2(join4(projectRoot, "pnpm-lock.yaml"))) return "pnpm";
473
+ if (existsSync2(join4(projectRoot, "bun.lockb")) || existsSync2(join4(projectRoot, "bun.lock"))) return "bun";
474
+ if (existsSync2(join4(projectRoot, "yarn.lock"))) return "yarn";
475
+ if (existsSync2(join4(projectRoot, "package-lock.json"))) return "npm";
476
+ return "npm";
477
+ }
478
+ function missingDependencies(projectRoot, dependencies) {
479
+ const packageJsonPath = join4(projectRoot, "package.json");
480
+ if (!existsSync2(packageJsonPath)) return dependencies;
481
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
482
+ const installed = {
483
+ ...packageJson.dependencies,
484
+ ...packageJson.devDependencies,
485
+ ...packageJson.peerDependencies,
486
+ ...packageJson.optionalDependencies
487
+ };
488
+ return dependencies.filter((dependency) => !installed[dependency]);
489
+ }
490
+ function runInstall(projectRoot, dependencies) {
491
+ if (dependencies.length === 0) return;
492
+ const packageJsonPath = join4(projectRoot, "package.json");
493
+ if (!existsSync2(packageJsonPath)) {
494
+ warn(`No package.json found. Install these dependencies manually: ${dependencies.join(" ")}`);
495
+ return;
496
+ }
497
+ const packageManager = detectPackageManager(projectRoot);
498
+ const args = installArgs(packageManager, dependencies);
499
+ const result = spawnSync(packageManager, args, {
500
+ cwd: projectRoot,
501
+ stdio: "inherit",
502
+ shell: process.platform === "win32"
503
+ });
504
+ if (result.error) {
505
+ throw new CliError(`Could not run ${packageManager}: ${result.error.message}`);
506
+ }
507
+ if (result.status !== 0) {
508
+ throw new CliError(`${packageManager} ${args.join(" ")} failed with exit code ${result.status ?? 1}.`);
509
+ }
510
+ }
511
+ function installArgs(packageManager, dependencies) {
512
+ switch (packageManager) {
513
+ case "pnpm":
514
+ return ["add", ...dependencies];
515
+ case "yarn":
516
+ return ["add", ...dependencies];
517
+ case "bun":
518
+ return ["add", ...dependencies];
519
+ case "npm":
520
+ return ["install", ...dependencies];
521
+ }
522
+ }
523
+
524
+ // src/component-install.ts
525
+ import { mkdir as mkdir3, writeFile as writeFile3 } from "fs/promises";
526
+ import { dirname as dirname4, join as join5, relative as relative3 } from "path";
527
+
528
+ // src/templates.ts
529
+ function cnUtilityTemplate() {
530
+ return `import { clsx, type ClassValue } from "clsx";
531
+ import { twMerge } from "tailwind-merge";
532
+
533
+ export function cn(...inputs: ClassValue[]): string {
534
+ return twMerge(clsx(inputs));
535
+ }
536
+ `;
537
+ }
538
+ function primitivesTemplate(utilsImport) {
539
+ return `"use client";
540
+
541
+ import * as React from "react";
542
+ import { cn } from "${utilsImport}";
543
+
544
+ type Size = "sm" | "md" | "lg";
545
+ type Tone = "chili" | "violet" | "emerald" | "sky" | "amber" | "white" | "graphite";
546
+
547
+ type SharedProps = {
548
+ children: React.ReactNode;
549
+ size?: Size;
550
+ tone?: Tone;
551
+ showArrow?: boolean;
552
+ className?: string;
553
+ href?: string;
554
+ external?: boolean;
555
+ disabled?: boolean;
556
+ };
557
+
558
+ type ButtonProps = SharedProps & Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, keyof SharedProps>;
559
+ type AnchorProps = SharedProps & Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, keyof SharedProps | "href">;
560
+
561
+ const sizes: Record<Size, string> = {
562
+ sm: "h-9 px-4 text-[13px]",
563
+ md: "h-10 px-5 text-sm",
564
+ lg: "h-12 px-6 text-[15px]",
565
+ };
566
+
567
+ const tones: Record<Tone, string> = {
568
+ chili: "from-chili-400 via-chili-500 to-chili-700 text-white shadow-[0_10px_30px_-10px_rgba(205,28,24,0.55)]",
569
+ violet: "from-violet-300 via-violet-600 to-violet-800 text-white shadow-[0_10px_30px_-10px_rgba(124,58,237,0.55)]",
570
+ emerald: "from-emerald-300 via-emerald-600 to-emerald-800 text-white shadow-[0_10px_30px_-10px_rgba(16,185,129,0.55)]",
571
+ sky: "from-sky-300 via-sky-600 to-sky-800 text-white shadow-[0_10px_30px_-10px_rgba(14,165,233,0.55)]",
572
+ amber: "from-amber-300 via-amber-500 to-amber-700 text-black shadow-[0_10px_30px_-10px_rgba(245,158,11,0.55)]",
573
+ white: "from-white via-white to-neutral-200 text-black shadow-[0_10px_30px_-10px_rgba(255,255,255,0.35)]",
574
+ graphite: "from-neutral-700 via-neutral-800 to-neutral-950 text-white shadow-[0_10px_30px_-10px_rgba(0,0,0,0.55)]",
575
+ };
576
+
577
+ export function WensityButton({
578
+ children,
579
+ size = "md",
580
+ tone = "chili",
581
+ showArrow = true,
582
+ className,
583
+ href,
584
+ external,
585
+ disabled,
586
+ ...props
587
+ }: ButtonProps | AnchorProps) {
588
+ const classes = cn(
589
+ "group relative inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-xl bg-gradient-to-b font-medium transition-[box-shadow,background-color,color,border-color,opacity] duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-chili-500 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
590
+ sizes[size],
591
+ tones[tone],
592
+ disabled && "pointer-events-none opacity-60",
593
+ className
594
+ );
595
+ const content = (
596
+ <>
597
+ <span className="relative z-10 inline-flex items-center gap-2">{children}</span>
598
+ {showArrow && <span aria-hidden className="relative z-10">-&gt;</span>}
599
+ <span aria-hidden className="pointer-events-none absolute inset-0 overflow-hidden rounded-xl">
600
+ <span className="absolute inset-x-0 top-0 h-px bg-gradient-to-r from-transparent via-white/40 to-transparent" />
601
+ </span>
602
+ </>
603
+ );
604
+
605
+ if (href && !disabled) {
606
+ return (
607
+ <a
608
+ href={href}
609
+ target={external || /^(https?:|mailto:|tel:)/i.test(href) ? "_blank" : undefined}
610
+ rel={external || /^(https?:|mailto:|tel:)/i.test(href) ? "noreferrer" : undefined}
611
+ className={classes}
612
+ {...(props as React.AnchorHTMLAttributes<HTMLAnchorElement>)}
613
+ >
614
+ {content}
615
+ </a>
616
+ );
617
+ }
618
+
619
+ return (
620
+ <button
621
+ type="button"
622
+ className={classes}
623
+ disabled={disabled}
624
+ {...(props as React.ButtonHTMLAttributes<HTMLButtonElement>)}
625
+ >
626
+ {content}
627
+ </button>
628
+ );
629
+ }
630
+
631
+ export function WensitySecondaryButton({
632
+ children,
633
+ size = "md",
634
+ showArrow = false,
635
+ className,
636
+ href,
637
+ external,
638
+ disabled,
639
+ ...props
640
+ }: ButtonProps | AnchorProps) {
641
+ const classes = cn(
642
+ "group relative inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-xl border border-black/10 bg-white font-semibold text-neutral-950 shadow-[0_8px_24px_-18px_rgba(15,23,42,0.35)] transition-[box-shadow,background-color,color,border-color,opacity] duration-200 hover:border-black/15 hover:bg-neutral-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-chili-500 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 dark:border-white/10 dark:bg-white/[0.06] dark:text-white dark:hover:border-white/15 dark:hover:bg-white/[0.09]",
643
+ sizes[size],
644
+ disabled && "pointer-events-none opacity-60",
645
+ className
646
+ );
647
+ const content = (
648
+ <>
649
+ <span className="relative z-10 inline-flex items-center gap-2">{children}</span>
650
+ {showArrow && <span aria-hidden className="relative z-10">-&gt;</span>}
651
+ </>
652
+ );
653
+
654
+ if (href && !disabled) {
655
+ return (
656
+ <a
657
+ href={href}
658
+ target={external || /^(https?:|mailto:|tel:)/i.test(href) ? "_blank" : undefined}
659
+ rel={external || /^(https?:|mailto:|tel:)/i.test(href) ? "noreferrer" : undefined}
660
+ className={classes}
661
+ {...(props as React.AnchorHTMLAttributes<HTMLAnchorElement>)}
662
+ >
663
+ {content}
664
+ </a>
665
+ );
666
+ }
667
+
668
+ return (
669
+ <button
670
+ type="button"
671
+ className={classes}
672
+ disabled={disabled}
673
+ {...(props as React.ButtonHTMLAttributes<HTMLButtonElement>)}
674
+ >
675
+ {content}
676
+ </button>
677
+ );
678
+ }
679
+ `;
680
+ }
681
+
682
+ // src/component-install.ts
683
+ var ALWAYS_PRESENT_PACKAGES = /* @__PURE__ */ new Set(["react", "react-dom", "next"]);
684
+ async function prepareInstall(projectRoot, config, payload) {
685
+ const fallbackFileName = componentFileName(payload.slug);
686
+ const files = [];
687
+ const componentContents = [];
688
+ let needsUtility = false;
689
+ let needsPrimitives = false;
690
+ for (const payloadFile of payload.files) {
691
+ const safePayloadPath = sanitizePayloadPath(payloadFile.path, fallbackFileName);
692
+ const relativeOutputPath = toPosixPath(join5(config.components, safePayloadPath));
693
+ const transformed = transformComponentSource(payloadFile.content, config);
694
+ needsUtility ||= transformed.needsUtility || transformed.needsPrimitives;
695
+ needsPrimitives ||= transformed.needsPrimitives;
696
+ componentContents.push(transformed.content);
697
+ files.push({
698
+ absolutePath: resolveInside(projectRoot, relativeOutputPath),
699
+ relativePath: relativeOutputPath,
700
+ content: transformed.content,
701
+ kind: "component"
702
+ });
703
+ }
704
+ if (needsUtility) {
705
+ const utilityPath = withSourceExtension(config.utils, "ts");
706
+ if (!await fileExists(resolveInside(projectRoot, utilityPath))) {
707
+ files.push({
708
+ absolutePath: resolveInside(projectRoot, utilityPath),
709
+ relativePath: utilityPath,
710
+ content: cnUtilityTemplate(),
711
+ kind: "helper"
712
+ });
713
+ }
714
+ }
715
+ if (needsPrimitives) {
716
+ const primitivesPath = toPosixPath(join5(config.components, "primitives.tsx"));
717
+ if (!await fileExists(resolveInside(projectRoot, primitivesPath))) {
718
+ const primitives = primitivesTemplate(config.aliases.utils);
719
+ files.push({
720
+ absolutePath: resolveInside(projectRoot, primitivesPath),
721
+ relativePath: primitivesPath,
722
+ content: primitives,
723
+ kind: "helper"
724
+ });
725
+ componentContents.push(primitives);
726
+ }
727
+ }
728
+ const dependencies = collectDependencies({
729
+ declared: payload.dependencies ?? [],
730
+ source: componentContents.join("\n"),
731
+ needsUtility
732
+ });
733
+ return { files, dependencies };
734
+ }
735
+ async function assertNoConflicts(files, force) {
736
+ if (force) return;
737
+ const conflicts = [];
738
+ for (const file of files) {
739
+ if (await fileExists(file.absolutePath)) conflicts.push(file.relativePath);
740
+ }
741
+ if (conflicts.length > 0) {
742
+ throw new CliError(`Refusing to overwrite existing files. Use --force to replace:
743
+ ${conflicts.map((path) => ` - ${path}`).join("\n")}`);
744
+ }
745
+ }
746
+ async function writePlannedFiles(files) {
747
+ for (const file of files) {
748
+ await mkdir3(dirname4(file.absolutePath), { recursive: true });
749
+ await writeFile3(file.absolutePath, file.content, "utf8");
750
+ }
751
+ }
752
+ function summarizeFiles(projectRoot, files) {
753
+ return files.map((file) => toPosixPath(relative3(projectRoot, file.absolutePath)));
754
+ }
755
+ function transformComponentSource(content, config) {
756
+ const needsUtility = content.includes("@wensity/ui/lib");
757
+ const needsPrimitives = content.includes("@wensity/ui/components");
758
+ let transformed = content;
759
+ transformed = transformed.replace(/from\s+["']@wensity\/ui\/lib["']/g, `from "${config.aliases.utils}"`);
760
+ transformed = transformed.replace(/from\s+["']@wensity\/ui\/components["']/g, `from "${config.aliases.components}/primitives"`);
761
+ return { content: transformed, needsUtility, needsPrimitives };
762
+ }
763
+ function collectDependencies(input) {
764
+ const dependencies = /* @__PURE__ */ new Set();
765
+ for (const dependency of input.declared) {
766
+ const normalized = normalizePackageName(dependency);
767
+ if (normalized) dependencies.add(normalized);
768
+ }
769
+ for (const dependency of parseImports(input.source)) {
770
+ const normalized = normalizePackageName(dependency);
771
+ if (normalized) dependencies.add(normalized);
772
+ }
773
+ if (input.needsUtility) {
774
+ dependencies.add("clsx");
775
+ dependencies.add("tailwind-merge");
776
+ }
777
+ return [...dependencies].sort();
778
+ }
779
+ function parseImports(source) {
780
+ const imports = [];
781
+ const patterns = [
782
+ /(?:import|export)\s+(?:type\s+)?(?:[\s\S]*?\s+from\s*)?["']([^"']+)["']/g,
783
+ /import\(\s*["']([^"']+)["']\s*\)/g
784
+ ];
785
+ for (const pattern of patterns) {
786
+ for (const match of source.matchAll(pattern)) {
787
+ if (match[1]) imports.push(match[1]);
788
+ }
789
+ }
790
+ return imports;
791
+ }
792
+ function normalizePackageName(specifier) {
793
+ if (!specifier || specifier.startsWith(".") || specifier.startsWith("/") || specifier.startsWith("@/")) {
794
+ return void 0;
795
+ }
796
+ if (specifier.startsWith("@wensity/ui")) return void 0;
797
+ const packageName = specifier.startsWith("@") ? specifier.split("/").slice(0, 2).join("/") : specifier.split("/")[0];
798
+ if (ALWAYS_PRESENT_PACKAGES.has(packageName)) return void 0;
799
+ if (packageName.startsWith("node:")) return void 0;
800
+ return packageName;
801
+ }
802
+ function withSourceExtension(path, extension) {
803
+ const directory = parentDirectory(path);
804
+ const fileName = extensionlessPath(path.split("/").at(-1) ?? path);
805
+ return toPosixPath(join5(directory === "." ? "" : directory, `${fileName}.${extension}`));
806
+ }
807
+
808
+ // src/commands.ts
809
+ async function run(context) {
810
+ const parsed = parseArgs(context.argv);
811
+ if (hasFlag(parsed, "version")) {
812
+ print(CLI_VERSION);
813
+ return;
814
+ }
815
+ if (hasFlag(parsed, "help") || parsed.positionals.length === 0) {
816
+ print(HELP_TEXT.trimEnd());
817
+ return;
818
+ }
819
+ const [command, ...positionals] = parsed.positionals;
820
+ switch (command) {
821
+ case "init":
822
+ await initCommand(context, parsed);
823
+ return;
824
+ case "login":
825
+ await loginCommand(context, parsed);
826
+ return;
827
+ case "logout":
828
+ await logoutCommand(context);
829
+ return;
830
+ case "whoami":
831
+ await whoamiCommand(context, parsed);
832
+ return;
833
+ case "list":
834
+ await listCommand(context, parsed);
835
+ return;
836
+ case "add":
837
+ await addCommand(context, parsed, positionals, false);
838
+ return;
839
+ case "update":
840
+ await addCommand(context, parsed, positionals, true);
841
+ return;
842
+ case "version":
843
+ print(CLI_VERSION);
844
+ return;
845
+ default:
846
+ throw new CliError(`Unknown command: ${command}
847
+
848
+ ${HELP_TEXT.trimEnd()}`);
849
+ }
850
+ }
851
+ async function initCommand(context, parsed) {
852
+ const dryRun = hasFlag(parsed, "dry-run");
853
+ const force = hasFlag(parsed, "force");
854
+ const install = !hasFlag(parsed, "no-install");
855
+ const apiUrl = getStringOption(parsed, "api-url") ?? context.env.WENSITY_API_URL ?? DEFAULT_API_ORIGIN;
856
+ const config = createDefaultConfig(context.cwd, apiUrl);
857
+ const configPath = join6(context.cwd, CONFIG_FILE_NAME);
858
+ const utilityPath = resolveConfiguredFilePath(context.cwd, config.utils);
859
+ const utilityRelativePath = toPosixPath(config.utils.endsWith(".ts") ? config.utils : `${config.utils}.ts`);
860
+ const dependencies = ["clsx", "tailwind-merge"];
861
+ if (dryRun) {
862
+ print("Would write:");
863
+ print(formatList([CONFIG_FILE_NAME, utilityRelativePath]));
864
+ if (install) print(`Would install: ${dependencies.join(" ")}`);
865
+ return;
866
+ }
867
+ await writeConfigFile(configPath, config, force);
868
+ if (force || !await fileExists(utilityPath)) {
869
+ await writeTextFile(utilityPath, cnUtilityTemplate());
870
+ }
871
+ if (install) {
872
+ runInstall(context.cwd, missingDependencies(context.cwd, dependencies));
873
+ }
874
+ print("Created wensity.json and local Wensity helper files.");
875
+ }
876
+ async function loginCommand(context, parsed) {
877
+ const apiUrl = getStringOption(parsed, "api-url") ?? context.env.WENSITY_API_URL ?? DEFAULT_API_ORIGIN;
878
+ const apiBaseUrl = normalizeUrlOrigin(apiUrl);
879
+ const providedToken = getStringOption(parsed, "token") ?? context.env.WENSITY_TOKEN;
880
+ const token = providedToken ?? await readTokenInteractively();
881
+ if (!token) {
882
+ throw new CliError("A token is required. Create one in the Wensity dashboard and run wensity login again.");
883
+ }
884
+ const whoami = await requestJson(joinUrl(apiBaseUrl, "/api/cli/whoami"), { token });
885
+ const path = await writeCredentials(context.env, {
886
+ version: 1,
887
+ apiBaseUrl,
888
+ token,
889
+ user: whoami.user,
890
+ plan: whoami.plan,
891
+ savedAt: (/* @__PURE__ */ new Date()).toISOString()
892
+ });
893
+ print(`Logged in as ${whoami.user.email}.`);
894
+ print(`Credentials saved to ${path}.`);
895
+ }
896
+ async function logoutCommand(context) {
897
+ const removed = await deleteCredentials(context.env);
898
+ print(removed ? "Logged out of Wensity." : "No Wensity credentials were stored.");
899
+ }
900
+ async function whoamiCommand(context, parsed) {
901
+ const credentials = await requireCredentials(context.env);
902
+ const whoami = await requestJson(joinUrl(credentials.apiBaseUrl, "/api/cli/whoami"), {
903
+ token: credentials.token
904
+ });
905
+ if (hasFlag(parsed, "json")) {
906
+ printJson({ ...whoami, apiBaseUrl: credentials.apiBaseUrl, credentialsPath: credentialsPath(context.env) });
907
+ return;
908
+ }
909
+ print(`Signed in as ${whoami.user.email}`);
910
+ if (whoami.plan?.tier) {
911
+ print(`Plan: ${whoami.plan.tier}${whoami.plan.status ? ` (${whoami.plan.status})` : ""}`);
912
+ }
913
+ }
914
+ async function listCommand(context, parsed) {
915
+ const apiUrl = getStringOption(parsed, "api-url") ?? context.env.WENSITY_API_URL;
916
+ const registryOverride = getStringOption(parsed, "registry");
917
+ const loaded = await loadConfig(context.cwd, { registry: registryOverride, apiUrl });
918
+ const response = await requestJson(loaded.config.registry);
919
+ if (hasFlag(parsed, "json")) {
920
+ printJson(response);
921
+ return;
922
+ }
923
+ if (response.components.length === 0) {
924
+ print("No registry components found.");
925
+ return;
926
+ }
927
+ print("Available Wensity components:");
928
+ print(formatComponentGroup("Free", response.components.filter((component) => component.access !== "pro")));
929
+ print(formatComponentGroup("Pro", response.components.filter((component) => component.access === "pro")));
930
+ }
931
+ function formatComponentGroup(label, components) {
932
+ const rows = components.map((component) => {
933
+ const category = component.category ? ` ${component.category}` : "";
934
+ return ` ${component.slug.padEnd(32)}${category}`;
935
+ });
936
+ return rows.length > 0 ? `
937
+ ${label}
938
+ ${rows.join("\n")}` : `
939
+ ${label}
940
+ No components`;
941
+ }
942
+ async function addCommand(context, parsed, positionals, updateMode) {
943
+ const slug = positionals[0];
944
+ if (!slug) throw new CliError(`Missing component slug.
945
+
946
+ Usage: wensity ${updateMode ? "update" : "add"} <slug>`);
947
+ const apiUrl = getStringOption(parsed, "api-url") ?? context.env.WENSITY_API_URL;
948
+ const registryOverride = getStringOption(parsed, "registry");
949
+ const loaded = await loadConfig(context.cwd, { registry: registryOverride, apiUrl });
950
+ const credentials = await readCredentials(context.env);
951
+ const payload = await fetchComponentPayload({ slug, registry: loaded.config.registry, credentials });
952
+ const prepared = await prepareInstall(loaded.projectRoot, loaded.config, payload);
953
+ const force = updateMode || hasFlag(parsed, "force");
954
+ const dryRun = hasFlag(parsed, "dry-run");
955
+ const install = !hasFlag(parsed, "no-install");
956
+ await assertNoConflicts(prepared.files, force || dryRun);
957
+ const plannedFiles = summarizeFiles(loaded.projectRoot, prepared.files);
958
+ const missing = missingDependencies(loaded.projectRoot, prepared.dependencies);
959
+ if (dryRun) {
960
+ print("Would write:");
961
+ print(formatList(plannedFiles));
962
+ if (install && missing.length > 0) print(`Would install: ${missing.join(" ")}`);
963
+ return;
964
+ }
965
+ await writePlannedFiles(prepared.files);
966
+ if (install) {
967
+ runInstall(loaded.projectRoot, missing);
968
+ }
969
+ print(`${updateMode ? "Updated" : "Added"} ${payload.name}.`);
970
+ print("Files written:");
971
+ print(formatList(plannedFiles));
972
+ if (!install && missing.length > 0) {
973
+ warn(`Dependencies were not installed: ${missing.join(" ")}`);
974
+ }
975
+ }
976
+ async function fetchComponentPayload(input) {
977
+ info(`Fetching ${input.slug}...`);
978
+ const metadata = await requestJson(registryMetadataUrl(input.registry, input.slug));
979
+ if (input.credentials) {
980
+ const cliEndpoint = metadata.endpoints?.cli ?? `/api/cli/${input.slug}`;
981
+ const endpoint2 = cliEndpoint.startsWith("http") ? cliEndpoint : joinUrl(input.credentials.apiBaseUrl, cliEndpoint);
982
+ return requestJson(endpoint2, { token: input.credentials.token });
983
+ }
984
+ if (metadata.access === "pro") {
985
+ throw new CliError(`${metadata.name} is a premium component. Run wensity login, then try again.`);
986
+ }
987
+ const publicSourceEndpoint = metadata.endpoints?.registry ?? registrySlugUrl(input.registry, input.slug);
988
+ const endpoint = publicSourceEndpoint.startsWith("http") ? publicSourceEndpoint : joinUrl(registryOrigin(input.registry), publicSourceEndpoint);
989
+ return requestJson(endpoint);
990
+ }
991
+ async function requireCredentials(env) {
992
+ const credentials = await readCredentials(env);
993
+ if (!credentials) {
994
+ throw new CliError("Not logged in. Run wensity login with an API token from the Wensity dashboard.");
995
+ }
996
+ return credentials;
997
+ }
998
+ function registryMetadataUrl(registry, slug) {
999
+ const url = new URL(registry);
1000
+ url.searchParams.set("slug", slug);
1001
+ return url.toString();
1002
+ }
1003
+ function registrySlugUrl(registry, slug) {
1004
+ return joinUrl(registry.endsWith("/") ? registry : `${registry}/`, slug);
1005
+ }
1006
+ function registryOrigin(registry) {
1007
+ const url = new URL(registry);
1008
+ return `${url.protocol}//${url.host}`;
1009
+ }
1010
+ async function writeTextFile(path, content) {
1011
+ await mkdir4(dirname5(path), { recursive: true });
1012
+ await writeFile4(path, content, "utf8");
1013
+ }
1014
+
1015
+ // src/index.ts
1016
+ run({
1017
+ argv: process.argv.slice(2),
1018
+ cwd: process.cwd(),
1019
+ env: process.env
1020
+ }).catch((error) => {
1021
+ if (error instanceof CliError) {
1022
+ process.stderr.write(`${error.message}
1023
+ `);
1024
+ process.exitCode = error.exitCode;
1025
+ return;
1026
+ }
1027
+ const message = error instanceof Error ? error.stack ?? error.message : String(error);
1028
+ process.stderr.write(`${message}
1029
+ `);
1030
+ process.exitCode = 1;
1031
+ });
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "wensity",
3
+ "version": "0.1.0",
4
+ "description": "Wensity component registry CLI",
5
+ "type": "module",
6
+ "bin": {
7
+ "wensity": "dist/index.js"
8
+ },
9
+ "exports": {
10
+ "./package.json": "./package.json"
11
+ },
12
+ "files": [
13
+ "dist",
14
+ "README.md"
15
+ ],
16
+ "scripts": {
17
+ "build": "tsup src/index.ts --format esm --platform node --target node20 --clean",
18
+ "typecheck": "tsc --noEmit",
19
+ "lint": "echo 'no lint'"
20
+ },
21
+ "engines": {
22
+ "node": ">=20.0.0"
23
+ },
24
+ "devDependencies": {
25
+ "@types/node": "^22.10.2",
26
+ "tsup": "^8.3.5",
27
+ "typescript": "^5.7.2"
28
+ },
29
+ "publishConfig": {
30
+ "access": "public"
31
+ }
32
+ }