Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 18:50:16 +08:00
commit b38883ce98
39 changed files with 4530 additions and 0 deletions

340
src/cli.js Executable file
View File

@@ -0,0 +1,340 @@
#!/usr/bin/env node
/**
* Skill Porter CLI
* Command-line interface for converting between Claude and Gemini formats
*/
import { program } from 'commander';
import chalk from 'chalk';
import { SkillPorter, PLATFORM_TYPES } from './index.js';
import { PRGenerator } from './optional-features/pr-generator.js';
import { ForkSetup } from './optional-features/fork-setup.js';
import fs from 'fs/promises';
import path from 'path';
const porter = new SkillPorter();
// Version from package.json
const packagePath = new URL('../package.json', import.meta.url);
const packageData = JSON.parse(await fs.readFile(packagePath, 'utf8'));
program
.name('skill-porter')
.description('Universal tool to convert Claude Code skills to Gemini CLI extensions and vice versa')
.version(packageData.version);
// Convert command
program
.command('convert <source-path>')
.description('Convert a skill or extension between platforms')
.option('-t, --to <platform>', 'Target platform (claude or gemini)', 'gemini')
.option('-o, --output <path>', 'Output directory path')
.option('--no-validate', 'Skip validation after conversion')
.action(async (sourcePath, options) => {
try {
console.log(chalk.blue('\n🔄 Converting skill/extension...\n'));
const result = await porter.convert(
path.resolve(sourcePath),
options.to,
{
outputPath: options.output ? path.resolve(options.output) : undefined,
validate: options.validate !== false
}
);
if (result.success) {
console.log(chalk.green('✓ Conversion successful!\n'));
if (result.files && result.files.length > 0) {
console.log(chalk.bold('Generated files:'));
result.files.forEach(file => {
console.log(chalk.gray(` - ${file}`));
});
console.log();
}
if (result.validation) {
if (result.validation.valid) {
console.log(chalk.green('✓ Validation passed\n'));
} else {
console.log(chalk.yellow('⚠ Validation warnings:\n'));
result.validation.errors.forEach(error => {
console.log(chalk.yellow(` - ${error}`));
});
console.log();
}
if (result.validation.warnings && result.validation.warnings.length > 0) {
console.log(chalk.yellow('Warnings:'));
result.validation.warnings.forEach(warning => {
console.log(chalk.yellow(` - ${warning}`));
});
console.log();
}
}
// Installation instructions
const targetPlatform = options.to;
console.log(chalk.bold('Next steps:'));
if (targetPlatform === PLATFORM_TYPES.GEMINI) {
console.log(chalk.gray(` gemini extensions install ${options.output || sourcePath}`));
} else {
console.log(chalk.gray(` cp -r ${options.output || sourcePath} ~/.claude/skills/`));
}
console.log();
} else {
console.log(chalk.red('✗ Conversion failed\n'));
if (result.errors && result.errors.length > 0) {
console.log(chalk.red('Errors:'));
result.errors.forEach(error => {
console.log(chalk.red(` - ${error}`));
});
console.log();
}
process.exit(1);
}
} catch (error) {
console.error(chalk.red(`\n✗ Error: ${error.message}\n`));
process.exit(1);
}
});
// Analyze command
program
.command('analyze <path>')
.description('Analyze a directory to detect platform type')
.action(async (dirPath) => {
try {
console.log(chalk.blue('\n🔍 Analyzing directory...\n'));
const detection = await porter.analyze(path.resolve(dirPath));
console.log(chalk.bold('Detection Results:'));
console.log(chalk.gray(` Platform: ${chalk.white(detection.platform)}`));
console.log(chalk.gray(` Confidence: ${chalk.white(detection.confidence)}\n`));
if (detection.files.claude.length > 0) {
console.log(chalk.bold('Claude files found:'));
detection.files.claude.forEach(file => {
const status = file.valid ? chalk.green('✓') : chalk.red('✗');
const issue = file.issue ? chalk.gray(` (${file.issue})`) : '';
console.log(` ${status} ${file.file}${issue}`);
});
console.log();
}
if (detection.files.gemini.length > 0) {
console.log(chalk.bold('Gemini files found:'));
detection.files.gemini.forEach(file => {
const status = file.valid ? chalk.green('✓') : chalk.red('✗');
const issue = file.issue ? chalk.gray(` (${file.issue})`) : '';
console.log(` ${status} ${file.file}${issue}`);
});
console.log();
}
if (detection.files.shared.length > 0) {
console.log(chalk.bold('Shared files found:'));
detection.files.shared.forEach(file => {
console.log(chalk.gray(` - ${file.file}`));
});
console.log();
}
if (detection.metadata.claude || detection.metadata.gemini) {
console.log(chalk.bold('Metadata:'));
if (detection.metadata.claude) {
console.log(chalk.gray(` Name: ${detection.metadata.claude.name || 'N/A'}`));
console.log(chalk.gray(` Description: ${detection.metadata.claude.description || 'N/A'}`));
}
if (detection.metadata.gemini) {
console.log(chalk.gray(` Name: ${detection.metadata.gemini.name || 'N/A'}`));
console.log(chalk.gray(` Version: ${detection.metadata.gemini.version || 'N/A'}`));
}
console.log();
}
} catch (error) {
console.error(chalk.red(`\n✗ Error: ${error.message}\n`));
process.exit(1);
}
});
// Validate command
program
.command('validate <path>')
.description('Validate a skill or extension')
.option('-p, --platform <type>', 'Platform type (claude, gemini, or universal)')
.action(async (dirPath, options) => {
try {
console.log(chalk.blue('\n✓ Validating...\n'));
const validation = await porter.validate(
path.resolve(dirPath),
options.platform
);
if (validation.valid) {
console.log(chalk.green('✓ Validation passed!\n'));
} else {
console.log(chalk.red('✗ Validation failed\n'));
}
if (validation.errors && validation.errors.length > 0) {
console.log(chalk.red('Errors:'));
validation.errors.forEach(error => {
console.log(chalk.red(` - ${error}`));
});
console.log();
}
if (validation.warnings && validation.warnings.length > 0) {
console.log(chalk.yellow('Warnings:'));
validation.warnings.forEach(warning => {
console.log(chalk.yellow(` - ${warning}`));
});
console.log();
}
if (!validation.valid) {
process.exit(1);
}
} catch (error) {
console.error(chalk.red(`\n✗ Error: ${error.message}\n`));
process.exit(1);
}
});
// Make universal command
program
.command('universal <source-path>')
.description('Make a skill/extension work on both platforms')
.option('-o, --output <path>', 'Output directory path')
.action(async (sourcePath, options) => {
try {
console.log(chalk.blue('\n🌐 Creating universal skill/extension...\n'));
const result = await porter.makeUniversal(
path.resolve(sourcePath),
{
outputPath: options.output ? path.resolve(options.output) : undefined
}
);
if (result.success) {
console.log(chalk.green('✓ Successfully created universal skill/extension!\n'));
console.log(chalk.gray('Your skill/extension now works with both Claude Code and Gemini CLI.\n'));
} else {
console.log(chalk.red('✗ Failed to create universal skill/extension\n'));
if (result.errors && result.errors.length > 0) {
result.errors.forEach(error => {
console.log(chalk.red(` - ${error}`));
});
console.log();
}
process.exit(1);
}
} catch (error) {
console.error(chalk.red(`\n✗ Error: ${error.message}\n`));
process.exit(1);
}
});
// Create PR command
program
.command('create-pr <source-path>')
.description('Create a pull request to add dual-platform support')
.option('-t, --to <platform>', 'Target platform to add (claude or gemini)', 'gemini')
.option('-b, --base <branch>', 'Base branch for PR', 'main')
.option('-r, --remote <name>', 'Git remote name', 'origin')
.option('--draft', 'Create as draft PR')
.action(async (sourcePath, options) => {
try {
console.log(chalk.blue('\n📝 Creating pull request...\n'));
// First, convert if not already done
const result = await porter.convert(
path.resolve(sourcePath),
options.to,
{ validate: true }
);
if (!result.success) {
console.log(chalk.red('✗ Conversion failed\n'));
result.errors.forEach(error => console.log(chalk.red(` - ${error}`)));
process.exit(1);
}
console.log(chalk.green('✓ Conversion completed\n'));
// Generate PR
const prGen = new PRGenerator(path.resolve(sourcePath));
const prResult = await prGen.generate({
targetPlatform: options.to,
remote: options.remote,
baseBranch: options.base,
draft: options.draft
});
if (prResult.success) {
console.log(chalk.green('✓ Pull request created!\n'));
console.log(chalk.bold('PR URL:'));
console.log(chalk.cyan(` ${prResult.prUrl}\n`));
console.log(chalk.gray(`Branch: ${prResult.branch}\n`));
} else {
console.log(chalk.red('✗ Failed to create pull request\n'));
prResult.errors.forEach(error => {
console.log(chalk.red(` - ${error}`));
});
process.exit(1);
}
} catch (error) {
console.error(chalk.red(`\n✗ Error: ${error.message}\n`));
process.exit(1);
}
});
// Fork setup command
program
.command('fork <source-path>')
.description('Create a fork with dual-platform setup')
.option('-l, --location <path>', 'Fork location directory', '.')
.option('-u, --url <url>', 'Repository URL to clone (optional)')
.action(async (sourcePath, options) => {
try {
console.log(chalk.blue('\n🍴 Setting up fork with dual-platform support...\n'));
const forkSetup = new ForkSetup(path.resolve(sourcePath));
const result = await forkSetup.setup({
forkLocation: path.resolve(options.location),
repoUrl: options.url
});
if (result.success) {
console.log(chalk.green('✓ Fork created successfully!\n'));
console.log(chalk.bold('Fork location:'));
console.log(chalk.cyan(` ${result.forkPath}\n`));
console.log(chalk.bold('Installations:'));
console.log(chalk.gray(` Claude Code: ${result.installations.claude || 'N/A'}`));
console.log(chalk.gray(` Gemini CLI: ${result.installations.gemini || 'N/A'}\n`));
console.log(chalk.bold('Next steps:'));
console.log(chalk.gray(' 1. Navigate to fork: cd ' + result.forkPath));
console.log(chalk.gray(' 2. For Gemini: ' + result.installations.gemini));
console.log(chalk.gray(' 3. Test on both platforms\n'));
} else {
console.log(chalk.red('✗ Fork setup failed\n'));
result.errors.forEach(error => {
console.log(chalk.red(` - ${error}`));
});
process.exit(1);
}
} catch (error) {
console.error(chalk.red(`\n✗ Error: ${error.message}\n`));
process.exit(1);
}
});
program.parse();