Skip to content

Commit 939071f

Browse files
Add batch tool import system and library integration framework
- Implement BatchToolImporter for parsing and importing tool lists - Add ConversionLibraryManager with ImageMagick, FFmpeg, VERT.sh integration - Create CLI commands: 'import' and 'libraries' - Support intelligent duplicate detection and conflict resolution - Include sample import file with user's requested formats Co-authored-by: devinschumacher <45643901+devinschumacher@users.noreply.github.com>
1 parent 52a9fdd commit 939071f

8 files changed

Lines changed: 1117 additions & 6 deletions

File tree

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@
1010
"serptools": "cd packages/app-core && npm run cli",
1111
"tools:generate": "cd packages/app-core && npm run generate",
1212
"tools:validate": "cd packages/app-core && npm run validate",
13-
"tools:stats": "cd packages/app-core && npm run stats"
13+
"tools:stats": "cd packages/app-core && npm run stats",
14+
"tools:import": "cd packages/app-core && npm run import",
15+
"tools:libraries": "cd packages/app-core && npm run libraries"
1416
},
1517
"devDependencies": {
1618
"@serp-tools/eslint-config": "workspace:*",

packages/app-core/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@
1313
"cli": "tsx src/cli.ts",
1414
"generate": "tsx src/cli.ts generate",
1515
"validate": "tsx src/cli.ts validate",
16-
"stats": "tsx src/cli.ts stats"
16+
"stats": "tsx src/cli.ts stats",
17+
"import": "tsx src/cli.ts import",
18+
"libraries": "tsx src/cli.ts libraries"
1719
},
1820
"dependencies": {
1921
"@next/third-parties": "^15.5.0",

packages/app-core/src/cli.ts

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import chalk from 'chalk';
1212
import inquirer from 'inquirer';
1313
import { ToolGenerator, createToolGenerator, Tool } from './lib/tool-generator.js';
1414
import { ToolRegistryManager, createRegistryManager } from './lib/tool-registry.js';
15+
import { BatchToolImporter, createBatchToolImporter } from './lib/batch-importer.js';
16+
import { createLibraryManager, getConversionRecommendations, generateLibraryMatrix } from './lib/library-integration.js';
1517
import path from 'path';
1618

1719
const DEFAULT_TOOLS_DIR = './apps/tools/app';
@@ -329,4 +331,212 @@ program
329331
}
330332
});
331333

