get-ui-cli 0.0.0-stage → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,148 @@
1
+ // Source transforms: import rewriting for shadcn-style registry code and
2
+ // Import rewriting and icon-placeholder transforms. Pure — no Node APIs.
3
+ export const DEFAULT_ALIASES = {
4
+ components: '@/components',
5
+ ui: '@/components/ui',
6
+ utils: '@/lib/utils',
7
+ lib: '@/lib',
8
+ hooks: '@/hooks',
9
+ };
10
+ const SPECIFIER_RE = /(\bfrom\s*|\bimport\s*\(\s*|\bimport\s+|\brequire\s*\(\s*|\bjest\.mock\s*\(\s*)(['"])([^'"\n]+)\2/g;
11
+ function stripExt(spec) {
12
+ return spec.replace(/\.(tsx?|jsx?|mjs|cjs)$/, '');
13
+ }
14
+ /**
15
+ * Map one module specifier from registry-internal form to the user's aliases.
16
+ * `fileMap` maps specifiers of files that are being installed (derived from their
17
+ * registry `path`/`target`) to where they will actually live.
18
+ */
19
+ export function mapSpecifier(spec, aliases, fileMap = {}) {
20
+ const bare = stripExt(spec);
21
+ if (fileMap[spec])
22
+ return fileMap[spec];
23
+ if (fileMap[bare])
24
+ return fileMap[bare];
25
+ if (spec === 'cn' || spec === '@/lib/utils' || spec === '@/utils/cn')
26
+ return aliases.utils;
27
+ let m;
28
+ if ((m = bare.match(/^@\/registry\/(?:.+\/)?lib\/utils$/)))
29
+ return aliases.utils;
30
+ if ((m = bare.match(/^@\/registry\/(?:[^/]+\/)*?ui\/(.+)$/)))
31
+ return `${aliases.ui}/${m[1]}`;
32
+ if ((m = bare.match(/^@\/registry\/(?:[^/]+\/)*?hooks\/(.+)$/)))
33
+ return `${aliases.hooks}/${m[1]}`;
34
+ if ((m = bare.match(/^@\/registry\/(?:[^/]+\/)*?lib\/(.+)$/)))
35
+ return `${aliases.lib}/${m[1]}`;
36
+ if ((m = bare.match(/^@\/registry\/(?:[^/]+\/)*?(?:components|blocks|examples?)\/(.+)$/)))
37
+ return `${aliases.components}/${m[1]}`;
38
+ // e.g. `@/registry/magicui/marquee` — library-flat registries ship registry:ui files.
39
+ if ((m = bare.match(/^@\/registry\/[^/]+\/(.+)$/)))
40
+ return `${aliases.ui}/${m[1]}`;
41
+ if ((m = spec.match(/^@\/components\/ui\/(.+)$/)))
42
+ return `${aliases.ui}/${m[1]}`;
43
+ if ((m = spec.match(/^@\/components\/(.+)$/)))
44
+ return `${aliases.components}/${m[1]}`;
45
+ if ((m = spec.match(/^@\/hooks\/(.+)$/)))
46
+ return `${aliases.hooks}/${m[1]}`;
47
+ if ((m = spec.match(/^@\/lib\/(.+)$/)))
48
+ return `${aliases.lib}/${m[1]}`;
49
+ return spec;
50
+ }
51
+ /** Rewrite all import/export/require specifiers in `code`. */
52
+ export function rewriteImports(code, aliases, fileMap = {}) {
53
+ return code.replace(SPECIFIER_RE, (_all, lead, quote, spec) => {
54
+ return `${lead}${quote}${mapSpecifier(spec, aliases, fileMap)}${quote}`;
55
+ });
56
+ }
57
+ /** List module specifiers imported by `code`. */
58
+ export function listImports(code) {
59
+ const out = new Set();
60
+ for (const m of code.matchAll(SPECIFIER_RE))
61
+ out.add(m[3]);
62
+ return [...out];
63
+ }
64
+ // ---------------------------------------------------------------------------
65
+ // shadcn "IconPlaceholder" (base-* styles / ReUI) -> concrete lucide-react icons
66
+ // ---------------------------------------------------------------------------
67
+ const ICON_LIB_ATTRS = ['lucide', 'tabler', 'hugeicons', 'phosphor', 'remixicon'];
68
+ /** Find the end index (exclusive) of a JSX opening tag starting at `start` (`<`). */
69
+ function jsxTagEnd(code, start) {
70
+ let depth = 0;
71
+ let quote = null;
72
+ for (let i = start + 1; i < code.length; i++) {
73
+ const ch = code[i];
74
+ if (quote) {
75
+ if (ch === '\\')
76
+ i++;
77
+ else if (ch === quote)
78
+ quote = null;
79
+ continue;
80
+ }
81
+ if (ch === '"' || ch === "'" || ch === '`')
82
+ quote = ch;
83
+ else if (ch === '{')
84
+ depth++;
85
+ else if (ch === '}')
86
+ depth--;
87
+ else if (depth === 0 && ch === '/' && code[i + 1] === '>')
88
+ return { end: i + 2, selfClosing: true };
89
+ else if (depth === 0 && ch === '>')
90
+ return { end: i + 1, selfClosing: false };
91
+ }
92
+ return null;
93
+ }
94
+ /**
95
+ * Replace `<IconPlaceholder lucide="CheckIcon" tabler="…" … />` with `<CheckIcon … />`
96
+ * and swap the placeholder import for a lucide-react import.
97
+ */
98
+ export function transformIconPlaceholders(code) {
99
+ if (!code.includes('IconPlaceholder'))
100
+ return code;
101
+ const icons = new Set();
102
+ let out = '';
103
+ let i = 0;
104
+ const re = /<IconPlaceholder\b/g;
105
+ let m;
106
+ while ((m = re.exec(code))) {
107
+ const tag = jsxTagEnd(code, m.index);
108
+ if (!tag)
109
+ break;
110
+ let attrs = code.slice(m.index + '<IconPlaceholder'.length, tag.selfClosing ? tag.end - 2 : tag.end - 1);
111
+ const lucide = attrs.match(/\blucide=(?:"([^"]+)"|\{\s*["']([^"']+)["']\s*\})/);
112
+ const name = lucide?.[1] ?? lucide?.[2] ?? 'CircleIcon';
113
+ icons.add(name);
114
+ for (const a of ICON_LIB_ATTRS)
115
+ attrs = attrs.replace(new RegExp(`\\s*\\b${a}=(?:"[^"]*"|\\{[^}]*\\})`, 'g'), '');
116
+ out += code.slice(i, m.index) + `<${name}${attrs}${tag.selfClosing ? '/>' : '>'}`;
117
+ i = tag.end;
118
+ re.lastIndex = tag.end;
119
+ }
120
+ out += code.slice(i);
121
+ out = out.replace(/<\/IconPlaceholder>/g, () => `</${[...icons][0] ?? 'CircleIcon'}>`);
122
+ // Names already imported from lucide-react.
123
+ const existing = new Set();
124
+ for (const im of out.matchAll(/import\s*\{([^}]*)\}\s*from\s*["']lucide-react["']/g))
125
+ for (const n of im[1].split(','))
126
+ existing.add(n.trim().split(/\s+as\s+/).pop().trim());
127
+ const needed = [...icons].filter((n) => !existing.has(n)).sort();
128
+ const lucideImport = needed.length ? `import { ${needed.join(', ')} } from "lucide-react"` : '';
129
+ const importRe = /import\s*\{\s*IconPlaceholder\s*\}\s*from\s*["'][^"']+["'];?/;
130
+ if (importRe.test(out))
131
+ out = out.replace(importRe, lucideImport);
132
+ else if (lucideImport)
133
+ out = `${lucideImport}\n${out}`;
134
+ return out;
135
+ }
136
+ /**
137
+ * React 19's types dropped the global `JSX` namespace, so `JSX.Element` in older
138
+ * components fails to type-check. Import the namespace from react where it's used.
139
+ */
140
+ export function importJsxNamespace(code) {
141
+ if (!/(?<![\w.$])JSX\./.test(code))
142
+ return code;
143
+ if (/\bimport\b[^;]*?\bJSX\b[^;]*?\bfrom\s*['"]react['"]/.test(code))
144
+ return code;
145
+ const line = 'import type { JSX } from "react";\n';
146
+ const directive = code.match(/^\s*(['"])use (?:client|server)\1;?[^\n]*\n/);
147
+ return directive ? code.slice(0, directive[0].length) + line + code.slice(directive[0].length) : line + code;
148
+ }
@@ -0,0 +1,3 @@
1
+ export declare const KNOWN_MAJORS: Record<string, string>;
2
+ /** `motion` → `motion@12`; a dependency that already names a version is left alone. */
3
+ export declare function withKnownMajor(dep: string): string;
@@ -0,0 +1,44 @@
1
+ // Major versions the indexed components were written for. When a project doesn't
2
+ // have one of these packages yet, get-ui installs this major instead of whatever
3
+ // is newest (motion 14 and lucide-react 1.x, for example, break components written
4
+ // for motion 12 and lucide 0.x). Keep in step with the exact pins the website's
5
+ // live previews use (web/src/lib/preview/build.ts, PINS), so what you preview is
6
+ // what you install. Pure.
7
+ import { packageName } from './resolve.js';
8
+ export const KNOWN_MAJORS = {
9
+ motion: '12',
10
+ 'framer-motion': '12',
11
+ 'lucide-react': '0',
12
+ 'date-fns': '4',
13
+ '@base-ui/react': '1',
14
+ '@tanstack/react-table': '9',
15
+ '@tanstack/react-virtual': '3',
16
+ sonner: '2',
17
+ 'radix-ui': '1',
18
+ '@radix-ui/react-slot': '1',
19
+ '@radix-ui/react-use-controllable-state': '1',
20
+ 'class-variance-authority': '0',
21
+ clsx: '2',
22
+ 'tailwind-merge': '3',
23
+ recharts: '3',
24
+ '@tabler/icons-react': '3',
25
+ 'react-day-picker': '9',
26
+ 'react-use-measure': '2',
27
+ 'next-themes': '0',
28
+ '@dnd-kit/core': '6',
29
+ '@dnd-kit/sortable': '10',
30
+ '@dnd-kit/utilities': '3',
31
+ 'input-otp': '1',
32
+ 'embla-carousel-react': '8',
33
+ 'embla-carousel-autoplay': '8',
34
+ vaul: '1',
35
+ cobe: '0',
36
+ gsap: '3',
37
+ };
38
+ /** `motion` → `motion@12`; a dependency that already names a version is left alone. */
39
+ export function withKnownMajor(dep) {
40
+ if (packageName(dep) !== dep)
41
+ return dep;
42
+ const major = KNOWN_MAJORS[dep];
43
+ return major ? `${dep}@${major}` : dep;
44
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,390 @@
1
+ #!/usr/bin/env node
2
+ import { autocomplete, cancel, confirm, intro, isCancel, log, note, outro, select, spinner } from '@clack/prompts';
3
+ import { Command } from 'commander';
4
+ import fs from 'node:fs';
5
+ import path from 'node:path';
6
+ import pc from 'picocolors';
7
+ import { findExact, searchItems, searchItemsScored } from './core/search.js';
8
+ import { resolveItem } from './core/resolve.js';
9
+ import { LIBRARIES } from './registry-types.js';
10
+ import { detectProject } from './node/project.js';
11
+ import { loadRegistry } from './node/registry.js';
12
+ import { UTILS_SOURCE, installCommand, planInstall, runCommand, writePlan } from './node/install.js';
13
+ const VERSION = '2.0.0';
14
+ // Where `Preview:` links point; override for self-hosted or preview deployments.
15
+ const PREVIEW_BASE = `${(process.env.GET_UI_SITE ?? 'https://get-ui.dev').replace(/\/+$/, '')}/c/`;
16
+ const interactive = () => !!process.stdin.isTTY && !!process.stdout.isTTY && !process.env.CI;
17
+ /**
18
+ * Auth for paid registries (Aceternity Pro, ReUI Pro…): GET_UI_TOKEN_<LIBRARY> (e.g.
19
+ * GET_UI_TOKEN_REUI, GET_UI_TOKEN_ACETERNITY) or REUI_LICENSE_KEY / ACETERNITY_API_KEY.
20
+ */
21
+ function tokenEnvNames(libraryId) {
22
+ const id = libraryId.toUpperCase().replace(/[^A-Z0-9]+/g, '_');
23
+ return [`GET_UI_TOKEN_${id}`, ...(id === 'REUI' ? ['REUI_LICENSE_KEY'] : []), ...(id === 'ACETERNITY' ? ['ACETERNITY_API_KEY'] : [])];
24
+ }
25
+ function makeAuthHeaders(registry) {
26
+ return (url) => {
27
+ for (const lib of Object.values(registry.libraries)) {
28
+ if (!lib.itemBase || new URL(url).host !== new URL(lib.itemBase).host)
29
+ continue;
30
+ const token = tokenEnvNames(lib.id).map((n) => process.env[n]).find(Boolean);
31
+ if (token)
32
+ return { Authorization: `Bearer ${token}` };
33
+ }
34
+ return undefined;
35
+ };
36
+ }
37
+ function explainFailure(item, msg) {
38
+ if (/\b(401|403)\b|Authentication required|Unauthorized/i.test(msg))
39
+ return `${item.id} is a paid/pro component. Set ${tokenEnvNames(item.library)[0]} to your ${item.library} licence key / API token to install it.`;
40
+ return `${item.id}: ${msg}`;
41
+ }
42
+ function label(item) {
43
+ const lib = item.library;
44
+ return `${item.title} ${pc.dim(`[${lib} · ${item.category}]`)}${item.pro ? ` ${pc.yellow('Pro')}` : ''}`;
45
+ }
46
+ function bail(msg) {
47
+ cancel(msg);
48
+ process.exit(1);
49
+ }
50
+ function truncate(s, n) {
51
+ return s.length > n ? s.slice(0, n - 1) + '…' : s;
52
+ }
53
+ function table(rows, header) {
54
+ const all = [header, ...rows];
55
+ const widths = header.map((_, i) => Math.max(...all.map((r) => r[i].length)));
56
+ const fmt = (r) => r.map((c, i) => c.padEnd(widths[i])).join(' ').trimEnd();
57
+ return [pc.bold(fmt(header)), pc.dim(widths.map((w) => '─'.repeat(w)).join(' ')), ...rows.map(fmt)].join('\n');
58
+ }
59
+ async function getRegistry(quiet = false) {
60
+ const s = quiet ? null : spinner();
61
+ s?.start('Loading component index…');
62
+ try {
63
+ const { registry, source } = await loadRegistry();
64
+ const pro = registry.items.filter((i) => i.pro).length;
65
+ const free = registry.items.length - pro;
66
+ s?.stop(`Loaded ${pc.bold(free.toLocaleString('en-US'))} components${pro ? ` (+${pro.toLocaleString('en-US')} Pro)` : ''} ${pc.dim(`(${source})`)}`);
67
+ return registry;
68
+ }
69
+ catch (e) {
70
+ s?.stop(pc.red('Failed to load the component index.'));
71
+ throw e;
72
+ }
73
+ }
74
+ /** Turn a user query into a registry item (exact match, or interactive pick). */
75
+ async function pickItem(registry, query, opts) {
76
+ const items = registry.items;
77
+ if (query) {
78
+ const exact = findExact(items, query, opts.library);
79
+ if (exact)
80
+ return exact;
81
+ }
82
+ const results = query ? searchItemsScored(items, query, { library: opts.library, limit: 15 }) : [];
83
+ // Paid items need a licence key; rank free ones first (stable sort keeps relevance order).
84
+ results.sort((a, b) => Number(!!a.item.pro) - Number(!!b.item.pro));
85
+ if (query && !results.length)
86
+ bail(`No components match "${query}". Try ${pc.cyan('get-ui search <text>')}.`);
87
+ if (!interactive() || opts.yes) {
88
+ if (!query)
89
+ bail('No component given. Pass an id, e.g. `get-ui add magicui/marquee`.');
90
+ const top = results[0].item;
91
+ log.info(`"${query}" → ${pc.cyan(top.id)} ${pc.dim(`(best match of ${results.length})`)}`);
92
+ return top;
93
+ }
94
+ const toOption = (i) => ({ value: i.id, label: label(i), hint: i.id });
95
+ const choice = await autocomplete({
96
+ message: query ? `Pick a component matching "${query}"` : 'Search for a component',
97
+ initialUserInput: query ?? '',
98
+ maxItems: 15,
99
+ options: function () {
100
+ const q = (this.userInput ?? '').trim();
101
+ const list = q ? searchItems(items, q, { library: opts.library, limit: 15 }) : items.filter((i) => !opts.library || i.library === opts.library).slice(0, 15);
102
+ return list.map(toOption);
103
+ },
104
+ });
105
+ if (isCancel(choice))
106
+ bail('Cancelled.');
107
+ const item = items.find((i) => i.id === choice);
108
+ if (!item)
109
+ bail('Nothing selected.');
110
+ return item;
111
+ }
112
+ function printPlan(plan, results, pm) {
113
+ const lines = [];
114
+ for (const { item, result } of results) {
115
+ lines.push(`${pc.bold(pc.cyan(item.id))}${result.tree.length > 1 ? pc.dim(` + ${result.tree.slice(1).join(', ')}`) : ''}`);
116
+ }
117
+ lines.push('');
118
+ lines.push(pc.bold('Files'));
119
+ for (const f of plan.files) {
120
+ const tag = f.kind === 'css' ? pc.magenta('merge ') : f.status === 'new' ? pc.green('create') : f.status === 'identical' ? pc.dim('same ') : pc.yellow('exists');
121
+ lines.push(` ${tag} ${f.rel}${f.note ? pc.dim(` (${truncate(f.note, 90)})`) : ''}`);
122
+ }
123
+ if (!plan.files.length)
124
+ lines.push(pc.dim(' (none)'));
125
+ if (plan.dependencies.length || plan.devDependencies.length) {
126
+ lines.push('');
127
+ lines.push(pc.bold(`Packages (${pm})`));
128
+ if (plan.dependencies.length)
129
+ lines.push(` ${plan.dependencies.join(' ')}`);
130
+ if (plan.devDependencies.length)
131
+ lines.push(` ${pc.dim('dev:')} ${plan.devDependencies.join(' ')}`);
132
+ }
133
+ note(lines.join('\n'), 'Install plan');
134
+ for (const w of plan.warnings)
135
+ log.warn(w);
136
+ }
137
+ async function addCommand(queries, opts) {
138
+ console.log();
139
+ intro(pc.bgCyan(pc.black(' get-ui ')));
140
+ const cwd = path.resolve(opts.cwd);
141
+ if (!fs.existsSync(cwd))
142
+ bail(`Directory not found: ${cwd}`);
143
+ if (!fs.existsSync(path.join(cwd, 'package.json')))
144
+ log.warn(`No package.json in ${cwd} — files will still be written, but packages can't be installed.`);
145
+ const registry = await getRegistry();
146
+ const picked = [];
147
+ for (const q of queries.length ? queries : [undefined]) {
148
+ const item = await pickItem(registry, q, opts);
149
+ if (!picked.some((p) => p.id === item.id))
150
+ picked.push(item);
151
+ }
152
+ const s = spinner();
153
+ s.start(`Resolving ${picked.map((p) => p.id).join(', ')}…`);
154
+ const cache = new Map();
155
+ const settled = await Promise.allSettled(picked.map((item) => resolveItem(registry, item, { cache, headers: makeAuthHeaders(registry) })));
156
+ const results = [];
157
+ const failures = [];
158
+ settled.forEach((r, i) => {
159
+ if (r.status === 'rejected')
160
+ return failures.push(explainFailure(picked[i], r.reason?.message ?? String(r.reason)));
161
+ // A dependency that didn't download would leave a half-installed component; install none of it.
162
+ const missing = r.value.warnings.filter((w) => /^Could not fetch|requires authentication|^Unknown registry namespace/.test(w));
163
+ if (missing.length)
164
+ return failures.push(`${picked[i].id}: ${missing.join('; ')}. Nothing was installed for it — try again in a moment.`);
165
+ results.push({ item: picked[i], result: r.value });
166
+ });
167
+ if (!results.length) {
168
+ s.stop(pc.red('Resolution failed.'));
169
+ bail(failures.join('\n'));
170
+ }
171
+ const total = results.reduce((n, r) => n + r.result.files.length, 0);
172
+ s.stop(`Resolved ${total} file${total === 1 ? '' : 's'} from ${results.reduce((n, r) => n + r.result.tree.length, 0)} registry item(s).`);
173
+ for (const f of failures)
174
+ log.error(`Skipped — ${f}`);
175
+ const project = detectProject(cwd);
176
+ const plan = planInstall(results.map((r) => r.result), project);
177
+ log.info(pc.dim(`Project: ${project.hasSrc ? 'src/, ' : ''}alias ${project.aliasPrefix ?? '(relative imports)'}, Tailwind ${project.tailwind.version ?? '?'}${project.tailwind.css ? `, css ${path.relative(cwd, project.tailwind.css)}` : ''}, ${project.packageManager}`));
178
+ printPlan(plan, results, project.packageManager);
179
+ const cmds = [
180
+ plan.dependencies.length ? installCommand(project.packageManager, plan.dependencies) : null,
181
+ plan.devDependencies.length ? installCommand(project.packageManager, plan.devDependencies, true) : null,
182
+ ].filter((c) => !!c);
183
+ if (opts.dryRun) {
184
+ if (cmds.length)
185
+ log.info(`Would run: ${cmds.map((c) => pc.cyan(c.join(' '))).join(' && ')}`);
186
+ outro(pc.yellow('Dry run — nothing was written.'));
187
+ return;
188
+ }
189
+ if (!opts.yes && interactive()) {
190
+ const ok = await confirm({ message: 'Write these files?' });
191
+ if (isCancel(ok) || !ok)
192
+ bail('Cancelled.');
193
+ }
194
+ const written = await writePlan(plan, async (file) => {
195
+ if (opts.overwrite)
196
+ return 'overwrite';
197
+ if (opts.yes || !interactive())
198
+ return 'skip';
199
+ const answer = await select({
200
+ message: `${file.rel} already exists and differs. What should I do?`,
201
+ options: [
202
+ { value: 'skip', label: 'Keep mine (skip)' },
203
+ { value: 'overwrite', label: 'Overwrite' },
204
+ ],
205
+ });
206
+ if (isCancel(answer))
207
+ bail('Cancelled.');
208
+ return answer;
209
+ });
210
+ for (const f of written.written)
211
+ log.success(`${f.kind === 'css' ? 'Updated' : 'Wrote'} ${pc.cyan(f.rel)}`);
212
+ for (const f of written.skipped)
213
+ log.warn(`Skipped ${f.rel} (exists — use --overwrite to replace)`);
214
+ if (written.unchanged.length)
215
+ log.info(pc.dim(`${written.unchanged.length} file(s) already up to date.`));
216
+ let packagesFailed = false;
217
+ if (cmds.length) {
218
+ if (!opts.install || !project.hasPackageJson) {
219
+ log.info(`Install packages with: ${cmds.map((c) => pc.cyan(c.join(' '))).join(' && ')}`);
220
+ }
221
+ else {
222
+ for (const cmd of cmds) {
223
+ const sp = spinner();
224
+ sp.start(`Running ${cmd.join(' ')}`);
225
+ try {
226
+ await runCommand(cmd, cwd);
227
+ sp.stop(`Installed ${cmd.slice(2).filter((a) => !a.startsWith('-')).join(', ')}`);
228
+ }
229
+ catch (e) {
230
+ packagesFailed = true;
231
+ sp.stop(pc.red('Package install failed.'));
232
+ log.error(e.message);
233
+ log.info(`Run it yourself: ${pc.cyan(cmd.join(' '))}`);
234
+ }
235
+ }
236
+ }
237
+ }
238
+ for (const { item } of results)
239
+ log.message(`${pc.dim('Preview:')} ${pc.underline(PREVIEW_BASE + item.id)}`);
240
+ const ready = results.map((r) => r.item.title).join(', ');
241
+ if (failures.length || packagesFailed) {
242
+ process.exitCode = 1;
243
+ outro(pc.yellow(packagesFailed ? `Files written for ${ready}, but packages didn't install (see above).` : `${ready} ready to use; ${failures.length} not installed (see above).`));
244
+ return;
245
+ }
246
+ outro(pc.green(`Done! ${ready} ready to use.`));
247
+ }
248
+ const program = new Command();
249
+ program.name('get-ui').description('Install components from shadcn/ui, Magic UI, Aceternity, ReUI, Kokonut UI, Cult UI, Motion Primitives, Unlumen UI and Amicro.').version(VERSION);
250
+ program
251
+ .command('add [queries...]', { isDefault: true })
252
+ .description('Add one or more components (id like magicui/marquee, a name, or free text)')
253
+ .option('-y, --yes', 'skip prompts; pick the best match; keep existing files')
254
+ .option('-o, --overwrite', 'overwrite existing files')
255
+ .option('--dry-run', 'show what would be written / installed without changing anything')
256
+ .option('--cwd <dir>', 'project directory', process.cwd())
257
+ .option('-l, --library <id>', `restrict matches to a library (${Object.keys(LIBRARIES).join(', ')})`)
258
+ .option('--no-install', 'do not run the package manager; print the command instead')
259
+ .action(async (queries, opts) => {
260
+ await addCommand(queries ?? [], opts);
261
+ });
262
+ program
263
+ .command('search <query...>')
264
+ .description('Search the component index')
265
+ .option('-l, --library <id>', 'filter by library')
266
+ .option('-c, --category <name>', 'filter by category')
267
+ .option('-n, --limit <n>', 'number of results', '20')
268
+ .action(async (query, opts) => {
269
+ const registry = await getRegistry(true);
270
+ const results = searchItems(registry.items, query.join(' '), { library: opts.library, category: opts.category, limit: Number(opts.limit) || 20 });
271
+ if (!results.length) {
272
+ console.log(pc.yellow(`No components match "${query.join(' ')}".`));
273
+ process.exitCode = 1;
274
+ return;
275
+ }
276
+ console.log(table(results.map((i) => [i.id, truncate(i.title, 48), i.library, i.category]), ['ID', 'TITLE', 'LIBRARY', 'CATEGORY']));
277
+ console.log(pc.dim(`\nInstall with: npx get-ui-cli add ${results[0].id}`));
278
+ });
279
+ program
280
+ .command('list')
281
+ .description('Show counts per library / category, or list items')
282
+ .option('-l, --library <id>', 'filter by library')
283
+ .option('-c, --category <name>', 'filter by category (lists items)')
284
+ .action(async (opts) => {
285
+ const registry = await getRegistry(true);
286
+ let items = registry.items;
287
+ if (opts.library)
288
+ items = items.filter((i) => i.library === opts.library);
289
+ if (opts.category)
290
+ items = items.filter((i) => i.category.toLowerCase() === opts.category.toLowerCase());
291
+ if (!items.length) {
292
+ console.log(pc.yellow('No components match those filters.'));
293
+ process.exitCode = 1;
294
+ return;
295
+ }
296
+ if (opts.category) {
297
+ console.log(table(items.map((i) => [i.id, truncate(i.title, 56), i.library]), ['ID', 'TITLE', 'LIBRARY']));
298
+ console.log(pc.dim(`\n${items.length} components`));
299
+ return;
300
+ }
301
+ const key = opts.library ? (i) => i.category : (i) => i.library;
302
+ const counts = new Map();
303
+ for (const i of items)
304
+ counts.set(key(i), (counts.get(key(i)) ?? 0) + 1);
305
+ const rows = [...counts.entries()].sort((a, b) => b[1] - a[1]).map(([k, n]) => [k, String(n), opts.library ? '' : registry.libraries[k]?.name ?? '']);
306
+ console.log(table(rows, [opts.library ? 'CATEGORY' : 'LIBRARY', 'COUNT', opts.library ? '' : 'NAME']));
307
+ console.log(pc.dim(`\n${items.length} components. Use --library / --category to drill down.`));
308
+ });
309
+ program
310
+ .command('view <id>')
311
+ .description('Show metadata, files and dependencies for a component (no writes)')
312
+ .option('--cwd <dir>', 'project directory (used to show target paths)', process.cwd())
313
+ .action(async (id, opts) => {
314
+ const registry = await getRegistry(true);
315
+ const item = findExact(registry.items, id) ?? searchItems(registry.items, id, { limit: 1 })[0];
316
+ if (!item) {
317
+ console.log(pc.red(`No component "${id}".`));
318
+ process.exitCode = 1;
319
+ return;
320
+ }
321
+ console.log(`${pc.bold(item.title)} ${pc.dim(item.id)}`);
322
+ if (item.description)
323
+ console.log(item.description);
324
+ console.log(pc.dim(`${registry.libraries[item.library]?.name ?? item.library} · ${item.category} · ${item.kind} · ${item.format}${item.author ? ` · by ${item.author}` : ''}`));
325
+ console.log(`Docs: ${item.docs}`);
326
+ console.log(`Source: ${item.source}`);
327
+ console.log(`Preview: ${PREVIEW_BASE}${item.id}\n`);
328
+ let result;
329
+ try {
330
+ result = await resolveItem(registry, item, { headers: makeAuthHeaders(registry) });
331
+ }
332
+ catch (e) {
333
+ console.log(pc.red(explainFailure(item, e.message)));
334
+ process.exitCode = 1;
335
+ return;
336
+ }
337
+ const project = detectProject(opts.cwd);
338
+ const plan = planInstall([result], project);
339
+ if (result.tree.length > 1)
340
+ console.log(`${pc.bold('Pulls in:')} ${result.tree.slice(1).join(', ')}`);
341
+ console.log(pc.bold('Files:'));
342
+ for (const f of plan.files)
343
+ console.log(` ${f.kind === 'css' ? pc.magenta('merge') : ' '} ${f.rel}${f.note ? pc.dim(` (${truncate(f.note, 80)})`) : ''}`);
344
+ const deps = result.dependencies;
345
+ console.log(`${pc.bold('Dependencies:')} ${deps.length ? deps.join(' ') : pc.dim('none')}`);
346
+ if (result.devDependencies.length)
347
+ console.log(`${pc.bold('Dev dependencies:')} ${result.devDependencies.join(' ')}`);
348
+ for (const w of result.warnings)
349
+ console.log(pc.yellow(`! ${w}`));
350
+ console.log(pc.dim(`\nInstall with: npx get-ui-cli add ${item.id}`));
351
+ });
352
+ program
353
+ .command('init')
354
+ .description('Create lib/utils.ts (cn helper) and install clsx + tailwind-merge')
355
+ .option('--cwd <dir>', 'project directory', process.cwd())
356
+ .option('--no-install', 'do not run the package manager')
357
+ .action(async (opts) => {
358
+ intro(pc.bgCyan(pc.black(' get-ui init ')));
359
+ const project = detectProject(opts.cwd);
360
+ if (fs.existsSync(project.utilsFile))
361
+ log.info(`${path.relative(project.cwd, project.utilsFile)} already exists.`);
362
+ else {
363
+ fs.mkdirSync(path.dirname(project.utilsFile), { recursive: true });
364
+ fs.writeFileSync(project.utilsFile, UTILS_SOURCE);
365
+ log.success(`Created ${pc.cyan(path.relative(project.cwd, project.utilsFile))}`);
366
+ }
367
+ const missing = ['clsx', 'tailwind-merge'].filter((d) => !project.installed.has(d));
368
+ if (missing.length) {
369
+ const cmd = installCommand(project.packageManager, missing);
370
+ if (!opts.install || !project.hasPackageJson)
371
+ log.info(`Install packages with: ${pc.cyan(cmd.join(' '))}`);
372
+ else {
373
+ const s = spinner();
374
+ s.start(`Running ${cmd.join(' ')}`);
375
+ try {
376
+ await runCommand(cmd, project.cwd);
377
+ s.stop(`Installed ${missing.join(', ')}`);
378
+ }
379
+ catch (e) {
380
+ s.stop(pc.red('Install failed'));
381
+ log.error(e.message);
382
+ }
383
+ }
384
+ }
385
+ outro(pc.green(`Ready. Try ${pc.cyan('npx get-ui-cli add magicui/marquee')}`));
386
+ });
387
+ program.parseAsync().catch((e) => {
388
+ console.error(pc.red(e instanceof Error ? e.message : String(e)));
389
+ process.exit(1);
390
+ });
@@ -0,0 +1,43 @@
1
+ import { type ResolveResult, type ResolvedFile } from '../core/resolve.js';
2
+ import type { PackageManager, ProjectConfig } from './project.js';
3
+ export declare const UTILS_SOURCE = "import { clsx, type ClassValue } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs))\n}\n";
4
+ export type FileStatus = 'new' | 'identical' | 'modified';
5
+ export interface PlannedFile {
6
+ abs: string;
7
+ /** Path relative to the project root, for display. */
8
+ rel: string;
9
+ content: string;
10
+ status: FileStatus;
11
+ /** Registry item the file came from (or `get-ui` for generated files). */
12
+ item: string;
13
+ /** CSS merges are applied without prompting (they never remove anything). */
14
+ kind: 'file' | 'css';
15
+ note?: string;
16
+ }
17
+ export interface InstallPlan {
18
+ files: PlannedFile[];
19
+ dependencies: string[];
20
+ devDependencies: string[];
21
+ warnings: string[];
22
+ }
23
+ export declare function destinationFor(file: ResolvedFile, project: ProjectConfig): string;
24
+ /**
25
+ * An existing file at the destination that this file can't replace or reuse:
26
+ * - the same primitive in the other flavour (Radix vs Base UI `button.tsx`);
27
+ * - a different component with the same name (two libraries' `carousel.tsx`);
28
+ * - a version missing something the incoming one offers (a `size` prop, a badge
29
+ * `success` variant) that the components being installed are likely to use.
30
+ * A user's styling edits keep the API, so their file is reused as before.
31
+ */
32
+ export declare function clashes(existing: string, incoming: string): boolean;
33
+ /** Compute everything that would be written / installed, without touching disk. */
34
+ export declare function planInstall(results: ResolveResult[], project: ProjectConfig): InstallPlan;
35
+ export type ConflictDecision = 'overwrite' | 'skip';
36
+ export interface WriteResult {
37
+ written: PlannedFile[];
38
+ skipped: PlannedFile[];
39
+ unchanged: PlannedFile[];
40
+ }
41
+ export declare function writePlan(plan: InstallPlan, onConflict: (file: PlannedFile) => Promise<ConflictDecision>): Promise<WriteResult>;
42
+ export declare function installCommand(pm: PackageManager, deps: string[], dev?: boolean): string[];
43
+ export declare function runCommand(cmd: string[], cwd: string): Promise<void>;