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.
- package/dist/index.js +901 -119
- 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.
|
|
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
|
|
18
|
+
// ─── Component registry ─────────────────────────────────
|
|
19
19
|
const COMPONENTS_BASE_URL = "https://raw.githubusercontent.com/sushanttverma/Praxys-UI/main/app/components/ui";
|
|
20
|
-
const
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
248
|
-
spinner.succeed(`Added ${
|
|
249
|
-
|
|
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
|
-
.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
1061
|
+
program.parseAsync().then(() => checkForUpdates());
|