tweenlabs 0.1.3 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +118 -3
  2. package/bin/cli.js +303 -90
  3. package/package.json +62 -60
package/README.md CHANGED
@@ -7,15 +7,15 @@ keywords: TweenLabs, GSAP component library, GSAP Next.js, GSAP animations React
7
7
 
8
8
  # <img src="https://raw.githubusercontent.com/TweenLabs/TweenLabs/master/public/logo.svg" alt="TweenLabs Logo" width="40" height="40" align="center" /> TweenLabs
9
9
 
10
-
11
10
  > The open-source **GSAP animation component library** for Next.js developers — learn, copy, and contribute modern web animation patterns built with **GSAP 3.15**, **Next.js 16**, and **Lenis**.
12
11
 
13
- **[Live Demo](https://tweenlabs.xyz)** • **[Contributing Guide](#contributing)** • **[Roadmap](#roadmap)**
12
+ **[Live Demo](https://tweenlabs.xyz)** • **[Contributing Guide](#contributing)** • **[Roadmap](#roadmap)**
14
13
 
15
14
  ![Next.js](https://img.shields.io/badge/Next.js-16-black?style=flat-square)
16
15
  ![GSAP](https://img.shields.io/badge/GSAP-3.15-88CE02?style=flat-square)
17
16
  ![React](https://img.shields.io/badge/React-19-61DAFB?style=flat-square)
18
17
  ![License](https://img.shields.io/badge/license-MIT-blue?style=flat-square)
18
+ ![npm](https://img.shields.io/npm/v/tweenlabs?style=flat-square&color=CB3837)
19
19
 
20
20
  <!-- [Contributors](https://img.shields.io/github/contributors/TweenLabs/TweenLabs?style=flat-square) -->
21
21
 
@@ -31,6 +31,114 @@ No paid plugins. No locked content. Just clean, modern animation patterns anyone
31
31
 
32
32
  ---
33
33
 
34
+ ## ⚡ CLI — Install Components Instantly
35
+
36
+ TweenLabs ships with a **zero-dependency CLI** that lets you pull components directly into your codebase — no copy-paste required.
37
+
38
+ ### Initialize configuration
39
+
40
+ Run the `init` command to configure your preferred installation path. This creates a `tweenlabs.config.json` file in the root of your project, meaning you won't be prompted for the path when adding components in the future.
41
+
42
+ ```bash
43
+ npx tweenlabs@latest init
44
+ ```
45
+
46
+ By default, it will detect your project setup and suggest `./src/components/tweenlabs` or `./components/tweenlabs`.
47
+
48
+ ### Add a component
49
+
50
+ ```bash
51
+ npx tweenlabs@latest add <component-slug>
52
+ ```
53
+
54
+ This will:
55
+ 1. Fetch the component files from the TweenLabs registry
56
+ 2. Detect your project layout (supports `src/` and non-`src/` setups)
57
+ 3. Automatically resolve the install path to `src/components/tweenlabs/`
58
+ 4. Create the directory if it doesn't exist
59
+ 5. Detect and install any missing npm dependencies
60
+
61
+ ### Browse & install interactively
62
+
63
+ Run `add` without a slug to get an interactive picker:
64
+
65
+ ```bash
66
+ npx tweenlabs@latest add
67
+ ```
68
+
69
+ ```
70
+ ▲ tweenlabs v0.1.6
71
+
72
+ Select a component to install:
73
+
74
+ [1] . All Components
75
+ [2] gravity-drop Physics-based falling animations with realistic bounce
76
+ [3] scroll-assembly Content reveals synced with scroll position
77
+ [4] border-reveal Inward/outward border animations
78
+ ...
79
+
80
+ 👉 Enter the number of the component to add (1-8):
81
+ ```
82
+
83
+ ### List all available components
84
+
85
+ ```bash
86
+ npx tweenlabs@latest list
87
+ ```
88
+
89
+ ### Install all components at once
90
+
91
+ ```bash
92
+ npx tweenlabs@latest add .
93
+ ```
94
+
95
+ ---
96
+
97
+ ## 📁 Output Structure
98
+
99
+ After installation, components land here by default:
100
+
101
+ ```
102
+ your-project/
103
+ └── src/
104
+ └── components/
105
+ └── tweenlabs/
106
+ ├── GravityDrop.tsx
107
+ ├── BorderReveal.tsx
108
+ └── ...
109
+ ```
110
+
111
+ > **tweenlabs.config.json:** If a `tweenlabs.config.json` exists in your project's root, the CLI reads the `path` option and installs components there. This takes highest precedence.
112
+ >
113
+ > **shadcn UI users:** If no `tweenlabs.config.json` exists but your project has a `components.json`, TweenLabs reads the `aliases.components` field and installs into the matching directory under a `tweenlabs/` subfolder automatically.
114
+
115
+ ---
116
+
117
+ ## 🚩 CLI Flags
118
+
119
+ | Flag | Short | Description |
120
+ |------|-------|-------------|
121
+ | `--yes` | `-y` | Skip all prompts; accept all defaults and auto-install dependencies |
122
+ | `--path <dir>` | `-p` | Override the install directory |
123
+ | `--overwrite` | `-o` | Overwrite existing files without prompting |
124
+ | `--help` | `-h` | Show help |
125
+ | `--version` | `-v` | Show CLI version |
126
+
127
+ ### Examples
128
+
129
+ ```bash
130
+ # Install with a custom path
131
+ npx tweenlabs@latest add gravity-drop --path src/ui/animations
132
+
133
+ # Install all, skip all prompts, overwrite existing files
134
+ npx tweenlabs@latest add . --yes --overwrite
135
+
136
+ # Install with pnpm dlx
137
+ pnpm dlx tweenlabs add border-reveal
138
+ ```
139
+
140
+ ---
141
+
34
142
  ## ✨ Animation Components
35
143
 
36
144
  | Component | Description | GSAP Features Used |
@@ -57,7 +165,9 @@ No paid plugins. No locked content. Just clean, modern animation patterns anyone
57
165
 
58
166
  ---
59
167
 
60
- ## 🚀 Quick Start
168
+ ## 🚀 Quick Start (Playground)
169
+
170
+ Want to run the full TweenLabs playground locally?
61
171
 
62
172
  ```bash
63
173
  # Install pnpm if you don't have it
@@ -123,6 +233,11 @@ git push origin feat/your-animation-name
123
233
 
124
234
  ## 🗺 Roadmap
125
235
 
236
+ - [x] CLI — install components via `npx tweenlabs add`
237
+ - [x] Auto-detect package manager (npm, pnpm, yarn, bun)
238
+ - [x] Auto-install missing dependencies
239
+ - [x] Interactive component picker
240
+ - [x] `components.json` / path resolution
126
241
  - [ ] Export as npm package (`@tweenlabs/components`)
127
242
  - [ ] Storybook integration for isolated component previews
128
243
  - [ ] Unit tests for animation logic
package/bin/cli.js CHANGED
@@ -22,13 +22,14 @@ const helpText = `
22
22
  ${colors.bold}${colors.cyan}TweenLabs CLI${colors.reset} - Install premium GSAP components directly into your codebase.
23
23
 
24
24
  ${colors.bold}Usage:${colors.reset}
25
+ npx tweenlabs init Initialize configuration file
25
26
  npx tweenlabs list List all available components
26
27
  npx tweenlabs add <component-slug> Install a specific component
27
28
 
28
29
  ${colors.bold}Options:${colors.reset}
29
30
  -y, --yes Skip all prompts (auto-accept defaults & install dependencies)
30
31
  -p, --path <path> Specify a custom directory to install the component
31
- -o, --overwrite Overwrite existing component files without prompting
32
+ -o, --overwrite Overwrite existing component files/configuration without prompting
32
33
  -h, --help Show this help message
33
34
  -v, --version Show version
34
35
  `;
@@ -79,6 +80,31 @@ function askQuestion(query) {
79
80
  );
80
81
  }
81
82
 
83
+ // Levenshtein distance helper for spelling suggestions
84
+ function getLevenshteinDistance(a, b) {
85
+ const tmp = [];
86
+ let i;
87
+ let j;
88
+ const alen = a.length;
89
+ const blen = b.length;
90
+ if (alen === 0) return blen;
91
+ if (blen === 0) return alen;
92
+ for (i = 0; i <= alen; i++) {
93
+ tmp[i] = [i];
94
+ }
95
+ for (j = 0; j <= blen; j++) {
96
+ tmp[0][j] = j;
97
+ }
98
+ for (i = 1; i <= alen; i++) {
99
+ for (j = 1; j <= blen; j++) {
100
+ tmp[i][j] = a.charAt(i - 1) === b.charAt(j - 1)
101
+ ? tmp[i - 1][j - 1]
102
+ : Math.min(tmp[i - 1][j - 1] + 1, Math.min(tmp[i][j - 1] + 1, tmp[i - 1][j] + 1));
103
+ }
104
+ }
105
+ return tmp[alen][blen];
106
+ }
107
+
82
108
  // Detect client's package manager
83
109
  function detectPackageManager() {
84
110
  if (fs.existsSync(path.join(process.cwd(), "pnpm-lock.yaml"))) return "pnpm";
@@ -95,16 +121,22 @@ async function main() {
95
121
  process.exit(0);
96
122
  }
97
123
 
124
+ let version = "0.1.6";
125
+ try {
126
+ const pkg = require("../package.json");
127
+ version = pkg.version;
128
+ } catch (_e) {}
129
+
98
130
  if (args.includes("--version") || args.includes("-v")) {
99
- try {
100
- const pkg = require("../package.json");
101
- console.log(pkg.version);
102
- } catch (_err) {
103
- console.log("0.1.1");
104
- }
131
+ console.log(version);
105
132
  process.exit(0);
106
133
  }
107
134
 
135
+ // Print CLI Header Banner
136
+ console.log(
137
+ `\n${colors.bold}${colors.cyan}▲ tweenlabs${colors.reset} ${colors.gray}v${version}${colors.reset}\n`,
138
+ );
139
+
108
140
  // Parse flags
109
141
  const isYes = args.includes("-y") || args.includes("--yes");
110
142
  const isOverwrite = args.includes("-o") || args.includes("--overwrite");
@@ -139,29 +171,86 @@ async function main() {
139
171
  process.exit(0);
140
172
  }
141
173
 
174
+ if (cleanArgs[0] === "init") {
175
+ const configPath = path.join(process.cwd(), "tweenlabs.config.json");
176
+ if (fs.existsSync(configPath) && !isOverwrite && !isYes) {
177
+ console.log(
178
+ `${colors.yellow}! tweenlabs.config.json already exists.${colors.reset}`
179
+ );
180
+ const overwriteConfirm = await askQuestion(
181
+ `Overwrite configuration file? (y/n) ${colors.gray}[y]${colors.reset}: `
182
+ );
183
+ if (
184
+ overwriteConfirm &&
185
+ overwriteConfirm.toLowerCase() !== "y" &&
186
+ overwriteConfirm.toLowerCase() !== "yes"
187
+ ) {
188
+ console.log(`${colors.yellow}! Configuration initialization cancelled.${colors.reset}`);
189
+ process.exit(0);
190
+ }
191
+ }
192
+
193
+ let defaultDir = "";
194
+ if (fs.existsSync(path.join(process.cwd(), "src"))) {
195
+ defaultDir = "./src/components/tweenlabs";
196
+ } else {
197
+ defaultDir = "./components/tweenlabs";
198
+ }
199
+
200
+ let targetPath = defaultDir;
201
+ if (!isYes) {
202
+ const inputPath = await askQuestion(
203
+ `Configure component installation path (${defaultDir}): `
204
+ );
205
+ if (inputPath) {
206
+ targetPath = inputPath;
207
+ }
208
+ }
209
+
210
+ try {
211
+ const configData = {
212
+ path: targetPath,
213
+ };
214
+ fs.writeFileSync(configPath, JSON.stringify(configData, null, 2), "utf-8");
215
+ console.log(
216
+ `\n${colors.green}✔ Created tweenlabs.config.json with path: ${colors.bold}${targetPath}${colors.reset}\n`
217
+ );
218
+ process.exit(0);
219
+ } catch (err) {
220
+ console.error(
221
+ `${colors.red}Error: Failed to create tweenlabs.config.json.${colors.reset}`
222
+ );
223
+ console.error(`${colors.gray}Details: ${err.message}${colors.reset}`);
224
+ process.exit(1);
225
+ }
226
+ }
227
+
142
228
  if (cleanArgs[0] === "list") {
143
- console.log(
144
- `\n${colors.cyan}🔍 Fetching available components list...${colors.reset}`,
145
- );
229
+ console.log(`${colors.cyan}Fetching registry...${colors.reset}`);
146
230
  const domain =
147
231
  process.env.TWEENLABS_REGISTRY_URL || "https://tweenlabs.xyz";
148
232
  const url = `${domain}/api/registry/list`;
149
233
  try {
150
234
  const data = await fetchJson(url);
151
235
  console.log(
152
- `\n${colors.bold}${colors.green}Available TweenLabs Components:${colors.reset}\n`,
236
+ `\n${colors.bold}${colors.green}Available Components:${colors.reset}\n`,
153
237
  );
154
- for (const comp of data.components) {
238
+ const rows = data.components.map((comp) => ({
239
+ slug: comp.cleanSlug || comp.slug,
240
+ desc: comp.description || "",
241
+ }));
242
+ const maxSlugLen = Math.max(...rows.map((r) => r.slug.length), 10);
243
+ for (const row of rows) {
244
+ const paddedSlug = row.slug.padEnd(maxSlugLen + 4, " ");
155
245
  console.log(
156
- ` ${colors.bold}${colors.cyan}* ${comp.name}${colors.reset}`,
246
+ ` ${colors.cyan}${paddedSlug}${colors.reset}${colors.gray}${row.desc}${colors.reset}`,
157
247
  );
158
- console.log(` Slug: ${comp.slug} (or ${comp.cleanSlug})`);
159
- console.log(` Desc: ${comp.description}\n`);
160
248
  }
249
+ console.log("");
161
250
  process.exit(0);
162
251
  } catch (err) {
163
252
  console.error(
164
- `\n${colors.red} Failed to fetch components list.${colors.reset}`,
253
+ `${colors.red}Error: Failed to fetch components list.${colors.reset}`,
165
254
  );
166
255
  console.error(`${colors.gray}Details: ${err.message}${colors.reset}`);
167
256
  process.exit(1);
@@ -170,81 +259,123 @@ async function main() {
170
259
 
171
260
  if (cleanArgs[0] !== "add") {
172
261
  console.log(
173
- `${colors.red}Error: Unknown command "${cleanArgs[0]}". Did you mean "add" or "list"?${colors.reset}`,
262
+ `${colors.red}Error: Unknown command "${cleanArgs[0]}". Did you mean "add", "list" or "init"?${colors.reset}`,
174
263
  );
175
264
  console.log(helpText);
176
265
  process.exit(1);
177
266
  }
178
267
 
179
- const componentSlug = cleanArgs[1];
268
+ let componentSlug = cleanArgs[1];
180
269
  if (!componentSlug) {
270
+ console.log(`${colors.cyan}Fetching registry...${colors.reset}`);
271
+ const domain =
272
+ process.env.TWEENLABS_REGISTRY_URL || "https://tweenlabs.xyz";
273
+ const listUrl = `${domain}/api/registry/list`;
274
+ let listData;
275
+ try {
276
+ listData = await fetchJson(listUrl);
277
+ } catch (err) {
278
+ console.error(
279
+ `${colors.red}Error: Failed to fetch components list.${colors.reset}`,
280
+ );
281
+ console.error(`${colors.gray}Details: ${err.message}${colors.reset}`);
282
+ process.exit(1);
283
+ }
284
+
181
285
  console.log(
182
- `${colors.red}Error: Please specify a component slug to add.${colors.reset}`,
286
+ `${colors.bold}${colors.green}Select a component to install:${colors.reset}\n`,
183
287
  );
184
- console.log(
185
- colors.gray +
186
- "Example: npx tweenlabs add 11-magnetic-dock" +
187
- colors.reset,
288
+ const components = listData.components;
289
+ const maxIndexLen = String(components.length + 1).length;
290
+ const maxSlugLen = Math.max(
291
+ ...components.map((c) => (c.cleanSlug || c.slug).length),
292
+ 10,
188
293
  );
189
- process.exit(1);
190
- }
191
294
 
192
- console.log(
193
- `\n${colors.cyan}🔍 Fetching ${colors.bold}${componentSlug}${colors.reset}${colors.cyan} registry data...${colors.reset}`,
194
- );
295
+ // Print "All Components" option first
296
+ const allIndexStr = "[1]".padStart(maxIndexLen + 2, " ");
297
+ const allSlugStr = ".".padEnd(maxSlugLen + 4, " ");
298
+ console.log(
299
+ ` ${colors.bold}${colors.cyan}${allIndexStr}${colors.reset} ${colors.cyan}${allSlugStr}${colors.reset}${colors.gray}All Components${colors.reset}`,
300
+ );
195
301
 
196
- // Determine registry domain (allow local testing via env variable)
197
- const domain = process.env.TWEENLABS_REGISTRY_URL || "https://tweenlabs.xyz";
198
- const url = `${domain}/api/registry/${componentSlug}`;
302
+ for (let i = 0; i < components.length; i++) {
303
+ const indexStr = `[${i + 2}]`.padStart(maxIndexLen + 2, " ");
304
+ const slugStr = (components[i].cleanSlug || components[i].slug).padEnd(
305
+ maxSlugLen + 4,
306
+ " ",
307
+ );
308
+ console.log(
309
+ ` ${colors.bold}${colors.cyan}${indexStr}${colors.reset} ${colors.cyan}${slugStr}${colors.reset}${colors.gray}${components[i].name}${colors.reset}`,
310
+ );
311
+ }
312
+ console.log("");
199
313
 
200
- let componentData;
201
- try {
202
- componentData = await fetchJson(url);
203
- } catch (err) {
204
- console.error(
205
- `\n${colors.red}❌ Failed to fetch component. Make sure the slug is correct and the server is running.${colors.reset}`,
314
+ const choiceStr = await askQuestion(
315
+ `? Enter the number of the component to add (1-${components.length + 1}): `,
206
316
  );
207
- console.error(`${colors.gray}Details: ${err.message}${colors.reset}`);
208
- process.exit(1);
209
- }
317
+ const choice = parseInt(choiceStr, 10);
318
+ if (Number.isNaN(choice) || choice < 1 || choice > components.length + 1) {
319
+ console.log(`${colors.red}Error: Invalid choice. Exiting.${colors.reset}`);
320
+ process.exit(1);
321
+ }
210
322
 
211
- console.log(
212
- `${colors.green}✓ Component found: ${colors.bold}${componentData.className}${colors.reset}`,
213
- );
323
+ if (choice === 1) {
324
+ componentSlug = ".";
325
+ } else {
326
+ componentSlug =
327
+ components[choice - 2].cleanSlug || components[choice - 2].slug;
328
+ }
329
+ }
214
330
 
215
331
  // Resolve target directory
216
332
  let targetDir = "";
217
333
  if (customPath) {
218
334
  targetDir = path.resolve(process.cwd(), customPath);
219
335
  } else {
220
- // 1. Try to read components.json (shadcn configuration)
221
- const componentsJsonPath = path.join(process.cwd(), "components.json");
222
- if (fs.existsSync(componentsJsonPath)) {
336
+ // 1. Try to read tweenlabs.config.json (our config)
337
+ const tweenlabsConfigPath = path.join(process.cwd(), "tweenlabs.config.json");
338
+ if (fs.existsSync(tweenlabsConfigPath)) {
223
339
  try {
224
- const config = JSON.parse(fs.readFileSync(componentsJsonPath, "utf-8"));
225
- const compAlias = config.aliases?.components;
226
- if (compAlias) {
227
- const cleanAlias = compAlias.replace(/^[@~]\//, "");
228
- if (
229
- fs.existsSync(path.join(process.cwd(), "src")) &&
230
- !cleanAlias.startsWith("src/")
231
- ) {
232
- targetDir = path.join(
233
- process.cwd(),
234
- "src",
235
- cleanAlias,
236
- "tweenlabs",
237
- );
238
- } else {
239
- targetDir = path.join(process.cwd(), cleanAlias, "tweenlabs");
240
- }
340
+ const config = JSON.parse(fs.readFileSync(tweenlabsConfigPath, "utf-8"));
341
+ if (config.path) {
342
+ targetDir = path.resolve(process.cwd(), config.path);
241
343
  }
242
344
  } catch (_err) {
243
345
  // Ignore JSON parse errors
244
346
  }
245
347
  }
246
348
 
247
- // 2. Fallback if targetDir is still not resolved
349
+ // 2. Try to read components.json (shadcn configuration) if still not resolved
350
+ if (!targetDir) {
351
+ const componentsJsonPath = path.join(process.cwd(), "components.json");
352
+ if (fs.existsSync(componentsJsonPath)) {
353
+ try {
354
+ const config = JSON.parse(fs.readFileSync(componentsJsonPath, "utf-8"));
355
+ const compAlias = config.aliases?.components;
356
+ if (compAlias) {
357
+ const cleanAlias = compAlias.replace(/^[@~]\//, "");
358
+ if (
359
+ fs.existsSync(path.join(process.cwd(), "src")) &&
360
+ !cleanAlias.startsWith("src/")
361
+ ) {
362
+ targetDir = path.join(
363
+ process.cwd(),
364
+ "src",
365
+ cleanAlias,
366
+ "tweenlabs",
367
+ );
368
+ } else {
369
+ targetDir = path.join(process.cwd(), cleanAlias, "tweenlabs");
370
+ }
371
+ }
372
+ } catch (_err) {
373
+ // Ignore JSON parse errors
374
+ }
375
+ }
376
+ }
377
+
378
+ // 3. Fallback if targetDir is still not resolved
248
379
  if (!targetDir) {
249
380
  if (fs.existsSync(path.join(process.cwd(), "src"))) {
250
381
  targetDir = path.join(process.cwd(), "src", "components", "tweenlabs");
@@ -255,29 +386,111 @@ async function main() {
255
386
  }
256
387
 
257
388
  console.log(
258
- `📁 Target directory: ${colors.bold}${path.relative(process.cwd(), targetDir)}${colors.reset}`,
389
+ `Target directory: ${colors.bold}${path.relative(process.cwd(), targetDir)}${colors.reset}`,
259
390
  );
260
391
 
261
- // Check if any files already exist
262
- let hasExistingFiles = false;
263
- for (const file of componentData.files) {
264
- const filePath = path.join(targetDir, file.name);
265
- if (fs.existsSync(filePath)) {
266
- hasExistingFiles = true;
267
- break;
392
+ const domain = process.env.TWEENLABS_REGISTRY_URL || "https://tweenlabs.xyz";
393
+ let slugsToInstall = [];
394
+ if (componentSlug === "." || componentSlug === "all") {
395
+ console.log(
396
+ `${colors.cyan}Fetching all components list...${colors.reset}`,
397
+ );
398
+ const listUrl = `${domain}/api/registry/list`;
399
+ try {
400
+ const listData = await fetchJson(listUrl);
401
+ slugsToInstall = listData.components.map((c) => c.cleanSlug || c.slug);
402
+ } catch (err) {
403
+ console.error(
404
+ `${colors.red}Error: Failed to fetch components list.${colors.reset}`,
405
+ );
406
+ console.error(`${colors.gray}Details: ${err.message}${colors.reset}`);
407
+ process.exit(1);
268
408
  }
409
+ } else {
410
+ slugsToInstall = [componentSlug];
269
411
  }
270
412
 
271
- if (hasExistingFiles && !isOverwrite && !isYes) {
413
+ // Gather files and check conflicts
414
+ const filesToWrite = [];
415
+ const conflicts = [];
416
+ const allRequiredDeps = new Set();
417
+
418
+ for (const slug of slugsToInstall) {
419
+ console.log(
420
+ `${colors.cyan}Fetching ${colors.bold}${slug}${colors.reset}${colors.cyan} registry data...${colors.reset}`,
421
+ );
422
+ const url = `${domain}/api/registry/${slug}`;
423
+ try {
424
+ const componentData = await fetchJson(url);
425
+ for (const file of componentData.files) {
426
+ const filePath = path.join(targetDir, file.name);
427
+ if (fs.existsSync(filePath)) {
428
+ conflicts.push(path.relative(process.cwd(), filePath));
429
+ }
430
+ filesToWrite.push({ path: filePath, content: file.content });
431
+ }
432
+ const dependencies = componentData.dependencies || [];
433
+ for (const dep of dependencies) {
434
+ allRequiredDeps.add(dep);
435
+ }
436
+ } catch (_err) {
437
+ console.error(
438
+ `${colors.red}Error: Failed to fetch component "${slug}". Skipping.${colors.reset}`,
439
+ );
440
+ // Fetch registry list to suggest closest matches
441
+ let suggestions = [];
442
+ try {
443
+ const listData = await fetchJson(`${domain}/api/registry/list`);
444
+ const validSlugs = listData.components.map((c) => c.cleanSlug || c.slug);
445
+
446
+ // Find slugs with low Levenshtein distance
447
+ const matches = validSlugs.map((validSlug) => {
448
+ return {
449
+ slug: validSlug,
450
+ distance: getLevenshteinDistance(slug, validSlug)
451
+ };
452
+ });
453
+
454
+ // Sort by distance ascending
455
+ matches.sort((a, b) => a.distance - b.distance);
456
+
457
+ // Threshold: distance <= 3 or distance <= half of query length
458
+ suggestions = matches
459
+ .filter((m) => m.distance <= 3 || m.distance <= Math.round(slug.length / 2))
460
+ .map((m) => m.slug);
461
+ } catch (_e) {
462
+ // If list fetch fails, we just don't show suggestions
463
+ }
464
+
465
+ if (suggestions.length > 0) {
466
+ console.error(
467
+ `${colors.yellow}Did you mean: ${suggestions.map(s => `${colors.bold}${s}${colors.reset}`).join(", ")}?${colors.reset}\n`
468
+ );
469
+ }
470
+ }
471
+ }
472
+
473
+ if (filesToWrite.length === 0) {
474
+ console.log(`${colors.red}Error: No files to install. Exiting.${colors.reset}`);
475
+ process.exit(1);
476
+ }
477
+
478
+ if (conflicts.length > 0 && !isOverwrite && !isYes) {
479
+ console.log(
480
+ `\n${colors.yellow}! The following files already exist:${colors.reset}`,
481
+ );
482
+ for (const conflict of conflicts) {
483
+ console.log(` → ${conflict}`);
484
+ }
272
485
  const overwriteConfirm = await askQuestion(
273
- `⚠️ Component files already exist in ${colors.bold}${path.relative(process.cwd(), targetDir)}${colors.reset}. Overwrite? (y/n) ${colors.gray}[y]${colors.reset}: `,
486
+ `\nOverwrite these files? (y/n) ${colors.gray}[y]${colors.reset}: `,
274
487
  );
275
488
  if (
276
489
  overwriteConfirm &&
277
490
  overwriteConfirm.toLowerCase() !== "y" &&
278
491
  overwriteConfirm.toLowerCase() !== "yes"
279
492
  ) {
280
- console.log(`${colors.yellow}Installation cancelled.${colors.reset}`);
493
+ console.log(`${colors.yellow}! Installation cancelled.${colors.reset}`);
281
494
  process.exit(0);
282
495
  }
283
496
  }
@@ -288,19 +501,17 @@ async function main() {
288
501
  }
289
502
 
290
503
  // Write component files
291
- console.log(`\n${colors.cyan}💾 Writing component files...${colors.reset}`);
292
- for (const file of componentData.files) {
293
- const filePath = path.join(targetDir, file.name);
294
- fs.writeFileSync(filePath, file.content, "utf-8");
504
+ console.log(`\n${colors.bold}Writing component files...${colors.reset}`);
505
+ for (const file of filesToWrite) {
506
+ fs.writeFileSync(file.path, file.content, "utf-8");
295
507
  console.log(
296
- `${colors.green} Created: ${colors.bold}${path.relative(process.cwd(), filePath)}${colors.reset}`,
508
+ ` ${colors.green}✔${colors.reset} Created ${colors.bold}${path.relative(process.cwd(), file.path)}${colors.reset}`,
297
509
  );
298
510
  }
299
511
 
300
512
  // Check and install dependencies
301
- const dependencies = componentData.dependencies || [];
513
+ const dependencies = Array.from(allRequiredDeps);
302
514
  if (dependencies.length > 0) {
303
- // Read package.json to see if dependencies are already installed
304
515
  let pkgJson = {};
305
516
  try {
306
517
  pkgJson = JSON.parse(
@@ -316,8 +527,12 @@ async function main() {
316
527
  if (missingDeps.length > 0) {
317
528
  const pm = detectPackageManager();
318
529
  console.log(
319
- `\n${colors.cyan}📦 Installing missing dependencies: ${colors.bold}${missingDeps.join(", ")}${colors.reset} using ${pm}...`,
530
+ `\n${colors.bold}Installing missing dependencies using ${pm}...${colors.reset}`,
320
531
  );
532
+ for (const dep of missingDeps) {
533
+ console.log(` → ${dep}`);
534
+ }
535
+ console.log("");
321
536
 
322
537
  let installCmd = "";
323
538
  if (pm === "pnpm") installCmd = `pnpm add ${missingDeps.join(" ")}`;
@@ -328,26 +543,24 @@ async function main() {
328
543
  try {
329
544
  execSync(installCmd, { stdio: "inherit" });
330
545
  console.log(
331
- `${colors.green} Dependencies installed successfully!${colors.reset}`,
546
+ `\n${colors.green} Dependencies installed successfully!${colors.reset}`,
332
547
  );
333
548
  } catch (_err) {
334
549
  console.error(
335
- `${colors.red} Failed to install dependencies. Please run "${installCmd}" manually.${colors.reset}`,
550
+ `\n${colors.red}Error: Failed to install dependencies. Please run "${installCmd}" manually.${colors.reset}`,
336
551
  );
337
552
  }
338
553
  } else {
339
554
  console.log(
340
- `\n${colors.green} All dependencies (${dependencies.join(", ")}) already installed.${colors.reset}`,
555
+ `\n${colors.green} All dependencies (${dependencies.join(", ")}) already installed.${colors.reset}`,
341
556
  );
342
557
  }
343
558
  }
344
559
 
345
560
  console.log(
346
- `\n${colors.green}${colors.bold}🎉 Installation complete!${colors.reset}`,
347
- );
348
- console.log(
349
- `You can now import and use the ${colors.bold}${componentData.className}${colors.reset} component in your project.\n`,
561
+ `\n${colors.bold}${colors.green} Done! All requested components installed successfully.${colors.reset}`,
350
562
  );
563
+ console.log(`You can now import and use them in your project.\n`);
351
564
  }
352
565
 
353
566
  main().catch((err) => {
package/package.json CHANGED
@@ -1,60 +1,62 @@
1
- {
2
- "name": "tweenlabs",
3
- "version": "0.1.3",
4
- "license": "MIT",
5
- "repository": {
6
- "type": "git",
7
- "url": "git+https://github.com/TweenLabs/TweenLabs.git"
8
- },
9
- "homepage": "https://tweenlabs.xyz",
10
- "bugs": {
11
- "url": "https://github.com/TweenLabs/TweenLabs/issues"
12
- },
13
- "bin": {
14
- "tweenlabs": "bin/cli.js"
15
- },
16
- "files": [
17
- "bin",
18
- "README.md",
19
- "LICENSE"
20
- ],
21
- "scripts": {
22
- "dev": "next dev",
23
- "build": "node scripts/generate-llms-full.js && next build",
24
- "vercel-build": "convex deploy --cmd \"node scripts/generate-llms-full.js && next build\"",
25
- "start": "next start",
26
- "lint": "eslint"
27
- },
28
- "dependencies": {
29
- "@convex-dev/better-auth": "^0.12.4",
30
- "@gsap/react": "^2.1.2",
31
- "better-auth": "^1.6.19",
32
- "convex": "^1.41.0",
33
- "gsap": "^3.15.0",
34
- "lenis": "^1.3.23",
35
- "next": "16.2.7",
36
- "react": "19.2.4",
37
- "react-dom": "19.2.4"
38
- },
39
- "devDependencies": {
40
- "@biomejs/biome": "^2.5.0",
41
- "@tailwindcss/postcss": "^4",
42
- "@types/node": "^20",
43
- "@types/react": "^19",
44
- "@types/react-dom": "^19",
45
- "eslint": "^9",
46
- "eslint-config-next": "16.2.7",
47
- "sharp": "^0.35.1",
48
- "tailwindcss": "^4",
49
- "typescript": "^5"
50
- },
51
- "pnpm": {
52
- "ignoredBuiltDependencies": [
53
- "sharp",
54
- "unrs-resolver"
55
- ],
56
- "overrides": {
57
- "postcss": "^8.5.10"
58
- }
59
- }
60
- }
1
+ {
2
+ "name": "tweenlabs",
3
+ "version": "0.1.6",
4
+ "description": "Zero-dependency CLI to install premium GSAP animation components directly into your Next.js or React project.",
5
+ "keywords": ["gsap", "animation", "nextjs", "react", "components", "cli", "tweenlabs", "scrolltrigger", "lenis"],
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/TweenLabs/TweenLabs.git"
10
+ },
11
+ "homepage": "https://tweenlabs.xyz",
12
+ "bugs": {
13
+ "url": "https://github.com/TweenLabs/TweenLabs/issues"
14
+ },
15
+ "bin": {
16
+ "tweenlabs": "bin/cli.js"
17
+ },
18
+ "files": [
19
+ "bin",
20
+ "README.md",
21
+ "LICENSE"
22
+ ],
23
+ "scripts": {
24
+ "dev": "next dev",
25
+ "build": "node scripts/generate-llms-full.js && next build",
26
+ "vercel-build": "convex deploy --cmd \"node scripts/generate-llms-full.js && next build\"",
27
+ "start": "next start",
28
+ "lint": "eslint"
29
+ },
30
+ "dependencies": {
31
+ "@convex-dev/better-auth": "^0.12.4",
32
+ "@gsap/react": "^2.1.2",
33
+ "better-auth": "^1.6.19",
34
+ "convex": "^1.41.0",
35
+ "gsap": "^3.15.0",
36
+ "lenis": "^1.3.23",
37
+ "next": "16.2.7",
38
+ "react": "19.2.4",
39
+ "react-dom": "19.2.4"
40
+ },
41
+ "devDependencies": {
42
+ "@biomejs/biome": "^2.5.0",
43
+ "@tailwindcss/postcss": "^4",
44
+ "@types/node": "^20",
45
+ "@types/react": "^19",
46
+ "@types/react-dom": "^19",
47
+ "eslint": "^9",
48
+ "eslint-config-next": "16.2.7",
49
+ "sharp": "^0.35.1",
50
+ "tailwindcss": "^4",
51
+ "typescript": "^5"
52
+ },
53
+ "pnpm": {
54
+ "ignoredBuiltDependencies": [
55
+ "sharp",
56
+ "unrs-resolver"
57
+ ],
58
+ "overrides": {
59
+ "postcss": "^8.5.10"
60
+ }
61
+ }
62
+ }