praxys-ui 1.2.9 → 1.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +901 -119
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -3,10 +3,10 @@ import { Command } from "commander";
3
3
  import chalk from "chalk";
4
4
  import ora from "ora";
5
5
  import prompts from "prompts";
6
- import { existsSync, mkdirSync, writeFileSync } from "fs";
6
+ import { existsSync, mkdirSync, writeFileSync, readFileSync, unlinkSync } from "fs";
7
7
  import { join } from "path";
8
8
  import { execSync } from "child_process";
9
- const VERSION = "1.2.9";
9
+ const VERSION = "1.3.3";
10
10
  // ─── Utility file contents ──────────────────────────────
11
11
  const UTILS_CONTENT = `import { clsx, type ClassValue } from "clsx";
12
12
  import { twMerge } from "tailwind-merge";
@@ -15,81 +15,90 @@ export function cn(...inputs: ClassValue[]) {
15
15
  return twMerge(clsx(inputs));
16
16
  }
17
17
  `;
18
- // ─── Component registry (slug → raw GitHub URL) ─────────
18
+ // ─── Component registry ─────────────────────────────────
19
19
  const COMPONENTS_BASE_URL = "https://raw.githubusercontent.com/sushanttverma/Praxys-UI/main/app/components/ui";
20
- const COMPONENT_LIST = [
21
- "accordion",
22
- "alert",
23
- "animated-button",
24
- "animated-counter",
25
- "animated-hero",
26
- "animated-input",
27
- "animated-number",
28
- "animated-select",
29
- "animated-tabs",
30
- "animated-textarea",
31
- "animated-toggle",
32
- "autocomplete",
33
- "avatar-group",
34
- "badge",
35
- "breadcrumbs",
36
- "checkbox",
37
- "color-picker",
38
- "combobox",
39
- "command-menu",
40
- "creepy-button",
41
- "data-table",
42
- "date-picker",
43
- "displacement-text",
44
- "divider",
45
- "dropdown-menu",
46
- "expandable-bento-grid",
47
- "file-upload",
48
- "flip-fade-text",
49
- "flip-text",
50
- "floating-menu",
51
- "folder-preview",
52
- "glass-dock",
53
- "glow-border-card",
54
- "gradient-mesh",
55
- "image-comparison",
56
- "infinite-scroll",
57
- "interactive-book",
58
- "kbd",
59
- "light-lines",
60
- "line-hover-link",
61
- "liquid-metal",
62
- "liquid-ocean",
63
- "logo-slider",
64
- "magnetic-cursor",
65
- "masked-avatars",
66
- "modal-dialog",
67
- "morphing-text",
68
- "otp-input",
69
- "pagination",
70
- "parallax-scroll",
71
- "perspective-grid",
72
- "progress-bar",
73
- "radio-group",
74
- "rating",
75
- "reveal-loader",
76
- "sheet",
77
- "skeleton-loader",
78
- "slider",
79
- "social-flip-button",
80
- "spotlight-card",
81
- "spotlight-navbar",
82
- "staggered-grid",
83
- "stats-card",
84
- "stepper",
85
- "switch",
86
- "tag-input",
87
- "testimonials-card",
88
- "timeline",
89
- "toast-notification",
90
- "tooltip",
91
- "typewriter-text",
92
- ];
20
+ const COMPONENT_REGISTRY = {
21
+ "accordion": { title: "Accordion", description: "Smooth expand/collapse panels with animated chevron, supports single or multiple open panels.", category: "navigation", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
22
+ "alert": { title: "Alert", description: "Animated alert banner with four variants (info, success, warning, error), contextual icons, optional title, dismissible with exit animation.", category: "visual", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
23
+ "animated-button": { title: "Animated Button", description: "A button with a shiny border sweep and text reveal effect, perfect for calls to action.", category: "buttons", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
24
+ "animated-counter": { title: "Animated Counter", description: "A number counter that animates from one value to another using spring physics, triggered when scrolled into view.", category: "text", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
25
+ "animated-hero": { title: "Animated Hero", description: "A reusable hero section with staggered entrance animations, pulsing radial glow, grid background, badge, and dual CTA buttons.", category: "media", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
26
+ "animated-input": { title: "Animated Input", description: "A floating-label text input with animated border, focus ring, optional left/right icons, error state, and three sizes.", category: "buttons", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
27
+ "animated-number": { title: "Animated Number", description: "Smoothly animates between number values with a spring transition.", category: "text", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
28
+ "animated-select": { title: "Animated Select", description: "An accessible custom dropdown select with animated open/close, keyboard navigation, and spring transitions.", category: "navigation", dependencies: ["framer-motion", "clsx", "tailwind-merge", "lucide-react"] },
29
+ "animated-tabs": { title: "Animated Tabs", description: "Tab navigation with a smooth sliding indicator and crossfade content transitions.", category: "navigation", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
30
+ "animated-textarea": { title: "Animated Textarea", description: "A floating-label textarea with animated border, focus ring, character counter, auto-resize support, and error state.", category: "buttons", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
31
+ "animated-toggle": { title: "Animated Toggle", description: "A switch toggle with spring-animated knob, multiple sizes, ARIA role='switch', keyboard support, and disabled state.", category: "buttons", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
32
+ "autocomplete": { title: "Autocomplete", description: "An accessible autocomplete input with async search, debouncing, keyboard navigation, loading states, and animated dropdown.", category: "navigation", dependencies: ["framer-motion", "clsx", "tailwind-merge", "lucide-react"], isNew: true },
33
+ "avatar-group": { title: "Avatar Group", description: "Stacked avatar circles with overlap, max display count with '+N' overflow indicator, three sizes, and fallback initials.", category: "visual", dependencies: ["clsx", "tailwind-merge"] },
34
+ "badge": { title: "Badge", description: "Animated badge with multiple variants (default, success, warning, error, info), three sizes, optional icon, and removable.", category: "buttons", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
35
+ "breadcrumbs": { title: "Breadcrumbs", description: "Navigation breadcrumbs with Next.js Link integration, custom separator support, aria-label accessibility, and current page indicator.", category: "navigation", dependencies: ["clsx", "tailwind-merge", "lucide-react"] },
36
+ "checkbox": { title: "Checkbox", description: "An accessible animated checkbox with spring check-mark animation, error state, label support, and keyboard interaction.", category: "buttons", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
37
+ "color-picker": { title: "Color Picker", description: "A comprehensive color picker with HSL sliders, preset swatches, hex/RGB/HSL format toggling, and copy-to-clipboard.", category: "buttons", dependencies: ["framer-motion", "clsx", "tailwind-merge", "lucide-react"], isNew: true },
38
+ "combobox": { title: "Combobox", description: "A searchable select component with keyboard navigation, multi-select support, and animated dropdown.", category: "navigation", dependencies: ["framer-motion", "clsx", "tailwind-merge", "lucide-react"] },
39
+ "command-menu": { title: "Command Menu", description: "A command palette with search filtering, grouped items, keyboard navigation, match highlighting, and shortcut badges.", category: "navigation", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
40
+ "creepy-button": { title: "Creepy Button", description: "A horror-inspired button with flickering background, dripping accent line, and staggered glitchy text animation on hover.", category: "buttons", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
41
+ "data-table": { title: "Data Table", description: "Sortable data table with column definitions, ascending/descending sort indicators, striped rows, and hover state.", category: "cards", dependencies: ["framer-motion", "clsx", "tailwind-merge", "lucide-react"] },
42
+ "date-picker": { title: "Date Picker", description: "A fully-featured date picker with calendar view, keyboard navigation, month/year selectors, and optional range selection.", category: "buttons", dependencies: ["framer-motion", "clsx", "tailwind-merge", "lucide-react"] },
43
+ "displacement-text": { title: "3D Displacement Text", description: "Mouse-reactive 3D text with depth shadows that follows cursor movement, creating a dramatic displacement effect.", category: "text", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
44
+ "divider": { title: "Divider", description: "Animated divider with horizontal/vertical orientation, optional centered label, and gradient mode.", category: "cards", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
45
+ "dropdown-menu": { title: "Dropdown Menu", description: "An animated dropdown menu with full keyboard navigation, click-outside close, divider and disabled item support.", category: "navigation", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
46
+ "expandable-bento-grid": { title: "Expandable Bento Grid", description: "A bento-style grid where clicking an item expands it into a full overlay modal with smooth layout animations.", category: "cards", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
47
+ "file-upload": { title: "File Upload", description: "An accessible drag-and-drop file upload component with validation, progress animation, file list preview, and keyboard interaction.", category: "buttons", dependencies: ["framer-motion", "clsx", "tailwind-merge", "lucide-react"], isNew: true },
48
+ "flip-fade-text": { title: "Flip Fade Text", description: "A rotating text component that cycles through words with a 3D flip and fade transition, perfect for hero taglines.", category: "text", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
49
+ "flip-text": { title: "Flip Text", description: "Characters flip in with a smooth 3D rotation on mount and on hover, great for headings and titles.", category: "text", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
50
+ "floating-menu": { title: "Floating Menu", description: "A floating pill menu that expands into a card with GSAP-animated hamburger-to-X, text scramble effect, and staggered item reveals.", category: "navigation", dependencies: ["gsap", "clsx", "tailwind-merge"], isNew: true },
51
+ "folder-preview": { title: "Folder Preview", description: "An interactive folder component that expands to reveal a file tree with staggered entrance animations and custom icons.", category: "media", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
52
+ "glass-dock": { title: "Glass Dock", description: "A macOS-inspired dock with glassmorphism styling, spring-animated hover magnification, and tooltips.", category: "navigation", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
53
+ "glow-border-card": { title: "Glow Border Card", description: "A card with an animated glowing border that follows cursor movement.", category: "cards", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
54
+ "gradient-mesh": { title: "Gradient Mesh", description: "Animated multi-color gradient mesh background with smooth transitions between color states.", category: "visual", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
55
+ "image-comparison": { title: "Image Comparison", description: "A before/after image comparison slider with pointer-capture dragging, clip-based reveal, and an animated drag handle.", category: "media", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
56
+ "infinite-scroll": { title: "Infinite Scroll", description: "An Intersection Observer-based infinite scroll container with loading state, configurable threshold, and animated loader.", category: "navigation", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
57
+ "interactive-book": { title: "Interactive Book", description: "A 3D page-flip book component with AnimatePresence transitions, directional flip variants, and dot navigation.", category: "media", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
58
+ "kbd": { title: "Kbd", description: "Keyboard shortcut indicator with monospace font, shadow border styling, and inline usage support.", category: "text", dependencies: ["clsx", "tailwind-merge"] },
59
+ "light-lines": { title: "Light Lines", description: "Animated vertical light beams that sweep across a container, creating a dramatic visual effect.", category: "visual", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
60
+ "line-hover-link": { title: "Line Hover Link", description: "A link with an animated underline that slides in on hover.", category: "navigation", dependencies: ["clsx", "tailwind-merge"] },
61
+ "liquid-metal": { title: "Liquid Metal", description: "A cursor-reactive surface with chrome-like liquid metal reflections that follow mouse movement.", category: "visual", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
62
+ "liquid-ocean": { title: "Liquid Ocean", description: "Animated layered SVG waves with configurable colors, wave count, and speed for a mesmerizing ocean effect.", category: "visual", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
63
+ "logo-slider": { title: "Logo Slider", description: "An infinite-scrolling marquee of logos or brand icons with fade edges, pause-on-hover, and bidirectional support.", category: "media", dependencies: ["clsx", "tailwind-merge"] },
64
+ "magnetic-cursor": { title: "Magnetic Cursor", description: "A wrapper that creates a magnetic pull effect, attracting elements toward the cursor with configurable strength.", category: "visual", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
65
+ "masked-avatars": { title: "Masked Avatars", description: "A stacked avatar group with overlapping circular avatars, hover-to-pop animation, tooltips, and a +N overflow indicator.", category: "media", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
66
+ "modal-dialog": { title: "Modal Dialog", description: "An animated modal dialog with backdrop blur, spring scale transition, Escape key handling, scroll lock, and ARIA attributes.", category: "navigation", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
67
+ "morphing-text": { title: "Morphing Text", description: "Text that morphs between words using a blur crossfade effect, creating smooth character interpolation transitions.", category: "text", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
68
+ "otp-input": { title: "OTP Input", description: "An accessible OTP/PIN input component with auto-focus, paste support, keyboard navigation, and animated focus states.", category: "buttons", dependencies: ["framer-motion", "clsx", "tailwind-merge", "lucide-react"], isNew: true },
69
+ "pagination": { title: "Pagination", description: "Accessible pagination with animated active-page indicator, smart ellipsis, two sizes, and prev/next buttons.", category: "navigation", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
70
+ "parallax-scroll": { title: "Parallax Scroll", description: "Scroll-driven parallax layers with configurable speed multipliers for creating depth effects.", category: "visual", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
71
+ "perspective-grid": { title: "Perspective Grid", description: "A 3D perspective grid that tilts items on hover with smooth spring animations and staggered entrance.", category: "cards", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
72
+ "progress-bar": { title: "Progress Bar", description: "An animated progress bar with multiple sizes, optional label and value display, custom colors, and candy-stripe animation.", category: "visual", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
73
+ "radio-group": { title: "Radio Group", description: "An accessible animated radio group with spring selection animation, horizontal/vertical layout, and keyboard navigation.", category: "buttons", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
74
+ "rating": { title: "Rating", description: "An accessible animated rating component with star icons, half-star support, custom icons, hover states, and spring animations.", category: "buttons", dependencies: ["framer-motion", "clsx", "tailwind-merge", "lucide-react"], isNew: true },
75
+ "reveal-loader": { title: "Reveal Loader", description: "A loading animation with a curtain reveal effect — shows a progress bar, then slides away to reveal content.", category: "visual", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
76
+ "sheet": { title: "Sheet", description: "A slide-in panel overlay (drawer) with four sides, backdrop blur, scroll lock, Escape-to-close, and spring transition.", category: "navigation", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
77
+ "skeleton-loader": { title: "Skeleton Loader", description: "Animated placeholder loading states with shimmer effect, supporting text, avatar, card, and button variants.", category: "visual", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
78
+ "slider": { title: "Slider", description: "An accessible animated slider with drag interaction, keyboard navigation, tooltip value display, and spring animations.", category: "buttons", dependencies: ["framer-motion", "clsx", "tailwind-merge", "lucide-react"], isNew: true },
79
+ "social-flip-button": { title: "Social Flip Button", description: "A button that flips to reveal social media icons on hover, with a smooth 3D rotation transition.", category: "buttons", dependencies: ["framer-motion", "clsx", "tailwind-merge", "lucide-react"] },
80
+ "spotlight-card": { title: "Spotlight Card", description: "A card with a radial spotlight that follows the cursor, creating a flashlight reveal effect.", category: "cards", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
81
+ "spotlight-navbar": { title: "Spotlight Navbar", description: "A horizontal navigation bar with a smooth animated spotlight background that follows the hovered item.", category: "navigation", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
82
+ "staggered-grid": { title: "Staggered Grid", description: "A grid layout where children animate in with a staggered fade-up effect as they enter the viewport.", category: "cards", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
83
+ "stats-card": { title: "Stats Card", description: "Animated statistics card with spring-based number counter, trend indicator, optional icon, and scroll-triggered entrance.", category: "cards", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
84
+ "stepper": { title: "Stepper", description: "A multi-step indicator with animated check icons, spring-scaled active step, and animated connector lines.", category: "navigation", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
85
+ "switch": { title: "Switch", description: "An accessible animated toggle switch with spring thumb animation, size variants, label support, and keyboard interaction.", category: "buttons", dependencies: ["framer-motion", "clsx", "tailwind-merge", "lucide-react"], isNew: true },
86
+ "tag-input": { title: "Tag Input", description: "Animated tag input with Enter/comma to add, Backspace to remove, max tags limit, and AnimatePresence transitions.", category: "buttons", dependencies: ["framer-motion", "clsx", "tailwind-merge", "lucide-react"] },
87
+ "testimonials-card": { title: "Testimonials Card", description: "An auto-rotating testimonials card with smooth crossfade transitions, avatar display, and dot navigation.", category: "cards", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
88
+ "timeline": { title: "Timeline", description: "Alternating two-column timeline with scroll-triggered animations, connecting lines, active-state pulse rings, and optional icons.", category: "cards", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
89
+ "toast-notification": { title: "Toast Notification", description: "Stackable animated toast notifications with variants (success, error, warning, info), auto-dismiss, and manual dismiss.", category: "visual", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
90
+ "tooltip": { title: "Tooltip", description: "A tooltip with 4 positions, configurable delay, direction-aware motion animation, and arrow pointer.", category: "navigation", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
91
+ "typewriter-text": { title: "Typewriter Text", description: "An animated typing effect that cycles through strings, typing and deleting characters with a blinking cursor.", category: "text", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
92
+ };
93
+ const COMPONENT_LIST = Object.keys(COMPONENT_REGISTRY);
94
+ const CATEGORY_COLORS = {
95
+ buttons: "#E84E2D",
96
+ cards: "#3B82F6",
97
+ text: "#8B5CF6",
98
+ navigation: "#10B981",
99
+ visual: "#F59E0B",
100
+ media: "#EC4899",
101
+ };
93
102
  // ─── Helpers ─────────────────────────────────────────────
94
103
  function detectPackageManager() {
95
104
  const cwd = process.cwd();
@@ -127,6 +136,141 @@ async function fetchComponent(slug) {
127
136
  }
128
137
  return res.text();
129
138
  }
139
+ function toPascalCase(slug) {
140
+ return slug
141
+ .split("-")
142
+ .map((s) => s.charAt(0).toUpperCase() + s.slice(1))
143
+ .join("");
144
+ }
145
+ function loadConfig() {
146
+ const configPath = join(process.cwd(), "praxys.config.json");
147
+ if (!existsSync(configPath))
148
+ return null;
149
+ try {
150
+ const raw = readFileSync(configPath, "utf-8");
151
+ return JSON.parse(raw);
152
+ }
153
+ catch {
154
+ return null;
155
+ }
156
+ }
157
+ function getComponentsDir(optDir) {
158
+ if (optDir)
159
+ return optDir;
160
+ const config = loadConfig();
161
+ return config?.componentsDir ?? "components/ui";
162
+ }
163
+ function fuzzyMatch(query, text) {
164
+ const lower = text.toLowerCase();
165
+ const q = query.toLowerCase();
166
+ return lower.includes(q);
167
+ }
168
+ function truncate(str, max) {
169
+ if (str.length <= max)
170
+ return str;
171
+ return str.slice(0, max - 1) + "…";
172
+ }
173
+ function levenshtein(a, b) {
174
+ const m = a.length, n = b.length;
175
+ const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
176
+ for (let i = 0; i <= m; i++)
177
+ dp[i][0] = i;
178
+ for (let j = 0; j <= n; j++)
179
+ dp[0][j] = j;
180
+ for (let i = 1; i <= m; i++) {
181
+ for (let j = 1; j <= n; j++) {
182
+ dp[i][j] = a[i - 1] === b[j - 1]
183
+ ? dp[i - 1][j - 1]
184
+ : 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
185
+ }
186
+ }
187
+ return dp[m][n];
188
+ }
189
+ function didYouMean(input) {
190
+ let best = "";
191
+ let bestDist = Infinity;
192
+ for (const slug of COMPONENT_LIST) {
193
+ const dist = levenshtein(input.toLowerCase(), slug.toLowerCase());
194
+ if (dist < bestDist) {
195
+ bestDist = dist;
196
+ best = slug;
197
+ }
198
+ }
199
+ // Only suggest if distance is reasonable (less than ~40% of input length)
200
+ if (bestDist <= Math.max(2, Math.floor(input.length * 0.4))) {
201
+ return best;
202
+ }
203
+ return null;
204
+ }
205
+ function printNotFound(slug) {
206
+ const suggestion = didYouMean(slug);
207
+ if (suggestion) {
208
+ console.log(chalk.red(` Component "${slug}" not found.`) + chalk.dim(` Did you mean ${chalk.bold(suggestion)}?`));
209
+ }
210
+ else {
211
+ console.log(chalk.red(` Component "${slug}" not found.`));
212
+ }
213
+ }
214
+ async function checkForUpdates() {
215
+ try {
216
+ const res = await fetch("https://registry.npmjs.org/praxys-ui/latest", {
217
+ signal: AbortSignal.timeout(3000),
218
+ });
219
+ if (!res.ok)
220
+ return;
221
+ const data = await res.json();
222
+ const latest = data.version;
223
+ if (latest && latest !== VERSION) {
224
+ console.log(chalk.dim(` Update available: ${VERSION} → ${chalk.bold(latest)} Run ${chalk.cyan("npm i -g praxys-ui")} to update.`));
225
+ console.log("");
226
+ }
227
+ }
228
+ catch {
229
+ // Silently ignore — no network, timeout, etc.
230
+ }
231
+ }
232
+ function colorizeSource(source) {
233
+ const lines = source.split("\n");
234
+ return lines
235
+ .map((line) => {
236
+ // Comments
237
+ if (line.trimStart().startsWith("//"))
238
+ return chalk.dim.green(line);
239
+ if (line.trimStart().startsWith("/*") || line.trimStart().startsWith("*"))
240
+ return chalk.dim.green(line);
241
+ // Highlight keywords
242
+ let colored = line;
243
+ const keywords = [
244
+ "import",
245
+ "export",
246
+ "from",
247
+ "const",
248
+ "let",
249
+ "var",
250
+ "function",
251
+ "return",
252
+ "if",
253
+ "else",
254
+ "type",
255
+ "interface",
256
+ "default",
257
+ "async",
258
+ "await",
259
+ "new",
260
+ "class",
261
+ "extends",
262
+ "implements",
263
+ ];
264
+ for (const kw of keywords) {
265
+ const regex = new RegExp(`\\b${kw}\\b`, "g");
266
+ colored = colored.replace(regex, chalk.cyan(kw));
267
+ }
268
+ // Highlight strings
269
+ colored = colored.replace(/(["'`])(?:(?!\1|\\).|\\.)*\1/g, (m) => chalk.yellow(m));
270
+ return colored;
271
+ })
272
+ .join("\n");
273
+ }
130
274
  // ─── Commands ────────────────────────────────────────────
131
275
  const program = new Command();
132
276
  program
@@ -136,15 +280,14 @@ program
136
280
  // ── init ─────────────────────────────────────────────────
137
281
  program
138
282
  .command("init")
283
+ .alias("i")
139
284
  .description("Initialize Praxys UI in your project")
140
285
  .action(async () => {
141
286
  console.log("");
142
287
  console.log(chalk.bold(` ${chalk.hex("#E84E2D")("Praxys UI")} — init`));
143
288
  console.log("");
144
- // Detect package manager
145
289
  const pm = detectPackageManager();
146
290
  console.log(chalk.dim(` Package manager: ${pm}`));
147
- // Ask for component directory
148
291
  const { componentDir } = await prompts({
149
292
  type: "text",
150
293
  name: "componentDir",
@@ -165,7 +308,7 @@ program
165
308
  console.log(chalk.yellow(" Cancelled."));
166
309
  return;
167
310
  }
168
- // 1. Install dependencies
311
+ // Install dependencies
169
312
  const spinner = ora("Installing dependencies...").start();
170
313
  try {
171
314
  const deps = ["clsx", "tailwind-merge", "framer-motion"];
@@ -177,7 +320,7 @@ program
177
320
  console.log(chalk.dim(" Run manually: " +
178
321
  installCmd(pm, ["clsx", "tailwind-merge", "framer-motion"])));
179
322
  }
180
- // 2. Create utils file
323
+ // Create utils file
181
324
  const utilsSpinner = ora("Creating utility files...").start();
182
325
  try {
183
326
  const utilsPath = join(process.cwd(), utilsDir);
@@ -194,9 +337,17 @@ program
194
337
  catch {
195
338
  utilsSpinner.fail("Failed to create utils file");
196
339
  }
197
- // 3. Create component directory
340
+ // Create component directory
198
341
  const compPath = join(process.cwd(), componentDir);
199
342
  ensureDir(compPath);
343
+ // Write config file
344
+ const configPath = join(process.cwd(), "praxys.config.json");
345
+ const config = {
346
+ componentsDir: componentDir,
347
+ utilsDir: utilsDir,
348
+ };
349
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
350
+ console.log(chalk.green(" ✓ Created praxys.config.json"));
200
351
  console.log("");
201
352
  console.log(chalk.green(" ✓ Praxys UI initialized!"));
202
353
  console.log("");
@@ -205,33 +356,18 @@ program
205
356
  console.log("");
206
357
  });
207
358
  // ── add <component> ──────────────────────────────────────
208
- program
209
- .command("add")
210
- .description("Add a component to your project")
211
- .argument("<component>", "Component slug (e.g. animated-button)")
212
- .option("-d, --dir <directory>", "Component directory", "components/ui")
213
- .action(async (component, opts) => {
214
- console.log("");
215
- console.log(chalk.bold(` ${chalk.hex("#E84E2D")("Praxys UI")} — add ${chalk.cyan(component)}`));
216
- console.log("");
217
- // Validate component name
218
- if (!COMPONENT_LIST.includes(component)) {
219
- console.log(chalk.red(` Component "${component}" not found.`));
220
- console.log("");
221
- console.log(chalk.dim(" Available components:"));
222
- COMPONENT_LIST.forEach((c) => console.log(chalk.dim(` - ${c}`)));
223
- console.log("");
224
- return;
225
- }
359
+ async function addSingleComponent(component, dir, skipExisting) {
226
360
  const spinner = ora(`Fetching ${component}...`).start();
227
361
  try {
228
362
  const source = await fetchComponent(component);
229
- // Keep source as-is; import path @/lib/utils works with standard Next.js alias
230
- const rewritten = source;
231
- const compDir = join(process.cwd(), opts.dir);
363
+ const compDir = join(process.cwd(), dir);
232
364
  ensureDir(compDir);
233
365
  const filePath = join(compDir, `${component}.tsx`);
234
366
  if (existsSync(filePath)) {
367
+ if (skipExisting) {
368
+ spinner.info(`Skipped ${component} (already exists)`);
369
+ return true;
370
+ }
235
371
  spinner.stop();
236
372
  const { overwrite } = await prompts({
237
373
  type: "confirm",
@@ -241,39 +377,685 @@ program
241
377
  });
242
378
  if (!overwrite) {
243
379
  console.log(chalk.yellow(" Skipped."));
244
- return;
380
+ return true;
245
381
  }
246
382
  }
247
- writeFileSync(filePath, rewritten, "utf-8");
248
- spinner.succeed(`Added ${opts.dir}/${component}.tsx`);
249
- console.log("");
250
- console.log(chalk.dim(` Import: ${chalk.bold(`import ${toPascalCase(component)} from '@/${opts.dir}/${component}'`)}`));
251
- console.log("");
383
+ writeFileSync(filePath, source, "utf-8");
384
+ spinner.succeed(`Added ${dir}/${component}.tsx`);
385
+ return true;
252
386
  }
253
387
  catch (err) {
254
388
  spinner.fail(`Failed to fetch ${component}`);
255
389
  console.log(chalk.dim(` ${err instanceof Error ? err.message : "Unknown error"}`));
390
+ return false;
391
+ }
392
+ }
393
+ async function installDepsForComponents(slugs) {
394
+ // Collect unique dependencies across all given components
395
+ const allDeps = new Set();
396
+ for (const slug of slugs) {
397
+ const meta = COMPONENT_REGISTRY[slug];
398
+ if (meta) {
399
+ for (const dep of meta.dependencies) {
400
+ allDeps.add(dep);
401
+ }
402
+ }
403
+ }
404
+ if (allDeps.size === 0)
405
+ return;
406
+ const pm = detectPackageManager();
407
+ const depsArr = Array.from(allDeps);
408
+ const spinner = ora(`Installing dependencies: ${depsArr.join(", ")}...`).start();
409
+ try {
410
+ execSync(installCmd(pm, depsArr), { stdio: "pipe", cwd: process.cwd() });
411
+ spinner.succeed(`Installed ${depsArr.length} dependencies`);
412
+ }
413
+ catch {
414
+ spinner.fail("Failed to install dependencies");
415
+ console.log(chalk.dim(` Run manually: ${installCmd(pm, depsArr)}`));
416
+ }
417
+ }
418
+ program
419
+ .command("add")
420
+ .description("Add one or more components to your project")
421
+ .argument("[components...]", 'Component slugs (e.g. animated-button alert) or "all". Omit for interactive picker.')
422
+ .option("-d, --dir <directory>", "Component directory")
423
+ .option("-y, --yes", "Skip overwrite prompts (skip existing files)", false)
424
+ .option("--install-deps", "Install component dependencies after adding", false)
425
+ .action(async (components, opts) => {
426
+ const dir = getComponentsDir(opts.dir);
427
+ // ── interactive picker when no args ──────────────────
428
+ if (!components || components.length === 0) {
429
+ console.log("");
430
+ console.log(chalk.bold(` ${chalk.hex("#E84E2D")("Praxys UI")} — add components`));
431
+ console.log("");
432
+ const categoryOrder = ["buttons", "cards", "text", "navigation", "visual", "media"];
433
+ const choices = [];
434
+ for (const cat of categoryOrder) {
435
+ const color = CATEGORY_COLORS[cat] || "#FFFFFF";
436
+ // Category separator
437
+ choices.push({
438
+ title: chalk.hex(color).bold(`── ${cat.toUpperCase()} ──`),
439
+ value: `__sep_${cat}`,
440
+ description: "",
441
+ });
442
+ for (const [slug, meta] of Object.entries(COMPONENT_REGISTRY)) {
443
+ if (meta.category !== cat)
444
+ continue;
445
+ const newBadge = meta.isNew ? chalk.green(" NEW") : "";
446
+ choices.push({
447
+ title: `${meta.title}${newBadge}`,
448
+ value: slug,
449
+ description: truncate(meta.description, 50),
450
+ });
451
+ }
452
+ }
453
+ const { selected } = await prompts({
454
+ type: "multiselect",
455
+ name: "selected",
456
+ message: "Pick components to add (space to select, enter to confirm)",
457
+ choices: choices.map((c) => ({
458
+ title: c.title,
459
+ value: c.value,
460
+ description: c.description,
461
+ disabled: c.value.startsWith("__sep_"),
462
+ })),
463
+ hint: "- Space to select. Return to submit",
464
+ });
465
+ if (!selected || selected.length === 0) {
466
+ console.log(chalk.yellow(" No components selected."));
467
+ console.log("");
468
+ return;
469
+ }
470
+ const slugs = selected.filter((s) => !s.startsWith("__sep_"));
471
+ if (slugs.length === 0) {
472
+ console.log(chalk.yellow(" No components selected."));
473
+ console.log("");
474
+ return;
475
+ }
476
+ let added = 0;
477
+ let failed = 0;
478
+ for (const slug of slugs) {
479
+ const ok = await addSingleComponent(slug, dir, opts.yes);
480
+ if (ok)
481
+ added++;
482
+ else
483
+ failed++;
484
+ }
485
+ console.log("");
486
+ console.log(chalk.green(` ✓ ${added} components added`) +
487
+ (failed > 0 ? chalk.red(`, ${failed} failed`) : ""));
488
+ if (opts.installDeps) {
489
+ await installDepsForComponents(slugs);
490
+ }
491
+ console.log("");
492
+ return;
493
+ }
494
+ const label = components.length === 1 ? chalk.cyan(components[0]) : chalk.cyan(`${components.length} components`);
495
+ console.log("");
496
+ console.log(chalk.bold(` ${chalk.hex("#E84E2D")("Praxys UI")} — add ${label}`));
497
+ console.log("");
498
+ // ── add all ──────────────────────────────────────────
499
+ if (components.includes("all")) {
500
+ const { confirm } = await prompts({
501
+ type: "confirm",
502
+ name: "confirm",
503
+ message: `Add all ${COMPONENT_LIST.length} components to ${dir}?`,
504
+ initial: true,
505
+ });
506
+ if (!confirm) {
507
+ console.log(chalk.yellow(" Cancelled."));
508
+ return;
509
+ }
510
+ // Parallel fetch in batches of 6
511
+ let added = 0;
512
+ let failed = 0;
513
+ const batchSize = 6;
514
+ for (let i = 0; i < COMPONENT_LIST.length; i += batchSize) {
515
+ const batch = COMPONENT_LIST.slice(i, i + batchSize);
516
+ const results = await Promise.allSettled(batch.map((slug) => addSingleComponent(slug, dir, opts.yes)));
517
+ for (const r of results) {
518
+ if (r.status === "fulfilled" && r.value)
519
+ added++;
520
+ else
521
+ failed++;
522
+ }
523
+ }
524
+ console.log("");
525
+ console.log(chalk.green(` ✓ ${added} components added`) +
526
+ (failed > 0 ? chalk.red(`, ${failed} failed`) : ""));
527
+ if (opts.installDeps) {
528
+ await installDepsForComponents(COMPONENT_LIST);
529
+ }
530
+ console.log("");
531
+ return;
532
+ }
533
+ // ── validate all slugs first ─────────────────────────
534
+ const invalidSlugs = components.filter((c) => !COMPONENT_LIST.includes(c));
535
+ if (invalidSlugs.length > 0) {
536
+ for (const bad of invalidSlugs) {
537
+ const suggestion = didYouMean(bad);
538
+ if (suggestion) {
539
+ console.log(chalk.red(` Component "${bad}" not found.`) + chalk.dim(` Did you mean ${chalk.bold(suggestion)}?`));
540
+ }
541
+ else {
542
+ console.log(chalk.red(` Component "${bad}" not found.`));
543
+ }
544
+ }
545
+ console.log(chalk.dim(`\n Run ${chalk.bold("praxys-ui list")} to see available components.`));
546
+ console.log("");
547
+ return;
548
+ }
549
+ // ── add single or multiple ───────────────────────────
550
+ if (components.length === 1) {
551
+ const slug = components[0];
552
+ const ok = await addSingleComponent(slug, dir, false);
553
+ if (ok && opts.installDeps) {
554
+ await installDepsForComponents([slug]);
555
+ }
556
+ console.log("");
557
+ console.log(chalk.dim(` Import: ${chalk.bold(`import ${toPascalCase(slug)} from '@/${dir}/${slug}'`)}`));
558
+ console.log("");
559
+ }
560
+ else {
561
+ // Parallel fetch in batches of 6
562
+ let added = 0;
563
+ let failed = 0;
564
+ const batchSize = 6;
565
+ for (let i = 0; i < components.length; i += batchSize) {
566
+ const batch = components.slice(i, i + batchSize);
567
+ const results = await Promise.allSettled(batch.map((slug) => addSingleComponent(slug, dir, opts.yes)));
568
+ for (const r of results) {
569
+ if (r.status === "fulfilled" && r.value)
570
+ added++;
571
+ else
572
+ failed++;
573
+ }
574
+ }
575
+ console.log("");
576
+ console.log(chalk.green(` ✓ ${added} components added`) +
577
+ (failed > 0 ? chalk.red(`, ${failed} failed`) : ""));
578
+ if (opts.installDeps) {
579
+ await installDepsForComponents(components);
580
+ }
581
+ console.log("");
256
582
  }
257
583
  });
258
584
  // ── list ─────────────────────────────────────────────────
259
585
  program
260
586
  .command("list")
587
+ .alias("ls")
261
588
  .description("List all available components")
262
- .action(() => {
589
+ .option("-c, --category <category>", "Filter by category (buttons, cards, text, navigation, visual, media)")
590
+ .option("-n, --new", "Show only new components", false)
591
+ .option("-s, --search <query>", "Search components by name or description")
592
+ .option("--installed", "Show only locally installed components", false)
593
+ .option("-d, --dir <directory>", "Component directory (used with --installed)")
594
+ .action((opts) => {
263
595
  console.log("");
264
596
  console.log(chalk.bold(` ${chalk.hex("#E84E2D")("Praxys UI")} — components`));
265
597
  console.log("");
266
- COMPONENT_LIST.forEach((c) => console.log(` ${chalk.hex("#E84E2D")("●")} ${c}`));
598
+ let entries = Object.entries(COMPONENT_REGISTRY);
599
+ // Filter by category
600
+ if (opts.category) {
601
+ const cat = opts.category.toLowerCase();
602
+ entries = entries.filter(([, meta]) => meta.category === cat);
603
+ if (entries.length === 0) {
604
+ console.log(chalk.yellow(` No components found in category "${opts.category}".`));
605
+ console.log(chalk.dim(` Categories: buttons, cards, text, navigation, visual, media`));
606
+ console.log("");
607
+ return;
608
+ }
609
+ }
610
+ // Filter by installed
611
+ if (opts.installed) {
612
+ const compPath = join(process.cwd(), getComponentsDir(opts.dir));
613
+ entries = entries.filter(([slug]) => existsSync(join(compPath, `${slug}.tsx`)));
614
+ if (entries.length === 0) {
615
+ console.log(chalk.yellow(` No installed components found.`));
616
+ console.log("");
617
+ return;
618
+ }
619
+ }
620
+ // Filter by new
621
+ if (opts.new) {
622
+ entries = entries.filter(([, meta]) => meta.isNew);
623
+ }
624
+ // Filter by search
625
+ if (opts.search) {
626
+ const q = opts.search;
627
+ entries = entries.filter(([slug, meta]) => fuzzyMatch(q, slug) ||
628
+ fuzzyMatch(q, meta.title) ||
629
+ fuzzyMatch(q, meta.description));
630
+ if (entries.length === 0) {
631
+ console.log(chalk.yellow(` No components matched "${opts.search}".`));
632
+ console.log("");
633
+ return;
634
+ }
635
+ }
636
+ // Group by category
637
+ const grouped = {};
638
+ for (const entry of entries) {
639
+ const cat = entry[1].category;
640
+ if (!grouped[cat])
641
+ grouped[cat] = [];
642
+ grouped[cat].push(entry);
643
+ }
644
+ const categoryOrder = ["buttons", "cards", "text", "navigation", "visual", "media"];
645
+ let total = 0;
646
+ for (const cat of categoryOrder) {
647
+ const items = grouped[cat];
648
+ if (!items || items.length === 0)
649
+ continue;
650
+ const color = CATEGORY_COLORS[cat] || "#FFFFFF";
651
+ console.log(chalk.hex(color).bold(` ${cat.toUpperCase()}`));
652
+ for (const [slug, meta] of items) {
653
+ const newBadge = meta.isNew ? chalk.green(" NEW") : "";
654
+ const desc = chalk.dim(` — ${truncate(meta.description, 60)}`);
655
+ console.log(` ${chalk.hex(color)("●")} ${slug}${newBadge}${desc}`);
656
+ total++;
657
+ }
658
+ console.log("");
659
+ }
660
+ console.log(chalk.dim(` ${total} components shown`));
661
+ console.log("");
662
+ });
663
+ // ── info <component> ─────────────────────────────────────
664
+ program
665
+ .command("info")
666
+ .description("Show detailed information about a component")
667
+ .argument("<component>", "Component slug")
668
+ .action((component) => {
669
+ const meta = COMPONENT_REGISTRY[component];
670
+ if (!meta) {
671
+ console.log("");
672
+ printNotFound(component);
673
+ console.log(chalk.dim(` Run ${chalk.bold("praxys-ui list")} to see available components.`));
674
+ console.log("");
675
+ return;
676
+ }
677
+ const catColor = CATEGORY_COLORS[meta.category] || "#FFFFFF";
267
678
  console.log("");
268
- console.log(chalk.dim(` ${COMPONENT_LIST.length} components available`));
679
+ console.log(chalk.bold(` ${meta.title}`) + (meta.isNew ? chalk.green(" NEW") : ""));
680
+ console.log(chalk.hex(catColor)(` Category: ${meta.category}`));
681
+ console.log(` Dependencies: ${chalk.cyan(meta.dependencies.join(", "))}`);
682
+ console.log(` ${chalk.dim(meta.description)}`);
683
+ console.log(chalk.dim(` Docs: https://praxysui.vercel.app/components/${component}`));
684
+ console.log("");
685
+ });
686
+ // ── view <component> ─────────────────────────────────────
687
+ program
688
+ .command("view")
689
+ .description("View the source code of a component")
690
+ .argument("<component>", "Component slug")
691
+ .action(async (component) => {
692
+ if (!COMPONENT_REGISTRY[component]) {
693
+ console.log("");
694
+ printNotFound(component);
695
+ console.log(chalk.dim(` Run ${chalk.bold("praxys-ui list")} to see available components.`));
696
+ console.log("");
697
+ return;
698
+ }
699
+ const spinner = ora(`Fetching ${component} source...`).start();
700
+ try {
701
+ const source = await fetchComponent(component);
702
+ spinner.stop();
703
+ console.log("");
704
+ console.log(chalk.bold(` ${COMPONENT_REGISTRY[component].title} — source`));
705
+ console.log(chalk.dim(" ─".repeat(30)));
706
+ console.log("");
707
+ console.log(colorizeSource(source));
708
+ console.log("");
709
+ }
710
+ catch (err) {
711
+ spinner.fail(`Failed to fetch ${component}`);
712
+ console.log(chalk.dim(` ${err instanceof Error ? err.message : "Unknown error"}`));
713
+ }
714
+ });
715
+ // ── diff <component> ─────────────────────────────────────
716
+ program
717
+ .command("diff")
718
+ .description("Compare local component with latest remote version")
719
+ .argument("<component>", "Component slug")
720
+ .option("-d, --dir <directory>", "Component directory")
721
+ .action(async (component, opts) => {
722
+ const dir = getComponentsDir(opts.dir);
723
+ if (!COMPONENT_REGISTRY[component]) {
724
+ console.log("");
725
+ printNotFound(component);
726
+ console.log("");
727
+ return;
728
+ }
729
+ const localPath = join(process.cwd(), dir, `${component}.tsx`);
730
+ if (!existsSync(localPath)) {
731
+ console.log("");
732
+ console.log(chalk.yellow(` Not installed. ${component}.tsx not found in ${dir}/`));
733
+ console.log("");
734
+ return;
735
+ }
736
+ const spinner = ora(`Fetching latest ${component}...`).start();
737
+ try {
738
+ const remote = await fetchComponent(component);
739
+ const local = readFileSync(localPath, "utf-8");
740
+ spinner.stop();
741
+ if (local === remote) {
742
+ console.log("");
743
+ console.log(chalk.green(` ✓ ${component} is up to date.`));
744
+ console.log("");
745
+ return;
746
+ }
747
+ console.log("");
748
+ console.log(chalk.bold(` ${component} — diff (local vs remote)`));
749
+ console.log(chalk.dim(" ─".repeat(30)));
750
+ console.log("");
751
+ const localLines = local.split("\n");
752
+ const remoteLines = remote.split("\n");
753
+ const maxLen = Math.max(localLines.length, remoteLines.length);
754
+ for (let i = 0; i < maxLen; i++) {
755
+ const localLine = localLines[i];
756
+ const remoteLine = remoteLines[i];
757
+ if (localLine === remoteLine)
758
+ continue;
759
+ if (localLine !== undefined && remoteLine === undefined) {
760
+ console.log(chalk.red(` - L${i + 1}: ${localLine}`));
761
+ }
762
+ else if (localLine === undefined && remoteLine !== undefined) {
763
+ console.log(chalk.green(` + L${i + 1}: ${remoteLine}`));
764
+ }
765
+ else if (localLine !== remoteLine) {
766
+ console.log(chalk.red(` - L${i + 1}: ${localLine}`));
767
+ console.log(chalk.green(` + L${i + 1}: ${remoteLine}`));
768
+ }
769
+ }
770
+ console.log("");
771
+ }
772
+ catch (err) {
773
+ spinner.fail(`Failed to fetch ${component}`);
774
+ console.log(chalk.dim(` ${err instanceof Error ? err.message : "Unknown error"}`));
775
+ }
776
+ });
777
+ // ── remove <component> ───────────────────────────────────
778
+ program
779
+ .command("remove")
780
+ .alias("rm")
781
+ .description("Remove a component from your project")
782
+ .argument("<component>", "Component slug")
783
+ .option("-d, --dir <directory>", "Component directory")
784
+ .option("-y, --yes", "Skip confirmation prompt", false)
785
+ .action(async (component, opts) => {
786
+ const dir = getComponentsDir(opts.dir);
787
+ if (!COMPONENT_REGISTRY[component]) {
788
+ console.log("");
789
+ printNotFound(component);
790
+ console.log("");
791
+ return;
792
+ }
793
+ const filePath = join(process.cwd(), dir, `${component}.tsx`);
794
+ if (!existsSync(filePath)) {
795
+ console.log("");
796
+ console.log(chalk.yellow(` ${component}.tsx not found in ${dir}/`));
797
+ console.log("");
798
+ return;
799
+ }
800
+ if (!opts.yes) {
801
+ const { confirm } = await prompts({
802
+ type: "confirm",
803
+ name: "confirm",
804
+ message: `Remove ${dir}/${component}.tsx?`,
805
+ initial: false,
806
+ });
807
+ if (!confirm) {
808
+ console.log(chalk.yellow(" Cancelled."));
809
+ return;
810
+ }
811
+ }
812
+ try {
813
+ unlinkSync(filePath);
814
+ console.log("");
815
+ console.log(chalk.green(` ✓ Removed ${dir}/${component}.tsx`));
816
+ console.log("");
817
+ }
818
+ catch (err) {
819
+ console.log("");
820
+ console.log(chalk.red(` Failed to remove ${component}.tsx`));
821
+ console.log(chalk.dim(` ${err instanceof Error ? err.message : "Unknown error"}`));
822
+ console.log("");
823
+ }
824
+ });
825
+ // ── update [component] ───────────────────────────────────
826
+ program
827
+ .command("update")
828
+ .description("Update installed components to the latest version")
829
+ .argument("[component]", "Component slug (omit to check all)")
830
+ .option("-d, --dir <directory>", "Component directory")
831
+ .option("--check", "Only check for updates, don't write files", false)
832
+ .action(async (component, opts) => {
833
+ const dir = getComponentsDir(opts.dir);
834
+ const compDir = join(process.cwd(), dir);
835
+ console.log("");
836
+ console.log(chalk.bold(` ${chalk.hex("#E84E2D")("Praxys UI")} — update`));
837
+ console.log("");
838
+ // Determine which components to check
839
+ let slugsToCheck;
840
+ if (component) {
841
+ if (!COMPONENT_REGISTRY[component]) {
842
+ printNotFound(component);
843
+ console.log("");
844
+ return;
845
+ }
846
+ slugsToCheck = [component];
847
+ }
848
+ else {
849
+ // Find all installed components
850
+ slugsToCheck = COMPONENT_LIST.filter((slug) => existsSync(join(compDir, `${slug}.tsx`)));
851
+ if (slugsToCheck.length === 0) {
852
+ console.log(chalk.yellow(` No installed components found in ${dir}/`));
853
+ console.log("");
854
+ return;
855
+ }
856
+ console.log(chalk.dim(` Checking ${slugsToCheck.length} installed components...\n`));
857
+ }
858
+ let updatedCount = 0;
859
+ let upToDateCount = 0;
860
+ let failedCount = 0;
861
+ for (const slug of slugsToCheck) {
862
+ const localPath = join(compDir, `${slug}.tsx`);
863
+ if (!existsSync(localPath)) {
864
+ if (component) {
865
+ console.log(chalk.yellow(` Not installed. ${slug}.tsx not found in ${dir}/`));
866
+ }
867
+ continue;
868
+ }
869
+ const spinner = ora(`Checking ${slug}...`).start();
870
+ try {
871
+ const remote = await fetchComponent(slug);
872
+ const local = readFileSync(localPath, "utf-8");
873
+ if (local === remote) {
874
+ spinner.succeed(`${slug} is up to date`);
875
+ upToDateCount++;
876
+ continue;
877
+ }
878
+ if (opts.check) {
879
+ spinner.warn(`${slug} has updates available`);
880
+ updatedCount++;
881
+ continue;
882
+ }
883
+ // Show diff summary
884
+ const localLines = local.split("\n");
885
+ const remoteLines = remote.split("\n");
886
+ let additions = 0;
887
+ let removals = 0;
888
+ const maxLen = Math.max(localLines.length, remoteLines.length);
889
+ for (let i = 0; i < maxLen; i++) {
890
+ if (localLines[i] !== remoteLines[i]) {
891
+ if (localLines[i] !== undefined)
892
+ removals++;
893
+ if (remoteLines[i] !== undefined)
894
+ additions++;
895
+ }
896
+ }
897
+ spinner.stop();
898
+ console.log(` ${slug}: ${chalk.green(`+${additions}`)} ${chalk.red(`-${removals}`)} lines changed`);
899
+ const { overwrite } = await prompts({
900
+ type: "confirm",
901
+ name: "overwrite",
902
+ message: ` Update ${slug}?`,
903
+ initial: true,
904
+ });
905
+ if (overwrite) {
906
+ writeFileSync(localPath, remote, "utf-8");
907
+ console.log(chalk.green(` ✓ Updated ${slug}`));
908
+ updatedCount++;
909
+ }
910
+ else {
911
+ console.log(chalk.yellow(` Skipped ${slug}`));
912
+ }
913
+ }
914
+ catch {
915
+ spinner.fail(`Failed to check ${slug}`);
916
+ failedCount++;
917
+ }
918
+ }
919
+ console.log("");
920
+ if (opts.check) {
921
+ console.log(chalk.dim(` ${upToDateCount} up to date, ${updatedCount} with updates available`) +
922
+ (failedCount > 0 ? chalk.red(`, ${failedCount} failed`) : ""));
923
+ }
924
+ else {
925
+ console.log(chalk.dim(` ${updatedCount} updated, ${upToDateCount} up to date`) +
926
+ (failedCount > 0 ? chalk.red(`, ${failedCount} failed`) : ""));
927
+ }
928
+ console.log("");
929
+ });
930
+ // ── doctor ───────────────────────────────────────────────
931
+ program
932
+ .command("doctor")
933
+ .description("Check your Praxys UI project setup for issues")
934
+ .action(() => {
935
+ console.log("");
936
+ console.log(chalk.bold(` ${chalk.hex("#E84E2D")("Praxys UI")} — doctor`));
937
+ console.log("");
938
+ let issues = 0;
939
+ // 1. Config file
940
+ const config = loadConfig();
941
+ if (config) {
942
+ console.log(chalk.green(" ✓ praxys.config.json found"));
943
+ }
944
+ else {
945
+ console.log(chalk.yellow(" ✗ praxys.config.json not found"));
946
+ console.log(chalk.dim(" Run `praxys-ui init` to create it."));
947
+ issues++;
948
+ }
949
+ // 2. Components directory
950
+ const compDir = config?.componentsDir ?? "components/ui";
951
+ const compPath = join(process.cwd(), compDir);
952
+ if (existsSync(compPath)) {
953
+ console.log(chalk.green(` ✓ Components directory exists (${compDir})`));
954
+ }
955
+ else {
956
+ console.log(chalk.yellow(` ✗ Components directory missing (${compDir})`));
957
+ issues++;
958
+ }
959
+ // 3. Utils file
960
+ const utilsDir = config?.utilsDir ?? "lib";
961
+ const utilsPath = join(process.cwd(), utilsDir, "utils.ts");
962
+ if (existsSync(utilsPath)) {
963
+ console.log(chalk.green(` ✓ Utils file exists (${utilsDir}/utils.ts)`));
964
+ }
965
+ else {
966
+ console.log(chalk.yellow(` ✗ Utils file missing (${utilsDir}/utils.ts)`));
967
+ issues++;
968
+ }
969
+ // 4. Package manager
970
+ const pm = detectPackageManager();
971
+ console.log(chalk.green(` ✓ Package manager: ${pm}`));
972
+ // 5. Core dependencies
973
+ const pkgJsonPath = join(process.cwd(), "package.json");
974
+ if (existsSync(pkgJsonPath)) {
975
+ try {
976
+ const pkgJson = JSON.parse(readFileSync(pkgJsonPath, "utf-8"));
977
+ const allDeps = { ...pkgJson.dependencies, ...pkgJson.devDependencies };
978
+ const coreDeps = ["clsx", "tailwind-merge", "framer-motion"];
979
+ for (const dep of coreDeps) {
980
+ if (allDeps[dep]) {
981
+ console.log(chalk.green(` ✓ ${dep} installed (${allDeps[dep]})`));
982
+ }
983
+ else {
984
+ console.log(chalk.yellow(` ✗ ${dep} not found in package.json`));
985
+ issues++;
986
+ }
987
+ }
988
+ }
989
+ catch {
990
+ console.log(chalk.yellow(" ✗ Could not parse package.json"));
991
+ issues++;
992
+ }
993
+ }
994
+ else {
995
+ console.log(chalk.yellow(" ✗ No package.json found"));
996
+ issues++;
997
+ }
998
+ // 6. Installed components count
999
+ if (existsSync(compPath)) {
1000
+ const installed = COMPONENT_LIST.filter((slug) => existsSync(join(compPath, `${slug}.tsx`)));
1001
+ console.log(chalk.green(` ✓ ${installed.length}/${COMPONENT_LIST.length} components installed`));
1002
+ }
1003
+ console.log("");
1004
+ if (issues === 0) {
1005
+ console.log(chalk.green.bold(" All checks passed!"));
1006
+ }
1007
+ else {
1008
+ console.log(chalk.yellow(` ${issues} issue${issues > 1 ? "s" : ""} found.`));
1009
+ }
1010
+ console.log("");
1011
+ });
1012
+ // ── stats ────────────────────────────────────────────────
1013
+ program
1014
+ .command("stats")
1015
+ .description("Show statistics about installed and available components")
1016
+ .option("-d, --dir <directory>", "Component directory")
1017
+ .action((opts) => {
1018
+ const dir = getComponentsDir(opts.dir);
1019
+ const compPath = join(process.cwd(), dir);
1020
+ console.log("");
1021
+ console.log(chalk.bold(` ${chalk.hex("#E84E2D")("Praxys UI")} — stats`));
1022
+ console.log("");
1023
+ const categoryOrder = ["buttons", "cards", "text", "navigation", "visual", "media"];
1024
+ // Count per category
1025
+ const catStats = {};
1026
+ for (const cat of categoryOrder) {
1027
+ catStats[cat] = { total: 0, installed: 0 };
1028
+ }
1029
+ let totalInstalled = 0;
1030
+ for (const [slug, meta] of Object.entries(COMPONENT_REGISTRY)) {
1031
+ catStats[meta.category].total++;
1032
+ if (existsSync(join(compPath, `${slug}.tsx`))) {
1033
+ catStats[meta.category].installed++;
1034
+ totalInstalled++;
1035
+ }
1036
+ }
1037
+ // Header
1038
+ console.log(chalk.dim(" Category Installed Available"));
1039
+ console.log(chalk.dim(" " + "─".repeat(40)));
1040
+ for (const cat of categoryOrder) {
1041
+ const { total, installed } = catStats[cat];
1042
+ const color = CATEGORY_COLORS[cat] || "#FFFFFF";
1043
+ const bar = installed > 0
1044
+ ? chalk.hex(color)("█".repeat(installed)) + chalk.dim("░".repeat(total - installed))
1045
+ : chalk.dim("░".repeat(total));
1046
+ const catLabel = cat.padEnd(16);
1047
+ console.log(` ${chalk.hex(color)(catLabel)} ${String(installed).padStart(3)}/${String(total).padEnd(5)} ${bar}`);
1048
+ }
1049
+ console.log(chalk.dim(" " + "─".repeat(40)));
1050
+ console.log(chalk.bold(` ${"Total".padEnd(16)} ${String(totalInstalled).padStart(3)}/${String(COMPONENT_LIST.length).padEnd(5)}`));
1051
+ // Coverage percentage
1052
+ const pct = COMPONENT_LIST.length > 0
1053
+ ? Math.round((totalInstalled / COMPONENT_LIST.length) * 100)
1054
+ : 0;
1055
+ console.log("");
1056
+ console.log(chalk.dim(` Coverage: ${pct}% of components installed`));
1057
+ console.log(chalk.dim(` Directory: ${dir}`));
269
1058
  console.log("");
270
1059
  });
271
- // ── helpers ──────────────────────────────────────────────
272
- function toPascalCase(slug) {
273
- return slug
274
- .split("-")
275
- .map((s) => s.charAt(0).toUpperCase() + s.slice(1))
276
- .join("");
277
- }
278
1060
  // ── run ──────────────────────────────────────────────────
279
- program.parse();
1061
+ program.parseAsync().then(() => checkForUpdates());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "praxys-ui",
3
- "version": "1.2.9",
3
+ "version": "1.3.3",
4
4
  "type": "module",
5
5
  "description": "CLI for scaffolding Praxys UI components into your project",
6
6
  "bin": {