@usebruno/cli
Version:
With Bruno CLI, you can now run your API collections with ease using simple command line commands.
316 lines (277 loc) • 11.3 kB
JavaScript
const fs = require('fs');
const path = require('path');
const chalk = require('chalk');
const jsyaml = require('js-yaml');
const axios = require('axios');
const { openApiToBruno, wsdlToBruno } = require('@usebruno/converters');
const { exists, isDirectory, sanitizeName } = require('../utils/filesystem');
const { createCollectionFromBrunoObject } = require('../utils/collection');
const command = 'import <type>';
const desc = 'Import a collection from other formats';
const builder = (yargs) => {
yargs
.positional('type', {
describe: 'Type of collection to import',
type: 'string',
choices: ['openapi', 'wsdl']
})
.option('source', {
alias: 's',
describe: 'Path to the source file or URL',
type: 'string',
demandOption: true
})
.option('output', {
alias: 'o',
describe: 'Path to the output directory',
type: 'string',
conflicts: 'output-file'
})
.option('output-file', {
alias: 'f',
describe: 'Path to the output JSON file',
type: 'string',
conflicts: 'output'
})
.option('collection-name', {
alias: 'n',
describe: 'Name for the imported collection',
type: 'string'
})
.option('insecure', {
type: 'boolean',
describe: 'Skip SSL certificate verification when fetching from URLs',
default: false
})
.option('group-by', {
alias: 'g',
describe: 'How to group the imported requests: "tags" groups by OpenAPI tags, "path" groups by URL path structure',
type: 'string',
choices: ['tags', 'path'],
default: 'tags'
})
.example('$0 import openapi --source api.yml --output ~/Desktop/my-collection --collection-name "My API"')
.example('$0 import openapi -s api.yml -o ~/Desktop/my-collection -n "My API"')
.example('$0 import openapi --source https://example.com/api-spec.json --output ~/Desktop --collection-name "Remote API"')
.example('$0 import openapi --source https://self-signed.example.com/api.json --insecure --output ~/Desktop')
.example('$0 import openapi --source api.yml --output-file ~/Desktop/my-collection.json --collection-name "My API"')
.example('$0 import openapi -s api.yml -f ~/Desktop/my-collection.json -n "My API"')
.example('$0 import openapi --source api.yml --output ~/Desktop/my-collection --group-by path')
.example('$0 import openapi -s api.yml -o ~/Desktop/my-collection -g tags')
.example('$0 import wsdl --source service.wsdl --output ~/Desktop/soap-collection --collection-name "SOAP Service"')
.example('$0 import wsdl -s https://example.com/service.wsdl -o ~/Desktop -n "Remote SOAP Service"');
};
const isUrl = (str) => {
try {
return Boolean(new URL(str));
} catch (error) {
return false;
}
};
const readOpenApiFile = async (source, options = {}) => {
try {
let content;
if (isUrl(source)) {
// Handle URL input
console.log(chalk.yellow(`Fetching specification from URL: ${source}`));
try {
const axiosOptions = {
timeout: 30000, // 30 second timeout
maxContentLength: 10 * 1024 * 1024,
validateStatus: status => status >= 200 && status < 300
};
// Skip SSL certificate validation if insecure flag is set
if (options.insecure) {
console.log(chalk.yellow('Warning: SSL certificate verification is disabled. Use with caution.'));
axiosOptions.httpsAgent = new (require('https')).Agent({ rejectUnauthorized: false });
}
const response = await axios.get(source, axiosOptions);
content = response.data;
} catch (error) {
if (error.code === 'ECONNABORTED') {
throw new Error('Request timed out. The server took too long to respond.');
} else if (error.code === 'CERT_HAS_EXPIRED' || error.code === 'DEPTH_ZERO_SELF_SIGNED_CERT' ||
error.code === 'ERR_TLS_CERT_ALTNAME_INVALID') {
throw new Error(`SSL Certificate error: ${error.code}. Try using --insecure if you trust this source.`);
} else if (error.response) {
throw new Error(`Failed to fetch from URL: ${error.response.status} ${error.response.statusText}`);
} else if (error.request) {
throw new Error(`No response received from server. Check the URL and your network connection.`);
} else {
throw new Error(`Error fetching URL: ${error.message}`);
}
}
// If response is already an object, return it directly
if (typeof content === 'object' && content !== null) {
return content;
}
} else {
// Handle file input
if (!await exists(source)) {
throw new Error(`File does not exist: ${source}`);
}
content = fs.readFileSync(source, 'utf8');
}
// If content is a string, try to parse as JSON or YAML
if (typeof content === 'string') {
try {
return JSON.parse(content);
} catch (jsonError) {
try {
return jsyaml.load(content);
} catch (yamlError) {
throw new Error('Failed to parse content as JSON or YAML');
}
}
}
return content;
} catch (error) {
// Let the specific error handling from above propagate
throw error;
}
};
const readWSDLFile = async (source, options = {}) => {
try {
let content;
if (isUrl(source)) {
// Handle URL input
console.log(chalk.yellow(`Fetching WSDL from URL: ${source}`));
try {
const axiosOptions = {
timeout: 30000, // 30 second timeout
maxContentLength: 10 * 1024 * 1024,
validateStatus: (status) => status >= 200 && status < 300
};
// Skip SSL certificate validation if insecure flag is set
if (options.insecure) {
console.log(chalk.yellow('Warning: SSL certificate verification is disabled. Use with caution.'));
axiosOptions.httpsAgent = new (require('https')).Agent({ rejectUnauthorized: false });
}
const response = await axios.get(source, axiosOptions);
content = response.data;
} catch (error) {
if (error.code === 'ECONNABORTED') {
throw new Error('Request timed out. The server took too long to respond.');
} else if (error.code === 'CERT_HAS_EXPIRED' || error.code === 'DEPTH_ZERO_SELF_SIGNED_CERT'
|| error.code === 'ERR_TLS_CERT_ALTNAME_INVALID') {
throw new Error(`SSL Certificate error: ${error.code}. Try using --insecure if you trust this source.`);
} else if (error.response) {
throw new Error(`Failed to fetch from URL: ${error.response.status} ${error.response.statusText}`);
} else if (error.request) {
throw new Error(`No response received from server. Check the URL and your network connection.`);
} else {
throw new Error(`Error fetching URL: ${error.message}`);
}
}
} else {
// Handle file input
if (!await exists(source)) {
throw new Error(`File does not exist: ${source}`);
}
content = fs.readFileSync(source, 'utf8');
}
// WSDL files are XML, so we return the content as a string
if (typeof content === 'string') {
return content;
}
throw new Error('WSDL content must be a string');
} catch (error) {
// Let the specific error handling from above propagate
throw error;
}
};
const handler = async (argv) => {
try {
const { type, source, output, outputFile, collectionName, insecure, groupBy } = argv;
if (!type || !['openapi', 'wsdl'].includes(type)) {
console.error(chalk.red('Only OpenAPI and WSDL imports are supported currently'));
process.exit(1);
}
if (!source) {
console.error(chalk.red('Source file or URL is required'));
process.exit(1);
}
if (!output && !outputFile) {
console.error(chalk.red('Either --output or --output-file is required'));
process.exit(1);
}
let brunoCollection;
if (type === 'openapi') {
console.log(chalk.yellow(`Reading OpenAPI specification from ${source}...`));
const openApiSpec = await readOpenApiFile(source, { insecure });
if (!openApiSpec) {
console.error(chalk.red('Failed to parse OpenAPI specification'));
process.exit(1);
}
console.log(chalk.yellow('Converting OpenAPI specification to Bruno format...'));
// Convert OpenAPI to Bruno format
brunoCollection = openApiToBruno(openApiSpec, { groupBy });
} else if (type === 'wsdl') {
console.log(chalk.yellow(`Reading WSDL from ${source}...`));
const wsdlContent = await readWSDLFile(source, { insecure });
if (!wsdlContent) {
console.error(chalk.red('Failed to read WSDL file'));
process.exit(1);
}
console.log(chalk.yellow('Converting WSDL to Bruno format...'));
// Convert WSDL to Bruno format
brunoCollection = await wsdlToBruno(wsdlContent);
}
// Override collection name if provided
if (collectionName) {
brunoCollection.name = collectionName;
}
if (outputFile) {
// Save as JSON file
const outputPath = path.resolve(outputFile);
fs.writeFileSync(outputPath, JSON.stringify(brunoCollection, null, 2));
console.log(chalk.green(`Bruno collection saved as JSON to ${outputPath}`));
} else if (output) {
const resolvedOutput = path.resolve(output);
// Check if output is an existing directory
const isOutputDirectory = await exists(resolvedOutput) && isDirectory(resolvedOutput);
// Determine the final output directory
let outputDir;
if (isOutputDirectory) {
// If output is an existing directory, use collection name to create a subdirectory
const dirName = sanitizeName(brunoCollection.name);
outputDir = path.join(resolvedOutput, dirName);
// Check if this subfolder already exists
if (await exists(outputDir)) {
const dirContents = fs.readdirSync(outputDir);
if (dirContents.length > 0) {
console.error(chalk.red(`Output directory is not empty: ${outputDir}`));
process.exit(1);
}
} else {
// Create the subfolder
fs.mkdirSync(outputDir, { recursive: true });
}
} else {
// If output doesn't exist or is not a directory, use it directly
outputDir = resolvedOutput;
// Check if parent directory exists
const parentDir = path.dirname(outputDir);
if (!await exists(parentDir)) {
console.error(chalk.red(`Parent directory does not exist: ${parentDir}`));
process.exit(1);
}
fs.mkdirSync(outputDir, { recursive: true });
}
await createCollectionFromBrunoObject(brunoCollection, outputDir);
console.log(chalk.green(`Bruno collection created at ${outputDir}`));
}
} catch (error) {
console.error(chalk.red(`Error: ${error.message}`));
process.exit(1);
}
};
module.exports = {
command,
desc,
builder,
handler,
isUrl,
readOpenApiFile,
readWSDLFile
};