mediaguru 1.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.
- package/CHANGELOG.md +25 -0
- package/CODE_OF_CONDUCT.md +41 -0
- package/CONTRIBUTING.md +50 -0
- package/LICENSE +21 -0
- package/README.md +193 -0
- package/RELEASE.md +38 -0
- package/SECURITY.md +24 -0
- package/bin/mediaguru.js +2 -0
- package/dist/cli/interactive.d.ts +1 -0
- package/dist/cli/interactive.js +647 -0
- package/dist/core/batch/index.d.ts +16 -0
- package/dist/core/batch/index.js +66 -0
- package/dist/core/compress/index.d.ts +17 -0
- package/dist/core/compress/index.js +96 -0
- package/dist/core/config/index.d.ts +14 -0
- package/dist/core/config/index.js +56 -0
- package/dist/core/export/index.d.ts +12 -0
- package/dist/core/export/index.js +82 -0
- package/dist/core/image/index.d.ts +44 -0
- package/dist/core/image/index.js +206 -0
- package/dist/core/ocr/index.d.ts +14 -0
- package/dist/core/ocr/index.js +53 -0
- package/dist/core/pdf/index.d.ts +34 -0
- package/dist/core/pdf/index.js +121 -0
- package/dist/core/qr/index.d.ts +12 -0
- package/dist/core/qr/index.js +37 -0
- package/dist/core/screenshot/index.d.ts +12 -0
- package/dist/core/screenshot/index.js +46 -0
- package/dist/core/server/index.d.ts +5 -0
- package/dist/core/server/index.js +101 -0
- package/dist/core/text2img/index.d.ts +14 -0
- package/dist/core/text2img/index.js +64 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +429 -0
- package/dist/plugins/index.d.ts +41 -0
- package/dist/plugins/index.js +61 -0
- package/dist/tests/test.d.ts +1 -0
- package/dist/tests/test.js +108 -0
- package/dist/tests/test_playwright.d.ts +1 -0
- package/dist/tests/test_playwright.js +60 -0
- package/dist/utils/branding.d.ts +6 -0
- package/dist/utils/branding.js +21 -0
- package/dist/utils/file.d.ts +7 -0
- package/dist/utils/file.js +29 -0
- package/dist/utils/templates.d.ts +9 -0
- package/dist/utils/templates.js +347 -0
- package/mediaguru-1.0.0.tgz +0 -0
- package/package.json +51 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import { printHeader, printFooter } from './utils/branding.js';
|
|
7
|
+
import { launchInteractive } from './cli/interactive.js';
|
|
8
|
+
import { PdfEngine } from './core/pdf/index.js';
|
|
9
|
+
import { ImageEngine } from './core/image/index.js';
|
|
10
|
+
import { QrEngine } from './core/qr/index.js';
|
|
11
|
+
import { OcrEngine } from './core/ocr/index.js';
|
|
12
|
+
import { ScreenshotEngine } from './core/screenshot/index.js';
|
|
13
|
+
import { Text2ImgEngine } from './core/text2img/index.js';
|
|
14
|
+
import { CompressEngine } from './core/compress/index.js';
|
|
15
|
+
import { BatchEngine } from './core/batch/index.js';
|
|
16
|
+
import { ExportEngine } from './core/export/index.js';
|
|
17
|
+
import { ConfigManager } from './core/config/index.js';
|
|
18
|
+
import { PluginRegistry } from './plugins/index.js';
|
|
19
|
+
import { formatBytes } from './utils/file.js';
|
|
20
|
+
import { RestApiServer } from './core/server/index.js';
|
|
21
|
+
const program = new Command();
|
|
22
|
+
program
|
|
23
|
+
.name('mediaguru')
|
|
24
|
+
.description('MediaGuru CLI — Universal Document & Media Processing Toolkit')
|
|
25
|
+
.version('1.0.0');
|
|
26
|
+
// Trigger beforeCommand hooks
|
|
27
|
+
program.hook('preAction', async (thisCommand, actionCommand) => {
|
|
28
|
+
await PluginRegistry.triggerBeforeCommand(actionCommand.name(), actionCommand.args);
|
|
29
|
+
});
|
|
30
|
+
// Trigger afterCommand hooks and footer
|
|
31
|
+
program.hook('postAction', async (thisCommand, actionCommand) => {
|
|
32
|
+
await PluginRegistry.triggerAfterCommand(actionCommand.name(), actionCommand.args);
|
|
33
|
+
printFooter();
|
|
34
|
+
});
|
|
35
|
+
// PDF Command group
|
|
36
|
+
const pdf = program.command('pdf').description('PDF manipulation operations');
|
|
37
|
+
pdf
|
|
38
|
+
.command('merge')
|
|
39
|
+
.description('Merge multiple PDFs into a single file')
|
|
40
|
+
.argument('<files...>', 'Paths to PDF files')
|
|
41
|
+
.action(async (files) => {
|
|
42
|
+
const spinner = ora('Merging PDF documents...').start();
|
|
43
|
+
try {
|
|
44
|
+
const out = await PdfEngine.merge(files);
|
|
45
|
+
spinner.succeed(`PDF files merged successfully to: ${chalk.cyan(out)}`);
|
|
46
|
+
}
|
|
47
|
+
catch (e) {
|
|
48
|
+
spinner.fail(`Merge failed: ${e.message}`);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
pdf
|
|
52
|
+
.command('split')
|
|
53
|
+
.description('Split PDF into individual pages')
|
|
54
|
+
.argument('<file>', 'Path to PDF file')
|
|
55
|
+
.action(async (file) => {
|
|
56
|
+
const spinner = ora('Splitting PDF pages...').start();
|
|
57
|
+
try {
|
|
58
|
+
const pages = await PdfEngine.split(file);
|
|
59
|
+
spinner.succeed(`PDF split complete. Extracted ${pages.length} pages:`);
|
|
60
|
+
pages.forEach(p => console.log(` - ${chalk.cyan(p)}`));
|
|
61
|
+
}
|
|
62
|
+
catch (e) {
|
|
63
|
+
spinner.fail(`Split failed: ${e.message}`);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
pdf
|
|
67
|
+
.command('extract')
|
|
68
|
+
.description('Extract plain text from PDF')
|
|
69
|
+
.argument('<file>', 'Path to PDF file')
|
|
70
|
+
.action(async (file) => {
|
|
71
|
+
const spinner = ora('Extracting text from PDF...').start();
|
|
72
|
+
try {
|
|
73
|
+
const text = await PdfEngine.extractText(file);
|
|
74
|
+
spinner.succeed('Extraction completed. Text content:');
|
|
75
|
+
console.log(chalk.dim('\n' + '─'.repeat(40)));
|
|
76
|
+
console.log(text.trim());
|
|
77
|
+
console.log(chalk.dim('─'.repeat(40)));
|
|
78
|
+
}
|
|
79
|
+
catch (e) {
|
|
80
|
+
spinner.fail(`Extraction failed: ${e.message}`);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
pdf
|
|
84
|
+
.command('compress')
|
|
85
|
+
.description('Optimize and compress PDF document size')
|
|
86
|
+
.argument('<file>', 'Path to PDF file')
|
|
87
|
+
.action(async (file) => {
|
|
88
|
+
const spinner = ora('Compressing PDF stream...').start();
|
|
89
|
+
try {
|
|
90
|
+
const out = await PdfEngine.compress(file);
|
|
91
|
+
spinner.succeed(`PDF optimized and saved: ${chalk.cyan(out)}`);
|
|
92
|
+
}
|
|
93
|
+
catch (e) {
|
|
94
|
+
spinner.fail(`Compression failed: ${e.message}`);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
// Root level pdf default actions (markdown/HTML compile)
|
|
98
|
+
pdf
|
|
99
|
+
.argument('<file>', 'Markdown (.md) or HTML (.html) source document')
|
|
100
|
+
.action(async (file) => {
|
|
101
|
+
const ext = path.extname(file).toLowerCase();
|
|
102
|
+
const spinner = ora('Compiling document to PDF...').start();
|
|
103
|
+
try {
|
|
104
|
+
let out = '';
|
|
105
|
+
if (ext === '.md') {
|
|
106
|
+
out = await PdfEngine.markdownToPdf(file);
|
|
107
|
+
spinner.succeed(`Markdown compiled: ${chalk.cyan(out)}`);
|
|
108
|
+
}
|
|
109
|
+
else if (ext === '.html' || ext === '.htm') {
|
|
110
|
+
out = await PdfEngine.htmlToPdf(file);
|
|
111
|
+
spinner.succeed(`HTML compiled: ${chalk.cyan(out)}`);
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
spinner.fail(`Unsupported file type: ${ext}. Please supply .md or .html`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
catch (e) {
|
|
118
|
+
spinner.fail(`Compilation failed: ${e.message}`);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
// Image Command group
|
|
122
|
+
const image = program.command('image').description('Image manipulation operations');
|
|
123
|
+
image
|
|
124
|
+
.command('resize')
|
|
125
|
+
.description('Resize image dimensions')
|
|
126
|
+
.argument('<file>', 'Path to image file')
|
|
127
|
+
.argument('<size>', 'Resolution dimensions (e.g. 800x600 or 800)')
|
|
128
|
+
.action(async (file, size) => {
|
|
129
|
+
const spinner = ora('Resizing image layout...').start();
|
|
130
|
+
try {
|
|
131
|
+
const out = await ImageEngine.resize(file, size);
|
|
132
|
+
spinner.succeed(`Image resized: ${chalk.cyan(out)}`);
|
|
133
|
+
}
|
|
134
|
+
catch (e) {
|
|
135
|
+
spinner.fail(`Resize failed: ${e.message}`);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
image
|
|
139
|
+
.command('convert')
|
|
140
|
+
.description('Convert image format')
|
|
141
|
+
.argument('<file>', 'Path to image file')
|
|
142
|
+
.argument('<format>', 'Target format (webp, png, jpg, jpeg)')
|
|
143
|
+
.action(async (file, format) => {
|
|
144
|
+
const spinner = ora(`Converting format to ${format}...`).start();
|
|
145
|
+
try {
|
|
146
|
+
const out = await ImageEngine.convert(file, format);
|
|
147
|
+
spinner.succeed(`Image converted: ${chalk.cyan(out)}`);
|
|
148
|
+
}
|
|
149
|
+
catch (e) {
|
|
150
|
+
spinner.fail(`Conversion failed: ${e.message}`);
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
image
|
|
154
|
+
.command('watermark')
|
|
155
|
+
.description('Overlay a watermark logo on top of an image')
|
|
156
|
+
.argument('<file>', 'Path to main image file')
|
|
157
|
+
.argument('<watermark>', 'Path to watermark logo image')
|
|
158
|
+
.action(async (file, watermark) => {
|
|
159
|
+
const spinner = ora('Overlaying watermark composite...').start();
|
|
160
|
+
try {
|
|
161
|
+
const out = await ImageEngine.watermark(file, watermark);
|
|
162
|
+
spinner.succeed(`Watermark composite applied: ${chalk.cyan(out)}`);
|
|
163
|
+
}
|
|
164
|
+
catch (e) {
|
|
165
|
+
spinner.fail(`Watermarking failed: ${e.message}`);
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
image
|
|
169
|
+
.command('remove-bg')
|
|
170
|
+
.description('Remove background from image')
|
|
171
|
+
.argument('<file>', 'Path to image file')
|
|
172
|
+
.option('--api', 'Force use API background removal engine (requires remove.bg API key)')
|
|
173
|
+
.action(async (file, options) => {
|
|
174
|
+
const engineType = options.api ? 'api' : 'local';
|
|
175
|
+
const spinner = ora(`Removing background (engine: ${engineType})...`).start();
|
|
176
|
+
try {
|
|
177
|
+
const out = await ImageEngine.removeBg(file, engineType);
|
|
178
|
+
spinner.succeed(`Background removed: ${chalk.cyan(out)}`);
|
|
179
|
+
}
|
|
180
|
+
catch (e) {
|
|
181
|
+
spinner.fail(`Background removal failed: ${e.message}`);
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
// QR Generator Command
|
|
185
|
+
program
|
|
186
|
+
.command('qr')
|
|
187
|
+
.description('Generate QR Code from text or url')
|
|
188
|
+
.argument('<text>', 'URL or text to encode')
|
|
189
|
+
.option('--svg', 'Output SVG vector path')
|
|
190
|
+
.option('--size <number>', 'Resolution size in px', '300')
|
|
191
|
+
.action(async (text, options) => {
|
|
192
|
+
const spinner = ora('Generating QR Code...').start();
|
|
193
|
+
try {
|
|
194
|
+
const sizeVal = parseInt(options.size, 10) || 300;
|
|
195
|
+
const out = await QrEngine.generate(text, { svg: options.svg, size: sizeVal });
|
|
196
|
+
spinner.succeed(`QR Code saved: ${chalk.cyan(out)}`);
|
|
197
|
+
}
|
|
198
|
+
catch (e) {
|
|
199
|
+
spinner.fail(`Generation failed: ${e.message}`);
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
// OCR Command
|
|
203
|
+
program
|
|
204
|
+
.command('ocr')
|
|
205
|
+
.description('Extract text from image (OCR)')
|
|
206
|
+
.argument('<file>', 'Path to image file')
|
|
207
|
+
.option('--export <format>', 'Export result to file format (txt, markdown, json)')
|
|
208
|
+
.action(async (file, options) => {
|
|
209
|
+
const spinner = ora('Parsing image text (OCR)...').start();
|
|
210
|
+
try {
|
|
211
|
+
const exportFormat = options.export ? options.export.toLowerCase() : undefined;
|
|
212
|
+
const res = await OcrEngine.extract(file, { exportFormat });
|
|
213
|
+
spinner.succeed('OCR Extraction completed. Result:');
|
|
214
|
+
console.log(chalk.dim('\n' + '─'.repeat(40)));
|
|
215
|
+
console.log(res.text.trim());
|
|
216
|
+
console.log(chalk.dim('─'.repeat(40)));
|
|
217
|
+
if (res.exportedPath) {
|
|
218
|
+
console.log(`\nExport file written: ${chalk.cyan(res.exportedPath)}`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
catch (e) {
|
|
222
|
+
spinner.fail(`OCR Extraction failed: ${e.message}`);
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
// Website Screenshot Command
|
|
226
|
+
program
|
|
227
|
+
.command('screenshot')
|
|
228
|
+
.description('Capture responsive website screenshot')
|
|
229
|
+
.argument('<url>', 'Webpage URL')
|
|
230
|
+
.option('--full-page', 'Capture full scroll height')
|
|
231
|
+
.option('--mobile', 'Emulate high-end mobile layout')
|
|
232
|
+
.action(async (url, options) => {
|
|
233
|
+
const spinner = ora('Rendering website screenshot...').start();
|
|
234
|
+
try {
|
|
235
|
+
const out = await ScreenshotEngine.capture(url, {
|
|
236
|
+
fullPage: options.fullPage,
|
|
237
|
+
mobile: options.mobile,
|
|
238
|
+
});
|
|
239
|
+
spinner.succeed(`Website screenshot captured: ${chalk.cyan(out)}`);
|
|
240
|
+
}
|
|
241
|
+
catch (e) {
|
|
242
|
+
spinner.fail(`Screenshot failed: ${e.message}`);
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
// Text-to-Image (Social Card) Command
|
|
246
|
+
program
|
|
247
|
+
.command('text2img')
|
|
248
|
+
.description('Generate stylized social announcement cards or banner images')
|
|
249
|
+
.argument('<text>', 'Card text body')
|
|
250
|
+
.option('--theme <type>', 'Visual color theme (dark, light, glass)', 'dark')
|
|
251
|
+
.option('--type <layout>', 'Aspect ratio structure (social, quote, poster, banner)', 'social')
|
|
252
|
+
.option('--title <string>', 'Optional category/header title')
|
|
253
|
+
.option('--author <string>', 'Optional author credits')
|
|
254
|
+
.action(async (text, options) => {
|
|
255
|
+
const spinner = ora('Rendering custom card template in browser...').start();
|
|
256
|
+
try {
|
|
257
|
+
const out = await Text2ImgEngine.generate(text, {
|
|
258
|
+
theme: options.theme.toLowerCase(),
|
|
259
|
+
type: options.type.toLowerCase(),
|
|
260
|
+
title: options.title,
|
|
261
|
+
author: options.author,
|
|
262
|
+
});
|
|
263
|
+
spinner.succeed(`Social graphics card generated: ${chalk.cyan(out)}`);
|
|
264
|
+
}
|
|
265
|
+
catch (e) {
|
|
266
|
+
spinner.fail(`Text2img rendering failed: ${e.message}`);
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
// Compression Command
|
|
270
|
+
program
|
|
271
|
+
.command('compress')
|
|
272
|
+
.description('Reduce image file size or entire directories recursively')
|
|
273
|
+
.argument('<path>', 'File or folder path')
|
|
274
|
+
.action(async (targetPath) => {
|
|
275
|
+
const stat = fs.statSync(targetPath);
|
|
276
|
+
if (stat.isDirectory()) {
|
|
277
|
+
const spinner = ora('Compressing folder files recursively...').start();
|
|
278
|
+
try {
|
|
279
|
+
const results = await CompressEngine.compressFolder(targetPath);
|
|
280
|
+
spinner.succeed(`Recursive folder compression completed!`);
|
|
281
|
+
let totalOriginal = 0;
|
|
282
|
+
let totalCompressed = 0;
|
|
283
|
+
results.forEach(r => {
|
|
284
|
+
totalOriginal += r.originalSize;
|
|
285
|
+
totalCompressed += r.compressedSize;
|
|
286
|
+
});
|
|
287
|
+
const totalSaved = totalOriginal - totalCompressed;
|
|
288
|
+
const totalPct = totalOriginal > 0 ? (totalSaved / totalOriginal) * 100 : 0;
|
|
289
|
+
console.log(chalk.bold.yellow('\nBatch Compression Statistics:'));
|
|
290
|
+
console.log(`- Files processed: ${chalk.cyan(results.length)}`);
|
|
291
|
+
console.log(`- Total original size: ${chalk.bold(formatBytes(totalOriginal))}`);
|
|
292
|
+
console.log(`- Total compressed size: ${chalk.bold(formatBytes(totalCompressed))}`);
|
|
293
|
+
console.log(`- Total space saved: ${chalk.green.bold(formatBytes(totalSaved))} (${chalk.green.bold(totalPct.toFixed(1))}% reduction)`);
|
|
294
|
+
}
|
|
295
|
+
catch (e) {
|
|
296
|
+
spinner.fail(`Folder compression failed: ${e.message}`);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
const spinner = ora('Optimizing image stream...').start();
|
|
301
|
+
try {
|
|
302
|
+
const res = await CompressEngine.compressImage(targetPath);
|
|
303
|
+
spinner.succeed(`Image file optimized: ${chalk.cyan(res.filePath)}`);
|
|
304
|
+
console.log(chalk.bold.yellow('\nCompression Statistics:'));
|
|
305
|
+
console.log(`- Original size: ${chalk.bold(formatBytes(res.originalSize))}`);
|
|
306
|
+
console.log(`- Compressed size: ${chalk.bold(formatBytes(res.compressedSize))}`);
|
|
307
|
+
console.log(`- Savings: ${chalk.green.bold(formatBytes(res.savedBytes))} (${chalk.green.bold(res.percentage.toFixed(1))}% reduction)`);
|
|
308
|
+
}
|
|
309
|
+
catch (e) {
|
|
310
|
+
spinner.fail(`Image compression failed: ${e.message}`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
// Batch Command
|
|
315
|
+
program
|
|
316
|
+
.command('batch')
|
|
317
|
+
.description('Run operations across groups of matching files')
|
|
318
|
+
.argument('<pattern>', 'Glob file search pattern (e.g. "images/*.png")')
|
|
319
|
+
.option('--convert <format>', 'Convert batch to target format (png, jpg, webp)')
|
|
320
|
+
.option('--pdf', 'Compile batch documents into PDF files')
|
|
321
|
+
.action(async (pattern, options) => {
|
|
322
|
+
if (!options.convert && !options.pdf) {
|
|
323
|
+
console.error(chalk.red('Error: You must specify --convert <format> or --pdf.'));
|
|
324
|
+
process.exit(1);
|
|
325
|
+
}
|
|
326
|
+
const spinner = ora('Executing batch processing jobs...').start();
|
|
327
|
+
try {
|
|
328
|
+
let results;
|
|
329
|
+
if (options.convert) {
|
|
330
|
+
results = await BatchEngine.convertImages(pattern, options.convert);
|
|
331
|
+
}
|
|
332
|
+
else {
|
|
333
|
+
results = await BatchEngine.convertToPdf(pattern);
|
|
334
|
+
}
|
|
335
|
+
const succeeded = results.filter(r => r.success);
|
|
336
|
+
const failed = results.filter(r => !r.success);
|
|
337
|
+
spinner.succeed(`Batch operations completed!`);
|
|
338
|
+
console.log(chalk.bold.yellow('\nBatch Statistics:'));
|
|
339
|
+
console.log(`- Total tasks: ${chalk.cyan(results.length)}`);
|
|
340
|
+
console.log(`- Succeeded: ${chalk.green(succeeded.length)}`);
|
|
341
|
+
console.log(`- Failed: ${chalk.red(failed.length)}`);
|
|
342
|
+
if (failed.length > 0) {
|
|
343
|
+
console.log(chalk.bold.red('\nFailures:'));
|
|
344
|
+
failed.forEach(f => console.log(` - ${f.sourceFile}: ${chalk.dim(f.error)}`));
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
catch (e) {
|
|
348
|
+
spinner.fail(`Batch processing execution failed: ${e.message}`);
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
// Export Command
|
|
352
|
+
program
|
|
353
|
+
.command('export')
|
|
354
|
+
.description('Export configurations or convert JSON datasets to CSV/Markdown')
|
|
355
|
+
.argument('<format>', 'Target output format (json, markdown, csv)')
|
|
356
|
+
.option('--source <file>', 'Path to JSON file to convert (optional)')
|
|
357
|
+
.action(async (format, options) => {
|
|
358
|
+
const fmt = format.toLowerCase();
|
|
359
|
+
if (options.source) {
|
|
360
|
+
const spinner = ora(`Converting JSON file to ${format}...`).start();
|
|
361
|
+
try {
|
|
362
|
+
if (fmt === 'json') {
|
|
363
|
+
spinner.fail('Cannot convert a JSON file into a JSON file.');
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
const out = await ExportEngine.convertJsonData(options.source, fmt);
|
|
367
|
+
spinner.succeed(`Dataset exported: ${chalk.cyan(out)}`);
|
|
368
|
+
}
|
|
369
|
+
catch (e) {
|
|
370
|
+
spinner.fail(`Conversion failed: ${e.message}`);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
else {
|
|
374
|
+
const spinner = ora(`Exporting config parameters as ${format}...`).start();
|
|
375
|
+
try {
|
|
376
|
+
const out = await ExportEngine.exportConfig(fmt);
|
|
377
|
+
spinner.succeed(`Settings exported: ${chalk.cyan(out)}`);
|
|
378
|
+
}
|
|
379
|
+
catch (e) {
|
|
380
|
+
spinner.fail(`Export failed: ${e.message}`);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
// Config Command
|
|
385
|
+
program
|
|
386
|
+
.command('config')
|
|
387
|
+
.description('Print or modify configuration parameters')
|
|
388
|
+
.action(async () => {
|
|
389
|
+
const current = ConfigManager.load();
|
|
390
|
+
console.log(chalk.bold.yellow('\nCurrent MediaGuru Settings:'));
|
|
391
|
+
console.log(`- Default Image Format: ${chalk.cyan(current.defaultImageFormat)}`);
|
|
392
|
+
console.log(`- PDF Rendering Engine: ${chalk.cyan(current.pdfEngine)}`);
|
|
393
|
+
console.log(`- Compression Quality: ${chalk.cyan(current.compressionQuality + '%')}`);
|
|
394
|
+
console.log(`- Screenshot Resolution: ${chalk.cyan(current.screenshotResolution)}`);
|
|
395
|
+
console.log(`- Default Output Folder: ${chalk.cyan(current.outputFolder)}`);
|
|
396
|
+
console.log(chalk.dim('\nTo edit settings, run the CLI without arguments to open interactive mode.'));
|
|
397
|
+
});
|
|
398
|
+
// Standalone REST API Server Command
|
|
399
|
+
program
|
|
400
|
+
.command('server')
|
|
401
|
+
.description('Start a local HTTP REST API server')
|
|
402
|
+
.option('--port <number>', 'Port to listen on', '3000')
|
|
403
|
+
.action(async (options) => {
|
|
404
|
+
const port = parseInt(options.port, 10) || 3000;
|
|
405
|
+
const server = new RestApiServer();
|
|
406
|
+
const spinner = ora('Starting HTTP REST Server...').start();
|
|
407
|
+
try {
|
|
408
|
+
spinner.stop(); // Stop spinner to avoid overlapping text
|
|
409
|
+
await server.start(port);
|
|
410
|
+
}
|
|
411
|
+
catch (e) {
|
|
412
|
+
spinner.fail(`Server failed to start: ${e.message}`);
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
// Bootstrap CLI
|
|
416
|
+
async function bootstrap() {
|
|
417
|
+
// Inject plugin custom commands
|
|
418
|
+
PluginRegistry.injectCustomCommands(program);
|
|
419
|
+
// If no arguments, launch interactive mode
|
|
420
|
+
if (process.argv.length <= 2) {
|
|
421
|
+
await launchInteractive();
|
|
422
|
+
}
|
|
423
|
+
else {
|
|
424
|
+
// Print ASCII art header on direct command invocations
|
|
425
|
+
printHeader();
|
|
426
|
+
program.parse(process.argv);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
bootstrap();
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
export interface IMediaGuruPlugin {
|
|
3
|
+
name: string;
|
|
4
|
+
version: string;
|
|
5
|
+
description?: string;
|
|
6
|
+
/**
|
|
7
|
+
* Executed before any built-in command runs.
|
|
8
|
+
*/
|
|
9
|
+
beforeCommand?(commandName: string, args: any[]): Promise<void>;
|
|
10
|
+
/**
|
|
11
|
+
* Executed after any built-in command runs successfully.
|
|
12
|
+
*/
|
|
13
|
+
afterCommand?(commandName: string, result: any): Promise<void>;
|
|
14
|
+
/**
|
|
15
|
+
* Allows the plugin to inject brand new subcommands into Commander.js!
|
|
16
|
+
*/
|
|
17
|
+
registerCustomCommands?(program: Command): void;
|
|
18
|
+
}
|
|
19
|
+
export declare class PluginRegistry {
|
|
20
|
+
private static plugins;
|
|
21
|
+
/**
|
|
22
|
+
* Registers a plugin instance.
|
|
23
|
+
*/
|
|
24
|
+
static registerPlugin(plugin: IMediaGuruPlugin): void;
|
|
25
|
+
/**
|
|
26
|
+
* Retrieves all loaded plugins.
|
|
27
|
+
*/
|
|
28
|
+
static getPlugins(): IMediaGuruPlugin[];
|
|
29
|
+
/**
|
|
30
|
+
* Lifecycle trigger: before command execution.
|
|
31
|
+
*/
|
|
32
|
+
static triggerBeforeCommand(commandName: string, args: any[]): Promise<void>;
|
|
33
|
+
/**
|
|
34
|
+
* Lifecycle trigger: after command execution.
|
|
35
|
+
*/
|
|
36
|
+
static triggerAfterCommand(commandName: string, result: any): Promise<void>;
|
|
37
|
+
/**
|
|
38
|
+
* Scans loaded plugins and lets them inject their custom subcommands.
|
|
39
|
+
*/
|
|
40
|
+
static injectCustomCommands(program: Command): void;
|
|
41
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export class PluginRegistry {
|
|
2
|
+
static plugins = new Map();
|
|
3
|
+
/**
|
|
4
|
+
* Registers a plugin instance.
|
|
5
|
+
*/
|
|
6
|
+
static registerPlugin(plugin) {
|
|
7
|
+
this.plugins.set(plugin.name, plugin);
|
|
8
|
+
// console.log(`[PluginRegistry] Registered: ${plugin.name} (v${plugin.version})`);
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Retrieves all loaded plugins.
|
|
12
|
+
*/
|
|
13
|
+
static getPlugins() {
|
|
14
|
+
return Array.from(this.plugins.values());
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Lifecycle trigger: before command execution.
|
|
18
|
+
*/
|
|
19
|
+
static async triggerBeforeCommand(commandName, args) {
|
|
20
|
+
for (const plugin of this.plugins.values()) {
|
|
21
|
+
if (plugin.beforeCommand) {
|
|
22
|
+
try {
|
|
23
|
+
await plugin.beforeCommand(commandName, args);
|
|
24
|
+
}
|
|
25
|
+
catch (err) {
|
|
26
|
+
console.warn(`[PluginRegistry] Plugin "${plugin.name}" error in beforeCommand:`, err);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Lifecycle trigger: after command execution.
|
|
33
|
+
*/
|
|
34
|
+
static async triggerAfterCommand(commandName, result) {
|
|
35
|
+
for (const plugin of this.plugins.values()) {
|
|
36
|
+
if (plugin.afterCommand) {
|
|
37
|
+
try {
|
|
38
|
+
await plugin.afterCommand(commandName, result);
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
console.warn(`[PluginRegistry] Plugin "${plugin.name}" error in afterCommand:`, err);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Scans loaded plugins and lets them inject their custom subcommands.
|
|
48
|
+
*/
|
|
49
|
+
static injectCustomCommands(program) {
|
|
50
|
+
for (const plugin of this.plugins.values()) {
|
|
51
|
+
if (plugin.registerCustomCommands) {
|
|
52
|
+
try {
|
|
53
|
+
plugin.registerCustomCommands(program);
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
console.error(`[PluginRegistry] Plugin "${plugin.name}" failed to register custom commands:`, err);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import sharp from 'sharp';
|
|
5
|
+
import { ConfigManager } from '../core/config/index.js';
|
|
6
|
+
import { ImageEngine } from '../core/image/index.js';
|
|
7
|
+
import { QrEngine } from '../core/qr/index.js';
|
|
8
|
+
import { ExportEngine } from '../core/export/index.js';
|
|
9
|
+
async function runTests() {
|
|
10
|
+
console.log(chalk.bold.cyan('========================================'));
|
|
11
|
+
console.log(chalk.bold.cyan(' MediaGuru Engine Validation Suite '));
|
|
12
|
+
console.log(chalk.bold.cyan('========================================\n'));
|
|
13
|
+
const testDir = './test_sandbox';
|
|
14
|
+
if (!fs.existsSync(testDir)) {
|
|
15
|
+
fs.mkdirSync(testDir, { recursive: true });
|
|
16
|
+
}
|
|
17
|
+
// Setup Config
|
|
18
|
+
ConfigManager.update({
|
|
19
|
+
outputFolder: testDir,
|
|
20
|
+
compressionQuality: 85,
|
|
21
|
+
defaultImageFormat: 'png',
|
|
22
|
+
});
|
|
23
|
+
console.log(chalk.green('✓ Config Manager initialized and updated.'));
|
|
24
|
+
// Create mock source image (100x100 solid blue box)
|
|
25
|
+
const mockImagePath = path.join(testDir, 'source.png');
|
|
26
|
+
const mockWatermarkPath = path.join(testDir, 'watermark.png');
|
|
27
|
+
await sharp({
|
|
28
|
+
create: {
|
|
29
|
+
width: 200,
|
|
30
|
+
height: 200,
|
|
31
|
+
channels: 4,
|
|
32
|
+
background: { r: 0, g: 0, b: 255, alpha: 1 },
|
|
33
|
+
},
|
|
34
|
+
})
|
|
35
|
+
.png()
|
|
36
|
+
.toFile(mockImagePath);
|
|
37
|
+
// Solid white 40x40 watermark box
|
|
38
|
+
await sharp({
|
|
39
|
+
create: {
|
|
40
|
+
width: 40,
|
|
41
|
+
height: 40,
|
|
42
|
+
channels: 4,
|
|
43
|
+
background: { r: 255, g: 255, b: 255, alpha: 1 },
|
|
44
|
+
},
|
|
45
|
+
})
|
|
46
|
+
.png()
|
|
47
|
+
.toFile(mockWatermarkPath);
|
|
48
|
+
console.log(chalk.green('✓ Mock testing images created successfully.'));
|
|
49
|
+
// Test Image Resize
|
|
50
|
+
console.log(chalk.yellow('\nTesting Image Resizing...'));
|
|
51
|
+
const resizedPath = await ImageEngine.resize(mockImagePath, '100x100');
|
|
52
|
+
if (fs.existsSync(resizedPath)) {
|
|
53
|
+
console.log(chalk.green(`✓ Image resized successfully: ${resizedPath}`));
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
throw new Error('Image resize failed to output file.');
|
|
57
|
+
}
|
|
58
|
+
// Test Image Format Convert
|
|
59
|
+
console.log(chalk.yellow('\nTesting Image Conversion...'));
|
|
60
|
+
const convertedPath = await ImageEngine.convert(mockImagePath, 'webp');
|
|
61
|
+
if (fs.existsSync(convertedPath)) {
|
|
62
|
+
console.log(chalk.green(`✓ Image converted to WebP successfully: ${convertedPath}`));
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
throw new Error('Image conversion failed to output file.');
|
|
66
|
+
}
|
|
67
|
+
// Test Image Watermark
|
|
68
|
+
console.log(chalk.yellow('\nTesting Image Watermarking...'));
|
|
69
|
+
const watermarkedPath = await ImageEngine.watermark(mockImagePath, mockWatermarkPath);
|
|
70
|
+
if (fs.existsSync(watermarkedPath)) {
|
|
71
|
+
console.log(chalk.green(`✓ Watermark overlay successful: ${watermarkedPath}`));
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
throw new Error('Watermark overlay failed to output file.');
|
|
75
|
+
}
|
|
76
|
+
// Test QR Generator
|
|
77
|
+
console.log(chalk.yellow('\nTesting QR Code Generation...'));
|
|
78
|
+
const qrPng = await QrEngine.generate('https://kontyra.com', { svg: false, size: 250 });
|
|
79
|
+
const qrSvg = await QrEngine.generate('https://kontyra.com', { svg: true, size: 250 });
|
|
80
|
+
if (fs.existsSync(qrPng) && fs.existsSync(qrSvg)) {
|
|
81
|
+
console.log(chalk.green(`✓ QR PNG generated successfully: ${qrPng}`));
|
|
82
|
+
console.log(chalk.green(`✓ QR SVG generated successfully: ${qrSvg}`));
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
throw new Error('QR Code generation failed.');
|
|
86
|
+
}
|
|
87
|
+
// Test Exports System
|
|
88
|
+
console.log(chalk.yellow('\nTesting Exports System...'));
|
|
89
|
+
const configJson = await ExportEngine.exportConfig('json', { outputPath: path.join(testDir, 'config.json') });
|
|
90
|
+
const configMd = await ExportEngine.exportConfig('markdown', { outputPath: path.join(testDir, 'config.md') });
|
|
91
|
+
const configCsv = await ExportEngine.exportConfig('csv', { outputPath: path.join(testDir, 'config.csv') });
|
|
92
|
+
if (fs.existsSync(configJson) && fs.existsSync(configMd) && fs.existsSync(configCsv)) {
|
|
93
|
+
console.log(chalk.green('✓ Configurations exported successfully in JSON, MD, and CSV.'));
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
throw new Error('Export config failed.');
|
|
97
|
+
}
|
|
98
|
+
// Clean up mock generated source files
|
|
99
|
+
console.log(chalk.cyan('\nCleaning up mock testing files...'));
|
|
100
|
+
// (we leave other outputs in the sandbox folder for manual inspection if needed)
|
|
101
|
+
console.log(chalk.bold.green('\n========================================'));
|
|
102
|
+
console.log(chalk.bold.green(' ALL CORE ENGINE TESTS PASSED! '));
|
|
103
|
+
console.log(chalk.bold.green('========================================\n'));
|
|
104
|
+
}
|
|
105
|
+
runTests().catch((err) => {
|
|
106
|
+
console.error(chalk.bold.red('\n❌ TEST SUITE FAILED:'), err);
|
|
107
|
+
process.exit(1);
|
|
108
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|