@djangocfg/imgai 1.0.1

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,981 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import path2 from 'path';
4
+ import { fileURLToPath } from 'url';
5
+ import dotenv from 'dotenv';
6
+ import chalk from 'chalk';
7
+ import ora from 'ora';
8
+ import inquirer from 'inquirer';
9
+ import fs from 'fs-extra';
10
+ import sharp from 'sharp';
11
+ import OpenAI from 'openai';
12
+ import Anthropic from '@anthropic-ai/sdk';
13
+ import { glob } from 'glob';
14
+
15
+ var ImageGenerator = class {
16
+ config;
17
+ openai;
18
+ anthropic;
19
+ constructor(config) {
20
+ this.config = config;
21
+ this.initializeProviders();
22
+ }
23
+ initializeProviders() {
24
+ const openaiKey = this.config.openaiApiKey || process.env.OPENAI_API_KEY;
25
+ const anthropicKey = this.config.anthropicApiKey || process.env.ANTHROPIC_API_KEY;
26
+ if (openaiKey) {
27
+ this.openai = new OpenAI({ apiKey: openaiKey });
28
+ }
29
+ if (anthropicKey) {
30
+ this.anthropic = new Anthropic({ apiKey: anthropicKey });
31
+ }
32
+ if (!this.openai && this.config.provider === "openai") {
33
+ throw new Error("OpenAI API key required. Set OPENAI_API_KEY or pass openaiApiKey in config.");
34
+ }
35
+ }
36
+ // ──────────────────────────────────────────────────────────────────────────
37
+ // SINGLE IMAGE GENERATION
38
+ // ──────────────────────────────────────────────────────────────────────────
39
+ async generate(options) {
40
+ const startTime = Date.now();
41
+ try {
42
+ const fullPrompt = this.buildPrompt(options.prompt);
43
+ const imageData = await this.generateWithOpenAI(fullPrompt);
44
+ const filename = options.filename || this.generateFilename(options.prompt);
45
+ const category = options.category || "general";
46
+ const outputDir = path2.join(
47
+ this.config.projectRoot,
48
+ this.config.outputDir,
49
+ category
50
+ );
51
+ await fs.ensureDir(outputDir);
52
+ const originalPath = path2.join(outputDir, `${filename}.png`);
53
+ await fs.writeFile(originalPath, Buffer.from(imageData, "base64"));
54
+ const resizeOptions = options.resize || this.config.resize;
55
+ let finalPath = originalPath;
56
+ if (resizeOptions) {
57
+ finalPath = await this.resizeImage(originalPath, resizeOptions);
58
+ if (finalPath !== originalPath) {
59
+ await fs.remove(originalPath);
60
+ }
61
+ }
62
+ const imageInfo = await this.getImageInfo(finalPath, category);
63
+ imageInfo.metadata = {
64
+ prompt: options.prompt,
65
+ caption: options.metadata?.caption,
66
+ tags: options.tags,
67
+ category,
68
+ generatedAt: /* @__PURE__ */ new Date(),
69
+ model: "dall-e-3"
70
+ };
71
+ const duration = Date.now() - startTime;
72
+ return {
73
+ success: true,
74
+ imagePath: finalPath,
75
+ imageUrl: imageInfo.url,
76
+ imageInfo,
77
+ duration
78
+ };
79
+ } catch (error) {
80
+ return {
81
+ success: false,
82
+ error: error instanceof Error ? error.message : "Unknown error",
83
+ duration: Date.now() - startTime
84
+ };
85
+ }
86
+ }
87
+ // ──────────────────────────────────────────────────────────────────────────
88
+ // BATCH GENERATION
89
+ // ──────────────────────────────────────────────────────────────────────────
90
+ async generateBatch(options) {
91
+ const startTime = Date.now();
92
+ const results = [];
93
+ const concurrency = options.concurrency || 2;
94
+ const delayBetween = options.delayBetween || 2e3;
95
+ for (let i = 0; i < options.items.length; i += concurrency) {
96
+ const batch = options.items.slice(i, i + concurrency);
97
+ const batchPromises = batch.map(async (item) => {
98
+ const result = await this.generate({
99
+ prompt: item.prompt,
100
+ filename: item.filename,
101
+ category: item.category
102
+ });
103
+ return result;
104
+ });
105
+ const batchResults = await Promise.all(batchPromises);
106
+ for (const result of batchResults) {
107
+ results.push(result);
108
+ if (options.onProgress) {
109
+ options.onProgress(results.length, options.items.length, result);
110
+ }
111
+ }
112
+ if (i + concurrency < options.items.length) {
113
+ await this.delay(delayBetween);
114
+ }
115
+ }
116
+ return {
117
+ total: results.length,
118
+ success: results.filter((r) => r.success).length,
119
+ failed: results.filter((r) => !r.success).length,
120
+ results,
121
+ duration: Date.now() - startTime
122
+ };
123
+ }
124
+ // ──────────────────────────────────────────────────────────────────────────
125
+ // PROMPT ENHANCEMENT (using Claude/GPT)
126
+ // ──────────────────────────────────────────────────────────────────────────
127
+ async enhancePrompt(basePrompt, context) {
128
+ const systemPrompt = `You are an expert at creating image generation prompts.
129
+ Given a description, create:
130
+ 1. A detailed visual prompt (max 150 words) for DALL-E 3
131
+ 2. A descriptive filename (lowercase, hyphens, max 30 chars)
132
+ 3. A short caption (max 50 words)
133
+
134
+ ${this.config.style ? `Style guide: ${this.config.style}` : ""}
135
+
136
+ Return ONLY valid JSON: {"prompt": "...", "filename": "...", "caption": "..."}`;
137
+ const userPrompt = context ? `Description: ${basePrompt}
138
+ Context: ${context}` : `Description: ${basePrompt}`;
139
+ if (this.anthropic && this.config.provider === "anthropic") {
140
+ const response = await this.anthropic.messages.create({
141
+ model: "claude-3-5-sonnet-20241022",
142
+ max_tokens: 500,
143
+ messages: [
144
+ { role: "user", content: `${systemPrompt}
145
+
146
+ ${userPrompt}` }
147
+ ]
148
+ });
149
+ const text = response.content[0].type === "text" ? response.content[0].text : "";
150
+ return JSON.parse(text);
151
+ }
152
+ if (this.openai) {
153
+ const response = await this.openai.chat.completions.create({
154
+ model: "gpt-4-turbo-preview",
155
+ messages: [
156
+ { role: "system", content: systemPrompt },
157
+ { role: "user", content: userPrompt }
158
+ ],
159
+ max_tokens: 500,
160
+ temperature: 0.7,
161
+ response_format: { type: "json_object" }
162
+ });
163
+ const text = response.choices[0]?.message?.content || "{}";
164
+ return JSON.parse(text);
165
+ }
166
+ throw new Error("No AI provider configured for prompt enhancement");
167
+ }
168
+ // ──────────────────────────────────────────────────────────────────────────
169
+ // PRIVATE METHODS
170
+ // ──────────────────────────────────────────────────────────────────────────
171
+ async generateWithOpenAI(prompt) {
172
+ if (!this.openai) {
173
+ throw new Error("OpenAI client not initialized");
174
+ }
175
+ const response = await this.openai.images.generate({
176
+ model: "dall-e-3",
177
+ prompt,
178
+ n: 1,
179
+ size: this.config.size,
180
+ quality: this.config.quality,
181
+ response_format: "b64_json"
182
+ });
183
+ const imageData = response.data?.[0]?.b64_json;
184
+ if (!imageData) {
185
+ throw new Error("No image data received from OpenAI");
186
+ }
187
+ return imageData;
188
+ }
189
+ async resizeImage(inputPath, options) {
190
+ const { width, height, quality = 85, format = "webp", fit = "inside" } = options;
191
+ let pipeline = sharp(inputPath);
192
+ if (width || height) {
193
+ pipeline = pipeline.resize(width, height, {
194
+ fit,
195
+ withoutEnlargement: true
196
+ });
197
+ }
198
+ const outputPath = inputPath.replace(/\.[^.]+$/, `.${format}`);
199
+ switch (format) {
200
+ case "webp":
201
+ pipeline = pipeline.webp({ quality });
202
+ break;
203
+ case "jpeg":
204
+ pipeline = pipeline.jpeg({ quality });
205
+ break;
206
+ case "png":
207
+ pipeline = pipeline.png({ quality });
208
+ break;
209
+ case "avif":
210
+ pipeline = pipeline.avif({ quality });
211
+ break;
212
+ }
213
+ await pipeline.toFile(outputPath);
214
+ return outputPath;
215
+ }
216
+ async getImageInfo(imagePath, category) {
217
+ const stats = await fs.stat(imagePath);
218
+ const metadata = await sharp(imagePath).metadata();
219
+ const filename = path2.basename(imagePath);
220
+ const extension = path2.extname(filename).slice(1);
221
+ const id = path2.basename(filename, path2.extname(filename));
222
+ const publicDir = path2.join(this.config.projectRoot, this.config.publicDir);
223
+ const relativePath = path2.relative(publicDir, imagePath);
224
+ const url = "/" + relativePath.replace(/\\/g, "/");
225
+ return {
226
+ id,
227
+ filename,
228
+ extension,
229
+ path: relativePath,
230
+ url,
231
+ size: stats.size,
232
+ width: metadata.width,
233
+ height: metadata.height,
234
+ createdAt: stats.birthtime,
235
+ modifiedAt: stats.mtime
236
+ };
237
+ }
238
+ buildPrompt(basePrompt) {
239
+ if (this.config.style) {
240
+ return `${this.config.style}. ${basePrompt}`;
241
+ }
242
+ return basePrompt;
243
+ }
244
+ generateFilename(prompt) {
245
+ return prompt.toLowerCase().replace(/[^a-z0-9\s]/g, "").replace(/\s+/g, "-").substring(0, 30);
246
+ }
247
+ delay(ms) {
248
+ return new Promise((resolve) => setTimeout(resolve, ms));
249
+ }
250
+ };
251
+ var DEFAULT_EXTENSIONS = ["png", "jpg", "jpeg", "webp", "avif", "gif", "svg"];
252
+ var DEFAULT_DIRECTORIES = ["static/images", "images", "assets"];
253
+ var ImageScanner = class {
254
+ config;
255
+ constructor(config) {
256
+ this.config = config;
257
+ }
258
+ // ──────────────────────────────────────────────────────────────────────────
259
+ // SCAN IMAGES
260
+ // ──────────────────────────────────────────────────────────────────────────
261
+ async scan(options = {}) {
262
+ const startTime = Date.now();
263
+ const {
264
+ directories = DEFAULT_DIRECTORIES,
265
+ extensions = DEFAULT_EXTENSIONS,
266
+ includeDimensions = true,
267
+ recursive = true
268
+ } = options;
269
+ const publicDir = path2.join(this.config.projectRoot, this.config.publicDir);
270
+ const images = [];
271
+ const byCategory = {};
272
+ const byExtension = {};
273
+ let totalSize = 0;
274
+ const patterns = directories.flatMap((dir) => {
275
+ const basePath = path2.join(publicDir, dir);
276
+ const extPattern = `*.{${extensions.join(",")}}`;
277
+ return recursive ? [path2.join(basePath, "**", extPattern)] : [path2.join(basePath, extPattern)];
278
+ });
279
+ for (const pattern of patterns) {
280
+ const files = await glob(pattern, { nodir: true });
281
+ for (const filePath of files) {
282
+ try {
283
+ const imageInfo = await this.processImage(filePath, publicDir, includeDimensions);
284
+ images.push(imageInfo);
285
+ totalSize += imageInfo.size;
286
+ byExtension[imageInfo.extension] = (byExtension[imageInfo.extension] || 0) + 1;
287
+ const category = this.extractCategory(imageInfo.path);
288
+ if (!byCategory[category]) {
289
+ byCategory[category] = [];
290
+ }
291
+ byCategory[category].push(imageInfo);
292
+ } catch (error) {
293
+ console.warn(`Failed to process image: ${filePath}`, error);
294
+ }
295
+ }
296
+ }
297
+ images.sort((a, b) => b.modifiedAt.getTime() - a.modifiedAt.getTime());
298
+ return {
299
+ images,
300
+ total: images.length,
301
+ byCategory,
302
+ byExtension,
303
+ totalSize,
304
+ scanDuration: Date.now() - startTime
305
+ };
306
+ }
307
+ // ──────────────────────────────────────────────────────────────────────────
308
+ // BUILD CATALOG
309
+ // ──────────────────────────────────────────────────────────────────────────
310
+ async buildCatalog(options = {}) {
311
+ const scanResult = await this.scan(options);
312
+ const byId = {};
313
+ for (const image of scanResult.images) {
314
+ byId[image.id] = image;
315
+ }
316
+ return {
317
+ version: "1.0.0",
318
+ generatedAt: /* @__PURE__ */ new Date(),
319
+ count: scanResult.total,
320
+ categories: scanResult.byCategory,
321
+ images: scanResult.images,
322
+ byId
323
+ };
324
+ }
325
+ // ──────────────────────────────────────────────────────────────────────────
326
+ // FIND SPECIFIC IMAGES
327
+ // ──────────────────────────────────────────────────────────────────────────
328
+ async findByCategory(category) {
329
+ const scanResult = await this.scan();
330
+ return scanResult.byCategory[category] || [];
331
+ }
332
+ async findById(id) {
333
+ const scanResult = await this.scan();
334
+ return scanResult.images.find((img) => img.id === id);
335
+ }
336
+ async findByExtension(extension) {
337
+ const scanResult = await this.scan();
338
+ return scanResult.images.filter((img) => img.extension === extension);
339
+ }
340
+ // ──────────────────────────────────────────────────────────────────────────
341
+ // PRIVATE METHODS
342
+ // ──────────────────────────────────────────────────────────────────────────
343
+ async processImage(filePath, publicDir, includeDimensions) {
344
+ const stats = await fs.stat(filePath);
345
+ const filename = path2.basename(filePath);
346
+ const extension = path2.extname(filename).slice(1).toLowerCase();
347
+ const id = path2.basename(filename, path2.extname(filename));
348
+ const relativePath = path2.relative(publicDir, filePath).replace(/\\/g, "/");
349
+ const url = "/" + relativePath;
350
+ let width;
351
+ let height;
352
+ if (includeDimensions && extension !== "svg") {
353
+ try {
354
+ const metadata = await sharp(filePath).metadata();
355
+ width = metadata.width;
356
+ height = metadata.height;
357
+ } catch {
358
+ }
359
+ }
360
+ return {
361
+ id,
362
+ filename,
363
+ extension,
364
+ path: relativePath,
365
+ url,
366
+ size: stats.size,
367
+ width,
368
+ height,
369
+ createdAt: stats.birthtime,
370
+ modifiedAt: stats.mtime
371
+ };
372
+ }
373
+ extractCategory(imagePath) {
374
+ const parts = imagePath.split("/");
375
+ if (parts.length > 1) {
376
+ return parts[parts.length - 2];
377
+ }
378
+ return "root";
379
+ }
380
+ };
381
+ function formatBytes(bytes) {
382
+ if (bytes === 0) return "0 B";
383
+ const k = 1024;
384
+ const sizes = ["B", "KB", "MB", "GB"];
385
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
386
+ return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
387
+ }
388
+ function formatDuration(ms) {
389
+ if (ms < 1e3) return `${ms}ms`;
390
+ return `${(ms / 1e3).toFixed(2)}s`;
391
+ }
392
+ var ConfigGenerator = class {
393
+ config;
394
+ scanner;
395
+ constructor(config) {
396
+ this.config = config;
397
+ this.scanner = new ImageScanner(config);
398
+ }
399
+ // ──────────────────────────────────────────────────────────────────────────
400
+ // GENERATE IMAGES.TS
401
+ // ──────────────────────────────────────────────────────────────────────────
402
+ async generate() {
403
+ const catalog = await this.scanner.buildCatalog();
404
+ const outputPath = path2.join(this.config.projectRoot, this.config.configOutputPath);
405
+ await fs.ensureDir(path2.dirname(outputPath));
406
+ const content = this.generateTypeScript(catalog);
407
+ await fs.writeFile(outputPath, content, "utf-8");
408
+ return outputPath;
409
+ }
410
+ // ──────────────────────────────────────────────────────────────────────────
411
+ // TYPESCRIPT GENERATION
412
+ // ──────────────────────────────────────────────────────────────────────────
413
+ generateTypeScript(catalog) {
414
+ const categories = Object.keys(catalog.categories).sort();
415
+ const allIds = catalog.images.map((img) => img.id).sort();
416
+ return `/**
417
+ * Auto-generated image catalog
418
+ * Generated: ${catalog.generatedAt.toISOString()}
419
+ * Total images: ${catalog.count}
420
+ *
421
+ * DO NOT EDIT - This file is auto-generated by @djangocfg/imgai
422
+ * Run \`imgai sync\` to regenerate
423
+ */
424
+
425
+ // ============================================================================
426
+ // TYPES
427
+ // ============================================================================
428
+
429
+ export interface ImageData {
430
+ id: string;
431
+ filename: string;
432
+ url: string;
433
+ path: string;
434
+ extension: string;
435
+ width?: number;
436
+ height?: number;
437
+ size: number;
438
+ }
439
+
440
+ export type ImageId = ${allIds.length > 0 ? allIds.map((id) => `'${id}'`).join(" | ") : "string"};
441
+ export type ImageCategory = ${categories.length > 0 ? categories.map((c) => `'${c}'`).join(" | ") : "string"};
442
+
443
+ // ============================================================================
444
+ // IMAGE CATALOG
445
+ // ============================================================================
446
+
447
+ export const IMAGES: Record<ImageId, ImageData> = {
448
+ ${catalog.images.map((img) => this.generateImageEntry(img)).join(",\n")}
449
+ } as const;
450
+
451
+ // ============================================================================
452
+ // BY CATEGORY
453
+ // ============================================================================
454
+
455
+ export const IMAGES_BY_CATEGORY: Record<ImageCategory, readonly ImageData[]> = {
456
+ ${categories.map((cat) => this.generateCategoryEntry(cat, catalog.categories[cat] || [])).join(",\n")}
457
+ } as const;
458
+
459
+ // ============================================================================
460
+ // HELPERS
461
+ // ============================================================================
462
+
463
+ /** Get image by ID */
464
+ export function getImage(id: ImageId): ImageData | undefined {
465
+ return IMAGES[id];
466
+ }
467
+
468
+ /** Get image URL by ID */
469
+ export function getImageUrl(id: ImageId): string | undefined {
470
+ return IMAGES[id]?.url;
471
+ }
472
+
473
+ /** Get all images in category */
474
+ export function getImagesByCategory(category: ImageCategory): readonly ImageData[] {
475
+ return IMAGES_BY_CATEGORY[category] || [];
476
+ }
477
+
478
+ /** Get all image IDs */
479
+ export function getAllImageIds(): ImageId[] {
480
+ return Object.keys(IMAGES) as ImageId[];
481
+ }
482
+
483
+ /** Get all categories */
484
+ export function getAllCategories(): ImageCategory[] {
485
+ return Object.keys(IMAGES_BY_CATEGORY) as ImageCategory[];
486
+ }
487
+
488
+ /** Check if image exists */
489
+ export function hasImage(id: string): id is ImageId {
490
+ return id in IMAGES;
491
+ }
492
+
493
+ // ============================================================================
494
+ // METADATA
495
+ // ============================================================================
496
+
497
+ export const IMAGE_CATALOG_META = {
498
+ version: '${catalog.version}',
499
+ generatedAt: '${catalog.generatedAt.toISOString()}',
500
+ totalImages: ${catalog.count},
501
+ categories: ${JSON.stringify(categories)},
502
+ } as const;
503
+ `;
504
+ }
505
+ generateImageEntry(img) {
506
+ const data = {
507
+ id: img.id,
508
+ filename: img.filename,
509
+ url: img.url,
510
+ path: img.path,
511
+ extension: img.extension,
512
+ size: img.size
513
+ };
514
+ if (img.width) data.width = img.width;
515
+ if (img.height) data.height = img.height;
516
+ return ` '${img.id}': ${JSON.stringify(data, null, 4).replace(/\n/g, "\n ").replace(/}$/, " }")}`;
517
+ }
518
+ generateCategoryEntry(category, images) {
519
+ const imageRefs = images.map((img) => `IMAGES['${img.id}']`);
520
+ return ` '${category}': [${imageRefs.join(", ")}]`;
521
+ }
522
+ };
523
+
524
+ // src/cli/index.ts
525
+ var ImageAICLI = class {
526
+ config;
527
+ generator;
528
+ scanner;
529
+ configGenerator;
530
+ constructor(config) {
531
+ this.config = config;
532
+ this.generator = new ImageGenerator(config);
533
+ this.scanner = new ImageScanner(config);
534
+ this.configGenerator = new ConfigGenerator(config);
535
+ }
536
+ // ──────────────────────────────────────────────────────────────────────────
537
+ // INTERACTIVE MODE
538
+ // ──────────────────────────────────────────────────────────────────────────
539
+ async interactive() {
540
+ this.printHeader();
541
+ while (true) {
542
+ try {
543
+ const { action } = await inquirer.prompt([
544
+ {
545
+ type: "list",
546
+ name: "action",
547
+ message: chalk.cyan("What would you like to do?"),
548
+ choices: [
549
+ { name: `${chalk.green("\u{1F3A8}")} Generate image from prompt`, value: "generate" },
550
+ { name: `${chalk.blue("\u2728")} Generate with AI-enhanced prompt`, value: "enhance" },
551
+ { name: `${chalk.yellow("\u{1F4E6}")} Batch generate images`, value: "batch" },
552
+ new inquirer.Separator(),
553
+ { name: `${chalk.magenta("\u{1F50D}")} Scan & catalog images`, value: "scan" },
554
+ { name: `${chalk.cyan("\u26A1")} Sync images.ts config`, value: "sync" },
555
+ { name: `${chalk.white("\u{1F4CA}")} Show statistics`, value: "stats" },
556
+ new inquirer.Separator(),
557
+ { name: `${chalk.gray("\u2699\uFE0F")} Settings`, value: "settings" },
558
+ { name: `${chalk.red("\u{1F6AA}")} Exit`, value: "exit" }
559
+ ]
560
+ }
561
+ ]);
562
+ switch (action) {
563
+ case "generate":
564
+ await this.promptGenerate();
565
+ break;
566
+ case "enhance":
567
+ await this.promptEnhance();
568
+ break;
569
+ case "batch":
570
+ await this.promptBatch();
571
+ break;
572
+ case "scan":
573
+ await this.runScan();
574
+ break;
575
+ case "sync":
576
+ await this.runSync();
577
+ break;
578
+ case "stats":
579
+ await this.showStats();
580
+ break;
581
+ case "settings":
582
+ await this.showSettings();
583
+ break;
584
+ case "exit":
585
+ console.log(chalk.cyan("\n\u{1F44B} Goodbye!\n"));
586
+ return;
587
+ }
588
+ console.log("");
589
+ } catch (error) {
590
+ if (error instanceof Error && error.name === "ExitPromptError") {
591
+ console.log(chalk.cyan("\n\n\u{1F44B} Goodbye!\n"));
592
+ return;
593
+ }
594
+ throw error;
595
+ }
596
+ }
597
+ }
598
+ // ──────────────────────────────────────────────────────────────────────────
599
+ // GENERATE COMMANDS
600
+ // ──────────────────────────────────────────────────────────────────────────
601
+ async generate(options) {
602
+ const spinner = ora("Generating image...").start();
603
+ try {
604
+ const result = await this.generator.generate(options);
605
+ if (result.success) {
606
+ spinner.succeed(chalk.green("Image generated successfully!"));
607
+ console.log(chalk.gray(` Path: ${result.imagePath}`));
608
+ console.log(chalk.gray(` URL: ${result.imageUrl}`));
609
+ console.log(chalk.gray(` Time: ${formatDuration(result.duration || 0)}`));
610
+ } else {
611
+ spinner.fail(chalk.red(`Generation failed: ${result.error}`));
612
+ }
613
+ } catch (error) {
614
+ spinner.fail(chalk.red(`Error: ${error instanceof Error ? error.message : "Unknown error"}`));
615
+ }
616
+ }
617
+ async scan(options = {}) {
618
+ const spinner = ora("Scanning images...").start();
619
+ try {
620
+ const result = await this.scanner.scan(options);
621
+ spinner.succeed(chalk.green(`Found ${result.total} images`));
622
+ console.log(chalk.gray(` Total size: ${formatBytes(result.totalSize)}`));
623
+ console.log(chalk.gray(` Scan time: ${formatDuration(result.scanDuration)}`));
624
+ console.log(chalk.cyan("\n Categories:"));
625
+ for (const [category, images] of Object.entries(result.byCategory)) {
626
+ console.log(chalk.gray(` ${category}: ${images.length} images`));
627
+ }
628
+ console.log(chalk.cyan("\n Extensions:"));
629
+ for (const [ext, count] of Object.entries(result.byExtension)) {
630
+ console.log(chalk.gray(` .${ext}: ${count} files`));
631
+ }
632
+ } catch (error) {
633
+ spinner.fail(chalk.red(`Error: ${error instanceof Error ? error.message : "Unknown error"}`));
634
+ }
635
+ }
636
+ async sync() {
637
+ const spinner = ora("Syncing images.ts config...").start();
638
+ try {
639
+ const outputPath = await this.configGenerator.generate();
640
+ spinner.succeed(chalk.green("Config synced successfully!"));
641
+ console.log(chalk.gray(` Output: ${outputPath}`));
642
+ } catch (error) {
643
+ spinner.fail(chalk.red(`Error: ${error instanceof Error ? error.message : "Unknown error"}`));
644
+ }
645
+ }
646
+ // ──────────────────────────────────────────────────────────────────────────
647
+ // PRIVATE PROMPT METHODS
648
+ // ──────────────────────────────────────────────────────────────────────────
649
+ async promptGenerate() {
650
+ const answers = await inquirer.prompt([
651
+ {
652
+ type: "input",
653
+ name: "prompt",
654
+ message: "Enter image description:",
655
+ validate: (input) => input.trim() ? true : "Description is required"
656
+ },
657
+ {
658
+ type: "input",
659
+ name: "filename",
660
+ message: "Filename (optional, auto-generated if empty):"
661
+ },
662
+ {
663
+ type: "input",
664
+ name: "category",
665
+ message: "Category/folder (default: general):",
666
+ default: "general"
667
+ }
668
+ ]);
669
+ await this.generate({
670
+ prompt: answers.prompt,
671
+ filename: answers.filename || void 0,
672
+ category: answers.category
673
+ });
674
+ }
675
+ async promptEnhance() {
676
+ const answers = await inquirer.prompt([
677
+ {
678
+ type: "input",
679
+ name: "description",
680
+ message: "Describe what you want:",
681
+ validate: (input) => input.trim() ? true : "Description is required"
682
+ },
683
+ {
684
+ type: "input",
685
+ name: "context",
686
+ message: "Additional context (optional):"
687
+ },
688
+ {
689
+ type: "input",
690
+ name: "category",
691
+ message: "Category/folder (default: general):",
692
+ default: "general"
693
+ }
694
+ ]);
695
+ const spinner = ora("Enhancing prompt with AI...").start();
696
+ try {
697
+ const enhanced = await this.generator.enhancePrompt(
698
+ answers.description,
699
+ answers.context || void 0
700
+ );
701
+ spinner.succeed("Prompt enhanced!");
702
+ console.log(chalk.cyan("\n Enhanced prompt:"));
703
+ console.log(chalk.gray(` ${enhanced.prompt}
704
+ `));
705
+ console.log(chalk.gray(` Filename: ${enhanced.filename}`));
706
+ console.log(chalk.gray(` Caption: ${enhanced.caption}
707
+ `));
708
+ const { confirm } = await inquirer.prompt([
709
+ {
710
+ type: "confirm",
711
+ name: "confirm",
712
+ message: "Generate image with this prompt?",
713
+ default: true
714
+ }
715
+ ]);
716
+ if (confirm) {
717
+ await this.generate({
718
+ prompt: enhanced.prompt,
719
+ filename: enhanced.filename,
720
+ category: answers.category,
721
+ metadata: { caption: enhanced.caption }
722
+ });
723
+ }
724
+ } catch (error) {
725
+ spinner.fail(chalk.red(`Error: ${error instanceof Error ? error.message : "Unknown error"}`));
726
+ }
727
+ }
728
+ async promptBatch() {
729
+ console.log(chalk.cyan("\n Enter prompts (one per line, empty line to finish):"));
730
+ const prompts = [];
731
+ let lineNumber = 1;
732
+ while (true) {
733
+ const { prompt } = await inquirer.prompt([
734
+ {
735
+ type: "input",
736
+ name: "prompt",
737
+ message: ` ${lineNumber}.`
738
+ }
739
+ ]);
740
+ if (!prompt.trim()) break;
741
+ prompts.push(prompt);
742
+ lineNumber++;
743
+ }
744
+ if (prompts.length === 0) {
745
+ console.log(chalk.yellow(" No prompts entered."));
746
+ return;
747
+ }
748
+ const { category, concurrency } = await inquirer.prompt([
749
+ {
750
+ type: "input",
751
+ name: "category",
752
+ message: "Category for all images:",
753
+ default: "batch"
754
+ },
755
+ {
756
+ type: "list",
757
+ name: "concurrency",
758
+ message: "Concurrent workers:",
759
+ choices: [
760
+ { name: "1 (slow, safe)", value: 1 },
761
+ { name: "2 (balanced)", value: 2 },
762
+ { name: "3 (fast)", value: 3 }
763
+ ],
764
+ default: 2
765
+ }
766
+ ]);
767
+ console.log(chalk.cyan(`
768
+ Generating ${prompts.length} images...
769
+ `));
770
+ const result = await this.generator.generateBatch({
771
+ items: prompts.map((prompt, i) => ({
772
+ prompt,
773
+ category,
774
+ filename: `batch-${Date.now()}-${i + 1}`
775
+ })),
776
+ concurrency,
777
+ onProgress: (current, total, res) => {
778
+ const status = res.success ? chalk.green("\u2713") : chalk.red("\u2717");
779
+ console.log(` ${status} ${current}/${total}`);
780
+ }
781
+ });
782
+ console.log(chalk.cyan("\n Batch complete!"));
783
+ console.log(chalk.gray(` Success: ${result.success}`));
784
+ console.log(chalk.gray(` Failed: ${result.failed}`));
785
+ console.log(chalk.gray(` Time: ${formatDuration(result.duration)}`));
786
+ }
787
+ async runScan() {
788
+ await this.scan();
789
+ }
790
+ async runSync() {
791
+ await this.sync();
792
+ }
793
+ async showStats() {
794
+ const spinner = ora("Gathering statistics...").start();
795
+ try {
796
+ const result = await this.scanner.scan();
797
+ spinner.stop();
798
+ console.log(chalk.cyan("\n \u{1F4CA} Image Statistics\n"));
799
+ console.log(chalk.white(` Total images: ${result.total}`));
800
+ console.log(chalk.white(` Total size: ${formatBytes(result.totalSize)}`));
801
+ console.log(chalk.white(` Categories: ${Object.keys(result.byCategory).length}`));
802
+ console.log(chalk.cyan("\n By Category:"));
803
+ for (const [category, images] of Object.entries(result.byCategory).sort((a, b) => b[1].length - a[1].length)) {
804
+ const size = images.reduce((sum, img) => sum + img.size, 0);
805
+ console.log(chalk.gray(` ${category.padEnd(20)} ${images.length.toString().padStart(4)} images ${formatBytes(size).padStart(10)}`));
806
+ }
807
+ console.log(chalk.cyan("\n By Extension:"));
808
+ for (const [ext, count] of Object.entries(result.byExtension).sort((a, b) => b[1] - a[1])) {
809
+ console.log(chalk.gray(` .${ext.padEnd(6)} ${count} files`));
810
+ }
811
+ } catch (error) {
812
+ spinner.fail(chalk.red(`Error: ${error instanceof Error ? error.message : "Unknown error"}`));
813
+ }
814
+ }
815
+ async showSettings() {
816
+ console.log(chalk.cyan("\n \u2699\uFE0F Current Settings\n"));
817
+ console.log(chalk.gray(` Provider: ${this.config.provider}`));
818
+ console.log(chalk.gray(` Size: ${this.config.size}`));
819
+ console.log(chalk.gray(` Quality: ${this.config.quality}`));
820
+ console.log(chalk.gray(` Output dir: ${this.config.outputDir}`));
821
+ console.log(chalk.gray(` Config path: ${this.config.configOutputPath}`));
822
+ if (this.config.resize) {
823
+ console.log(chalk.cyan("\n Resize Settings:"));
824
+ console.log(chalk.gray(` Width: ${this.config.resize.width || "auto"}`));
825
+ console.log(chalk.gray(` Height: ${this.config.resize.height || "auto"}`));
826
+ console.log(chalk.gray(` Format: ${this.config.resize.format}`));
827
+ console.log(chalk.gray(` Quality: ${this.config.resize.quality}`));
828
+ }
829
+ if (this.config.style) {
830
+ console.log(chalk.cyan("\n Style:"));
831
+ console.log(chalk.gray(` ${this.config.style.substring(0, 100)}...`));
832
+ }
833
+ }
834
+ // ──────────────────────────────────────────────────────────────────────────
835
+ // UI HELPERS
836
+ // ──────────────────────────────────────────────────────────────────────────
837
+ printHeader() {
838
+ console.log("");
839
+ console.log(chalk.cyan(" \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557"));
840
+ console.log(chalk.cyan(" \u2551") + chalk.white.bold(" \u{1F3A8} IMGAI - Image Generator ") + chalk.cyan("\u2551"));
841
+ console.log(chalk.cyan(" \u2551") + chalk.gray(" AI-powered image generation & sync ") + chalk.cyan("\u2551"));
842
+ console.log(chalk.cyan(" \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D"));
843
+ console.log("");
844
+ }
845
+ };
846
+
847
+ // src/cli/bin.ts
848
+ var __filename = fileURLToPath(import.meta.url);
849
+ var __dirname = path2.dirname(__filename);
850
+ dotenv.config();
851
+ dotenv.config({ path: path2.resolve(process.cwd(), ".env.local") });
852
+ dotenv.config({ path: path2.resolve(__dirname, "../../.env") });
853
+ var program = new Command();
854
+ function createConfig(options) {
855
+ const projectRoot = options.root || process.cwd();
856
+ return {
857
+ provider: options.provider || "openai",
858
+ openaiApiKey: process.env.OPENAI_API_KEY,
859
+ anthropicApiKey: process.env.ANTHROPIC_API_KEY,
860
+ size: options.size || "1792x1024",
861
+ quality: options.quality || "standard",
862
+ style: options.style,
863
+ outputDir: options.output || "public/static/images",
864
+ projectRoot,
865
+ srcDir: "src",
866
+ publicDir: "public",
867
+ configOutputPath: options.config || "src/core/images.ts",
868
+ resize: options.resize ? {
869
+ width: options.width,
870
+ height: options.height,
871
+ quality: options.quality || 85,
872
+ format: options.format || "webp",
873
+ fit: "inside"
874
+ } : void 0
875
+ };
876
+ }
877
+ program.name("imgai").description("AI-powered image generation & management for Next.js").version("1.0.0");
878
+ program.command("interactive", { isDefault: true }).alias("i").description("Start interactive mode").option("-r, --root <path>", "Project root directory").action(async (options) => {
879
+ const config = createConfig(options);
880
+ const cli = new ImageAICLI(config);
881
+ await cli.interactive();
882
+ });
883
+ program.command("generate <prompt>").alias("g").description("Generate image from prompt").option("-f, --filename <name>", "Output filename").option("-c, --category <name>", "Category/folder", "general").option("-r, --root <path>", "Project root directory").option("-o, --output <path>", "Output directory", "public/static/images").option("--size <size>", "Image size (1024x1024, 1792x1024, 1024x1792)", "1792x1024").option("--quality <quality>", "Image quality (standard, hd)", "standard").option("--style <style>", "Style prefix for prompts").option("--resize", "Enable resize").option("--width <pixels>", "Resize width", parseInt).option("--height <pixels>", "Resize height", parseInt).option("--format <format>", "Output format (webp, jpeg, png, avif)", "webp").action(async (prompt, options) => {
884
+ const config = createConfig(options);
885
+ const generator = new ImageGenerator(config);
886
+ const spinner = ora("Generating image...").start();
887
+ try {
888
+ const result = await generator.generate({
889
+ prompt,
890
+ filename: options.filename,
891
+ category: options.category
892
+ });
893
+ if (result.success) {
894
+ spinner.succeed(chalk.green("Image generated!"));
895
+ console.log(chalk.gray(` Path: ${result.imagePath}`));
896
+ console.log(chalk.gray(` URL: ${result.imageUrl}`));
897
+ } else {
898
+ spinner.fail(chalk.red(result.error));
899
+ }
900
+ } catch (error) {
901
+ spinner.fail(chalk.red(error instanceof Error ? error.message : "Unknown error"));
902
+ process.exit(1);
903
+ }
904
+ });
905
+ program.command("scan").alias("s").description("Scan and list all images").option("-r, --root <path>", "Project root directory").option("-d, --dirs <dirs>", "Directories to scan (comma-separated)", "static/images,images").option("--no-dimensions", "Skip reading image dimensions").action(async (options) => {
906
+ const config = createConfig(options);
907
+ const scanner = new ImageScanner(config);
908
+ const spinner = ora("Scanning images...").start();
909
+ try {
910
+ const result = await scanner.scan({
911
+ directories: options.dirs?.split(","),
912
+ includeDimensions: options.dimensions !== false
913
+ });
914
+ spinner.succeed(chalk.green(`Found ${result.total} images`));
915
+ console.log(chalk.gray(`
916
+ Total size: ${formatBytes(result.totalSize)}`));
917
+ console.log(chalk.gray(` Scan time: ${formatDuration(result.scanDuration)}`));
918
+ console.log(chalk.cyan("\n Categories:"));
919
+ for (const [category, images] of Object.entries(result.byCategory)) {
920
+ console.log(chalk.gray(` ${category}: ${images.length} images`));
921
+ }
922
+ console.log(chalk.cyan("\n Extensions:"));
923
+ for (const [ext, count] of Object.entries(result.byExtension)) {
924
+ console.log(chalk.gray(` .${ext}: ${count} files`));
925
+ }
926
+ } catch (error) {
927
+ spinner.fail(chalk.red(error instanceof Error ? error.message : "Unknown error"));
928
+ process.exit(1);
929
+ }
930
+ });
931
+ program.command("sync").description("Generate/update images.ts config file").option("-r, --root <path>", "Project root directory").option("-o, --output <path>", "Output path for images.ts", "src/core/images.ts").action(async (options) => {
932
+ const config = createConfig({ ...options, config: options.output });
933
+ const generator = new ConfigGenerator(config);
934
+ const spinner = ora("Generating images.ts...").start();
935
+ try {
936
+ const outputPath = await generator.generate();
937
+ spinner.succeed(chalk.green("Config synced!"));
938
+ console.log(chalk.gray(` Output: ${outputPath}`));
939
+ } catch (error) {
940
+ spinner.fail(chalk.red(error instanceof Error ? error.message : "Unknown error"));
941
+ process.exit(1);
942
+ }
943
+ });
944
+ program.command("batch").alias("b").description("Batch generate images from file").argument("<file>", "JSON file with prompts array").option("-r, --root <path>", "Project root directory").option("-c, --concurrency <num>", "Concurrent generations", "2").option("--category <name>", "Category for all images", "batch").action(async (file, options) => {
945
+ const config = createConfig(options);
946
+ const generator = new ImageGenerator(config);
947
+ const fs4 = await import('fs-extra');
948
+ const filePath = path2.resolve(process.cwd(), file);
949
+ if (!await fs4.pathExists(filePath)) {
950
+ console.error(chalk.red(`File not found: ${filePath}`));
951
+ process.exit(1);
952
+ }
953
+ const data = await fs4.readJson(filePath);
954
+ const prompts = Array.isArray(data) ? data : data.prompts;
955
+ if (!Array.isArray(prompts)) {
956
+ console.error(chalk.red("Invalid file format. Expected array or { prompts: [] }"));
957
+ process.exit(1);
958
+ }
959
+ console.log(chalk.cyan(`
960
+ Generating ${prompts.length} images...
961
+ `));
962
+ const result = await generator.generateBatch({
963
+ items: prompts.map((item, i) => ({
964
+ prompt: typeof item === "string" ? item : item.prompt,
965
+ filename: typeof item === "string" ? void 0 : item.filename,
966
+ category: options.category
967
+ })),
968
+ concurrency: parseInt(options.concurrency),
969
+ onProgress: (current, total, res) => {
970
+ const status = res.success ? chalk.green("\u2713") : chalk.red("\u2717");
971
+ console.log(` ${status} ${current}/${total} - ${res.imagePath || res.error}`);
972
+ }
973
+ });
974
+ console.log(chalk.cyan("\n Batch complete!"));
975
+ console.log(chalk.gray(` Success: ${result.success}`));
976
+ console.log(chalk.gray(` Failed: ${result.failed}`));
977
+ console.log(chalk.gray(` Time: ${formatDuration(result.duration)}`));
978
+ });
979
+ program.parse();
980
+ //# sourceMappingURL=bin.js.map
981
+ //# sourceMappingURL=bin.js.map