@nclamvn/vibecode-cli 2.2.1 → 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,465 @@
1
+ // ═══════════════════════════════════════════════════════════════════════════════
2
+ // VIBECODE CLI - Images Command
3
+ // AI-powered image generation and management
4
+ // ═══════════════════════════════════════════════════════════════════════════════
5
+
6
+ import chalk from 'chalk';
7
+ import path from 'path';
8
+ import inquirer from 'inquirer';
9
+ import {
10
+ ImageService,
11
+ createImageService,
12
+ searchImages,
13
+ generateImages,
14
+ getCuratedCollection,
15
+ CURATED_COLLECTIONS
16
+ } from '../services/image-service.js';
17
+
18
+ /**
19
+ * Images command entry point
20
+ */
21
+ export async function imagesCommand(query, options = {}) {
22
+ const cwd = process.cwd();
23
+
24
+ // List generated images
25
+ if (options.list) {
26
+ return listImages(cwd);
27
+ }
28
+
29
+ // Search mode
30
+ if (options.search || query) {
31
+ return searchAndDisplay(query || options.search, options);
32
+ }
33
+
34
+ // Replace placeholders
35
+ if (options.replace) {
36
+ return replacePlaceholders(cwd, options);
37
+ }
38
+
39
+ // Generate hero image
40
+ if (options.hero) {
41
+ return generateHeroImage(cwd, options);
42
+ }
43
+
44
+ // Generate product images
45
+ if (options.products) {
46
+ return generateProductImages(cwd, parseInt(options.products) || 6, options);
47
+ }
48
+
49
+ // Generate full image set
50
+ if (options.generate) {
51
+ return generateFullSet(cwd, options);
52
+ }
53
+
54
+ // Default: interactive mode
55
+ return interactiveMode(cwd, options);
56
+ }
57
+
58
+ /**
59
+ * Search and display images
60
+ */
61
+ async function searchAndDisplay(query, options = {}) {
62
+ console.log(chalk.cyan(`\n 🔍 Searching for "${query}"...\n`));
63
+
64
+ const service = createImageService({ verbose: true });
65
+ const images = await service.searchImages(query, {
66
+ count: parseInt(options.count) || 8,
67
+ orientation: options.orientation || 'landscape'
68
+ });
69
+
70
+ if (images.length === 0) {
71
+ console.log(chalk.yellow(' No images found.\n'));
72
+ return;
73
+ }
74
+
75
+ console.log(chalk.green(` Found ${images.length} images:\n`));
76
+
77
+ for (let i = 0; i < images.length; i++) {
78
+ const img = images[i];
79
+ console.log(chalk.white(` ${i + 1}. ${img.alt || 'Image'}`));
80
+ console.log(chalk.gray(` ${img.url}`));
81
+ console.log(chalk.gray(` by ${img.photographer}\n`));
82
+ }
83
+
84
+ // Offer to download
85
+ const { download } = await inquirer.prompt([{
86
+ type: 'confirm',
87
+ name: 'download',
88
+ message: 'Download these images?',
89
+ default: false
90
+ }]);
91
+
92
+ if (download) {
93
+ const downloadPath = path.join(process.cwd(), 'public', 'images');
94
+ console.log(chalk.cyan(`\n Downloading to ${downloadPath}...\n`));
95
+
96
+ for (let i = 0; i < images.length; i++) {
97
+ try {
98
+ const filename = `${query.replace(/\s+/g, '-')}-${i + 1}.jpg`;
99
+ await service.downloadImage(images[i].url, filename, { directory: downloadPath });
100
+ console.log(chalk.green(` ✓ ${filename}`));
101
+ } catch (error) {
102
+ console.log(chalk.red(` ✗ Failed: ${error.message}`));
103
+ }
104
+ }
105
+
106
+ console.log(chalk.green(`\n ✅ Download complete!\n`));
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Replace placeholder images in project
112
+ */
113
+ async function replacePlaceholders(projectPath, options = {}) {
114
+ console.log(chalk.cyan(`
115
+ ╭────────────────────────────────────────────────────────────────────╮
116
+ │ 🔄 REPLACING PLACEHOLDER IMAGES │
117
+ ╰────────────────────────────────────────────────────────────────────╯
118
+ `));
119
+
120
+ const service = createImageService({ verbose: true });
121
+ const result = await service.replacePlaceholders(projectPath);
122
+
123
+ if (result.replacedCount === 0) {
124
+ console.log(chalk.yellow(' No placeholder images found to replace.\n'));
125
+ } else {
126
+ console.log(chalk.green(` ✅ Replaced ${result.replacedCount} placeholder images!\n`));
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Generate hero image for project
132
+ */
133
+ async function generateHeroImage(projectPath, options = {}) {
134
+ console.log(chalk.cyan(`
135
+ ╭────────────────────────────────────────────────────────────────────╮
136
+ │ 🖼️ GENERATING HERO IMAGE │
137
+ ╰────────────────────────────────────────────────────────────────────╯
138
+ `));
139
+
140
+ const theme = options.theme || 'tech';
141
+ const result = await generateImages(projectPath, 'web', {
142
+ hero: true,
143
+ products: 0,
144
+ team: 0,
145
+ testimonials: 0,
146
+ theme
147
+ });
148
+
149
+ if (result.downloaded.length > 0) {
150
+ console.log(chalk.green(` ✅ Hero image generated!`));
151
+ console.log(chalk.gray(` Path: public/images/${result.downloaded[0].filename}\n`));
152
+ } else {
153
+ console.log(chalk.red(` ❌ Failed to generate hero image.\n`));
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Generate product images
159
+ */
160
+ async function generateProductImages(projectPath, count, options = {}) {
161
+ console.log(chalk.cyan(`
162
+ ╭────────────────────────────────────────────────────────────────────╮
163
+ │ 📦 GENERATING PRODUCT IMAGES │
164
+ ╰────────────────────────────────────────────────────────────────────╯
165
+ `));
166
+
167
+ const theme = options.theme || 'tech';
168
+ const result = await generateImages(projectPath, 'ecommerce', {
169
+ hero: false,
170
+ products: count,
171
+ team: 0,
172
+ testimonials: 0,
173
+ theme
174
+ });
175
+
176
+ const productCount = result.downloaded.filter(d => d.type === 'product').length;
177
+ if (productCount > 0) {
178
+ console.log(chalk.green(` ✅ ${productCount} product images generated!`));
179
+ console.log(chalk.gray(` Path: public/images/\n`));
180
+ } else {
181
+ console.log(chalk.red(` ❌ Failed to generate product images.\n`));
182
+ }
183
+ }
184
+
185
+ /**
186
+ * Generate full image set
187
+ */
188
+ async function generateFullSet(projectPath, options = {}) {
189
+ console.log(chalk.cyan(`
190
+ ╭────────────────────────────────────────────────────────────────────╮
191
+ │ 📸 GENERATING FULL IMAGE SET │
192
+ ╰────────────────────────────────────────────────────────────────────╯
193
+ `));
194
+
195
+ const theme = options.theme || 'tech';
196
+
197
+ const result = await generateImages(projectPath, 'full', {
198
+ hero: true,
199
+ products: parseInt(options.productCount) || 6,
200
+ team: parseInt(options.teamCount) || 4,
201
+ testimonials: parseInt(options.testimonialCount) || 4,
202
+ theme
203
+ });
204
+
205
+ const total = result.downloaded.length;
206
+ if (total > 0) {
207
+ console.log(chalk.green(` ✅ Generated ${total} images!`));
208
+ console.log(chalk.gray(` Path: public/images/\n`));
209
+
210
+ // Summary
211
+ const byType = {};
212
+ for (const item of result.downloaded) {
213
+ byType[item.type] = (byType[item.type] || 0) + 1;
214
+ }
215
+
216
+ console.log(chalk.gray(' Summary:'));
217
+ for (const [type, count] of Object.entries(byType)) {
218
+ console.log(chalk.gray(` - ${type}: ${count}`));
219
+ }
220
+ console.log('');
221
+ } else {
222
+ console.log(chalk.red(` ❌ Failed to generate images.\n`));
223
+ }
224
+ }
225
+
226
+ /**
227
+ * Interactive mode
228
+ */
229
+ async function interactiveMode(projectPath, options = {}) {
230
+ console.log(chalk.cyan(`
231
+ ╭────────────────────────────────────────────────────────────────────╮
232
+ │ 📸 VIBECODE IMAGE GENERATOR │
233
+ │ │
234
+ │ Generate professional images for your project │
235
+ ╰────────────────────────────────────────────────────────────────────╯
236
+ `));
237
+
238
+ const { action } = await inquirer.prompt([{
239
+ type: 'list',
240
+ name: 'action',
241
+ message: 'What would you like to do?',
242
+ choices: [
243
+ { name: '🔍 Search for images', value: 'search' },
244
+ { name: '🖼️ Generate hero image', value: 'hero' },
245
+ { name: '📦 Generate product images', value: 'products' },
246
+ { name: '👥 Generate team photos', value: 'team' },
247
+ { name: '📸 Generate full image set', value: 'full' },
248
+ { name: '🔄 Replace placeholder images', value: 'replace' },
249
+ { name: '📋 Show curated collections', value: 'collections' },
250
+ { name: '👋 Exit', value: 'exit' }
251
+ ]
252
+ }]);
253
+
254
+ if (action === 'exit') {
255
+ return;
256
+ }
257
+
258
+ if (action === 'search') {
259
+ const { query } = await inquirer.prompt([{
260
+ type: 'input',
261
+ name: 'query',
262
+ message: 'Search query:',
263
+ default: 'tech hero'
264
+ }]);
265
+ return searchAndDisplay(query, options);
266
+ }
267
+
268
+ if (action === 'hero') {
269
+ const { theme } = await inquirer.prompt([{
270
+ type: 'list',
271
+ name: 'theme',
272
+ message: 'Select theme:',
273
+ choices: ['tech', 'business', 'creative', 'nature', 'abstract']
274
+ }]);
275
+ return generateHeroImage(projectPath, { ...options, theme });
276
+ }
277
+
278
+ if (action === 'products') {
279
+ const { count, theme } = await inquirer.prompt([
280
+ {
281
+ type: 'number',
282
+ name: 'count',
283
+ message: 'Number of product images:',
284
+ default: 6
285
+ },
286
+ {
287
+ type: 'list',
288
+ name: 'theme',
289
+ message: 'Product category:',
290
+ choices: ['tech', 'fashion', 'food', 'lifestyle']
291
+ }
292
+ ]);
293
+ return generateProductImages(projectPath, count, { ...options, theme });
294
+ }
295
+
296
+ if (action === 'team') {
297
+ const { count } = await inquirer.prompt([{
298
+ type: 'number',
299
+ name: 'count',
300
+ message: 'Number of team photos:',
301
+ default: 4
302
+ }]);
303
+
304
+ const result = await generateImages(projectPath, 'team', {
305
+ hero: false,
306
+ products: 0,
307
+ team: count,
308
+ testimonials: 0
309
+ });
310
+
311
+ console.log(chalk.green(` ✅ ${result.downloaded.length} team photos generated!\n`));
312
+ }
313
+
314
+ if (action === 'full') {
315
+ const { theme } = await inquirer.prompt([{
316
+ type: 'list',
317
+ name: 'theme',
318
+ message: 'Select theme:',
319
+ choices: ['tech', 'business', 'creative']
320
+ }]);
321
+ return generateFullSet(projectPath, { ...options, theme });
322
+ }
323
+
324
+ if (action === 'replace') {
325
+ return replacePlaceholders(projectPath, options);
326
+ }
327
+
328
+ if (action === 'collections') {
329
+ showCollections();
330
+ }
331
+ }
332
+
333
+ /**
334
+ * Show available curated collections
335
+ */
336
+ function showCollections() {
337
+ console.log(chalk.cyan(`
338
+ ╭────────────────────────────────────────────────────────────────────╮
339
+ │ 📚 CURATED IMAGE COLLECTIONS │
340
+ ╰────────────────────────────────────────────────────────────────────╯
341
+ `));
342
+
343
+ console.log(chalk.white.bold(' Hero Images:'));
344
+ console.log(chalk.gray(' tech, business, creative, nature, abstract'));
345
+
346
+ console.log(chalk.white.bold('\n Product Images:'));
347
+ console.log(chalk.gray(' tech, fashion, food, lifestyle'));
348
+
349
+ console.log(chalk.white.bold('\n People:'));
350
+ console.log(chalk.gray(' team (8 photos), testimonials (6 avatars)'));
351
+
352
+ console.log(chalk.white.bold('\n Backgrounds:'));
353
+ console.log(chalk.gray(' gradient, pattern'));
354
+
355
+ console.log(chalk.gray(`
356
+ ─────────────────────────────────────────────────────────────────────
357
+
358
+ Usage:
359
+ ${chalk.cyan('vibecode images --search "tech hero"')} Search images
360
+ ${chalk.cyan('vibecode images --hero --theme tech')} Generate hero
361
+ ${chalk.cyan('vibecode images --products 6')} Generate products
362
+ ${chalk.cyan('vibecode images --generate')} Full image set
363
+ ${chalk.cyan('vibecode images --replace')} Replace placeholders
364
+
365
+ With go command:
366
+ ${chalk.cyan('vibecode go --template landing-saas --with-images')}
367
+ `));
368
+ }
369
+
370
+ /**
371
+ * List generated images in project
372
+ */
373
+ async function listImages(projectPath) {
374
+ const imagesDir = path.join(projectPath, 'public', 'images');
375
+
376
+ console.log(chalk.cyan(`
377
+ ╭────────────────────────────────────────────────────────────────────╮
378
+ │ 🖼️ GENERATED IMAGES │
379
+ ╰────────────────────────────────────────────────────────────────────╯
380
+ `));
381
+
382
+ try {
383
+ const fs = await import('fs/promises');
384
+ const files = await fs.readdir(imagesDir);
385
+ const images = files.filter(f => /\.(jpg|jpeg|png|webp|gif|svg)$/i.test(f));
386
+
387
+ if (images.length === 0) {
388
+ console.log(chalk.gray(' No images found.\n'));
389
+ console.log(chalk.gray(` Run ${chalk.cyan('vibecode images --generate')} to generate images.\n`));
390
+ return;
391
+ }
392
+
393
+ let totalSize = 0;
394
+ for (const img of images) {
395
+ const stats = await fs.stat(path.join(imagesDir, img));
396
+ const size = stats.size / 1024;
397
+ totalSize += size;
398
+
399
+ const sizeStr = size >= 1024
400
+ ? `${(size / 1024).toFixed(1)} MB`
401
+ : `${size.toFixed(1)} KB`;
402
+
403
+ const icon = img.includes('hero') ? '🖼️ ' :
404
+ img.includes('product') ? '📦' :
405
+ img.includes('team') ? '👤' :
406
+ img.includes('testimonial') ? '💬' :
407
+ img.includes('bg') ? '🎨' : '📸';
408
+
409
+ console.log(chalk.green(` ${icon} ${img.padEnd(35)} ${sizeStr.padStart(10)}`));
410
+ }
411
+
412
+ const totalStr = totalSize >= 1024
413
+ ? `${(totalSize / 1024).toFixed(1)} MB`
414
+ : `${totalSize.toFixed(1)} KB`;
415
+
416
+ console.log(chalk.gray(`\n ─────────────────────────────────────────────────────`));
417
+ console.log(chalk.white(` Total: ${images.length} images (${totalStr})`));
418
+ console.log(chalk.gray(` Location: ${imagesDir}\n`));
419
+
420
+ } catch (error) {
421
+ console.log(chalk.yellow(' No images directory found.\n'));
422
+ console.log(chalk.gray(` Run ${chalk.cyan('vibecode images --generate')} to generate images.\n`));
423
+ }
424
+ }
425
+
426
+ /**
427
+ * Auto-generate images for project (called from go.js)
428
+ */
429
+ export async function autoGenerateImages(projectPath, options = {}) {
430
+ const {
431
+ template,
432
+ theme = 'tech'
433
+ } = options;
434
+
435
+ // Determine what images to generate based on template
436
+ let imageConfig = {
437
+ hero: true,
438
+ products: 0,
439
+ team: 0,
440
+ testimonials: 0,
441
+ theme
442
+ };
443
+
444
+ if (template) {
445
+ if (template.includes('ecommerce') || template.includes('shop')) {
446
+ imageConfig.products = 8;
447
+ }
448
+ if (template.includes('saas') || template.includes('landing')) {
449
+ imageConfig.testimonials = 4;
450
+ }
451
+ if (template.includes('agency') || template.includes('portfolio')) {
452
+ imageConfig.team = 4;
453
+ }
454
+ if (template.includes('dashboard')) {
455
+ imageConfig.hero = false; // Dashboards don't need hero images
456
+ }
457
+ }
458
+
459
+ try {
460
+ return await generateImages(projectPath, template || 'web', imageConfig);
461
+ } catch (error) {
462
+ console.log(chalk.yellow(` ⚠ Image generation failed: ${error.message}`));
463
+ return { downloaded: [], failed: [{ error: error.message }] };
464
+ }
465
+ }