334+
// Batch import command
335+
program
336+
.command('import')
337+
.description('Batch import tools from a file or input')
338+
.option('-f, --file <file>', 'Import from file')
339+
.option('-i, --input <input>', 'Import from direct input string')
340+
.option('--dry-run', 'Analyze without creating tools')
341+
.option('--skip-existing', 'Skip tools that already exist')
342+
.option('--generate-content', 'Generate basic content for new tools')
343+
.option('--report <file>', 'Save report to file')
344+
.action(async (options) => {
345+
try {
346+
console.log(chalk.blue('📥 Batch Tool Import'));
347+
348+
if (!options.file && !options.input) {
349+
console.error(chalk.red('❌ Please provide either --file or --input'));
350+
process.exit(1);
351+
}
352+
353+
const registryManager = createRegistryManager(DEFAULT_REGISTRY_PATH);
354+
const importer = createBatchToolImporter(registryManager);
355+
356+
let input: string;
357+
if (options.file) {
358+
console.log(chalk.gray(`Reading from file: ${options.file}`));
359+
const fs = await import('fs/promises');
360+
input = await fs.readFile(options.file, 'utf-8');
361+
} else {
362+
input = options.input;
363+
}
364+
365+
// Parse input
366+
console.log(chalk.gray('Parsing import requests...'));
367+
const requests = importer.parseImportList(input);
368+
console.log(chalk.cyan(`Found ${requests.length} conversion requests`));
369+
370+
// Analyze requests
371+
console.log(chalk.gray('Analyzing against existing tools...'));
372+
const analysis = await importer.analyzeImportRequests(requests);
373+
374+
console.log(chalk.cyan(`\n📊 Analysis Results:`));
375+
console.log(` Total requests: ${analysis.total}`);
376+
console.log(` Existing tools: ${analysis.existing.length}`);
377+
console.log(` New tools: ${analysis.new.length}`);
378+
console.log(` Conflicts: ${analysis.conflicts.length}`);
379+
380+
// Show existing tools
381+
if (analysis.existing.length > 0) {
382+
console.log(chalk.yellow(`\n⚠️ Existing Tools (${analysis.existing.length}):`));
383+
analysis.existing.slice(0, 5).forEach(({ request, existingTool, match }) => {
384+
console.log(` ${match === 'exact' ? '🎯' : '🔍'} ${request.from}${request.to} (${match} match: ${existingTool.name})`);
385+
});
386+
if (analysis.existing.length > 5) {
387+
console.log(` ... and ${analysis.existing.length - 5} more`);
388+
}
389+
}
390+
391+
// Show conflicts
392+
if (analysis.conflicts.length > 0) {
393+
console.log(chalk.red(`\n❌ Conflicts (${analysis.conflicts.length}):`));
394+
analysis.conflicts.forEach(({ request, issue }) => {
395+
console.log(` ${request.from}${request.to}: ${issue}`);
396+
});
397+
}
398+
399+
// Execute import if not dry run
400+
let execution;
401+
if (!options.dryRun && analysis.new.length > 0) {
402+
console.log(chalk.blue(`\n🚀 Creating ${analysis.new.length} new tools...`));
403+
404+
execution = await importer.executeImport(analysis.new, {
405+
skipExisting: options.skipExisting,
406+
generateContent: options.generateContent,
407+
dryRun: false
408+
});
409+
410+
console.log(chalk.green(`✅ Import completed:`));
411+
console.log(` Created: ${execution.created.length}`);
412+
console.log(` Skipped: ${execution.skipped.length}`);
413+
console.log(` Errors: ${execution.errors.length}`);
414+
415+
if (execution.errors.length > 0) {
416+
console.log(chalk.red(`\n❌ Errors:`));
417+
execution.errors.forEach(({ tool, error }) => {
418+
console.log(` ${tool.from}${tool.to}: ${error}`);
419+
});
420+
}
421+
} else if (options.dryRun) {
422+
console.log(chalk.yellow('\n🔍 Dry run completed - no tools were created'));
423+
}
424+
425+
// Generate and save report
426+
const report = importer.generateImportReport(analysis, execution);
427+
428+
if (options.report) {
429+
const fs = await import('fs/promises');
430+
await fs.writeFile(options.report, report);
431+
console.log(chalk.green(`\n📝 Report saved to: ${options.report}`));
432+
} else {
433+
console.log('\n' + report);
434+
}
435+
436+
} catch (error) {
437+
console.error(chalk.red('❌ Import failed:'), error);
438+
process.exit(1);
439+
}
440+
});
441+
442+
// Libraries command
443+
program
444+
.command('libraries')
445+
.description('Show available conversion libraries and their capabilities')
446+
.option('--check-availability', 'Check which libraries are currently available')
447+
.option('--matrix', 'Show conversion compatibility matrix')
448+
.option('--recommend <conversion>', 'Get library recommendations for a conversion (format: "jpg:png")')
449+
.action(async (options) => {
450+
try {
451+
console.log(chalk.blue('📚 Conversion Libraries'));
452+
453+
const manager = createLibraryManager();
454+
const libraries = manager.getAllLibraries();
455+
456+
if (options.recommend) {
457+
const [from, to] = options.recommend.split(':');
458+
if (!from || !to) {
459+
console.error(chalk.red('❌ Conversion format should be "from:to" (e.g., "jpg:png")'));
460+
process.exit(1);
461+
}
462+
463+
console.log(chalk.cyan(`\n🎯 Recommendations for ${from}${to}:`));
464+
const recommendations = await getConversionRecommendations(from, to);
465+
466+
if (recommendations.recommended) {
467+
console.log(chalk.green(`\n✅ Recommended: ${recommendations.recommended.name}`));
468+
console.log(` ${recommendations.recommended.description}`);
469+
console.log(` Platform: ${recommendations.recommended.capabilities.platform}`);
470+
console.log(` License: ${recommendations.recommended.capabilities.license}`);
471+
}
472+
473+
if (recommendations.alternatives.length > 0) {
474+
console.log(chalk.yellow(`\n🔄 Alternatives:`));
475+
recommendations.alternatives.forEach(lib => {
476+
console.log(` • ${lib.name} (${lib.capabilities.platform})`);
477+
});
478+
}
479+
480+
if (recommendations.unsupported) {
481+
console.log(chalk.red('\n❌ This conversion is not supported by any available library'));
482+
}
483+
484+
return;
485+
}
486+
487+
if (options.matrix) {
488+
console.log(chalk.cyan('\n📊 Conversion Compatibility Matrix:'));
489+
const { matrix } = generateLibraryMatrix();
490+
491+
// Show a sample of the matrix (top formats)
492+
const topFormats = ['jpg', 'png', 'gif', 'webp', 'mp4', 'pdf'];
493+
console.log('\nFormat compatibility (sample):');
494+
console.log('FROM \\ TO ' + topFormats.map(f => f.padEnd(8)).join(''));
495+
console.log('─'.repeat(80));
496+
497+
topFormats.forEach(from => {
498+
const row = from.padEnd(10) + topFormats.map(to => {
499+
const libs = matrix[from]?.[to];
500+
return libs ? `${libs.length}libs`.padEnd(8) : '─'.padEnd(8);
501+
}).join('');
502+
console.log(row);
503+
});
504+
console.log('\nNote: Numbers indicate available libraries for each conversion');
505+
return;
506+
}
507+
508+
console.log(chalk.cyan(`\n📋 Available Libraries (${libraries.length}):`));
509+
510+
for (const library of libraries) {
511+
console.log(`\n${chalk.bold(library.name)} (${library.id})`);
512+
console.log(` ${library.description}`);
513+
console.log(` Platform: ${library.capabilities.platform}`);
514+
console.log(` License: ${library.capabilities.license}`);
515+
console.log(` Input formats: ${library.capabilities.supportedFormats.input.slice(0, 10).join(', ')}${library.capabilities.supportedFormats.input.length > 10 ? '...' : ''}`);
516+
console.log(` Output formats: ${library.capabilities.supportedFormats.output.slice(0, 10).join(', ')}${library.capabilities.supportedFormats.output.length > 10 ? '...' : ''}`);
517+
console.log(` Operations: ${library.capabilities.operations.join(', ')}`);
518+
519+
if (library.capabilities.homepage) {
520+
console.log(` Homepage: ${library.capabilities.homepage}`);
521+
}
522+
}
523+
524+
if (options.checkAvailability) {
525+
console.log(chalk.cyan('\n🔍 Checking library availability...'));
526+
const availability = await manager.checkLibraryAvailability();
527+
528+
console.log('\nAvailability Status:');
529+
for (const [id, available] of availability) {
530+
const library = manager.getLibrary(id);
531+
const status = available ? chalk.green('✅ Available') : chalk.red('❌ Unavailable');
532+
console.log(` ${library?.name}: ${status}`);
533+
}
534+
}
535+
536+
} catch (error) {
537+
console.error(chalk.red('❌ Libraries command failed:'), error);
538+
process.exit(1);
539+
}
540+
});
541+
332542
program.parse();

packages/app-core/src/index.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,30 @@ export {
2020
type ToolMetrics
2121
} from './lib/tool-registry';
2222

23+
// Batch Import System
24+
export {
25+
BatchToolImporter,
26+
createBatchToolImporter,
27+
type ImportToolRequest,
28+
type ImportAnalysisResult,
29+
type ImportExecutionResult
30+
} from './lib/batch-importer';
31+
32+
// Library Integration System
33+
export {
34+
ConversionLibraryManager,
35+
ImageMagickLibrary,
36+
FFmpegLibrary,
37+
VertShLibrary,
38+
CanvasLibrary,
39+
createLibraryManager,
40+
getConversionRecommendations,
41+
generateLibraryMatrix,
42+
type ConversionLibrary,
43+
type LibraryCapability,
44+
type LibraryManager
45+
} from './lib/library-integration';
46+
2347
// Plugin System
2448
export {
2549
PluginManager,

0 commit comments

Comments
 (0)