@nclamvn/vibecode-cli 2.2.0 → 3.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,513 @@
1
+ // ═══════════════════════════════════════════════════════════════════════════════
2
+ // VIBECODE CLI - Image Service
3
+ // AI-powered image generation and integration using Unsplash API
4
+ // ═══════════════════════════════════════════════════════════════════════════════
5
+
6
+ import fs from 'fs/promises';
7
+ import path from 'path';
8
+ import https from 'https';
9
+ import chalk from 'chalk';
10
+
11
+ // Curated image collections for professional results
12
+ const CURATED_COLLECTIONS = {
13
+ hero: {
14
+ tech: [
15
+ 'https://images.unsplash.com/photo-1518770660439-4636190af475?w=1920&q=80',
16
+ 'https://images.unsplash.com/photo-1451187580459-43490279c0fa?w=1920&q=80',
17
+ 'https://images.unsplash.com/photo-1550751827-4bd374c3f58b?w=1920&q=80',
18
+ 'https://images.unsplash.com/photo-1526374965328-7f61d4dc18c5?w=1920&q=80',
19
+ 'https://images.unsplash.com/photo-1504639725590-34d0984388bd?w=1920&q=80'
20
+ ],
21
+ business: [
22
+ 'https://images.unsplash.com/photo-1497366216548-37526070297c?w=1920&q=80',
23
+ 'https://images.unsplash.com/photo-1497215728101-856f4ea42174?w=1920&q=80',
24
+ 'https://images.unsplash.com/photo-1454165804606-c3d57bc86b40?w=1920&q=80',
25
+ 'https://images.unsplash.com/photo-1522071820081-009f0129c71c?w=1920&q=80',
26
+ 'https://images.unsplash.com/photo-1552664730-d307ca884978?w=1920&q=80'
27
+ ],
28
+ creative: [
29
+ 'https://images.unsplash.com/photo-1499951360447-b19be8fe80f5?w=1920&q=80',
30
+ 'https://images.unsplash.com/photo-1558655146-9f40138edfeb?w=1920&q=80',
31
+ 'https://images.unsplash.com/photo-1542744094-3a31f272c490?w=1920&q=80',
32
+ 'https://images.unsplash.com/photo-1516321318423-f06f85e504b3?w=1920&q=80',
33
+ 'https://images.unsplash.com/photo-1460925895917-afdab827c52f?w=1920&q=80'
34
+ ],
35
+ nature: [
36
+ 'https://images.unsplash.com/photo-1469474968028-56623f02e42e?w=1920&q=80',
37
+ 'https://images.unsplash.com/photo-1441974231531-c6227db76b6e?w=1920&q=80',
38
+ 'https://images.unsplash.com/photo-1501854140801-50d01698950b?w=1920&q=80',
39
+ 'https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?w=1920&q=80',
40
+ 'https://images.unsplash.com/photo-1433086966358-54859d0ed716?w=1920&q=80'
41
+ ],
42
+ abstract: [
43
+ 'https://images.unsplash.com/photo-1557672172-298e090bd0f1?w=1920&q=80',
44
+ 'https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?w=1920&q=80',
45
+ 'https://images.unsplash.com/photo-1579546929518-9e396f3cc809?w=1920&q=80',
46
+ 'https://images.unsplash.com/photo-1558591710-4b4a1ae0f04d?w=1920&q=80',
47
+ 'https://images.unsplash.com/photo-1508615039623-a25605d2b022?w=1920&q=80'
48
+ ]
49
+ },
50
+ products: {
51
+ tech: [
52
+ 'https://images.unsplash.com/photo-1505740420928-5e560c06d30e?w=800&q=80',
53
+ 'https://images.unsplash.com/photo-1523275335684-37898b6baf30?w=800&q=80',
54
+ 'https://images.unsplash.com/photo-1546868871-7041f2a55e12?w=800&q=80',
55
+ 'https://images.unsplash.com/photo-1585386959984-a4155224a1ad?w=800&q=80',
56
+ 'https://images.unsplash.com/photo-1491933382434-500287f9b54b?w=800&q=80',
57
+ 'https://images.unsplash.com/photo-1560343090-f0409e92791a?w=800&q=80'
58
+ ],
59
+ fashion: [
60
+ 'https://images.unsplash.com/photo-1542291026-7eec264c27ff?w=800&q=80',
61
+ 'https://images.unsplash.com/photo-1549298916-b41d501d3772?w=800&q=80',
62
+ 'https://images.unsplash.com/photo-1441986300917-64674bd600d8?w=800&q=80',
63
+ 'https://images.unsplash.com/photo-1556905055-8f358a7a47b2?w=800&q=80',
64
+ 'https://images.unsplash.com/photo-1467043237213-65f2da53396f?w=800&q=80'
65
+ ],
66
+ food: [
67
+ 'https://images.unsplash.com/photo-1504674900247-0877df9cc836?w=800&q=80',
68
+ 'https://images.unsplash.com/photo-1512621776951-a57141f2eefd?w=800&q=80',
69
+ 'https://images.unsplash.com/photo-1565299624946-b28f40a0ae38?w=800&q=80',
70
+ 'https://images.unsplash.com/photo-1482049016gy-d606572dc5de?w=800&q=80',
71
+ 'https://images.unsplash.com/photo-1540189549336-e6e99c3679fe?w=800&q=80'
72
+ ],
73
+ lifestyle: [
74
+ 'https://images.unsplash.com/photo-1516321497487-e288fb19713f?w=800&q=80',
75
+ 'https://images.unsplash.com/photo-1483985988355-763728e1935b?w=800&q=80',
76
+ 'https://images.unsplash.com/photo-1556742049-0cfed4f6a45d?w=800&q=80',
77
+ 'https://images.unsplash.com/photo-1472851294608-062f824d29cc?w=800&q=80',
78
+ 'https://images.unsplash.com/photo-1525328437458-0c4d4db7cab4?w=800&q=80'
79
+ ]
80
+ },
81
+ team: [
82
+ 'https://images.unsplash.com/photo-1522071820081-009f0129c71c?w=400&q=80',
83
+ 'https://images.unsplash.com/photo-1560250097-0b93528c311a?w=400&q=80',
84
+ 'https://images.unsplash.com/photo-1573496359142-b8d87734a5a2?w=400&q=80',
85
+ 'https://images.unsplash.com/photo-1519085360753-af0119f7cbe7?w=400&q=80',
86
+ 'https://images.unsplash.com/photo-1580489944761-15a19d654956?w=400&q=80',
87
+ 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=400&q=80',
88
+ 'https://images.unsplash.com/photo-1494790108755-2616b612b786?w=400&q=80',
89
+ 'https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=400&q=80'
90
+ ],
91
+ testimonials: [
92
+ 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=200&q=80',
93
+ 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=200&q=80',
94
+ 'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=200&q=80',
95
+ 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=200&q=80',
96
+ 'https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=200&q=80',
97
+ 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=200&q=80'
98
+ ],
99
+ logos: [
100
+ 'https://images.unsplash.com/photo-1611162617474-5b21e879e113?w=200&q=80',
101
+ 'https://images.unsplash.com/photo-1611162616305-c69b3fa7fbe0?w=200&q=80',
102
+ 'https://images.unsplash.com/photo-1611162618071-b39a2ec055fb?w=200&q=80'
103
+ ],
104
+ icons: {
105
+ general: [
106
+ 'https://images.unsplash.com/photo-1611162616475-46b635cb6868?w=100&q=80'
107
+ ]
108
+ },
109
+ backgrounds: {
110
+ gradient: [
111
+ 'https://images.unsplash.com/photo-1557683316-973673baf926?w=1920&q=80',
112
+ 'https://images.unsplash.com/photo-1557682250-33bd709cbe85?w=1920&q=80',
113
+ 'https://images.unsplash.com/photo-1557682224-5b8590cd9ec5?w=1920&q=80',
114
+ 'https://images.unsplash.com/photo-1557682260-96773eb01377?w=1920&q=80'
115
+ ],
116
+ pattern: [
117
+ 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=1920&q=80',
118
+ 'https://images.unsplash.com/photo-1553356084-58ef4a67b2a7?w=1920&q=80'
119
+ ]
120
+ }
121
+ };
122
+
123
+ // Unsplash API configuration
124
+ const UNSPLASH_API_URL = 'https://api.unsplash.com';
125
+
126
+ /**
127
+ * ImageService - AI-powered image generation and integration
128
+ */
129
+ export class ImageService {
130
+ constructor(options = {}) {
131
+ this.accessKey = options.accessKey || process.env.UNSPLASH_ACCESS_KEY;
132
+ this.downloadPath = options.downloadPath || './public/images';
133
+ this.verbose = options.verbose || false;
134
+ }
135
+
136
+ /**
137
+ * Search Unsplash for images
138
+ */
139
+ async searchImages(query, options = {}) {
140
+ const {
141
+ count = 5,
142
+ orientation = 'landscape', // landscape, portrait, squarish
143
+ size = 'regular' // raw, full, regular, small, thumb
144
+ } = options;
145
+
146
+ // If no API key, use curated fallback
147
+ if (!this.accessKey) {
148
+ return this.getCuratedImages(query, count);
149
+ }
150
+
151
+ try {
152
+ const params = new URLSearchParams({
153
+ query,
154
+ per_page: count,
155
+ orientation
156
+ });
157
+
158
+ const response = await this.fetchWithTimeout(
159
+ `${UNSPLASH_API_URL}/search/photos?${params}`,
160
+ {
161
+ headers: {
162
+ 'Authorization': `Client-ID ${this.accessKey}`
163
+ }
164
+ }
165
+ );
166
+
167
+ if (!response.ok) {
168
+ throw new Error(`Unsplash API error: ${response.status}`);
169
+ }
170
+
171
+ const data = await response.json();
172
+
173
+ return data.results.map(img => ({
174
+ id: img.id,
175
+ url: img.urls[size],
176
+ downloadUrl: img.links.download_location,
177
+ alt: img.alt_description || query,
178
+ photographer: img.user.name,
179
+ photographerUrl: img.user.links.html,
180
+ width: img.width,
181
+ height: img.height
182
+ }));
183
+ } catch (error) {
184
+ if (this.verbose) {
185
+ console.log(chalk.yellow(` Unsplash API failed, using curated images`));
186
+ }
187
+ return this.getCuratedImages(query, count);
188
+ }
189
+ }
190
+
191
+ /**
192
+ * Get curated images based on query (fallback when no API key)
193
+ */
194
+ getCuratedImages(query, count = 5) {
195
+ const queryLower = query.toLowerCase();
196
+ let images = [];
197
+
198
+ // Match query to curated collections
199
+ if (queryLower.includes('hero') || queryLower.includes('banner')) {
200
+ if (queryLower.includes('tech')) {
201
+ images = CURATED_COLLECTIONS.hero.tech;
202
+ } else if (queryLower.includes('business') || queryLower.includes('corporate')) {
203
+ images = CURATED_COLLECTIONS.hero.business;
204
+ } else if (queryLower.includes('creative') || queryLower.includes('design')) {
205
+ images = CURATED_COLLECTIONS.hero.creative;
206
+ } else if (queryLower.includes('nature') || queryLower.includes('outdoor')) {
207
+ images = CURATED_COLLECTIONS.hero.nature;
208
+ } else {
209
+ images = CURATED_COLLECTIONS.hero.abstract;
210
+ }
211
+ } else if (queryLower.includes('product')) {
212
+ if (queryLower.includes('tech') || queryLower.includes('gadget')) {
213
+ images = CURATED_COLLECTIONS.products.tech;
214
+ } else if (queryLower.includes('fashion') || queryLower.includes('clothing')) {
215
+ images = CURATED_COLLECTIONS.products.fashion;
216
+ } else if (queryLower.includes('food')) {
217
+ images = CURATED_COLLECTIONS.products.food;
218
+ } else {
219
+ images = CURATED_COLLECTIONS.products.lifestyle;
220
+ }
221
+ } else if (queryLower.includes('team') || queryLower.includes('person') || queryLower.includes('people')) {
222
+ images = CURATED_COLLECTIONS.team;
223
+ } else if (queryLower.includes('testimonial') || queryLower.includes('avatar')) {
224
+ images = CURATED_COLLECTIONS.testimonials;
225
+ } else if (queryLower.includes('logo') || queryLower.includes('brand')) {
226
+ images = CURATED_COLLECTIONS.logos;
227
+ } else if (queryLower.includes('background') || queryLower.includes('bg')) {
228
+ if (queryLower.includes('gradient')) {
229
+ images = CURATED_COLLECTIONS.backgrounds.gradient;
230
+ } else {
231
+ images = CURATED_COLLECTIONS.backgrounds.pattern;
232
+ }
233
+ } else {
234
+ // Default to abstract hero images
235
+ images = CURATED_COLLECTIONS.hero.abstract;
236
+ }
237
+
238
+ // Shuffle and take requested count
239
+ const shuffled = [...images].sort(() => Math.random() - 0.5);
240
+ return shuffled.slice(0, count).map((url, index) => ({
241
+ id: `curated-${index}`,
242
+ url,
243
+ downloadUrl: url,
244
+ alt: query,
245
+ photographer: 'Unsplash',
246
+ photographerUrl: 'https://unsplash.com',
247
+ width: 1920,
248
+ height: 1080
249
+ }));
250
+ }
251
+
252
+ /**
253
+ * Download image to local path
254
+ */
255
+ async downloadImage(imageUrl, filename, options = {}) {
256
+ const { directory = this.downloadPath } = options;
257
+
258
+ // Ensure directory exists
259
+ await fs.mkdir(directory, { recursive: true });
260
+
261
+ const filePath = path.join(directory, filename);
262
+
263
+ return new Promise((resolve, reject) => {
264
+ const file = require('fs').createWriteStream(filePath);
265
+
266
+ https.get(imageUrl, (response) => {
267
+ // Handle redirects
268
+ if (response.statusCode === 301 || response.statusCode === 302) {
269
+ const redirectUrl = response.headers.location;
270
+ https.get(redirectUrl, (redirectResponse) => {
271
+ redirectResponse.pipe(file);
272
+ file.on('finish', () => {
273
+ file.close();
274
+ resolve(filePath);
275
+ });
276
+ }).on('error', reject);
277
+ return;
278
+ }
279
+
280
+ response.pipe(file);
281
+ file.on('finish', () => {
282
+ file.close();
283
+ resolve(filePath);
284
+ });
285
+ }).on('error', (err) => {
286
+ fs.unlink(filePath).catch(() => {});
287
+ reject(err);
288
+ });
289
+ });
290
+ }
291
+
292
+ /**
293
+ * Generate images for a project
294
+ */
295
+ async generateProjectImages(projectPath, projectType, options = {}) {
296
+ const {
297
+ hero = true,
298
+ products = 0,
299
+ team = 0,
300
+ testimonials = 0,
301
+ theme = 'tech'
302
+ } = options;
303
+
304
+ const imagesDir = path.join(projectPath, 'public', 'images');
305
+ await fs.mkdir(imagesDir, { recursive: true });
306
+
307
+ const results = {
308
+ downloaded: [],
309
+ failed: []
310
+ };
311
+
312
+ console.log(chalk.cyan('\n 📸 Generating project images...\n'));
313
+
314
+ // Hero image
315
+ if (hero) {
316
+ try {
317
+ const heroImages = await this.searchImages(`hero ${theme}`, { count: 1 });
318
+ if (heroImages.length > 0) {
319
+ const filename = 'hero.jpg';
320
+ await this.downloadImage(heroImages[0].url, filename, { directory: imagesDir });
321
+ results.downloaded.push({ type: 'hero', filename, url: heroImages[0].url });
322
+ console.log(chalk.green(` ✓ Hero image downloaded`));
323
+ }
324
+ } catch (error) {
325
+ results.failed.push({ type: 'hero', error: error.message });
326
+ console.log(chalk.yellow(` ⚠ Hero image failed: ${error.message}`));
327
+ }
328
+ }
329
+
330
+ // Product images
331
+ if (products > 0) {
332
+ try {
333
+ const productImages = await this.searchImages(`product ${theme}`, { count: products });
334
+ for (let i = 0; i < productImages.length; i++) {
335
+ const filename = `product-${i + 1}.jpg`;
336
+ await this.downloadImage(productImages[i].url, filename, { directory: imagesDir });
337
+ results.downloaded.push({ type: 'product', filename, url: productImages[i].url });
338
+ }
339
+ console.log(chalk.green(` ✓ ${productImages.length} product images downloaded`));
340
+ } catch (error) {
341
+ results.failed.push({ type: 'products', error: error.message });
342
+ console.log(chalk.yellow(` ⚠ Product images failed: ${error.message}`));
343
+ }
344
+ }
345
+
346
+ // Team photos
347
+ if (team > 0) {
348
+ try {
349
+ const teamImages = this.getCuratedImages('team', team);
350
+ for (let i = 0; i < teamImages.length; i++) {
351
+ const filename = `team-${i + 1}.jpg`;
352
+ await this.downloadImage(teamImages[i].url, filename, { directory: imagesDir });
353
+ results.downloaded.push({ type: 'team', filename, url: teamImages[i].url });
354
+ }
355
+ console.log(chalk.green(` ✓ ${teamImages.length} team photos downloaded`));
356
+ } catch (error) {
357
+ results.failed.push({ type: 'team', error: error.message });
358
+ console.log(chalk.yellow(` ⚠ Team photos failed: ${error.message}`));
359
+ }
360
+ }
361
+
362
+ // Testimonial avatars
363
+ if (testimonials > 0) {
364
+ try {
365
+ const testimonialImages = this.getCuratedImages('testimonial', testimonials);
366
+ for (let i = 0; i < testimonialImages.length; i++) {
367
+ const filename = `testimonial-${i + 1}.jpg`;
368
+ await this.downloadImage(testimonialImages[i].url, filename, { directory: imagesDir });
369
+ results.downloaded.push({ type: 'testimonial', filename, url: testimonialImages[i].url });
370
+ }
371
+ console.log(chalk.green(` ✓ ${testimonialImages.length} testimonial avatars downloaded`));
372
+ } catch (error) {
373
+ results.failed.push({ type: 'testimonials', error: error.message });
374
+ console.log(chalk.yellow(` ⚠ Testimonial avatars failed: ${error.message}`));
375
+ }
376
+ }
377
+
378
+ console.log(chalk.gray(`\n Total: ${results.downloaded.length} images downloaded to ${imagesDir}\n`));
379
+
380
+ return results;
381
+ }
382
+
383
+ /**
384
+ * Replace placeholder images in project files
385
+ */
386
+ async replacePlaceholders(projectPath, options = {}) {
387
+ const { extensions = ['.js', '.jsx', '.tsx', '.html', '.vue', '.svelte'] } = options;
388
+
389
+ const placeholderPatterns = [
390
+ /https?:\/\/via\.placeholder\.com\/\d+x?\d*/g,
391
+ /https?:\/\/placehold\.co\/\d+x?\d*/g,
392
+ /https?:\/\/placekitten\.com\/\d+\/?\d*/g,
393
+ /https?:\/\/picsum\.photos\/\d+\/?\d*/g,
394
+ /\/placeholder\.(jpg|png|svg)/g
395
+ ];
396
+
397
+ let replacedCount = 0;
398
+
399
+ const processFile = async (filePath) => {
400
+ try {
401
+ let content = await fs.readFile(filePath, 'utf-8');
402
+ let modified = false;
403
+
404
+ for (const pattern of placeholderPatterns) {
405
+ const matches = content.match(pattern);
406
+ if (matches) {
407
+ for (const match of matches) {
408
+ // Determine replacement based on context
409
+ const images = this.getCuratedImages('abstract', 1);
410
+ if (images.length > 0) {
411
+ content = content.replace(match, images[0].url);
412
+ modified = true;
413
+ replacedCount++;
414
+ }
415
+ }
416
+ }
417
+ }
418
+
419
+ if (modified) {
420
+ await fs.writeFile(filePath, content);
421
+ if (this.verbose) {
422
+ console.log(chalk.gray(` Updated: ${path.relative(projectPath, filePath)}`));
423
+ }
424
+ }
425
+ } catch (error) {
426
+ // Skip files that can't be read
427
+ }
428
+ };
429
+
430
+ const walkDir = async (dir) => {
431
+ try {
432
+ const entries = await fs.readdir(dir, { withFileTypes: true });
433
+
434
+ for (const entry of entries) {
435
+ const fullPath = path.join(dir, entry.name);
436
+
437
+ // Skip node_modules and hidden directories
438
+ if (entry.isDirectory()) {
439
+ if (entry.name !== 'node_modules' && !entry.name.startsWith('.')) {
440
+ await walkDir(fullPath);
441
+ }
442
+ } else if (entry.isFile()) {
443
+ const ext = path.extname(entry.name).toLowerCase();
444
+ if (extensions.includes(ext)) {
445
+ await processFile(fullPath);
446
+ }
447
+ }
448
+ }
449
+ } catch (error) {
450
+ // Skip directories that can't be read
451
+ }
452
+ };
453
+
454
+ await walkDir(projectPath);
455
+
456
+ return { replacedCount };
457
+ }
458
+
459
+ /**
460
+ * Fetch with timeout helper
461
+ */
462
+ async fetchWithTimeout(url, options = {}, timeout = 10000) {
463
+ const controller = new AbortController();
464
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
465
+
466
+ try {
467
+ const response = await fetch(url, {
468
+ ...options,
469
+ signal: controller.signal
470
+ });
471
+ clearTimeout(timeoutId);
472
+ return response;
473
+ } catch (error) {
474
+ clearTimeout(timeoutId);
475
+ throw error;
476
+ }
477
+ }
478
+ }
479
+
480
+ /**
481
+ * Create ImageService instance
482
+ */
483
+ export function createImageService(options = {}) {
484
+ return new ImageService(options);
485
+ }
486
+
487
+ /**
488
+ * Quick image search helper
489
+ */
490
+ export async function searchImages(query, options = {}) {
491
+ const service = createImageService(options);
492
+ return service.searchImages(query, options);
493
+ }
494
+
495
+ /**
496
+ * Generate images for project helper
497
+ */
498
+ export async function generateImages(projectPath, projectType, options = {}) {
499
+ const service = createImageService(options);
500
+ return service.generateProjectImages(projectPath, projectType, options);
501
+ }
502
+
503
+ /**
504
+ * Get curated collection
505
+ */
506
+ export function getCuratedCollection(type, subtype = null) {
507
+ if (subtype && CURATED_COLLECTIONS[type]?.[subtype]) {
508
+ return CURATED_COLLECTIONS[type][subtype];
509
+ }
510
+ return CURATED_COLLECTIONS[type] || [];
511
+ }
512
+
513
+ export { CURATED_COLLECTIONS };